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 = []
|
nodes = []
|
||||||
edges = []
|
edges = []
|
||||||
steps = plan_data.get("steps", [])
|
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:
|
for step in steps:
|
||||||
node_type = step.get("node_type", "EFFECT")
|
node_type = step.get("node_type", "EFFECT")
|
||||||
color = NODE_COLORS.get(node_type, NODE_COLORS["default"])
|
color = NODE_COLORS.get(node_type, NODE_COLORS["default"])
|
||||||
step_id = step.get("step_id", "")
|
step_id = step.get("step_id", "")
|
||||||
|
cache_id = step.get("cache_id", "")
|
||||||
|
|
||||||
# Get execution result for this step if available
|
# Check if this step's output exists in cache (completed)
|
||||||
step_result = step_results.get(step_id, {})
|
# For completed runs, check the actual cache
|
||||||
result_status = step_result.get("status")
|
has_cached = cache_manager.has_content(cache_id) if cache_id else False
|
||||||
|
|
||||||
# Determine status from result or plan
|
if has_cached:
|
||||||
if result_status in ("completed", "cached", "completed_by_other"):
|
status = "cached"
|
||||||
status = result_status
|
elif run.status == "completed":
|
||||||
elif step.get("cached", False):
|
# Run completed but this step not in cache - still mark as done
|
||||||
status = "cached"
|
status = "cached"
|
||||||
elif run.status == "running":
|
elif run.status == "running":
|
||||||
status = "pending"
|
status = "running"
|
||||||
else:
|
else:
|
||||||
status = "pending"
|
status = "pending"
|
||||||
|
|
||||||
@@ -1438,13 +1424,6 @@ async def run_plan_visualization(run_id: str, request: Request):
|
|||||||
else:
|
else:
|
||||||
label = step_id[:12] + "..." if len(step_id) > 12 else step_id
|
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({
|
nodes.append({
|
||||||
"data": {
|
"data": {
|
||||||
"id": step_id,
|
"id": step_id,
|
||||||
@@ -1456,8 +1435,7 @@ async def run_plan_visualization(run_id: str, request: Request):
|
|||||||
"status": status,
|
"status": status,
|
||||||
"color": color,
|
"color": color,
|
||||||
"config": step.get("config"),
|
"config": step.get("config"),
|
||||||
"outputs": step_outputs,
|
"hasCached": has_cached,
|
||||||
"outputCacheIds": output_cache_ids,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -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
|
# Stats summary - count from built nodes to reflect actual execution status
|
||||||
total = len(nodes)
|
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")
|
cached_count = sum(1 for n in nodes if n["data"]["status"] == "cached")
|
||||||
pending_count = total - completed_count - cached_count
|
running_count = sum(1 for n in nodes if n["data"]["status"] == "running")
|
||||||
|
pending_count = total - cached_count - running_count
|
||||||
# Plan name for display
|
|
||||||
plan_name = run.plan_name or plan_data.get("recipe", run.recipe)
|
|
||||||
|
|
||||||
content = f'''
|
content = f'''
|
||||||
<a href="/runs" class="inline-flex items-center text-blue-400 hover:text-blue-300 mb-6">
|
<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') || '';
|
typeEl.textContent = node.data('nodeType') || '';
|
||||||
|
|
||||||
// Handle artifact preview
|
// Handle artifact preview
|
||||||
var outputs = node.data('outputs') || [];
|
|
||||||
var cacheId = node.data('cacheId');
|
var cacheId = node.data('cacheId');
|
||||||
var status = node.data('status');
|
var status = node.data('status');
|
||||||
var hasPlayableArtifact = false;
|
var hasCached = node.data('hasCached');
|
||||||
|
|
||||||
// Check for playable artifacts
|
// Show video preview if artifact is cached
|
||||||
if ((status === 'cached' || status === 'completed' || status === 'completed_by_other') && outputs.length > 0) {{
|
if (hasCached && cacheId) {{
|
||||||
// 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) {{
|
|
||||||
previewEl.classList.remove('hidden');
|
previewEl.classList.remove('hidden');
|
||||||
|
videoPlayer.src = '/cache/' + cacheId + '/raw';
|
||||||
}} else {{
|
}} else {{
|
||||||
previewEl.classList.add('hidden');
|
previewEl.classList.add('hidden');
|
||||||
videoPlayer.src = '';
|
videoPlayer.src = '';
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Show outputs list
|
// Show artifact link if cached
|
||||||
if (outputs.length > 0) {{
|
if (hasCached && cacheId) {{
|
||||||
outputsList.classList.remove('hidden');
|
outputsList.classList.remove('hidden');
|
||||||
outputsContainer.innerHTML = '';
|
outputsContainer.innerHTML = '<div class="flex items-center justify-between bg-dark-600 rounded p-2">' +
|
||||||
outputs.forEach(function(output, idx) {{
|
'<div class="flex-1 min-w-0">' +
|
||||||
var outCacheId = output.cache_id || output.cacheId || '';
|
'<div class="text-sm text-gray-200">Output Artifact</div>' +
|
||||||
var outName = output.name || ('output_' + idx);
|
'<div class="text-xs text-gray-500 font-mono truncate">' + cacheId.substring(0, 16) + '...</div>' +
|
||||||
var outMediaType = output.media_type || output.mediaType || 'unknown';
|
'</div>' +
|
||||||
var outStatus = output.status || (outCacheId ? 'cached' : 'pending');
|
'<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>' +
|
||||||
var statusColor = outStatus === 'cached' || outStatus === 'completed' ? 'text-green-400' : 'text-yellow-400';
|
'</div>';
|
||||||
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;
|
|
||||||
}});
|
|
||||||
}} else {{
|
}} else {{
|
||||||
outputsList.classList.add('hidden');
|
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
|
// 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;
|
if (!window.artdagCy) return;
|
||||||
var node = window.artdagCy.getElementById(stepId);
|
var node = window.artdagCy.getElementById(stepId);
|
||||||
if (node && node.length) {{
|
if (node && node.length) {{
|
||||||
node.data('status', status);
|
node.data('status', status);
|
||||||
if (cacheId) node.data('cacheId', cacheId);
|
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