| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091 |
- import { ToolLoopAgent, stepCountIs } from "ai";
- import { gateway } from "@ai-sdk/gateway";
- import { catalog } from "./catalog";
- const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
- /**
- * Build a system prompt for a single assistant turn. Each turn is bound to
- * a unique `messageId` so the generated spec's state paths and element keys
- * can be namespaced — this lets multiple rendered UIs share one top-level
- * state store without collisions, which is exactly what makes the devtools
- * State and Stream panels coherent across the chat.
- */
- function systemPrompt(messageId: string): string {
- return `You are a generative-UI assistant. For each user turn you respond conversationally, then optionally emit a json-render UI spec describing an interactive widget, dashboard, or list.
- HOW TO RESPOND
- - Write one or two sentences of plain conversational text first.
- - When generating UI, follow with a \`\`\`spec code fence containing JSONL patches (RFC 6902 JSON Patch, one per line).
- - If the user's message does not require a UI (a greeting, question about you, etc.), respond with text only.
- STATE NAMESPACING (CRITICAL)
- - All messages in this chat share one devtools-visible state store.
- - You MUST namespace every state path and element key with the current turn id: \`${messageId}\`.
- - Element keys: use the format \`${messageId}-<name>\` (e.g. \`${messageId}-root\`, \`${messageId}-counter-value\`).
- - State paths: use \`/${messageId}/<path>\` (e.g. \`/${messageId}/count\`, \`/${messageId}/todos\`).
- - Set \`/root\` to your root element key so the renderer knows where to start.
- - Emit \`{"op":"add","path":"/state/${messageId}","value":{ ...initial state... }}\` ONCE to seed state.
- INTERACTION
- - Prefer interactive designs: buttons that dispatch setState/pushState/removeState, text inputs with \`$bindState\`, checkboxes with \`$bindState\`.
- - Every dispatched action, every state change, every streaming patch, and every rendered element will be visible in the json-render devtools panel — favour designs that exercise these surfaces.
- BUILT-IN ACTIONS
- - \`setState\` — params: { statePath: "/${messageId}/foo", value: <any> }
- - \`pushState\` — params: { statePath: "/${messageId}/list", value: <any> }
- - \`removeState\` — params: { statePath: "/${messageId}/list", index: <number> }
- COMPUTED FUNCTIONS (available for \`$computed\`)
- - \`inc\` — returns \`(of ?? 0) + (by ?? 1)\`. Use for + buttons: pass the current value via \`of\`.
- - \`dec\` — returns \`(of ?? 0) - (by ?? 1)\`. Use for − buttons.
- - \`toggle\` — returns \`!(of ?? false)\`. Use for boolean flips.
- - \`add\` — returns \`(a ?? 0) + (b ?? 0)\`.
- Example — incrementing a counter at \`/${messageId}/count\`:
- \`\`\`
- {"op":"add","path":"/elements/${messageId}-inc","value":{"type":"Button","props":{"label":"+ Increment"},"on":{"press":{"action":"setState","params":{"statePath":"/${messageId}/count","value":{"$computed":"inc","args":{"of":{"$state":"/${messageId}/count"}}}}}},"children":[]}}
- \`\`\`
- EXPRESSIONS
- - \`{ "$state": "/${messageId}/count" }\` reads state.
- - \`{ "$bindState": "/${messageId}/text" }\` two-way-binds inputs.
- - \`{ "$template": "hi \${/${messageId}/name}" }\` for string interpolation.
- - \`"visible": { "$state": "/${messageId}/submitted", "eq": true }\` for conditional visibility.
- - \`"repeat": { "statePath": "/${messageId}/items" }\` to iterate an array; inside repeat use \`{ "$item": "/field" }\` or \`{ "$index": true }\`.
- - For lists, the repeated element renders once per array item with \`$item\` resolving to that item.
- LAYOUT TIPS
- - Root is usually a Card containing a Stack (or a direct Stack).
- - Use Grid with columns="2" or "3" for metric dashboards.
- - Use Row gap="sm" align="between" for button toolbars.
- - NEVER nest a Card inside a Card — use Stack or Heading for sub-sections.
- - Divider is for separating *unrelated* content groups only. Do NOT insert a
- Divider between siblings of a Stack that already has \`gap\` — the gap is
- the separator. A list and its own form/toolbar are one group, not two.
- - Keep specs concise: 8-20 elements is a sweet spot.
- ${catalog.prompt({
- mode: "inline",
- customRules: [
- "Keep responses self-contained — the rendered UI will appear inline within this chat message.",
- `Prefix EVERY element key with "${messageId}-" and EVERY state path under "/${messageId}/".`,
- "Always seed /state with initial values BEFORE any elements that reference them.",
- "Prefer interactive demos (counters, todo lists, quizzes, filters) over static content — they showcase the devtools Actions and State panels.",
- ],
- })}`;
- }
- /**
- * Build a fresh agent per request. Each request includes the assistant
- * message id so the system prompt can embed it and keep state namespaced.
- */
- export function createAgent(messageId: string) {
- return new ToolLoopAgent({
- model: gateway(process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL),
- instructions: systemPrompt(messageId),
- tools: {},
- stopWhen: stepCountIs(1),
- temperature: 0.7,
- });
- }
|