From 6a68fcfba1f6038983b7f49a5b2dd88cffb535eb Mon Sep 17 00:00:00 2001 From: Amos Date: Sat, 22 Aug 2026 10:03:20 +0300 Subject: [PATCH] Wave 2 prep: LedgerEvent.ref, demo bridge protocol, artifact stubs, sendText in ChatActionsContext --- components/artifacts/demo-card.tsx | 10 ++++++++++ components/artifacts/index.ts | 9 ++++++++- components/artifacts/plan-divider.tsx | 9 +++++++++ components/artifacts/visual-card.tsx | 9 +++++++++ components/chat.tsx | 4 +++- src/core/contracts.ts | 21 +++++++++++++++++++++ 6 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 components/artifacts/demo-card.tsx create mode 100644 components/artifacts/plan-divider.tsx create mode 100644 components/artifacts/visual-card.tsx diff --git a/components/artifacts/demo-card.tsx b/components/artifacts/demo-card.tsx new file mode 100644 index 0000000..cae2f3a --- /dev/null +++ b/components/artifacts/demo-card.tsx @@ -0,0 +1,10 @@ +import type { ArtifactPartProps } from "@/src/core/contracts"; + +/** + * Demo artifact card (W6): sandboxed iframe rendering DemoResult.html with + * the auto-injected resize/error bridge (contracts.ts "Demo iframe bridge"). + * While input-available: job activity card (poll /api/jobs). + */ +export function DemoCard(_props: ArtifactPartProps) { + return null; +} diff --git a/components/artifacts/index.ts b/components/artifacts/index.ts index ccbd599..0114bc3 100644 --- a/components/artifacts/index.ts +++ b/components/artifacts/index.ts @@ -1,6 +1,9 @@ import type { ComponentType } from "react"; import type { ArtifactPartProps } from "@/src/core/contracts"; +import { DemoCard } from "@/components/artifacts/demo-card"; import { JobCard } from "@/components/artifacts/job-card"; +import { PlanDivider } from "@/components/artifacts/plan-divider"; +import { VisualCard } from "@/components/artifacts/visual-card"; /** * Artifact renderer registry — the extension point for tool-part UI. @@ -16,5 +19,9 @@ import { JobCard } from "@/components/artifacts/job-card"; */ export const artifactRenderers: Record> = { research: JobCard, - // W6: "demo" → sandboxed iframe card, "visual" → svg card + // Phase divider where the plan lands (W5 fills in the component): + plan_init: PlanDivider, + // Artifacts (W6 fills in the components): + demo: DemoCard, + visual: VisualCard, }; diff --git a/components/artifacts/plan-divider.tsx b/components/artifacts/plan-divider.tsx new file mode 100644 index 0000000..a978320 --- /dev/null +++ b/components/artifacts/plan-divider.tsx @@ -0,0 +1,9 @@ +import type { ArtifactPartProps } from "@/src/core/contracts"; + +/** + * Phase divider rendered where plan_init lands in the transcript (W5). + * Probe → teach transition marker per vision §2 "Two phases". + */ +export function PlanDivider(_props: ArtifactPartProps) { + return null; +} diff --git a/components/artifacts/visual-card.tsx b/components/artifacts/visual-card.tsx new file mode 100644 index 0000000..1eed86b --- /dev/null +++ b/components/artifacts/visual-card.tsx @@ -0,0 +1,9 @@ +import type { ArtifactPartProps } from "@/src/core/contracts"; + +/** + * Visual artifact card (W6): renders VisualResult.svg inline. + * While input-available: job activity card (poll /api/jobs). + */ +export function VisualCard(_props: ArtifactPartProps) { + return null; +} diff --git a/components/chat.tsx b/components/chat.tsx index f187d0c..bfaa942 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -37,6 +37,8 @@ const COLUMN = "mx-auto w-full max-w-3xl px-4"; export const ChatActionsContext = createContext<{ chatId: string; syncFromServer: () => Promise; + /** Send a user message into the session (demo error-fix loop, W6). */ + sendText: (text: string) => void; } | null>(null); /** useLayoutEffect that is silent during SSR (client components still render on the server in Next). */ @@ -143,7 +145,7 @@ export function Chat({ }; return ( - + void sendMessage({ text: t }) }}>
diff --git a/src/core/contracts.ts b/src/core/contracts.ts index 3c64b90..5ec604f 100644 --- a/src/core/contracts.ts +++ b/src/core/contracts.ts @@ -99,6 +99,9 @@ export interface LedgerEvent { kind: LedgerEventKind; chatId: string; at: string; + /** Idempotency key: the quiz part's toolCallId or `taught:` — + * re-emission of the same chat must never double-record an event. */ + ref?: string; /** The learner's quiz note — diagnostic gold (vision §1 calibration). */ note?: string; /** The quiz question text, when nodeId is null (later attribution input). */ @@ -206,6 +209,24 @@ export interface VisualResult { notes?: string; } +// ─── Demo iframe bridge (W6) ──────────────────────────────────────────────── +// postMessage protocol between the sandboxed demo iframe and its parent card. +// The bridge script is auto-injected into every demo HTML by the demo-card +// renderer (never the model's job): ResizeObserver → DEMO_RESIZE with the +// document height; window.onerror → DEMO_ERROR with the message. + +export interface DemoResizeMessage { + type: "demo-resize"; + height: number; +} + +export interface DemoErrorMessage { + type: "demo-error"; + message: string; +} + +export type DemoBridgeMessage = DemoResizeMessage | DemoErrorMessage; + // ─── Artifact renderer registry (client extension point) ──────────────────── /**