Spaces:
Sleeping
Sleeping
| // --- Globals & Config --- | |
| const CONFIG = { | |
| viewportPadding: 200, | |
| nodeWidth: 200, | |
| nodeHeight: 50, | |
| indentWidth: 60, | |
| rowHeight: 80 | |
| }; | |
| let currentTab = 'visualizer'; | |
| let sourceLines = []; | |
| let allNodes = []; | |
| let isTicking = false; | |
| // --- Konva Setup --- | |
| const stage = new Konva.Stage({ | |
| container: 'container', | |
| width: window.innerWidth - 400, // Adjust for sidebar width | |
| height: window.innerHeight, | |
| draggable: true | |
| }); | |
| const layer = new Konva.Layer(); | |
| stage.add(layer); | |
| // --- UI Logic: Tabs --- | |
| function switchTab(tabId) { | |
| document.querySelectorAll('.tab-pane').forEach(el => el.classList.remove('active')); | |
| document.getElementById(`tab-${tabId}`).classList.add('active'); | |
| document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active')); | |
| // Find the button that triggered this or match by ID if needed | |
| const btn = document.querySelector(`button[onclick="switchTab('${tabId}')"]`); | |
| if(btn) btn.classList.add('active'); | |
| currentTab = tabId; | |
| if (tabId === 'dataset') { | |
| loadDatasetTable(); | |
| } | |
| } | |
| // --- UI Logic: Status --- | |
| function setStatus(msg, state) { | |
| const el = document.getElementById('statusIndicator'); | |
| el.innerText = msg; | |
| el.className = ''; | |
| if(state === 'ready') el.classList.add('status-ready'); | |
| if(state === 'working') el.classList.add('status-working'); | |
| if(state === 'success') el.classList.add('status-success'); | |
| if(state === 'error') el.classList.add('status-error'); | |
| } | |
| // --- Core: Culling & Rendering Optimization --- | |
| function updateVisibleNodes() { | |
| isTicking = false; | |
| const scale = stage.scaleX(); | |
| const stageX = stage.x(); | |
| const stageY = stage.y(); | |
| const viewX = -(stageX / scale) - CONFIG.viewportPadding; | |
| const viewY = -(stageY / scale) - CONFIG.viewportPadding; | |
| const viewW = (stage.width() / scale) + (CONFIG.viewportPadding * 2); | |
| const viewH = (stage.height() / scale) + (CONFIG.viewportPadding * 2); | |
| const viewRight = viewX + viewW; | |
| const viewBottom = viewY + viewH; | |
| let nodesChanged = false; | |
| for (let i = 0; i < allNodes.length; i++) { | |
| const node = allNodes[i]; | |
| const isVisible = ( | |
| node.x < viewRight && | |
| node.x + CONFIG.nodeWidth > viewX && | |
| node.y < viewBottom && | |
| node.y + CONFIG.nodeHeight > viewY | |
| ); | |
| if (node.visible !== isVisible) { | |
| node.group.visible(isVisible); | |
| node.visible = isVisible; | |
| nodesChanged = true; | |
| } | |
| } | |
| if (nodesChanged) { | |
| layer.batchDraw(); | |
| } | |
| } | |
| function requestUpdate() { | |
| if (!isTicking) { | |
| requestAnimationFrame(updateVisibleNodes); | |
| isTicking = true; | |
| } | |
| } | |
| // --- Canvas Interactions --- | |
| stage.on('dragmove', requestUpdate); | |
| stage.on('wheel', (e) => { | |
| e.evt.preventDefault(); | |
| const oldScale = stage.scaleX(); | |
| const pointer = stage.getPointerPosition(); | |
| const scaleBy = 1.05; | |
| const newScale = e.evt.deltaY > 0 ? oldScale / scaleBy : oldScale * scaleBy; | |
| stage.scale({ x: newScale, y: newScale }); | |
| const newPos = { | |
| x: pointer.x - (pointer.x - stage.x()) / oldScale * newScale, | |
| y: pointer.y - (pointer.y - stage.y()) / oldScale * newScale | |
| }; | |
| stage.position(newPos); | |
| requestUpdate(); | |
| }); | |
| window.addEventListener('resize', () => { | |
| stage.width(window.innerWidth - 400); | |
| stage.height(window.innerHeight); | |
| requestUpdate(); | |
| }); | |
| // --- API: Visualizer --- | |
| function visualize() { | |
| const code = document.getElementById('codeInput').value; | |
| setStatus('Parsing...', 'working'); | |
| fetch('/parse', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ code: code }) | |
| }) | |
| .then(res => res.json()) | |
| .then(data => { | |
| if(data.error) { | |
| alert(data.error); | |
| setStatus('Error', 'error'); | |
| } else { | |
| drawGraph(data, code); | |
| setStatus(`Rendered ${data.nodes.length} nodes`, 'ready'); | |
| } | |
| }) | |
| .catch(err => setStatus('Network Error', 'error')); | |
| } | |
| function drawGraph(data, fullSourceCode) { | |
| layer.destroyChildren(); | |
| allNodes = []; | |
| sourceLines = fullSourceCode.split('\n'); | |
| const nodeMap = {}; | |
| const colors = { | |
| 'function': '#a29bfe', 'class': '#e84393', 'if': '#fab1a0', | |
| 'for': '#fdcb6e', 'while': '#fdcb6e', 'return': '#55efc4', | |
| 'assigned_variable': '#74b9ff', 'import': '#b2bec3', 'try': '#ff7675' | |
| }; | |
| // Draw Nodes | |
| data.nodes.forEach((node, index) => { | |
| const x = 100 + (node.lvl * CONFIG.indentWidth); | |
| const y = 50 + (index * CONFIG.rowHeight); | |
| const group = new Konva.Group({ | |
| x: x, | |
| y: y, | |
| listening: true | |
| }); | |
| const rect = new Konva.Rect({ | |
| width: CONFIG.nodeWidth, | |
| height: CONFIG.nodeHeight, | |
| fill: '#2d3436', | |
| stroke: colors[node.type] || '#636e72', | |
| strokeWidth: 2, | |
| cornerRadius: 8, | |
| shadowColor: 'black', | |
| shadowBlur: 10, | |
| shadowOpacity: 0.3, | |
| hitStrokeWidth: 0 | |
| }); | |
| const text = new Konva.Text({ | |
| x: 10, y: 10, | |
| text: node.lbl, | |
| fontSize: 14, fontFamily: 'JetBrains Mono', fill: '#fff', | |
| width: 180, ellipsis: true, | |
| listening: false | |
| }); | |
| const vecText = new Konva.Text({ | |
| x: 10, y: 32, | |
| text: `V:[${node.vec[0]}, ${node.vec[2]}...]`, | |
| fontSize: 10, fontFamily: 'JetBrains Mono', fill: '#636e72', | |
| listening: false | |
| }); | |
| group.add(rect); | |
| group.add(text); | |
| group.add(vecText); | |
| // Interaction | |
| group.on('click', () => { | |
| const start = node.loc[0] - 1; | |
| const end = node.loc[1]; | |
| const snippet = sourceLines.slice(start, end).join('\n'); | |
| console.log(snippet); // Optional: log to console | |
| // Could add a toast or modal to show snippet here | |
| }); | |
| group.on('mouseover', () => { | |
| document.body.style.cursor = 'pointer'; | |
| rect.fill('#353b48'); | |
| layer.batchDraw(); | |
| }); | |
| group.on('mouseout', () => { | |
| document.body.style.cursor = 'default'; | |
| rect.fill('#2d3436'); | |
| layer.batchDraw(); | |
| }); | |
| layer.add(group); | |
| nodeMap[node.id] = { x, y }; | |
| allNodes.push({ group, x, y, visible: true }); | |
| }); | |
| // Draw Connections | |
| data.connections.forEach(conn => { | |
| const f = nodeMap[conn.f]; | |
| const t = nodeMap[conn.t]; | |
| if (f && t) { | |
| const line = new Konva.Line({ | |
| points: [f.x+20, f.y+50, f.x+20, t.y-10, t.x+20, t.y-10, t.x+20, t.y], | |
| stroke: '#636e72', | |
| strokeWidth: 2, | |
| tension: 0.2, | |
| opacity: 0.5, | |
| listening: false | |
| }); | |
| layer.add(line); | |
| line.moveToBottom(); | |
| } | |
| }); | |
| layer.batchDraw(); | |
| stage.x(50); | |
| stage.y(50); | |
| requestUpdate(); | |
| } | |
| // --- API: Dataset --- | |
| function addToDataset() { | |
| const code = document.getElementById('codeInput').value; | |
| setStatus('Saving...', 'working'); | |
| fetch('/dataset/add', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ code: code }) | |
| }) | |
| .then(res => res.json()) | |
| .then(data => { | |
| if(data.status === 'success') { | |
| setStatus('Saved to Dataset!', 'success'); | |
| } else { | |
| alert(data.message); | |
| setStatus('Save Failed', 'error'); | |
| } | |
| }) | |
| .catch(() => setStatus('Network Error', 'error')); | |
| } | |
| function loadDatasetTable() { | |
| const tbody = document.getElementById('datasetTableBody'); | |
| tbody.innerHTML = '<tr><td colspan="4">Loading...</td></tr>'; | |
| fetch('/dataset/list') | |
| .then(res => res.json()) | |
| .then(entries => { | |
| tbody.innerHTML = ''; | |
| if(entries.length === 0) { | |
| tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; opacity:0.5">Dataset is empty</td></tr>'; | |
| return; | |
| } | |
| entries.forEach(entry => { | |
| const row = ` | |
| <tr> | |
| <td class="mono">${entry.id.split('_')[1]}</td> | |
| <td>${new Date(entry.timestamp).toLocaleTimeString()}</td> | |
| <td><span class="badge">${entry.node_count} nodes</span></td> | |
| <td class="mono-dim">${entry.snippet}</td> | |
| </tr> | |
| `; | |
| tbody.innerHTML += row; | |
| }); | |
| }); | |
| } | |
| // --- API: Hugging Face Upload --- | |
| function openUploadModal() { | |
| document.getElementById('uploadModal').classList.remove('hidden'); | |
| } | |
| function closeUploadModal() { | |
| document.getElementById('uploadModal').classList.add('hidden'); | |
| } | |
| function performUpload() { | |
| const token = document.getElementById('hfToken').value; | |
| const repo = document.getElementById('hfRepo').value; | |
| if(!token || !repo) return alert("Please fill in both fields"); | |
| const btn = document.querySelector('#uploadModal .btn-primary'); | |
| const originalText = btn.innerText; | |
| btn.innerText = "Pushing..."; | |
| btn.disabled = true; | |
| fetch('/dataset/upload_hf', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ token: token, repo_id: repo }) | |
| }) | |
| .then(res => res.json()) | |
| .then(data => { | |
| btn.innerText = originalText; | |
| btn.disabled = false; | |
| if(data.status === 'success') { | |
| closeUploadModal(); | |
| alert(data.message); | |
| } else { | |
| alert("Error: " + data.message); | |
| } | |
| }) | |
| .catch(err => { | |
| btn.innerText = originalText; | |
| btn.disabled = false; | |
| alert("Network Error"); | |
| }); | |
| } |