/** * LLM Chat Panel Widget */ import BasicWidget from "../basic_widget.js"; import toastService from "../../services/toast.js"; import appContext from "../../components/app_context.js"; import server from "../../services/server.js"; import { TPL, addMessageToChat, showSources, hideSources, showLoadingIndicator, hideLoadingIndicator } from "./ui.js"; import { formatMarkdown } from "./utils.js"; import { createChatSession, checkSessionExists, setupStreamingResponse, getDirectResponse } from "./communication.js"; import { extractInChatToolSteps } from "./message_processor.js"; import { validateEmbeddingProviders } from "./validation.js"; import type { MessageData, ToolExecutionStep, ChatData } from "./types.js"; import { applySyntaxHighlight } from "../../services/syntax_highlight.js"; import "../../stylesheets/llm_chat.css"; export default class LlmChatPanel extends BasicWidget { private noteContextChatMessages!: HTMLElement; private noteContextChatForm!: HTMLFormElement; private noteContextChatInput!: HTMLTextAreaElement; private noteContextChatSendButton!: HTMLButtonElement; private chatContainer!: HTMLElement; private loadingIndicator!: HTMLElement; private sourcesList!: HTMLElement; private sourcesContainer!: HTMLElement; private sourcesCount!: HTMLElement; private useAdvancedContextCheckbox!: HTMLInputElement; private showThinkingCheckbox!: HTMLInputElement; private validationWarning!: HTMLElement; private chatNoteId: string | null = null; private noteId: string | null = null; // The actual noteId for the Chat Note private currentNoteId: string | null = null; private _messageHandlerId: number | null = null; private _messageHandler: any = null; // Callbacks for data persistence private onSaveData: ((data: any) => Promise) | null = null; private onGetData: (() => Promise) | null = null; private messages: MessageData[] = []; private sources: Array<{noteId: string; title: string; similarity?: number; content?: string}> = []; private metadata: { model?: string; provider?: string; temperature?: number; maxTokens?: number; toolExecutions?: Array<{ id: string; name: string; arguments: any; result: any; error?: string; timestamp: string; }>; lastUpdated?: string; usage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number; }; } = { model: 'default', temperature: 0.7, toolExecutions: [] }; // Public getters and setters for private properties public getCurrentNoteId(): string | null { return this.currentNoteId; } public setCurrentNoteId(noteId: string | null): void { this.currentNoteId = noteId; } public getMessages(): MessageData[] { return this.messages; } public setMessages(messages: MessageData[]): void { this.messages = messages; } public getChatNoteId(): string | null { return this.chatNoteId; } public setChatNoteId(chatNoteId: string | null): void { this.chatNoteId = chatNoteId; } public getNoteContextChatMessages(): HTMLElement { return this.noteContextChatMessages; } public clearNoteContextChatMessages(): void { this.noteContextChatMessages.innerHTML = ''; } doRender() { this.$widget = $(TPL); const element = this.$widget[0]; this.noteContextChatMessages = element.querySelector('.note-context-chat-messages') as HTMLElement; this.noteContextChatForm = element.querySelector('.note-context-chat-form') as HTMLFormElement; this.noteContextChatInput = element.querySelector('.note-context-chat-input') as HTMLTextAreaElement; this.noteContextChatSendButton = element.querySelector('.note-context-chat-send-button') as HTMLButtonElement; this.chatContainer = element.querySelector('.note-context-chat-container') as HTMLElement; this.loadingIndicator = element.querySelector('.loading-indicator') as HTMLElement; this.sourcesList = element.querySelector('.sources-list') as HTMLElement; this.sourcesContainer = element.querySelector('.sources-container') as HTMLElement; this.sourcesCount = element.querySelector('.sources-count') as HTMLElement; this.useAdvancedContextCheckbox = element.querySelector('.use-advanced-context-checkbox') as HTMLInputElement; this.showThinkingCheckbox = element.querySelector('.show-thinking-checkbox') as HTMLInputElement; this.validationWarning = element.querySelector('.provider-validation-warning') as HTMLElement; // Set up event delegation for the settings link this.validationWarning.addEventListener('click', (e) => { const target = e.target as HTMLElement; if (target.classList.contains('settings-link') || target.closest('.settings-link')) { console.log('Settings link clicked, navigating to AI settings URL'); window.location.href = '#root/_hidden/_options/_optionsAi'; } }); this.initializeEventListeners(); return this.$widget; } cleanup() { console.log(`LlmChatPanel cleanup called, removing any active WebSocket subscriptions`); this._messageHandler = null; this._messageHandlerId = null; } /** * Set the callbacks for data persistence */ setDataCallbacks( saveDataCallback: (data: any) => Promise, getDataCallback: () => Promise ) { this.onSaveData = saveDataCallback; this.onGetData = getDataCallback; } /** * Save current chat data to the note attribute */ async saveCurrentData() { if (!this.onSaveData) { return; } try { // Extract current tool execution steps if any exist const toolSteps = extractInChatToolSteps(this.noteContextChatMessages); // Get tool executions from both UI and any cached executions in metadata let toolExecutions: Array<{ id: string; name: string; arguments: any; result: any; error?: string; timestamp: string; }> = []; // First include any tool executions already in metadata (from streaming events) if (this.metadata?.toolExecutions && Array.isArray(this.metadata.toolExecutions)) { toolExecutions = [...this.metadata.toolExecutions]; console.log(`Including ${toolExecutions.length} tool executions from metadata`); } // Also extract any visible tool steps from the UI const extractedExecutions = toolSteps.map(step => { // Parse tool execution information if (step.type === 'tool-execution') { try { const content = JSON.parse(step.content); return { id: content.toolCallId || `tool-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`, name: content.tool || 'unknown', arguments: content.args || {}, result: content.result || {}, error: content.error, timestamp: new Date().toISOString() }; } catch (e) { // If we can't parse it, create a basic record return { id: `tool-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`, name: 'unknown', arguments: {}, result: step.content, timestamp: new Date().toISOString() }; } } else if (step.type === 'result' && step.name) { // Handle result steps with a name return { id: `tool-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`, name: step.name, arguments: {}, result: step.content, timestamp: new Date().toISOString() }; } return { id: `tool-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`, name: 'unknown', arguments: {}, result: 'Unrecognized tool step', timestamp: new Date().toISOString() }; }); // Merge the tool executions, keeping only unique IDs const existingIds = new Set(toolExecutions.map((t: {id: string}) => t.id)); for (const exec of extractedExecutions) { if (!existingIds.has(exec.id)) { toolExecutions.push(exec); existingIds.add(exec.id); } } const dataToSave: ChatData = { messages: this.messages, chatNoteId: this.chatNoteId, noteId: this.noteId, toolSteps: toolSteps, // Add sources if we have them sources: this.sources || [], // Add metadata metadata: { model: this.metadata?.model || 'default', provider: this.metadata?.provider || undefined, temperature: this.metadata?.temperature || 0.7, lastUpdated: new Date().toISOString(), // Add tool executions toolExecutions: toolExecutions } }; 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`); // 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 await this.onSaveData(dataToSave); } catch (error) { console.error('Error saving chat data:', error); } } /** * Load saved chat data from the note attribute */ async loadSavedData(): Promise { if (!this.onGetData) { return false; } try { const savedData = await this.onGetData() as ChatData; if (savedData?.messages?.length > 0) { // Load messages this.messages = savedData.messages; // Clear and rebuild the chat UI this.noteContextChatMessages.innerHTML = ''; this.messages.forEach(message => { const role = message.role as 'user' | 'assistant'; this.addMessageToChat(role, message.content); }); // Restore tool execution steps if they exist if (savedData.toolSteps && Array.isArray(savedData.toolSteps) && savedData.toolSteps.length > 0) { console.log(`Restoring ${savedData.toolSteps.length} saved tool steps`); this.restoreInChatToolSteps(savedData.toolSteps); } // Load sources if available if (savedData.sources && Array.isArray(savedData.sources)) { this.sources = savedData.sources; console.log(`Loaded ${this.sources.length} sources from saved data`); // Show sources in the UI if they exist if (this.sources.length > 0) { this.showSources(this.sources); } } // Load metadata if available if (savedData.metadata) { this.metadata = { ...this.metadata, ...savedData.metadata }; // Ensure tool executions are loaded if (savedData.metadata.toolExecutions && Array.isArray(savedData.metadata.toolExecutions)) { console.log(`Loaded ${savedData.metadata.toolExecutions.length} tool executions from saved data`); if (!this.metadata.toolExecutions) { this.metadata.toolExecutions = []; } // Make sure we don't lose any tool executions this.metadata.toolExecutions = savedData.metadata.toolExecutions; } console.log(`Loaded metadata from saved data:`, this.metadata); } // 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`); return false; } return true; } } catch (error) { console.error('Failed to load saved chat data', error); } return false; } /** * Restore tool execution steps in the chat UI */ private restoreInChatToolSteps(steps: ToolExecutionStep[]) { if (!steps || steps.length === 0) return; // Create the tool execution element const toolExecutionElement = document.createElement('div'); toolExecutionElement.className = 'chat-tool-execution mb-3'; // Insert before the assistant message if it exists const assistantMessage = this.noteContextChatMessages.querySelector('.assistant-message:last-child'); if (assistantMessage) { this.noteContextChatMessages.insertBefore(toolExecutionElement, assistantMessage); } else { // Otherwise append to the end this.noteContextChatMessages.appendChild(toolExecutionElement); } // Fill with tool execution content toolExecutionElement.innerHTML = `
Tool Execution
${this.renderToolStepsHtml(steps)}
`; // Add event listener for the toggle button const toggleButton = toolExecutionElement.querySelector('.tool-execution-toggle'); if (toggleButton) { toggleButton.addEventListener('click', () => { const stepsContainer = toolExecutionElement.querySelector('.tool-execution-container'); const icon = toggleButton.querySelector('i'); if (stepsContainer) { if (stepsContainer.classList.contains('collapsed')) { // Expand stepsContainer.classList.remove('collapsed'); (stepsContainer as HTMLElement).style.display = 'block'; if (icon) { icon.className = 'bx bx-chevron-down'; } } else { // Collapse stepsContainer.classList.add('collapsed'); (stepsContainer as HTMLElement).style.display = 'none'; if (icon) { icon.className = 'bx bx-chevron-right'; } } } }); } // Add click handler for the header to toggle expansion as well const header = toolExecutionElement.querySelector('.tool-execution-header'); if (header) { header.addEventListener('click', (e) => { // Only toggle if the click isn't on the toggle button itself const target = e.target as HTMLElement; if (target && !target.closest('.tool-execution-toggle')) { const toggleButton = toolExecutionElement.querySelector('.tool-execution-toggle'); toggleButton?.dispatchEvent(new Event('click')); } }); (header as HTMLElement).style.cursor = 'pointer'; } } /** * Render HTML for tool execution steps */ private renderToolStepsHtml(steps: ToolExecutionStep[]): string { if (!steps || steps.length === 0) return ''; return steps.map(step => { let icon = 'bx-info-circle'; let className = 'info'; let content = ''; if (step.type === 'executing') { icon = 'bx-code-block'; className = 'executing'; content = `
${step.content || 'Executing tools...'}
`; } else if (step.type === 'result') { icon = 'bx-terminal'; className = 'result'; content = `
Tool: ${step.name || 'unknown'}
${step.content || ''}
`; } else if (step.type === 'error') { icon = 'bx-error-circle'; className = 'error'; content = `
Tool: ${step.name || 'unknown'}
${step.content || 'Error occurred'}
`; } else if (step.type === 'generating') { icon = 'bx-message-dots'; className = 'generating'; content = `
${step.content || 'Generating response...'}
`; } return `
${content}
`; }).join(''); } async refresh() { if (!this.isVisible()) { return; } // Check for any provider validation issues when refreshing await validateEmbeddingProviders(this.validationWarning); // Get current note context if needed const currentActiveNoteId = appContext.tabManager.getActiveContext()?.note?.noteId || null; // 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`); // 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 // Update our current noteId this.currentNoteId = currentActiveNoteId; } // Always try to load saved data for the current note 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) { // Create a new chat session await this.createChatSession(); } } /** * Create a new chat session */ 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 (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; console.log(`Created new chat session with noteId: ${this.noteId}`); } else { throw new Error("Failed to create chat session - no ID returned"); } // Save the note ID as the session identifier await this.saveCurrentData(); } catch (error) { console.error('Error creating chat session:', error); toastService.showError('Failed to create chat session'); } } /** * Handle sending a user message to the LLM service */ private async sendMessage(content: string) { if (!content.trim()) return; // Add the user message to the UI and data model this.addMessageToChat('user', content); this.messages.push({ role: 'user', content: content }); // Save the data immediately after a user message await this.saveCurrentData(); // Clear input and show loading state this.noteContextChatInput.value = ''; showLoadingIndicator(this.loadingIndicator); this.hideSources(); try { const useAdvancedContext = this.useAdvancedContextCheckbox.checked; 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}`); // Create the message parameters const messageParams = { content, useAdvancedContext, showThinking }; // Try websocket streaming (preferred method) try { await this.setupStreamingResponse(messageParams); } catch (streamingError) { console.warn("WebSocket streaming failed, falling back to direct response:", streamingError); // If streaming fails, fall back to direct response const handled = await this.handleDirectResponse(messageParams); if (!handled) { // If neither method works, show an error throw new Error("Failed to get response from server"); } } // Note: We don't need to save here since the streaming completion and direct response methods // both call saveCurrentData() when they're done } catch (error) { console.error('Error processing user message:', error); toastService.showError('Failed to process message'); // Add a generic error message to the UI this.addMessageToChat('assistant', 'Sorry, I encountered an error processing your message. Please try again.'); this.messages.push({ role: 'assistant', content: 'Sorry, I encountered an error processing your message. Please try again.' }); // Save the data even after error await this.saveCurrentData(); } } /** * Process a new user message - add to UI and save */ private async processUserMessage(content: string) { // Check for validation issues first await validateEmbeddingProviders(this.validationWarning); // Make sure we have a valid session if (!this.chatNoteId) { // If no session ID, create a new session await this.createChatSession(); if (!this.chatNoteId) { // If still no session ID, show error and return console.error("Failed to create chat session"); toastService.showError("Failed to create chat session"); return; } } // Add user message to messages array if not already added if (!this.messages.some(msg => msg.role === 'user' && msg.content === content)) { this.messages.push({ role: 'user', content: content }); } // Clear input and show loading state this.noteContextChatInput.value = ''; showLoadingIndicator(this.loadingIndicator); this.hideSources(); try { const useAdvancedContext = this.useAdvancedContextCheckbox.checked; const showThinking = this.showThinkingCheckbox.checked; // Save current state to the Chat Note before getting a response await this.saveCurrentData(); // Add logging to verify parameters console.log(`Sending message with: useAdvancedContext=${useAdvancedContext}, showThinking=${showThinking}, noteId=${this.currentNoteId}, sessionId=${this.chatNoteId}`); // Create the message parameters const messageParams = { content, useAdvancedContext, showThinking }; // Try websocket streaming (preferred method) try { await this.setupStreamingResponse(messageParams); } catch (streamingError) { console.warn("WebSocket streaming failed, falling back to direct response:", streamingError); // If streaming fails, fall back to direct response const handled = await this.handleDirectResponse(messageParams); if (!handled) { // If neither method works, show an error throw new Error("Failed to get response from server"); } } // Save final state after getting the response await this.saveCurrentData(); } catch (error) { this.handleError(error as Error); // Make sure we save the current state even on error await this.saveCurrentData(); } } /** * Try to get a direct response from the server */ private async handleDirectResponse(messageParams: any): Promise { try { if (!this.chatNoteId) return false; console.log(`Getting direct response using sessionId: ${this.chatNoteId} (noteId: ${this.noteId})`); // Get a direct response from the server const postResponse = await getDirectResponse(this.chatNoteId, messageParams); // If the POST request returned content directly, display it if (postResponse && postResponse.content) { // Store metadata from the response if (postResponse.metadata) { console.log("Received metadata from response:", postResponse.metadata); this.metadata = { ...this.metadata, ...postResponse.metadata }; } // Store sources from the response if (postResponse.sources && postResponse.sources.length > 0) { console.log(`Received ${postResponse.sources.length} sources from response`); this.sources = postResponse.sources; this.showSources(postResponse.sources); } // Process the assistant response this.processAssistantResponse(postResponse.content, postResponse); hideLoadingIndicator(this.loadingIndicator); return true; } return false; } catch (error) { console.error("Error with direct response:", error); return false; } } /** * Process an assistant response - add to UI and save */ private async processAssistantResponse(content: string, fullResponse?: any) { // Add the response to the chat UI this.addMessageToChat('assistant', content); // Add to our local message array too this.messages.push({ role: 'assistant', content, timestamp: new Date() }); // If we received tool execution information, add it to metadata if (fullResponse?.metadata?.toolExecutions) { console.log(`Storing ${fullResponse.metadata.toolExecutions.length} tool executions from response`); // Make sure our metadata has toolExecutions if (!this.metadata.toolExecutions) { this.metadata.toolExecutions = []; } // Add new tool executions this.metadata.toolExecutions = [ ...this.metadata.toolExecutions, ...fullResponse.metadata.toolExecutions ]; } // Save to note this.saveCurrentData().catch(err => { console.error("Failed to save assistant response to note:", err); }); } /** * Set up streaming response via WebSocket */ private async setupStreamingResponse(messageParams: any): Promise { if (!this.chatNoteId) { throw new Error("No session ID available"); } console.log(`Setting up streaming response using sessionId: ${this.chatNoteId} (noteId: ${this.noteId})`); // Store tool executions captured during streaming const toolExecutionsCache: Array<{ id: string; name: string; arguments: any; result: any; error?: string; timestamp: string; }> = []; return setupStreamingResponse( this.chatNoteId, messageParams, // Content update handler (content: string, isDone: boolean = false) => { this.updateStreamingUI(content, isDone); // Update session data with additional metadata when streaming is complete if (isDone) { // Update our metadata with info from the server server.get<{ metadata?: { model?: string; provider?: string; temperature?: number; maxTokens?: number; toolExecutions?: Array<{ id: string; name: string; arguments: any; result: any; error?: string; timestamp: string; }>; lastUpdated?: string; usage?: { promptTokens?: number; completionTokens?: number; totalTokens?: number; }; }; sources?: Array<{ noteId: string; title: string; similarity?: number; content?: string; }>; }>(`llm/chat/${this.chatNoteId}`) .then((sessionData) => { console.log("Got updated session data:", sessionData); // Store metadata if (sessionData.metadata) { this.metadata = { ...this.metadata, ...sessionData.metadata }; } // Store sources if (sessionData.sources && sessionData.sources.length > 0) { this.sources = sessionData.sources; this.showSources(sessionData.sources); } // Make sure we include the cached tool executions if (toolExecutionsCache.length > 0) { console.log(`Including ${toolExecutionsCache.length} cached tool executions in metadata`); if (!this.metadata.toolExecutions) { this.metadata.toolExecutions = []; } // Add any tool executions from our cache that aren't already in metadata const existingIds = new Set((this.metadata.toolExecutions || []).map((t: {id: string}) => t.id)); for (const toolExec of toolExecutionsCache) { if (!existingIds.has(toolExec.id)) { this.metadata.toolExecutions.push(toolExec); existingIds.add(toolExec.id); } } } // Save the updated data to the note this.saveCurrentData() .catch(err => console.error("Failed to save data after streaming completed:", err)); }) .catch(err => console.error("Error fetching session data after streaming:", err)); } }, // Thinking update handler (thinking: string) => { this.showThinkingState(thinking); }, // Tool execution handler (toolData: any) => { this.showToolExecutionInfo(toolData); // Cache tools we see during streaming to include them in the final saved data if (toolData && toolData.action === 'result' && toolData.tool) { // Create a tool execution record const toolExec = { id: toolData.toolCallId || `tool-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`, name: toolData.tool, arguments: toolData.args || {}, result: toolData.result || {}, error: toolData.error, timestamp: new Date().toISOString() }; // Add to both our local cache for immediate saving and to metadata for later saving toolExecutionsCache.push(toolExec); // Initialize toolExecutions array if it doesn't exist if (!this.metadata.toolExecutions) { this.metadata.toolExecutions = []; } // Add tool execution to our metadata this.metadata.toolExecutions.push(toolExec); console.log(`Cached tool execution for ${toolData.tool} to be saved later`); // Save immediately after receiving a tool execution // This ensures we don't lose tool execution data if streaming fails this.saveCurrentData().catch(err => { console.error("Failed to save tool execution data:", err); }); } }, // Complete handler () => { hideLoadingIndicator(this.loadingIndicator); }, // Error handler (error: Error) => { this.handleError(error); } ); } /** * Update the UI with streaming content */ private updateStreamingUI(assistantResponse: string, isDone: boolean = false) { // Get the existing assistant message or create a new one let assistantMessageEl = this.noteContextChatMessages.querySelector('.assistant-message:last-child'); if (!assistantMessageEl) { // If no assistant message yet, create one assistantMessageEl = document.createElement('div'); assistantMessageEl.className = 'assistant-message message mb-3'; this.noteContextChatMessages.appendChild(assistantMessageEl); // Add assistant profile icon const profileIcon = document.createElement('div'); profileIcon.className = 'profile-icon'; profileIcon.innerHTML = ''; assistantMessageEl.appendChild(profileIcon); // Add message content container const messageContent = document.createElement('div'); messageContent.className = 'message-content'; assistantMessageEl.appendChild(messageContent); } // Update the content const messageContent = assistantMessageEl.querySelector('.message-content') as HTMLElement; messageContent.innerHTML = formatMarkdown(assistantResponse); // Apply syntax highlighting if this is the final update if (isDone) { applySyntaxHighlight($(assistantMessageEl as HTMLElement)); // Update message in the data model for storage // Find the last assistant message to update, or add a new one if none exists const assistantMessages = this.messages.filter(msg => msg.role === 'assistant'); const lastAssistantMsgIndex = assistantMessages.length > 0 ? this.messages.lastIndexOf(assistantMessages[assistantMessages.length - 1]) : -1; if (lastAssistantMsgIndex >= 0) { // Update existing message this.messages[lastAssistantMsgIndex].content = assistantResponse; } else { // Add new message this.messages.push({ role: 'assistant', content: assistantResponse }); } // Hide loading indicator hideLoadingIndicator(this.loadingIndicator); // Save the final state to the Chat Note this.saveCurrentData().catch(err => { console.error("Failed to save assistant response to note:", err); }); } // Scroll to bottom this.chatContainer.scrollTop = this.chatContainer.scrollHeight; } /** * Handle general errors in the send message flow */ private handleError(error: Error) { hideLoadingIndicator(this.loadingIndicator); toastService.showError('Error sending message: ' + error.message); } private addMessageToChat(role: 'user' | 'assistant', content: string) { addMessageToChat(this.noteContextChatMessages, this.chatContainer, role, content); } private showSources(sources: Array<{noteId: string, title: string}>) { showSources( this.sourcesList, this.sourcesContainer, this.sourcesCount, sources, (noteId: string) => { // Open the note in a new tab but don't switch to it appContext.tabManager.openTabWithNoteWithHoisting(noteId, { activate: false }); } ); } private hideSources() { hideSources(this.sourcesContainer); } /** * Handle tool execution updates */ private showToolExecutionInfo(toolExecutionData: any) { console.log(`Showing tool execution info: ${JSON.stringify(toolExecutionData)}`); // Enhanced debugging for tool execution if (!toolExecutionData) { console.error('Tool execution data is missing or undefined'); return; } // Check for required properties const actionType = toolExecutionData.action || ''; const toolName = toolExecutionData.tool || 'unknown'; console.log(`Tool execution details: action=${actionType}, tool=${toolName}, hasResult=${!!toolExecutionData.result}`); // Force action to 'result' if missing but result is present if (!actionType && toolExecutionData.result) { console.log('Setting missing action to "result" since result is present'); toolExecutionData.action = 'result'; } // Create or get the tool execution container let toolExecutionElement = this.noteContextChatMessages.querySelector('.chat-tool-execution'); if (!toolExecutionElement) { toolExecutionElement = document.createElement('div'); toolExecutionElement.className = 'chat-tool-execution mb-3'; // Create header with title and dropdown toggle const header = document.createElement('div'); header.className = 'tool-execution-header d-flex align-items-center p-2 rounded'; header.innerHTML = ` Tool Execution `; toolExecutionElement.appendChild(header); // Create container for tool steps const stepsContainer = document.createElement('div'); stepsContainer.className = 'tool-execution-container p-2 rounded mb-2'; toolExecutionElement.appendChild(stepsContainer); // Add to chat messages this.noteContextChatMessages.appendChild(toolExecutionElement); // Add click handler for toggle button const toggleButton = toolExecutionElement.querySelector('.tool-execution-toggle'); if (toggleButton) { toggleButton.addEventListener('click', () => { const stepsContainer = toolExecutionElement?.querySelector('.tool-execution-container'); const icon = toggleButton.querySelector('i'); if (stepsContainer) { if (stepsContainer.classList.contains('collapsed')) { // Expand stepsContainer.classList.remove('collapsed'); (stepsContainer as HTMLElement).style.display = 'block'; if (icon) { icon.className = 'bx bx-chevron-down'; } } else { // Collapse stepsContainer.classList.add('collapsed'); (stepsContainer as HTMLElement).style.display = 'none'; if (icon) { icon.className = 'bx bx-chevron-right'; } } } }); } // Add click handler for the header to toggle expansion as well header.addEventListener('click', (e) => { // Only toggle if the click isn't on the toggle button itself const target = e.target as HTMLElement; if (target && !target.closest('.tool-execution-toggle')) { const toggleButton = toolExecutionElement?.querySelector('.tool-execution-toggle'); toggleButton?.dispatchEvent(new Event('click')); } }); (header as HTMLElement).style.cursor = 'pointer'; } // Get the steps container const stepsContainer = toolExecutionElement.querySelector('.tool-execution-container'); if (!stepsContainer) return; // Process based on action type const action = toolExecutionData.action || ''; if (action === 'start' || action === 'executing') { // Tool execution started const step = document.createElement('div'); step.className = 'tool-step executing p-2 mb-2 rounded'; step.innerHTML = `
Executing tool: ${toolExecutionData.tool || 'unknown'}
${toolExecutionData.args ? `
Args: ${JSON.stringify(toolExecutionData.args || {}, null, 2)}
` : ''} `; stepsContainer.appendChild(step); } else if (action === 'result' || action === 'complete') { // Tool execution completed with results const step = document.createElement('div'); step.className = 'tool-step result p-2 mb-2 rounded'; let resultDisplay = ''; // Special handling for note search tools which have a specific structure if ((toolExecutionData.tool === 'search_notes' || toolExecutionData.tool === 'keyword_search_notes') && typeof toolExecutionData.result === 'object' && toolExecutionData.result.results) { const results = toolExecutionData.result.results; if (results.length === 0) { resultDisplay = `
No notes found matching the search criteria.
`; } else { resultDisplay = `
Found ${results.length} notes:
    ${results.map((note: any) => `
  • ${note.title} ${note.similarity < 1 ? `(similarity: ${(note.similarity * 100).toFixed(0)}%)` : ''}
  • `).join('')}
`; } } // Format the result based on type for other tools else if (typeof toolExecutionData.result === 'object') { // For objects, format as pretty JSON resultDisplay = `
${JSON.stringify(toolExecutionData.result, null, 2)}
`; } else { // For simple values, display as text resultDisplay = `
${String(toolExecutionData.result)}
`; } step.innerHTML = `
Tool: ${toolExecutionData.tool || 'unknown'}
${resultDisplay}
`; stepsContainer.appendChild(step); // Add event listeners for note links if this is a note search result if (toolExecutionData.tool === 'search_notes' || toolExecutionData.tool === 'keyword_search_notes') { const noteLinks = step.querySelectorAll('.note-link'); noteLinks.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const noteId = (e.currentTarget as HTMLElement).getAttribute('data-note-id'); if (noteId) { // Open the note in a new tab but don't switch to it appContext.tabManager.openTabWithNoteWithHoisting(noteId, { activate: false }); } }); }); } } else if (action === 'error') { // Tool execution failed const step = document.createElement('div'); step.className = 'tool-step error p-2 mb-2 rounded'; step.innerHTML = `
Error in tool: ${toolExecutionData.tool || 'unknown'}
${toolExecutionData.error || 'Unknown error'}
`; stepsContainer.appendChild(step); } else if (action === 'generating') { // Generating final response with tool results const step = document.createElement('div'); step.className = 'tool-step generating p-2 mb-2 rounded'; step.innerHTML = `
Generating response with tool results...
`; stepsContainer.appendChild(step); } // Make sure the loading indicator is shown during tool execution this.loadingIndicator.style.display = 'flex'; // Scroll the chat container to show the tool execution this.chatContainer.scrollTop = this.chatContainer.scrollHeight; } /** * Show thinking state in the UI */ private showThinkingState(thinkingData: string) { // Thinking state is now updated via the in-chat UI in updateStreamingUI // This method is now just a hook for the WebSocket handlers // Show the loading indicator this.loadingIndicator.style.display = 'flex'; } private initializeEventListeners() { this.noteContextChatForm.addEventListener('submit', (e) => { e.preventDefault(); const content = this.noteContextChatInput.value; this.sendMessage(content); }); // Add auto-resize functionality to the textarea this.noteContextChatInput.addEventListener('input', () => { this.noteContextChatInput.style.height = 'auto'; this.noteContextChatInput.style.height = `${this.noteContextChatInput.scrollHeight}px`; }); // Handle Enter key (send on Enter, new line on Shift+Enter) this.noteContextChatInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.noteContextChatForm.dispatchEvent(new Event('submit')); } }); } }