From 34f498b128b375a8e3af455676775bd6abee9647 Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Wed, 7 Oct 2026 19:13:24 +0300 Subject: [PATCH] artifacts: the height ratchet fixed, invert switch persisted + restyled (release polish 7.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bridge measures the BODY (scrollHeight + rect + margins) instead of the viewport-clamped documentElement — documentElement.scrollHeight latches at the parent's own height, so demos could grow but never shrink on window resize (owner-reported); smoke: 1036 → 2386 → 1036 px - invert now PERSISTS as details.ui.inverted on the steer record, written by the silent artifact-ui route (dismissal pattern: no wake, no SSE) and seeded through the projection (SubagentArtifactData.ui); survives navigation + reload, never enters teacher context or exports - invert restyled as the house Toggle switch (settings controls) on both cards; demo header is the caption grammar: 'INTERACTIVE DEMO — title' left (title normal-case, same size/tracking), switch + download far right; figure header: label left, switch/download/open far right 1439 tests green (route + pure-seam pins added); live smoke passed --- app/api/chats/[id]/artifact-ui/route.ts | 41 +++++++++++ components/artifacts/demo-card.tsx | 91 ++++++++++++++++++------- components/artifacts/steer-card.tsx | 4 +- components/artifacts/visual-card.tsx | 70 +++++++++++-------- src/core/contracts/jobs.ts | 24 +++++++ src/core/view.ts | 7 +- tests/artifact-ui.test.ts | 90 ++++++++++++++++++++++++ tests/demos.test.tsx | 46 ++++++++++++- 8 files changed, 312 insertions(+), 61 deletions(-) create mode 100644 app/api/chats/[id]/artifact-ui/route.ts create mode 100644 tests/artifact-ui.test.ts diff --git a/app/api/chats/[id]/artifact-ui/route.ts b/app/api/chats/[id]/artifact-ui/route.ts new file mode 100644 index 0000000..4bf1b86 --- /dev/null +++ b/app/api/chats/[id]/artifact-ui/route.ts @@ -0,0 +1,41 @@ +/** + * THE ARTIFACT-UI ROUTE (owner, 2026-10-07): persists the learner's invert + * switch onto a demo/figure steer message (details.ui.inverted) so it + * survives session navigation and restarts. Modeled on the dismissal + * route's SILENCE: no wake, no agent, no tokens, no SSE — the record is + * the only state that moves, and the client already holds the toggled + * state optimistically (no refetch churn). + * + * Guards: 404 unknown chat, 400 for a body that isn't + * { input: { messageId, inverted } } or targets no demo/visual steer. + */ +import { setSteerUiInverted, type SteerDetails } from "@/src/core"; +import { getChat, stores } from "@/src/server"; + +export async function POST(req: Request, ctx: { params: Promise<{ id: string }> }): Promise { + const { id } = await ctx.params; + const chat = getChat(id); + if (!chat) return Response.json({ error: "Unknown chat" }, { status: 404 }); + + const body = (await req.json().catch(() => null)) as { input?: unknown } | null; + const input = body?.input as { messageId?: unknown; inverted?: unknown } | null | undefined; + if ( + !input || + typeof input !== "object" || + typeof input.messageId !== "string" || + typeof input.inverted !== "boolean" + ) { + return Response.json({ error: "input must be { messageId: string, inverted: boolean }" }, { status: 400 }); + } + + // Honesty: the target must actually be a demo/figure steer. + const target = chat.messages.find((m) => m.id === input.messageId); + const details = target ? ((target as { details?: unknown }).details as Partial | undefined) : undefined; + if (!details || (details.kind !== "demo" && details.kind !== "visual")) { + return Response.json({ error: "No demo/figure steer with that message id" }, { status: 400 }); + } + + const messages = setSteerUiInverted(chat.messages, input.messageId, input.inverted); + stores.chatStore.saveMessages(id, messages); + return Response.json({ ok: true }); +} diff --git a/components/artifacts/demo-card.tsx b/components/artifacts/demo-card.tsx index 574cd93..305ef4a 100644 --- a/components/artifacts/demo-card.tsx +++ b/components/artifacts/demo-card.tsx @@ -1,7 +1,8 @@ "use client"; import { useContext, useEffect, useMemo, useRef, useState } from "react"; -import { ContrastIcon, DownloadIcon } from "lucide-react"; +import { DownloadIcon } from "lucide-react"; +import { Toggle } from "@/components/settings/controls"; import { ChatActionsContext } from "@/components/chat"; import { downloadFile, slugFilename } from "@/components/artifacts/download"; import { DispatchCard } from "@/components/artifacts/dispatch-card"; @@ -28,7 +29,14 @@ import type { ArtifactPartProps, DemoResult } from "@/src/core"; export const DEMO_BRIDGE_SCRIPT = `';