diff --git a/apps/client/src/widgets/llm_chat/communication.ts b/apps/client/src/widgets/llm_chat/communication.ts index bb58a47b2..87687cab9 100644 --- a/apps/client/src/widgets/llm_chat/communication.ts +++ b/apps/client/src/widgets/llm_chat/communication.ts @@ -6,8 +6,10 @@ import type { SessionResponse } from "./types.js"; /** * Create a new chat session + * @param currentNoteId - Optional current note ID for context + * @returns The noteId of the created chat note */ -export async function createChatSession(currentNoteId?: string): Promise<{chatNoteId: string | null, noteId: string | null}> { +export async function createChatSession(currentNoteId?: string): Promise { try { const resp = await server.post('llm/chat', { title: 'Note Chat', @@ -15,48 +17,42 @@ export async function createChatSession(currentNoteId?: string): Promise<{chatNo }); if (resp && resp.id) { - // The backend might provide the noteId separately from the chatNoteId - // If noteId is provided, use it; otherwise, we'll need to query for it separately - return { - chatNoteId: resp.id, - noteId: resp.noteId || null - }; + // Backend returns the chat note ID as 'id' + return resp.id; } } catch (error) { console.error('Failed to create chat session:', error); } - return { - chatNoteId: null, - noteId: null - }; + return null; } /** - * Check if a session exists + * Check if a chat note exists + * @param noteId - The ID of the chat note */ -export async function checkSessionExists(chatNoteId: string): Promise { +export async function checkSessionExists(noteId: string): Promise { try { - // Validate that we have a proper note ID format, not a session ID - // Note IDs in Trilium are typically longer or in a different format - if (chatNoteId && chatNoteId.length === 16 && /^[A-Za-z0-9]+$/.test(chatNoteId)) { - console.warn(`Invalid note ID format detected: ${chatNoteId} appears to be a legacy session ID`); - return false; - } - - const sessionCheck = await server.getWithSilentNotFound(`llm/chat/${chatNoteId}`); + const sessionCheck = await server.getWithSilentNotFound(`llm/chat/${noteId}`); return !!(sessionCheck && sessionCheck.id); } catch (error: any) { - console.log(`Error checking chat note ${chatNoteId}:`, error); + console.log(`Error checking chat note ${noteId}:`, error); return false; } } /** * Set up streaming response via WebSocket + * @param noteId - The ID of the chat note + * @param messageParams - Message parameters + * @param onContentUpdate - Callback for content updates + * @param onThinkingUpdate - Callback for thinking updates + * @param onToolExecution - Callback for tool execution + * @param onComplete - Callback for completion + * @param onError - Callback for errors */ export async function setupStreamingResponse( - chatNoteId: string, + noteId: string, messageParams: any, onContentUpdate: (content: string, isDone?: boolean) => void, onThinkingUpdate: (thinking: string) => void, @@ -64,13 +60,6 @@ export async function setupStreamingResponse( onComplete: () => void, onError: (error: Error) => void ): Promise { - // Validate that we have a proper note ID format, not a session ID - if (chatNoteId && chatNoteId.length === 16 && /^[A-Za-z0-9]+$/.test(chatNoteId)) { - console.error(`Invalid note ID format: ${chatNoteId} appears to be a legacy session ID`); - onError(new Error("Invalid note ID format - using a legacy session ID")); - return; - } - return new Promise((resolve, reject) => { let assistantResponse = ''; let postToolResponse = ''; // Separate accumulator for post-tool execution content @@ -87,12 +76,12 @@ export async function setupStreamingResponse( // Create a unique identifier for this response process const responseId = `llm-stream-${Date.now()}-${Math.floor(Math.random() * 1000)}`; - console.log(`[${responseId}] Setting up WebSocket streaming for chat note ${chatNoteId}`); + console.log(`[${responseId}] Setting up WebSocket streaming for chat note ${noteId}`); // Send the initial request to initiate streaming (async () => { try { - const streamResponse = await server.post(`llm/chat/${chatNoteId}/messages/stream`, { + const streamResponse = await server.post(`llm/chat/${noteId}/messages/stream`, { content: messageParams.content, useAdvancedContext: messageParams.useAdvancedContext, showThinking: messageParams.showThinking, @@ -158,7 +147,7 @@ export async function setupStreamingResponse( const message = customEvent.detail; // Only process messages for our chat note - if (!message || message.chatNoteId !== chatNoteId) { + if (!message || message.chatNoteId !== noteId) { return; } @@ -172,12 +161,12 @@ export async function setupStreamingResponse( cleanupTimeoutId = null; } - console.log(`[${responseId}] LLM Stream message received via CustomEvent: chatNoteId=${chatNoteId}, content=${!!message.content}, contentLength=${message.content?.length || 0}, thinking=${!!message.thinking}, toolExecution=${!!message.toolExecution}, done=${!!message.done}, type=${message.type || 'llm-stream'}`); + console.log(`[${responseId}] LLM Stream message received via CustomEvent: chatNoteId=${noteId}, content=${!!message.content}, contentLength=${message.content?.length || 0}, thinking=${!!message.thinking}, toolExecution=${!!message.toolExecution}, done=${!!message.done}, type=${message.type || 'llm-stream'}`); // Mark first message received if (!receivedAnyMessage) { receivedAnyMessage = true; - console.log(`[${responseId}] First message received for chat note ${chatNoteId}`); + console.log(`[${responseId}] First message received for chat note ${noteId}`); // Clear the initial timeout since we've received a message if (initialTimeoutId !== null) { @@ -298,7 +287,7 @@ export async function setupStreamingResponse( // Set new timeout timeoutId = window.setTimeout(() => { - console.warn(`[${responseId}] Stream timeout for chat note ${chatNoteId}`); + console.warn(`[${responseId}] Stream timeout for chat note ${noteId}`); // Clean up performCleanup(); @@ -369,7 +358,7 @@ export async function setupStreamingResponse( // Handle completion if (message.done) { - console.log(`[${responseId}] Stream completed for chat note ${chatNoteId}, has content: ${!!message.content}, content length: ${message.content?.length || 0}, current response: ${assistantResponse.length} chars`); + console.log(`[${responseId}] Stream completed for chat note ${noteId}, has content: ${!!message.content}, content length: ${message.content?.length || 0}, current response: ${assistantResponse.length} chars`); // Dump message content to console for debugging if (message.content) { @@ -428,9 +417,9 @@ export async function setupStreamingResponse( // Set initial timeout for receiving any message initialTimeoutId = window.setTimeout(() => { - console.warn(`[${responseId}] No messages received for initial period in chat note ${chatNoteId}`); + console.warn(`[${responseId}] No messages received for initial period in chat note ${noteId}`); if (!receivedAnyMessage) { - console.error(`[${responseId}] WebSocket connection not established for chat note ${chatNoteId}`); + console.error(`[${responseId}] WebSocket connection not established for chat note ${noteId}`); if (timeoutId !== null) { window.clearTimeout(timeoutId); @@ -463,15 +452,9 @@ function cleanupEventListener(listener: ((event: Event) => void) | null): void { /** * Get a direct response from the server without streaming */ -export async function getDirectResponse(chatNoteId: string, messageParams: any): Promise { +export async function getDirectResponse(noteId: string, messageParams: any): Promise { try { - // Validate that we have a proper note ID format, not a session ID - if (chatNoteId && chatNoteId.length === 16 && /^[A-Za-z0-9]+$/.test(chatNoteId)) { - console.error(`Invalid note ID format: ${chatNoteId} appears to be a legacy session ID`); - throw new Error("Invalid note ID format - using a legacy session ID"); - } - - const postResponse = await server.post(`llm/chat/${chatNoteId}/messages`, { + const postResponse = await server.post(`llm/chat/${noteId}/messages`, { message: messageParams.content, includeContext: messageParams.useAdvancedContext, options: { diff --git a/apps/client/src/widgets/llm_chat/llm_chat_panel.ts b/apps/client/src/widgets/llm_chat/llm_chat_panel.ts index 32ffab50d..57a95fd5b 100644 --- a/apps/client/src/widgets/llm_chat/llm_chat_panel.ts +++ b/apps/client/src/widgets/llm_chat/llm_chat_panel.ts @@ -37,9 +37,10 @@ export default class LlmChatPanel extends BasicWidget { private thinkingBubble!: HTMLElement; private thinkingText!: HTMLElement; private thinkingToggle!: HTMLElement; - private chatNoteId: string | null = null; - private noteId: string | null = null; // The actual noteId for the Chat Note - private currentNoteId: string | null = null; + + // Simplified to just use noteId - this represents the AI Chat note we're working with + private noteId: string | null = null; + private currentNoteId: string | null = null; // The note providing context (for regular notes) private _messageHandlerId: number | null = null; private _messageHandler: any = null; @@ -90,12 +91,21 @@ export default class LlmChatPanel extends BasicWidget { this.messages = messages; } - public getChatNoteId(): string | null { - return this.chatNoteId; + public getNoteId(): string | null { + return this.noteId; } - public setChatNoteId(chatNoteId: string | null): void { - this.chatNoteId = chatNoteId; + public setNoteId(noteId: string | null): void { + this.noteId = noteId; + } + + // Deprecated - keeping for backward compatibility but mapping to noteId + public getChatNoteId(): string | null { + return this.noteId; + } + + public setChatNoteId(noteId: string | null): void { + this.noteId = noteId; } public getNoteContextChatMessages(): HTMLElement { @@ -307,10 +317,16 @@ export default class LlmChatPanel extends BasicWidget { } } - const dataToSave: ChatData = { + // Only save if we have a valid note ID + if (!this.noteId) { + console.warn('Cannot save chat data: no noteId available'); + return; + } + + const dataToSave = { messages: this.messages, - chatNoteId: this.chatNoteId, noteId: this.noteId, + chatNoteId: this.noteId, // For backward compatibility toolSteps: toolSteps, // Add sources if we have them sources: this.sources || [], @@ -325,7 +341,7 @@ export default class LlmChatPanel extends BasicWidget { } }; - console.log(`Saving chat data with chatNoteId: ${this.chatNoteId}, noteId: ${this.noteId}, ${toolSteps.length} tool steps, ${this.sources?.length || 0} sources, ${toolExecutions.length} tool executions`); + console.log(`Saving chat data with noteId: ${this.noteId}, ${toolSteps.length} tool steps, ${this.sources?.length || 0} sources, ${toolExecutions.length} tool executions`); // Save the data to the note attribute via the callback // This is the ONLY place we should save data, letting the container widget handle persistence @@ -400,7 +416,6 @@ export default class LlmChatPanel extends BasicWidget { // Load Chat Note ID if available if (savedData.noteId) { console.log(`Using noteId as Chat Note ID: ${savedData.noteId}`); - this.chatNoteId = savedData.noteId; this.noteId = savedData.noteId; } else { console.log(`No noteId found in saved data, cannot load chat session`); @@ -550,6 +565,15 @@ export default class LlmChatPanel extends BasicWidget { // Get current note context if needed const currentActiveNoteId = appContext.tabManager.getActiveContext()?.note?.noteId || null; + // For AI Chat notes, the note itself IS the chat session + // So currentNoteId and noteId should be the same + if (this.noteId && currentActiveNoteId === this.noteId) { + // We're in an AI Chat note - don't reset, just load saved data + console.log(`Refreshing AI Chat note ${this.noteId} - loading saved data`); + await this.loadSavedData(); + return; + } + // If we're switching to a different note, we need to reset if (this.currentNoteId !== currentActiveNoteId) { console.log(`Note ID changed from ${this.currentNoteId} to ${currentActiveNoteId}, resetting chat panel`); @@ -557,7 +581,6 @@ export default class LlmChatPanel extends BasicWidget { // Reset the UI and data this.noteContextChatMessages.innerHTML = ''; this.messages = []; - this.chatNoteId = null; this.noteId = null; // Also reset the chat note ID this.hideSources(); // Hide any sources from previous note @@ -569,7 +592,7 @@ export default class LlmChatPanel extends BasicWidget { const hasSavedData = await this.loadSavedData(); // Only create a new session if we don't have a session or saved data - if (!this.chatNoteId || !this.noteId || !hasSavedData) { + if (!this.noteId || !hasSavedData) { // Create a new chat session await this.createChatSession(); } @@ -580,19 +603,15 @@ export default class LlmChatPanel extends BasicWidget { */ private async createChatSession() { try { - // Create a new chat session, passing the current note ID if it exists - const { chatNoteId, noteId } = await createChatSession( - this.currentNoteId ? this.currentNoteId : undefined - ); + // If we already have a noteId (for AI Chat notes), use it + const contextNoteId = this.noteId || this.currentNoteId; - if (chatNoteId) { - // If we got back an ID from the API, use it - this.chatNoteId = chatNoteId; - - // For new sessions, the noteId should equal the chatNoteId - // This ensures we're using the note ID consistently - this.noteId = noteId || chatNoteId; + // Create a new chat session, passing the context note ID + const noteId = await createChatSession(contextNoteId ? contextNoteId : undefined); + if (noteId) { + // Set the note ID for this chat + this.noteId = noteId; console.log(`Created new chat session with noteId: ${this.noteId}`); } else { throw new Error("Failed to create chat session - no ID returned"); @@ -645,7 +664,7 @@ export default class LlmChatPanel extends BasicWidget { const showThinking = this.showThinkingCheckbox.checked; // Add logging to verify parameters - console.log(`Sending message with: useAdvancedContext=${useAdvancedContext}, showThinking=${showThinking}, noteId=${this.currentNoteId}, sessionId=${this.chatNoteId}`); + console.log(`Sending message with: useAdvancedContext=${useAdvancedContext}, showThinking=${showThinking}, noteId=${this.currentNoteId}, sessionId=${this.noteId}`); // Create the message parameters const messageParams = { @@ -695,11 +714,11 @@ export default class LlmChatPanel extends BasicWidget { await validateEmbeddingProviders(this.validationWarning); // Make sure we have a valid session - if (!this.chatNoteId) { + if (!this.noteId) { // If no session ID, create a new session await this.createChatSession(); - if (!this.chatNoteId) { + if (!this.noteId) { // If still no session ID, show error and return console.error("Failed to create chat session"); toastService.showError("Failed to create chat session"); @@ -730,7 +749,7 @@ export default class LlmChatPanel extends BasicWidget { await this.saveCurrentData(); // Add logging to verify parameters - console.log(`Sending message with: useAdvancedContext=${useAdvancedContext}, showThinking=${showThinking}, noteId=${this.currentNoteId}, sessionId=${this.chatNoteId}`); + console.log(`Sending message with: useAdvancedContext=${useAdvancedContext}, showThinking=${showThinking}, noteId=${this.currentNoteId}, sessionId=${this.noteId}`); // Create the message parameters const messageParams = { @@ -767,12 +786,12 @@ export default class LlmChatPanel extends BasicWidget { */ private async handleDirectResponse(messageParams: any): Promise { try { - if (!this.chatNoteId) return false; + if (!this.noteId) return false; - console.log(`Getting direct response using sessionId: ${this.chatNoteId} (noteId: ${this.noteId})`); + console.log(`Getting direct response using sessionId: ${this.noteId} (noteId: ${this.noteId})`); // Get a direct response from the server - const postResponse = await getDirectResponse(this.chatNoteId, messageParams); + const postResponse = await getDirectResponse(this.noteId, messageParams); // If the POST request returned content directly, display it if (postResponse && postResponse.content) { @@ -845,11 +864,11 @@ export default class LlmChatPanel extends BasicWidget { * Set up streaming response via WebSocket */ private async setupStreamingResponse(messageParams: any): Promise { - if (!this.chatNoteId) { + if (!this.noteId) { throw new Error("No session ID available"); } - console.log(`Setting up streaming response using sessionId: ${this.chatNoteId} (noteId: ${this.noteId})`); + console.log(`Setting up streaming response using sessionId: ${this.noteId} (noteId: ${this.noteId})`); // Store tool executions captured during streaming const toolExecutionsCache: Array<{ @@ -862,7 +881,7 @@ export default class LlmChatPanel extends BasicWidget { }> = []; return setupStreamingResponse( - this.chatNoteId, + this.noteId, messageParams, // Content update handler (content: string, isDone: boolean = false) => { @@ -898,7 +917,7 @@ export default class LlmChatPanel extends BasicWidget { similarity?: number; content?: string; }>; - }>(`llm/chat/${this.chatNoteId}`) + }>(`llm/chat/${this.noteId}`) .then((sessionData) => { console.log("Got updated session data:", sessionData); diff --git a/apps/client/src/widgets/llm_chat/types.ts b/apps/client/src/widgets/llm_chat/types.ts index 300a7856a..7181651d0 100644 --- a/apps/client/src/widgets/llm_chat/types.ts +++ b/apps/client/src/widgets/llm_chat/types.ts @@ -11,7 +11,7 @@ export interface ChatResponse { export interface SessionResponse { id: string; title: string; - noteId?: string; + noteId: string; // The ID of the chat note } export interface ToolExecutionStep { @@ -33,8 +33,8 @@ export interface MessageData { export interface ChatData { messages: MessageData[]; - chatNoteId: string | null; - noteId?: string | null; + noteId: string; // The ID of the chat note + chatNoteId?: string; // Deprecated - kept for backward compatibility, should equal noteId toolSteps: ToolExecutionStep[]; sources?: Array<{ noteId: string; diff --git a/apps/client/src/widgets/type_widgets/ai_chat.ts b/apps/client/src/widgets/type_widgets/ai_chat.ts index e96cf5f20..f733b499b 100644 --- a/apps/client/src/widgets/type_widgets/ai_chat.ts +++ b/apps/client/src/widgets/type_widgets/ai_chat.ts @@ -94,6 +94,11 @@ export default class AiChatTypeWidget extends TypeWidget { this.llmChatPanel.clearNoteContextChatMessages(); this.llmChatPanel.setMessages([]); + // Set the note ID for the chat panel + if (note) { + this.llmChatPanel.setNoteId(note.noteId); + } + // This will load saved data via the getData callback await this.llmChatPanel.refresh(); this.isInitialized = true; @@ -130,7 +135,7 @@ export default class AiChatTypeWidget extends TypeWidget { // Reset the chat panel UI this.llmChatPanel.clearNoteContextChatMessages(); this.llmChatPanel.setMessages([]); - this.llmChatPanel.setChatNoteId(null); + this.llmChatPanel.setNoteId(this.note.noteId); } // Call the parent method to refresh @@ -152,6 +157,7 @@ export default class AiChatTypeWidget extends TypeWidget { // Make sure the chat panel has the current note ID if (this.note) { this.llmChatPanel.setCurrentNoteId(this.note.noteId); + this.llmChatPanel.setNoteId(this.note.noteId); } this.initPromise = (async () => { @@ -186,7 +192,7 @@ export default class AiChatTypeWidget extends TypeWidget { // Format the data properly - this is the canonical format of the data const formattedData = { messages: data.messages || [], - chatNoteId: data.chatNoteId || this.note.noteId, + noteId: this.note.noteId, // Always use the note's own ID toolSteps: data.toolSteps || [], sources: data.sources || [], metadata: { diff --git a/apps/server/src/routes/api/llm.ts b/apps/server/src/routes/api/llm.ts index 013ce779d..be046bdd6 100644 --- a/apps/server/src/routes/api/llm.ts +++ b/apps/server/src/routes/api/llm.ts @@ -814,10 +814,11 @@ async function streamMessage(req: Request, res: Response) { throw new Error('Content cannot be empty'); } - // Check if session exists - const session = restChatService.getSessions().get(chatNoteId); + // Get or create session from Chat Note + // This will check the sessions store first, and if not found, create from the Chat Note + const session = await restChatService.getOrCreateSessionFromChatNote(chatNoteId, true); if (!session) { - throw new Error('Chat not found'); + throw new Error('Chat not found and could not be created from note'); } // Update last active timestamp diff --git a/apps/server/src/services/llm/chat/rest_chat_service.ts b/apps/server/src/services/llm/chat/rest_chat_service.ts index 0a400ad91..432cc6e08 100644 --- a/apps/server/src/services/llm/chat/rest_chat_service.ts +++ b/apps/server/src/services/llm/chat/rest_chat_service.ts @@ -480,27 +480,56 @@ class RestChatService { const options: any = req.body || {}; const title = options.title || 'Chat Session'; - // Use the currentNoteId as the chatNoteId if provided - let chatNoteId = options.chatNoteId; + // Determine the note ID for the chat + let noteId = options.noteId || options.chatNoteId; // Accept either name for backward compatibility - // If currentNoteId is provided but chatNoteId is not, use currentNoteId - if (!chatNoteId && options.currentNoteId) { - chatNoteId = options.currentNoteId; - log.info(`Using provided currentNoteId ${chatNoteId} as chatNoteId`); + // If currentNoteId is provided, check if it's already an AI Chat note + if (!noteId && options.currentNoteId) { + // Import becca to check note type + const becca = (await import('../../../becca/becca.js')).default; + const note = becca.notes[options.currentNoteId]; + + // Check if this is an AI Chat note by looking at its content structure + if (note) { + try { + const content = note.getContent(); + if (content) { + const contentStr = typeof content === 'string' ? content : content.toString(); + const parsedContent = JSON.parse(contentStr); + // AI Chat notes have a messages array and noteId in their content + if (parsedContent.messages && Array.isArray(parsedContent.messages) && parsedContent.noteId) { + // This looks like an AI Chat note - use it directly + noteId = options.currentNoteId; + log.info(`Using existing AI Chat note ${noteId} as session`); + } + } + } catch (e) { + // Not JSON content, so not an AI Chat note + } + } + + if (!noteId) { + log.info(`Creating new chat note from context of note ${options.currentNoteId}`); + // Don't use the currentNoteId as the chat note ID - create a new one + } } - // If we still don't have a chatNoteId, create a new Chat Note - if (!chatNoteId) { + // If we don't have a noteId, create a new Chat Note + if (!noteId) { // Create a new Chat Note via the storage service const chatStorageService = (await import('../../llm/chat_storage_service.js')).default; const newChat = await chatStorageService.createChat(title); - chatNoteId = newChat.id; - log.info(`Created new Chat Note with ID: ${chatNoteId}`); + noteId = newChat.id; + log.info(`Created new Chat Note with ID: ${noteId}`); + } else { + // We have a noteId - this means we're working with an existing aiChat note + // Don't create another note, just use the existing one + log.info(`Using existing Chat Note with ID: ${noteId}`); } - // Create a new session through our session store + // Create a new session through our session store using the note ID const session = SessionsStore.createSession({ - chatNoteId, + chatNoteId: noteId, // This is really the noteId of the chat note title, systemPrompt: options.systemPrompt, contextNoteId: options.contextNoteId, @@ -511,10 +540,10 @@ class RestChatService { }); return { - id: session.id, + id: session.id, // This will be the same as noteId title: session.title, createdAt: session.createdAt, - noteId: chatNoteId // Return the note ID explicitly + noteId: noteId // Return the note ID for clarity }; } catch (error: any) { log.error(`Error creating LLM session: ${error.message || 'Unknown error'}`);