/** * 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 = `
Options:
`; /** * 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 = ''; avatarElement.classList.add('user-avatar'); } else { avatarElement.innerHTML = ''; 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 = `
${source.title}
`; // 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 = `
${step.content}
`; break; case 'result': icon = 'bx-terminal text-success'; labelClass = 'fw-bold'; content = `
Tool: ${step.name || 'unknown'}
${step.content}
`; break; case 'error': icon = 'bx-error-circle text-danger'; labelClass = 'fw-bold text-danger'; content = `
Tool: ${step.name || 'unknown'}
${step.content}
`; break; case 'generating': icon = 'bx-message-dots text-info'; labelClass = ''; content = `
${step.content}
`; break; default: icon = 'bx-info-circle text-muted'; labelClass = ''; content = `
${step.content}
`; } html += `
${content}
`; }); return html; }