2025-03-09 02:19:26 +00:00
import BasicWidget from "./basic_widget.js" ;
import toastService from "../services/toast.js" ;
import server from "../services/server.js" ;
import appContext from "../components/app_context.js" ;
import utils from "../services/utils.js" ;
import { t } from "../services/i18n.js" ;
2025-03-10 17:34:31 +00:00
import libraryLoader from "../services/library_loader.js" ;
2025-03-10 23:09:15 +00:00
import { applySyntaxHighlight } from "../services/syntax_highlight.js" ;
2025-03-17 16:33:30 +00:00
import options from "../services/options.js" ;
2025-03-28 23:46:50 +00:00
import { marked } from "marked" ;
2025-03-10 17:34:31 +00:00
// Import the LLM Chat CSS
( async function ( ) {
await libraryLoader . requireCss ( 'stylesheets/llm_chat.css' ) ;
} ) ( ) ;
2025-03-09 02:19:26 +00:00
2025-03-28 21:19:59 +00:00
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.processing.common' ) } . . . < / 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.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.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.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.show_thinking' ) }
< i class = "bx bx-info-circle small text-muted" > < / i >
< / label >
< / div >
< / div >
< / form >
< / div >
` ;
2025-03-09 02:19:26 +00:00
interface ChatResponse {
id : string ;
messages : Array < { role : string ; content : string } > ;
sources? : Array < { noteId : string ; title : string } > ;
}
interface SessionResponse {
id : string ;
title : string ;
}
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 ;
2025-03-10 03:34:48 +00:00
private useAdvancedContextCheckbox ! : HTMLInputElement ;
2025-03-19 18:49:14 +00:00
private showThinkingCheckbox ! : HTMLInputElement ;
2025-03-17 16:33:30 +00:00
private validationWarning ! : HTMLElement ;
2025-03-09 02:19:26 +00:00
private sessionId : string | null = null ;
private currentNoteId : string | null = null ;
2025-03-30 19:32:38 +00:00
2025-03-29 21:55:37 +00:00
// Callbacks for data persistence
private onSaveData : ( ( data : any ) = > Promise < void > ) | null = null ;
private onGetData : ( ( ) = > Promise < any > ) | null = null ;
private messages : Array < { role : string ; content : string ; timestamp? : Date } > = [ ] ;
2025-03-09 02:19:26 +00:00
2025-03-30 19:56:09 +00:00
// 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 ( ) : Array < { role : string ; content : string ; timestamp? : Date } > {
return this . messages ;
}
public setMessages ( messages : Array < { role : string ; content : string ; timestamp? : Date } > ) : 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 = '' ;
}
2025-03-09 02:19:26 +00:00
doRender() {
2025-03-28 21:19:59 +00:00
this . $widget = $ ( TPL ) ;
2025-03-09 02:19:26 +00:00
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 ;
2025-03-10 03:34:48 +00:00
this . useAdvancedContextCheckbox = element . querySelector ( '.use-advanced-context-checkbox' ) as HTMLInputElement ;
2025-03-19 18:49:14 +00:00
this . showThinkingCheckbox = element . querySelector ( '.show-thinking-checkbox' ) as HTMLInputElement ;
2025-03-17 16:33:30 +00:00
this . validationWarning = element . querySelector ( '.provider-validation-warning' ) as HTMLElement ;
2025-03-09 02:19:26 +00:00
2025-03-17 17:16:18 +00:00
// 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' ;
}
} ) ;
2025-03-09 02:19:26 +00:00
this . initializeEventListeners ( ) ;
2025-03-29 22:11:07 +00:00
// Don't create a session here - wait for refresh
// This prevents the wrong session from being created for the wrong note
2025-03-30 19:32:38 +00:00
2025-03-09 02:19:26 +00:00
return this . $widget ;
}
2025-03-29 21:55:37 +00:00
/ * *
* Set the callbacks for data persistence
* /
setDataCallbacks (
saveDataCallback : ( data : any ) = > Promise < void > ,
getDataCallback : ( ) = > Promise < any >
) {
this . onSaveData = saveDataCallback ;
this . onGetData = getDataCallback ;
}
2025-03-30 19:32:38 +00:00
2025-03-29 21:55:37 +00:00
/ * *
* Load saved chat data from the note
* /
async loadSavedData() {
if ( ! this . onGetData ) {
console . log ( "No getData callback available" ) ;
return ;
}
2025-03-30 19:32:38 +00:00
2025-03-29 21:55:37 +00:00
try {
const data = await this . onGetData ( ) ;
2025-03-29 22:11:07 +00:00
console . log ( ` Loading chat data for noteId: ${ this . currentNoteId } ` , data ) ;
2025-03-30 19:32:38 +00:00
2025-03-29 22:11:07 +00:00
// Make sure we're loading data for the correct note
if ( data && data . noteId && data . noteId !== this . currentNoteId ) {
console . warn ( ` Data noteId ${ data . noteId } doesn't match current noteId ${ this . currentNoteId } ` ) ;
}
2025-03-30 19:32:38 +00:00
2025-03-29 21:55:37 +00:00
if ( data && data . messages && Array . isArray ( data . messages ) ) {
// Clear existing messages in the UI
this . noteContextChatMessages . innerHTML = '' ;
this . messages = [ ] ;
2025-03-30 19:32:38 +00:00
2025-03-29 21:55:37 +00:00
// Add each message to the UI
data . messages . forEach ( ( message : { role : string ; content : string } ) = > {
if ( message . role === 'user' || message . role === 'assistant' ) {
this . addMessageToChat ( message . role , message . content ) ;
// Track messages in our local array too
this . messages . push ( message ) ;
}
} ) ;
2025-03-30 19:32:38 +00:00
2025-03-29 21:55:37 +00:00
// Scroll to bottom
this . chatContainer . scrollTop = this . chatContainer . scrollHeight ;
2025-03-29 22:11:07 +00:00
console . log ( ` Successfully loaded ${ data . messages . length } messages for noteId: ${ this . currentNoteId } ` ) ;
2025-03-30 19:32:38 +00:00
2025-03-29 21:55:37 +00:00
return true ;
}
} catch ( e ) {
2025-03-29 22:11:07 +00:00
console . error ( ` Error loading saved chat data for noteId: ${ this . currentNoteId } : ` , e ) ;
2025-03-29 21:55:37 +00:00
}
2025-03-30 19:32:38 +00:00
2025-03-29 21:55:37 +00:00
return false ;
}
2025-03-30 19:32:38 +00:00
2025-03-29 21:55:37 +00:00
/ * *
* Save the current chat data to the note
* /
async saveCurrentData() {
if ( ! this . onSaveData ) {
console . log ( "No saveData callback available" ) ;
return ;
}
2025-03-30 19:32:38 +00:00
2025-03-29 21:55:37 +00:00
try {
2025-03-29 22:11:07 +00:00
// Include the current note ID for tracking purposes
2025-03-29 21:55:37 +00:00
await this . onSaveData ( {
messages : this.messages ,
2025-03-29 22:11:07 +00:00
lastUpdated : new Date ( ) ,
noteId : this.currentNoteId // Include the note ID to help with debugging
2025-03-29 21:55:37 +00:00
} ) ;
2025-03-29 22:11:07 +00:00
console . log ( ` Saved chat data for noteId: ${ this . currentNoteId } with ${ this . messages . length } messages ` ) ;
2025-03-29 21:55:37 +00:00
return true ;
} catch ( e ) {
2025-03-29 22:11:07 +00:00
console . error ( ` Error saving chat data for noteId: ${ this . currentNoteId } : ` , e ) ;
2025-03-29 21:55:37 +00:00
return false ;
}
}
2025-03-09 02:19:26 +00:00
async refresh() {
if ( ! this . isVisible ( ) ) {
return ;
}
2025-03-17 16:33:30 +00:00
// Check for any provider validation issues when refreshing
await this . validateEmbeddingProviders ( ) ;
2025-03-09 02:19:26 +00:00
// Get current note context if needed
2025-03-29 22:11:07 +00:00
const currentActiveNoteId = appContext . tabManager . getActiveContext ( ) ? . note ? . noteId || null ;
2025-03-30 19:32:38 +00:00
2025-03-29 22:11:07 +00:00
// 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 ` ) ;
2025-03-30 19:32:38 +00:00
2025-03-29 22:11:07 +00:00
// Reset the UI and data
this . noteContextChatMessages . innerHTML = '' ;
this . messages = [ ] ;
this . sessionId = null ;
this . hideSources ( ) ; // Hide any sources from previous note
2025-03-30 19:32:38 +00:00
2025-03-29 22:11:07 +00:00
// Update our current noteId
this . currentNoteId = currentActiveNoteId ;
}
2025-03-30 19:32:38 +00:00
2025-03-29 22:11:07 +00:00
// Always try to load saved data for the current note
2025-03-29 21:55:37 +00:00
const hasSavedData = await this . loadSavedData ( ) ;
2025-03-30 19:32:38 +00:00
2025-03-29 22:11:07 +00:00
// Only create a new session if we don't have a session or saved data
2025-03-29 21:55:37 +00:00
if ( ! this . sessionId || ! hasSavedData ) {
2025-03-09 02:19:26 +00:00
// Create a new chat session
await this . createChatSession ( ) ;
}
}
private async createChatSession() {
2025-03-17 16:33:30 +00:00
// Check for validation issues first
await this . validateEmbeddingProviders ( ) ;
2025-03-09 02:19:26 +00:00
try {
const resp = await server . post < SessionResponse > ( 'llm/sessions' , {
title : 'Note Chat'
} ) ;
if ( resp && resp . id ) {
this . sessionId = resp . id ;
}
} catch ( error ) {
console . error ( 'Failed to create chat session:' , error ) ;
toastService . showError ( 'Failed to create chat session' ) ;
}
}
2025-03-30 19:32:38 +00:00
/ * *
* Handle sending a user message to the LLM service
* /
2025-03-09 02:19:26 +00:00
private async sendMessage ( content : string ) {
if ( ! content . trim ( ) || ! this . sessionId ) {
return ;
}
2025-03-17 16:33:30 +00:00
// Check for provider validation issues before sending
await this . validateEmbeddingProviders ( ) ;
2025-03-30 19:32:38 +00:00
// Process the user message
await this . processUserMessage ( content ) ;
// Clear input and show loading state
2025-03-10 03:34:48 +00:00
this . noteContextChatInput . value = '' ;
2025-03-09 02:19:26 +00:00
this . showLoadingIndicator ( ) ;
2025-03-10 03:34:48 +00:00
this . hideSources ( ) ;
2025-03-09 02:19:26 +00:00
try {
2025-03-10 03:34:48 +00:00
const useAdvancedContext = this . useAdvancedContextCheckbox . checked ;
2025-03-19 18:49:14 +00:00
const showThinking = this . showThinkingCheckbox . checked ;
// Add logging to verify parameters
console . log ( ` Sending message with: useAdvancedContext= ${ useAdvancedContext } , showThinking= ${ showThinking } , noteId= ${ this . currentNoteId } ` ) ;
2025-03-10 03:34:48 +00:00
2025-03-10 04:28:56 +00:00
// Create the message parameters
const messageParams = {
content ,
contextNoteId : this.currentNoteId ,
2025-03-19 18:49:14 +00:00
useAdvancedContext ,
showThinking
2025-03-10 04:28:56 +00:00
} ;
2025-03-30 19:32:38 +00:00
// First try to get a direct response
const handled = await this . handleDirectResponse ( messageParams ) ;
if ( handled ) return ;
2025-03-10 05:06:33 +00:00
2025-03-30 19:32:38 +00:00
// If no direct response, set up streaming
await this . setupStreamingResponse ( messageParams ) ;
} catch ( error ) {
this . handleError ( error as Error ) ;
}
}
2025-03-10 05:06:33 +00:00
2025-03-30 19:32:38 +00:00
/ * *
* 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
* @returns true if response was handled , false if streaming should be used
* /
private async handleDirectResponse ( messageParams : any ) : Promise < boolean > {
// Send the message via POST request
const postResponse = await server . post < any > ( ` llm/sessions/ ${ this . sessionId } /messages ` , 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 ) ;
2025-03-10 05:06:33 +00:00
}
2025-03-10 04:28:56 +00:00
2025-03-30 19:32:38 +00:00
this . hideLoadingIndicator ( ) ;
return true ;
}
2025-03-10 05:06:33 +00:00
2025-03-30 19:32:38 +00:00
return false ;
}
2025-03-09 02:19:26 +00:00
2025-03-30 19:32:38 +00:00
/ * *
* 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 ) ;
2025-03-10 05:06:33 +00:00
2025-03-30 19:32:38 +00:00
// Add to our local message array too
this . messages . push ( {
role : 'assistant' ,
content ,
timestamp : new Date ( )
} ) ;
2025-03-09 02:19:26 +00:00
2025-03-30 19:32:38 +00:00
// Save to note
this . saveCurrentData ( ) . catch ( err = > {
console . error ( "Failed to save assistant response to note:" , err ) ;
} ) ;
}
/ * *
* Set up streaming response from the server
* /
private async setupStreamingResponse ( messageParams : any ) {
const useAdvancedContext = messageParams . useAdvancedContext ;
const showThinking = messageParams . showThinking ;
// Set up streaming via EventSource
const streamUrl = ` ./api/llm/sessions/ ${ this . sessionId } /messages?format=stream&useAdvancedContext= ${ useAdvancedContext } &showThinking= ${ showThinking } ` ;
const source = new EventSource ( streamUrl ) ;
let assistantResponse = '' ;
let receivedAnyContent = false ;
let timeoutId : number | null = null ;
// Set up timeout for streaming response
timeoutId = this . setupStreamingTimeout ( source ) ;
// Handle streaming response
source . onmessage = ( event ) = > this . handleStreamingMessage (
event ,
source ,
timeoutId ,
assistantResponse ,
receivedAnyContent
) ;
// Handle streaming errors
source . onerror = ( ) = > this . handleStreamingError (
source ,
timeoutId ,
receivedAnyContent
) ;
}
/ * *
* Set up timeout for streaming response
* @returns Timeout ID for the created timeout
* /
private setupStreamingTimeout ( source : EventSource ) : number {
// Set a timeout to handle case where streaming doesn't work properly
return window . setTimeout ( ( ) = > {
// If we haven't received any content after a reasonable timeout (10 seconds),
// add a fallback message and close the stream
this . hideLoadingIndicator ( ) ;
const errorMessage = 'I\'m having trouble generating a response right now. Please try again later.' ;
this . processAssistantResponse ( errorMessage ) ;
source . close ( ) ;
} , 10000 ) ;
}
/ * *
* Handle messages from the streaming response
* /
private handleStreamingMessage (
event : MessageEvent ,
source : EventSource ,
timeoutId : number | null ,
assistantResponse : string ,
receivedAnyContent : boolean
) {
if ( event . data === '[DONE]' ) {
this . handleStreamingComplete ( source , timeoutId , receivedAnyContent , assistantResponse ) ;
return ;
}
try {
const data = JSON . parse ( event . data ) ;
console . log ( "Received streaming data:" , data ) ; // Debug log
// Handle both content and error cases
if ( data . content ) {
receivedAnyContent = true ;
assistantResponse += data . content ;
// Update the UI with the accumulated response
this . updateStreamingUI ( assistantResponse ) ;
} else if ( data . error ) {
// Handle error message
2025-03-10 03:34:48 +00:00
this . hideLoadingIndicator ( ) ;
2025-03-30 19:32:38 +00:00
this . addMessageToChat ( 'assistant' , ` Error: ${ data . error } ` ) ;
receivedAnyContent = true ;
source . close ( ) ;
2025-03-10 05:06:33 +00:00
if ( timeoutId !== null ) {
window . clearTimeout ( timeoutId ) ;
}
2025-03-30 19:32:38 +00:00
}
2025-03-10 05:06:33 +00:00
2025-03-30 19:32:38 +00:00
// Scroll to the bottom
this . chatContainer . scrollTop = this . chatContainer . scrollHeight ;
} catch ( e ) {
console . error ( 'Error parsing SSE message:' , e , 'Raw data:' , event . data ) ;
}
}
2025-03-09 02:19:26 +00:00
2025-03-30 19:32:38 +00:00
/ * *
* Update the UI with streaming content as it arrives
* /
private updateStreamingUI ( assistantResponse : string ) {
const assistantElement = this . noteContextChatMessages . querySelector ( '.assistant-message:last-child .message-content' ) ;
if ( assistantElement ) {
assistantElement . innerHTML = this . formatMarkdown ( assistantResponse ) ;
// Apply syntax highlighting to any code blocks in the updated content
applySyntaxHighlight ( $ ( assistantElement as HTMLElement ) ) ;
} else {
this . addMessageToChat ( 'assistant' , assistantResponse ) ;
}
}
/ * *
* Handle completion of streaming response
* /
private handleStreamingComplete (
source : EventSource ,
timeoutId : number | null ,
receivedAnyContent : boolean ,
assistantResponse : string
) {
// Stream completed
source . close ( ) ;
this . hideLoadingIndicator ( ) ;
// Clear the timeout since we're done
if ( timeoutId !== null ) {
window . clearTimeout ( timeoutId ) ;
}
// If we didn't receive any content but the stream completed normally,
// display a message to the user
if ( ! receivedAnyContent ) {
const defaultMessage = 'I processed your request, but I don\'t have any specific information to share at the moment.' ;
this . processAssistantResponse ( defaultMessage ) ;
} else if ( assistantResponse ) {
// Save the completed streaming response to the message array
this . messages . push ( {
role : 'assistant' ,
content : assistantResponse ,
timestamp : new Date ( )
} ) ;
// Save to note
this . saveCurrentData ( ) . catch ( err = > {
console . error ( "Failed to save assistant response to note:" , err ) ;
} ) ;
2025-03-09 02:19:26 +00:00
}
}
2025-03-30 19:32:38 +00:00
/ * *
* Handle errors during streaming response
* /
private handleStreamingError (
source : EventSource ,
timeoutId : number | null ,
receivedAnyContent : boolean
) {
source . close ( ) ;
this . hideLoadingIndicator ( ) ;
// Clear the timeout if there was an error
if ( timeoutId !== null ) {
window . clearTimeout ( timeoutId ) ;
}
// Only show error message if we haven't received any content yet
if ( ! receivedAnyContent ) {
const connectionError = 'Error connecting to the LLM service. Please try again.' ;
this . processAssistantResponse ( connectionError ) ;
}
}
/ * *
* Handle general errors in the send message flow
* /
private handleError ( error : Error ) {
this . hideLoadingIndicator ( ) ;
toastService . showError ( 'Error sending message: ' + error . message ) ;
}
2025-03-09 02:19:26 +00:00
private addMessageToChat ( role : 'user' | 'assistant' , content : string ) {
const messageElement = document . createElement ( 'div' ) ;
2025-03-10 17:34:31 +00:00
messageElement . className = ` chat-message ${ role } -message mb-3 d-flex ` ;
2025-03-09 02:19:26 +00:00
const avatarElement = document . createElement ( 'div' ) ;
2025-03-10 17:34:31 +00:00
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' ) ;
}
2025-03-09 02:19:26 +00:00
const contentElement = document . createElement ( 'div' ) ;
2025-03-10 17:34:31 +00:00
contentElement . className = 'message-content p-3 rounded flex-grow-1' ;
2025-03-09 02:19:26 +00:00
2025-03-10 17:34:31 +00:00
if ( role === 'user' ) {
contentElement . classList . add ( 'user-content' , 'bg-light' ) ;
} else {
contentElement . classList . add ( 'assistant-content' ) ;
}
2025-03-09 02:19:26 +00:00
2025-03-10 17:34:31 +00:00
// Format the content with markdown
contentElement . innerHTML = this . formatMarkdown ( content ) ;
2025-03-09 02:19:26 +00:00
messageElement . appendChild ( avatarElement ) ;
messageElement . appendChild ( contentElement ) ;
this . noteContextChatMessages . appendChild ( messageElement ) ;
2025-03-10 23:09:15 +00:00
// Apply syntax highlighting to any code blocks in the message
applySyntaxHighlight ( $ ( contentElement ) ) ;
2025-03-09 02:19:26 +00:00
// Scroll to bottom
this . chatContainer . scrollTop = this . chatContainer . scrollHeight ;
}
private showSources ( sources : Array < { noteId : string , title : string } > ) {
this . sourcesList . innerHTML = '' ;
2025-03-10 05:27:27 +00:00
// Update the sources count
const sourcesCount = this . $widget [ 0 ] . querySelector ( '.sources-count' ) as HTMLElement ;
if ( sourcesCount ) {
sourcesCount . textContent = sources . length . toString ( ) ;
}
2025-03-09 02:19:26 +00:00
sources . forEach ( source = > {
const sourceElement = document . createElement ( 'div' ) ;
2025-03-10 17:34:31 +00:00
sourceElement . className = 'source-item p-2 mb-1 border rounded d-flex align-items-center' ;
2025-03-10 05:27:27 +00:00
2025-03-10 05:52:33 +00:00
// Create the direct link to the note
2025-03-10 05:27:27 +00:00
sourceElement . innerHTML = `
2025-03-10 17:34:31 +00:00
< div class = "d-flex align-items-center w-100" >
2025-03-10 05:27:27 +00:00
< a href = "#root/${source.noteId}"
data - note - id = "${source.noteId}"
2025-03-10 17:34:31 +00:00
class = "source-link text-truncate d-flex align-items-center"
2025-03-10 05:27:27 +00:00
title = "Open note: ${source.title}" >
2025-03-10 17:34:31 +00:00
< i class = "bx bx-file-blank me-1" > < / i >
< span class = "source-title" > $ { source . title } < / span >
2025-03-10 05:27:27 +00:00
< / a >
2025-03-10 05:52:33 +00:00
< / div > ` ;
2025-03-09 02:19:26 +00:00
2025-03-10 05:52:33 +00:00
// Add click handler for better user experience
2025-03-09 02:19:26 +00:00
sourceElement . querySelector ( '.source-link' ) ? . addEventListener ( 'click' , ( e ) = > {
e . preventDefault ( ) ;
2025-03-10 05:57:16 +00:00
e . stopPropagation ( ) ;
// Open the note in a new tab but don't switch to it
appContext . tabManager . openTabWithNoteWithHoisting ( source . noteId , { activate : false } ) ;
return false ; // Additional measure to prevent the event from bubbling up
2025-03-09 02:19:26 +00:00
} ) ;
this . sourcesList . appendChild ( sourceElement ) ;
} ) ;
const sourcesContainer = this . $widget [ 0 ] . querySelector ( '.sources-container' ) as HTMLElement ;
if ( sourcesContainer ) {
sourcesContainer . style . display = 'block' ;
}
}
private hideSources() {
const sourcesContainer = this . $widget [ 0 ] . querySelector ( '.sources-container' ) as HTMLElement ;
if ( sourcesContainer ) {
sourcesContainer . style . display = 'none' ;
}
}
private showLoadingIndicator() {
this . loadingIndicator . style . display = 'flex' ;
}
private hideLoadingIndicator() {
this . loadingIndicator . style . display = 'none' ;
}
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' ) ) ;
}
} ) ;
}
2025-03-10 03:34:48 +00:00
/ * *
* Format markdown content for display
* /
private formatMarkdown ( content : string ) : string {
2025-03-10 23:09:15 +00:00
if ( ! content ) return '' ;
2025-03-19 18:49:14 +00:00
// 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 ;
} ) ;
2025-03-28 23:46:50 +00:00
// 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
2025-03-10 23:09:15 +00:00
} ) ;
2025-03-28 23:46:50 +00:00
// 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 ;
}
2025-03-10 23:09:15 +00:00
2025-03-19 18:49:14 +00:00
// Restore thinking visualization blocks
thinkingBlocks . forEach ( ( block , index ) = > {
processedContent = processedContent . replace ( ` __THINKING_BLOCK_ ${ index } __ ` , block ) ;
} ) ;
2025-03-10 23:09:15 +00:00
return processedContent ;
2025-03-10 03:34:48 +00:00
}
2025-03-17 16:33:30 +00:00
/ * *
* Validate embedding providers configuration
* Check if there are issues with the embedding providers that might affect LLM functionality
* /
async validateEmbeddingProviders() {
try {
// Check if AI is enabled
const aiEnabled = options . is ( 'aiEnabled' ) ;
if ( ! aiEnabled ) {
this . validationWarning . style . display = 'none' ;
return ;
}
// Get the default embedding provider
const defaultProvider = options . get ( 'embeddingsDefaultProvider' ) || 'openai' ;
// 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 the setting is true
const ollamaEnabled = options . is ( 'ollamaEnabled' ) ;
if ( ollamaEnabled ) {
enabledProviders . push ( 'ollama' ) ;
}
// Local is always available
enabledProviders . push ( 'local' ) ;
// Perform validation checks
const defaultInPrecedence = precedenceList . includes ( defaultProvider ) ;
const defaultIsEnabled = enabledProviders . includes ( defaultProvider ) ;
const allPrecedenceEnabled = precedenceList . every ( ( p : string ) = > enabledProviders . includes ( p ) ) ;
2025-03-20 22:05:10 +00:00
// Get embedding queue status
const embeddingStats = await server . get ( 'embeddings/stats' ) 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 ;
2025-03-17 16:33:30 +00:00
// Show warning if there are issues
2025-03-20 22:05:10 +00:00
if ( ! defaultInPrecedence || ! defaultIsEnabled || ! allPrecedenceEnabled || hasEmbeddingsInQueue ) {
2025-03-17 17:16:18 +00:00
let message = '<i class="bx bx-error-circle me-2"></i><strong>AI Provider Configuration Issues</strong>' ;
message += '<ul class="mb-1 ps-4">' ;
2025-03-17 16:33:30 +00:00
if ( ! defaultInPrecedence ) {
2025-03-17 17:16:18 +00:00
message += ` <li>The default embedding provider " ${ defaultProvider } " is not in your provider precedence list.</li> ` ;
2025-03-17 16:33:30 +00:00
}
if ( ! defaultIsEnabled ) {
2025-03-17 17:16:18 +00:00
message += ` <li>The default embedding provider " ${ defaultProvider } " is not enabled.</li> ` ;
2025-03-17 16:33:30 +00:00
}
if ( ! allPrecedenceEnabled ) {
const disabledProviders = precedenceList . filter ( ( p : string ) = > ! enabledProviders . includes ( p ) ) ;
2025-03-17 17:16:18 +00:00
message += ` <li>The following providers in your precedence list are not enabled: ${ disabledProviders . join ( ', ' ) } .</li> ` ;
2025-03-17 16:33:30 +00:00
}
2025-03-20 22:05:10 +00:00
if ( hasEmbeddingsInQueue ) {
message += ` <li>Currently processing embeddings for ${ queuedNotes } notes. Some AI features may produce incomplete results until processing completes.</li> ` ;
}
2025-03-17 17:16:18 +00:00
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>' ;
2025-03-17 16:33:30 +00:00
2025-03-17 17:16:18 +00:00
// Update HTML content - no need to attach event listeners here anymore
2025-03-17 16:33:30 +00:00
this . validationWarning . innerHTML = message ;
this . validationWarning . style . display = 'block' ;
} else {
this . validationWarning . style . display = 'none' ;
}
} catch ( error ) {
console . error ( 'Error validating embedding providers:' , error ) ;
this . validationWarning . style . display = 'none' ;
}
}
2025-03-09 02:19:26 +00:00
}