Created
July 31, 2026 19:16
-
-
Save lostintangent/753daad03e0e3785c61434ce82188b60 to your computer and use it in GitHub Desktop.
Agent Copy Desk - Toy Box app
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "title": "Agent Copy Desk", | |
| "description": "Turn a short brief and a tone into generated copy written by an app-owned worker.", | |
| "icon": "Feather", | |
| "defaultState": { "brief": "", "tone": "friendly", "copy": "", "generatedAt": null, "requestId": null } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { useState } from "react"; | |
| import { | |
| AppBadge, | |
| AppButton, | |
| AppEmptyState, | |
| AppHeader, | |
| AppModelPicker, | |
| AppShell, | |
| AppTextarea, | |
| useAppActions, | |
| useAppState, | |
| useWorkspace, | |
| } from "@toy-box/app"; | |
| import { Check, Feather, Loader2, Sparkles, X } from "lucide-react"; | |
| const TONES = ["friendly", "confident", "playful", "technical"] as const; | |
| type Tone = (typeof TONES)[number]; | |
| type DeskState = { | |
| brief: string; | |
| tone: Tone; | |
| copy: string; | |
| generatedAt: string | null; | |
| requestId: string | null; | |
| }; | |
| function normalize(raw: unknown): DeskState { | |
| const value = (raw ?? {}) as Partial<DeskState>; | |
| return { | |
| brief: typeof value.brief === "string" ? value.brief : "", | |
| tone: TONES.includes(value.tone as Tone) ? (value.tone as Tone) : "friendly", | |
| copy: typeof value.copy === "string" ? value.copy : "", | |
| generatedAt: typeof value.generatedAt === "string" ? value.generatedAt : null, | |
| requestId: typeof value.requestId === "string" ? value.requestId : null, | |
| }; | |
| } | |
| const WORKER_KIND = "agent-copy-desk-draft"; | |
| type WorkerMetadata = { kind?: string; requestId?: string }; | |
| export default function AgentCopyDesk() { | |
| const [rawState, updateState] = useAppState<unknown>(); | |
| const state = normalize(rawState); | |
| const workers = useWorkspace((workspace) => workspace.workers); | |
| const defaultModel = useWorkspace((workspace) => workspace.defaultModel); | |
| const actions = useAppActions(); | |
| const [model, setModel] = useState(defaultModel); | |
| const [spawning, setSpawning] = useState(false); | |
| const activeModel = model ?? defaultModel; | |
| const pendingWorker = workers.find((worker) => { | |
| const metadata = (worker.metadata ?? {}) as WorkerMetadata; | |
| return metadata.kind === WORKER_KIND && metadata.requestId !== state.requestId; | |
| }); | |
| const isPending = spawning || pendingWorker !== undefined; | |
| const canAsk = state.brief.trim() !== "" && activeModel !== null && !isPending; | |
| function setBrief(brief: string) { | |
| updateState((current) => ({ ...normalize(current), brief })); | |
| } | |
| function setTone(tone: Tone) { | |
| updateState((current) => ({ ...normalize(current), tone })); | |
| } | |
| async function ask() { | |
| if (!canAsk || !activeModel) return; | |
| const { brief, tone } = state; | |
| const requestId = `copy-${Date.now()}`; | |
| setSpawning(true); | |
| try { | |
| await actions.spawnWorker({ | |
| name: "Copy draft", | |
| metadata: { kind: WORKER_KIND, requestId }, | |
| message: { | |
| model: activeModel, | |
| content: `Write marketing copy for this brief, in a ${tone} tone. | |
| Brief: | |
| ${brief.trim()} | |
| Produce two to four short sentences of finished copy with no preamble, headings, | |
| or quotation marks. Set "copy" in the current app state to that text, set | |
| "generatedAt" to the current ISO 8601 timestamp, set "requestId" to "${requestId}", | |
| and preserve every other field exactly as it is.`, | |
| }, | |
| }); | |
| } finally { | |
| setSpawning(false); | |
| } | |
| } | |
| async function cancel() { | |
| if (!pendingWorker) return; | |
| await actions.stopWorker(pendingWorker.sessionId); | |
| } | |
| return ( | |
| <AppShell> | |
| <AppHeader> | |
| <div className="flex min-w-0 flex-1 items-center gap-2"> | |
| <Feather className="size-4 shrink-0 text-primary" /> | |
| <span className="truncate text-sm font-semibold">Agent Copy Desk</span> | |
| {isPending ? ( | |
| <AppBadge variant="secondary" className="shrink-0 gap-1 text-[11px]"> | |
| <Loader2 className="size-3 animate-spin" /> | |
| Drafting | |
| </AppBadge> | |
| ) : state.copy ? ( | |
| <AppBadge variant="outline" className="shrink-0 gap-1 text-[11px]"> | |
| <Check className="size-3" /> | |
| Ready | |
| </AppBadge> | |
| ) : null} | |
| </div> | |
| </AppHeader> | |
| <div className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-3 @2xl:flex-row"> | |
| <section className="flex shrink-0 flex-col gap-2.5 @2xl:w-80"> | |
| <div className="flex flex-col gap-1.5"> | |
| <label htmlFor="copy-desk-brief" className="text-xs font-medium text-muted-foreground"> | |
| Brief | |
| </label> | |
| <AppTextarea | |
| id="copy-desk-brief" | |
| value={state.brief} | |
| onChange={(event) => setBrief(event.target.value)} | |
| placeholder="What are we writing about?" | |
| rows={4} | |
| className="resize-none text-sm" | |
| /> | |
| </div> | |
| <div className="flex flex-col gap-1.5"> | |
| <span className="text-xs font-medium text-muted-foreground">Tone</span> | |
| <div className="flex flex-wrap gap-1.5"> | |
| {TONES.map((tone) => ( | |
| <AppButton | |
| key={tone} | |
| variant={state.tone === tone ? "default" : "outline"} | |
| size="sm" | |
| onClick={() => setTone(tone)} | |
| aria-pressed={state.tone === tone} | |
| className="h-7 px-2.5 text-xs capitalize" | |
| > | |
| {tone} | |
| </AppButton> | |
| ))} | |
| </div> | |
| </div> | |
| {activeModel ? ( | |
| <div className="flex flex-col gap-1.5"> | |
| <span className="text-xs font-medium text-muted-foreground">Model</span> | |
| <AppModelPicker value={activeModel} onValueChange={setModel} /> | |
| </div> | |
| ) : null} | |
| <div className="flex items-center gap-1.5"> | |
| <AppButton size="sm" onClick={ask} disabled={!canAsk} className="h-8 flex-1 gap-1.5"> | |
| {isPending ? ( | |
| <Loader2 className="size-3.5 animate-spin" /> | |
| ) : ( | |
| <Sparkles className="size-3.5" /> | |
| )} | |
| {isPending ? "Working…" : "Ask agent"} | |
| </AppButton> | |
| {pendingWorker ? ( | |
| <AppButton | |
| variant="outline" | |
| size="sm" | |
| onClick={cancel} | |
| className="h-8 shrink-0 gap-1.5 px-2.5 text-xs" | |
| aria-label="Cancel the running draft" | |
| > | |
| <X className="size-3.5" /> | |
| Cancel | |
| </AppButton> | |
| ) : null} | |
| </div> | |
| </section> | |
| <section className="flex min-h-40 min-w-0 flex-1 flex-col gap-1.5"> | |
| <div className="flex items-center justify-between gap-2"> | |
| <span className="text-xs font-medium text-muted-foreground">Generated copy</span> | |
| {state.generatedAt ? ( | |
| <span className="truncate text-[11px] text-muted-foreground"> | |
| {new Date(state.generatedAt).toLocaleString()} | |
| </span> | |
| ) : null} | |
| </div> | |
| <div className="min-h-0 flex-1 overflow-y-auto rounded-lg border border-border bg-card/60 p-3"> | |
| {isPending && !state.copy ? ( | |
| <div className="flex flex-col gap-2" aria-live="polite"> | |
| <div className="h-3 w-4/5 animate-pulse rounded bg-muted" /> | |
| <div className="h-3 w-full animate-pulse rounded bg-muted" /> | |
| <div className="h-3 w-3/5 animate-pulse rounded bg-muted" /> | |
| </div> | |
| ) : state.copy ? ( | |
| <p className="text-sm leading-relaxed whitespace-pre-wrap">{state.copy}</p> | |
| ) : ( | |
| <AppEmptyState | |
| title="No copy yet" | |
| description="Write a brief, pick a tone, then ask the agent." | |
| /> | |
| )} | |
| </div> | |
| </section> | |
| </div> | |
| </AppShell> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment