mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 20:06:08 +01:00 
			
		
		
		
	break up the chat_panel into smaller files
This commit is contained in:
		
							
								
								
									
										256
									
								
								src/public/app/widgets/llm_chat/communication.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								src/public/app/widgets/llm_chat/communication.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,256 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Communication functions for LLM Chat
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import server from "../../services/server.js";
 | 
				
			||||||
 | 
					import type { SessionResponse } from "./types.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Create a new chat session
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export async function createChatSession(): Promise<string | null> {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const resp = await server.post<SessionResponse>('llm/sessions', {
 | 
				
			||||||
 | 
					            title: 'Note Chat'
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (resp && resp.id) {
 | 
				
			||||||
 | 
					            return resp.id;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.error('Failed to create chat session:', error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Check if a session exists
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export async function checkSessionExists(sessionId: string): Promise<boolean> {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        const sessionCheck = await server.get<any>(`llm/sessions/${sessionId}`);
 | 
				
			||||||
 | 
					        return !!(sessionCheck && sessionCheck.id);
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.log(`Error checking session ${sessionId}:`, error);
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Set up streaming response via WebSocket
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export async function setupStreamingResponse(
 | 
				
			||||||
 | 
					    sessionId: string,
 | 
				
			||||||
 | 
					    messageParams: any,
 | 
				
			||||||
 | 
					    onContentUpdate: (content: string) => void,
 | 
				
			||||||
 | 
					    onThinkingUpdate: (thinking: string) => void,
 | 
				
			||||||
 | 
					    onToolExecution: (toolData: any) => void,
 | 
				
			||||||
 | 
					    onComplete: () => void,
 | 
				
			||||||
 | 
					    onError: (error: Error) => void
 | 
				
			||||||
 | 
					): Promise<void> {
 | 
				
			||||||
 | 
					    return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					        let assistantResponse = '';
 | 
				
			||||||
 | 
					        let receivedAnyContent = false;
 | 
				
			||||||
 | 
					        let timeoutId: number | null = null;
 | 
				
			||||||
 | 
					        let initialTimeoutId: number | null = null;
 | 
				
			||||||
 | 
					        let receivedAnyMessage = false;
 | 
				
			||||||
 | 
					        let eventListener: ((event: Event) => void) | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 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 session ${sessionId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Create a message handler for CustomEvents
 | 
				
			||||||
 | 
					        eventListener = (event: Event) => {
 | 
				
			||||||
 | 
					            const customEvent = event as CustomEvent;
 | 
				
			||||||
 | 
					            const message = customEvent.detail;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Only process messages for our session
 | 
				
			||||||
 | 
					            if (!message || message.sessionId !== sessionId) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            console.log(`[${responseId}] LLM Stream message received via CustomEvent: session=${sessionId}, content=${!!message.content}, contentLength=${message.content?.length || 0}, thinking=${!!message.thinking}, toolExecution=${!!message.toolExecution}, done=${!!message.done}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Mark first message received
 | 
				
			||||||
 | 
					            if (!receivedAnyMessage) {
 | 
				
			||||||
 | 
					                receivedAnyMessage = true;
 | 
				
			||||||
 | 
					                console.log(`[${responseId}] First message received for session ${sessionId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Clear the initial timeout since we've received a message
 | 
				
			||||||
 | 
					                if (initialTimeoutId !== null) {
 | 
				
			||||||
 | 
					                    window.clearTimeout(initialTimeoutId);
 | 
				
			||||||
 | 
					                    initialTimeoutId = null;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Handle content updates
 | 
				
			||||||
 | 
					            if (message.content) {
 | 
				
			||||||
 | 
					                receivedAnyContent = true;
 | 
				
			||||||
 | 
					                assistantResponse += message.content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Update the UI immediately
 | 
				
			||||||
 | 
					                onContentUpdate(assistantResponse);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Reset timeout since we got content
 | 
				
			||||||
 | 
					                if (timeoutId !== null) {
 | 
				
			||||||
 | 
					                    window.clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Set new timeout
 | 
				
			||||||
 | 
					                timeoutId = window.setTimeout(() => {
 | 
				
			||||||
 | 
					                    console.warn(`[${responseId}] Stream timeout for session ${sessionId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Clean up
 | 
				
			||||||
 | 
					                    cleanupEventListener(eventListener);
 | 
				
			||||||
 | 
					                    reject(new Error('Stream timeout'));
 | 
				
			||||||
 | 
					                }, 30000);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Handle tool execution updates
 | 
				
			||||||
 | 
					            if (message.toolExecution) {
 | 
				
			||||||
 | 
					                console.log(`[${responseId}] Received tool execution update: action=${message.toolExecution.action || 'unknown'}`);
 | 
				
			||||||
 | 
					                onToolExecution(message.toolExecution);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Handle thinking state updates
 | 
				
			||||||
 | 
					            if (message.thinking) {
 | 
				
			||||||
 | 
					                console.log(`[${responseId}] Received thinking update: ${message.thinking.substring(0, 50)}...`);
 | 
				
			||||||
 | 
					                onThinkingUpdate(message.thinking);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Handle completion
 | 
				
			||||||
 | 
					            if (message.done) {
 | 
				
			||||||
 | 
					                console.log(`[${responseId}] Stream completed for session ${sessionId}, 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) {
 | 
				
			||||||
 | 
					                    console.log(`[${responseId}] CONTENT IN DONE MESSAGE (first 200 chars): "${message.content.substring(0, 200)}..."`);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Clear timeout if set
 | 
				
			||||||
 | 
					                if (timeoutId !== null) {
 | 
				
			||||||
 | 
					                    window.clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					                    timeoutId = null;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Check if we have content in the done message
 | 
				
			||||||
 | 
					                if (message.content) {
 | 
				
			||||||
 | 
					                    console.log(`[${responseId}] Processing content in done message: ${message.content.length} chars`);
 | 
				
			||||||
 | 
					                    receivedAnyContent = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Replace current response if we didn't have content before or if it's empty
 | 
				
			||||||
 | 
					                    if (assistantResponse.length === 0) {
 | 
				
			||||||
 | 
					                        console.log(`[${responseId}] Using content from done message as full response`);
 | 
				
			||||||
 | 
					                        assistantResponse = message.content;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    // Otherwise append it if it's different
 | 
				
			||||||
 | 
					                    else if (message.content !== assistantResponse) {
 | 
				
			||||||
 | 
					                        console.log(`[${responseId}] Appending content from done message to existing response`);
 | 
				
			||||||
 | 
					                        assistantResponse += message.content;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    else {
 | 
				
			||||||
 | 
					                        console.log(`[${responseId}] Content in done message is identical to existing response, not appending`);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    onContentUpdate(assistantResponse);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Clean up and resolve
 | 
				
			||||||
 | 
					                cleanupEventListener(eventListener);
 | 
				
			||||||
 | 
					                onComplete();
 | 
				
			||||||
 | 
					                resolve();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Register event listener for the custom event
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            window.addEventListener('llm-stream-message', eventListener);
 | 
				
			||||||
 | 
					            console.log(`[${responseId}] Event listener added for llm-stream-message events`);
 | 
				
			||||||
 | 
					        } catch (err) {
 | 
				
			||||||
 | 
					            console.error(`[${responseId}] Error setting up event listener:`, err);
 | 
				
			||||||
 | 
					            reject(err);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Set initial timeout for receiving any message
 | 
				
			||||||
 | 
					        initialTimeoutId = window.setTimeout(() => {
 | 
				
			||||||
 | 
					            console.warn(`[${responseId}] No messages received for initial period in session ${sessionId}`);
 | 
				
			||||||
 | 
					            if (!receivedAnyMessage) {
 | 
				
			||||||
 | 
					                console.error(`[${responseId}] WebSocket connection not established for session ${sessionId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (timeoutId !== null) {
 | 
				
			||||||
 | 
					                    window.clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Clean up
 | 
				
			||||||
 | 
					                cleanupEventListener(eventListener);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Show error message to user
 | 
				
			||||||
 | 
					                reject(new Error('WebSocket connection not established'));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }, 10000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Send the streaming request to start the process
 | 
				
			||||||
 | 
					        console.log(`[${responseId}] Sending HTTP POST request to initiate streaming: /llm/sessions/${sessionId}/messages/stream`);
 | 
				
			||||||
 | 
					        server.post(`llm/sessions/${sessionId}/messages/stream`, {
 | 
				
			||||||
 | 
					            ...messageParams,
 | 
				
			||||||
 | 
					            stream: true // Explicitly indicate this is a streaming request
 | 
				
			||||||
 | 
					        }).catch(err => {
 | 
				
			||||||
 | 
					            console.error(`[${responseId}] HTTP error sending streaming request for session ${sessionId}:`, err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Clean up timeouts
 | 
				
			||||||
 | 
					            if (initialTimeoutId !== null) {
 | 
				
			||||||
 | 
					                window.clearTimeout(initialTimeoutId);
 | 
				
			||||||
 | 
					                initialTimeoutId = null;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (timeoutId !== null) {
 | 
				
			||||||
 | 
					                window.clearTimeout(timeoutId);
 | 
				
			||||||
 | 
					                timeoutId = null;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Clean up event listener
 | 
				
			||||||
 | 
					            cleanupEventListener(eventListener);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            reject(err);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Clean up an event listener
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function cleanupEventListener(listener: ((event: Event) => void) | null): void {
 | 
				
			||||||
 | 
					    if (listener) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            window.removeEventListener('llm-stream-message', listener);
 | 
				
			||||||
 | 
					            console.log(`Successfully removed event listener`);
 | 
				
			||||||
 | 
					        } catch (err) {
 | 
				
			||||||
 | 
					            console.error(`Error removing event listener:`, err);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Get a direct response from the server
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export async function getDirectResponse(sessionId: string, messageParams: any): Promise<any> {
 | 
				
			||||||
 | 
					    // Create a copy of the params without any streaming flags
 | 
				
			||||||
 | 
					    const postParams = {
 | 
				
			||||||
 | 
					        ...messageParams,
 | 
				
			||||||
 | 
					        stream: false  // Explicitly set to false to ensure we get a direct response
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    console.log(`Sending direct POST request for session ${sessionId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Send the message via POST request with the updated params
 | 
				
			||||||
 | 
					    return server.post<any>(`llm/sessions/${sessionId}/messages`, postParams);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Get embedding statistics
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export async function getEmbeddingStats(): Promise<any> {
 | 
				
			||||||
 | 
					    return server.get('llm/embeddings/stats');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										6
									
								
								src/public/app/widgets/llm_chat/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/public/app/widgets/llm_chat/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * LLM Chat Panel Widget Module
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import LlmChatPanel from './llm_chat_panel.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default LlmChatPanel;
 | 
				
			||||||
							
								
								
									
										682
									
								
								src/public/app/widgets/llm_chat/llm_chat_panel.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										682
									
								
								src/public/app/widgets/llm_chat/llm_chat_panel.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,682 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 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 libraryLoader from "../../services/library_loader.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { TPL, addMessageToChat, showSources, hideSources, showLoadingIndicator, hideLoadingIndicator, renderToolStepsHtml } from "./ui.js";
 | 
				
			||||||
 | 
					import { formatMarkdown } from "./utils.js";
 | 
				
			||||||
 | 
					import { createChatSession, checkSessionExists, setupStreamingResponse, getDirectResponse } from "./communication.js";
 | 
				
			||||||
 | 
					import { extractToolExecutionSteps, extractFinalResponse, 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 the LLM Chat CSS
 | 
				
			||||||
 | 
					(async function() {
 | 
				
			||||||
 | 
					    await libraryLoader.requireCss('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 sessionId: string | null = null;
 | 
				
			||||||
 | 
					    private currentNoteId: string | null = null;
 | 
				
			||||||
 | 
					    private _messageHandlerId: number | null = null;
 | 
				
			||||||
 | 
					    private _messageHandler: any = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Callbacks for data persistence
 | 
				
			||||||
 | 
					    private onSaveData: ((data: any) => Promise<void>) | null = null;
 | 
				
			||||||
 | 
					    private onGetData: (() => Promise<any>) | null = null;
 | 
				
			||||||
 | 
					    private messages: MessageData[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 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 getSessionId(): string | null {
 | 
				
			||||||
 | 
					        return this.sessionId;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public setSessionId(sessionId: string | null): void {
 | 
				
			||||||
 | 
					        this.sessionId = sessionId;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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<void>,
 | 
				
			||||||
 | 
					        getDataCallback: () => Promise<any>
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const dataToSave: ChatData = {
 | 
				
			||||||
 | 
					                messages: this.messages,
 | 
				
			||||||
 | 
					                sessionId: this.sessionId,
 | 
				
			||||||
 | 
					                toolSteps: toolSteps
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            console.log(`Saving chat data with sessionId: ${this.sessionId} and ${toolSteps.length} tool steps`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await this.onSaveData(dataToSave);
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            console.error('Failed to save chat data', error);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Load saved chat data from the note attribute
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async loadSavedData(): Promise<boolean> {
 | 
				
			||||||
 | 
					        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 session ID if available
 | 
				
			||||||
 | 
					                if (savedData.sessionId) {
 | 
				
			||||||
 | 
					                    try {
 | 
				
			||||||
 | 
					                        // Verify the session still exists
 | 
				
			||||||
 | 
					                        const sessionExists = await checkSessionExists(savedData.sessionId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (sessionExists) {
 | 
				
			||||||
 | 
					                            console.log(`Restored session ${savedData.sessionId}`);
 | 
				
			||||||
 | 
					                            this.sessionId = savedData.sessionId;
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            console.log(`Saved session ${savedData.sessionId} not found, will create new one`);
 | 
				
			||||||
 | 
					                            this.sessionId = null;
 | 
				
			||||||
 | 
					                            await this.createChatSession();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } catch (error) {
 | 
				
			||||||
 | 
					                        console.log(`Error checking saved session ${savedData.sessionId}, creating a new one`);
 | 
				
			||||||
 | 
					                        this.sessionId = null;
 | 
				
			||||||
 | 
					                        await this.createChatSession();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    // No saved session ID, create a new one
 | 
				
			||||||
 | 
					                    this.sessionId = null;
 | 
				
			||||||
 | 
					                    await this.createChatSession();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                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 = `
 | 
				
			||||||
 | 
					            <div class="tool-execution-container p-2 rounded mb-2">
 | 
				
			||||||
 | 
					                <div class="tool-execution-header d-flex align-items-center justify-content-between mb-2">
 | 
				
			||||||
 | 
					                    <div>
 | 
				
			||||||
 | 
					                        <i class="bx bx-code-block text-primary me-2"></i>
 | 
				
			||||||
 | 
					                        <span class="fw-bold">Tool Execution</span>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <button type="button" class="btn btn-sm btn-link p-0 text-muted tool-execution-chat-clear" title="Clear tool execution history">
 | 
				
			||||||
 | 
					                        <i class="bx bx-x"></i>
 | 
				
			||||||
 | 
					                    </button>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="tool-execution-chat-steps">
 | 
				
			||||||
 | 
					                    ${renderToolStepsHtml(steps)}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Add event listener for the clear button
 | 
				
			||||||
 | 
					        const clearButton = toolExecutionElement.querySelector('.tool-execution-chat-clear');
 | 
				
			||||||
 | 
					        if (clearButton) {
 | 
				
			||||||
 | 
					            clearButton.addEventListener('click', (e) => {
 | 
				
			||||||
 | 
					                e.preventDefault();
 | 
				
			||||||
 | 
					                e.stopPropagation();
 | 
				
			||||||
 | 
					                toolExecutionElement.remove();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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.sessionId = null;
 | 
				
			||||||
 | 
					            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.sessionId || !hasSavedData) {
 | 
				
			||||||
 | 
					            // Create a new chat session
 | 
				
			||||||
 | 
					            await this.createChatSession();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private async createChatSession() {
 | 
				
			||||||
 | 
					        // Check for validation issues first
 | 
				
			||||||
 | 
					        await validateEmbeddingProviders(this.validationWarning);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            const sessionId = await createChatSession();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (sessionId) {
 | 
				
			||||||
 | 
					                this.sessionId = sessionId;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            console.error('Failed to create 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;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Check for provider validation issues before sending
 | 
				
			||||||
 | 
					        await validateEmbeddingProviders(this.validationWarning);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Make sure we have a valid session
 | 
				
			||||||
 | 
					        if (!this.sessionId) {
 | 
				
			||||||
 | 
					            // If no session ID, create a new session
 | 
				
			||||||
 | 
					            await this.createChatSession();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!this.sessionId) {
 | 
				
			||||||
 | 
					                // If still no session ID, show error and return
 | 
				
			||||||
 | 
					                console.error("Failed to create chat session");
 | 
				
			||||||
 | 
					                toastService.showError("Failed to create chat session");
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Verify the session exists on the server
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                const sessionExists = await checkSessionExists(this.sessionId);
 | 
				
			||||||
 | 
					                if (!sessionExists) {
 | 
				
			||||||
 | 
					                    console.log(`Session ${this.sessionId} not found, creating a new one`);
 | 
				
			||||||
 | 
					                    await this.createChatSession();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } catch (error) {
 | 
				
			||||||
 | 
					                console.log(`Error checking session ${this.sessionId}, creating a new one`);
 | 
				
			||||||
 | 
					                await this.createChatSession();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Process the user message
 | 
				
			||||||
 | 
					        await this.processUserMessage(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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Add logging to verify parameters
 | 
				
			||||||
 | 
					            console.log(`Sending message with: useAdvancedContext=${useAdvancedContext}, showThinking=${showThinking}, noteId=${this.currentNoteId}, sessionId=${this.sessionId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // 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");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            this.handleError(error as Error);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Process a new user message - add to UI and save
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private async processUserMessage(content: string) {
 | 
				
			||||||
 | 
					        // Add user message to the chat UI
 | 
				
			||||||
 | 
					        this.addMessageToChat('user', content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Add to our local message array too
 | 
				
			||||||
 | 
					        this.messages.push({
 | 
				
			||||||
 | 
					            role: 'user',
 | 
				
			||||||
 | 
					            content,
 | 
				
			||||||
 | 
					            timestamp: new Date()
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Save to note
 | 
				
			||||||
 | 
					        this.saveCurrentData().catch(err => {
 | 
				
			||||||
 | 
					            console.error("Failed to save user message to note:", err);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Try to get a direct response from the server
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private async handleDirectResponse(messageParams: any): Promise<boolean> {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            if (!this.sessionId) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Get a direct response from the server
 | 
				
			||||||
 | 
					            const postResponse = await getDirectResponse(this.sessionId, messageParams);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // If the POST request returned content directly, display it
 | 
				
			||||||
 | 
					            if (postResponse && postResponse.content) {
 | 
				
			||||||
 | 
					                this.processAssistantResponse(postResponse.content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // If there are sources, show them
 | 
				
			||||||
 | 
					                if (postResponse.sources && postResponse.sources.length > 0) {
 | 
				
			||||||
 | 
					                    this.showSources(postResponse.sources);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                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) {
 | 
				
			||||||
 | 
					        // 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()
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 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<void> {
 | 
				
			||||||
 | 
					        if (!this.sessionId) {
 | 
				
			||||||
 | 
					            throw new Error("No session ID available");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return setupStreamingResponse(
 | 
				
			||||||
 | 
					            this.sessionId,
 | 
				
			||||||
 | 
					            messageParams,
 | 
				
			||||||
 | 
					            // Content update handler
 | 
				
			||||||
 | 
					            (content: string) => {
 | 
				
			||||||
 | 
					                this.updateStreamingUI(content);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            // Thinking update handler
 | 
				
			||||||
 | 
					            (thinking: string) => {
 | 
				
			||||||
 | 
					                this.showThinkingState(thinking);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            // Tool execution handler
 | 
				
			||||||
 | 
					            (toolData: any) => {
 | 
				
			||||||
 | 
					                this.showToolExecutionInfo(toolData);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            // Complete handler
 | 
				
			||||||
 | 
					            () => {
 | 
				
			||||||
 | 
					                hideLoadingIndicator(this.loadingIndicator);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            // Error handler
 | 
				
			||||||
 | 
					            (error: Error) => {
 | 
				
			||||||
 | 
					                this.handleError(error);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Update the UI with streaming content as it arrives
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private updateStreamingUI(assistantResponse: string) {
 | 
				
			||||||
 | 
					        const logId = `ui-update-${Date.now()}`;
 | 
				
			||||||
 | 
					        console.log(`[${logId}] Updating UI with response text: ${assistantResponse.length} chars`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!this.noteContextChatMessages) {
 | 
				
			||||||
 | 
					            console.error(`[${logId}] noteContextChatMessages element not available`);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Extract the tool execution steps and final response
 | 
				
			||||||
 | 
					        const toolSteps = extractToolExecutionSteps(assistantResponse);
 | 
				
			||||||
 | 
					        const finalResponseText = extractFinalResponse(assistantResponse);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Find existing assistant message or create one if needed
 | 
				
			||||||
 | 
					        let assistantElement = this.noteContextChatMessages.querySelector('.assistant-message:last-child .message-content');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // First, check if we need to add the tool execution steps to the chat flow
 | 
				
			||||||
 | 
					        if (toolSteps.length > 0) {
 | 
				
			||||||
 | 
					            // Look for an existing tool execution element in the chat flow
 | 
				
			||||||
 | 
					            let toolExecutionElement = this.noteContextChatMessages.querySelector('.chat-tool-execution');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!toolExecutionElement) {
 | 
				
			||||||
 | 
					                // Create a new tool execution element in the chat flow
 | 
				
			||||||
 | 
					                // Place it right before the assistant message if it exists, or at the end of chat
 | 
				
			||||||
 | 
					                toolExecutionElement = document.createElement('div');
 | 
				
			||||||
 | 
					                toolExecutionElement.className = 'chat-tool-execution mb-3';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // If there's an assistant message, insert before it
 | 
				
			||||||
 | 
					                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);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Update the tool execution content
 | 
				
			||||||
 | 
					            toolExecutionElement.innerHTML = `
 | 
				
			||||||
 | 
					                <div class="tool-execution-container p-2 rounded mb-2">
 | 
				
			||||||
 | 
					                    <div class="tool-execution-header d-flex align-items-center justify-content-between mb-2">
 | 
				
			||||||
 | 
					                        <div>
 | 
				
			||||||
 | 
					                            <i class="bx bx-code-block text-primary me-2"></i>
 | 
				
			||||||
 | 
					                            <span class="fw-bold">Tool Execution</span>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <button type="button" class="btn btn-sm btn-link p-0 text-muted tool-execution-chat-clear" title="Clear tool execution history">
 | 
				
			||||||
 | 
					                            <i class="bx bx-x"></i>
 | 
				
			||||||
 | 
					                        </button>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <div class="tool-execution-chat-steps">
 | 
				
			||||||
 | 
					                        ${renderToolStepsHtml(toolSteps)}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Add event listener for the clear button
 | 
				
			||||||
 | 
					            const clearButton = toolExecutionElement.querySelector('.tool-execution-chat-clear');
 | 
				
			||||||
 | 
					            if (clearButton) {
 | 
				
			||||||
 | 
					                clearButton.addEventListener('click', (e) => {
 | 
				
			||||||
 | 
					                    e.preventDefault();
 | 
				
			||||||
 | 
					                    e.stopPropagation();
 | 
				
			||||||
 | 
					                    toolExecutionElement?.remove();
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Now update or create the assistant message with the final response
 | 
				
			||||||
 | 
					        if (finalResponseText) {
 | 
				
			||||||
 | 
					            if (assistantElement) {
 | 
				
			||||||
 | 
					                console.log(`[${logId}] Found existing assistant message element, updating with final response`);
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    // Format the final response with markdown
 | 
				
			||||||
 | 
					                    const formattedResponse = formatMarkdown(finalResponseText);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Update the content
 | 
				
			||||||
 | 
					                    assistantElement.innerHTML = formattedResponse || '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Apply syntax highlighting to any code blocks in the updated content
 | 
				
			||||||
 | 
					                    applySyntaxHighlight($(assistantElement as HTMLElement));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    console.log(`[${logId}] Successfully updated existing element with final response`);
 | 
				
			||||||
 | 
					                } catch (err) {
 | 
				
			||||||
 | 
					                    console.error(`[${logId}] Error updating existing element:`, err);
 | 
				
			||||||
 | 
					                    // Fallback to text content if HTML update fails
 | 
				
			||||||
 | 
					                    try {
 | 
				
			||||||
 | 
					                        assistantElement.textContent = finalResponseText;
 | 
				
			||||||
 | 
					                        console.log(`[${logId}] Fallback to text content successful`);
 | 
				
			||||||
 | 
					                    } catch (fallbackErr) {
 | 
				
			||||||
 | 
					                        console.error(`[${logId}] Even fallback update failed:`, fallbackErr);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                console.log(`[${logId}] No existing assistant message element found, creating new one`);
 | 
				
			||||||
 | 
					                // Create a new message in the chat
 | 
				
			||||||
 | 
					                this.addMessageToChat('assistant', finalResponseText);
 | 
				
			||||||
 | 
					                console.log(`[${logId}] Successfully added new assistant message`);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Always try to scroll to the latest content
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            if (this.chatContainer) {
 | 
				
			||||||
 | 
					                this.chatContainer.scrollTop = this.chatContainer.scrollHeight;
 | 
				
			||||||
 | 
					                console.log(`[${logId}] Scrolled to latest content`);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (scrollErr) {
 | 
				
			||||||
 | 
					            console.error(`[${logId}] Error scrolling to latest content:`, scrollErr);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Show tool execution information in the UI
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private showToolExecutionInfo(toolExecutionData: any) {
 | 
				
			||||||
 | 
					        console.log(`Showing tool execution info: ${JSON.stringify(toolExecutionData)}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // We'll update the in-chat tool execution area in the updateStreamingUI method
 | 
				
			||||||
 | 
					        // This method is now just a hook for the WebSocket handlers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Make sure the loading indicator is shown during tool execution
 | 
				
			||||||
 | 
					        this.loadingIndicator.style.display = 'flex';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 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'));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										119
									
								
								src/public/app/widgets/llm_chat/message_processor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/public/app/widgets/llm_chat/message_processor.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Message processing functions for LLM Chat
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import type { ToolExecutionStep } from "./types.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Extract tool execution steps from the response
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function extractToolExecutionSteps(content: string): ToolExecutionStep[] {
 | 
				
			||||||
 | 
					    if (!content) return [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const steps: ToolExecutionStep[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check for executing tools marker
 | 
				
			||||||
 | 
					    if (content.includes('[Executing tools...]')) {
 | 
				
			||||||
 | 
					        steps.push({
 | 
				
			||||||
 | 
					            type: 'executing',
 | 
				
			||||||
 | 
					            content: 'Executing tools...'
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Extract tool results with regex
 | 
				
			||||||
 | 
					    const toolResultRegex = /\[Tool: ([^\]]+)\]([\s\S]*?)(?=\[|$)/g;
 | 
				
			||||||
 | 
					    let match;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    while ((match = toolResultRegex.exec(content)) !== null) {
 | 
				
			||||||
 | 
					        const toolName = match[1];
 | 
				
			||||||
 | 
					        const toolContent = match[2].trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        steps.push({
 | 
				
			||||||
 | 
					            type: toolContent.includes('Error:') ? 'error' : 'result',
 | 
				
			||||||
 | 
					            name: toolName,
 | 
				
			||||||
 | 
					            content: toolContent
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check for generating response marker
 | 
				
			||||||
 | 
					    if (content.includes('[Generating response with tool results...]')) {
 | 
				
			||||||
 | 
					        steps.push({
 | 
				
			||||||
 | 
					            type: 'generating',
 | 
				
			||||||
 | 
					            content: 'Generating response with tool results...'
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return steps;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Extract the final response without tool execution steps
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function extractFinalResponse(content: string): string {
 | 
				
			||||||
 | 
					    if (!content) return '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Remove all tool execution markers and their content
 | 
				
			||||||
 | 
					    let finalResponse = content
 | 
				
			||||||
 | 
					        .replace(/\[Executing tools\.\.\.\]\n*/g, '')
 | 
				
			||||||
 | 
					        .replace(/\[Tool: [^\]]+\][\s\S]*?(?=\[|$)/g, '')
 | 
				
			||||||
 | 
					        .replace(/\[Generating response with tool results\.\.\.\]\n*/g, '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Trim any extra whitespace
 | 
				
			||||||
 | 
					    finalResponse = finalResponse.trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return finalResponse;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Extract tool execution steps from the DOM that are within the chat flow
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function extractInChatToolSteps(chatMessagesElement: HTMLElement): ToolExecutionStep[] {
 | 
				
			||||||
 | 
					    const steps: ToolExecutionStep[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Look for tool execution in the chat flow
 | 
				
			||||||
 | 
					    const toolExecutionElement = chatMessagesElement.querySelector('.chat-tool-execution');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (toolExecutionElement) {
 | 
				
			||||||
 | 
					        // Find all tool step elements
 | 
				
			||||||
 | 
					        const stepElements = toolExecutionElement.querySelectorAll('.tool-step');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        stepElements.forEach(stepEl => {
 | 
				
			||||||
 | 
					            const stepHtml = stepEl.innerHTML;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Determine the step type based on icons or classes present
 | 
				
			||||||
 | 
					            let type = 'info';
 | 
				
			||||||
 | 
					            let name: string | undefined;
 | 
				
			||||||
 | 
					            let content = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (stepHtml.includes('bx-code-block')) {
 | 
				
			||||||
 | 
					                type = 'executing';
 | 
				
			||||||
 | 
					                content = 'Executing tools...';
 | 
				
			||||||
 | 
					            } else if (stepHtml.includes('bx-terminal')) {
 | 
				
			||||||
 | 
					                type = 'result';
 | 
				
			||||||
 | 
					                // Extract the tool name from the step
 | 
				
			||||||
 | 
					                const nameMatch = stepHtml.match(/<span[^>]*>Tool: ([^<]+)<\/span>/);
 | 
				
			||||||
 | 
					                name = nameMatch ? nameMatch[1] : 'unknown';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Extract the content from the div with class mt-1 ps-3
 | 
				
			||||||
 | 
					                const contentEl = stepEl.querySelector('.mt-1.ps-3');
 | 
				
			||||||
 | 
					                content = contentEl ? contentEl.innerHTML : '';
 | 
				
			||||||
 | 
					            } else if (stepHtml.includes('bx-error-circle')) {
 | 
				
			||||||
 | 
					                type = 'error';
 | 
				
			||||||
 | 
					                const nameMatch = stepHtml.match(/<span[^>]*>Tool: ([^<]+)<\/span>/);
 | 
				
			||||||
 | 
					                name = nameMatch ? nameMatch[1] : 'unknown';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const contentEl = stepEl.querySelector('.mt-1.ps-3.text-danger');
 | 
				
			||||||
 | 
					                content = contentEl ? contentEl.innerHTML : '';
 | 
				
			||||||
 | 
					            } else if (stepHtml.includes('bx-message-dots')) {
 | 
				
			||||||
 | 
					                type = 'generating';
 | 
				
			||||||
 | 
					                content = 'Generating response with tool results...';
 | 
				
			||||||
 | 
					            } else if (stepHtml.includes('bx-loader-alt')) {
 | 
				
			||||||
 | 
					                // Skip the initializing spinner
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            steps.push({ type, name, content });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return steps;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								src/public/app/widgets/llm_chat/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/public/app/widgets/llm_chat/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Types for LLM Chat Panel
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ChatResponse {
 | 
				
			||||||
 | 
					    id: string;
 | 
				
			||||||
 | 
					    messages: Array<{role: string; content: string}>;
 | 
				
			||||||
 | 
					    sources?: Array<{noteId: string; title: string}>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface SessionResponse {
 | 
				
			||||||
 | 
					    id: string;
 | 
				
			||||||
 | 
					    title: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ToolExecutionStep {
 | 
				
			||||||
 | 
					    type: string;
 | 
				
			||||||
 | 
					    name?: string;
 | 
				
			||||||
 | 
					    content: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface MessageData {
 | 
				
			||||||
 | 
					    role: string;
 | 
				
			||||||
 | 
					    content: string;
 | 
				
			||||||
 | 
					    timestamp?: Date;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ChatData {
 | 
				
			||||||
 | 
					    messages: MessageData[];
 | 
				
			||||||
 | 
					    sessionId: string | null;
 | 
				
			||||||
 | 
					    toolSteps: ToolExecutionStep[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										251
									
								
								src/public/app/widgets/llm_chat/ui.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								src/public/app/widgets/llm_chat/ui.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,251 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * UI-related functions for LLM Chat
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import { t } from "../../services/i18n.js";
 | 
				
			||||||
 | 
					import type { ToolExecutionStep } from "./types.js";
 | 
				
			||||||
 | 
					import { formatMarkdown, applyHighlighting } from "./utils.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Template for the chat widget
 | 
				
			||||||
 | 
					export const TPL = `
 | 
				
			||||||
 | 
					<div class="note-context-chat h-100 w-100 d-flex flex-column">
 | 
				
			||||||
 | 
					    <!-- Move validation warning outside the card with better styling -->
 | 
				
			||||||
 | 
					    <div class="provider-validation-warning alert alert-warning m-2 border-left border-warning" style="display: none; padding-left: 15px; border-left: 4px solid #ffc107; background-color: rgba(255, 248, 230, 0.9); font-size: 0.9rem; box-shadow: 0 2px 5px rgba(0,0,0,0.05);"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="note-context-chat-container flex-grow-1 overflow-auto p-3">
 | 
				
			||||||
 | 
					        <div class="note-context-chat-messages"></div>
 | 
				
			||||||
 | 
					        <div class="loading-indicator" style="display: none;">
 | 
				
			||||||
 | 
					            <div class="spinner-border spinner-border-sm text-primary" role="status">
 | 
				
			||||||
 | 
					                <span class="visually-hidden">Loading...</span>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <span class="ms-2">${t('ai_llm.agent.processing')}</span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="sources-container p-2 border-top" style="display: none;">
 | 
				
			||||||
 | 
					        <h6 class="m-0 p-1 d-flex align-items-center">
 | 
				
			||||||
 | 
					            <i class="bx bx-link-alt me-1"></i> ${t('ai_llm.sources')}
 | 
				
			||||||
 | 
					            <span class="badge bg-primary rounded-pill ms-2 sources-count"></span>
 | 
				
			||||||
 | 
					        </h6>
 | 
				
			||||||
 | 
					        <div class="sources-list mt-2"></div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <form class="note-context-chat-form d-flex flex-column border-top p-2">
 | 
				
			||||||
 | 
					        <div class="d-flex chat-input-container mb-2">
 | 
				
			||||||
 | 
					            <textarea
 | 
				
			||||||
 | 
					                class="form-control note-context-chat-input"
 | 
				
			||||||
 | 
					                placeholder="${t('ai_llm.enter_message')}"
 | 
				
			||||||
 | 
					                rows="2"
 | 
				
			||||||
 | 
					            ></textarea>
 | 
				
			||||||
 | 
					            <button type="submit" class="btn btn-primary note-context-chat-send-button ms-2 d-flex align-items-center justify-content-center">
 | 
				
			||||||
 | 
					                <i class="bx bx-send"></i>
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div class="d-flex align-items-center context-option-container mt-1 justify-content-end">
 | 
				
			||||||
 | 
					            <small class="text-muted me-auto fst-italic">Options:</small>
 | 
				
			||||||
 | 
					            <div class="form-check form-switch me-3 small">
 | 
				
			||||||
 | 
					                <input class="form-check-input use-advanced-context-checkbox" type="checkbox" id="useEnhancedContext" checked>
 | 
				
			||||||
 | 
					                <label class="form-check-label small" for="useEnhancedContext" title="${t('ai.enhanced_context_description')}">
 | 
				
			||||||
 | 
					                    ${t('ai_llm.use_enhanced_context')}
 | 
				
			||||||
 | 
					                    <i class="bx bx-info-circle small text-muted"></i>
 | 
				
			||||||
 | 
					                </label>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="form-check form-switch small">
 | 
				
			||||||
 | 
					                <input class="form-check-input show-thinking-checkbox" type="checkbox" id="showThinking">
 | 
				
			||||||
 | 
					                <label class="form-check-label small" for="showThinking" title="${t('ai.show_thinking_description')}">
 | 
				
			||||||
 | 
					                    ${t('ai_llm.show_thinking')}
 | 
				
			||||||
 | 
					                    <i class="bx bx-info-circle small text-muted"></i>
 | 
				
			||||||
 | 
					                </label>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Add a message to the chat UI
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function addMessageToChat(messagesContainer: HTMLElement, chatContainer: HTMLElement, role: 'user' | 'assistant', content: string) {
 | 
				
			||||||
 | 
					    const messageElement = document.createElement('div');
 | 
				
			||||||
 | 
					    messageElement.className = `chat-message ${role}-message mb-3 d-flex`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const avatarElement = document.createElement('div');
 | 
				
			||||||
 | 
					    avatarElement.className = 'message-avatar d-flex align-items-center justify-content-center me-2';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (role === 'user') {
 | 
				
			||||||
 | 
					        avatarElement.innerHTML = '<i class="bx bx-user"></i>';
 | 
				
			||||||
 | 
					        avatarElement.classList.add('user-avatar');
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        avatarElement.innerHTML = '<i class="bx bx-bot"></i>';
 | 
				
			||||||
 | 
					        avatarElement.classList.add('assistant-avatar');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const contentElement = document.createElement('div');
 | 
				
			||||||
 | 
					    contentElement.className = 'message-content p-3 rounded flex-grow-1';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (role === 'user') {
 | 
				
			||||||
 | 
					        contentElement.classList.add('user-content', 'bg-light');
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        contentElement.classList.add('assistant-content');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Format the content with markdown
 | 
				
			||||||
 | 
					    contentElement.innerHTML = formatMarkdown(content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    messageElement.appendChild(avatarElement);
 | 
				
			||||||
 | 
					    messageElement.appendChild(contentElement);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    messagesContainer.appendChild(messageElement);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Apply syntax highlighting to any code blocks in the message
 | 
				
			||||||
 | 
					    applyHighlighting(contentElement);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Scroll to bottom
 | 
				
			||||||
 | 
					    chatContainer.scrollTop = chatContainer.scrollHeight;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Show sources in the UI
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function showSources(
 | 
				
			||||||
 | 
					    sourcesList: HTMLElement,
 | 
				
			||||||
 | 
					    sourcesContainer: HTMLElement,
 | 
				
			||||||
 | 
					    sourcesCount: HTMLElement,
 | 
				
			||||||
 | 
					    sources: Array<{noteId: string, title: string}>,
 | 
				
			||||||
 | 
					    onSourceClick: (noteId: string) => void
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    sourcesList.innerHTML = '';
 | 
				
			||||||
 | 
					    sourcesCount.textContent = sources.length.toString();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sources.forEach(source => {
 | 
				
			||||||
 | 
					        const sourceElement = document.createElement('div');
 | 
				
			||||||
 | 
					        sourceElement.className = 'source-item p-2 mb-1 border rounded d-flex align-items-center';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Create the direct link to the note
 | 
				
			||||||
 | 
					        sourceElement.innerHTML = `
 | 
				
			||||||
 | 
					            <div class="d-flex align-items-center w-100">
 | 
				
			||||||
 | 
					                <a href="#root/${source.noteId}"
 | 
				
			||||||
 | 
					                   data-note-id="${source.noteId}"
 | 
				
			||||||
 | 
					                   class="source-link text-truncate d-flex align-items-center"
 | 
				
			||||||
 | 
					                   title="Open note: ${source.title}">
 | 
				
			||||||
 | 
					                    <i class="bx bx-file-blank me-1"></i>
 | 
				
			||||||
 | 
					                    <span class="source-title">${source.title}</span>
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					            </div>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Add click handler
 | 
				
			||||||
 | 
					        sourceElement.querySelector('.source-link')?.addEventListener('click', (e) => {
 | 
				
			||||||
 | 
					            e.preventDefault();
 | 
				
			||||||
 | 
					            e.stopPropagation();
 | 
				
			||||||
 | 
					            onSourceClick(source.noteId);
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sourcesList.appendChild(sourceElement);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sourcesContainer.style.display = 'block';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Hide sources in the UI
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function hideSources(sourcesContainer: HTMLElement) {
 | 
				
			||||||
 | 
					    sourcesContainer.style.display = 'none';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Show loading indicator
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function showLoadingIndicator(loadingIndicator: HTMLElement) {
 | 
				
			||||||
 | 
					    const logId = `ui-${Date.now()}`;
 | 
				
			||||||
 | 
					    console.log(`[${logId}] Showing loading indicator`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        loadingIndicator.style.display = 'flex';
 | 
				
			||||||
 | 
					        const forceUpdate = loadingIndicator.offsetHeight;
 | 
				
			||||||
 | 
					        console.log(`[${logId}] Loading indicator initialized`);
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					        console.error(`[${logId}] Error showing loading indicator:`, err);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Hide loading indicator
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function hideLoadingIndicator(loadingIndicator: HTMLElement) {
 | 
				
			||||||
 | 
					    const logId = `ui-${Date.now()}`;
 | 
				
			||||||
 | 
					    console.log(`[${logId}] Hiding loading indicator`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        loadingIndicator.style.display = 'none';
 | 
				
			||||||
 | 
					        const forceUpdate = loadingIndicator.offsetHeight;
 | 
				
			||||||
 | 
					        console.log(`[${logId}] Loading indicator hidden`);
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					        console.error(`[${logId}] Error hiding loading indicator:`, err);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Render tool steps as HTML for display in chat
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function renderToolStepsHtml(steps: ToolExecutionStep[]): string {
 | 
				
			||||||
 | 
					    if (!steps || steps.length === 0) return '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let html = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    steps.forEach(step => {
 | 
				
			||||||
 | 
					        let icon, labelClass, content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        switch (step.type) {
 | 
				
			||||||
 | 
					            case 'executing':
 | 
				
			||||||
 | 
					                icon = 'bx-code-block text-primary';
 | 
				
			||||||
 | 
					                labelClass = '';
 | 
				
			||||||
 | 
					                content = `<div class="d-flex align-items-center">
 | 
				
			||||||
 | 
					                    <i class="bx ${icon} me-1"></i>
 | 
				
			||||||
 | 
					                    <span>${step.content}</span>
 | 
				
			||||||
 | 
					                </div>`;
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            case 'result':
 | 
				
			||||||
 | 
					                icon = 'bx-terminal text-success';
 | 
				
			||||||
 | 
					                labelClass = 'fw-bold';
 | 
				
			||||||
 | 
					                content = `<div class="d-flex align-items-center">
 | 
				
			||||||
 | 
					                    <i class="bx ${icon} me-1"></i>
 | 
				
			||||||
 | 
					                    <span class="${labelClass}">Tool: ${step.name || 'unknown'}</span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="mt-1 ps-3">${step.content}</div>`;
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            case 'error':
 | 
				
			||||||
 | 
					                icon = 'bx-error-circle text-danger';
 | 
				
			||||||
 | 
					                labelClass = 'fw-bold text-danger';
 | 
				
			||||||
 | 
					                content = `<div class="d-flex align-items-center">
 | 
				
			||||||
 | 
					                    <i class="bx ${icon} me-1"></i>
 | 
				
			||||||
 | 
					                    <span class="${labelClass}">Tool: ${step.name || 'unknown'}</span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="mt-1 ps-3 text-danger">${step.content}</div>`;
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            case 'generating':
 | 
				
			||||||
 | 
					                icon = 'bx-message-dots text-info';
 | 
				
			||||||
 | 
					                labelClass = '';
 | 
				
			||||||
 | 
					                content = `<div class="d-flex align-items-center">
 | 
				
			||||||
 | 
					                    <i class="bx ${icon} me-1"></i>
 | 
				
			||||||
 | 
					                    <span>${step.content}</span>
 | 
				
			||||||
 | 
					                </div>`;
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                icon = 'bx-info-circle text-muted';
 | 
				
			||||||
 | 
					                labelClass = '';
 | 
				
			||||||
 | 
					                content = `<div class="d-flex align-items-center">
 | 
				
			||||||
 | 
					                    <i class="bx ${icon} me-1"></i>
 | 
				
			||||||
 | 
					                    <span>${step.content}</span>
 | 
				
			||||||
 | 
					                </div>`;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        html += `<div class="tool-step my-1">${content}</div>`;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return html;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										93
									
								
								src/public/app/widgets/llm_chat/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/public/app/widgets/llm_chat/utils.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,93 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Utility functions for LLM Chat
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import { marked } from "marked";
 | 
				
			||||||
 | 
					import { applySyntaxHighlight } from "../../services/syntax_highlight.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Format markdown content for display
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function formatMarkdown(content: string): string {
 | 
				
			||||||
 | 
					    if (!content) return '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // First, extract HTML thinking visualization to protect it from replacements
 | 
				
			||||||
 | 
					    const thinkingBlocks: string[] = [];
 | 
				
			||||||
 | 
					    let processedContent = content.replace(/<div class=['"](thinking-process|reasoning-process)['"][\s\S]*?<\/div>/g, (match) => {
 | 
				
			||||||
 | 
					        const placeholder = `__THINKING_BLOCK_${thinkingBlocks.length}__`;
 | 
				
			||||||
 | 
					        thinkingBlocks.push(match);
 | 
				
			||||||
 | 
					        return placeholder;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Use marked library to parse the markdown
 | 
				
			||||||
 | 
					    const markedContent = marked(processedContent, {
 | 
				
			||||||
 | 
					        breaks: true,   // Convert line breaks to <br>
 | 
				
			||||||
 | 
					        gfm: true,      // Enable GitHub Flavored Markdown
 | 
				
			||||||
 | 
					        silent: true    // Ignore errors
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Handle potential promise (though it shouldn't be with our options)
 | 
				
			||||||
 | 
					    if (typeof markedContent === 'string') {
 | 
				
			||||||
 | 
					        processedContent = markedContent;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        console.warn('Marked returned a promise unexpectedly');
 | 
				
			||||||
 | 
					        // Use the original content as fallback
 | 
				
			||||||
 | 
					        processedContent = content;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Restore thinking visualization blocks
 | 
				
			||||||
 | 
					    thinkingBlocks.forEach((block, index) => {
 | 
				
			||||||
 | 
					        processedContent = processedContent.replace(`__THINKING_BLOCK_${index}__`, block);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return processedContent;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Simple HTML escaping for safer content display
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function escapeHtml(text: string): string {
 | 
				
			||||||
 | 
					    if (typeof text !== 'string') {
 | 
				
			||||||
 | 
					        text = String(text || '');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return text
 | 
				
			||||||
 | 
					        .replace(/&/g, '&')
 | 
				
			||||||
 | 
					        .replace(/</g, '<')
 | 
				
			||||||
 | 
					        .replace(/>/g, '>')
 | 
				
			||||||
 | 
					        .replace(/"/g, '"')
 | 
				
			||||||
 | 
					        .replace(/'/g, ''');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Apply syntax highlighting to content
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function applyHighlighting(element: HTMLElement): void {
 | 
				
			||||||
 | 
					    applySyntaxHighlight($(element));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Format tool arguments for display
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function formatToolArgs(args: any): string {
 | 
				
			||||||
 | 
					    if (!args || typeof args !== 'object') return '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Object.entries(args)
 | 
				
			||||||
 | 
					        .map(([key, value]) => {
 | 
				
			||||||
 | 
					            // Format the value based on its type
 | 
				
			||||||
 | 
					            let displayValue;
 | 
				
			||||||
 | 
					            if (typeof value === 'string') {
 | 
				
			||||||
 | 
					                displayValue = value.length > 50 ? `"${value.substring(0, 47)}..."` : `"${value}"`;
 | 
				
			||||||
 | 
					            } else if (value === null) {
 | 
				
			||||||
 | 
					                displayValue = 'null';
 | 
				
			||||||
 | 
					            } else if (Array.isArray(value)) {
 | 
				
			||||||
 | 
					                displayValue = '[...]'; // Simplified array representation
 | 
				
			||||||
 | 
					            } else if (typeof value === 'object') {
 | 
				
			||||||
 | 
					                displayValue = '{...}'; // Simplified object representation
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                displayValue = String(value);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return `<span class="text-primary">${escapeHtml(key)}</span>: ${escapeHtml(displayValue)}`;
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .join(', ');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										104
									
								
								src/public/app/widgets/llm_chat/validation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/public/app/widgets/llm_chat/validation.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,104 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Validation functions for LLM Chat
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					import options from "../../services/options.js";
 | 
				
			||||||
 | 
					import { getEmbeddingStats } from "./communication.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Validate embedding providers configuration
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export async function validateEmbeddingProviders(validationWarning: HTMLElement): Promise<void> {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        // Check if AI is enabled
 | 
				
			||||||
 | 
					        const aiEnabled = options.is('aiEnabled');
 | 
				
			||||||
 | 
					        if (!aiEnabled) {
 | 
				
			||||||
 | 
					            validationWarning.style.display = 'none';
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get provider precedence
 | 
				
			||||||
 | 
					        const precedenceStr = options.get('aiProviderPrecedence') || 'openai,anthropic,ollama';
 | 
				
			||||||
 | 
					        let precedenceList: string[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (precedenceStr) {
 | 
				
			||||||
 | 
					            if (precedenceStr.startsWith('[') && precedenceStr.endsWith(']')) {
 | 
				
			||||||
 | 
					                precedenceList = JSON.parse(precedenceStr);
 | 
				
			||||||
 | 
					            } else if (precedenceStr.includes(',')) {
 | 
				
			||||||
 | 
					                precedenceList = precedenceStr.split(',').map(p => p.trim());
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                precedenceList = [precedenceStr];
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get enabled providers - this is a simplification since we don't have direct DB access
 | 
				
			||||||
 | 
					        // We'll determine enabled status based on the presence of keys or settings
 | 
				
			||||||
 | 
					        const enabledProviders: string[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // OpenAI is enabled if API key is set
 | 
				
			||||||
 | 
					        const openaiKey = options.get('openaiApiKey');
 | 
				
			||||||
 | 
					        if (openaiKey) {
 | 
				
			||||||
 | 
					            enabledProviders.push('openai');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Anthropic is enabled if API key is set
 | 
				
			||||||
 | 
					        const anthropicKey = options.get('anthropicApiKey');
 | 
				
			||||||
 | 
					        if (anthropicKey) {
 | 
				
			||||||
 | 
					            enabledProviders.push('anthropic');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Ollama is enabled if base URL is set
 | 
				
			||||||
 | 
					        const ollamaBaseUrl = options.get('ollamaBaseUrl');
 | 
				
			||||||
 | 
					        if (ollamaBaseUrl) {
 | 
				
			||||||
 | 
					            enabledProviders.push('ollama');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Local is always available
 | 
				
			||||||
 | 
					        enabledProviders.push('local');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Perform validation checks
 | 
				
			||||||
 | 
					        const allPrecedenceEnabled = precedenceList.every((p: string) => enabledProviders.includes(p));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Get embedding queue status
 | 
				
			||||||
 | 
					        const embeddingStats = await getEmbeddingStats() as {
 | 
				
			||||||
 | 
					            success: boolean,
 | 
				
			||||||
 | 
					            stats: {
 | 
				
			||||||
 | 
					                totalNotesCount: number;
 | 
				
			||||||
 | 
					                embeddedNotesCount: number;
 | 
				
			||||||
 | 
					                queuedNotesCount: number;
 | 
				
			||||||
 | 
					                failedNotesCount: number;
 | 
				
			||||||
 | 
					                lastProcessedDate: string | null;
 | 
				
			||||||
 | 
					                percentComplete: number;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        const queuedNotes = embeddingStats?.stats?.queuedNotesCount || 0;
 | 
				
			||||||
 | 
					        const hasEmbeddingsInQueue = queuedNotes > 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Show warning if there are issues
 | 
				
			||||||
 | 
					        if (!allPrecedenceEnabled || hasEmbeddingsInQueue) {
 | 
				
			||||||
 | 
					            let message = '<i class="bx bx-error-circle me-2"></i><strong>AI Provider Configuration Issues</strong>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            message += '<ul class="mb-1 ps-4">';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!allPrecedenceEnabled) {
 | 
				
			||||||
 | 
					                const disabledProviders = precedenceList.filter((p: string) => !enabledProviders.includes(p));
 | 
				
			||||||
 | 
					                message += `<li>The following providers in your precedence list are not enabled: ${disabledProviders.join(', ')}.</li>`;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (hasEmbeddingsInQueue) {
 | 
				
			||||||
 | 
					                message += `<li>Currently processing embeddings for ${queuedNotes} notes. Some AI features may produce incomplete results until processing completes.</li>`;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            message += '</ul>';
 | 
				
			||||||
 | 
					            message += '<div class="mt-2"><a href="javascript:" class="settings-link btn btn-sm btn-outline-secondary"><i class="bx bx-cog me-1"></i>Open AI Settings</a></div>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Update HTML content
 | 
				
			||||||
 | 
					            validationWarning.innerHTML = message;
 | 
				
			||||||
 | 
					            validationWarning.style.display = 'block';
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            validationWarning.style.display = 'none';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					        console.error('Error validating embedding providers:', error);
 | 
				
			||||||
 | 
					        validationWarning.style.display = 'none';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user