narniagooooo commited on
Commit
3756f8f
·
1 Parent(s): 357d0cf

Add moving ant particles, fix ACO trail render, more contrast fixes

Browse files

The ACO trail glow was silently never rendering: the .then(js=...)
step after run_swarm_gen read swarm_state, a gr.State, but gr.State
values aren't mirrored to the client the way JSON/HTML component
values are, so the JS callback always got an empty payload. Route
the swarm data through a hidden gr.JSON bridge (swarm_data_json)
instead, matching the pattern that already worked for the graph
render step.

Also add actual moving ant particles (small glowing dots walking the
top-tau edges via requestAnimationFrame) since only static trail
glow existed before, and force !important on toolbar button
backgrounds, node-detail bold values, and the Insights placeholder
text -- Gradio's own stylesheet was still winning those.

Files changed (1) hide show
  1. app.py +69 -11
app.py CHANGED
@@ -829,7 +829,7 @@ def handle_upload(file):
829
 
830
  def clear_all():
831
  return (
832
- "", None, EMPTY_STATS_HTML, EMPTY_SWARM_HTML, None, None, None,
833
  "Build a graph first, then click **Generate Insights**.",
834
  )
835
 
@@ -845,11 +845,11 @@ def run_swarm_gen(graph_state, algo_state):
845
  last = None
846
  for progress in acs.run(n_ants=40, n_iter=30):
847
  last = progress
848
- yield render_swarm_progress_html(G, progress, running=True), progress
849
 
850
  insights = compute_swarm_insights(G, algo_state, last)
851
  last = {**last, "insights": insights}
852
- yield render_swarm_progress_html(G, last, running=False, insights=insights), last
853
 
854
 
855
  # ═══════════════════════════════════════════════════════════════════════════
@@ -914,6 +914,7 @@ gradio-app, .gradio-container, body {
914
  .nd-name { font-family: var(--font-head); font-size: 14px; margin-bottom: 4px; color: var(--cyan) !important; }
915
  .nd-type { font-family: var(--font-mono); font-size: 9px; color: var(--fog) !important; letter-spacing: 0.08em; margin-bottom: 6px; }
916
  .nd-info { font-family: var(--font-body); font-size: 11.5px; color: var(--ice) !important; opacity: 0.95; line-height: 1.55; }
 
917
  .nd-placeholder { color: var(--fog) !important; font-style: italic; font-size: 11px; }
918
  #entity-preview { font-family: var(--font-mono); font-size: 10.5px; color: var(--lime) !important; }
919
 
@@ -999,11 +1000,14 @@ gradio-app, .gradio-container, body {
999
  }
1000
  .ov-toolbar { position: absolute; top: 12px; left: 12px; display: flex; gap: 6px; flex-wrap: wrap; z-index: 5; max-width: 70%; }
1001
  .ov-btn {
1002
- font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.07em; text-transform: uppercase;
1003
- padding: 6px 10px; border: 1px solid var(--border); background: rgba(6,13,24,0.85); color: var(--fog) !important;
1004
- cursor: pointer; backdrop-filter: blur(6px); border-radius: 3px; transition: all 0.15s;
 
 
 
 
1005
  }
1006
- .ov-btn:hover, .ov-btn.active { border-color: var(--cyan); color: var(--cyan) !important; background: rgba(0,229,255,0.12); box-shadow: 0 0 10px rgba(0,229,255,0.3); }
1007
  .legend { position: absolute; bottom: 12px; right: 12px; display: flex; gap: 10px; flex-wrap: wrap; z-index: 5; max-width: 55%; justify-content: flex-end; }
1008
  .legend-item { display: flex; align-items: center; gap: 5px; font-family: var(--font-mono); font-size: 9px; color: var(--fog) !important; }
1009
  .legend-dot { width: 8px; height: 8px; border-radius: 2px; }
@@ -1029,6 +1033,10 @@ gradio-app, .gradio-container, body {
1029
  .cm-item:hover { background: rgba(0,229,255,0.12); color: var(--cyan) !important; }
1030
 
1031
  #insights-output { font-family: var(--font-body) !important; }
 
 
 
 
1032
  #insights-output h2 {
1033
  font-family: var(--font-head) !important; font-size: 12px !important; letter-spacing: 0.07em;
1034
  text-transform: uppercase; color: var(--plasma) !important; border-bottom: 1px solid var(--border);
@@ -1093,6 +1101,7 @@ function shapePath(type, r) {{
1093
  const SKG = {{ state: {{}} }};
1094
 
1095
  SKG.clearGraph = function() {{
 
1096
  d3.select('#graph-svg').selectAll('*').remove();
1097
  d3.select('#minimap-svg').selectAll('*').remove();
1098
  const es = document.getElementById('empty-state');
@@ -1269,6 +1278,7 @@ function updateMinimapViewport(transform, W, H) {{
1269
 
1270
  SKG.renderGraph = function(payload) {{
1271
  if (!payload || !payload.nodes || !payload.nodes.length) return;
 
1272
  const wrap = document.getElementById('graph-canvas-wrap');
1273
  const es = document.getElementById('empty-state');
1274
  if (es) es.style.display = 'none';
@@ -1320,6 +1330,8 @@ SKG.renderGraph = function(payload) {{
1320
  .attr('filter', 'url(#aco-glow)')
1321
  .attr('pointer-events', 'none');
1322
 
 
 
1323
  const maxPR = payload.maxPagerank || 1;
1324
 
1325
  const nodeSel = zoomG.append('g').attr('class', 'nodes').selectAll('g')
@@ -1368,7 +1380,7 @@ SKG.renderGraph = function(payload) {{
1368
  }});
1369
 
1370
  SKG.state = {{
1371
- payload, svg, zoomG, zoomBehavior, simulation, linkSel, acoLinkSel, nodeSel,
1372
  W, H, layoutMode: 'force', pathMode: false, pathSelected: [],
1373
  }};
1374
  }};
@@ -1459,6 +1471,49 @@ SKG.renderACOTrails = function(swarmState) {{
1459
  const pathSet = new Set(swarmState.best_path);
1460
  st.nodeSel.select('.node-shape').attr('stroke-width', d => pathSet.has(d.id) ? 3.2 : 1.6);
1461
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1462
  }};
1463
 
1464
  function skgCheckDemoParam() {{
@@ -1545,6 +1600,7 @@ with gr.Blocks(title="Scripture Knowledge Graph") as demo:
1545
 
1546
  with gr.Column(scale=3, min_width=480):
1547
  graph_data_json = gr.JSON(visible=False)
 
1548
  gr.HTML(GRAPH_CONTAINER_HTML, elem_id="graph-outer")
1549
 
1550
  with gr.Column(scale=1, min_width=300, elem_id="sidebar-right"):
@@ -1579,11 +1635,13 @@ with gr.Blocks(title="Scripture Knowledge Graph") as demo:
1579
 
1580
  clear_btn.click(
1581
  fn=clear_all,
1582
- outputs=[notes_input, graph_data_json, stats_html, swarm_html, algo_state, graph_state, swarm_state, insights_out],
1583
  ).then(fn=None, outputs=[], js=JS_CLEAR_GRAPH)
1584
 
1585
- run_swarm_btn.click(fn=run_swarm_gen, inputs=[graph_state, algo_state], outputs=[swarm_html, swarm_state]).then(
1586
- fn=None, inputs=[swarm_state], outputs=[], js=JS_RENDER_ACO
 
 
1587
  )
1588
 
1589
  insights_btn.click(fn=generate_insights, inputs=[notes_input, graph_state, algo_state, swarm_state], outputs=[insights_out])
 
829
 
830
  def clear_all():
831
  return (
832
+ "", None, EMPTY_STATS_HTML, EMPTY_SWARM_HTML, None, None, None, None,
833
  "Build a graph first, then click **Generate Insights**.",
834
  )
835
 
 
845
  last = None
846
  for progress in acs.run(n_ants=40, n_iter=30):
847
  last = progress
848
+ yield render_swarm_progress_html(G, progress, running=True), progress, progress
849
 
850
  insights = compute_swarm_insights(G, algo_state, last)
851
  last = {**last, "insights": insights}
852
+ yield render_swarm_progress_html(G, last, running=False, insights=insights), last, last
853
 
854
 
855
  # ═══════════════════════════════════════════════════════════════════════════
 
914
  .nd-name { font-family: var(--font-head); font-size: 14px; margin-bottom: 4px; color: var(--cyan) !important; }
915
  .nd-type { font-family: var(--font-mono); font-size: 9px; color: var(--fog) !important; letter-spacing: 0.08em; margin-bottom: 6px; }
916
  .nd-info { font-family: var(--font-body); font-size: 11.5px; color: var(--ice) !important; opacity: 0.95; line-height: 1.55; }
917
+ .nd-info b { color: var(--cyan) !important; font-weight: 700; }
918
  .nd-placeholder { color: var(--fog) !important; font-style: italic; font-size: 11px; }
919
  #entity-preview { font-family: var(--font-mono); font-size: 10.5px; color: var(--lime) !important; }
920
 
 
1000
  }
1001
  .ov-toolbar { position: absolute; top: 12px; left: 12px; display: flex; gap: 6px; flex-wrap: wrap; z-index: 5; max-width: 70%; }
1002
  .ov-btn {
1003
+ font-family: var(--font-mono) !important; font-size: 9px !important; letter-spacing: 0.07em; text-transform: uppercase;
1004
+ padding: 6px 10px; border: 1px solid var(--border) !important; background: rgba(6,13,24,0.9) !important; color: var(--fog) !important;
1005
+ cursor: pointer; backdrop-filter: blur(6px); border-radius: 3px; transition: all 0.15s; box-shadow: none !important;
1006
+ }
1007
+ .ov-btn:hover, .ov-btn.active {
1008
+ border-color: var(--cyan) !important; color: var(--cyan) !important; background: rgba(0,229,255,0.16) !important;
1009
+ box-shadow: 0 0 10px rgba(0,229,255,0.3) !important;
1010
  }
 
1011
  .legend { position: absolute; bottom: 12px; right: 12px; display: flex; gap: 10px; flex-wrap: wrap; z-index: 5; max-width: 55%; justify-content: flex-end; }
1012
  .legend-item { display: flex; align-items: center; gap: 5px; font-family: var(--font-mono); font-size: 9px; color: var(--fog) !important; }
1013
  .legend-dot { width: 8px; height: 8px; border-radius: 2px; }
 
1033
  .cm-item:hover { background: rgba(0,229,255,0.12); color: var(--cyan) !important; }
1034
 
1035
  #insights-output { font-family: var(--font-body) !important; }
1036
+ #insights-output, #insights-output p, #insights-output li, #insights-output span, #insights-output div {
1037
+ color: var(--ice) !important;
1038
+ }
1039
+ #insights-output strong, #insights-output b { color: var(--cyan) !important; }
1040
  #insights-output h2 {
1041
  font-family: var(--font-head) !important; font-size: 12px !important; letter-spacing: 0.07em;
1042
  text-transform: uppercase; color: var(--plasma) !important; border-bottom: 1px solid var(--border);
 
1101
  const SKG = {{ state: {{}} }};
1102
 
1103
  SKG.clearGraph = function() {{
1104
+ if (SKG.antsRafId) {{ cancelAnimationFrame(SKG.antsRafId); SKG.antsRafId = null; }}
1105
  d3.select('#graph-svg').selectAll('*').remove();
1106
  d3.select('#minimap-svg').selectAll('*').remove();
1107
  const es = document.getElementById('empty-state');
 
1278
 
1279
  SKG.renderGraph = function(payload) {{
1280
  if (!payload || !payload.nodes || !payload.nodes.length) return;
1281
+ if (SKG.antsRafId) {{ cancelAnimationFrame(SKG.antsRafId); SKG.antsRafId = null; }}
1282
  const wrap = document.getElementById('graph-canvas-wrap');
1283
  const es = document.getElementById('empty-state');
1284
  if (es) es.style.display = 'none';
 
1330
  .attr('filter', 'url(#aco-glow)')
1331
  .attr('pointer-events', 'none');
1332
 
1333
+ const antsG = zoomG.append('g').attr('class', 'ants-g');
1334
+
1335
  const maxPR = payload.maxPagerank || 1;
1336
 
1337
  const nodeSel = zoomG.append('g').attr('class', 'nodes').selectAll('g')
 
1380
  }});
1381
 
1382
  SKG.state = {{
1383
+ payload, svg, zoomG, zoomBehavior, simulation, linkSel, acoLinkSel, nodeSel, antsG,
1384
  W, H, layoutMode: 'force', pathMode: false, pathSelected: [],
1385
  }};
1386
  }};
 
1471
  const pathSet = new Set(swarmState.best_path);
1472
  st.nodeSel.select('.node-shape').attr('stroke-width', d => pathSet.has(d.id) ? 3.2 : 1.6);
1473
  }}
1474
+
1475
+ SKG.startAntAnimation(tau);
1476
+ }};
1477
+
1478
+ SKG.startAntAnimation = function(tau) {{
1479
+ const st = SKG.state;
1480
+ if (SKG.antsRafId) {{ cancelAnimationFrame(SKG.antsRafId); SKG.antsRafId = null; }}
1481
+ if (!st.antsG || !st.payload || !st.payload.edges.length) return;
1482
+
1483
+ const scored = st.payload.edges.map(e => {{
1484
+ const s = e.source.id || e.source, t = e.target.id || e.target;
1485
+ const v = tau[[s, t].sort().join('|||')] || 0;
1486
+ return {{ edge: e, v }};
1487
+ }}).sort((a, b) => b.v - a.v);
1488
+
1489
+ const topEdges = scored.slice(0, Math.min(16, scored.length)).filter(x => x.v > 0).map(x => x.edge);
1490
+ st.antsG.selectAll('circle').remove();
1491
+ if (!topEdges.length) return;
1492
+
1493
+ const ants = topEdges.map(edge => ({{
1494
+ edge, t: Math.random(), dir: Math.random() < 0.5 ? 1 : -1, speed: 0.15 + Math.random() * 0.2,
1495
+ }}));
1496
+
1497
+ const antSel = st.antsG.selectAll('circle').data(ants).enter().append('circle')
1498
+ .attr('r', 2.4)
1499
+ .attr('fill', '#88ffaa')
1500
+ .attr('filter', 'url(#aco-glow)');
1501
+
1502
+ let lastTime = performance.now();
1503
+ function tick(now) {{
1504
+ const dt = Math.min((now - lastTime) / 1000, 0.1);
1505
+ lastTime = now;
1506
+ antSel.each(function(a) {{
1507
+ a.t += a.dir * a.speed * dt;
1508
+ if (a.t > 1) {{ a.t = 1; a.dir = -1; }}
1509
+ if (a.t < 0) {{ a.t = 0; a.dir = 1; }}
1510
+ }});
1511
+ antSel
1512
+ .attr('cx', a => a.edge.source.x + (a.edge.target.x - a.edge.source.x) * a.t)
1513
+ .attr('cy', a => a.edge.source.y + (a.edge.target.y - a.edge.source.y) * a.t);
1514
+ SKG.antsRafId = requestAnimationFrame(tick);
1515
+ }}
1516
+ SKG.antsRafId = requestAnimationFrame(tick);
1517
  }};
1518
 
1519
  function skgCheckDemoParam() {{
 
1600
 
1601
  with gr.Column(scale=3, min_width=480):
1602
  graph_data_json = gr.JSON(visible=False)
1603
+ swarm_data_json = gr.JSON(visible=False)
1604
  gr.HTML(GRAPH_CONTAINER_HTML, elem_id="graph-outer")
1605
 
1606
  with gr.Column(scale=1, min_width=300, elem_id="sidebar-right"):
 
1635
 
1636
  clear_btn.click(
1637
  fn=clear_all,
1638
+ outputs=[notes_input, graph_data_json, stats_html, swarm_html, algo_state, graph_state, swarm_state, swarm_data_json, insights_out],
1639
  ).then(fn=None, outputs=[], js=JS_CLEAR_GRAPH)
1640
 
1641
+ run_swarm_btn.click(
1642
+ fn=run_swarm_gen, inputs=[graph_state, algo_state], outputs=[swarm_html, swarm_data_json, swarm_state]
1643
+ ).then(
1644
+ fn=None, inputs=[swarm_data_json], outputs=[], js=JS_RENDER_ACO
1645
  )
1646
 
1647
  insights_btn.click(fn=generate_insights, inputs=[notes_input, graph_state, algo_state, swarm_state], outputs=[insights_out])