Files
giles fd97812e3d Initial artdag-common shared library
Shared components for L1 and L2 servers:
- Jinja2 template system with base template and components
- Middleware for auth and content negotiation
- Pydantic models for requests/responses
- Utility functions for pagination, media, formatting
- Constants for Tailwind/HTMX/Cytoscape CDNs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 07:07:59 +00:00

177 lines
5.5 KiB
HTML

{#
Cytoscape.js DAG visualization component.
Usage:
{% from "components/dag.html" import dag_container, dag_scripts, dag_legend %}
{# In head block #}
{{ dag_scripts() }}
{# In content #}
{{ dag_container(id="plan-dag", height="400px") }}
{{ dag_legend() }}
{# In scripts block #}
<script>
initDag('plan-dag', {{ nodes | tojson }}, {{ edges | tojson }});
</script>
#}
{% macro dag_scripts() %}
<script src="{{ CYTOSCAPE_CDN }}"></script>
<script src="{{ DAGRE_CDN }}"></script>
<script src="{{ CYTOSCAPE_DAGRE_CDN }}"></script>
<script>
// Global Cytoscape instance for WebSocket updates
window.artdagCy = null;
function initDag(containerId, nodes, edges) {
const nodeColors = {{ NODE_COLORS | tojson }};
window.artdagCy = cytoscape({
container: document.getElementById(containerId),
elements: {
nodes: nodes,
edges: edges
},
style: [
{
selector: 'node',
style: {
'label': 'data(label)',
'text-valign': 'center',
'text-halign': 'center',
'background-color': function(ele) {
return nodeColors[ele.data('nodeType')] || nodeColors['default'];
},
'color': '#fff',
'font-size': '10px',
'width': 80,
'height': 40,
'shape': 'round-rectangle',
'text-wrap': 'wrap',
'text-max-width': '70px',
}
},
{
selector: 'node[status="cached"], node[status="completed"]',
style: {
'border-width': 3,
'border-color': '#22c55e'
}
},
{
selector: 'node[status="running"]',
style: {
'border-width': 3,
'border-color': '#eab308',
'border-style': 'dashed'
}
},
{
selector: 'node:selected',
style: {
'border-width': 3,
'border-color': '#3b82f6'
}
},
{
selector: 'edge',
style: {
'width': 2,
'line-color': '#6b7280',
'target-arrow-color': '#6b7280',
'target-arrow-shape': 'triangle',
'curve-style': 'bezier'
}
}
],
layout: {
name: 'dagre',
rankDir: 'TB',
nodeSep: 50,
rankSep: 80,
padding: 20
},
userZoomingEnabled: true,
userPanningEnabled: true,
boxSelectionEnabled: false
});
// Click handler for node details
window.artdagCy.on('tap', 'node', function(evt) {
const node = evt.target;
const data = node.data();
showNodeDetails(data);
});
return window.artdagCy;
}
function showNodeDetails(data) {
const panel = document.getElementById('node-details');
if (!panel) return;
panel.innerHTML = `
<h4 class="font-medium text-white mb-2">${data.label || data.id}</h4>
<dl class="space-y-1 text-sm">
<div class="flex justify-between">
<dt class="text-gray-400">Type</dt>
<dd class="text-white">${data.nodeType || 'Unknown'}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-400">Status</dt>
<dd class="text-white">${data.status || 'pending'}</dd>
</div>
${data.cacheId ? `
<div class="flex justify-between">
<dt class="text-gray-400">Cache ID</dt>
<dd class="text-white font-mono text-xs">${data.cacheId.substring(0, 16)}...</dd>
</div>
` : ''}
${data.level !== undefined ? `
<div class="flex justify-between">
<dt class="text-gray-400">Level</dt>
<dd class="text-white">${data.level}</dd>
</div>
` : ''}
</dl>
`;
panel.classList.remove('hidden');
}
// Future WebSocket support: update node status in real-time
function updateNodeStatus(stepId, status, cacheId) {
if (!window.artdagCy) return;
const node = window.artdagCy.getElementById(stepId);
if (node && node.length > 0) {
node.data('status', status);
if (cacheId) {
node.data('cacheId', cacheId);
}
}
}
</script>
{% endmacro %}
{% macro dag_container(id="dag-container", height="400px", class="") %}
<div id="{{ id }}" class="w-full bg-dark-700 rounded-lg {{ class }}" style="height: {{ height }};"></div>
<div id="node-details" class="hidden mt-4 p-4 bg-dark-600 rounded-lg"></div>
{% endmacro %}
{% macro dag_legend(node_types=None) %}
{% set types = node_types or ["SOURCE", "EFFECT", "_LIST"] %}
<div class="flex gap-4 text-sm flex-wrap mt-4">
{% for type in types %}
<span class="flex items-center gap-2">
<span class="w-4 h-4 rounded" style="background-color: {{ NODE_COLORS.get(type, NODE_COLORS.default) }}"></span>
{{ type }}
</span>
{% endfor %}
<span class="flex items-center gap-2">
<span class="w-4 h-4 rounded border-2 border-green-500 bg-dark-600"></span>
Cached
</span>
</div>
{% endmacro %}