/* app.js — Codebase Oracle frontend logic */ const API = window.location.origin; // ── State ───────────────────────────────────────────────────────────────────── const state = { indexed: false, querying: false, queryType: 'micro', macroSubtype: 'overall_architecture', lastResponse: '', }; // ── DOM Refs ────────────────────────────────────────────────────────────────── const $ = id => document.getElementById(id); const dom = { pathInput: $('pathInput'), zipLabel: $('zipLabelText'), zipInput: $('zipInput'), zipIndexBtn: $('zipIndexBtn'), zipIndexBtnText: $('zipIndexBtnText'), zipIndexSpinner: $('zipIndexSpinner'), indexBtn: $('indexBtn'), indexBtnText: $('indexBtnText'), indexSpinner: $('indexSpinner'), statusBadge: $('statusBadge'), statusText: $('statusText'), chunkCount: $('chunkCount'), treeContainer: $('treeContainer'), messages: $('messages'), queryInput: $('queryInput'), sendBtn: $('sendBtn'), functionInput: $('functionInput'), classInput: $('classInput'), moduleInput: $('moduleInput'), macroSubtypes: $('macroSubtypes'), }; // ── Marked config ───────────────────────────────────────────────────────────── marked.setOptions({ breaks: true, gfm: true, }); marked.use({ renderer: (() => { const r = new marked.Renderer(); r.code = (code, lang) => { const highlighted = (lang && hljs.getLanguage(lang)) ? hljs.highlight(code, { language: lang }).value : hljs.highlightAuto(code).value; return `
${highlighted}
`; }; return r; })(), }); // ── Status ──────────────────────────────────────────────────────────────────── function setStatus(type, text) { dom.statusBadge.className = `status-badge status-badge--${type}`; dom.statusText.textContent = text; } // ── Index ───────────────────────────────────────────────────────────────────── dom.zipInput.addEventListener('change', () => { if (dom.zipInput.files.length > 0) { dom.zipLabel.textContent = dom.zipInput.files[0].name; document.getElementById('zipLabel').classList.add('has-file'); dom.pathInput.value = ''; } }); dom.indexBtn.addEventListener('click', async () => { const path = dom.pathInput.value.trim(); if (!path) { appendMessage('error', null, '❌ Provide an absolute path to index.'); return; } dom.indexBtn.disabled = true; dom.indexBtnText.classList.add('hidden'); dom.indexSpinner.classList.remove('hidden'); setStatus('indexing', 'Indexing…'); try { const res = await fetch(`${API}/index`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ root_path: path }), }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || 'Indexing failed'); state.indexed = true; setStatus('ready', `Ready · ${data.total_chunks} chunks`); dom.chunkCount.textContent = `${data.total_chunks} chunks`; dom.sendBtn.disabled = false; appendMessage('system', null, `✔ Indexed **${path}**\n\n` + `| Metric | Value |\n|--------|-------|\n` + `| Class chunks | ${data.class_chunks} |\n` + `| Function chunks | ${data.function_chunks} |\n` + `| Total chunks | ${data.total_chunks} |\n` + `| Graph nodes | ${data.graph_nodes} |\n` + `| Graph edges | ${data.graph_edges} |` ); loadTree(); } catch (err) { setStatus('error', 'Error'); appendMessage('error', null, `❌ ${err.message}`); } finally { dom.indexBtn.disabled = false; dom.indexBtnText.classList.remove('hidden'); dom.indexSpinner.classList.add('hidden'); } }); dom.zipIndexBtn.addEventListener('click', async () => { const zipFile = dom.zipInput.files[0]; if (!zipFile) { appendMessage('error', null, '❌ Please upload a ZIP file first.'); return; } dom.zipIndexBtn.disabled = true; dom.zipIndexBtnText.classList.add('hidden'); dom.zipIndexSpinner.classList.remove('hidden'); setStatus('indexing', 'Indexing…'); try { const form = new FormData(); form.append('file', zipFile); const res = await fetch(`${API}/upload-index`, { method: 'POST', body: form }); const data = await res.json(); if (!res.ok) throw new Error(data.detail || 'Indexing failed'); state.indexed = true; setStatus('ready', `Ready · ${data.total_chunks} chunks`); dom.chunkCount.textContent = `${data.total_chunks} chunks`; dom.sendBtn.disabled = false; appendMessage('system', null, `✔ Indexed **${zipFile.name}**\n\n` + `| Metric | Value |\n|--------|-------|\n` + `| Class chunks | ${data.class_chunks} |\n` + `| Function chunks | ${data.function_chunks} |\n` + `| Total chunks | ${data.total_chunks} |\n` + `| Graph nodes | ${data.graph_nodes} |\n` + `| Graph edges | ${data.graph_edges} |` ); loadTree(); } catch (err) { setStatus('error', 'Error'); appendMessage('error', null, `❌ ${err.message}`); } finally { dom.zipIndexBtn.disabled = false; dom.zipIndexBtnText.classList.remove('hidden'); dom.zipIndexSpinner.classList.add('hidden'); dom.zipLabel.textContent = 'Upload ZIP'; document.getElementById('zipLabel').classList.remove('has-file'); dom.zipInput.value = ''; } }); // ── Tree ────────────────────────────────────────────────────────────────────── async function loadTree() { try { const res = await fetch(`${API}/tree`); const data = await res.json(); if (!data.success || !data.tree.length) { dom.treeContainer.innerHTML = '

No structure found.

'; return; } dom.treeContainer.innerHTML = ''; data.tree.forEach(node => { dom.treeContainer.appendChild(buildTreeNode(node, 0)); }); } catch (err) { console.error('Tree load error:', err); } } const ICONS = { module: { icon: '📦', cls: 'icon-module' }, file: { icon: '📄', cls: 'icon-file' }, class: { icon: '🔷', cls: 'icon-class' }, function: { icon: 'ƒ', cls: 'icon-function' }, }; function buildTreeNode(node, depth) { const wrap = document.createElement('div'); const hasKids = node.children && node.children.length > 0; const info = ICONS[node.type] || ICONS.file; wrap.className = 'tree-node'; wrap.dataset.depth = depth; const row = document.createElement('div'); row.className = 'tree-node__row'; const toggle = document.createElement('span'); toggle.className = 'tree-node__toggle'; toggle.textContent = hasKids ? '▶' : ''; const icon = document.createElement('span'); icon.className = `tree-node__icon ${info.cls}`; icon.textContent = info.icon; const label = document.createElement('span'); label.className = 'tree-node__name'; label.textContent = node.name; row.append(toggle, icon, label); wrap.appendChild(row); if (hasKids) { const children = document.createElement('div'); children.className = 'tree-node__children'; node.children.forEach(child => { children.appendChild(buildTreeNode(child, depth + 1)); }); wrap.appendChild(children); row.addEventListener('click', () => { const open = children.classList.toggle('open'); toggle.classList.toggle('open', open); }); } // Click leaf → auto-fill inputs if (node.type === 'function') { row.addEventListener('click', () => { dom.functionInput.value = node.name; highlightRow(row); }); } if (node.type === 'class') { row.addEventListener('click', () => { dom.classInput.value = node.name; highlightRow(row); }); } return wrap; } function highlightRow(row) { document.querySelectorAll('.tree-node__row.selected') .forEach(r => r.classList.remove('selected')); row.classList.add('selected'); } // ── Query Type Tabs ─────────────────────────────────────────────────────────── document.querySelectorAll('.query-type-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.query-type-btn') .forEach(b => b.classList.remove('active')); btn.classList.add('active'); state.queryType = btn.dataset.type; updateControls(); }); }); document.querySelectorAll('.subtype-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.subtype-btn') .forEach(b => b.classList.remove('active')); btn.classList.add('active'); state.macroSubtype = btn.dataset.subtype; updateControls(); }); }); function updateControls() { const isMacro = state.queryType === 'macro'; const needsModule = isMacro && state.macroSubtype === 'module_responsibility'; dom.macroSubtypes.classList.toggle('hidden', !isMacro); dom.functionInput.classList.toggle('hidden', isMacro); dom.classInput.classList.toggle('hidden', isMacro); dom.moduleInput.classList.toggle('hidden', !needsModule); } // ── Textarea auto-resize ────────────────────────────────────────────────────── dom.queryInput.addEventListener('input', () => { dom.queryInput.style.height = 'auto'; dom.queryInput.style.height = Math.min(dom.queryInput.scrollHeight, 160) + 'px'; }); dom.queryInput.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (!dom.sendBtn.disabled) sendQuery(); } }); dom.sendBtn.addEventListener('click', sendQuery); // ── Send Query ──────────────────────────────────────────────────────────────── async function sendQuery() { const query = dom.queryInput.value.trim(); if (!query || state.querying) return; const badge = [ state.queryType, state.queryType === 'macro' ? state.macroSubtype.replace(/_/g, ' ') : dom.functionInput.value.trim(), ].filter(Boolean).join(' · '); appendMessage('user', badge, query); dom.queryInput.value = ''; dom.queryInput.style.height = 'auto'; const thinkId = appendThinking(); state.querying = true; dom.sendBtn.disabled = true; try { const res = await fetch(`${API}/query`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query_type: state.queryType, query, subtype: state.queryType === 'macro' ? state.macroSubtype : '', function_name: dom.functionInput.value.trim(), class_name: dom.classInput.value.trim(), module_name: dom.moduleInput.value.trim(), followup: !!state.lastResponse, previous_response: state.lastResponse, }), }); const data = await res.json(); removeMessage(thinkId); if (!res.ok) throw new Error(data.detail || 'Query failed'); if (!data.success) throw new Error(data.error || 'No result'); state.lastResponse = data.content; appendMessage('assistant', buildMetaBadge(data.metadata), data.content); } catch (err) { removeMessage(thinkId); appendMessage('error', null, `❌ ${err.message}`); } finally { state.querying = false; dom.sendBtn.disabled = false; } } // ── Messages ────────────────────────────────────────────────────────────────── let msgCounter = 0; function appendMessage(role, badge, content) { const id = `msg-${++msgCounter}`; const div = document.createElement('div'); div.id = id; div.className = `message message--${role}`; const roleLabel = { user: 'You', assistant: 'Oracle', system: 'System', error: 'Error', thinking: 'Thinking', }[role] || role; let html = `
${roleLabel} ${badge ? `${badge}` : ''}
`; html += role === 'thinking' ? `${content}` : marked.parse(content || ''); html += `
`; div.innerHTML = html; dom.messages.appendChild(div); dom.messages.scrollTop = dom.messages.scrollHeight; return id; } function appendThinking() { return appendMessage('thinking', null, 'Analyzing codebase…'); } function removeMessage(id) { document.getElementById(id)?.remove(); } function buildMetaBadge(meta) { if (!meta) return null; return [ meta.query_type, meta.function_name, meta.chunks_used ? `${meta.chunks_used} chunks` : null, meta.context_chars ? `${meta.context_chars} chars` : null, ].filter(Boolean).join(' · '); } // ── Init ────────────────────────────────────────────────────────────────────── (async () => { try { const res = await fetch(`${API}/status`); const data = await res.json(); if (data.indexed) { state.indexed = true; setStatus('ready', `Ready · ${data.total_chunks} chunks`); dom.chunkCount.textContent = `${data.total_chunks} chunks`; dom.sendBtn.disabled = false; loadTree(); } } catch { /* server not up yet — stay idle */ } updateControls(); })();