mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-22 23:31:12 +02:00
Centralize chat session state sync with single subscription via atom
This commit is contained in:
parent
4cb835f19f
commit
39d434b00c
4 changed files with 48 additions and 18 deletions
|
|
@ -50,7 +50,8 @@ import {
|
||||||
type MessageRecord,
|
type MessageRecord,
|
||||||
type ThreadRecord,
|
type ThreadRecord,
|
||||||
} from "@/lib/chat/thread-persistence";
|
} 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 { useMessagesElectric } from "@/hooks/use-messages-electric";
|
||||||
import {
|
import {
|
||||||
trackChatCreated,
|
trackChatCreated,
|
||||||
|
|
@ -260,8 +261,11 @@ export default function NewChatPage() {
|
||||||
// Get current user for author info in shared chats
|
// Get current user for author info in shared chats
|
||||||
const { data: currentUser } = useAtomValue(currentUserAtom);
|
const { data: currentUser } = useAtomValue(currentUserAtom);
|
||||||
|
|
||||||
// Live collaboration: sync messages from other users via Electric SQL
|
// Live collaboration: sync session state and messages via Electric SQL
|
||||||
const { isAiResponding, respondingToUserId } = useChatSessionState(threadId);
|
useChatSessionStateSync(threadId);
|
||||||
|
const sessionState = useAtomValue(chatSessionStateAtom);
|
||||||
|
const isAiResponding = sessionState?.isAiResponding ?? false;
|
||||||
|
const respondingToUserId = sessionState?.respondingToUserId ?? null;
|
||||||
const { data: membersData } = useAtomValue(membersAtom);
|
const { data: membersData } = useAtomValue(membersAtom);
|
||||||
|
|
||||||
const handleElectricMessagesUpdate = useCallback(
|
const handleElectricMessagesUpdate = useCallback(
|
||||||
|
|
|
||||||
15
surfsense_web/atoms/chat/chat-session-state.atom.ts
Normal file
15
surfsense_web/atoms/chat/chat-session-state.atom.ts
Normal file
|
|
@ -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<ChatSessionStateData | null>(null);
|
||||||
|
|
@ -61,7 +61,7 @@ import {
|
||||||
import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking";
|
import type { ThinkingStep } from "@/components/tool-ui/deepagent-thinking";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import type { Document } from "@/contracts/types/document.types";
|
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 { useCommentsElectric } from "@/hooks/use-comments-electric";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
|
@ -236,7 +236,9 @@ const Composer: FC = () => {
|
||||||
}
|
}
|
||||||
return typeof chat_id === "string" ? Number.parseInt(chat_id, 10) || null : null;
|
return typeof chat_id === "string" ? Number.parseInt(chat_id, 10) || null : null;
|
||||||
}, [chat_id]);
|
}, [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;
|
const isBlockedByOtherUser = isAiResponding && respondingToUserId !== currentUser?.id;
|
||||||
|
|
||||||
// Sync comments for the entire thread via Electric SQL (one subscription per thread)
|
// Sync comments for the entire thread via Electric SQL (one subscription per thread)
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,39 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useShape } from "@electric-sql/react";
|
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";
|
import type { ChatSessionState } from "@/contracts/types/chat-session-state.types";
|
||||||
|
|
||||||
const ELECTRIC_URL = process.env.NEXT_PUBLIC_ELECTRIC_URL || "http://localhost:5133";
|
const ELECTRIC_URL = process.env.NEXT_PUBLIC_ELECTRIC_URL || "http://localhost:5133";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to get live chat session state for collaboration.
|
* Syncs chat session state for a thread via Electric SQL.
|
||||||
* Tracks which user the AI is currently responding to.
|
* Call once per thread (in page.tsx). Updates global atom.
|
||||||
*/
|
*/
|
||||||
export function useChatSessionState(threadId: number | null) {
|
export function useChatSessionStateSync(threadId: number | null) {
|
||||||
const { data, isLoading, isError, error } = useShape<ChatSessionState>({
|
const setSessionState = useSetAtom(chatSessionStateAtom);
|
||||||
|
|
||||||
|
const { data } = useShape<ChatSessionState>({
|
||||||
url: `${ELECTRIC_URL}/v1/shape`,
|
url: `${ELECTRIC_URL}/v1/shape`,
|
||||||
params: {
|
params: {
|
||||||
table: "chat_session_state",
|
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 {
|
const row = data?.[0];
|
||||||
sessionState,
|
setSessionState({
|
||||||
isAiResponding: !!sessionState?.ai_responding_to_user_id,
|
threadId,
|
||||||
respondingToUserId: sessionState?.ai_responding_to_user_id ?? null,
|
isAiResponding: !!row?.ai_responding_to_user_id,
|
||||||
loading: isLoading,
|
respondingToUserId: row?.ai_responding_to_user_id ?? null,
|
||||||
error: isError ? error : null,
|
});
|
||||||
};
|
}, [threadId, data, setSessionState]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue