Spaces:
Sleeping
Sleeping
| /* 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 `<pre><code class="hljs language-${lang || ''}">${highlighted}</code></pre>`; | |
| }; | |
| 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 = | |
| '<div class="tree-placeholder"><p>No structure found.</p></div>'; | |
| 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 = `<div class="message__meta"> | |
| <span class="message__role">${roleLabel}</span> | |
| ${badge ? `<span class="message__badge">${badge}</span>` : ''} | |
| </div> | |
| <div class="message__content">`; | |
| html += role === 'thinking' | |
| ? `<span class="spinner"></span>${content}` | |
| : marked.parse(content || ''); | |
| html += `</div>`; | |
| 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(); | |
| })(); |