broadfield-dev commited on
Commit
e0139c6
·
verified ·
1 Parent(s): b54f37a

Update static/canvas.js

Browse files
Files changed (1) hide show
  1. static/canvas.js +170 -85
static/canvas.js CHANGED
@@ -1,47 +1,64 @@
1
- // static/js/canvas.js
2
-
3
- // --- Configuration ---
4
  const CONFIG = {
5
- // Only render nodes within this padding of the viewport
6
- viewportPadding: 200,
7
- // Throttle scroll events to run only every 16ms (60fps)
8
- throttleMs: 16,
9
- // Visual settings
10
  nodeWidth: 200,
11
  nodeHeight: 50,
12
  indentWidth: 60,
13
  rowHeight: 80
14
  };
15
 
16
- // --- Setup Stage ---
17
- const width = window.innerWidth - 400; // Adjust for sidebar
18
- const height = window.innerHeight;
 
19
 
 
20
  const stage = new Konva.Stage({
21
  container: 'container',
22
- width: width,
23
- height: height,
24
  draggable: true
25
  });
26
 
27
  const layer = new Konva.Layer();
28
  stage.add(layer);
29
 
30
- // Global State
31
- let allNodes = []; // Array of { group, x, y, visible }
32
- let sourceLines = []; // Cached source code lines
33
- let isTicking = false; // For throttling
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- // --- 1. Optimization Core: Viewport Culling ---
36
  function updateVisibleNodes() {
37
  isTicking = false;
38
 
39
- // Get the visible viewport in "World Coordinates" (accounting for zoom/pan)
40
  const scale = stage.scaleX();
41
  const stageX = stage.x();
42
  const stageY = stage.y();
43
 
44
- // The logic: Invert the transform to find what part of the world is visible
45
  const viewX = -(stageX / scale) - CONFIG.viewportPadding;
46
  const viewY = -(stageY / scale) - CONFIG.viewportPadding;
47
  const viewW = (stage.width() / scale) + (CONFIG.viewportPadding * 2);
@@ -50,14 +67,10 @@ function updateVisibleNodes() {
50
  const viewRight = viewX + viewW;
51
  const viewBottom = viewY + viewH;
52
 
53
- // Batch updates to prevent multiple redraws
54
  let nodesChanged = false;
55
 
56
- // Fast loop (standard for-loop is faster than .forEach for massive arrays)
57
  for (let i = 0; i < allNodes.length; i++) {
58
  const node = allNodes[i];
59
-
60
- // Simple Bounding Box Collision Check
61
  const isVisible = (
62
  node.x < viewRight &&
63
  node.x + CONFIG.nodeWidth > viewX &&
@@ -65,7 +78,6 @@ function updateVisibleNodes() {
65
  node.y + CONFIG.nodeHeight > viewY
66
  );
67
 
68
- // Only touch the DOM/Konva object if state changes (Save CPU)
69
  if (node.visible !== isVisible) {
70
  node.group.visible(isVisible);
71
  node.visible = isVisible;
@@ -74,12 +86,10 @@ function updateVisibleNodes() {
74
  }
75
 
76
  if (nodesChanged) {
77
- // layer.batchDraw() is more efficient than layer.draw()
78
  layer.batchDraw();
79
  }
80
  }
81
 
82
- // Request Animation Frame Wrapper (Throttling)
83
  function requestUpdate() {
84
  if (!isTicking) {
85
  requestAnimationFrame(updateVisibleNodes);
@@ -87,18 +97,9 @@ function requestUpdate() {
87
  }
88
  }
89
 
90
- // Bind optimization to interactions
91
  stage.on('dragmove', requestUpdate);
92
- stage.on('wheel', (e) => {
93
- // ... (Zoom logic below) ...
94
- // After zoom, we must update visibility
95
- requestUpdate();
96
- });
97
-
98
-
99
- // --- 2. Standard Logic (Zoom & Draw) ---
100
 
101
- // Zoom Logic
102
  stage.on('wheel', (e) => {
103
  e.evt.preventDefault();
104
  const oldScale = stage.scaleX();
@@ -113,19 +114,19 @@ stage.on('wheel', (e) => {
113
  y: pointer.y - (pointer.y - stage.y()) / oldScale * newScale
114
  };
115
  stage.position(newPos);
 
116
  });
117
 
118
- // Resize Handler
119
  window.addEventListener('resize', () => {
120
  stage.width(window.innerWidth - 400);
121
  stage.height(window.innerHeight);
122
  requestUpdate();
123
  });
124
 
125
- // API Listener
126
- document.getElementById('btnVisualize').addEventListener('click', () => {
127
  const code = document.getElementById('codeInput').value;
128
- log('Parsing...');
129
 
130
  fetch('/parse', {
131
  method: 'POST',
@@ -134,40 +135,37 @@ document.getElementById('btnVisualize').addEventListener('click', () => {
134
  })
135
  .then(res => res.json())
136
  .then(data => {
137
- if(data.error) log(data.error, 'error');
138
- else {
 
 
139
  drawGraph(data, code);
140
- log(`Graph: ${data.nodes.length} nodes`);
141
  }
142
- });
143
- });
 
144
 
145
  function drawGraph(data, fullSourceCode) {
146
- // Cleanup old memory
147
  layer.destroyChildren();
148
- allNodes = [];
149
-
150
- // Cache source lines
151
  sourceLines = fullSourceCode.split('\n');
152
 
153
  const nodeMap = {};
154
-
155
- // --- Batch Create Nodes ---
 
 
 
 
 
156
  data.nodes.forEach((node, index) => {
157
  const x = 100 + (node.lvl * CONFIG.indentWidth);
158
  const y = 50 + (index * CONFIG.rowHeight);
159
 
160
- // Styling
161
- const colors = {
162
- 'function': '#a29bfe', 'class': '#e84393', 'if': '#fab1a0',
163
- 'for': '#fdcb6e', 'while': '#fdcb6e', 'return': '#55efc4',
164
- 'assigned_variable': '#74b9ff', 'import': '#b2bec3'
165
- };
166
-
167
  const group = new Konva.Group({
168
  x: x,
169
  y: y,
170
- // Optimization: Stop this group from catching mouse events if not needed
171
  listening: true
172
  });
173
 
@@ -181,7 +179,6 @@ function drawGraph(data, fullSourceCode) {
181
  shadowColor: 'black',
182
  shadowBlur: 10,
183
  shadowOpacity: 0.3,
184
- // Optimization: Perfect bounding box for hit detection
185
  hitStrokeWidth: 0
186
  });
187
 
@@ -190,7 +187,6 @@ function drawGraph(data, fullSourceCode) {
190
  text: node.lbl,
191
  fontSize: 14, fontFamily: 'JetBrains Mono', fill: '#fff',
192
  width: 180, ellipsis: true,
193
- // Optimization: Text doesn't need to listen to clicks, the Group/Rect handles it
194
  listening: false
195
  });
196
 
@@ -210,35 +206,40 @@ function drawGraph(data, fullSourceCode) {
210
  const start = node.loc[0] - 1;
211
  const end = node.loc[1];
212
  const snippet = sourceLines.slice(start, end).join('\n');
213
- log(`Source:\n${snippet}`);
 
214
  });
215
 
216
- // Hover
217
- group.on('mouseover', () => { document.body.style.cursor = 'pointer'; rect.fill('#353b48'); layer.batchDraw(); });
218
- group.on('mouseout', () => { document.body.style.cursor = 'default'; rect.fill('#2d3436'); layer.batchDraw(); });
 
 
 
 
 
 
 
 
219
 
220
  layer.add(group);
221
 
222
- // Save Reference for Culling Logic
223
  nodeMap[node.id] = { x, y };
224
- allNodes.push({
225
- group: group,
226
- x: x,
227
- y: y,
228
- visible: true
229
- });
230
  });
231
 
232
  // Draw Connections
233
- // Optimization: Draw connections on a generic "listening: false" shape to avoid hit-detection overhead
234
  data.connections.forEach(conn => {
235
  const f = nodeMap[conn.f];
236
  const t = nodeMap[conn.t];
237
  if (f && t) {
238
  const line = new Konva.Line({
239
  points: [f.x+20, f.y+50, f.x+20, t.y-10, t.x+20, t.y-10, t.x+20, t.y],
240
- stroke: '#636e72', strokeWidth: 2, tension: 0.2, opacity: 0.5,
241
- listening: false // Critical: Don't calculate mouse hits for lines
 
 
 
242
  });
243
  layer.add(line);
244
  line.moveToBottom();
@@ -246,15 +247,99 @@ function drawGraph(data, fullSourceCode) {
246
  });
247
 
248
  layer.batchDraw();
249
- // Initial cull calculation
250
- updateVisibleNodes();
 
251
  }
252
 
253
- function log(msg, type='info') {
254
- const consoleBody = document.getElementById('logOutput');
255
- const color = type === 'error' ? '#ff7675' : '#55efc4';
256
- if(consoleBody) {
257
- consoleBody.innerHTML += `<div style="color:${color}">> ${msg}</div>`;
258
- consoleBody.scrollTop = consoleBody.scrollHeight;
259
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  }
 
1
+ // --- Globals & Config ---
 
 
2
  const CONFIG = {
3
+ viewportPadding: 200,
 
 
 
 
4
  nodeWidth: 200,
5
  nodeHeight: 50,
6
  indentWidth: 60,
7
  rowHeight: 80
8
  };
9
 
10
+ let currentTab = 'visualizer';
11
+ let sourceLines = [];
12
+ let allNodes = [];
13
+ let isTicking = false;
14
 
15
+ // --- Konva Setup ---
16
  const stage = new Konva.Stage({
17
  container: 'container',
18
+ width: window.innerWidth - 400, // Adjust for sidebar width
19
+ height: window.innerHeight,
20
  draggable: true
21
  });
22
 
23
  const layer = new Konva.Layer();
24
  stage.add(layer);
25
 
26
+ // --- UI Logic: Tabs ---
27
+ function switchTab(tabId) {
28
+ document.querySelectorAll('.tab-pane').forEach(el => el.classList.remove('active'));
29
+ document.getElementById(`tab-${tabId}`).classList.add('active');
30
+
31
+ document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
32
+ // Find the button that triggered this or match by ID if needed
33
+ const btn = document.querySelector(`button[onclick="switchTab('${tabId}')"]`);
34
+ if(btn) btn.classList.add('active');
35
+
36
+ currentTab = tabId;
37
+
38
+ if (tabId === 'dataset') {
39
+ loadDatasetTable();
40
+ }
41
+ }
42
+
43
+ // --- UI Logic: Status ---
44
+ function setStatus(msg, state) {
45
+ const el = document.getElementById('statusIndicator');
46
+ el.innerText = msg;
47
+ el.className = '';
48
+ if(state === 'ready') el.classList.add('status-ready');
49
+ if(state === 'working') el.classList.add('status-working');
50
+ if(state === 'success') el.classList.add('status-success');
51
+ if(state === 'error') el.classList.add('status-error');
52
+ }
53
 
54
+ // --- Core: Culling & Rendering Optimization ---
55
  function updateVisibleNodes() {
56
  isTicking = false;
57
 
 
58
  const scale = stage.scaleX();
59
  const stageX = stage.x();
60
  const stageY = stage.y();
61
 
 
62
  const viewX = -(stageX / scale) - CONFIG.viewportPadding;
63
  const viewY = -(stageY / scale) - CONFIG.viewportPadding;
64
  const viewW = (stage.width() / scale) + (CONFIG.viewportPadding * 2);
 
67
  const viewRight = viewX + viewW;
68
  const viewBottom = viewY + viewH;
69
 
 
70
  let nodesChanged = false;
71
 
 
72
  for (let i = 0; i < allNodes.length; i++) {
73
  const node = allNodes[i];
 
 
74
  const isVisible = (
75
  node.x < viewRight &&
76
  node.x + CONFIG.nodeWidth > viewX &&
 
78
  node.y + CONFIG.nodeHeight > viewY
79
  );
80
 
 
81
  if (node.visible !== isVisible) {
82
  node.group.visible(isVisible);
83
  node.visible = isVisible;
 
86
  }
87
 
88
  if (nodesChanged) {
 
89
  layer.batchDraw();
90
  }
91
  }
92
 
 
93
  function requestUpdate() {
94
  if (!isTicking) {
95
  requestAnimationFrame(updateVisibleNodes);
 
97
  }
98
  }
99
 
100
+ // --- Canvas Interactions ---
101
  stage.on('dragmove', requestUpdate);
 
 
 
 
 
 
 
 
102
 
 
103
  stage.on('wheel', (e) => {
104
  e.evt.preventDefault();
105
  const oldScale = stage.scaleX();
 
114
  y: pointer.y - (pointer.y - stage.y()) / oldScale * newScale
115
  };
116
  stage.position(newPos);
117
+ requestUpdate();
118
  });
119
 
 
120
  window.addEventListener('resize', () => {
121
  stage.width(window.innerWidth - 400);
122
  stage.height(window.innerHeight);
123
  requestUpdate();
124
  });
125
 
126
+ // --- API: Visualizer ---
127
+ function visualize() {
128
  const code = document.getElementById('codeInput').value;
129
+ setStatus('Parsing...', 'working');
130
 
131
  fetch('/parse', {
132
  method: 'POST',
 
135
  })
136
  .then(res => res.json())
137
  .then(data => {
138
+ if(data.error) {
139
+ alert(data.error);
140
+ setStatus('Error', 'error');
141
+ } else {
142
  drawGraph(data, code);
143
+ setStatus(`Rendered ${data.nodes.length} nodes`, 'ready');
144
  }
145
+ })
146
+ .catch(err => setStatus('Network Error', 'error'));
147
+ }
148
 
149
  function drawGraph(data, fullSourceCode) {
 
150
  layer.destroyChildren();
151
+ allNodes = [];
 
 
152
  sourceLines = fullSourceCode.split('\n');
153
 
154
  const nodeMap = {};
155
+ const colors = {
156
+ 'function': '#a29bfe', 'class': '#e84393', 'if': '#fab1a0',
157
+ 'for': '#fdcb6e', 'while': '#fdcb6e', 'return': '#55efc4',
158
+ 'assigned_variable': '#74b9ff', 'import': '#b2bec3', 'try': '#ff7675'
159
+ };
160
+
161
+ // Draw Nodes
162
  data.nodes.forEach((node, index) => {
163
  const x = 100 + (node.lvl * CONFIG.indentWidth);
164
  const y = 50 + (index * CONFIG.rowHeight);
165
 
 
 
 
 
 
 
 
166
  const group = new Konva.Group({
167
  x: x,
168
  y: y,
 
169
  listening: true
170
  });
171
 
 
179
  shadowColor: 'black',
180
  shadowBlur: 10,
181
  shadowOpacity: 0.3,
 
182
  hitStrokeWidth: 0
183
  });
184
 
 
187
  text: node.lbl,
188
  fontSize: 14, fontFamily: 'JetBrains Mono', fill: '#fff',
189
  width: 180, ellipsis: true,
 
190
  listening: false
191
  });
192
 
 
206
  const start = node.loc[0] - 1;
207
  const end = node.loc[1];
208
  const snippet = sourceLines.slice(start, end).join('\n');
209
+ console.log(snippet); // Optional: log to console
210
+ // Could add a toast or modal to show snippet here
211
  });
212
 
213
+ group.on('mouseover', () => {
214
+ document.body.style.cursor = 'pointer';
215
+ rect.fill('#353b48');
216
+ layer.batchDraw();
217
+ });
218
+
219
+ group.on('mouseout', () => {
220
+ document.body.style.cursor = 'default';
221
+ rect.fill('#2d3436');
222
+ layer.batchDraw();
223
+ });
224
 
225
  layer.add(group);
226
 
 
227
  nodeMap[node.id] = { x, y };
228
+ allNodes.push({ group, x, y, visible: true });
 
 
 
 
 
229
  });
230
 
231
  // Draw Connections
 
232
  data.connections.forEach(conn => {
233
  const f = nodeMap[conn.f];
234
  const t = nodeMap[conn.t];
235
  if (f && t) {
236
  const line = new Konva.Line({
237
  points: [f.x+20, f.y+50, f.x+20, t.y-10, t.x+20, t.y-10, t.x+20, t.y],
238
+ stroke: '#636e72',
239
+ strokeWidth: 2,
240
+ tension: 0.2,
241
+ opacity: 0.5,
242
+ listening: false
243
  });
244
  layer.add(line);
245
  line.moveToBottom();
 
247
  });
248
 
249
  layer.batchDraw();
250
+ stage.x(50);
251
+ stage.y(50);
252
+ requestUpdate();
253
  }
254
 
255
+ // --- API: Dataset ---
256
+ function addToDataset() {
257
+ const code = document.getElementById('codeInput').value;
258
+ setStatus('Saving...', 'working');
259
+
260
+ fetch('/dataset/add', {
261
+ method: 'POST',
262
+ headers: { 'Content-Type': 'application/json' },
263
+ body: JSON.stringify({ code: code })
264
+ })
265
+ .then(res => res.json())
266
+ .then(data => {
267
+ if(data.status === 'success') {
268
+ setStatus('Saved to Dataset!', 'success');
269
+ } else {
270
+ alert(data.message);
271
+ setStatus('Save Failed', 'error');
272
+ }
273
+ })
274
+ .catch(() => setStatus('Network Error', 'error'));
275
+ }
276
+
277
+ function loadDatasetTable() {
278
+ const tbody = document.getElementById('datasetTableBody');
279
+ tbody.innerHTML = '<tr><td colspan="4">Loading...</td></tr>';
280
+
281
+ fetch('/dataset/list')
282
+ .then(res => res.json())
283
+ .then(entries => {
284
+ tbody.innerHTML = '';
285
+ if(entries.length === 0) {
286
+ tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; opacity:0.5">Dataset is empty</td></tr>';
287
+ return;
288
+ }
289
+
290
+ entries.forEach(entry => {
291
+ const row = `
292
+ <tr>
293
+ <td class="mono">${entry.id.split('_')[1]}</td>
294
+ <td>${new Date(entry.timestamp).toLocaleTimeString()}</td>
295
+ <td><span class="badge">${entry.node_count} nodes</span></td>
296
+ <td class="mono-dim">${entry.snippet}</td>
297
+ </tr>
298
+ `;
299
+ tbody.innerHTML += row;
300
+ });
301
+ });
302
+ }
303
+
304
+ // --- API: Hugging Face Upload ---
305
+ function openUploadModal() {
306
+ document.getElementById('uploadModal').classList.remove('hidden');
307
+ }
308
+
309
+ function closeUploadModal() {
310
+ document.getElementById('uploadModal').classList.add('hidden');
311
+ }
312
+
313
+ function performUpload() {
314
+ const token = document.getElementById('hfToken').value;
315
+ const repo = document.getElementById('hfRepo').value;
316
+
317
+ if(!token || !repo) return alert("Please fill in both fields");
318
+
319
+ const btn = document.querySelector('#uploadModal .btn-primary');
320
+ const originalText = btn.innerText;
321
+ btn.innerText = "Pushing...";
322
+ btn.disabled = true;
323
+
324
+ fetch('/dataset/upload_hf', {
325
+ method: 'POST',
326
+ headers: { 'Content-Type': 'application/json' },
327
+ body: JSON.stringify({ token: token, repo_id: repo })
328
+ })
329
+ .then(res => res.json())
330
+ .then(data => {
331
+ btn.innerText = originalText;
332
+ btn.disabled = false;
333
+ if(data.status === 'success') {
334
+ closeUploadModal();
335
+ alert(data.message);
336
+ } else {
337
+ alert("Error: " + data.message);
338
+ }
339
+ })
340
+ .catch(err => {
341
+ btn.innerText = originalText;
342
+ btn.disabled = false;
343
+ alert("Network Error");
344
+ });
345
  }