Replace plan JSON with colored S-expression display
- Add plan_to_sexp() to convert plan to S-expression format - Syntax highlighting for S-expressions: - Pink: special forms (plan, recipe, def, ->) - Blue: primitives (source, effect, sequence, etc.) - Purple: keywords (:input, :name, etc.) - Green: strings - Yellow: parentheses - Gray: comments Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,59 @@ from ..services.run_service import RunService
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def plan_to_sexp(plan: dict, recipe_name: str = None) -> str:
|
||||||
|
"""Convert a plan to S-expression format for display."""
|
||||||
|
if not plan or not plan.get("steps"):
|
||||||
|
return ";; No plan available"
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append(f'(plan "{recipe_name or "unknown"}"')
|
||||||
|
|
||||||
|
# Group nodes by type for cleaner output
|
||||||
|
steps = plan.get("steps", [])
|
||||||
|
|
||||||
|
for step in steps:
|
||||||
|
step_id = step.get("id", "?")
|
||||||
|
step_type = step.get("type", "EFFECT")
|
||||||
|
inputs = step.get("inputs", [])
|
||||||
|
config = step.get("config", {})
|
||||||
|
|
||||||
|
# Build the step S-expression
|
||||||
|
if step_type == "SOURCE":
|
||||||
|
if config.get("input"):
|
||||||
|
# Variable input
|
||||||
|
input_name = config.get("name", config.get("input", "input"))
|
||||||
|
lines.append(f' (source :input "{input_name}")')
|
||||||
|
elif config.get("asset"):
|
||||||
|
# Fixed asset
|
||||||
|
lines.append(f' (source {config.get("asset", step_id)})')
|
||||||
|
else:
|
||||||
|
lines.append(f' (source {step_id})')
|
||||||
|
elif step_type == "EFFECT":
|
||||||
|
effect_name = config.get("effect", step_id)
|
||||||
|
if inputs:
|
||||||
|
inp_str = " ".join(inputs)
|
||||||
|
lines.append(f' (-> {inp_str} (effect {effect_name}))')
|
||||||
|
else:
|
||||||
|
lines.append(f' (effect {effect_name})')
|
||||||
|
elif step_type == "SEQUENCE":
|
||||||
|
if inputs:
|
||||||
|
inp_str = " ".join(inputs)
|
||||||
|
lines.append(f' (sequence {inp_str})')
|
||||||
|
else:
|
||||||
|
lines.append(f' (sequence)')
|
||||||
|
else:
|
||||||
|
# Generic node
|
||||||
|
if inputs:
|
||||||
|
inp_str = " ".join(inputs)
|
||||||
|
lines.append(f' ({step_type.lower()} {inp_str})')
|
||||||
|
else:
|
||||||
|
lines.append(f' ({step_type.lower()} {step_id})')
|
||||||
|
|
||||||
|
lines.append(')')
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
RUNS_KEY_PREFIX = "artdag:run:"
|
RUNS_KEY_PREFIX = "artdag:run:"
|
||||||
|
|
||||||
|
|
||||||
@@ -258,6 +311,9 @@ async def get_run(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Generate S-expression representation of the plan
|
||||||
|
plan_sexp = plan_to_sexp(plan, run.get("recipe_name"))
|
||||||
|
|
||||||
templates = get_templates(request)
|
templates = get_templates(request)
|
||||||
return render(templates, "runs/detail.html", request,
|
return render(templates, "runs/detail.html", request,
|
||||||
run=run,
|
run=run,
|
||||||
@@ -266,6 +322,7 @@ async def get_run(
|
|||||||
run_inputs=run_inputs,
|
run_inputs=run_inputs,
|
||||||
dag_elements=dag_elements,
|
dag_elements=dag_elements,
|
||||||
output_media_type=output_media_type,
|
output_media_type=output_media_type,
|
||||||
|
plan_sexp=plan_sexp,
|
||||||
active_tab="runs",
|
active_tab="runs",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -156,15 +156,41 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Plan JSON -->
|
<!-- Plan S-expression -->
|
||||||
<details class="mt-6">
|
<details class="mt-6" open>
|
||||||
<summary class="cursor-pointer text-gray-400 hover:text-white text-sm mb-2">
|
<summary class="cursor-pointer text-gray-400 hover:text-white text-sm mb-2">
|
||||||
Show Plan JSON
|
Plan (S-expression)
|
||||||
</summary>
|
</summary>
|
||||||
<div class="bg-gray-900 rounded-lg border border-gray-700 p-4 overflow-x-auto">
|
<div class="bg-gray-900 rounded-lg border border-gray-700 p-4 overflow-x-auto">
|
||||||
<pre class="text-sm text-gray-300 whitespace-pre-wrap">{{ plan | tojson(indent=2) }}</pre>
|
<pre class="text-sm font-mono sexp-code">{{ plan_sexp }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.sexp-code {
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
// Syntax highlight S-expressions
|
||||||
|
document.querySelectorAll('.sexp-code').forEach(el => {
|
||||||
|
let html = el.textContent;
|
||||||
|
// Comments
|
||||||
|
html = html.replace(/(;;.*)/g, '<span class="text-gray-500">$1</span>');
|
||||||
|
// Keywords (:keyword)
|
||||||
|
html = html.replace(/(:[a-zA-Z_-]+)/g, '<span class="text-purple-400">$1</span>');
|
||||||
|
// Strings
|
||||||
|
html = html.replace(/("(?:[^"\\]|\\.)*")/g, '<span class="text-green-400">$1</span>');
|
||||||
|
// Special forms
|
||||||
|
html = html.replace(/\b(plan|recipe|def|->)\b/g, '<span class="text-pink-400 font-semibold">$1</span>');
|
||||||
|
// Primitives
|
||||||
|
html = html.replace(/\((source|effect|sequence|segment|resize|transform|layer|blend|mux|analyze)\b/g,
|
||||||
|
'(<span class="text-blue-400">$1</span>');
|
||||||
|
// Parentheses
|
||||||
|
html = html.replace(/(\(|\))/g, '<span class="text-yellow-500">$1</span>');
|
||||||
|
el.innerHTML = html;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-gray-500">No plan available for this run.</p>
|
<p class="text-gray-500">No plan available for this run.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user