RasoSpeak / docs.html
Sourabh-Kumar04
Phase 7: Production hardening - legacy cleanup, RAG integration, D3 graph, CLI, accessibility
9308855
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>RasoSpeak β€” Documents</title>
<link rel="icon" type="image/png" href="logo.png"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"/>
<style>
* { font-family: 'Inter', system-ui, sans-serif; }
body { background: #FAFAFA; }
.nav-link { @apply px-4 py-2 rounded-lg text-sm font-medium transition-colors; }
.nav-link:hover { @apply bg-gray-100; }
.nav-link.active { @apply bg-emerald-50 text-emerald-700; }
</style>
</head>
<body class="min-h-screen">
<!-- Skip to main content -->
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-emerald-500 focus:text-white focus:rounded-lg focus:font-medium">Skip to main content</a>
<!-- Navigation -->
<nav class="bg-white border-b border-gray-200 sticky top-0 z-50" aria-label="Main navigation">
<div class="max-w-5xl mx-auto px-6">
<div class="flex items-center justify-between h-16">
<a href="index.html" class="flex items-center gap-3">
<img src="logo.png" alt="RasoSpeak" class="w-8 h-8">
<span class="font-bold text-lg">RasoSpeak</span>
</a>
<div class="flex items-center gap-1">
<a href="index.html" class="nav-link" aria-label="Home">Home</a>
<a href="chat.html" class="nav-link" aria-label="Chat">Chat</a>
<a href="memory.html" class="nav-link" aria-label="Memory">Memory</a>
<a href="coach.html" class="nav-link" aria-label="Coach">Coach</a>
<a href="docs.html" class="nav-link active" aria-label="Docs">Docs</a>
<a href="settings.html" class="nav-link" aria-label="Settings">Settings</a>
</div>
</div>
</div>
</nav>
<!-- Main Content -->
<main id="main-content" class="max-w-4xl mx-auto px-6 py-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-2xl font-bold mb-2">Knowledge Base</h1>
<p class="text-gray-600">Import documents, URLs, and notes to expand your AI's knowledge</p>
</div>
<!-- Import URL -->
<div class="bg-white rounded-lg border border-gray-200 p-6 mb-6">
<h2 class="text-lg font-semibold mb-4">Import from URL</h2>
<div class="flex gap-3">
<input type="text" id="doc-url" placeholder="Paste URL to import (article, blog, etc.)"
class="flex-1 px-4 py-3 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-emerald-500">
<button onclick="importUrl()" class="px-6 py-3 bg-emerald-500 text-white rounded-lg font-medium hover:bg-emerald-600 transition-colors">
Import
</button>
</div>
<p id="url-status" class="text-xs text-gray-400 mt-2"></p>
</div>
<!-- Import Text -->
<div class="bg-white rounded-lg border border-gray-200 p-6 mb-6">
<h2 class="text-lg font-semibold mb-4">Import Text</h2>
<textarea id="doc-text" placeholder="Paste text, notes, or any content to add to your memory..."
class="w-full h-32 px-4 py-3 border border-gray-200 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-emerald-500 mb-3"></textarea>
<div class="flex gap-3">
<input type="text" id="doc-title" placeholder="Title (optional)" class="flex-1 px-3 py-2 border border-gray-200 rounded-lg">
<select id="doc-category" class="px-3 py-2 border border-gray-200 rounded-lg text-sm">
<option value="note">Note</option>
<option value="article">Article</option>
<option value="book">Book</option>
<option value="research">Research</option>
<option value="other">Other</option>
</select>
<button onclick="importText()" class="flex-1 py-2 bg-emerald-500 text-white rounded-lg font-medium hover:bg-emerald-600 transition-colors">
Add to Memory
</button>
</div>
</div>
<!-- Upload File -->
<div class="bg-white rounded-lg border border-gray-200 p-6 mb-6">
<h2 class="text-lg font-semibold mb-4">Upload Document</h2>
<div id="upload-area" class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-emerald-500 transition-colors cursor-pointer" onclick="document.getElementById('doc-upload').click()" role="button" tabindex="0" aria-label="Click to upload a document" onkeydown="if(event.key==='Enter'||event.key===' '){document.getElementById('doc-upload').click()}">
<input type="file" id="doc-upload" accept=".pdf,.txt,.md,.doc,.docx" class="hidden" onchange="handleFileUpload(this)">
<div class="text-gray-400 mb-2">
<svg class="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
</svg>
</div>
<p class="text-gray-600 font-medium">Click to upload or drag and drop</p>
<p class="text-sm text-gray-400 mt-1">PDF, TXT, MD, DOC, DOCX</p>
</div>
<div id="upload-progress" class="hidden mt-4">
<div class="flex items-center gap-3">
<div class="flex-1 bg-gray-200 rounded-full h-2">
<div id="progress-bar" class="bg-emerald-500 h-2 rounded-full transition-all" style="width: 0%"></div>
</div>
<span id="progress-text" class="text-sm text-gray-500">0%</span>
</div>
</div>
</div>
<!-- Document List -->
<div class="bg-white rounded-lg border border-gray-200 p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold">Knowledge Base</h2>
<div class="flex items-center gap-2">
<div class="relative flex items-center">
<input type="text" id="doc-search" placeholder="Search knowledge..."
class="pl-10 pr-32 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 w-72"
aria-label="Search documents">
<button id="voice-search-btn" onclick="toggleVoiceSearch()" class="absolute left-2 p-1 text-gray-400 hover:text-emerald-500" aria-label="Toggle voice search">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"/>
</svg>
</button>
<div class="absolute right-1 flex gap-1">
<button onclick="searchDocs('wiki')" class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs hover:bg-blue-200" aria-label="Search Wikipedia">πŸ“š</button>
<button onclick="searchDocs('web')" class="px-2 py-1 bg-green-100 text-green-700 rounded text-xs hover:bg-green-200" aria-label="Search web">🌐</button>
<button onclick="searchDocs('rag')" class="px-2 py-1 bg-purple-100 text-purple-700 rounded text-xs hover:bg-purple-200" aria-label="RAG search">πŸ€–</button>
</div>
</div>
<button onclick="searchDocs('docs')" class="px-4 py-2 bg-gray-100 rounded-lg text-sm hover:bg-gray-200" aria-label="Search documents">πŸ“„ Docs</button>
<button onclick="loadDocuments()" class="px-4 py-2 bg-gray-100 rounded-lg text-sm hover:bg-gray-200" aria-label="Refresh document list">Refresh</button>
</div>
</div>
<div id="search-type-label" class="text-xs text-gray-400 mb-2">
πŸ“š Wikipedia | 🌐 Web | πŸ€– RAG | πŸ“„ Docs search
</div>
<div id="wiki-results" class="hidden mb-4 p-4 bg-blue-50 rounded-lg">
<h3 class="font-medium text-blue-800 mb-2">πŸ“š Wikipedia Results</h3>
<div id="wiki-results-list"></div>
<button onclick="addWikiToMemory()" class="mt-2 px-3 py-1 bg-blue-500 text-white rounded text-sm hover:bg-blue-600">Add to Memory</button>
</div>
<div id="web-results" class="hidden mb-4 p-4 bg-green-50 rounded-lg">
<h3 class="font-medium text-green-800 mb-2">🌐 Web Results</h3>
<div id="web-results-list"></div>
</div>
<div id="docs-list" class="space-y-3">
<div class="text-center text-gray-400 py-8">
<p>No documents yet</p>
<p class="text-sm mt-1">Import URLs, text, or upload files above</p>
</div>
</div>
<div id="docs-pagination" class="flex items-center justify-between mt-4 pt-4 border-t border-gray-100">
<p id="docs-count" class="text-sm text-gray-500"></p>
<div class="flex gap-2">
<button id="prev-page" onclick="prevPage()" class="px-3 py-1 bg-gray-100 rounded text-sm hover:bg-gray-200 disabled:opacity-50" disabled aria-label="Previous page">Previous</button>
<button id="next-page" onclick="nextPage()" class="px-3 py-1 bg-gray-100 rounded text-sm hover:bg-gray-200 disabled:opacity-50" disabled aria-label="Next page">Next</button>
</div>
</div>
</div>
</main>
<!-- Toast -->
<div id="toast" class="fixed bottom-6 left-1/2 -translate-x-1/2 bg-gray-900 text-white px-6 py-3 rounded-full text-sm font-medium shadow-xl transition-all duration-300 opacity-0 pointer-events-none translate-y-4 z-50" role="status" aria-live="polite" aria-atomic="true"></div>
<!-- Scripts -->
<script src="state.js"></script>
<script src="nlp.js"></script>
<script src="speech.js"></script>
<script src="ui.js"></script>
<script src="app.js"></script>
<script>
let currentPage = 1;
let totalDocs = 0;
const pageSize = 10;
let voiceSearchActive = false;
let voiceSearchRecognition = null;
// Toast notification
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.remove('opacity-0', 'translate-y-4', 'pointer-events-none');
setTimeout(() => t.classList.add('opacity-0', 'translate-y-4', 'pointer-events-none'), 3000);
}
// Voice search toggle
function toggleVoiceSearch() {
if (voiceSearchActive) {
// Turn off
voiceSearchActive = false;
if (voiceSearchRecognition) {
try { voiceSearchRecognition.stop(); } catch(e) {}
voiceSearchRecognition = null;
}
document.getElementById('voice-search-btn').classList.remove('text-emerald-500');
return;
}
// Request permission
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
stream.getTracks().forEach(track => track.stop());
startVoiceSearch();
})
.catch(err => {
showToast('Microphone permission denied');
});
}
function startVoiceSearch() {
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SR) {
showToast('Voice not supported - use Chrome');
return;
}
voiceSearchActive = true;
document.getElementById('voice-search-btn').classList.add('text-emerald-500');
showToast('Speak to search...');
voiceSearchRecognition = new SR();
voiceSearchRecognition.continuous = false;
voiceSearchRecognition.interimResults = false;
voiceSearchRecognition.lang = 'en-US';
voiceSearchRecognition.onresult = (event) => {
const transcript = event.results[0][0].transcript;
document.getElementById('doc-search').value = transcript;
searchDocuments('web'); // Default to web search
};
voiceSearchRecognition.onerror = (event) => {
console.error('Voice search error:', event.error);
if (event.error !== 'no-speech' && event.error !== 'aborted') {
showToast('Voice error: ' + event.error);
}
voiceSearchActive = false;
document.getElementById('voice-search-btn').classList.remove('text-emerald-500');
};
voiceSearchRecognition.onend = () => {
voiceSearchActive = false;
document.getElementById('voice-search-btn').classList.remove('text-emerald-500');
};
voiceSearchRecognition.start();
}
// Import URL
async function importUrl() {
const url = document.getElementById('doc-url').value.trim();
const status = document.getElementById('url-status');
if (!url) {
showToast('Please enter a URL');
return;
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
showToast('Please enter a valid URL starting with http:// or https://');
return;
}
status.textContent = 'Importing...';
try {
const resp = await fetch('/documents/url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
const data = await resp.json();
if (resp.ok) {
status.textContent = `Imported: ${data.title || 'Document'}`;
showToast(`Imported: ${data.title || 'Document'}`);
document.getElementById('doc-url').value = '';
loadDocuments();
} else {
status.textContent = 'Failed to import URL';
showToast(data.error || 'Failed to import URL');
}
} catch (err) {
status.textContent = 'Error importing URL';
showToast('Error importing URL');
console.error(err);
}
}
// Import Text
async function importText() {
const text = document.getElementById('doc-text').value.trim();
const title = document.getElementById('doc-title').value.trim() || 'Untitled Note';
const category = document.getElementById('doc-category').value;
if (!text) {
showToast('Please enter some text');
return;
}
try {
const resp = await fetch('/documents/text', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: text,
title,
category,
tags: []
})
});
if (resp.ok) {
showToast(`Added: ${title}`);
document.getElementById('doc-text').value = '';
document.getElementById('doc-title').value = '';
loadDocuments();
} else {
showToast('Failed to add note');
}
} catch (err) {
showToast('Error adding note');
console.error(err);
}
}
// Handle File Upload
async function handleFileUpload(input) {
const file = input.files?.[0];
if (!file) return;
const progress = document.getElementById('upload-progress');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
progress.classList.remove('hidden');
progressBar.style.width = '0%';
progressText.textContent = '0%';
try {
const formData = new FormData();
formData.append('file', file);
formData.append('title', file.name.replace(/\.[^/.]+$/, ''));
formData.append('category', 'document');
// Simulate progress
let prog = 0;
const interval = setInterval(() => {
prog = Math.min(prog + 10, 90);
progressBar.style.width = `${prog}%`;
progressText.textContent = `${prog}%`;
}, 100);
const resp = await fetch('/documents/upload', {
method: 'POST',
body: formData
});
clearInterval(interval);
progressBar.style.width = '100%';
progressText.textContent = '100%';
if (resp.ok) {
const data = await resp.json();
showToast(`Uploaded: ${data.title || file.name}`);
loadDocuments();
} else {
showToast('Failed to upload file');
}
} catch (err) {
showToast('Error uploading file');
console.error(err);
}
setTimeout(() => {
progress.classList.add('hidden');
}, 2000);
}
// Load Documents
async function loadDocuments(page = 1) {
const container = document.getElementById('docs-list');
container.innerHTML = '<div class="text-center text-gray-400 py-8"><p>Loading documents...</p></div>';
try {
const resp = await fetch('/documents');
const data = await resp.json();
const docs = data.documents || [];
totalDocs = docs.length;
currentPage = page;
if (docs.length === 0) {
container.innerHTML = `
<div class="text-center text-gray-400 py-8">
<p>No documents yet</p>
<p class="text-sm mt-1">Import URLs, text, or upload files above</p>
</div>
`;
document.getElementById('docs-count').textContent = '0 documents';
return;
}
// Paginate
const start = (page - 1) * pageSize;
const end = start + pageSize;
const pageDocs = docs.slice(start, end);
container.innerHTML = pageDocs.map(doc => `
<div class="flex items-start justify-between p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors">
<div class="flex-1">
<h3 class="font-medium text-gray-900">${doc.title || 'Untitled'}</h3>
<p class="text-sm text-gray-500 mt-1 line-clamp-2">${doc.content || doc.snippet || ''}</p>
<div class="flex items-center gap-2 mt-2">
<span class="px-2 py-0.5 bg-gray-100 rounded text-xs text-gray-600">${doc.category || 'other'}</span>
${doc.created_at ? `<span class="text-xs text-gray-400">${new Date(doc.created_at).toLocaleDateString()}</span>` : ''}
</div>
</div>
<div class="flex items-center gap-2 ml-4">
<button onclick="deleteDocument('${doc.id}')" class="p-2 text-gray-400 hover:text-red-500 transition-colors" aria-label="Delete document">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</button>
</div>
</div>
`).join('');
// Update pagination
document.getElementById('docs-count').textContent = `${totalDocs} documents`;
document.getElementById('prev-page').disabled = page <= 1;
document.getElementById('next-page').disabled = end >= totalDocs;
} catch (err) {
container.innerHTML = '<div class="text-center text-red-400 py-8"><p>Failed to load documents</p></div>';
console.error(err);
}
}
// Search with multiple methods
async function searchDocs(type = 'docs') {
const query = document.getElementById('doc-search').value.trim();
const searchLabel = document.getElementById('search-type-label');
const wikiResults = document.getElementById('wiki-results');
const webResults = document.getElementById('web-results');
const docsList = document.getElementById('docs-list');
if (!query) {
showToast('Enter a search term');
return;
}
// Hide all result areas
wikiResults.classList.add('hidden');
webResults.classList.add('hidden');
if (type === 'wiki') {
// Wikipedia search with RAG
searchLabel.textContent = 'πŸ“š Searching Wikipedia...';
wikiResults.classList.remove('hidden');
docsList.innerHTML = '<div class="text-center text-gray-400 py-4"><p>Searching Wikipedia...</p></div>';
try {
const resp = await fetch('/rag/wikipedia', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, max_results: 5 })
});
const data = await resp.json();
if (data.answer) {
document.getElementById('wiki-results-list').innerHTML = `
<div class="mb-3 p-3 bg-white rounded">
<p class="text-gray-800">${data.answer}</p>
</div>
<div class="text-sm text-gray-600">Sources:</div>
${(data.sources || []).map(s => `
<a href="${s.url}" target="_blank" class="block text-blue-600 hover:underline text-sm mt-1">β€’ ${s.title}</a>
`).join('')}
`;
searchLabel.textContent = `πŸ“š Wiki complete (${data.context_chunks} chunks) | 🌐 Web | πŸ€– RAG | πŸ“„ Docs`;
} else {
document.getElementById('wiki-results-list').innerHTML = '<p class="text-gray-500">No Wikipedia results found</p>';
}
} catch (err) {
console.error(err);
showToast('Wikipedia search failed');
}
} else if (type === 'web') {
// Web search
searchLabel.textContent = '🌐 Searching web...';
webResults.classList.remove('hidden');
docsList.innerHTML = '<div class="text-center text-gray-400 py-4"><p>Searching web...</p></div>';
try {
const resp = await fetch('/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, num_results: 8, include_summary: true })
});
const data = await resp.json();
const results = data.results || [];
document.getElementById('web-results-list').innerHTML = results.map(r => `
<div class="p-3 bg-white rounded mb-2">
<a href="${r.url}" target="_blank" class="font-medium text-blue-600 hover:underline">${r.title}</a>
<p class="text-sm text-gray-600 mt-1">${r.snippet || ''}</p>
</div>
`).join('') || '<p class="text-gray-500">No results found</p>';
docsList.innerHTML = '';
searchLabel.textContent = `🌐 ${results.length} web results | πŸ“š Wiki | πŸ€– RAG | πŸ“„ Docs`;
} catch (err) {
console.error(err);
showToast('Web search failed');
}
} else if (type === 'rag') {
// RAG search with LangChain
searchLabel.textContent = 'πŸ€– Searching RAG knowledge base...';
docsList.innerHTML = '<div class="text-center text-gray-400 py-4"><p>Searching knowledge base...</p></div>';
try {
const resp = await fetch('/rag/comprehensive', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});
const data = await resp.json();
let html = '';
// Wikipedia results
if (data.wikipedia && data.wikipedia.answer) {
html += `
<div class="mb-4 p-4 bg-blue-50 rounded-lg">
<h4 class="font-medium text-blue-800 mb-2">πŸ“š From Wikipedia</h4>
<p class="text-gray-700">${data.wikipedia.answer}</p>
</div>
`;
}
// Local documents
if (data.local_documents && data.local_documents.length > 0) {
html += `
<div class="mb-4">
<h4 class="font-medium text-gray-700 mb-2">πŸ“„ From Your Documents</h4>
${data.local_documents.map(d => `
<div class="p-3 border border-gray-200 rounded mb-2">
<p class="text-sm font-medium">${d.title}</p>
<p class="text-sm text-gray-600 mt-1">${d.content?.substring(0, 200)}...</p>
</div>
`).join('')}
</div>
`;
}
// Web results
if (data.web && data.web.length > 0) {
html += `
<div class="mb-4">
<h4 class="font-medium text-gray-700 mb-2">🌐 From Web</h4>
${data.web.slice(0, 3).map(r => `
<a href="${r.url}" target="_blank" class="block text-blue-600 hover:underline text-sm">${r.title}</a>
`).join('')}
</div>
`;
}
if (!html) {
html = '<div class="text-center text-gray-400 py-4"><p>No results from any source</p></div>';
}
docsList.innerHTML = html;
searchLabel.textContent = `πŸ€– RAG complete | πŸ“š Wiki | 🌐 Web | πŸ“„ Docs`;
} catch (err) {
console.error(err);
showToast('RAG search failed - try other methods');
}
} else {
// Search local documents
searchLabel.textContent = 'πŸ“„ Searching documents...';
docsList.innerHTML = '<div class="text-center text-gray-400 py-8"><p>Searching...</p></div>';
try {
const resp = await fetch(`/documents/search?query=${encodeURIComponent(query)}`);
const data = await resp.json();
const results = data.results || [];
if (results.length === 0) {
docsList.innerHTML = '<div class="text-center text-gray-400 py-8"><p>No matches in your documents</p></div>';
searchLabel.textContent = 'πŸ“„ No doc results | πŸ“š Wiki | 🌐 Web | πŸ€– RAG';
return;
}
docsList.innerHTML = results.map(doc => `
<div class="flex items-start justify-between p-4 border border-gray-200 rounded-lg hover:bg-gray-50">
<div class="flex-1">
<h3 class="font-medium text-gray-900">${doc.title || 'Untitled'}</h3>
<p class="text-sm text-gray-500 mt-1 line-clamp-2">${doc.snippet || doc.content || ''}</p>
</div>
</div>
`).join('');
searchLabel.textContent = `πŸ“„ ${results.length} doc results | πŸ“š Wiki | 🌐 Web | πŸ€– RAG`;
} catch (err) {
console.error(err);
showToast('Doc search failed');
}
}
}
// Add Wikipedia results to memory
async function addWikiToMemory() {
const query = document.getElementById('doc-search').value;
try {
const resp = await fetch('/rag/comprehensive', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});
const data = await resp.json();
if (data.wikipedia && data.wikipedia.sources) {
for (const source of data.wikipedia.sources) {
await fetch('/rag/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: source.title,
title: source.title,
source: 'wikipedia',
url: source.url
})
});
}
showToast('Added to knowledge base!');
}
} catch (err) {
showToast('Failed to add to memory');
}
}
// Delete Document
async function deleteDocument(docId) {
if (!confirm('Delete this document?')) return;
try {
const resp = await fetch(`/documents/${docId}`, { method: 'DELETE' });
if (resp.ok) {
showToast('Document deleted');
loadDocuments(currentPage);
}
} catch (err) {
showToast('Failed to delete');
console.error(err);
}
}
// Pagination
function prevPage() {
if (currentPage > 1) loadDocuments(currentPage - 1);
}
function nextPage() {
if ((currentPage * pageSize) < totalDocs) loadDocuments(currentPage + 1);
}
// Initialize
window.addEventListener('load', () => {
loadDocuments();
});
</script>
</body>
</html>