Fix plan visualization to show completed status and artifact links

- Check cache_manager.has_content(cache_id) to determine if step is cached
- Show green border for cached nodes in completed runs
- Display artifact preview (video) when clicking on cached nodes
- Add "View" button to access cached artifacts directly
- Simplify node data structure (hasCached instead of outputs array)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
giles
2026-01-11 01:03:11 +00:00
parent 3db606bf15
commit 41ceae1e6c

113
server.py
View File

@@ -1394,38 +1394,24 @@ async def run_plan_visualization(run_id: str, request: Request):
nodes = []
edges = []
steps = plan_data.get("steps", [])
all_outputs = plan_data.get("outputs", []) # Pre-built outputs if available
# Use run's execution results if available (from completed runs)
step_results = run.step_results or {}
if run.all_outputs:
all_outputs = run.all_outputs
# Build output lookup by step_id
outputs_by_step = {}
for output in all_outputs:
step_id = output.get("step_id")
if step_id:
if step_id not in outputs_by_step:
outputs_by_step[step_id] = []
outputs_by_step[step_id].append(output)
for step in steps:
node_type = step.get("node_type", "EFFECT")
color = NODE_COLORS.get(node_type, NODE_COLORS["default"])
step_id = step.get("step_id", "")
cache_id = step.get("cache_id", "")
# Get execution result for this step if available
step_result = step_results.get(step_id, {})
result_status = step_result.get("status")
# Check if this step's output exists in cache (completed)
# For completed runs, check the actual cache
has_cached = cache_manager.has_content(cache_id) if cache_id else False
# Determine status from result or plan
if result_status in ("completed", "cached", "completed_by_other"):
status = result_status
elif step.get("cached", False):
if has_cached:
status = "cached"
elif run.status == "completed":
# Run completed but this step not in cache - still mark as done
status = "cached"
elif run.status == "running":
status = "pending"
status = "running"
else:
status = "pending"
@@ -1438,13 +1424,6 @@ async def run_plan_visualization(run_id: str, request: Request):
else:
label = step_id[:12] + "..." if len(step_id) > 12 else step_id
# Get outputs for this step - prefer result outputs, then plan outputs
step_outputs = step_result.get("outputs", []) or step.get("outputs", []) or outputs_by_step.get(step_id, [])
output_cache_ids = [o.get("cache_id") for o in step_outputs]
# Get cache_id from result if available
cache_id = step_result.get("cache_id") or step.get("cache_id", "")
nodes.append({
"data": {
"id": step_id,
@@ -1456,8 +1435,7 @@ async def run_plan_visualization(run_id: str, request: Request):
"status": status,
"color": color,
"config": step.get("config"),
"outputs": step_outputs,
"outputCacheIds": output_cache_ids,
"hasCached": has_cached,
}
})
@@ -1495,12 +1473,9 @@ async def run_plan_visualization(run_id: str, request: Request):
# Stats summary - count from built nodes to reflect actual execution status
total = len(nodes)
completed_count = sum(1 for n in nodes if n["data"]["status"] in ("completed", "completed_by_other"))
cached_count = sum(1 for n in nodes if n["data"]["status"] == "cached")
pending_count = total - completed_count - cached_count
# Plan name for display
plan_name = run.plan_name or plan_data.get("recipe", run.recipe)
running_count = sum(1 for n in nodes if n["data"]["status"] == "running")
pending_count = total - cached_count - running_count
content = f'''
<a href="/runs" class="inline-flex items-center text-blue-400 hover:text-blue-300 mb-6">
@@ -4698,63 +4673,29 @@ def render_dag_cytoscape(nodes_json: str, edges_json: str, container_id: str = "
typeEl.textContent = node.data('nodeType') || '';
// Handle artifact preview
var outputs = node.data('outputs') || [];
var cacheId = node.data('cacheId');
var status = node.data('status');
var hasPlayableArtifact = false;
var hasCached = node.data('hasCached');
// Check for playable artifacts
if ((status === 'cached' || status === 'completed' || status === 'completed_by_other') && outputs.length > 0) {{
// Find first video/audio output
for (var i = 0; i < outputs.length; i++) {{
var output = outputs[i];
var mediaType = output.media_type || output.mediaType || '';
var outputCacheId = output.cache_id || output.cacheId || cacheId;
if (outputCacheId && (mediaType.startsWith('video/') || mediaType.startsWith('audio/'))) {{
hasPlayableArtifact = true;
videoPlayer.src = '/cache/' + outputCacheId + '/raw';
break;
}}
}}
}} else if ((status === 'cached' || status === 'completed') && cacheId) {{
// Try cacheId directly for single-output nodes
hasPlayableArtifact = true;
videoPlayer.src = '/cache/' + cacheId + '/raw';
}}
if (hasPlayableArtifact) {{
// Show video preview if artifact is cached
if (hasCached && cacheId) {{
previewEl.classList.remove('hidden');
videoPlayer.src = '/cache/' + cacheId + '/raw';
}} else {{
previewEl.classList.add('hidden');
videoPlayer.src = '';
}}
// Show outputs list
if (outputs.length > 0) {{
// Show artifact link if cached
if (hasCached && cacheId) {{
outputsList.classList.remove('hidden');
outputsContainer.innerHTML = '';
outputs.forEach(function(output, idx) {{
var outCacheId = output.cache_id || output.cacheId || '';
var outName = output.name || ('output_' + idx);
var outMediaType = output.media_type || output.mediaType || 'unknown';
var outStatus = output.status || (outCacheId ? 'cached' : 'pending');
var statusColor = outStatus === 'cached' || outStatus === 'completed' ? 'text-green-400' : 'text-yellow-400';
var html = '<div class="flex items-center justify-between bg-dark-600 rounded p-2">' +
'<div class="flex-1 min-w-0">' +
'<div class="text-sm text-gray-200 truncate">' + outName + '</div>' +
'<div class="text-xs text-gray-500">' + outMediaType + '</div>' +
'</div>' +
'<div class="flex items-center gap-2">' +
'<span class="text-xs ' + statusColor + '">' + outStatus + '</span>';
if (outCacheId && (outStatus === 'cached' || outStatus === 'completed')) {{
html += '<a href="/cache/' + outCacheId + '" class="text-blue-400 hover:text-blue-300" title="View artifact">' +
'<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">' +
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>' +
'</svg></a>';
}}
html += '</div></div>';
outputsContainer.innerHTML += html;
}});
outputsContainer.innerHTML = '<div class="flex items-center justify-between bg-dark-600 rounded p-2">' +
'<div class="flex-1 min-w-0">' +
'<div class="text-sm text-gray-200">Output Artifact</div>' +
'<div class="text-xs text-gray-500 font-mono truncate">' + cacheId.substring(0, 16) + '...</div>' +
'</div>' +
'<a href="/cache/' + cacheId + '" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-xs rounded transition-colors">View</a>' +
'</div>';
}} else {{
outputsList.classList.add('hidden');
}}
@@ -4778,13 +4719,13 @@ def render_dag_cytoscape(nodes_json: str, edges_json: str, container_id: str = "
}});
// WebSocket update function for real-time status updates
window.updateNodeStatus = function(stepId, status, cacheId, outputs) {{
window.updateNodeStatus = function(stepId, status, cacheId, hasCached) {{
if (!window.artdagCy) return;
var node = window.artdagCy.getElementById(stepId);
if (node && node.length) {{
node.data('status', status);
if (cacheId) node.data('cacheId', cacheId);
if (outputs) node.data('outputs', outputs);
if (hasCached !== undefined) node.data('hasCached', hasCached);
}}
}};
}});