Use cache_id for plan node details and show input/output media
- Changed node click to use cache_id in URL instead of step_id - Updated route to lookup step by cache_id - Added input media previews showing thumbnails of each input step - Enhanced output preview with video/image/audio support - Added parameters section showing step config - Updated JavaScript to pass cacheId when clicking nodes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
199
server.py
199
server.py
@@ -1425,9 +1425,9 @@ async def load_plan_for_run_with_fallback(run: RunStatus) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
|
||||
@app.get("/run/{run_id}/plan/node/{step_id}", response_class=HTMLResponse)
|
||||
async def run_plan_node_detail(run_id: str, step_id: str, request: Request):
|
||||
"""HTMX partial: Get node detail HTML fragment."""
|
||||
@app.get("/run/{run_id}/plan/node/{cache_id}", response_class=HTMLResponse)
|
||||
async def run_plan_node_detail(run_id: str, cache_id: str, request: Request):
|
||||
"""HTMX partial: Get node detail HTML fragment by cache_id."""
|
||||
ctx = await get_user_context_from_cookie(request)
|
||||
if not ctx:
|
||||
return HTMLResponse('<p class="text-red-400">Login required</p>', status_code=401)
|
||||
@@ -1442,23 +1442,28 @@ async def run_plan_node_detail(run_id: str, step_id: str, request: Request):
|
||||
if not plan_data:
|
||||
return HTMLResponse('<p class="text-gray-400">Plan not found</p>')
|
||||
|
||||
# Find the step
|
||||
step = None
|
||||
# Build a lookup from cache_id to step and step_id to step
|
||||
steps_by_cache_id = {}
|
||||
steps_by_step_id = {}
|
||||
for s in plan_data.get("steps", []):
|
||||
if s.get("step_id") == step_id:
|
||||
step = s
|
||||
break
|
||||
if s.get("cache_id"):
|
||||
steps_by_cache_id[s["cache_id"]] = s
|
||||
if s.get("step_id"):
|
||||
steps_by_step_id[s["step_id"]] = s
|
||||
|
||||
# Find the step by cache_id
|
||||
step = steps_by_cache_id.get(cache_id)
|
||||
if not step:
|
||||
return HTMLResponse(f'<p class="text-gray-400">Step {step_id} not found</p>')
|
||||
return HTMLResponse(f'<p class="text-gray-400">Step with cache {cache_id[:16]}... not found</p>')
|
||||
|
||||
# Get step info
|
||||
step_id = step.get("step_id", "")
|
||||
step_name = step.get("name", step_id[:20])
|
||||
node_type = step.get("node_type", "EFFECT")
|
||||
cache_id = step.get("cache_id", "")
|
||||
config = step.get("config", {})
|
||||
level = step.get("level", 0)
|
||||
input_steps = step.get("input_steps", [])
|
||||
input_hashes = step.get("input_hashes", {})
|
||||
|
||||
# Check for IPFS CID
|
||||
step_cid = None
|
||||
@@ -1470,69 +1475,105 @@ async def run_plan_node_detail(run_id: str, step_id: str, request: Request):
|
||||
has_cached = cache_manager.has_content(cache_id) if cache_id else False
|
||||
color = NODE_COLORS.get(node_type, NODE_COLORS["default"])
|
||||
|
||||
# Build preview HTML
|
||||
preview_html = ""
|
||||
# Build INPUT media previews - show each input's cached content
|
||||
inputs_html = ""
|
||||
if input_steps:
|
||||
input_items = ""
|
||||
for inp_step_id in input_steps:
|
||||
inp_step = steps_by_step_id.get(inp_step_id)
|
||||
if inp_step:
|
||||
inp_cache_id = inp_step.get("cache_id", "")
|
||||
inp_name = inp_step.get("name", inp_step_id[:12])
|
||||
inp_has_cached = cache_manager.has_content(inp_cache_id) if inp_cache_id else False
|
||||
|
||||
# Build preview thumbnail for input
|
||||
inp_preview = ""
|
||||
if inp_has_cached and inp_cache_id:
|
||||
inp_media_type = detect_media_type(get_cache_path(inp_cache_id))
|
||||
if inp_media_type == "video":
|
||||
inp_preview = f'<video src="/cache/{inp_cache_id}/raw" class="w-full h-20 object-cover rounded" muted></video>'
|
||||
elif inp_media_type == "image":
|
||||
inp_preview = f'<img src="/cache/{inp_cache_id}/raw" class="w-full h-20 object-cover rounded">'
|
||||
else:
|
||||
inp_preview = f'<div class="w-full h-20 bg-dark-500 rounded flex items-center justify-center text-xs text-gray-400">File</div>'
|
||||
else:
|
||||
inp_preview = f'<div class="w-full h-20 bg-dark-500 rounded flex items-center justify-center text-xs text-gray-400">Not cached</div>'
|
||||
|
||||
input_items += f'''
|
||||
<a href="/cache/{inp_cache_id}" class="block bg-dark-600 rounded-lg overflow-hidden hover:bg-dark-500 transition-colors">
|
||||
{inp_preview}
|
||||
<div class="p-2">
|
||||
<div class="text-xs text-white truncate">{inp_name}</div>
|
||||
<div class="text-xs text-gray-500 font-mono truncate">{inp_cache_id[:12]}...</div>
|
||||
</div>
|
||||
</a>
|
||||
'''
|
||||
|
||||
inputs_html = f'''
|
||||
<div class="mt-4">
|
||||
<h5 class="text-sm font-medium text-gray-400 mb-2">Inputs ({len(input_steps)})</h5>
|
||||
<div class="grid grid-cols-2 gap-2">{input_items}</div>
|
||||
</div>'''
|
||||
|
||||
# Build OUTPUT preview
|
||||
output_preview = ""
|
||||
if has_cached and cache_id:
|
||||
media_type = detect_media_type(get_cache_path(cache_id))
|
||||
if media_type == "video":
|
||||
preview_html = f'''
|
||||
output_preview = f'''
|
||||
<div class="mb-4">
|
||||
<video src="/cache/{cache_id}/raw" controls muted class="w-full max-h-64 rounded-lg"></video>
|
||||
<h5 class="text-sm font-medium text-gray-400 mb-2">Output</h5>
|
||||
<video src="/cache/{cache_id}/raw" controls muted class="w-full max-h-48 rounded-lg"></video>
|
||||
</div>'''
|
||||
elif media_type == "image":
|
||||
preview_html = f'''
|
||||
output_preview = f'''
|
||||
<div class="mb-4">
|
||||
<img src="/cache/{cache_id}/raw" class="w-full max-h-64 rounded-lg object-contain">
|
||||
<h5 class="text-sm font-medium text-gray-400 mb-2">Output</h5>
|
||||
<img src="/cache/{cache_id}/raw" class="w-full max-h-48 rounded-lg object-contain">
|
||||
</div>'''
|
||||
elif media_type == "audio":
|
||||
output_preview = f'''
|
||||
<div class="mb-4">
|
||||
<h5 class="text-sm font-medium text-gray-400 mb-2">Output</h5>
|
||||
<audio src="/cache/{cache_id}/raw" controls class="w-full"></audio>
|
||||
</div>'''
|
||||
elif step_cid:
|
||||
ipfs_gateway = IPFS_GATEWAY_URL.rstrip('/') if IPFS_GATEWAY_URL else "https://ipfs.io/ipfs"
|
||||
preview_html = f'''
|
||||
output_preview = f'''
|
||||
<div class="mb-4">
|
||||
<video src="{ipfs_gateway}/{step_cid}" controls muted class="w-full max-h-64 rounded-lg"></video>
|
||||
<h5 class="text-sm font-medium text-gray-400 mb-2">Output (IPFS)</h5>
|
||||
<video src="{ipfs_gateway}/{step_cid}" controls muted class="w-full max-h-48 rounded-lg"></video>
|
||||
</div>'''
|
||||
|
||||
# Build output link
|
||||
output_html = ""
|
||||
output_link = ""
|
||||
if step_cid:
|
||||
output_html = f'''
|
||||
<div class="mt-4">
|
||||
<h5 class="text-sm font-medium text-gray-400 mb-2">Output (IPFS)</h5>
|
||||
<a href="/ipfs/{step_cid}" class="flex items-center justify-between bg-dark-600 rounded p-3 hover:bg-dark-500 transition-colors">
|
||||
<span class="font-mono text-xs text-gray-300 truncate">{step_cid}</span>
|
||||
<span class="px-3 py-1 bg-blue-600 text-white text-xs rounded ml-2">View</span>
|
||||
</a>
|
||||
</div>'''
|
||||
output_link = f'''
|
||||
<a href="/ipfs/{step_cid}" class="flex items-center justify-between bg-dark-600 rounded p-2 hover:bg-dark-500 transition-colors text-xs">
|
||||
<span class="font-mono text-gray-300 truncate">{step_cid[:24]}...</span>
|
||||
<span class="px-2 py-1 bg-blue-600 text-white rounded ml-2">View</span>
|
||||
</a>'''
|
||||
elif has_cached and cache_id:
|
||||
output_html = f'''
|
||||
<div class="mt-4">
|
||||
<h5 class="text-sm font-medium text-gray-400 mb-2">Output</h5>
|
||||
<a href="/cache/{cache_id}" class="flex items-center justify-between bg-dark-600 rounded p-3 hover:bg-dark-500 transition-colors">
|
||||
<span class="font-mono text-xs text-gray-300 truncate">{cache_id}</span>
|
||||
<span class="px-3 py-1 bg-blue-600 text-white text-xs rounded ml-2">View</span>
|
||||
</a>
|
||||
</div>'''
|
||||
output_link = f'''
|
||||
<a href="/cache/{cache_id}" class="flex items-center justify-between bg-dark-600 rounded p-2 hover:bg-dark-500 transition-colors text-xs">
|
||||
<span class="font-mono text-gray-300 truncate">{cache_id[:24]}...</span>
|
||||
<span class="px-2 py-1 bg-blue-600 text-white rounded ml-2">View</span>
|
||||
</a>'''
|
||||
|
||||
# Config display
|
||||
# Config/parameters display
|
||||
config_html = ""
|
||||
if config:
|
||||
config_json = json.dumps(config, indent=2)
|
||||
config_items = ""
|
||||
for key, value in config.items():
|
||||
if isinstance(value, (dict, list)):
|
||||
value_str = json.dumps(value)
|
||||
else:
|
||||
value_str = str(value)
|
||||
config_items += f'<div class="flex justify-between"><span class="text-gray-400">{key}:</span><span class="text-white">{value_str[:30]}</span></div>'
|
||||
config_html = f'''
|
||||
<div class="mt-4">
|
||||
<h5 class="text-sm font-medium text-gray-400 mb-2">Config</h5>
|
||||
<pre class="text-xs bg-dark-600 p-3 rounded overflow-x-auto max-h-40">{config_json}</pre>
|
||||
</div>'''
|
||||
|
||||
# Input steps
|
||||
inputs_html = ""
|
||||
if input_steps:
|
||||
inputs_list = "".join([
|
||||
f'<span class="px-2 py-1 bg-dark-500 rounded text-xs font-mono">{inp[:16]}...</span>'
|
||||
for inp in input_steps
|
||||
])
|
||||
inputs_html = f'''
|
||||
<div class="mt-4">
|
||||
<h5 class="text-sm font-medium text-gray-400 mb-2">Input Steps</h5>
|
||||
<div class="flex flex-wrap gap-2">{inputs_list}</div>
|
||||
<h5 class="text-sm font-medium text-gray-400 mb-2">Parameters</h5>
|
||||
<div class="bg-dark-600 rounded p-3 text-xs space-y-1">{config_items}</div>
|
||||
</div>'''
|
||||
|
||||
status = "cached" if (has_cached or step_cid) else ("completed" if run.status == "completed" else "pending")
|
||||
@@ -1554,14 +1595,14 @@ async def run_plan_node_detail(run_id: str, step_id: str, request: Request):
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{preview_html}
|
||||
<div class="text-sm space-y-2">
|
||||
<div><span class="text-gray-400">Step ID:</span> <span class="font-mono text-xs">{step_id}</span></div>
|
||||
<div><span class="text-gray-400">Cache ID:</span> <span class="font-mono text-xs">{cache_id[:32] if cache_id else "N/A"}...</span></div>
|
||||
</div>
|
||||
{output_html}
|
||||
{output_preview}
|
||||
{output_link}
|
||||
{inputs_html}
|
||||
{config_html}
|
||||
<div class="mt-4 text-xs text-gray-500 space-y-1">
|
||||
<div><span class="text-gray-400">Step ID:</span> <span class="font-mono">{step_id[:32]}...</span></div>
|
||||
<div><span class="text-gray-400">Cache ID:</span> <span class="font-mono">{cache_id[:32]}...</span></div>
|
||||
</div>
|
||||
''')
|
||||
|
||||
|
||||
@@ -5004,8 +5045,8 @@ def render_dag_cytoscape(nodes_json: str, edges_json: str, container_id: str = "
|
||||
var currentRunId = '{run_id}';
|
||||
var currentNodeId = '{initial_node}';
|
||||
|
||||
function loadNodeDetail(stepId) {{
|
||||
if (!currentRunId || !stepId) return;
|
||||
function loadNodeDetail(stepId, cacheId) {{
|
||||
if (!currentRunId || !cacheId) return;
|
||||
currentNodeId = stepId;
|
||||
|
||||
var detailsEl = document.getElementById('{container_id}-details');
|
||||
@@ -5014,12 +5055,12 @@ def render_dag_cytoscape(nodes_json: str, edges_json: str, container_id: str = "
|
||||
detailsEl.classList.remove('hidden');
|
||||
contentEl.innerHTML = '<div class="text-gray-400">Loading...</div>';
|
||||
|
||||
// Update URL without full page reload
|
||||
var newUrl = '/run/' + currentRunId + '/plan?node=' + encodeURIComponent(stepId);
|
||||
history.pushState({{ node: stepId }}, '', newUrl);
|
||||
// Update URL without full page reload - use cache_id
|
||||
var newUrl = '/run/' + currentRunId + '/plan?node=' + encodeURIComponent(cacheId);
|
||||
history.pushState({{ node: cacheId }}, '', newUrl);
|
||||
|
||||
// Fetch node details via HTMX-style fetch
|
||||
fetch('/run/' + currentRunId + '/plan/node/' + encodeURIComponent(stepId))
|
||||
// Fetch node details via cache_id
|
||||
fetch('/run/' + currentRunId + '/plan/node/' + encodeURIComponent(cacheId))
|
||||
.then(function(response) {{ return response.text(); }})
|
||||
.then(function(html) {{
|
||||
contentEl.innerHTML = html;
|
||||
@@ -5060,7 +5101,18 @@ def render_dag_cytoscape(nodes_json: str, edges_json: str, container_id: str = "
|
||||
// Handle browser back/forward
|
||||
window.addEventListener('popstate', function(event) {{
|
||||
if (event.state && event.state.node) {{
|
||||
loadNodeDetail(event.state.node);
|
||||
// event.state.node is now cache_id, find the corresponding step
|
||||
var cacheId = event.state.node;
|
||||
if (window.artdagCy) {{
|
||||
var nodes = window.artdagCy.nodes();
|
||||
for (var i = 0; i < nodes.length; i++) {{
|
||||
if (nodes[i].data('cacheId') === cacheId) {{
|
||||
loadNodeDetail(nodes[i].data('id'), cacheId);
|
||||
nodes[i].select();
|
||||
return;
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}} else {{
|
||||
closeNodeDetail();
|
||||
}}
|
||||
@@ -5157,7 +5209,8 @@ def render_dag_cytoscape(nodes_json: str, edges_json: str, container_id: str = "
|
||||
window.artdagCy.on('tap', 'node', function(evt) {{
|
||||
var node = evt.target;
|
||||
var stepId = node.data('id');
|
||||
loadNodeDetail(stepId);
|
||||
var cacheId = node.data('cacheId');
|
||||
loadNodeDetail(stepId, cacheId);
|
||||
}});
|
||||
|
||||
// Click on background closes detail
|
||||
@@ -5167,9 +5220,17 @@ def render_dag_cytoscape(nodes_json: str, edges_json: str, container_id: str = "
|
||||
}}
|
||||
}});
|
||||
|
||||
// Load initial node from URL if specified
|
||||
// Load initial node from URL if specified (currentNodeId is cache_id)
|
||||
if (currentNodeId) {{
|
||||
loadNodeDetail(currentNodeId);
|
||||
// Find node by cacheId and load its details
|
||||
var nodes = window.artdagCy.nodes();
|
||||
for (var i = 0; i < nodes.length; i++) {{
|
||||
if (nodes[i].data('cacheId') === currentNodeId) {{
|
||||
loadNodeDetail(nodes[i].data('id'), currentNodeId);
|
||||
nodes[i].select();
|
||||
break;
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
// WebSocket update function for real-time status updates
|
||||
|
||||
Reference in New Issue
Block a user