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()
|
||||
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:"
|
||||
|
||||
|
||||
@@ -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)
|
||||
return render(templates, "runs/detail.html", request,
|
||||
run=run,
|
||||
@@ -266,6 +322,7 @@ async def get_run(
|
||||
run_inputs=run_inputs,
|
||||
dag_elements=dag_elements,
|
||||
output_media_type=output_media_type,
|
||||
plan_sexp=plan_sexp,
|
||||
active_tab="runs",
|
||||
)
|
||||
|
||||
|
||||
@@ -156,15 +156,41 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Plan JSON -->
|
||||
<details class="mt-6">
|
||||
<!-- Plan S-expression -->
|
||||
<details class="mt-6" open>
|
||||
<summary class="cursor-pointer text-gray-400 hover:text-white text-sm mb-2">
|
||||
Show Plan JSON
|
||||
Plan (S-expression)
|
||||
</summary>
|
||||
<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>
|
||||
</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 %}
|
||||
<p class="text-gray-500">No plan available for this run.</p>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user