From 52b2344d5dea3a2314ba1715068ae2d40cd42e80 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 11 Jun 2026 13:07:25 +0000 Subject: [PATCH 1/3] feat: add turn review demo --- __tests__/turn-review-demo.test.ts | 71 ++++ app/dashboard/dashboard-client.tsx | 17 +- app/demo/dashboard/page.tsx | 18 +- app/demo/page.tsx | 4 +- app/demo/turn-review/page.tsx | 6 + components/dashboard/layout-shell.tsx | 5 +- components/dashboard/rail.tsx | 2 + components/turn-review/turn-review-board.tsx | 408 +++++++++++++++++++ lib/navigation-types.ts | 1 + lib/turn-review/demo-turn.ts | 166 ++++++++ lib/turn-review/types.ts | 57 +++ 11 files changed, 747 insertions(+), 8 deletions(-) create mode 100644 __tests__/turn-review-demo.test.ts create mode 100644 app/demo/turn-review/page.tsx create mode 100644 components/turn-review/turn-review-board.tsx create mode 100644 lib/turn-review/demo-turn.ts create mode 100644 lib/turn-review/types.ts diff --git a/__tests__/turn-review-demo.test.ts b/__tests__/turn-review-demo.test.ts new file mode 100644 index 0000000..91416cc --- /dev/null +++ b/__tests__/turn-review-demo.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; +import { DEMO_TURN_REVIEW } from "@/lib/turn-review/demo-turn"; + +const prohibitedPhrases = [ + "production-ready", + "deployed", + "live system", + "used in production", + "at scale", +]; + +const liveIntegrationPhrases = [ + "real customer", + "real order", + "production store", + "live Shopify", + "live Google Sheet", +]; + +describe("DEMO_TURN_REVIEW", () => { + it("has exactly four moves", () => { + expect(DEMO_TURN_REVIEW.moves).toHaveLength(4); + }); + + it("references valid proposed and default selected moves", () => { + const moveIds = new Set(DEMO_TURN_REVIEW.moves.map((move) => move.id)); + + expect(moveIds.has(DEMO_TURN_REVIEW.proposedMoveId)).toBe(true); + expect(moveIds.has(DEMO_TURN_REVIEW.defaultSelectedMoveId)).toBe(true); + }); + + it("includes regret, rollback, and linked receipt previews for every move", () => { + for (const move of DEMO_TURN_REVIEW.moves) { + expect(move.regret.ifChosen).toBeTruthy(); + expect(move.regret.ifRejected).toBeTruthy(); + + if (move.rollback.available) { + expect(move.rollback.steps.length).toBeGreaterThan(0); + } + + expect(move.receiptPreview.chosenMoveId).toBe(move.id); + } + }); + + it("uses cleared state only for low-blast-radius moves", () => { + const clearedMoves = DEMO_TURN_REVIEW.moves.filter( + (move) => move.policyState === "cleared", + ); + + expect(clearedMoves.length).toBeGreaterThan(0); + for (const move of clearedMoves) { + expect(move.blastRadius).toBeLessThanOrEqual(35); + } + }); + + it("does not include prohibited public claims", () => { + const serialized = JSON.stringify(DEMO_TURN_REVIEW).toLowerCase(); + + for (const phrase of prohibitedPhrases) { + expect(serialized).not.toContain(phrase.toLowerCase()); + } + }); + + it("does not include live integration phrases", () => { + const serialized = JSON.stringify(DEMO_TURN_REVIEW).toLowerCase(); + + for (const phrase of liveIntegrationPhrases) { + expect(serialized).not.toContain(phrase.toLowerCase()); + } + }); +}); diff --git a/app/dashboard/dashboard-client.tsx b/app/dashboard/dashboard-client.tsx index 8f2df7f..21e3ff4 100644 --- a/app/dashboard/dashboard-client.tsx +++ b/app/dashboard/dashboard-client.tsx @@ -15,6 +15,9 @@ import { DemoAnnotationProvider } from "@/components/demo/demo-annotation-provid import { DemoAnnotationToggle } from "@/components/demo/demo-annotation-toggle"; import { VoiceProvider } from "@/components/dashboard/voice-provider"; import { VoiceIndicator } from "@/components/dashboard/voice-indicator"; +import { DEMO_TURN_REVIEW } from "@/lib/turn-review/demo-turn"; +import { TurnReviewBoard } from "@/components/turn-review/turn-review-board"; +import type { ActiveView } from "@/lib/navigation-types"; // ── Inner content that has access to layout context ────────────────── @@ -77,6 +80,8 @@ function DashboardContent({ userName }: { userName: string }) { {/* Content area — switches between workspace, chat, actions, and history */} {isWorkspaceView(activeView) ? ( + ) : activeView === "turn-review" ? ( + ) : activeView === "chat" ? ( ) : activeView === "actions" ? ( @@ -102,7 +107,15 @@ function DashboardContent({ userName }: { userName: string }) { // ── Main export ────────────────────────────────────────────────────── -export function DashboardClient({ userName, isDemo = false }: { userName: string; isDemo?: boolean }) { +export function DashboardClient({ + userName, + isDemo = false, + initialView, +}: { + userName: string; + isDemo?: boolean; + initialView?: ActiveView; +}) { const content = ( @@ -113,7 +126,7 @@ export function DashboardClient({ userName, isDemo = false }: { userName: string const shell = ( - + {content} diff --git a/app/demo/dashboard/page.tsx b/app/demo/dashboard/page.tsx index 8202027..7d32da3 100644 --- a/app/demo/dashboard/page.tsx +++ b/app/demo/dashboard/page.tsx @@ -2,19 +2,31 @@ import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import { DEMO_COOKIE_NAME } from "@/lib/demo/config"; import { DashboardClient } from "@/app/dashboard/dashboard-client"; +import { ACTIVE_VIEWS, type ActiveView } from "@/lib/navigation-types"; + +interface DemoDashboardPageProps { + searchParams?: Promise>; +} + +function parseInitialView(value: string | string[] | undefined): ActiveView | undefined { + const raw = Array.isArray(value) ? value[0] : value; + return ACTIVE_VIEWS.includes(raw as ActiveView) ? (raw as ActiveView) : undefined; +} /** * Demo dashboard — same UI as production but reads from demo cookie * instead of Auth0 session. All API calls check for the demo cookie * and return mock data. */ -export default async function DemoDashboardPage() { +export default async function DemoDashboardPage({ searchParams }: DemoDashboardPageProps) { const cookieStore = await cookies(); const hasDemo = cookieStore.has(DEMO_COOKIE_NAME); + const params = searchParams ? await searchParams : undefined; if (!hasDemo) { - redirect("/demo"); + const view = Array.isArray(params?.view) ? params?.view[0] : params?.view; + redirect(view ? `/demo?next=${encodeURIComponent(`/demo/dashboard?view=${view}`)}` : "/demo"); } - return ; + return ; } diff --git a/app/demo/page.tsx b/app/demo/page.tsx index 53bbd08..05780bf 100644 --- a/app/demo/page.tsx +++ b/app/demo/page.tsx @@ -14,9 +14,11 @@ export default function DemoLoginPage() { function handleLogin() { setLoading(true); + const next = new URLSearchParams(window.location.search).get("next"); + const safeNext = next?.startsWith("/demo/") ? next : "/demo/dashboard"; // Set demo session cookie (client-side, 1 hour expiry) document.cookie = `${DEMO_COOKIE_NAME}=true; path=/; max-age=3600; SameSite=Lax`; - router.push("/demo/dashboard"); + router.push(safeNext); } return ( diff --git a/app/demo/turn-review/page.tsx b/app/demo/turn-review/page.tsx new file mode 100644 index 0000000..a8f7d86 --- /dev/null +++ b/app/demo/turn-review/page.tsx @@ -0,0 +1,6 @@ +import { DEMO_TURN_REVIEW } from "@/lib/turn-review/demo-turn"; +import { TurnReviewBoard } from "@/components/turn-review/turn-review-board"; + +export default function TurnReviewDemoPage() { + return ; +} diff --git a/components/dashboard/layout-shell.tsx b/components/dashboard/layout-shell.tsx index d1b2605..72c9b43 100644 --- a/components/dashboard/layout-shell.tsx +++ b/components/dashboard/layout-shell.tsx @@ -75,15 +75,16 @@ interface LayoutShellProps { children: ReactNode; userName: string; isDemo?: boolean; + initialView?: ActiveView; } -export function LayoutShell({ children, userName, isDemo = false }: LayoutShellProps) { +export function LayoutShell({ children, userName, isDemo = false, initialView = "workspace" }: LayoutShellProps) { const [railExpanded, setRailExpanded] = useState(false); const [inspectorItem, setInspectorItem] = useState( null, ); const [activeChatId, setActiveChatId] = useState(() => generateChatId()); - const [activeView, setActiveView] = useState("workspace"); + const [activeView, setActiveView] = useState(initialView); const [pendingPrompt, setPendingPromptState] = useState(null); const pendingPromptRef = useRef(null); const [pendingAction, setPendingActionState] = useState(null); diff --git a/components/dashboard/rail.tsx b/components/dashboard/rail.tsx index d4276af..c193af8 100644 --- a/components/dashboard/rail.tsx +++ b/components/dashboard/rail.tsx @@ -8,6 +8,7 @@ import { ClockIcon, LayersIcon, ActivityIcon, + GitBranchIcon, HistoryIcon, ZapIcon, MenuIcon, @@ -42,6 +43,7 @@ const NAV_GROUPS = [ { items: [ { icon: LayoutGridIcon, label: "Workspace", id: "workspace" as const, enabled: true }, + { icon: GitBranchIcon, label: "Turn Review", id: "turn-review" as const, enabled: true }, { icon: MessageSquareIcon, label: "Chat", id: "chat" as const, enabled: true }, { icon: ClockIcon, label: "Timeline", id: "timeline" as const, enabled: true }, { icon: LayersIcon, label: "Drafts", id: "drafts" as const, enabled: true }, diff --git a/components/turn-review/turn-review-board.tsx b/components/turn-review/turn-review-board.tsx new file mode 100644 index 0000000..4daac59 --- /dev/null +++ b/components/turn-review/turn-review-board.tsx @@ -0,0 +1,408 @@ +"use client"; + +import { useMemo, useState } from "react"; +import type { TurnMove, TurnReviewScenario, TurnRiskState } from "@/lib/turn-review/types"; + +interface TurnReviewBoardProps { + scenario: TurnReviewScenario; + embedded?: boolean; +} + +const stateStyles: Record = { + cleared: "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900 dark:bg-emerald-950/40 dark:text-emerald-300", + review: "border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-300", + blocked: "border-red-200 bg-red-50 text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300", +}; + +const stateSurfaceStyles: Record = { + cleared: "border-emerald-200 bg-emerald-50/70 text-emerald-950 dark:border-emerald-900 dark:bg-emerald-950/20 dark:text-emerald-100", + review: "border-amber-200 bg-amber-50/80 text-amber-950 dark:border-amber-900 dark:bg-amber-950/20 dark:text-amber-100", + blocked: "border-red-200 bg-red-50/80 text-red-950 dark:border-red-900 dark:bg-red-950/20 dark:text-red-100", +}; + +const moveContrast: Record< + TurnMove["kind"], + { + label: string; + mobileSignal: string; + emphasis: string; + border: string; + tint: string; + accent: string; + } +> = { + aggressive: { + label: "Max reach / max cleanup", + mobileSignal: "Fastest upside, widest blast radius", + emphasis: "HIGH BLAST", + border: "border-red-200 dark:border-red-900", + tint: "bg-red-50/70 dark:bg-red-950/20", + accent: "text-red-700 dark:text-red-300", + }, + controlled: { + label: "Best useful test", + mobileSignal: "Balanced: one SKU, approval, rollback", + emphasis: "RECOMMENDED", + border: "border-amber-200 dark:border-amber-900", + tint: "bg-amber-50/80 dark:bg-amber-950/20", + accent: "text-amber-700 dark:text-amber-300", + }, + scout: { + label: "Safest learning", + mobileSignal: "No write, better evidence, slower result", + emphasis: "LOWEST RISK", + border: "border-emerald-200 dark:border-emerald-900", + tint: "bg-emerald-50/70 dark:bg-emerald-950/20", + accent: "text-emerald-700 dark:text-emerald-300", + }, + defensive: { + label: "Stop and review", + mobileSignal: "Avoids execution, highest opportunity cost", + emphasis: "HOLD TURN", + border: "border-border", + tint: "bg-muted/50", + accent: "text-muted-foreground", + }, +}; + +export function TurnReviewBoard({ scenario, embedded = false }: TurnReviewBoardProps) { + const [selectedMoveId, setSelectedMoveId] = useState(scenario.defaultSelectedMoveId); + + const proposedMove = useMemo( + () => scenario.moves.find((move) => move.id === scenario.proposedMoveId) ?? scenario.moves[0], + [scenario.moves, scenario.proposedMoveId], + ); + const selectedMove = useMemo( + () => scenario.moves.find((move) => move.id === selectedMoveId) ?? proposedMove, + [scenario.moves, selectedMoveId, proposedMove], + ); + + return ( +
+
+ + +
+ + +
+ +
+ + +
+
+
+ ); +} + +function SituationStrip({ scenario }: { scenario: TurnReviewScenario }) { + return ( +
+
+
+

+ Turn Review · synthetic demo +

+

+ {scenario.title} +

+

+ {scenario.situation} +

+
+
+

No execution

+

Choose a move; preview the receipt; no API calls.

+
+
+
+ ); +} + +function BoardMeters({ scenario }: { scenario: TurnReviewScenario }) { + return ( +
+
+

+ Board meters +

+ pressure before the turn +
+
+ {scenario.board.map((metric) => ( +
+
+
+

{metric.label}

+

{metric.note}

+
+ +
+
+
+
+

+ {metric.value}/100 +

+
+ ))} +
+
+ ); +} + +function ProposedMoveCard({ move, selectedMoveId }: { move: TurnMove; selectedMoveId: string }) { + const selectedDiffers = selectedMoveId !== move.id; + + return ( +
+
+
+

Proposed path

+

{move.label}

+

{move.oneLine}

+
+ +
+
+ + + +
+
+
+

Why

+

+ Strongest useful action: bounded SKU, approval gate, rehearsed return path. +

+
+
+

Selection

+

+ {selectedDiffers ? "Operator has selected a different move." : "Operator selection matches the AI proposal."} +

+
+
+
+ ); +} + +function MoveSelectionLane({ + moves, + proposedMoveId, + selectedMoveId, + onSelect, +}: { + moves: TurnMove[]; + proposedMoveId: string; + selectedMoveId: string; + onSelect: (moveId: string) => void; +}) { + return ( +
+
+

+ Move selection +

+ compare before reading receipt +
+
+ {moves.map((move) => ( + onSelect(move.id)} + /> + ))} +
+
+ ); +} + +function MoveCard({ + move, + isProposed, + isSelected, + onSelect, +}: { + move: TurnMove; + isProposed: boolean; + isSelected: boolean; + onSelect: () => void; +}) { + const contrast = moveContrast[move.kind]; + + return ( + + ); +} + +function EndTurnReceipt({ move }: { move: TurnMove }) { + const receipt = move.receiptPreview; + + return ( +
+

Receipt preview

+

End turn: {move.label}

+

+ Static receipt preview only. The button does not call a backend. +

+ +
+ + + + + +
+ +
+

Regret preview

+

If chosen: {move.regret.ifChosen}

+

If rejected: {move.regret.ifRejected}

+
+ + {move.rollback.available && ( +
+

Rollback rehearsal

+
    + {move.rollback.steps.map((step) => ( +
  1. {step}
  2. + ))} +
+

Residual: {move.rollback.residualRisk}

+
+ )} + + +
+ ); +} + +function StatePill({ state }: { state: TurnRiskState }) { + return ( + + {state} + + ); +} + +function Stat({ label, value }: { label: string; value: number }) { + return ( +
+

{label}

+

{value}

+
+ ); +} + +function DecisionMetric({ + label, + value, + goodWhen, +}: { + label: string; + value: number; + goodWhen: "high" | "low"; +}) { + const isStrong = goodWhen === "high" ? value >= 70 : value <= 35; + const isWeak = goodWhen === "high" ? value < 55 : value > 70; + const signal = isStrong ? "good" : isWeak ? "watch" : "mid"; + const signalClass = isStrong + ? "border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950/30 dark:text-emerald-200" + : isWeak + ? "border-red-200 bg-red-50 text-red-800 dark:border-red-900 dark:bg-red-950/30 dark:text-red-200" + : "border-amber-200 bg-amber-50 text-amber-800 dark:border-amber-900 dark:bg-amber-950/30 dark:text-amber-200"; + + return ( +
+
+ {label} + {value} +
+
+
+
+

{signal}

+
+ ); +} + +function ReceiptRow({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} diff --git a/lib/navigation-types.ts b/lib/navigation-types.ts index a01204c..1f763e8 100644 --- a/lib/navigation-types.ts +++ b/lib/navigation-types.ts @@ -10,6 +10,7 @@ export const ACTIVE_VIEWS = [ "history", "actions", "workspace", + "turn-review", "drafts", "timeline", "activity", diff --git a/lib/turn-review/demo-turn.ts b/lib/turn-review/demo-turn.ts new file mode 100644 index 0000000..7d30669 --- /dev/null +++ b/lib/turn-review/demo-turn.ts @@ -0,0 +1,166 @@ +import type { TurnReviewScenario } from "./types"; + +export const DEMO_TURN_REVIEW: TurnReviewScenario = { + id: "turn-review-running-shoe-promo", + title: "Running-shoe promotion turn review", + situation: + "An AI agent proposes a 20% promotional move for STR-001 Classic Runner after finding margin room and seasonal demand. Choose the turn before any write happens.", + proposedMoveId: "move-controlled-promo", + defaultSelectedMoveId: "move-controlled-promo", + board: [ + { + id: "margin-pressure", + label: "Margin pressure", + value: 68, + state: "review", + note: "Discount fits the floor, but leaves little room for stacking.", + }, + { + id: "inventory-exposure", + label: "Inventory exposure", + value: 46, + state: "review", + note: "Single SKU has enough stock for a limited test.", + }, + { + id: "customer-comms-blast-radius", + label: "Customer comms blast radius", + value: 72, + state: "blocked", + note: "Broad messaging would spread the change before learning.", + }, + { + id: "rollback-confidence", + label: "Rollback confidence", + value: 82, + state: "cleared", + note: "Promo flag and price field can be rehearsed before approval.", + }, + ], + moves: [ + { + id: "move-aggressive-promo", + kind: "aggressive", + label: "Execute broad promo now", + oneLine: "Apply the 20% promo across the running group and notify shoppers.", + policyState: "blocked", + blastRadius: 88, + reversibility: 42, + evidenceStrength: 54, + timeToLearn: "Same day", + operatorCost: "High: approval, comms review, rollback watch", + regret: { + ifChosen: "If margin assumptions are wrong, many shoppers see a noisy price correction.", + ifRejected: "You may miss a short demand window for the running group.", + }, + rollback: { + available: true, + steps: [ + "Turn promo flags off for affected synthetic SKUs.", + "Clear promo prices back to blank fields.", + "Send an internal receipt note for manual comms review.", + ], + residualRisk: "Customer-facing comms may already have copied the broader offer.", + }, + receiptPreview: { + receiptId: "trn-prev-001-aggressive", + chosenMoveId: "move-aggressive-promo", + policyDecision: "blocked: broad promo plus comms blast radius", + approvalMode: "Escalated approval required before any write", + rollbackAvailable: true, + auditLabel: "synthetic-turn/aggressive/broad-promo", + }, + }, + { + id: "move-controlled-promo", + kind: "controlled", + label: "Run limited promo with approval", + oneLine: "Activate 20% on STR-001 only, gated by approval and receipt preview.", + policyState: "review", + blastRadius: 48, + reversibility: 78, + evidenceStrength: 83, + timeToLearn: "24 hours", + operatorCost: "Medium: one approval and one rollback rehearsal", + regret: { + ifChosen: "If demand is weak, one SKU carries the test cost and can be reversed cleanly.", + ifRejected: "You lose the strongest useful evidence without reducing much risk.", + }, + rollback: { + available: true, + steps: [ + "Restore STR-001 promo active to false.", + "Clear STR-001 promo price.", + "Attach policy and approval result to the synthetic receipt.", + ], + residualRisk: "Small merchandising confusion if the offer is copied elsewhere manually.", + }, + receiptPreview: { + receiptId: "trn-prev-002-controlled", + chosenMoveId: "move-controlled-promo", + policyDecision: "review: single SKU write with margin guardrail", + approvalMode: "Step-up approval before synthetic write simulation", + rollbackAvailable: true, + auditLabel: "synthetic-turn/controlled/str-001-promo", + }, + }, + { + id: "move-scout-check", + kind: "scout", + label: "Scout with read-only check", + oneLine: "Do not write; verify price, stock, and margin assumptions first.", + policyState: "cleared", + blastRadius: 18, + reversibility: 96, + evidenceStrength: 67, + timeToLearn: "1 hour", + operatorCost: "Low: review findings before the next turn", + regret: { + ifChosen: "You delay the promo, but learn whether the write is still safe.", + ifRejected: "You may approve with stale inventory or margin evidence.", + }, + rollback: { + available: false, + steps: [], + residualRisk: "No write occurs, so rollback is not needed.", + }, + receiptPreview: { + receiptId: "trn-prev-003-scout", + chosenMoveId: "move-scout-check", + policyDecision: "cleared: read-only scout", + approvalMode: "No approval needed for read-only synthetic check", + rollbackAvailable: false, + auditLabel: "synthetic-turn/scout/read-only-check", + }, + }, + { + id: "move-defensive-block", + kind: "defensive", + label: "Block and require manual review", + oneLine: "Stop the turn until an operator confirms margin and comms constraints.", + policyState: "review", + blastRadius: 28, + reversibility: 90, + evidenceStrength: 45, + timeToLearn: "Next review cycle", + operatorCost: "Medium: manual check and a new turn decision", + regret: { + ifChosen: "You avoid execution risk, but the demand signal may expire.", + ifRejected: "You accept more automation risk than the current evidence supports.", + }, + rollback: { + available: false, + steps: [], + residualRisk: "No write occurs; opportunity cost is the main residual risk.", + }, + receiptPreview: { + receiptId: "trn-prev-004-defensive", + chosenMoveId: "move-defensive-block", + policyDecision: "review: blocked by operator choice", + approvalMode: "Manual review required before a future turn", + rollbackAvailable: false, + auditLabel: "synthetic-turn/defensive/manual-review", + }, + }, + ], +}; diff --git a/lib/turn-review/types.ts b/lib/turn-review/types.ts new file mode 100644 index 0000000..d7f5fa0 --- /dev/null +++ b/lib/turn-review/types.ts @@ -0,0 +1,57 @@ +export type TurnMoveKind = "aggressive" | "controlled" | "scout" | "defensive"; + +export type TurnRiskState = "cleared" | "review" | "blocked"; + +export interface BoardPressureMetric { + id: string; + label: string; + value: number; // 0-100 + state: TurnRiskState; + note: string; +} + +export interface RegretPreview { + ifChosen: string; + ifRejected: string; +} + +export interface RollbackRehearsal { + available: boolean; + steps: string[]; + residualRisk: string; +} + +export interface TurnReceiptPreview { + receiptId: string; + chosenMoveId: string; + policyDecision: string; + approvalMode: string; + rollbackAvailable: boolean; + auditLabel: string; +} + +export interface TurnMove { + id: string; + kind: TurnMoveKind; + label: string; + oneLine: string; + policyState: TurnRiskState; + blastRadius: number; // 0-100 + reversibility: number; // 0-100 + evidenceStrength: number; // 0-100 + timeToLearn: string; + operatorCost: string; + regret: RegretPreview; + rollback: RollbackRehearsal; + receiptPreview: TurnReceiptPreview; +} + +export interface TurnReviewScenario { + id: string; + title: string; + situation: string; + proposedMoveId: string; + board: BoardPressureMetric[]; + moves: TurnMove[]; + defaultSelectedMoveId: string; +} From 0377c48ff0de9469dc895a1cc7269046f5ba9c81 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 11 Jun 2026 14:26:22 +0000 Subject: [PATCH 2/3] docs: update changelog for turn review demo --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 627e461..9fb8b1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Features + +- Add synthetic Turn Review demo for comparing operator move choices inside the demo dashboard. + ## [0.2.1] — 2026-04-06 ### Features From 4c6c8158e8bef3a14ac68a84498752db33d5bc42 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 11 Jun 2026 16:18:20 +0000 Subject: [PATCH 3/3] fix: address turn review accessibility feedback --- components/turn-review/turn-review-board.tsx | 6 ++++-- lib/turn-review/demo-turn.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/components/turn-review/turn-review-board.tsx b/components/turn-review/turn-review-board.tsx index 4daac59..7674a70 100644 --- a/components/turn-review/turn-review-board.tsx +++ b/components/turn-review/turn-review-board.tsx @@ -244,6 +244,7 @@ function MoveCard({ diff --git a/lib/turn-review/demo-turn.ts b/lib/turn-review/demo-turn.ts index 7d30669..d444b64 100644 --- a/lib/turn-review/demo-turn.ts +++ b/lib/turn-review/demo-turn.ts @@ -1,4 +1,4 @@ -import type { TurnReviewScenario } from "./types"; +import type { TurnReviewScenario } from "@/lib/turn-review/types"; export const DEMO_TURN_REVIEW: TurnReviewScenario = { id: "turn-review-running-shoe-promo",