Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 71 additions & 0 deletions __tests__/turn-review-demo.test.ts
Original file line number Diff line number Diff line change
@@ -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());
}
});
});
17 changes: 15 additions & 2 deletions app/dashboard/dashboard-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ──────────────────

Expand Down Expand Up @@ -77,6 +80,8 @@ function DashboardContent({ userName }: { userName: string }) {
{/* Content area — switches between workspace, chat, actions, and history */}
{isWorkspaceView(activeView) ? (
<Workspace />
) : activeView === "turn-review" ? (
<TurnReviewBoard scenario={DEMO_TURN_REVIEW} embedded />
) : activeView === "chat" ? (
<Chat key={activeChatId} chatId={activeChatId} />
) : activeView === "actions" ? (
Expand All @@ -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 = (
<WorkspaceProvider>
<VoiceProvider>
Expand All @@ -113,7 +126,7 @@ export function DashboardClient({ userName, isDemo = false }: { userName: string

const shell = (
<StatusBarProvider>
<LayoutShell userName={userName} isDemo={isDemo}>
<LayoutShell userName={userName} isDemo={isDemo} initialView={initialView}>
{content}
</LayoutShell>
</StatusBarProvider>
Expand Down
18 changes: 15 additions & 3 deletions app/demo/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, string | string[] | undefined>>;
}

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 <DashboardClient userName="Demo User" isDemo />;
return <DashboardClient userName="Demo User" isDemo initialView={parseInitialView(params?.view)} />;
}
4 changes: 3 additions & 1 deletion app/demo/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
6 changes: 6 additions & 0 deletions app/demo/turn-review/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <TurnReviewBoard scenario={DEMO_TURN_REVIEW} />;
}
5 changes: 3 additions & 2 deletions components/dashboard/layout-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<InspectableItem | null>(
null,
);
const [activeChatId, setActiveChatId] = useState(() => generateChatId());
const [activeView, setActiveView] = useState<ActiveView>("workspace");
const [activeView, setActiveView] = useState<ActiveView>(initialView);
const [pendingPrompt, setPendingPromptState] = useState<string | null>(null);
const pendingPromptRef = useRef<string | null>(null);
const [pendingAction, setPendingActionState] = useState<ActionDefinition | null>(null);
Expand Down
2 changes: 2 additions & 0 deletions components/dashboard/rail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ClockIcon,
LayersIcon,
ActivityIcon,
GitBranchIcon,
HistoryIcon,
ZapIcon,
MenuIcon,
Expand Down Expand Up @@ -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 },
Expand Down
Loading
Loading