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:
113
server.py
113
server.py
@@ -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);
|
||||
}}
|
||||
}};
|
||||
}});
|
||||
|
||||
Reference in New Issue
Block a user