mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-08 20:25:19 +02:00
update chat api service
This commit is contained in:
parent
68e4d9b23e
commit
db58571751
10 changed files with 79 additions and 191 deletions
|
|
@ -15,7 +15,7 @@ import { AnimatePresence, motion, type Variants } from "motion/react";
|
|||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { deleteChatMutationAtom } from "@/atoms/chats/chat-mutation.atoms";
|
||||
import { activeSearchSpaceChatsAtom } from "@/atoms/chats/chat-querie.atoms";
|
||||
import { chatsAtom } from "@/atoms/chats/chat-querie.atoms";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
|
@ -103,11 +103,7 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps)
|
|||
id: number;
|
||||
title: string;
|
||||
} | null>(null);
|
||||
const {
|
||||
isFetching: isFetchingChats,
|
||||
data: chats,
|
||||
error: fetchError,
|
||||
} = useAtomValue(activeSearchSpaceChatsAtom);
|
||||
const { isFetching: isFetchingChats, data: chats, error: fetchError } = useAtomValue(chatsAtom);
|
||||
const [{ isPending: isDeletingChat, mutateAsync: deleteChat, error: deleteError }] =
|
||||
useAtom(deleteChatMutationAtom);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,13 +6,15 @@ import { chatsApiService } from "@/lib/apis/chats-api.service";
|
|||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { queryClient } from "@/lib/query-client/client";
|
||||
import { activeSearchSpaceIdAtom } from "../seach-spaces/seach-space-queries.atom";
|
||||
import { globalChatsQueryParamsAtom } from "./ui.atoms";
|
||||
|
||||
export const deleteChatMutationAtom = atomWithMutation((get) => {
|
||||
const searchSpaceId = get(activeSearchSpaceIdAtom);
|
||||
const authToken = localStorage.getItem("surfsense_bearer_token");
|
||||
const chatsQueryParams = get(globalChatsQueryParamsAtom);
|
||||
|
||||
return {
|
||||
mutationKey: cacheKeys.activeSearchSpace.chats(searchSpaceId ?? ""),
|
||||
mutationKey: cacheKeys.chats.globalQueryParams(chatsQueryParams),
|
||||
enabled: !!searchSpaceId && !!authToken,
|
||||
mutationFn: async (request: DeleteChatRequest) => {
|
||||
return chatsApiService.deleteChat(request);
|
||||
|
|
@ -21,7 +23,7 @@ export const deleteChatMutationAtom = atomWithMutation((get) => {
|
|||
onSuccess: (_, request: DeleteChatRequest) => {
|
||||
toast.success("Chat deleted successfully");
|
||||
queryClient.setQueryData(
|
||||
cacheKeys.activeSearchSpace.chats(searchSpaceId!),
|
||||
cacheKeys.chats.globalQueryParams(chatsQueryParams),
|
||||
(oldData: Chat[]) => {
|
||||
return oldData.filter((chat) => chat.id !== request.id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ import { activeSearchSpaceIdAtom } from "@/atoms/seach-spaces/seach-space-querie
|
|||
import { chatsApiService } from "@/lib/apis/chats-api.service";
|
||||
import { podcastsApiService } from "@/lib/apis/podcasts-api.service";
|
||||
import { cacheKeys } from "@/lib/query-client/cache-keys";
|
||||
import { activeChatIdAtom } from "./ui.atoms";
|
||||
import { activeChatIdAtom, globalChatsQueryParamsAtom } from "./ui.atoms";
|
||||
|
||||
export const activeChatAtom = atomWithQuery((get) => {
|
||||
const activeChatId = get(activeChatIdAtom);
|
||||
const authToken = localStorage.getItem("surfsense_bearer_token");
|
||||
|
||||
return {
|
||||
queryKey: cacheKeys.activeSearchSpace.activeChat(activeChatId ?? ""),
|
||||
queryKey: cacheKeys.chats.activeChat(activeChatId ?? ""),
|
||||
enabled: !!activeChatId && !!authToken,
|
||||
queryFn: async () => {
|
||||
if (!authToken) {
|
||||
|
|
@ -30,16 +30,17 @@ export const activeChatAtom = atomWithQuery((get) => {
|
|||
};
|
||||
});
|
||||
|
||||
export const activeSearchSpaceChatsAtom = atomWithQuery((get) => {
|
||||
export const chatsAtom = atomWithQuery((get) => {
|
||||
const searchSpaceId = get(activeSearchSpaceIdAtom);
|
||||
const authToken = localStorage.getItem("surfsense_bearer_token");
|
||||
const queryParams = get(globalChatsQueryParamsAtom);
|
||||
|
||||
return {
|
||||
queryKey: cacheKeys.activeSearchSpace.chats(searchSpaceId ?? ""),
|
||||
queryKey: cacheKeys.chats.globalQueryParams(queryParams),
|
||||
enabled: !!searchSpaceId && !!authToken,
|
||||
queryFn: async () => {
|
||||
return chatsApiService.getChatsBySearchSpace({
|
||||
queryParams: { search_space_id: searchSpaceId! },
|
||||
return chatsApiService.getChats({
|
||||
queryParams: queryParams,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { atom } from "jotai";
|
||||
import type { GetChatsRequest } from "@/contracts/types/chat.types";
|
||||
|
||||
type ActiveChathatUIState = {
|
||||
isChatPannelOpen: boolean;
|
||||
|
|
@ -7,4 +8,10 @@ type ActiveChathatUIState = {
|
|||
export const activeChathatUIAtom = atom<ActiveChathatUIState>({
|
||||
isChatPannelOpen: false,
|
||||
});
|
||||
|
||||
export const activeChatIdAtom = atom<string | null>(null);
|
||||
|
||||
export const globalChatsQueryParamsAtom = atom<GetChatsRequest["queryParams"]>({
|
||||
limit: 5,
|
||||
skip: 0,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { deleteChatMutationAtom } from "@/atoms/chats/chat-mutation.atoms";
|
||||
import { chatsAtom } from "@/atoms/chats/chat-querie.atoms";
|
||||
import { globalChatsQueryParamsAtom } from "@/atoms/chats/ui.atoms";
|
||||
import { AppSidebar } from "@/components/sidebar/app-sidebar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -13,7 +17,7 @@ import {
|
|||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useChats, useSearchSpace, useUser } from "@/hooks";
|
||||
import { useSearchSpace, useUser } from "@/hooks";
|
||||
|
||||
interface AppSidebarProviderProps {
|
||||
searchSpaceId: string;
|
||||
|
|
@ -41,15 +45,14 @@ export function AppSidebarProvider({
|
|||
}: AppSidebarProviderProps) {
|
||||
const t = useTranslations("dashboard");
|
||||
const tCommon = useTranslations("common");
|
||||
const setChatsQueryParams = useSetAtom(globalChatsQueryParamsAtom);
|
||||
const { data: chats, error: chatError, isLoading: isLoadingChats } = useAtomValue(chatsAtom);
|
||||
const [{ isPending: isDeletingChat, mutateAsync: deleteChat, error: deleteError }] =
|
||||
useAtom(deleteChatMutationAtom);
|
||||
|
||||
// Use the new hooks
|
||||
const {
|
||||
chats,
|
||||
loading: isLoadingChats,
|
||||
error: chatError,
|
||||
fetchChats: fetchRecentChats,
|
||||
deleteChat,
|
||||
} = useChats({ searchSpaceId, limit: 5, skip: 0 });
|
||||
useEffect(() => {
|
||||
setChatsQueryParams((prev) => ({ ...prev, search_space_id: searchSpaceId, skip: 0, limit: 2 }));
|
||||
}, [searchSpaceId]);
|
||||
|
||||
const {
|
||||
searchSpace,
|
||||
|
|
@ -62,7 +65,6 @@ export function AppSidebarProvider({
|
|||
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [chatToDelete, setChatToDelete] = useState<{ id: number; name: string } | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
// Set isClient to true when component mounts on the client
|
||||
|
|
@ -72,29 +74,30 @@ export function AppSidebarProvider({
|
|||
|
||||
// Retry function
|
||||
const retryFetch = useCallback(() => {
|
||||
fetchRecentChats();
|
||||
fetchSearchSpace();
|
||||
}, [fetchRecentChats, fetchSearchSpace]);
|
||||
}, [fetchSearchSpace]);
|
||||
|
||||
// Transform API response to the format expected by AppSidebar
|
||||
const recentChats = useMemo(() => {
|
||||
return chats.map((chat) => ({
|
||||
name: chat.title || `Chat ${chat.id}`,
|
||||
url: `/dashboard/${chat.search_space_id}/researcher/${chat.id}`,
|
||||
icon: "MessageCircleMore",
|
||||
id: chat.id,
|
||||
search_space_id: chat.search_space_id,
|
||||
actions: [
|
||||
{
|
||||
name: "Delete",
|
||||
icon: "Trash2",
|
||||
onClick: () => {
|
||||
setChatToDelete({ id: chat.id, name: chat.title || `Chat ${chat.id}` });
|
||||
setShowDeleteDialog(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
return chats
|
||||
? chats.map((chat) => ({
|
||||
name: chat.title || `Chat ${chat.id}`,
|
||||
url: `/dashboard/${chat.search_space_id}/researcher/${chat.id}`,
|
||||
icon: "MessageCircleMore",
|
||||
id: chat.id,
|
||||
search_space_id: chat.search_space_id,
|
||||
actions: [
|
||||
{
|
||||
name: "Delete",
|
||||
icon: "Trash2",
|
||||
onClick: () => {
|
||||
setChatToDelete({ id: chat.id, name: chat.title || `Chat ${chat.id}` });
|
||||
setShowDeleteDialog(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
}))
|
||||
: [];
|
||||
}, [chats]);
|
||||
|
||||
// Handle delete chat with better error handling
|
||||
|
|
@ -102,13 +105,11 @@ export function AppSidebarProvider({
|
|||
if (!chatToDelete) return;
|
||||
|
||||
try {
|
||||
setIsDeleting(true);
|
||||
await deleteChat(chatToDelete.id);
|
||||
await deleteChat({ id: chatToDelete.id });
|
||||
} catch (error) {
|
||||
console.error("Error deleting chat:", error);
|
||||
// You could show a toast notification here
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setShowDeleteDialog(false);
|
||||
setChatToDelete(null);
|
||||
}
|
||||
|
|
@ -226,17 +227,17 @@ export function AppSidebarProvider({
|
|||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowDeleteDialog(false)}
|
||||
disabled={isDeleting}
|
||||
disabled={isDeletingChat}
|
||||
>
|
||||
{tCommon("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDeleteChat}
|
||||
disabled={isDeleting}
|
||||
disabled={isDeletingChat}
|
||||
className="gap-2"
|
||||
>
|
||||
{isDeleting ? (
|
||||
{isDeletingChat ? (
|
||||
<>
|
||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
{t("deleting")}
|
||||
|
|
|
|||
|
|
@ -20,14 +20,11 @@ export const chatDetails = chatSummary.extend({
|
|||
|
||||
export const getChatDetailsRequest = chatSummary.pick({ id: true });
|
||||
|
||||
export const getChatsBySearchSpaceRequest = z.object({
|
||||
export const getChatsRequest = z.object({
|
||||
queryParams: paginationQueryParams
|
||||
.extend({
|
||||
search_space_id: z.number().or(z.string()),
|
||||
search_space_id: z.number().or(z.string()).optional(),
|
||||
})
|
||||
.transform((entries) =>
|
||||
Object.fromEntries(Object.entries(entries).map(([k, v]) => [k, v.toString()]))
|
||||
)
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
|
|
@ -51,7 +48,7 @@ export const updateChatRequest = chatDetails.omit({
|
|||
export type ChatSummary = z.infer<typeof chatSummary>;
|
||||
export type ChatDetails = z.infer<typeof chatDetails> & { messages: Message[] };
|
||||
export type GetChatDetailsRequest = z.infer<typeof getChatDetailsRequest>;
|
||||
export type GetChatsBySearchSpaceRequest = z.infer<typeof getChatsBySearchSpaceRequest>;
|
||||
export type GetChatsRequest = z.infer<typeof getChatsRequest>;
|
||||
export type DeleteChatResponse = z.infer<typeof deleteChatResponse>;
|
||||
export type DeleteChatRequest = z.infer<typeof deleteChatRequest>;
|
||||
export type CreateChatRequest = z.infer<typeof createChatRequest>;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
export * from "./use-chats";
|
||||
export * from "./use-document-by-chunk";
|
||||
export * from "./use-logs";
|
||||
export * from "./use-search-source-connectors";
|
||||
|
|
|
|||
|
|
@ -1,124 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Chat {
|
||||
created_at: string;
|
||||
id: number;
|
||||
type: string;
|
||||
title: string;
|
||||
messages: string[];
|
||||
search_space_id: number;
|
||||
}
|
||||
|
||||
interface UseChatsOptions {
|
||||
searchSpaceId: string | number;
|
||||
limit?: number;
|
||||
skip?: number;
|
||||
autoFetch?: boolean;
|
||||
}
|
||||
|
||||
export function useChats({
|
||||
searchSpaceId,
|
||||
limit = 5,
|
||||
skip = 0,
|
||||
autoFetch = true,
|
||||
}: UseChatsOptions) {
|
||||
const [chats, setChats] = useState<Chat[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchChats = useCallback(async () => {
|
||||
try {
|
||||
// Only run on client-side
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
setLoading(true);
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats?limit=${limit}&skip=${skip}&search_space_id=${searchSpaceId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
||||
},
|
||||
method: "GET",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 401) {
|
||||
// Clear token and redirect to home
|
||||
localStorage.removeItem("surfsense_bearer_token");
|
||||
window.location.href = "/";
|
||||
throw new Error("Unauthorized: Redirecting to login page");
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch chats: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Sort chats by created_at in descending order (newest first)
|
||||
const sortedChats = data.sort(
|
||||
(a: Chat, b: Chat) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
|
||||
);
|
||||
|
||||
setChats(sortedChats);
|
||||
setError(null);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Failed to fetch chats");
|
||||
console.error("Error fetching chats:", err);
|
||||
setChats([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [searchSpaceId, limit, skip]);
|
||||
|
||||
const deleteChat = useCallback(async (chatId: number) => {
|
||||
try {
|
||||
// Only run on client-side
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/chats/${chatId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("surfsense_bearer_token")}`,
|
||||
},
|
||||
method: "DELETE",
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 401) {
|
||||
// Clear token and redirect to home
|
||||
localStorage.removeItem("surfsense_bearer_token");
|
||||
window.location.href = "/";
|
||||
throw new Error("Unauthorized: Redirecting to login page");
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete chat: ${response.status}`);
|
||||
}
|
||||
|
||||
// Update local state to remove the deleted chat
|
||||
setChats((prev) => prev.filter((chat) => chat.id !== chatId));
|
||||
} catch (err: any) {
|
||||
console.error("Error deleting chat:", err);
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFetch) {
|
||||
fetchChats();
|
||||
|
||||
// Set up a refresh interval (every 5 minutes)
|
||||
const intervalId = setInterval(fetchChats, 5 * 60 * 1000);
|
||||
|
||||
// Clean up interval on component unmount
|
||||
return () => clearInterval(intervalId);
|
||||
}
|
||||
}, [autoFetch, fetchChats]);
|
||||
|
||||
return { chats, loading, error, fetchChats, deleteChat };
|
||||
}
|
||||
|
|
@ -8,9 +8,9 @@ import {
|
|||
deleteChatRequest,
|
||||
deleteChatResponse,
|
||||
type GetChatDetailsRequest,
|
||||
type GetChatsBySearchSpaceRequest,
|
||||
type GetChatsRequest,
|
||||
getChatDetailsRequest,
|
||||
getChatsBySearchSpaceRequest,
|
||||
getChatsRequest,
|
||||
type UpdateChatRequest,
|
||||
updateChatRequest,
|
||||
} from "@/contracts/types/chat.types";
|
||||
|
|
@ -33,9 +33,9 @@ class ChatApiService {
|
|||
return baseApiService.get(`/api/v1/chats/${request.id}`, chatDetails);
|
||||
};
|
||||
|
||||
getChatsBySearchSpace = async (request: GetChatsBySearchSpaceRequest) => {
|
||||
getChats = async (request: GetChatsRequest) => {
|
||||
// Validate the request
|
||||
const parsedRequest = getChatsBySearchSpaceRequest.safeParse(request);
|
||||
const parsedRequest = getChatsRequest.safeParse(request);
|
||||
|
||||
if (!parsedRequest.success) {
|
||||
console.error("Invalid request:", parsedRequest.error);
|
||||
|
|
@ -45,8 +45,15 @@ class ChatApiService {
|
|||
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||
}
|
||||
|
||||
const queryParams = parsedRequest.data.queryParams
|
||||
? new URLSearchParams(parsedRequest.data.queryParams).toString()
|
||||
// Transform queries params to be string values
|
||||
const transformedQueryParams = parsedRequest.data.queryParams
|
||||
? Object.fromEntries(
|
||||
Object.entries(parsedRequest.data.queryParams).map(([k, v]) => [k, String(v)])
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const queryParams = transformedQueryParams
|
||||
? new URLSearchParams(transformedQueryParams).toString()
|
||||
: undefined;
|
||||
|
||||
return baseApiService.get(`/api/v1/chats?${queryParams}`, z.array(chatSummary));
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import type { GetChatsRequest } from "@/contracts/types/chat.types";
|
||||
|
||||
export const cacheKeys = {
|
||||
activeSearchSpace: {
|
||||
chats: (searchSpaceId: string) => ["active-search-space", "chats", searchSpaceId] as const,
|
||||
activeChat: (chatId: string) => ["active-search-space", "active-chat", chatId] as const,
|
||||
podcasts: (searchSpaceId: string) =>
|
||||
["active-search-space", "podcasts", searchSpaceId] as const,
|
||||
chats: {
|
||||
activeChat: (chatId: string) => ["active-chat", chatId] as const,
|
||||
globalQueryParams: (queries: GetChatsRequest["queryParams"]) =>
|
||||
["chats", ...(queries ? Object.values(queries) : [])] as const,
|
||||
},
|
||||
podcasts: () => ["podcasts"] as const,
|
||||
auth: {
|
||||
user: ["auth", "user"] as const,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue