Skip to content

feat(ai-studio): public AI workflow demo - Visualize node, agent web search, richer templates#48

Draft
librowski wants to merge 33 commits into
mainfrom
ai-studio-ux
Draft

feat(ai-studio): public AI workflow demo - Visualize node, agent web search, richer templates#48
librowski wants to merge 33 commits into
mainfrom
ai-studio-ux

Conversation

@librowski

Copy link
Copy Markdown
Collaborator

AI Studio public demo: Visualize node, agent web search, richer templates, demo hardening

What & why

Turns apps/ai-studio (the SDK reference app) into a polished, runnable public demo of Workflow Builder. Visitors land on a real, executable workflow, can run it against live LLM calls, and see results rendered in-canvas.

Scope: apps/** only - zero packages/sdk changes, so no changeset needed. The published SDK is untouched; everything here is reference-app / backend / worker code.

Highlights

Demo bootstrap

  • Auto-loads a flagship template (Customer Support Triage) on first visit instead of a blank canvas; returning visitor's saved diagram still wins.
  • First-visit disclaimer modal (real OpenRouter calls, not a benchmark).
  • Budget model default (google/gemini-2.5-flash-lite).
  • Execute endpoint gated by per-IP rate limit + optional Cloudflare Turnstile.
  • 5 starter templates.

Visualize node (generic output renderer)

  • Auto-detects the upstream output format and picks a renderer; or force one via a mode param.
  • 7 renderers: markdown / plain text / JSON tree / table / stat-cards (lightweight core) + chart (recharts) and diagram (mermaid), the latter two lazy-loaded as separate chunks so they never bloat the base bundle.
  • Renders inside the node body; always-on card with an empty state before a run.
  • Export (PNG / SVG / copy image / copy source) and a fullscreen expand modal.
  • Opt-in "AI adapt": an LLM reshapes the output into the chosen format via POST /api/visualize/adapt (optional, 501 when no key).
  • Markdown rendered inside table cells / stat-card values; embedded ```mermaid / ```json blocks render inline.

Templates

  • Each flow ends in a Visualize node and is shaped to present richly: AI Debate (verdict + For/Against table), Meeting Notes (action-items table), Content Repurposer (a converging "Content Pack" node), Support Triage (approved reply).
  • New Market Research Brief template showcasing agent web search → Visualize.

Agent web search (tool calling)

  • Opt-in per-node "Web search" toggle on the AI Agent node, backed by Tavily.
  • The AI SDK runs the tool call/execute loop inside the worker activity - no graph cycles, fully DAG-compatible.
  • Degrades gracefully: no TAVILY_API_KEY → tool simply not exposed, node runs unchanged.
  • Verified end-to-end live (agent searches, returns a sourced brief with real URLs).

Fixes & polish

  • Dark-mode contrast on the execution log / node-detail panels.
  • Welcome-modal heading alignment.
  • Theme-aware CDN logo linked to workflowbuilder.io, with the SDK app-bar logo hidden and the Save button kept clear (temporary consumer-side override; remove once the SDK exposes a logo/logoHref prop).
  • AI Agent outputSchema so {{ nodes.<id>.response }} mentions resolve to a clean pill instead of an unresolved label.

Hygiene

  • Removed dead code + de-exported internal-only symbols (knip-clean across ai-studio / backend / worker).
  • No internal planning docs in the PR.

Configuration (all optional, features degrade gracefully)

  • OPENROUTER_API_KEY - worker (run workflows) + backend (Visualize AI adapt).
  • TAVILY_API_KEY - worker (agent web search). Free tier ~1000/mo.
  • Turnstile site/secret keys - bot protection on execute.

Verification

  • typecheck + lint clean: ai-studio, backend, execution-worker.
  • tests pass: ai-studio 13, backend 71, execution-worker 4, execution-core 78.
  • knip clean (3 apps).
  • production build OK; chart-renderer (106 KB gz) and diagram-renderer (134 KB gz) confirmed as separate lazy chunks.
  • agent web search verified live end-to-end.

Known issues / follow-ups (not in this PR)

  • apps/docs has 4 pre-existing astro check type errors, unrelated to this branch (tracked: WB-326).
  • SDK renders a raw i18n key for genuinely-unresolvable variable mentions - latent SDK bug, worked around app-side here (tracked: WB-327).
  • arch.png / render-dark.png at repo root are left untracked (not part of this PR).

🤖 Generated with Claude Code

…laimer

Replace the internal Synergy sales-inquiry template with a generic, relatable
Customer Support Triage flagship (classify -> route by type -> specialist draft
-> QA), built on the existing trigger/agent/decision nodes.

Auto-load it on first visit via initialNodes/initialEdges on WorkflowBuilder.Root
so visitors land on a runnable workflow instead of a blank canvas; a returning
visitor's saved diagram still wins. Add a dismissible first-visit disclaimer
modal stating the workflows run real OpenRouter calls, that the demo is not a
model benchmark, and that its purpose is to showcase Workflow Builder.
Cheaper, fast default for the public demo (roughly an order of magnitude cheaper
than claude-3.5-haiku). Quality-per-cost is what matters here - the model is the
engine, not the product. Override with the AI_MODEL env var.
POST /api/workflows/:id/execute is the only endpoint that spends real LLM
budget, so it gets a per-IP fixed-window rate limit plus an optional Cloudflare
Turnstile check, applied right after authorization via guardExecution(). Both
controls are no-ops when unconfigured (TURNSTILE_SECRET_KEY empty), so local dev
runs unprotected; the rate limit is active out of the box as a budget backstop.
Turnstile verification fails closed on a verifier error.
Before the execute request, fetch a Turnstile token from an invisible widget
and send it as the cf-turnstile-token header. Degrades gracefully: with no
VITE_TURNSTILE_SITE_KEY the token is undefined, no header is sent, and the
backend skips verification - so local dev needs no keys.
…templates

Grow the picker into a small gallery of relatable, runnable examples beyond the
flagship: AI Debate (parallel personas that fan out then converge on a verdict),
Content Repurposer (one post fanned out to three channel variants), and Meeting
Notes to Action Items (a linear summarize -> extract -> format chain). All use
the existing trigger/agent/decision nodes.
Generalize the on-canvas preview node from markdown-only to a future generic
Visualizer: type ai-studio/markdown-preview -> ai-studio/visualize, palette label
Visualize (Eye icon), worker domain VisualizeNode + executeVisualize + registry,
flagship node preview-1 -> visualize-1. Rendering is still markdown for now;
renderer set, format detection, charts, diagrams, export and expand land in the
following commits. Includes the visualize plan and long-work backlog.
Heuristic detectFormat(text) -> {renderer, data?, chartable?} mapping an output
string to a renderer for the Visualize node's auto mode: mermaid->diagram,
JSON->{chart for {label,value}/{type,data}, table for object arrays, stat-cards
for flat scalar objects, json for nested}, CSV->table, else markdown. Conservative
(chart/diagram only on clear signals), prose falls back to markdown. 11 unit tests.
Adds a 'Render as' select to the Visualize node: VISUALIZE_MODES = auto +
markdown/text/json/table/stat-cards/chart/diagram, default auto. Auto detects the
format; the rest force a specific renderer.
Add a renderer registry (getRenderer) with markdown, text, JSON tree (collapsible,
hand-rolled), table (JSON array / CSV rows), and stat-cards (flat object) renderers.
The visualize card now runs detectFormat (or the node's mode), picks the renderer,
shows an 'Auto > X' badge, and renders into a framed body. chart and diagram fall
back to table/text for now (real renderers land next).
The visualize card now renders for the node at all times with a fixed minimum
size: an empty-state placeholder ('The visualization appears here after you run
the workflow') before a run, a generating indicator while running, and the
rendered output on completion. The reveal animation moved from the card frame to
the content so it plays when the result arrives, not on every render.
Add a lazy-loaded recharts renderer (bar/line/area/pie) for the chart mode and
chart-spec envelopes ({type, data}) or {label,value}/{x,y} arrays. recharts only
loads when a chart is shown (React.lazy + Suspense). The card shows a 'Try as
chart' chip when auto-detected data is chartable but rendered as a table.
Add a lazy-loaded mermaid renderer for the diagram mode: it renders a mermaid
source string to SVG in the browser (securityLevel strict), and falls back to
the raw text on a syntax error. mermaid (~150KB) only loads when a diagram is
shown (React.lazy).
Add export-visualization util (html-to-image): download PNG, copy image to
clipboard (with a download fallback on browsers like Firefox that cannot write
image blobs), copy source text, and download SVG (native serialization fast-path
for chart/diagram). The card header now offers Copy image / Download PNG / Copy
source actions over the rendered content.
Add an Expand action that opens the visualization full-size in a modal (rendered
through a portal to document.body so its fixed overlay escapes the React Flow
viewport transform). The modal renders the same renderer larger and offers
PNG / SVG / copy-image / copy-source export.
…omment

All visualize work items complete and verified (test/typecheck/lint/build).
chart/diagram visual smoke deferred (needs a structured upstream); covered by
unit tests + build (lazy chunks confirmed).
Move the visualization from a detached floating panel into the node itself: the
content renders in-flow inside the node body (via OptionalNodeContent), so the
node grows vertically to contain it and reads as one cohesive card. Drops the
redundant card title (the node header already shows it) in favor of a slim
badge + export/expand toolbar; the body scrolls when the output is long. Empty
state and the expand modal are unchanged.
Two causes of the giant 'Syntax error in text / mermaid version' graphics:
- The diagram renderer called mermaid.render on invalid input, and mermaid
  injects its error graphic into the DOM on failure. Now it validates with
  mermaid.parse({ suppressErrors: true }) first and falls back to raw text
  (with a note) without ever calling render, so nothing is injected.
- Auto-detection matched a bare leading word (graph/pie/timeline/journey), so
  prose starting with those was mis-detected as a diagram. Detection is now
  strict: a fenced ```mermaid block, or flowchart/graph WITH a direction, or a
  distinctive declaration keyword. Prose stays markdown. (+3 tests, 13 total)
Adapt mixed output to the right format without an LLM: the markdown renderer now
renders a fenced ```mermaid block as a real diagram and a ```json block via the
detected renderer (chart/table/...), so a markdown response with an embedded
diagram renders it inline instead of as raw code. The diagram and chart renderers
also extract a fenced block from a larger response when their mode is forced.
Adds an opt-in 'AI adapt' action on the Visualize node: it sends the upstream
output to a new backend endpoint (POST /api/visualize/adapt) that uses an LLM to
convert it into the active format (clean Mermaid / chart JSON / table / ...), then
renders the result. The endpoint reuses the execution abuse gate (per-IP rate
limit + optional Turnstile) and is disabled (501) when the backend has no
OPENROUTER_API_KEY. Verified end-to-end: action-items prose -> clean mermaid.
Adds an 'AI: adapt output to this format' Switch to the Visualize node's
properties panel. When on, the node automatically LLM-converts the upstream
output into the active render format on each run (no need to click the on-card
button, which is hidden while the toggle is on). Off keeps the manual on-card
adapt button for structured formats.
The format-adapt prompts were terse and gave the model little guidance, so
conversions were mediocre on a small model. Each format now has detailed,
role-framed instructions (pick the fitting diagram type and quote labels safely;
find a category + numeric measure and aggregate for charts; extract 2-8 KPIs for
stat-cards; use only facts, no invention) plus temperature 0.2 and output trim.
JSON renderers also strip a fenced block in case the model wraps the output.
LLM-produced field values often contain markdown (bold, lists, links, inline
code). A new RichText helper renders such strings as markdown in table cells and
stat-card values, while leaving plain strings (e.g. 'user_id') untouched so they
are not mangled. Raw HTML stays escaped (no rehype-raw) to avoid XSS from
untrusted model output.
…sualize label

- Set explicit text color (theme token) on the Execution Log and node-detail
  panels so their text stays readable in dark mode instead of inheriting a dark color.
- Align the welcome modal's icon and title on one horizontal row.
- Shorten the Visualize node's palette description so its subtitle fits.
Hide the SDK app-bar logo (no prop to replace or link it yet) and render
the Workflow Builder CDN logo over the slot, linked to workflowbuilder.io.
Swap the transparent dark-text / white-text variants by the SDK theme so
the logo reads on both light and dark app bars (the -solid asset bakes in
a white background). Remove once the SDK exposes a logo/logoHref prop.
display:none on the SDK logo collapsed its flex box, sliding the Save
button left under the fixed logo overlay. Hide the logo with visibility
and reserve its width instead so the nav-segment keeps its place.
End the debate flow in a Visualize node and shape the verdict as markdown
(verdict line + For/Against table + reasoning) so it renders richly.
End the flow in a Visualize node and shape the recap as markdown with an
action-items table (owner/task/due) so it renders as a structured artifact.
Add a Content Pack agent that merges the three channel drafts into one
sectioned markdown document, then a Visualize node to render it. Keeps the
fan-out and gives the flow a single final artifact.
Remove the unused isTurnstileEnabled helper and de-export symbols that are
only used within their own module (renderers, VISUALIZE_MODES/VisualizeMode,
DetectResult, VisualizeNode). knip-clean across ai-studio, backend, worker.
Remove the long-work backlog and crystallize plan; agent-process scratch,
not product docs for the public repo. Net diff vs main no longer includes them.
The ai-agent palette item had no outputSchema, so {{ nodes.<id>.response }}
references (e.g. in decision conditions) fell through to the SDK's unresolved
'missing mention' label. Declaring the response output renders a clean
'Classify Ticket · Response' pill instead.
Add an opt-in 'Web search' toggle to the AI Agent node. When enabled and
TAVILY_API_KEY is set, the executor gives the agent a Tavily-backed web_search
tool via the AI SDK tool-calling loop (capped at a few steps). The loop runs
inside the activity, so it needs no graph cycles and stays DAG-compatible.
Without the key the node runs unchanged (tool simply not exposed).
A Trigger -> Research Agent (web search enabled) -> Visualize flow that
showcases the AI Agent web-search tool: the agent searches and writes a
sourced markdown brief, rendered in the Visualize node. Needs TAVILY_API_KEY
to actually search; runs without it, just answers from the model.
@librowski librowski marked this pull request as draft June 26, 2026 10:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants