// --- 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 = 'Loading...'; fetch('/dataset/list') .then(res => res.json()) .then(entries => { tbody.innerHTML = ''; if(entries.length === 0) { tbody.innerHTML = 'Dataset is empty'; return; } entries.forEach(entry => { const row = ` ${entry.id.split('_')[1]} ${new Date(entry.timestamp).toLocaleTimeString()} ${entry.node_count} nodes ${entry.snippet} `; 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"); }); }