diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx index cb6e797bd..e76b83a97 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx @@ -50,7 +50,8 @@ import { type MessageRecord, type ThreadRecord, } from "@/lib/chat/thread-persistence"; -import { useChatSessionState } from "@/hooks/use-chat-session-state"; +import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom"; +import { useChatSessionStateSync } from "@/hooks/use-chat-session-state"; import { useMessagesElectric } from "@/hooks/use-messages-electric"; import { trackChatCreated, @@ -260,8 +261,11 @@ export default function NewChatPage() { // Get current user for author info in shared chats const { data: currentUser } = useAtomValue(currentUserAtom); - // Live collaboration: sync messages from other users via Electric SQL - const { isAiResponding, respondingToUserId } = useChatSessionState(threadId); + // Live collaboration: sync session state and messages via Electric SQL + useChatSessionStateSync(threadId); + const sessionState = useAtomValue(chatSessionStateAtom); + const isAiResponding = sessionState?.isAiResponding ?? false; + const respondingToUserId = sessionState?.respondingToUserId ?? null; const { data: membersData } = useAtomValue(membersAtom); const handleElectricMessagesUpdate = useCallback( diff --git a/surfsense_web/atoms/chat/chat-session-state.atom.ts b/surfsense_web/atoms/chat/chat-session-state.atom.ts new file mode 100644 index 000000000..4d83a45d4 --- /dev/null +++ b/surfsense_web/atoms/chat/chat-session-state.atom.ts @@ -0,0 +1,15 @@ +"use client"; + +import { atom } from "jotai"; + +export interface ChatSessionStateData { + threadId: number; + isAiResponding: boolean; + respondingToUserId: string | null; +} + +/** + * Global chat session state atom. + * Updated by useChatSessionStateSync hook, read anywhere. + */ +export const chatSessionStateAtom = atom(null); diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index e1169867c..e419258f2 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -61,7 +61,7 @@ import { import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking"; import { Button } from "@/components/ui/button"; import type { Document } from "@/contracts/types/document.types"; -import { useChatSessionState } from "@/hooks/use-chat-session-state"; +import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom"; import { useCommentsElectric } from "@/hooks/use-comments-electric"; import { cn } from "@/lib/utils"; @@ -236,7 +236,9 @@ const Composer: FC = () => { } return typeof chat_id === "string" ? Number.parseInt(chat_id, 10) || null : null; }, [chat_id]); - const { isAiResponding, respondingToUserId } = useChatSessionState(threadId); + const sessionState = useAtomValue(chatSessionStateAtom); + const isAiResponding = sessionState?.isAiResponding ?? false; + const respondingToUserId = sessionState?.respondingToUserId ?? null; const isBlockedByOtherUser = isAiResponding && respondingToUserId !== currentUser?.id; // Sync comments for the entire thread via Electric SQL (one subscription per thread) diff --git a/surfsense_web/hooks/use-chat-session-state.ts b/surfsense_web/hooks/use-chat-session-state.ts index fb263826f..f3bdd7722 100644 --- a/surfsense_web/hooks/use-chat-session-state.ts +++ b/surfsense_web/hooks/use-chat-session-state.ts @@ -1,30 +1,39 @@ "use client"; import { useShape } from "@electric-sql/react"; +import { useSetAtom } from "jotai"; +import { useEffect } from "react"; +import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom"; import type { ChatSessionState } from "@/contracts/types/chat-session-state.types"; const ELECTRIC_URL = process.env.NEXT_PUBLIC_ELECTRIC_URL || "http://localhost:5133"; /** - * Hook to get live chat session state for collaboration. - * Tracks which user the AI is currently responding to. + * Syncs chat session state for a thread via Electric SQL. + * Call once per thread (in page.tsx). Updates global atom. */ -export function useChatSessionState(threadId: number | null) { - const { data, isLoading, isError, error } = useShape({ +export function useChatSessionStateSync(threadId: number | null) { + const setSessionState = useSetAtom(chatSessionStateAtom); + + const { data } = useShape({ url: `${ELECTRIC_URL}/v1/shape`, params: { table: "chat_session_state", - where: `thread_id = ${threadId}`, + where: `thread_id = ${threadId ?? -1}`, }, }); - const sessionState = data?.[0] ?? null; + useEffect(() => { + if (!threadId) { + setSessionState(null); + return; + } - return { - sessionState, - isAiResponding: !!sessionState?.ai_responding_to_user_id, - respondingToUserId: sessionState?.ai_responding_to_user_id ?? null, - loading: isLoading, - error: isError ? error : null, - }; + const row = data?.[0]; + setSessionState({ + threadId, + isAiResponding: !!row?.ai_responding_to_user_id, + respondingToUserId: row?.ai_responding_to_user_id ?? null, + }); + }, [threadId, data, setSessionState]); }