cortex / static /script.js
Yashikaysn29's picture
Upload folder using huggingface_hub
18f908a verified
Raw
History Blame Contribute Delete
13.5 kB
// ── 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();