mirror of
https://github.com/zadam/trilium.git
synced 2026-03-30 00:30:22 +02:00
feat(llm): identify sidebar chat notes by note ID
This commit is contained in:
@@ -84,8 +84,20 @@ async function createSearchNote(opts = {}) {
|
||||
return await froca.getNote(note.noteId);
|
||||
}
|
||||
|
||||
async function createLlmChat() {
|
||||
const note = await server.post<FNoteRow>("special-notes/llm-chat");
|
||||
async function createLlmChat(sourceNoteId?: string) {
|
||||
const note = await server.post<FNoteRow>("special-notes/llm-chat", { sourceNoteId });
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
return await froca.getNote(note.noteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an existing LLM chat linked to the given note, or creates a new one.
|
||||
* Used by sidebar chat to maintain 1:1 mapping between notes and their chats.
|
||||
*/
|
||||
async function getOrCreateLlmChatForNote(noteId: string) {
|
||||
const note = await server.get<FNoteRow>(`special-notes/llm-chat-for-note/${noteId}`);
|
||||
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
|
||||
@@ -103,5 +115,6 @@ export default {
|
||||
getYearNote,
|
||||
createSqlConsole,
|
||||
createSearchNote,
|
||||
createLlmChat
|
||||
createLlmChat,
|
||||
getOrCreateLlmChatForNote
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ import options from "../../services/options.js";
|
||||
import server from "../../services/server.js";
|
||||
import { randomString } from "../../services/utils.js";
|
||||
import ActionButton from "../react/ActionButton.js";
|
||||
import { useTriliumEvent } from "../react/hooks.js";
|
||||
import { useActiveNoteContext, useTriliumEvent } from "../react/hooks.js";
|
||||
import ChatMessage from "../type_widgets/llm_chat/ChatMessage.js";
|
||||
import RightPanelWidget from "./RightPanelWidget.js";
|
||||
import "./SidebarChat.css";
|
||||
@@ -49,10 +49,12 @@ interface ModelOption extends LlmModelInfo {
|
||||
|
||||
/**
|
||||
* Sidebar chat widget that appears in the right panel.
|
||||
* Uses a hidden LLM chat note for persistence.
|
||||
* Uses a hidden LLM chat note for persistence, with 1:1 mapping to the active note.
|
||||
*/
|
||||
export default function SidebarChat() {
|
||||
const { note: activeNote } = useActiveNoteContext();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [sourceNoteId, setSourceNoteId] = useState<string | null>(null);
|
||||
const [chatNoteId, setChatNoteId] = useState<string | null>(null);
|
||||
const [messages, setMessages] = useState<StoredMessage[]>([]);
|
||||
const [input, setInput] = useState("");
|
||||
@@ -69,29 +71,55 @@ export default function SidebarChat() {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
// Initialize chat for a specific note
|
||||
const initializeChatForNote = useCallback(async (noteId: string) => {
|
||||
try {
|
||||
const note = await dateNoteService.getOrCreateLlmChatForNote(noteId);
|
||||
if (note) {
|
||||
setSourceNoteId(noteId);
|
||||
setChatNoteId(note.noteId);
|
||||
// Load existing messages if any
|
||||
await loadChatContent(note.noteId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to initialize sidebar chat:", err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Track when active note changes and load/create corresponding chat
|
||||
useEffect(() => {
|
||||
if (!visible || !activeNote) return;
|
||||
|
||||
const noteId = activeNote.noteId;
|
||||
// Don't switch chats if we're already on this note's chat
|
||||
if (noteId === sourceNoteId) return;
|
||||
|
||||
// Load or create chat for the new note
|
||||
initializeChatForNote(noteId);
|
||||
}, [activeNote, visible, sourceNoteId, initializeChatForNote]);
|
||||
|
||||
// Listen for toggle event
|
||||
useTriliumEvent("toggleSidebarChat", useCallback(() => {
|
||||
setVisible(v => {
|
||||
const newValue = !v;
|
||||
if (newValue && !chatNoteId) {
|
||||
// Create a new chat when first opened
|
||||
initializeChat();
|
||||
if (newValue && activeNote) {
|
||||
initializeChatForNote(activeNote.noteId);
|
||||
}
|
||||
return newValue;
|
||||
});
|
||||
}, [chatNoteId]));
|
||||
}, [activeNote, initializeChatForNote]));
|
||||
|
||||
// Listen for open event (always opens, creates chat if needed)
|
||||
useTriliumEvent("openSidebarChat", useCallback(() => {
|
||||
setVisible(true);
|
||||
if (!chatNoteId) {
|
||||
initializeChat();
|
||||
if (activeNote) {
|
||||
initializeChatForNote(activeNote.noteId);
|
||||
}
|
||||
// Ensure right pane is visible
|
||||
if (!options.is("rightPaneVisible")) {
|
||||
appContext.triggerEvent("toggleRightPane", {});
|
||||
}
|
||||
}, [chatNoteId]));
|
||||
}, [activeNote, initializeChatForNote]));
|
||||
|
||||
// Fetch available models on mount
|
||||
useEffect(() => {
|
||||
@@ -120,18 +148,6 @@ export default function SidebarChat() {
|
||||
scrollToBottom();
|
||||
}, [messages, streamingContent, streamingThinking, toolActivity, scrollToBottom]);
|
||||
|
||||
const initializeChat = async () => {
|
||||
try {
|
||||
const note = await dateNoteService.createLlmChat();
|
||||
if (note) {
|
||||
setChatNoteId(note.noteId);
|
||||
setMessages([]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to create sidebar chat:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const loadChatContent = async (noteId: string) => {
|
||||
try {
|
||||
const blob = await server.get<{ content: string }>(`notes/${noteId}/blob`);
|
||||
@@ -311,24 +327,31 @@ export default function SidebarChat() {
|
||||
}, [handleSubmit]);
|
||||
|
||||
const handleNewChat = useCallback(async () => {
|
||||
// Save current chat first if it has messages
|
||||
if (chatNoteId && messages.length > 0) {
|
||||
await saveChat(messages);
|
||||
if (!activeNote) return;
|
||||
// Clear messages for a fresh start with the same note
|
||||
setMessages([]);
|
||||
// Save cleared state
|
||||
if (chatNoteId) {
|
||||
await saveChat([]);
|
||||
}
|
||||
// Create a new chat
|
||||
await initializeChat();
|
||||
}, [chatNoteId, messages, saveChat]);
|
||||
}, [activeNote, chatNoteId, saveChat]);
|
||||
|
||||
const handleSaveChat = useCallback(async () => {
|
||||
if (!chatNoteId) return;
|
||||
try {
|
||||
await server.post("special-notes/save-llm-chat", { llmChatNoteId: chatNoteId });
|
||||
// After saving, create a new chat for continued use
|
||||
await initializeChat();
|
||||
// Clear the current chat after saving (note link is preserved)
|
||||
setMessages([]);
|
||||
// Force reload to get a fresh chat for this note
|
||||
if (activeNote) {
|
||||
setSourceNoteId(null);
|
||||
setChatNoteId(null);
|
||||
initializeChatForNote(activeNote.noteId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to save chat to permanent location:", err);
|
||||
}
|
||||
}, [chatNoteId]);
|
||||
}, [chatNoteId, activeNote, initializeChatForNote]);
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
|
||||
@@ -86,8 +86,13 @@ function createSearchNote(req: Request) {
|
||||
return specialNotesService.createSearchNote(searchString, ancestorNoteId);
|
||||
}
|
||||
|
||||
function createLlmChat() {
|
||||
return specialNotesService.createLlmChat();
|
||||
function createLlmChat(req: Request) {
|
||||
const sourceNoteId = req.body.sourceNoteId;
|
||||
return specialNotesService.createLlmChat(sourceNoteId);
|
||||
}
|
||||
|
||||
function getOrCreateLlmChatForNote(req: Request<{ noteId: string }>) {
|
||||
return specialNotesService.getOrCreateLlmChatForNote(req.params.noteId);
|
||||
}
|
||||
|
||||
function saveLlmChat(req: Request) {
|
||||
@@ -128,6 +133,7 @@ export default {
|
||||
createSearchNote,
|
||||
saveSearchNote,
|
||||
createLlmChat,
|
||||
getOrCreateLlmChatForNote,
|
||||
saveLlmChat,
|
||||
createLauncher,
|
||||
resetLauncher,
|
||||
|
||||
@@ -293,6 +293,7 @@ function register(app: express.Application) {
|
||||
apiRoute(PST, "/api/special-notes/search-note", specialNotesRoute.createSearchNote);
|
||||
apiRoute(PST, "/api/special-notes/save-search-note", specialNotesRoute.saveSearchNote);
|
||||
apiRoute(PST, "/api/special-notes/llm-chat", specialNotesRoute.createLlmChat);
|
||||
apiRoute(GET, "/api/special-notes/llm-chat-for-note/:noteId", specialNotesRoute.getOrCreateLlmChatForNote);
|
||||
apiRoute(PST, "/api/special-notes/save-llm-chat", specialNotesRoute.saveLlmChat);
|
||||
apiRoute(PST, "/api/special-notes/launchers/:noteId/reset", specialNotesRoute.resetLauncher);
|
||||
apiRoute(PST, "/api/special-notes/launchers/:parentNoteId/:launcherType", specialNotesRoute.createLauncher);
|
||||
|
||||
@@ -123,10 +123,13 @@ function saveSearchNote(searchNoteId: string) {
|
||||
return result satisfies SaveSearchNoteResponse;
|
||||
}
|
||||
|
||||
function createLlmChat() {
|
||||
function createLlmChat(sourceNoteId?: string) {
|
||||
const sourceNote = sourceNoteId ? becca.getNote(sourceNoteId) : null;
|
||||
const titleSuffix = sourceNote ? sourceNote.title : dateUtils.localNowDateTime();
|
||||
|
||||
const { note } = noteService.createNewNote({
|
||||
parentNoteId: getMonthlyParentNoteId("_llmChat", "llmChat"),
|
||||
title: `${t("special_notes.llm_chat_prefix")} ${dateUtils.localNowDateTime()}`,
|
||||
title: `${t("special_notes.llm_chat_prefix")} ${titleSuffix}`,
|
||||
content: JSON.stringify({
|
||||
version: 1,
|
||||
messages: []
|
||||
@@ -138,9 +141,33 @@ function createLlmChat() {
|
||||
note.setLabel("iconClass", "bx bx-message-square-dots");
|
||||
note.setLabel("keepCurrentHoisting");
|
||||
|
||||
// Link to source note if provided
|
||||
if (sourceNoteId) {
|
||||
note.setRelation("sourceNote", sourceNoteId);
|
||||
}
|
||||
|
||||
return note;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an existing LLM chat linked to the given note, or creates a new one.
|
||||
* Used by sidebar chat to maintain 1:1 mapping between notes and their chats.
|
||||
*/
|
||||
function getOrCreateLlmChatForNote(sourceNoteId: string) {
|
||||
// Search for existing chat with this source note relation
|
||||
const existingChat = searchService.findFirstNoteWithQuery(
|
||||
`~sourceNote.noteId="${sourceNoteId}"`,
|
||||
new SearchContext({ ancestorNoteId: "_llmChat" })
|
||||
);
|
||||
|
||||
if (existingChat) {
|
||||
return existingChat;
|
||||
}
|
||||
|
||||
// Create new chat linked to this note
|
||||
return createLlmChat(sourceNoteId);
|
||||
}
|
||||
|
||||
function getLlmChatHome() {
|
||||
const workspaceNote = hoistedNoteService.getWorkspaceNote();
|
||||
if (!workspaceNote) {
|
||||
@@ -335,6 +362,7 @@ export default {
|
||||
createSearchNote,
|
||||
saveSearchNote,
|
||||
createLlmChat,
|
||||
getOrCreateLlmChatForNote,
|
||||
saveLlmChat,
|
||||
createLauncher,
|
||||
resetLauncher,
|
||||
|
||||
Reference in New Issue
Block a user