From c70844b317081451ce9cefc42cc5d0c381fdd65d Mon Sep 17 00:00:00 2001 From: gaurav0107 Date: Mon, 8 Jun 2026 01:36:10 +0530 Subject: [PATCH 1/5] refactor(web/playground): PromptOption.versions carries template_messages Adds the structured Message[] field alongside the legacy single-string template so the composer (next task) can read messages directly. The page mapper falls back to wrapping the legacy template as a single human message when template_messages is absent (defensive - Plan B's api always sends both, but old api versions during a deploy gap may not). No behavior change yet; the composer still reads template until Task 4 rewrites it. Signed-off-by: gaurav0107 --- web/src/app/playground/page.tsx | 15 +++++++++++++-- web/src/components/PlaygroundClient.tsx | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/web/src/app/playground/page.tsx b/web/src/app/playground/page.tsx index a5a9828..6f627a6 100644 --- a/web/src/app/playground/page.tsx +++ b/web/src/app/playground/page.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { Shell } from "@/components/Shell"; import { PlaygroundComposer, + type Message, type PlaygroundSessionOut, type PromptOption, } from "@/components/PlaygroundClient"; @@ -34,7 +35,10 @@ interface PromptRow { interface PromptVersionRow { id: string; version: number; - template: string; + /** Plan B onwards; absent on very old api versions. */ + template_messages?: Message[]; + /** Legacy field; absent eventually. */ + template?: string; } interface PromptVersionList { @@ -85,7 +89,14 @@ export default async function PlaygroundPage() { versions: (versions.data?.versions ?? []).map((v) => ({ id: v.id, version: v.version, - template: v.template, + // Prefer the structured field; fall back to wrapping the legacy + // template (defensive — Plan B's api always sends both). + template_messages: + v.template_messages ?? + (v.template != null + ? [{ role: "human" as const, content: v.template }] + : []), + template: v.template ?? "", })), }; }), diff --git a/web/src/components/PlaygroundClient.tsx b/web/src/components/PlaygroundClient.tsx index 6fa92f5..6fb6223 100644 --- a/web/src/components/PlaygroundClient.tsx +++ b/web/src/components/PlaygroundClient.tsx @@ -22,11 +22,24 @@ import { ModelPicker } from "@/components/ModelPicker"; * iteration without changing the storage shape). */ +export interface Message { + role: "system" | "human"; + content: string; +} + export interface PromptOption { id: string; slug: string; name: string; - versions: { id: string; version: number; template: string }[]; + versions: { + id: string; + version: number; + /** Structured form (Plan A+B). Always set on new versions. */ + template_messages: Message[]; + /** Legacy single-string field. Kept for back-compat reads from the + * api during the deprecation window. We don't display this. */ + template: string; + }[]; } export interface PlaygroundSessionOut { From 3f780f0f2733c98b2be1487d0cf2f640d5e1eb5e Mon Sep 17 00:00:00 2001 From: gaurav0107 Date: Mon, 8 Jun 2026 01:37:12 +0530 Subject: [PATCH 2/5] feat(web/playground): extractVariablesFromMessages dedupes across turns Sister to extractVariables(template). Used by the composer (next task) to keep the Inputs panel in sync as the user edits System and Human bodies. Preserves first-seen order so the Inputs panel doesn't reshuffle while the user is mid-edit. Test file deferred until a JS test runner is configured for the web workspace. Signed-off-by: gaurav0107 --- web/src/components/PlaygroundClient.tsx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/web/src/components/PlaygroundClient.tsx b/web/src/components/PlaygroundClient.tsx index 6fb6223..2cb8fbf 100644 --- a/web/src/components/PlaygroundClient.tsx +++ b/web/src/components/PlaygroundClient.tsx @@ -78,6 +78,26 @@ function extractVariables(template: string): string[] { return [...out]; } +/** + * Run the same variable-detection regex across a list of messages and + * return the deduped union (preserving first-seen order). Used by the + * composer to keep the Inputs panel in sync as the user edits System + * and Human bodies. + */ +export function extractVariablesFromMessages(messages: Message[]): string[] { + const seen = new Set(); + const out: string[] = []; + for (const m of messages) { + for (const v of extractVariables(m.content)) { + if (!seen.has(v)) { + seen.add(v); + out.push(v); + } + } + } + return out; +} + export function PlaygroundComposer({ projectId, prompts, From ecde9631327178e04d54cb68f5d7d00f127ccd3b Mon Sep 17 00:00:00 2001 From: gaurav0107 Date: Mon, 8 Jun 2026 01:38:18 +0530 Subject: [PATCH 3/5] feat(web/playground): MessageEditor per-message card Self-contained, callback-driven. Renders the role pill (system/human dropdown), content textarea (auto-grows on newlines), and the reorder + delete affordances. Uses the design system tokens (--surface, --border, --r-3, .btn .btn-ghost .btn-sm) per DESIGN.md - no hardcoded hex values or one-off radii. Used by the composer rewrite in the next task. canDelete prop lets the caller block the last delete so the composer can never end up with zero messages. Signed-off-by: gaurav0107 --- .../components/playground/MessageEditor.tsx | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 web/src/components/playground/MessageEditor.tsx diff --git a/web/src/components/playground/MessageEditor.tsx b/web/src/components/playground/MessageEditor.tsx new file mode 100644 index 0000000..a2296a5 --- /dev/null +++ b/web/src/components/playground/MessageEditor.tsx @@ -0,0 +1,148 @@ +"use client"; + +import type { Message } from "@/components/PlaygroundClient"; + +interface MessageEditorProps { + message: Message; + onChange: (next: Message) => void; + onDelete: () => void; + /** Undefined when this is the first message. */ + onMoveUp?: () => void; + /** Undefined when this is the last message. */ + onMoveDown?: () => void; + /** Disables delete when there's only one message left. */ + canDelete: boolean; +} + +const ROLE_LABEL: Record = { + system: "SYSTEM", + human: "HUMAN", +}; + +export function MessageEditor({ + message, + onChange, + onDelete, + onMoveUp, + onMoveDown, + canDelete, +}: MessageEditorProps) { + return ( +
+
+ +
+ + + +
+
+