Skip to content

Commit 80df0b7

Browse files
committed
feat(web): Add workflow page types for integrated editor views
Add agent-as-editor panels, intent dispatch, and shared components for workflow page types on the kurt-simplification branch base. Backend: - parser.py: Add PageConfig, ColumnConfig, SceneConfig, TransitionConfig models with pages parsing in TOML/MD workflows and validation - registry.py: Add find_definition_by_workflow_id(), get_page_config(), get_definition_for_workflow() helpers using DoltDB - routes/pages.py: New route module with page data, edit dispatch, asset manifest endpoints - routes/files.py: Add /api/file/raw endpoint for media file serving - intent_dispatch.py: Intent-to-prompt conversion for agent editing Frontend: - DataTablePanel: Editable spreadsheet with seed data support - ImageViewerPanel: Image viewer with zoom and intent toolbar - MotionCanvasPanel: MC rendered output viewer with transport controls - VideoEditorPanel: Video player with trim/cut and intent dispatch - VideoSequencePanel: Multi-scene composition timeline - IntentToolbar: Shared intent capture (add text, shape, move, resize) - ShapePicker: SVG shape library browser - TextOverlayPopover: Text/font/color input for text intents - useFileWatch: Hook for polling file mtime changes - WorkflowRow: Page buttons with icons for each page type - App.jsx: PAGE_TYPE_TO_COMPONENT mapping and openWorkflowPage callback - styles.css: Complete CSS for all panels using design token system Docs: - CLAUDE.md: Workflow Pages section with architecture, page types, API endpoints, and asset configuration https://claude.ai/code/session_016p3y4FpBYpbKsoABaeG4ft
1 parent 6739dcf commit 80df0b7

21 files changed

Lines changed: 4983 additions & 4 deletions

CLAUDE.md

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,6 +1317,204 @@ Context about the task.
13171317
| Standard | 15-25 | 150,000 | 600 |
13181318
| Complex | 30-50 | 300,000 | 1800 |
13191319

1320+
### Workflow Pages (Integrated Editor Views)
1321+
1322+
Workflow pages define interactive views that open from workflow steps in the web UI.
1323+
All media panels follow the **agent-as-editor model**: the UI is a viewer with
1324+
lightweight intent-capture controls. Editing requests are dispatched to the Claude
1325+
Code agent, which performs the actual file modifications (editing Motion Canvas
1326+
`.tsx` scenes, running `ffmpeg` commands, using ImageMagick/Pillow for images).
1327+
1328+
Pages are defined in the `pages` array in workflow frontmatter (YAML/TOML) and
1329+
displayed as clickable buttons on workflow steps.
1330+
1331+
#### Page Types
1332+
1333+
| Type | Component | Use Case |
1334+
|------|-----------|----------|
1335+
| `data-table` | Editable spreadsheet | Seed data, extracted results, configuration |
1336+
| `image` | Image viewer + intent toolbar | Generated images, charts, screenshots |
1337+
| `motion-canvas` | Rendered MC output viewer + intent toolbar | Animated graphics, data visualizations |
1338+
| `video` | Video player with trim/cut + intent toolbar | Individual video files |
1339+
| `video-sequence` | Multi-scene composition timeline | Composed videos (MC scenes + clips) |
1340+
1341+
#### Architecture: Agent-as-Editor
1342+
1343+
```
1344+
User clicks "Add Text" --> UI captures intent (what, where) -->
1345+
dispatches to agent --> agent edits .tsx / runs ffmpeg / uses Pillow -->
1346+
file changes --> panel polls mtime --> auto-refresh preview
1347+
```
1348+
1349+
Panels never modify files directly. The **IntentToolbar** (shared across image,
1350+
motion-canvas, video, and video-sequence panels) captures structured intents:
1351+
1352+
| Control | Intent Dispatched |
1353+
|---------|-------------------|
1354+
| Add Text | `{ action: "add_text", text, position, style: { font, size, color } }` |
1355+
| Add Shape | `{ action: "add_shape", shape_id, position, size, animated }` |
1356+
| Move | `{ action: "move_element", element_id, new_position }` |
1357+
| Resize | `{ action: "resize_element", element_id, new_size }` |
1358+
| Delete | `{ action: "delete_element", element_id }` |
1359+
1360+
Intents are converted to natural language prompts server-side and dispatched to
1361+
the agent via `POST /api/workflows/{id}/pages/{page_id}/edit`.
1362+
1363+
#### Video Composition with Motion Canvas
1364+
1365+
The `video-sequence` page type uses Motion Canvas as the composition layer.
1366+
Multiple scenes (MC animations + video clips) compose into a single video:
1367+
1368+
```yaml
1369+
pages:
1370+
- id: final-video
1371+
type: video-sequence
1372+
title: Final Presentation
1373+
step: compose_video
1374+
output_path: output/final.mp4
1375+
resolution: [1920, 1080]
1376+
scenes:
1377+
- id: intro
1378+
type: motion-canvas
1379+
title: Animated Intro
1380+
scene_path: scenes/intro.tsx
1381+
rendered_path: output/intro.mp4
1382+
- id: interview
1383+
type: clip
1384+
title: Interview Footage
1385+
source_path: footage/interview.mp4
1386+
trim_start: 5.0
1387+
trim_end: 30.0
1388+
```
1389+
1390+
#### Defining Pages in Markdown
1391+
1392+
```yaml
1393+
---
1394+
name: topic-research
1395+
title: Topic Research Workflow
1396+
agent:
1397+
model: claude-sonnet-4-20250514
1398+
max_turns: 25
1399+
1400+
pages:
1401+
- id: seed-topics
1402+
type: data-table
1403+
title: Seed Topics
1404+
step: research_topics
1405+
seed: true
1406+
data_path: data/topics.csv
1407+
columns:
1408+
- name: topic
1409+
label: Topic
1410+
type: text
1411+
required: true
1412+
- name: priority
1413+
label: Priority
1414+
type: select
1415+
options: [high, medium, low]
1416+
1417+
- id: output-chart
1418+
type: image
1419+
title: Research Chart
1420+
step: generate_chart
1421+
image_path: output/research-chart.png
1422+
editable: true
1423+
assets_dir: assets
1424+
1425+
- id: summary-animation
1426+
type: motion-canvas
1427+
title: Summary Animation
1428+
step: create_animation
1429+
scene_path: scenes/summary.tsx
1430+
output_path: output/summary.mp4
1431+
duration: 10
1432+
fps: 30
1433+
1434+
- id: presentation-video
1435+
type: video
1436+
title: Presentation
1437+
step: render_video
1438+
video_path: output/presentation.mp4
1439+
trim: true
1440+
max_duration: 120
1441+
---
1442+
```
1443+
1444+
#### Seed Data Tables
1445+
1446+
When a page has `seed: true`, it acts as input data for the workflow:
1447+
1448+
1. User opens the seed data table from the workflow step
1449+
2. Adds/edits rows (e.g., adding new research topics)
1450+
3. Clicks **Save** to persist changes to `data_path`
1451+
4. UI prompts: "Seed data updated. Run the workflow with new data?"
1452+
5. Clicking **Run Workflow** starts a new workflow execution with the updated data
1453+
1454+
#### Column Types
1455+
1456+
| Type | Input | Description |
1457+
|------|-------|-------------|
1458+
| `text` | Text input | Default, free-form text |
1459+
| `number` | Number input | Numeric values |
1460+
| `boolean` | Checkbox | True/false toggle |
1461+
| `url` | Text input | URL values |
1462+
| `date` | Text input | Date strings |
1463+
| `select` | Dropdown | Choose from `options` list |
1464+
1465+
#### File Refresh After Agent Edits
1466+
1467+
All media panels use the `useFileWatch` hook which polls the page data endpoint
1468+
every 2 seconds. When the file's `mtime` changes, the panel auto-refreshes the
1469+
preview. Cache-busting is handled via `?v=<timestamp>` query parameters on
1470+
file URLs.
1471+
1472+
#### API Endpoints
1473+
1474+
| Endpoint | Method | Description |
1475+
|----------|--------|-------------|
1476+
| `/api/workflows/{id}/pages` | GET | List pages for a workflow |
1477+
| `/api/workflows/{id}/pages/{page_id}/data` | GET | Get page data (table rows, file metadata with mtime) |
1478+
| `/api/workflows/{id}/pages/{page_id}/data` | PUT | Update data-table rows |
1479+
| `/api/workflows/{id}/pages/{page_id}/run` | POST | Re-run workflow from seed data |
1480+
| `/api/workflows/{id}/pages/{page_id}/edit` | POST | Dispatch editing intents to agent |
1481+
| `/api/file/raw?path=<path>` | GET | Serve raw media files (video, image, font, SVG) |
1482+
| `/api/assets/shapes` | GET | Get SVG shape library manifest |
1483+
| `/api/assets/fonts` | GET | Get custom font library manifest |
1484+
1485+
#### Custom SVG Shapes
1486+
1487+
Store SVG shapes in `assets/shapes/` with a `manifest.json`:
1488+
1489+
```json
1490+
{
1491+
"shapes": [
1492+
{
1493+
"id": "arrow-expand",
1494+
"category": "arrows",
1495+
"title": "Expanding Arrow",
1496+
"svg_path": "arrows/arrow-expand.svg",
1497+
"animated": true
1498+
}
1499+
]
1500+
}
1501+
```
1502+
1503+
#### Custom Fonts
1504+
1505+
Store fonts in `assets/fonts/` with a `manifest.json`:
1506+
1507+
```json
1508+
{
1509+
"fonts": [
1510+
{ "id": "montserrat-bold", "family": "Montserrat", "weight": 700, "path": "Montserrat-Bold.woff2" }
1511+
]
1512+
}
1513+
```
1514+
1515+
Panels load fonts dynamically via the Font Loading API. The agent references
1516+
fonts by family name in MC scenes or by file path in ffmpeg/ImageMagick commands.
1517+
13201518
---
13211519

13221520
## Workflow Observability API
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""Intent-to-prompt conversion and dispatch for workflow page editing.
2+
3+
Converts structured editing intents from the UI into natural language prompts
4+
for the Claude Code agent. Supports two dispatch modes:
5+
- Chat dispatch: inject into existing Claude WebSocket session
6+
- Background dispatch: start a new agent workflow execution
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from typing import Any, Optional
12+
13+
14+
def build_edit_prompt(page: dict, intents: list[dict]) -> str:
15+
"""Convert structured editing intents into a natural language prompt.
16+
17+
Args:
18+
page: Page config dict (type, scene_path, video_path, image_path, etc.)
19+
intents: List of intent dicts with action, position, text, etc.
20+
21+
Returns:
22+
Natural language prompt string for the agent.
23+
"""
24+
page_type = page.get("type", "")
25+
target_file = _get_target_file(page)
26+
27+
lines = []
28+
lines.append(f"Edit the file at `{target_file}`:")
29+
lines.append("")
30+
31+
for intent in intents:
32+
line = _format_intent(intent, page_type)
33+
if line:
34+
lines.append(f"- {line}")
35+
36+
lines.append("")
37+
38+
# Add type-specific context
39+
if page_type == "motion-canvas":
40+
lines.append("This is a Motion Canvas scene file (.tsx). Use Motion Canvas 2D API.")
41+
lines.append("Import shapes from '@motion-canvas/2d' and use generator functions for animations.")
42+
assets_dir = page.get("assets_dir")
43+
if assets_dir:
44+
lines.append(f"Custom assets (shapes, fonts) are in `{assets_dir}/`.")
45+
elif page_type == "video":
46+
lines.append("Use ffmpeg CLI commands to apply the edits to the video file.")
47+
lines.append("Preserve the original file as a backup before modifying.")
48+
elif page_type == "video-sequence":
49+
lines.append("This is a video sequence project using Motion Canvas as the composition layer.")
50+
lines.append("Each scene is a .tsx file. Video clips are wrapped as MC Video elements.")
51+
elif page_type == "image":
52+
lines.append("Use ImageMagick or Python Pillow to apply the edits to the image file.")
53+
lines.append("Preserve the original file as a backup before modifying.")
54+
assets_dir = page.get("assets_dir")
55+
if assets_dir:
56+
lines.append(f"Custom fonts are in `{assets_dir}/fonts/` and shapes in `{assets_dir}/shapes/`.")
57+
58+
return "\n".join(lines)
59+
60+
61+
def _get_target_file(page: dict) -> str:
62+
"""Get the primary target file path for a page type."""
63+
page_type = page.get("type", "")
64+
if page_type == "motion-canvas":
65+
return page.get("scene_path", "scene.tsx")
66+
elif page_type == "video":
67+
return page.get("video_path", "video.mp4")
68+
elif page_type == "video-sequence":
69+
return page.get("output_path", "output/final.mp4")
70+
elif page_type == "image":
71+
return page.get("image_path", "image.png")
72+
return "unknown"
73+
74+
75+
def _format_intent(intent: dict, page_type: str) -> str:
76+
"""Format a single intent into a human-readable instruction."""
77+
action = intent.get("action", "")
78+
79+
if action == "add_text":
80+
text = intent.get("text", "")
81+
pos = intent.get("position", {})
82+
style = intent.get("style", {})
83+
parts = [f"Add text '{text}'"]
84+
if pos:
85+
parts.append(f"at position ({pos.get('x', 0)}, {pos.get('y', 0)})")
86+
if style.get("font"):
87+
parts.append(f"using font '{style['font']}'")
88+
if style.get("size"):
89+
parts.append(f"size {style['size']}")
90+
if style.get("color"):
91+
parts.append(f"color {style['color']}")
92+
time_range = intent.get("time_range")
93+
if time_range and page_type in ("video", "motion-canvas", "video-sequence"):
94+
parts.append(f"visible from {time_range.get('start', 0)}s to {time_range.get('end', 0)}s")
95+
return " ".join(parts)
96+
97+
elif action == "add_shape":
98+
shape_id = intent.get("shape_id", "shape")
99+
pos = intent.get("position", {})
100+
size = intent.get("size", {})
101+
animated = intent.get("animated", False)
102+
parts = [f"Add SVG shape '{shape_id}'"]
103+
if pos:
104+
parts.append(f"at ({pos.get('x', 0)}, {pos.get('y', 0)})")
105+
if size:
106+
parts.append(f"size {size.get('width', 100)}x{size.get('height', 100)}")
107+
if animated:
108+
parts.append("with entrance animation")
109+
return " ".join(parts)
110+
111+
elif action == "move_element":
112+
element_id = intent.get("element_id", "element")
113+
pos = intent.get("position", {})
114+
return f"Move element '{element_id}' to ({pos.get('x', 0)}, {pos.get('y', 0)})"
115+
116+
elif action == "resize_element":
117+
element_id = intent.get("element_id", "element")
118+
size = intent.get("size", {})
119+
return f"Resize element '{element_id}' to {size.get('width', 100)}x{size.get('height', 100)}"
120+
121+
elif action == "delete_element":
122+
element_id = intent.get("element_id", "element")
123+
return f"Delete element '{element_id}'"
124+
125+
elif action == "trim":
126+
time_range = intent.get("time_range", {})
127+
return f"Trim to {time_range.get('start', 0)}s - {time_range.get('end', 0)}s"
128+
129+
elif action == "cut":
130+
time_range = intent.get("time_range", {})
131+
return f"Cut segment from {time_range.get('start', 0)}s to {time_range.get('end', 0)}s"
132+
133+
return f"Unknown action: {action}"
134+
135+
136+
def dispatch_edit(
137+
workflow_id: str,
138+
page: dict,
139+
prompt: str,
140+
session_id: Optional[str] = None,
141+
) -> dict[str, Any]:
142+
"""Dispatch an editing prompt to the agent.
143+
144+
Args:
145+
workflow_id: DBOS workflow ID
146+
page: Page config dict
147+
prompt: Natural language editing prompt
148+
session_id: If provided, inject into existing Claude session
149+
150+
Returns:
151+
dict with dispatch result (status, workflow_id or session info)
152+
"""
153+
if session_id:
154+
# Mode A: Chat dispatch - inject into existing session
155+
# This would use the StreamSession to send a message
156+
return {
157+
"status": "dispatched",
158+
"mode": "chat",
159+
"session_id": session_id,
160+
"prompt": prompt,
161+
}
162+
else:
163+
# Mode B: Background dispatch - start new agent workflow
164+
try:
165+
from kurt.workflows.agents import run_definition
166+
from kurt.workflows.agents.registry import get_definition_for_workflow
167+
168+
definition = get_definition_for_workflow(workflow_id)
169+
if not definition:
170+
return {"status": "error", "detail": "Workflow definition not found"}
171+
172+
result = run_definition(
173+
definition["name"],
174+
inputs={"task": prompt},
175+
background=True,
176+
)
177+
return {
178+
"status": "started",
179+
"mode": "background",
180+
"workflow_id": result.get("workflow_id"),
181+
}
182+
except Exception as e:
183+
return {"status": "error", "detail": str(e)}

0 commit comments

Comments
 (0)