From 73b7f173c57a49f4054df135de8d5d8dac896506 Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 11 Jan 2026 11:44:21 +0000 Subject: [PATCH] Use cache_id for plan node details and show input/output media MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- server.py | 199 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 130 insertions(+), 69 deletions(-) diff --git a/server.py b/server.py index 22a06f4..f493ab0 100644 --- a/server.py +++ b/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('

Login required

', 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('

Plan not found

') - # 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'

Step {step_id} not found

') + return HTMLResponse(f'

Step with cache {cache_id[:16]}... not found

') # 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'' + elif inp_media_type == "image": + inp_preview = f'' + else: + inp_preview = f'
File
' + else: + inp_preview = f'
Not cached
' + + input_items += f''' + + {inp_preview} +
+
{inp_name}
+
{inp_cache_id[:12]}...
+
+
+ ''' + + inputs_html = f''' +
+
Inputs ({len(input_steps)})
+
{input_items}
+
''' + + # 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'''
- +
Output
+
''' elif media_type == "image": - preview_html = f''' + output_preview = f'''
- +
Output
+ +
''' + elif media_type == "audio": + output_preview = f''' +
+
Output
+
''' elif step_cid: ipfs_gateway = IPFS_GATEWAY_URL.rstrip('/') if IPFS_GATEWAY_URL else "https://ipfs.io/ipfs" - preview_html = f''' + output_preview = f'''
- +
Output (IPFS)
+
''' # Build output link - output_html = "" + output_link = "" if step_cid: - output_html = f''' -
-
Output (IPFS)
- - {step_cid} - View - -
''' + output_link = f''' + + {step_cid[:24]}... + View + ''' elif has_cached and cache_id: - output_html = f''' - ''' + output_link = f''' + + {cache_id[:24]}... + View + ''' - # 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'
{key}:{value_str[:30]}
' config_html = f'''
-
Config
-
{config_json}
-
''' - - # Input steps - inputs_html = "" - if input_steps: - inputs_list = "".join([ - f'{inp[:16]}...' - for inp in input_steps - ]) - inputs_html = f''' -
-
Input Steps
-
{inputs_list}
+
Parameters
+
{config_items}
''' 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): - {preview_html} -
-
Step ID: {step_id}
-
Cache ID: {cache_id[:32] if cache_id else "N/A"}...
-
- {output_html} + {output_preview} + {output_link} {inputs_html} {config_html} +
+
Step ID: {step_id[:32]}...
+
Cache ID: {cache_id[:32]}...
+
''') @@ -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 = '
Loading...
'; - // 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