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 ;
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 ( ) ;
// Create a session when first loaded
this . createChatSession ( ) ;
return this . $widget ;
}
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
this . currentNoteId = appContext . tabManager . getActiveContext ( ) ? . note ? . noteId || null ;
if ( ! this . sessionId ) {
// 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' ) ;
}
}
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 ( ) ;
// Add user message to the chat
2025-03-10 03:34:48 +00:00
this . addMessageToChat ( 'user' , content ) ;
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
} ;
// First, send the message via POST request
2025-03-10 05:06:33 +00:00
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 . addMessageToChat ( 'assistant' , postResponse . content ) ;
// If there are sources, show them
if ( postResponse . sources && postResponse . sources . length > 0 ) {
this . showSources ( postResponse . sources ) ;
}
this . hideLoadingIndicator ( ) ;
return ;
}
2025-03-10 04:28:56 +00:00
// Then set up streaming via EventSource
2025-03-19 18:49:14 +00:00
const streamUrl = ` ./api/llm/sessions/ ${ this . sessionId } /messages?format=stream&useAdvancedContext= ${ useAdvancedContext } &showThinking= ${ showThinking } ` ;
2025-03-10 04:28:56 +00:00
const source = new EventSource ( streamUrl ) ;
2025-03-10 03:34:48 +00:00
let assistantResponse = '' ;
2025-03-10 05:06:33 +00:00
let receivedAnyContent = false ;
let timeoutId : number | null = null ;
// Set a timeout to handle case where streaming doesn't work properly
timeoutId = window . setTimeout ( ( ) = > {
if ( ! receivedAnyContent ) {
// If we haven't received any content after a reasonable timeout (10 seconds),
// add a fallback message and close the stream
this . hideLoadingIndicator ( ) ;
this . addMessageToChat ( 'assistant' , 'I\'m having trouble generating a response right now. Please try again later.' ) ;
source . close ( ) ;
}
} , 10000 ) ;
2025-03-10 03:34:48 +00:00
// Handle streaming response
source . onmessage = ( event ) = > {
if ( event . data === '[DONE]' ) {
// Stream completed
source . close ( ) ;
this . hideLoadingIndicator ( ) ;
2025-03-10 05:06:33 +00:00
// 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 ) {
this . addMessageToChat ( 'assistant' , 'I processed your request, but I don\'t have any specific information to share at the moment.' ) ;
}
2025-03-10 03:34:48 +00:00
return ;
}
2025-03-09 02:19:26 +00:00
2025-03-10 03:34:48 +00:00
try {
const data = JSON . parse ( event . data ) ;
2025-03-10 05:06:33 +00:00
console . log ( "Received streaming data:" , data ) ; // Debug log
// Handle both content and error cases
2025-03-10 03:34:48 +00:00
if ( data . content ) {
2025-03-10 05:06:33 +00:00
receivedAnyContent = true ;
2025-03-10 03:34:48 +00:00
assistantResponse += data . content ;
2025-03-10 05:06:33 +00:00
2025-03-10 03:34:48 +00:00
// Update the UI with the accumulated response
const assistantElement = this . noteContextChatMessages . querySelector ( '.assistant-message:last-child .message-content' ) ;
if ( assistantElement ) {
assistantElement . innerHTML = this . formatMarkdown ( assistantResponse ) ;
2025-03-10 23:09:15 +00:00
// Apply syntax highlighting to any code blocks in the updated content
applySyntaxHighlight ( $ ( assistantElement as HTMLElement ) ) ;
2025-03-10 03:34:48 +00:00
} else {
this . addMessageToChat ( 'assistant' , assistantResponse ) ;
}
2025-03-10 05:06:33 +00:00
} else if ( data . error ) {
// Handle error message
this . hideLoadingIndicator ( ) ;
this . addMessageToChat ( 'assistant' , ` Error: ${ data . error } ` ) ;
receivedAnyContent = true ;
source . close ( ) ;
if ( timeoutId !== null ) {
window . clearTimeout ( timeoutId ) ;
}
2025-03-10 03:34:48 +00:00
}
2025-03-10 05:06:33 +00:00
// Scroll to the bottom
this . chatContainer . scrollTop = this . chatContainer . scrollHeight ;
2025-03-10 03:34:48 +00:00
} catch ( e ) {
2025-03-10 05:06:33 +00:00
console . error ( 'Error parsing SSE message:' , e , 'Raw data:' , event . data ) ;
2025-03-09 02:19:26 +00:00
}
2025-03-10 03:34:48 +00:00
} ;
2025-03-09 02:19:26 +00:00
2025-03-10 03:34:48 +00:00
source . onerror = ( ) = > {
source . close ( ) ;
this . hideLoadingIndicator ( ) ;
2025-03-10 05:06:33 +00:00
// 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 ) {
this . addMessageToChat ( 'assistant' , 'Error connecting to the LLM service. Please try again.' ) ;
}
2025-03-10 03:34:48 +00:00
} ;
2025-03-09 02:19:26 +00:00
} catch ( error ) {
this . hideLoadingIndicator ( ) ;
2025-03-10 03:34:48 +00:00
toastService . showError ( 'Error sending message: ' + ( error as 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
}