noding / static /canvas.js
broadfield-dev's picture
Update static/canvas.js
e0139c6 verified
// --- 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");
});
}