fix(llm/sidebar): no longer properly persisting the chat

This commit is contained in:
Elian Doran
2026-03-31 15:52:05 +03:00
parent 4c6aa3baf1
commit f4b9207379
2 changed files with 53 additions and 69 deletions

View File

@@ -104,7 +104,7 @@ export interface SavedData {
export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, onContentChange, dataSaved, updateInterval }: {
noteType: NoteType;
note: FNote,
note: FNote | null | undefined,
noteContext: NoteContext | null | undefined,
getData: () => Promise<SavedData | undefined> | SavedData | undefined,
onContentChange: (newContent: string) => void,
@@ -118,8 +118,8 @@ export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, on
return async () => {
const data = await getData();
// for read only notes
if (data === undefined || note.type !== noteType) return;
// for read only notes, or if note is not yet available (e.g. lazy creation)
if (data === undefined || !note || note.type !== noteType) return;
protected_session_holder.touchProtectedSessionIfNecessary(note);
@@ -138,7 +138,7 @@ export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, on
// React to note/blob changes.
useEffect(() => {
if (!blob) return;
if (!blob || !note) return;
noteSavedDataStore.set(note.noteId, blob.content);
spacedUpdate.allowUpdateWithoutChange(() => onContentChange(blob.content));
}, [ blob ]);

View File

@@ -10,7 +10,7 @@ import { formatDateTime } from "../../utils/formatters";
import ActionButton from "../react/ActionButton.js";
import Dropdown from "../react/Dropdown.js";
import { FormListItem } from "../react/FormList.js";
import { useActiveNoteContext, useNote, useNoteProperty } from "../react/hooks.js";
import { useActiveNoteContext, useEditorSpacedUpdate, useNote, useNoteProperty } from "../react/hooks.js";
import NoItems from "../react/NoItems.js";
import ChatInputBar from "../type_widgets/llm_chat/ChatInputBar.js";
import ChatMessage from "../type_widgets/llm_chat/ChatMessage.js";
@@ -25,9 +25,8 @@ import RightPanelWidget from "./RightPanelWidget.js";
*/
export default function SidebarChat() {
const [chatNoteId, setChatNoteId] = useState<string | null>(null);
const [shouldSave, setShouldSave] = useState(false);
const [recentChats, setRecentChats] = useState<RecentLlmChat[]>([]);
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const spacedUpdateRef = useRef<{ scheduleUpdate: () => void }>(null);
const historyDropdownRef = useRef<BootstrapDropdown | null>(null);
// Get the current active note context
@@ -40,10 +39,40 @@ export default function SidebarChat() {
// Use shared chat hook with sidebar-specific options
const chat = useLlmChat(
// onMessagesChange - trigger save
() => setShouldSave(true),
() => spacedUpdateRef.current?.scheduleUpdate(),
{ defaultEnableNoteTools: true, supportsExtendedThinking: true }
);
// Ref to access chat methods in callbacks without triggering re-runs
const chatRef = useRef(chat);
chatRef.current = chat;
// Persistence via useEditorSpacedUpdate (same mechanism as the LlmChat type widget).
// When chatNote is null (before lazy creation), saves are no-ops.
const spacedUpdate = useEditorSpacedUpdate({
note: chatNote,
noteType: "llmChat",
noteContext: null,
getData: () => {
const content = chatRef.current.getContent();
return { content: JSON.stringify(content) };
},
onContentChange: (content) => {
if (!content) {
chatRef.current.clearMessages();
return;
}
try {
const parsed: LlmChatContent = JSON.parse(content);
chatRef.current.loadFromContent(parsed);
} catch (e) {
console.error("Failed to parse LLM chat content:", e);
chatRef.current.clearMessages();
}
}
});
spacedUpdateRef.current = spacedUpdate;
// Update chat context when active note changes
useEffect(() => {
chat.setContextNoteId(activeNoteId ?? undefined);
@@ -54,42 +83,6 @@ export default function SidebarChat() {
chat.setChatNoteId(chatNoteId ?? undefined);
}, [chatNoteId, chat.setChatNoteId]);
// Ref to access chat methods in effects without triggering re-runs
const chatRef = useRef(chat);
chatRef.current = chat;
// Handle debounced save when shouldSave is triggered
useEffect(() => {
if (!shouldSave || !chatNoteId) {
setShouldSave(false);
return;
}
setShouldSave(false);
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
saveTimeoutRef.current = setTimeout(async () => {
const content = chat.getContent();
try {
await server.put(`notes/${chatNoteId}/data`, {
content: JSON.stringify(content)
});
} catch (err) {
console.error("Failed to save chat:", err);
}
}, 500);
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
saveTimeoutRef.current = undefined;
}
};
}, [shouldSave, chatNoteId, chat]);
// Load the most recent chat on mount (runs once)
useEffect(() => {
let cancelled = false;
@@ -102,16 +95,6 @@ export default function SidebarChat() {
if (existingChat) {
setChatNoteId(existingChat.noteId);
// Load content inline to avoid dependency issues
try {
const blob = await server.get<{ content: string }>(`notes/${existingChat.noteId}/blob`);
if (!cancelled && blob?.content) {
const parsed: LlmChatContent = JSON.parse(blob.content);
chatRef.current.loadFromContent(parsed);
}
} catch (err) {
console.error("Failed to load chat content:", err);
}
} else {
// No existing chat - will create on first message
setChatNoteId(null);
@@ -170,31 +153,38 @@ export default function SidebarChat() {
}, [handleSubmit]);
const handleNewChat = useCallback(async () => {
// Save any pending changes before switching
await spacedUpdate.updateNowIfNecessary();
try {
const note = await dateNoteService.createLlmChat();
if (note) {
setChatNoteId(note.noteId);
chat.clearMessages();
chatRef.current.clearMessages();
}
} catch (err) {
console.error("Failed to create new chat:", err);
}
}, [chat]);
}, [spacedUpdate]);
const handleSaveChat = useCallback(async () => {
if (!chatNoteId) return;
// Save any pending changes before moving the chat
await spacedUpdate.updateNowIfNecessary();
try {
await server.post("special-notes/save-llm-chat", { llmChatNoteId: chatNoteId });
// Create a new empty chat after saving
const note = await dateNoteService.createLlmChat();
if (note) {
setChatNoteId(note.noteId);
chat.clearMessages();
chatRef.current.clearMessages();
}
} catch (err) {
console.error("Failed to save chat to permanent location:", err);
}
}, [chatNoteId, chat]);
}, [chatNoteId, spacedUpdate]);
const loadRecentChats = useCallback(async () => {
try {
@@ -210,17 +200,11 @@ export default function SidebarChat() {
if (noteId === chatNoteId) return;
try {
const blob = await server.get<{ content: string }>(`notes/${noteId}/blob`);
if (blob?.content) {
const parsed: LlmChatContent = JSON.parse(blob.content);
setChatNoteId(noteId);
chat.loadFromContent(parsed);
}
} catch (err) {
console.error("Failed to load selected chat:", err);
}
}, [chatNoteId, chat]);
// Save any pending changes before switching
await spacedUpdate.updateNowIfNecessary();
setChatNoteId(noteId);
}, [chatNoteId, spacedUpdate]);
return (
<RightPanelWidget