File size: 13,506 Bytes
18f908a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
// ── 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();