voyageai-cli
Version:
CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search
776 lines (672 loc) • 28.2 kB
JavaScript
/**
* Knowledge Base UI Manager
* Handles wizard, panel sections, file list, and config interactions
*/
class KBUIManager {
constructor(kbManager) {
this.kbManager = kbManager;
this.isWizardOpen = false;
this.isPanelOpen = true;
this.currentStep = 'select'; // 'select' | 'add-docs'
this.sectionStates = this.loadSectionStates();
this.init();
}
init() {
this.loadInitialKB();
this.setupWizardEvents();
this.setupPanelEvents();
this.setupFileDropZone();
this.initSectionStates();
this.setupInstructionsSync();
}
// ── Section collapse/expand ──
loadSectionStates() {
try {
const saved = localStorage.getItem('__vai_kb_sections');
return saved ? JSON.parse(saved) : {
stats: true,
files: true,
upload: true,
instructions: false,
config: false
};
} catch {
return { stats: true, files: true, upload: true, instructions: false, config: false };
}
}
saveSectionStates() {
localStorage.setItem('__vai_kb_sections', JSON.stringify(this.sectionStates));
}
toggleSection(key) {
this.sectionStates[key] = !this.sectionStates[key];
this.saveSectionStates();
const header = document.querySelector(`.kb-section[data-kb-section="${key}"] .kb-section-header`);
const body = document.querySelector(`.kb-section-body[data-kb-section="${key}"]`);
if (!header || !body) return;
const isExpanding = this.sectionStates[key];
header.classList.toggle('expanded', isExpanding);
if (isExpanding) {
body.style.maxHeight = body.scrollHeight + 'px';
setTimeout(() => { body.style.maxHeight = 'none'; }, 250);
} else {
body.style.maxHeight = body.scrollHeight + 'px';
requestAnimationFrame(() => { body.style.maxHeight = '0px'; });
}
}
initSectionStates() {
for (const [key, expanded] of Object.entries(this.sectionStates)) {
const header = document.querySelector(`.kb-section[data-kb-section="${key}"] .kb-section-header`);
const body = document.querySelector(`.kb-section-body[data-kb-section="${key}"]`);
if (header && body) {
header.classList.toggle('expanded', expanded);
body.style.maxHeight = expanded ? 'none' : '0px';
}
}
}
// ── Instructions sync ──
setupInstructionsSync() {
const textarea = document.getElementById('kbSystemPrompt');
if (!textarea) return;
textarea.addEventListener('input', () => {
if (typeof saveChatSettingsDebounced === 'function') {
saveChatSettingsDebounced();
}
});
}
// ── Files list ──
async updateFilesList() {
const kbName = this.kbManager.currentKB;
if (!kbName) return;
const fileList = document.getElementById('kbFileList');
const fileCount = document.getElementById('kbFileCount');
if (!fileList) return;
try {
const docs = await this.kbManager.listDocs(kbName);
if (fileCount) fileCount.textContent = docs.length;
if (docs.length === 0) {
fileList.innerHTML = '<li class="kb-file-empty">No documents ingested</li>';
return;
}
fileList.innerHTML = docs.map(doc => {
const sizeStr = doc.size > 1024
? (doc.size / 1024).toFixed(1) + ' KB'
: (doc.size || 0) + ' B';
const safeName = this.escapeHtml(doc.fileName);
return `<li class="kb-file-item" data-filename="${safeName}">
<svg class="kb-file-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
</svg>
<div class="kb-file-info">
<div class="kb-file-name" title="${safeName}">${safeName}</div>
<div class="kb-file-meta">
<span>${doc.chunkCount} chunks</span>
<span>${sizeStr}</span>
</div>
</div>
<button class="kb-file-delete" onclick="window.kbUI.removeFile('${safeName.replace(/'/g, "\\'")}')" title="Remove">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/>
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
</button>
</li>`;
}).join('');
} catch (err) {
console.error('Error loading files list:', err);
fileList.innerHTML = '<li class="kb-file-empty">Error loading documents</li>';
}
}
async removeFile(fileName) {
if (!this.kbManager.currentKB) return;
if (!confirm(`Remove "${fileName}" from the knowledge base?`)) return;
try {
await this.kbManager.removeDocByName(this.kbManager.currentKB, fileName);
await this.updatePanelUI();
} catch (err) {
console.error('Error removing file:', err);
alert(`Error: ${err.message}`);
}
}
escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// ── Rename KB ──
startRename() {
const badge = document.getElementById('kbPanelName');
if (!badge || !this.kbManager.currentKB) return;
// Show the current display name (not internal name) for editing
const currentDisplay = this._currentDisplayName || this.kbManager.currentKB;
const input = document.createElement('input');
input.type = 'text';
input.className = 'kb-name-input';
input.value = currentDisplay;
input.maxLength = 80;
// Replace badge text with input
badge.textContent = '';
badge.appendChild(input);
badge.classList.remove('kb-name-editable');
input.focus();
input.select();
const commit = () => this.commitRename(input, badge, currentDisplay);
input.addEventListener('blur', commit, { once: true });
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); input.blur(); }
if (e.key === 'Escape') { input.value = currentDisplay; input.blur(); }
});
}
async commitRename(input, badge, previousDisplay) {
const newDisplay = input.value.trim();
badge.classList.add('kb-name-editable');
// No change or empty — revert
if (!newDisplay || newDisplay === previousDisplay) {
badge.textContent = previousDisplay;
return;
}
badge.textContent = 'Saving…';
try {
const result = await this.kbManager.renameKB(this.kbManager.currentKB, newDisplay);
this._currentDisplayName = result.displayName;
badge.textContent = result.displayName;
} catch (err) {
console.error('Rename failed:', err);
badge.textContent = previousDisplay;
alert(`Rename failed: ${err.message}`);
}
}
// ── Index status feedback ──
showIndexStatus(status) {
const el = document.getElementById('kbIndexStatus');
if (!el) return;
if (status === 'created') {
el.innerHTML = '<span style="color: var(--warning-color, #f59e0b);">⏳ Vector index creating…</span>';
el.title = 'Atlas vector search index was just created. It may take 1-2 minutes to become active.';
// Poll until active (Atlas indexes build asynchronously)
this.pollIndexReady();
} else if (status === 'exists') {
el.innerHTML = '<span style="color: var(--success-color, #22c55e);">✓ Index ready</span>';
el.title = 'Vector search index is active';
// Fade out after a few seconds
setTimeout(() => { el.style.opacity = '0.5'; }, 3000);
} else if (status === 'error') {
el.innerHTML = '<span style="color: var(--error-color, #ef4444);">⚠ Index error</span>';
el.title = 'Could not create vector search index. RAG queries may fail.';
} else {
el.innerHTML = '';
}
}
async pollIndexReady() {
// Atlas vector indexes build async — poll KB selection to check status
let attempts = 0;
const maxAttempts = 30; // ~60 seconds
const interval = setInterval(async () => {
attempts++;
if (attempts >= maxAttempts) {
clearInterval(interval);
const el = document.getElementById('kbIndexStatus');
if (el) {
el.innerHTML = '<span style="color: var(--warning-color, #f59e0b);">⚠ Index may still be building</span>';
}
return;
}
try {
const result = await this.kbManager.selectKB(this.kbManager.currentKB);
if (result.indexStatus === 'exists') {
clearInterval(interval);
this.showIndexStatus('exists');
}
} catch {
// Ignore polling errors
}
}, 2000);
}
// ── Auto-configure chat settings from KB ──
syncKBToChatConfig(kbName) {
if (!kbName) return;
const dbEl = document.getElementById('chatDb');
const collEl = document.getElementById('chatCollection');
if (dbEl) dbEl.value = 'vai_rag';
if (collEl) collEl.value = `kb_${kbName}_docs`;
// Persist and update header status
if (typeof saveChatSettings === 'function') saveChatSettings();
if (typeof updateChatStatus === 'function') updateChatStatus();
}
// ── Initial load ──
async loadInitialKB() {
const lastKB = localStorage.getItem('__vai_last_kb');
if (lastKB) {
try {
const result = await this.kbManager.selectKB(lastKB);
this.syncKBToChatConfig(lastKB);
this.updatePanelUI();
this.showIndexStatus(result.indexStatus);
} catch (err) {
console.warn('Last KB not found, showing wizard');
this.openWizard();
}
} else {
this.openWizard();
}
}
// ── Wizard ──
openWizard() {
const wizardEl = document.getElementById('kbWizard');
if (wizardEl) {
wizardEl.classList.add('visible');
this.isWizardOpen = true;
this.currentStep = 'select';
this.updateWizardUI();
this.populateKBDropdown(); // Refresh list every time (picks up renames)
}
}
closeWizard() {
const wizardEl = document.getElementById('kbWizard');
if (wizardEl) {
wizardEl.classList.remove('visible');
this.isWizardOpen = false;
}
}
updateWizardUI() {
const selectStep = document.getElementById('kbWizardSelectStep');
const docsStep = document.getElementById('kbWizardDocsStep');
if (this.currentStep === 'select') {
if (selectStep) selectStep.style.display = 'block';
if (docsStep) docsStep.style.display = 'none';
} else {
if (selectStep) selectStep.style.display = 'none';
if (docsStep) docsStep.style.display = 'block';
}
}
async onSelectExisting() {
const select = document.getElementById('kbWizardExistingSelect');
if (!select || !select.value) {
alert('Please select a knowledge base');
return;
}
try {
const result = await this.kbManager.selectKB(select.value);
this.syncKBToChatConfig(this.kbManager.currentKB);
this.updatePanelUI();
this.showIndexStatus(result.indexStatus);
this.closeWizard();
} catch (err) {
console.error('Error selecting KB:', err);
alert(`Error: ${err.message}`);
}
}
async onStartFresh() {
try {
await this.kbManager.selectKB(null);
this.syncKBToChatConfig(this.kbManager.currentKB);
this.currentStep = 'add-docs';
this.updateWizardUI();
this.updatePanelUI();
} catch (err) {
console.error('Error creating KB:', err);
alert(`Error: ${err.message}`);
}
}
onStartChatting() {
this.closeWizard();
}
setupWizardEvents() {
this.populateKBDropdown();
const existingBtn = document.getElementById('kbWizardUseExisting');
const freshBtn = document.getElementById('kbWizardStartFresh');
const chattingBtn = document.getElementById('kbWizardStartChatting');
const skipBtn = document.getElementById('kbWizardSkipFiles');
const closeBtn = document.getElementById('kbWizardClose');
if (existingBtn) existingBtn.addEventListener('click', () => this.onSelectExisting());
if (freshBtn) freshBtn.addEventListener('click', () => this.onStartFresh());
if (chattingBtn) chattingBtn.addEventListener('click', () => this.onStartChatting());
if (skipBtn) skipBtn.addEventListener('click', () => this.onStartChatting());
if (closeBtn) closeBtn.addEventListener('click', () => this.closeWizard());
this.setupWizardFileDropZone();
}
setupWizardFileDropZone() {
const dropZone = document.getElementById('kbWizardDropZone');
if (!dropZone) return;
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); });
});
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, () => { dropZone.classList.add('drag-over'); });
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, () => { dropZone.classList.remove('drag-over'); });
});
dropZone.addEventListener('drop', (e) => { this.handleWizardFileSelect(e.dataTransfer.files); });
dropZone.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file'; input.multiple = true; input.accept = '.txt,.md,.pdf';
input.onchange = (e) => this.handleWizardFileSelect(e.target.files);
input.click();
});
}
async handleWizardFileSelect(files) {
if (!files || files.length === 0) return;
const kbName = this.kbManager.currentKB;
if (!kbName) { alert('No knowledge base selected'); return; }
const validFiles = this.validateFiles(files);
if (validFiles.length === 0) { alert('No valid files selected (.txt, .md, or .pdf, max 10MB each)'); return; }
this.closeWizard();
await this.ingestFiles(validFiles, kbName);
}
async populateKBDropdown() {
try {
const kbs = await this.kbManager.listKBs();
const select = document.getElementById('kbWizardExistingSelect');
if (!select) return;
select.innerHTML = '<option value="">— Choose a knowledge base —</option>';
kbs.forEach(kb => {
const option = document.createElement('option');
option.value = kb.name;
option.textContent = `${kb.displayName || kb.name} (${kb.docCount} docs)`;
select.appendChild(option);
});
} catch (err) {
console.error('Error populating KB dropdown:', err);
}
}
// ── Panel events ──
setupPanelEvents() {
const switchBtn = document.getElementById('kbPanelSwitchBtn');
const clearBtn = document.getElementById('kbPanelClearBtn');
const deleteBtn = document.getElementById('kbPanelDeleteBtn');
const collapseBtn = document.getElementById('kbPanelCollapseBtn');
if (switchBtn) switchBtn.addEventListener('click', () => this.openWizard());
if (clearBtn) clearBtn.addEventListener('click', () => this.onClearKB());
if (deleteBtn) deleteBtn.addEventListener('click', () => this.onDeleteKB());
if (collapseBtn) collapseBtn.addEventListener('click', () => this.togglePanelCollapse());
// Paste and URL ingest buttons
const pasteBtn = document.getElementById('kbPasteIngestBtn');
const urlBtn = document.getElementById('kbURLIngestBtn');
if (pasteBtn) pasteBtn.addEventListener('click', () => this.handlePasteIngest());
if (urlBtn) urlBtn.addEventListener('click', () => this.handleURLIngest());
}
setupFileDropZone() {
const dropZone = document.getElementById('kbPanelDropZone');
const fileInput = document.getElementById('kbPanelFileInput');
if (!dropZone || !fileInput) return;
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); });
});
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, () => { dropZone.classList.add('drag-over'); });
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, () => { dropZone.classList.remove('drag-over'); });
});
dropZone.addEventListener('drop', (e) => { this.handleFileSelect(e.dataTransfer.files); });
fileInput.addEventListener('change', (e) => { this.handleFileSelect(e.target.files); });
dropZone.addEventListener('click', () => { fileInput.click(); });
}
// ── File handling ──
validateFiles(files) {
const validFiles = [];
for (const file of files) {
if (!['text/plain', 'text/markdown', 'application/x-markdown', 'application/pdf'].includes(file.type)) {
console.warn(`Skipping invalid file type: ${file.name}`);
continue;
}
if (file.size > 10 * 1024 * 1024) {
console.warn(`Skipping oversized file: ${file.name}`);
continue;
}
validFiles.push(file);
}
return validFiles;
}
async handleFileSelect(files) {
if (!files || files.length === 0) return;
const kbName = this.kbManager.currentKB;
if (!kbName) { alert('No knowledge base selected'); return; }
const validFiles = this.validateFiles(files);
if (validFiles.length === 0) { alert('No valid files selected (.txt, .md, or .pdf, max 10MB each)'); return; }
await this.ingestFiles(validFiles, kbName);
}
async ingestFiles(files, kbName) {
const progressContainer = document.getElementById('kbPanelProgress');
const progressBar = document.getElementById('kbPanelProgressBar');
const progressLabel = document.getElementById('kbPanelProgressLabel');
if (!progressContainer) return;
const warnings = [];
try {
progressContainer.style.display = 'block';
for await (const event of this.kbManager.ingestFiles(files, kbName)) {
if (event.type === 'progress') {
let label = '';
let percent = 0;
if (event.stage === 'reading') {
label = `Reading ${event.file}...`;
percent = 5;
} else if (event.stage === 'chunking') {
label = `Chunking ${event.file} (${event.chunks} chunks)`;
percent = 15;
} else if (event.stage === 'embedding') {
label = `Embedding ${event.current}/${event.total} chunks`;
percent = 15 + Math.round((event.current / event.total) * 70);
} else if (event.stage === 'storing') {
label = `Storing ${event.file}...`;
percent = 90;
} else if (event.stage === 'fetching') {
label = 'Fetching URL...';
percent = 5;
} else {
// Fallback for old-style progress (file-level)
label = `${event.current}/${event.total} — ${event.file || ''}`;
percent = event.total ? Math.round((event.current / event.total) * 100) : 0;
}
if (progressBar) progressBar.style.width = `${percent}%`;
if (progressLabel) progressLabel.textContent = label;
} else if (event.type === 'warning') {
if (event.warning) warnings.push(event.warning);
if (progressLabel) progressLabel.textContent = event.warning || 'Upload completed with warnings';
} else if (event.type === 'complete') {
if (progressBar) progressBar.style.width = '100%';
const hasWarnings = warnings.length > 0 || event.docCount === 0;
if (progressLabel) {
progressLabel.textContent = hasWarnings
? `Complete with warnings: ${event.docCount} docs, ${event.chunkCount} chunks`
: `✓ Complete: ${event.docCount} docs, ${event.chunkCount} chunks`;
}
setTimeout(() => {
this.updatePanelUI();
progressContainer.style.display = 'none';
const messages = [...warnings];
if (event.docCount === 0) {
messages.unshift('No documents were stored from that upload.');
}
if (messages.length > 0) {
alert(messages.join('\n'));
}
}, 1000);
} else if (event.type === 'error') {
console.error('Ingestion error:', event.error);
alert(`Ingestion error: ${event.error}`);
progressContainer.style.display = 'none';
}
}
} catch (err) {
console.error('Error ingesting files:', err);
alert(`Error: ${err.message}`);
progressContainer.style.display = 'none';
}
const fileInput = document.getElementById('kbPanelFileInput');
if (fileInput) fileInput.value = '';
}
// ── Paste text ingest ──
async handlePasteIngest() {
const textarea = document.getElementById('kbPasteText');
const titleInput = document.getElementById('kbPasteTitle');
if (!textarea) return;
const text = textarea.value.trim();
if (!text) { alert('Please paste some text content first.'); return; }
const kbName = this.kbManager.currentKB;
if (!kbName) { alert('No knowledge base selected'); return; }
const title = titleInput ? titleInput.value.trim() : '';
await this.ingestText(text, kbName, title);
// Clear inputs after successful ingest
textarea.value = '';
if (titleInput) titleInput.value = '';
}
async ingestText(text, kbName, title) {
const progressContainer = document.getElementById('kbPanelProgress');
const progressBar = document.getElementById('kbPanelProgressBar');
const progressLabel = document.getElementById('kbPanelProgressLabel');
if (!progressContainer) return;
try {
progressContainer.style.display = 'block';
for await (const event of this.kbManager.ingestText(text, kbName, title)) {
if (event.type === 'progress') {
const percent = event.total > 0 ? Math.round((event.current / event.total) * 100) : 0;
if (progressBar) progressBar.style.width = `${percent}%`;
const stageLabel = event.stage === 'chunking' ? 'Chunking...'
: event.stage === 'embedding' ? `Embedding ${event.current}/${event.total} chunks`
: 'Processing...';
if (progressLabel) progressLabel.textContent = stageLabel;
} else if (event.type === 'complete') {
if (progressBar) progressBar.style.width = '100%';
if (progressLabel) progressLabel.textContent = `Done: ${event.chunkCount} chunks ingested`;
setTimeout(() => {
this.updatePanelUI();
progressContainer.style.display = 'none';
}, 1000);
} else if (event.type === 'error') {
console.error('Text ingestion error:', event.error);
alert(`Ingestion error: ${event.error}`);
progressContainer.style.display = 'none';
}
}
} catch (err) {
console.error('Error ingesting text:', err);
alert(`Error: ${err.message}`);
progressContainer.style.display = 'none';
}
}
// ── URL ingest ──
async handleURLIngest() {
const urlInput = document.getElementById('kbURLInput');
if (!urlInput) return;
const url = urlInput.value.trim();
if (!url || !/^https?:\/\//i.test(url)) {
alert('Please enter a valid URL starting with http:// or https://');
return;
}
const kbName = this.kbManager.currentKB;
if (!kbName) { alert('No knowledge base selected'); return; }
await this.ingestURL(url, kbName);
// Clear input after successful ingest
urlInput.value = '';
}
async ingestURL(url, kbName) {
const progressContainer = document.getElementById('kbPanelProgress');
const progressBar = document.getElementById('kbPanelProgressBar');
const progressLabel = document.getElementById('kbPanelProgressLabel');
if (!progressContainer) return;
try {
progressContainer.style.display = 'block';
for await (const event of this.kbManager.ingestURL(url, kbName)) {
if (event.type === 'progress') {
const percent = event.total > 0 ? Math.round((event.current / event.total) * 100) : 0;
if (progressBar) progressBar.style.width = `${percent}%`;
const stageLabel = event.stage === 'fetching' ? 'Fetching URL...'
: event.stage === 'chunking' ? 'Chunking...'
: event.stage === 'embedding' ? `Embedding ${event.current}/${event.total} chunks`
: 'Processing...';
if (progressLabel) progressLabel.textContent = stageLabel;
} else if (event.type === 'complete') {
if (progressBar) progressBar.style.width = '100%';
if (progressLabel) progressLabel.textContent = `Done: ${event.chunkCount} chunks ingested`;
setTimeout(() => {
this.updatePanelUI();
progressContainer.style.display = 'none';
}, 1000);
} else if (event.type === 'error') {
console.error('URL ingestion error:', event.error);
alert(`Ingestion error: ${event.error}`);
progressContainer.style.display = 'none';
}
}
} catch (err) {
console.error('Error ingesting URL:', err);
alert(`Error: ${err.message}`);
progressContainer.style.display = 'none';
}
}
// ── Panel UI updates ──
async updatePanelUI() {
if (!this.kbManager.currentKB) return;
try {
const kb = await this.kbManager.getKBDetails(this.kbManager.currentKB);
const nameEl = document.getElementById('kbPanelName');
const docsEl = document.getElementById('kbPanelDocCount');
const chunksEl = document.getElementById('kbPanelChunkCount');
const sizeEl = document.getElementById('kbPanelSize');
this._currentDisplayName = kb.displayName || kb.name;
if (nameEl) nameEl.textContent = this._currentDisplayName;
if (docsEl) docsEl.textContent = kb.docCount || 0;
if (chunksEl) chunksEl.textContent = kb.chunkCount || 0;
if (sizeEl) {
const sizeKB = ((kb.size || 0) / 1024).toFixed(1);
sizeEl.textContent = `${sizeKB} KB`;
}
// Refresh files list
await this.updateFilesList();
} catch (err) {
console.error('Error updating panel:', err);
}
}
async onClearKB() {
if (!this.kbManager.currentKB) return;
const displayName = this._currentDisplayName || this.kbManager.currentKB;
const ok = window.confirm(`Clear all documents from "${displayName}"? This will remove the knowledge base and all its documents. This cannot be undone.`);
if (!ok) return;
const kbToClear = this.kbManager.currentKB;
try {
await this.kbManager.clearKB(kbToClear);
this.clearSelectionAndOpenWizard();
} catch (err) {
console.error('Error clearing KB:', err);
alert(`Error: ${err.message}`);
}
}
async onDeleteKB() {
if (!this.kbManager.currentKB) return;
const displayName = this._currentDisplayName || this.kbManager.currentKB;
const ok = window.confirm(`Delete knowledge base "${displayName}"? This will remove it and all its documents permanently. This cannot be undone.`);
if (!ok) return;
const kbToDelete = this.kbManager.currentKB;
try {
await this.kbManager.clearKB(kbToDelete);
this.clearSelectionAndOpenWizard();
} catch (err) {
console.error('Error deleting KB:', err);
alert(`Error: ${err.message}`);
}
}
clearSelectionAndOpenWizard() {
this.kbManager.currentKB = null;
localStorage.removeItem('__vai_last_kb');
if (typeof saveChatSettings === 'function') saveChatSettings();
if (typeof updateChatStatus === 'function') updateChatStatus();
this.openWizard();
}
togglePanelCollapse() {
const panel = document.getElementById('kbPanel');
if (panel) {
panel.classList.toggle('collapsed');
this.isPanelOpen = !this.isPanelOpen;
const toggleBtn = document.getElementById('chatKbToggle');
if (toggleBtn) toggleBtn.classList.toggle('active', this.isPanelOpen);
}
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
if (window.kbManager) {
window.kbUI = new KBUIManager(window.kbManager);
}
});