Spaces:
Sleeping
Sleeping
| // ββ State βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const state = { files: {}, connections: [], chatHistory: [] }; | |
| // ββ Navigation ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function showPage(name) { | |
| document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); | |
| document.querySelectorAll('.nav-item[data-page]').forEach(n => n.classList.remove('active')); | |
| document.getElementById('page-' + name).classList.add('active'); | |
| document.querySelector(`.nav-item[data-page="${name}"]`)?.classList.add('active'); | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| document.querySelectorAll('.nav-item[data-page]').forEach(item => { | |
| item.addEventListener('click', () => showPage(item.dataset.page)); | |
| }); | |
| // ββ API key βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function getKey() { return document.getElementById('apiKeyInput').value.trim(); } | |
| // ββ Upload ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const uploadZone = document.getElementById('uploadZone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const uploadStatus = document.getElementById('uploadStatus'); | |
| uploadZone.addEventListener('click', () => fileInput.click()); | |
| uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('dragover'); }); | |
| uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover')); | |
| uploadZone.addEventListener('drop', e => { | |
| e.preventDefault(); uploadZone.classList.remove('dragover'); | |
| if (e.dataTransfer.files[0]) handleUpload(e.dataTransfer.files[0]); | |
| }); | |
| fileInput.addEventListener('change', () => { if (fileInput.files[0]) handleUpload(fileInput.files[0]); }); | |
| function showStatus(msg, show = true) { | |
| uploadStatus.style.display = show ? 'block' : 'none'; | |
| uploadStatus.textContent = msg; | |
| } | |
| async function handleUpload(file) { | |
| const key = getKey(); | |
| if (!key) { alert('Please enter your Claude API key in the sidebar first.'); return; } | |
| showStatus(`β³ Processing ${file.name} β this may take 20-60 seconds...`); | |
| const fd = new FormData(); | |
| fd.append('file', file); | |
| fd.append('api_key', key); | |
| try { | |
| const res = await fetch('/api/upload', { method: 'POST', body: fd }); | |
| const data = await res.json(); | |
| if (!res.ok) { showStatus(`Error: ${data.detail || 'Upload failed'}`); return; } | |
| state.files[data.filename] = data; | |
| if (data.connections) state.connections = data.connections; | |
| showStatus(`β ${data.filename} indexed β ${data.chunk_count} chunks, summary generated`); | |
| updateAll(); | |
| document.getElementById('askBtn').disabled = false; | |
| } catch (e) { | |
| showStatus('Upload failed: ' + e.message); | |
| } finally { | |
| fileInput.value = ''; | |
| } | |
| } | |
| // ββ Update UI βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function updateAll() { | |
| updateStats(); | |
| updateSidebarFiles(); | |
| updateFilesGrid(); | |
| updateSummaries(); | |
| updateConnections(); | |
| } | |
| function updateStats() { | |
| const files = Object.values(state.files); | |
| document.getElementById('stat-docs').textContent = files.length; | |
| const totalChunks = files.reduce((s, f) => s + (f.chunk_count || 0), 0); | |
| document.getElementById('stat-chunks').textContent = totalChunks; | |
| const types = [...new Set(files.map(f => f.type?.toUpperCase()))]; | |
| document.getElementById('stat-types').textContent = types.length ? types.join(', ') : 'β'; | |
| } | |
| function fileIcon(type) { | |
| const icons = { pdf: 'ti-file-type-pdf', image: 'ti-photo', video: 'ti-video', text: 'ti-file-text' }; | |
| return icons[type] || 'ti-file'; | |
| } | |
| function updateSidebarFiles() { | |
| const container = document.getElementById('sidebar-files'); | |
| const files = Object.entries(state.files); | |
| if (!files.length) { container.innerHTML = ''; return; } | |
| container.innerHTML = files.map(([name, f]) => ` | |
| <div class="nav-item" onclick="showPage('summaries')"> | |
| <i class="ti ${fileIcon(f.type)}" aria-hidden="true"></i> | |
| <span class="file-name">${name}</span> | |
| <button class="del-btn" onclick="event.stopPropagation();deleteFile('${name}')" title="Remove">β</button> | |
| </div> | |
| `).join(''); | |
| } | |
| function updateFilesGrid() { | |
| const grid = document.getElementById('filesGrid'); | |
| const files = Object.entries(state.files); | |
| if (!files.length) { | |
| grid.innerHTML = `<div class="empty-state" style="grid-column:1/-1;"><i class="ti ti-inbox" aria-hidden="true"></i><p>No files yet β upload something to get started</p></div>`; | |
| return; | |
| } | |
| grid.innerHTML = files.map(([name, f]) => ` | |
| <div class="file-card" onclick="openModal('${name.replace(/'/g, "\\'")}')" style="cursor:pointer;"> | |
| <div class="file-thumb"> | |
| ${f.thumb_b64 ? `<img src="data:image/jpeg;base64,${f.thumb_b64}" alt="${name}"/>` : `<i class="ti ${fileIcon(f.type)}" aria-hidden="true"></i>`} | |
| </div> | |
| <div class="file-info"> | |
| <div class="file-name">${name}</div> | |
| <div class="file-type">${f.type || 'file'}</div> | |
| <div class="file-meta">${f.chunk_count || 0} chunks Β· click to explore</div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| function updateSummaries() { | |
| const list = document.getElementById('summaryList'); | |
| const files = Object.entries(state.files); | |
| if (!files.length) { | |
| list.innerHTML = `<div class="empty-state"><i class="ti ti-file-text" aria-hidden="true"></i><p>Upload files to see auto-generated summaries and entities</p></div>`; | |
| return; | |
| } | |
| list.innerHTML = files.map(([name, f]) => ` | |
| <div class="summary-item"> | |
| <div class="summary-filename"><i class="ti ${fileIcon(f.type)}" aria-hidden="true"></i>${name}</div> | |
| <div class="summary-text">${f.summary || 'No summary available.'}</div> | |
| ${f.entities?.length ? `<div class="entity-row">${f.entities.map(e => `<span class="entity-chip">${e}</span>`).join('')}</div>` : ''} | |
| ${f.topics?.length ? `<div class="entity-row" style="margin-top:6px;">${f.topics.map(t => `<span class="entity-chip" style="background:#eef4f0;border-color:#c4d8ca;color:#4a7a5a;">${t}</span>`).join('')}</div>` : ''} | |
| </div> | |
| `).join(''); | |
| } | |
| function updateConnections() { | |
| const list = document.getElementById('connList'); | |
| if (!state.connections.length) { | |
| list.innerHTML = `<div class="empty-state"><i class="ti ti-topology-star" aria-hidden="true"></i><p>Upload at least two files to discover cross-document connections</p></div>`; | |
| return; | |
| } | |
| list.innerHTML = state.connections.map(c => ` | |
| <div class="conn-item"> | |
| <div class="conn-icon"><i class="ti ti-link" aria-hidden="true"></i></div> | |
| <div> | |
| <div class="conn-docs">${c.doc1} β ${c.doc2}</div> | |
| <div class="conn-text">${c.connection}</div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| // ββ Ask βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const askBtn = document.getElementById('askBtn'); | |
| const queryInput = document.getElementById('queryInput'); | |
| const chatHistory = document.getElementById('chatHistory'); | |
| askBtn.addEventListener('click', runAsk); | |
| queryInput.addEventListener('keydown', e => { if (e.key === 'Enter' && !askBtn.disabled) runAsk(); }); | |
| async function runAsk() { | |
| const query = queryInput.value.trim(); | |
| if (!query) return; | |
| const key = getKey(); | |
| if (!key) { alert('Please enter your Claude API key first.'); return; } | |
| askBtn.disabled = true; | |
| askBtn.innerHTML = '<i class="ti ti-loader" aria-hidden="true"></i> Thinking...'; | |
| chatHistory.style.display = 'flex'; | |
| document.getElementById('suggestionsCard').style.display = 'none'; | |
| chatHistory.innerHTML += `<div class="chat-q">${query}</div>`; | |
| chatHistory.scrollTop = chatHistory.scrollHeight; | |
| const fd = new FormData(); | |
| fd.append('query', query); | |
| fd.append('api_key', key); | |
| try { | |
| const res = await fetch('/api/ask', { method: 'POST', body: fd }); | |
| const data = await res.json(); | |
| const confClass = { high: 'conf-high', medium: 'conf-medium', low: 'conf-low' }[data.confidence] || 'conf-medium'; | |
| const sources = (data.sources || []).map(s => `<span class="source-chip"><i class="ti ${fileIcon(s.file?.split('.').pop())} " aria-hidden="true"></i>${s.file}${s.page ? ` p.${s.page}` : ''}</span>`).join(''); | |
| chatHistory.innerHTML += ` | |
| <div class="chat-a"> | |
| <span class="confidence-badge ${confClass}">${data.confidence || 'medium'}</span> | |
| <div style="margin-top:8px;">${data.answer || 'No answer found.'}</div> | |
| ${sources ? `<div class="src">${sources}</div>` : ''} | |
| </div>`; | |
| chatHistory.scrollTop = chatHistory.scrollHeight; | |
| queryInput.value = ''; | |
| } catch (e) { | |
| chatHistory.innerHTML += `<div class="chat-a" style="color:var(--danger);">Error: ${e.message}</div>`; | |
| } finally { | |
| askBtn.disabled = false; | |
| askBtn.innerHTML = '<i class="ti ti-arrow-right" aria-hidden="true"></i>Ask'; | |
| } | |
| } | |
| function setQuery(el) { queryInput.value = el.textContent; queryInput.focus(); } | |
| // ββ Delete file βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function deleteFile(name) { | |
| await fetch(`/api/file/${encodeURIComponent(name)}`, { method: 'DELETE' }); | |
| delete state.files[name]; | |
| updateAll(); | |
| if (!Object.keys(state.files).length) document.getElementById('askBtn').disabled = true; | |
| } | |
| // ββ File detail modal βββββββββββββββββββββββββββββββββββββββββββββ | |
| let currentModalFile = null; | |
| function openModal(name) { | |
| const f = state.files[name]; | |
| if (!f) return; | |
| currentModalFile = name; | |
| document.getElementById('modal-filename').textContent = name; | |
| document.getElementById('modal-type').textContent = (f.type || 'file').toUpperCase() + ' Β· ' + (f.file_type_detected || ''); | |
| document.getElementById('modal-summary').textContent = f.summary || 'No summary available.'; | |
| // thumb | |
| const thumbWrap = document.getElementById('modal-thumb-wrap'); | |
| const thumbImg = document.getElementById('modal-thumb'); | |
| if (f.thumb_b64) { | |
| thumbImg.src = `data:image/jpeg;base64,${f.thumb_b64}`; | |
| thumbWrap.style.display = 'block'; | |
| } else { | |
| thumbWrap.style.display = 'none'; | |
| } | |
| // facts | |
| const facts = f.important_facts || []; | |
| const factsWrap = document.getElementById('modal-facts-wrap'); | |
| const factsEl = document.getElementById('modal-facts'); | |
| if (facts.length) { | |
| factsEl.innerHTML = facts.map(fact => `<div style="display:flex;gap:8px;align-items:flex-start;"><span style="color:#c17a4a;flex-shrink:0;">β</span><span style="font-family:'DM Sans',sans-serif;font-size:13px;color:#3d2e1f;line-height:1.6;">${fact}</span></div>`).join(''); | |
| factsWrap.style.display = 'block'; | |
| } else { | |
| factsWrap.style.display = 'none'; | |
| } | |
| // sections | |
| const sections = f.key_sections || []; | |
| const sectionsWrap = document.getElementById('modal-sections-wrap'); | |
| const sectionsEl = document.getElementById('modal-sections'); | |
| if (sections.length) { | |
| sectionsEl.innerHTML = sections.map(s => `<span class="badge" style="font-size:11px;">${s}</span>`).join(''); | |
| sectionsWrap.style.display = 'block'; | |
| } else { | |
| sectionsWrap.style.display = 'none'; | |
| } | |
| // entities | |
| document.getElementById('modal-entities').innerHTML = (f.entities || []).map(e => `<span class="entity-chip">${e}</span>`).join('') || '<span style="font-size:12px;color:#a89a88;">None extracted</span>'; | |
| // topics | |
| document.getElementById('modal-topics').innerHTML = (f.topics || []).map(t => `<span class="entity-chip" style="background:#eef4f0;border-color:#c4d8ca;color:#4a7a5a;">${t}</span>`).join('') || '<span style="font-size:12px;color:#a89a88;">None extracted</span>'; | |
| // preview | |
| document.getElementById('modal-preview').textContent = f.preview || 'No content preview available.'; | |
| document.getElementById('fileModal').style.display = 'block'; | |
| document.body.style.overflow = 'hidden'; | |
| } | |
| function closeModal() { | |
| document.getElementById('fileModal').style.display = 'none'; | |
| document.body.style.overflow = ''; | |
| currentModalFile = null; | |
| } | |
| function askAboutFile(name) { | |
| closeModal(); | |
| showPage('ask'); | |
| document.getElementById('queryInput').value = `Tell me everything important about ${name}`; | |
| document.getElementById('queryInput').focus(); | |
| } | |
| // close modal on backdrop click | |
| document.getElementById('fileModal')?.addEventListener('click', function(e) { | |
| if (e.target === this) closeModal(); | |
| }); | |
| async function loadStatus() { | |
| try { | |
| const res = await fetch('/api/status'); | |
| const data = await res.json(); | |
| for (const [name, info] of Object.entries(data.files || {})) { | |
| if (!state.files[name]) state.files[name] = info; | |
| } | |
| updateAll(); | |
| if (Object.keys(state.files).length) document.getElementById('askBtn').disabled = false; | |
| } catch (e) {} | |
| } | |
| loadStatus(); | |