Commit ·
3756f8f
1
Parent(s): 357d0cf
Add moving ant particles, fix ACO trail render, more contrast fixes
Browse filesThe 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.
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.
|
| 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(
|
| 1586 |
-
fn=
|
|
|
|
|
|
|
| 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])
|