Simplify live message sync using Electric as source of truth

This commit is contained in:
CREDO23 2026-01-22 20:40:49 +02:00
parent 39d434b00c
commit b1b63c6740

View file

@ -50,7 +50,6 @@ import {
type MessageRecord, type MessageRecord,
type ThreadRecord, type ThreadRecord,
} from "@/lib/chat/thread-persistence"; } from "@/lib/chat/thread-persistence";
import { chatSessionStateAtom } from "@/atoms/chat/chat-session-state.atom";
import { useChatSessionStateSync } from "@/hooks/use-chat-session-state"; import { useChatSessionStateSync } from "@/hooks/use-chat-session-state";
import { useMessagesElectric } from "@/hooks/use-messages-electric"; import { useMessagesElectric } from "@/hooks/use-messages-electric";
import { import {
@ -263,56 +262,38 @@ export default function NewChatPage() {
// Live collaboration: sync session state and messages via Electric SQL // Live collaboration: sync session state and messages via Electric SQL
useChatSessionStateSync(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(
(electricMessages: { id: number; thread_id: number; role: string; content: unknown; author_id: string | null; created_at: string }[]) => { (electricMessages: { id: number; thread_id: number; role: string; content: unknown; author_id: string | null; created_at: string }[]) => {
// Skip sync while AI is responding to us or during local streaming if (isRunning) {
if ((isAiResponding && respondingToUserId === currentUser?.id) || isRunning) {
return; return;
} }
setMessages((prev) => { setMessages((prev) => {
const existingIds = new Set( if (electricMessages.length < prev.length) {
prev.map((m) => { return prev;
const match = String(m.id).match(/^msg-(\d+)$/); }
return match ? Number.parseInt(match[1], 10) : null;
}).filter((id): id is number => id !== null)
);
const newConverted: ReturnType<typeof convertToThreadMessage>[] = [];
for (const msg of electricMessages) {
if (existingIds.has(msg.id)) continue;
return electricMessages.map((msg) => {
const member = msg.author_id const member = msg.author_id
? membersData?.find((m) => m.user_id === msg.author_id) ? membersData?.find((m) => m.user_id === msg.author_id)
: null; : null;
newConverted.push( return convertToThreadMessage({
convertToThreadMessage({ id: msg.id,
id: msg.id, thread_id: msg.thread_id,
thread_id: msg.thread_id, role: msg.role.toLowerCase() as "user" | "assistant" | "system",
role: msg.role.toLowerCase() as "user" | "assistant" | "system", content: msg.content,
content: msg.content, author_id: msg.author_id,
author_id: msg.author_id, created_at: msg.created_at,
created_at: msg.created_at, author_display_name: member?.user_display_name ?? null,
author_display_name: member?.user_display_name ?? null, author_avatar_url: member?.user_avatar_url ?? null,
author_avatar_url: member?.user_avatar_url ?? null, });
}) });
);
}
if (newConverted.length === 0) {
return prev;
}
return [...prev, ...newConverted];
}); });
}, },
[isAiResponding, respondingToUserId, currentUser?.id, isRunning, membersData] [isRunning, membersData]
); );
useMessagesElectric(threadId, handleElectricMessagesUpdate); useMessagesElectric(threadId, handleElectricMessagesUpdate);
@ -646,8 +627,6 @@ export default function NewChatPage() {
content: persistContent, content: persistContent,
}) })
.then(() => { .then(() => {
// For new threads, the backend updates the title from the first user message
// Invalidate threads query so sidebar shows the updated title in real-time
if (isNewThread) { if (isNewThread) {
queryClient.invalidateQueries({ queryKey: ["threads", String(searchSpaceId)] }); queryClient.invalidateQueries({ queryKey: ["threads", String(searchSpaceId)] });
} }