refactor: simplify presentation audience surface

This commit is contained in:
lda
2026-07-05 18:31:34 +07:00 Verified
parent 20f99a5034
commit 8a18732007
15 changed files with 118 additions and 956 deletions
@@ -1,15 +0,0 @@
type ChatDockProps = {
readonly openChat: () => void;
};
export const ChatDock = ({ openChat }: ChatDockProps) => (
<button
type="button"
className="chat-dock"
aria-label="open agent chat"
onClick={openChat}
>
<span className="chat-dock__icon">💬</span>
<span className="chat-dock__label">Chat</span>
</button>
);
@@ -1,52 +0,0 @@
import { discussionBranches, findScene, type MainSceneId } from "./storyboard.js";
type DiscussionIndexProps = {
readonly onSelect: (branchId: string) => void;
};
type BranchGroup = {
readonly parentSceneId: MainSceneId;
readonly sceneTitle: string;
readonly branches: typeof discussionBranches[number][];
};
const groupByParentScene = (): readonly BranchGroup[] => {
const groups = new Map<string, BranchGroup>();
for (const branch of discussionBranches) {
let group = groups.get(branch.parentSceneId);
if (!group) {
const scene = findScene(branch.parentSceneId);
group = {
parentSceneId: branch.parentSceneId,
sceneTitle: scene?.title ?? branch.parentSceneId,
branches: [],
};
groups.set(branch.parentSceneId, group);
}
group.branches.push(branch);
}
return Array.from(groups.values());
};
export const DiscussionIndex = ({ onSelect }: DiscussionIndexProps) => {
const groups = groupByParentScene();
return (
<div className="discussion-index" role="dialog" aria-label="discussion topics">
<h2>Discussion Topics</h2>
{groups.map((group) => (
<section key={group.parentSceneId}>
<h3>{group.sceneTitle}</h3>
<ul>
{group.branches.map((branch) => (
<li key={branch.id}>
<button type="button" onClick={() => onSelect(branch.id)}>
{branch.title}
</button>
</li>
))}
</ul>
</section>
))}
</div>
);
};
@@ -112,7 +112,7 @@ export const OperatorChat = ({ state, messages, onApprove, onDeny }: OperatorCha
const visibleMessages = messages && messages.length > 0 ? messages : fallbackMessages(state); const visibleMessages = messages && messages.length > 0 ? messages : fallbackMessages(state);
const composition = compositionForState(state); const composition = compositionForState(state);
return ( return (
<aside className="operator-chat" data-mode={composition.chatMode} data-chat-theme={composition.chatTheme} aria-label="scripted operator chat"> <aside className="operator-chat" data-mode={composition.chatMode} aria-label="scripted operator chat">
{visibleMessages.map((message) => ( {visibleMessages.map((message) => (
<div key={message.id} className={`chat-message chat-message--${message.role === "user" ? "operator" : "system"}`}> <div key={message.id} className={`chat-message chat-message--${message.role === "user" ? "operator" : "system"}`}>
<strong>{message.role === "user" ? "Operator" : "lda.chat"}</strong> <strong>{message.role === "user" ? "Operator" : "lda.chat"}</strong>
@@ -23,12 +23,12 @@ describe("PresentationRoute", () => {
expect(await screen.findByText(/The interface delegates durable work to lda\.chat/i)).toBeInTheDocument(); expect(await screen.findByText(/The interface delegates durable work to lda\.chat/i)).toBeInTheDocument();
}); });
it("renders replay-first chat, beat rail, and stage caption", () => { it("renders audience progress chrome without rail or mode label", () => {
render(<PresentationRoute />); render(<PresentationRoute />);
expect(screen.getByText("Prepare the thesis readiness report.")).toBeInTheDocument(); expect(screen.getByText("Prepare the thesis readiness report.")).toBeInTheDocument();
expect(screen.getByLabelText(/presentation scene rail/i)).toBeInTheDocument(); expect(screen.queryByLabelText(/presentation scene rail/i)).not.toBeInTheDocument();
expect(screen.getByText("Replay · running")).toBeInTheDocument(); expect(screen.getByLabelText("scene position")).toBeInTheDocument();
}); });
it("shows node spotlight when a graph node is selected", async () => { it("shows node spotlight when a graph node is selected", async () => {
@@ -63,16 +63,13 @@ describe("PresentationRoute", () => {
expect(await screen.findByRole("dialog", { name: /issue review/i })).toBeInTheDocument(); expect(await screen.findByRole("dialog", { name: /issue review/i })).toBeInTheDocument();
}); });
it("opens a positioning branch and returns to the exact originating beat", async () => { it("opens a positioning branch via hash and returns to the parent scene first beat", async () => {
window.location.hash = "#scene/positioning/lda-position"; window.location.hash = "#discuss/hosted-automation";
render(<PresentationRoute />); render(<PresentationRoute />);
await userEvent.click(screen.getByRole("button", { name: /discussion topics/i })); expect(await screen.findByRole("button", { name: /return to positioning/i })).toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: /hosted automation/i }));
expect(window.location.hash).toBe("#discuss/hosted-automation");
await userEvent.click(screen.getByRole("button", { name: /return to positioning/i })); await userEvent.click(screen.getByRole("button", { name: /return to positioning/i }));
expect(window.location.hash).toBe("#scene/positioning/lda-position"); expect(window.location.hash).toBe("#scene/positioning/landscape");
}); });
it("uses the parent scene as return location for a directly linked branch", async () => { it("uses the parent scene as return location for a directly linked branch", async () => {
@@ -82,11 +79,11 @@ describe("PresentationRoute", () => {
expect(window.location.hash).toBe("#scene/positioning/landscape"); expect(window.location.hash).toBe("#scene/positioning/landscape");
}); });
it("renders stable chat, primary, evidence, and navigation regions", () => { it("renders stable chat, primary, evidence, and progress regions", () => {
render(<PresentationRoute />); render(<PresentationRoute />);
expect(screen.getByLabelText(/agent chat region/i)).toBeInTheDocument(); expect(screen.getByLabelText(/agent chat region/i)).toBeInTheDocument();
expect(screen.getByLabelText(/primary presentation region/i)).toBeInTheDocument(); expect(screen.getByLabelText(/primary presentation region/i)).toBeInTheDocument();
expect(screen.getByLabelText(/evidence region/i)).toBeInTheDocument(); expect(screen.getByLabelText(/evidence region/i)).toBeInTheDocument();
expect(screen.getByLabelText(/presentation scene rail/i)).toBeInTheDocument(); expect(screen.getByLabelText("scene position")).toBeInTheDocument();
}); });
}); });
@@ -6,12 +6,9 @@ import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
import { useDemoTimeline } from "../demo/useDemoTimeline.js"; import { useDemoTimeline } from "../demo/useDemoTimeline.js";
import { PresentationCanvas } from "./PresentationCanvas.js"; import { PresentationCanvas } from "./PresentationCanvas.js";
import { PresentationStage } from "./PresentationStage.js"; import { PresentationStage } from "./PresentationStage.js";
import { PresenterControls } from "./PresenterControls.js";
import { ChatDock } from "./ChatDock.js";
import { import {
initialPresentationState, initialPresentationState,
presentationReducer, presentationReducer,
compositionForState,
} from "./presentation-state.js"; } from "./presentation-state.js";
import { hashForLocation } from "./storyboard-navigation.js"; import { hashForLocation } from "./storyboard-navigation.js";
import { findScene } from "./storyboard.js"; import { findScene } from "./storyboard.js";
@@ -93,9 +90,6 @@ export const PresentationRoute = () => {
dispatch({ type: "previous" }); dispatch({ type: "previous" });
} else if (event.key === "Escape") { } else if (event.key === "Escape") {
dispatch({ type: "close_overlay" }); dispatch({ type: "close_overlay" });
} else if (event.key === "p" || event.key === "P") {
if (!isBodyEvent) return;
dispatch({ type: "toggle_controls" });
} }
}; };
window.addEventListener("keydown", onKeyDown); window.addEventListener("keydown", onKeyDown);
@@ -164,12 +158,6 @@ export const PresentationRoute = () => {
setEvidence(replayEvidence); setEvidence(replayEvidence);
}, [demo, replayEvidence]); }, [demo, replayEvidence]);
const handleResetOverrides = useCallback(() => {
dispatch({ type: "set_stage_theme", theme: null });
dispatch({ type: "set_chat_theme", theme: null });
dispatch({ type: "set_chat_mode", mode: null });
}, []);
const handleResetScene = useCallback(() => { const handleResetScene = useCallback(() => {
if (state.location.kind !== "main") return; if (state.location.kind !== "main") return;
const scene = findScene(state.location.sceneId); const scene = findScene(state.location.sceneId);
@@ -181,9 +169,6 @@ export const PresentationRoute = () => {
dispatch({ type: "toggle_motion" }); dispatch({ type: "toggle_motion" });
}, []); }, []);
const composition = compositionForState(state);
const showChatDock = composition.chatMode === "dock" && state.location.kind !== "discussion";
return ( return (
<main className="presentation-route" aria-label="lda.chat presentation" data-motion={state.motionDisabled ? "disabled" : "enabled"}> <main className="presentation-route" aria-label="lda.chat presentation" data-motion={state.motionDisabled ? "disabled" : "enabled"}>
<PresentationCanvas> <PresentationCanvas>
@@ -200,25 +185,8 @@ export const PresentationRoute = () => {
closeOverlay={() => dispatch({ type: "close_overlay" })} closeOverlay={() => dispatch({ type: "close_overlay" })}
openDiscussion={handleOpenDiscussion} openDiscussion={handleOpenDiscussion}
closeDiscussion={handleCloseDiscussion} closeDiscussion={handleCloseDiscussion}
toggleDiscussionIndex={() => dispatch({ type: "toggle_discussion_index" })}
/> />
</PresentationCanvas> </PresentationCanvas>
{showChatDock && <ChatDock openChat={() => dispatch({ type: "set_chat_mode", mode: "rail" })} />}
{state.controlsOpen && (
<PresenterControls
state={state}
next={() => dispatch({ type: "next" })}
previous={() => dispatch({ type: "previous" })}
jump={handleJump}
setStageTheme={(theme) => dispatch({ type: "set_stage_theme", theme })}
setChatTheme={(theme) => dispatch({ type: "set_chat_theme", theme })}
setChatMode={(mode) => dispatch({ type: "set_chat_mode", mode })}
forceReplay={handleForceReplay}
resetOverrides={handleResetOverrides}
resetScene={handleResetScene}
toggleMotion={handleToggleMotion}
/>
)}
<button <button
type="button" type="button"
onClick={() => agent.startPreparedReplay()} onClick={() => agent.startPreparedReplay()}
@@ -2,15 +2,14 @@ import { domAnimation, LayoutGroup, LazyMotion } from "motion/react";
import type { EvidenceRecord } from "../app/state.js"; import type { EvidenceRecord } from "../app/state.js";
import type { AgentMessage } from "../demo/agent/events.js"; import type { AgentMessage } from "../demo/agent/events.js";
import { SceneBody } from "./SceneBody.js"; import { SceneBody } from "./SceneBody.js";
import { SceneRail } from "./SceneRail.js";
import { DiscussionIndex } from "./DiscussionIndex.js";
import { DiscussionPanel } from "./DiscussionPanel.js"; import { DiscussionPanel } from "./DiscussionPanel.js";
import { EvidenceDrawer } from "./EvidenceDrawer.js"; import { EvidenceDrawer } from "./EvidenceDrawer.js";
import { OperatorChat } from "./OperatorChat.js"; import { OperatorChat } from "./OperatorChat.js";
import { SceneProgress } from "./SceneProgress.js";
import type { PresentationState } from "./presentation-state.js"; import type { PresentationState } from "./presentation-state.js";
import { compositionForState } from "./presentation-state.js"; import { compositionForState } from "./presentation-state.js";
import type { DemoTimelineController } from "../demo/useDemoTimeline.js"; import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
import { findScene, type PresentationLocation } from "./storyboard.js"; import { findScene, type MainLocation, type PresentationLocation } from "./storyboard.js";
type PresentationStageProps = { type PresentationStageProps = {
readonly state: PresentationState; readonly state: PresentationState;
@@ -25,7 +24,6 @@ type PresentationStageProps = {
readonly closeOverlay: () => void; readonly closeOverlay: () => void;
readonly openDiscussion: (branchId: string) => void; readonly openDiscussion: (branchId: string) => void;
readonly closeDiscussion: () => void; readonly closeDiscussion: () => void;
readonly toggleDiscussionIndex: () => void;
}; };
export const PresentationStage = ({ export const PresentationStage = ({
@@ -41,7 +39,6 @@ export const PresentationStage = ({
closeOverlay, closeOverlay,
openDiscussion, openDiscussion,
closeDiscussion, closeDiscussion,
toggleDiscussionIndex,
}: PresentationStageProps) => { }: PresentationStageProps) => {
const composition = compositionForState(state); const composition = compositionForState(state);
@@ -55,8 +52,6 @@ export const PresentationStage = ({
<LayoutGroup id="presentation-stage"> <LayoutGroup id="presentation-stage">
<div <div
className="presentation-stage" className="presentation-stage"
data-stage-theme={composition.stageTheme}
data-chat-theme={composition.chatTheme}
data-chat-mode={composition.chatMode} data-chat-mode={composition.chatMode}
data-evidence-mode={composition.evidenceMode} data-evidence-mode={composition.evidenceMode}
data-scene-view={activeSceneView} data-scene-view={activeSceneView}
@@ -68,45 +63,27 @@ export const PresentationStage = ({
{state.location.kind === "discussion" ? ( {state.location.kind === "discussion" ? (
<DiscussionPanel branchId={state.location.branchId} onClose={closeDiscussion} /> <DiscussionPanel branchId={state.location.branchId} onClose={closeDiscussion} />
) : ( ) : (
<> <SceneBody
<SceneBody location={state.location}
location={state.location} demo={demo}
demo={demo} selectedNodeId={state.selectedNodeId}
selectedNodeId={state.selectedNodeId} selectNode={selectNode}
selectNode={selectNode} openEvidence={openEvidence}
openEvidence={openEvidence} onFocusPathChange={(path) => {
onFocusPathChange={(path) => { if (state.location.kind === "main") {
if (state.location.kind === "main") { jump({ ...state.location, focusPath: path });
jump({ ...state.location, focusPath: path }); }
}
}}
motionDisabled={false}
/>
<button
type="button"
className="presentation-stage__discussion-toggle"
onClick={toggleDiscussionIndex}
>
Discussion topics
</button>
</>
)}
{state.discussionIndexOpen && (
<DiscussionIndex
onSelect={(branchId) => {
toggleDiscussionIndex();
openDiscussion(branchId);
}} }}
motionDisabled={false}
/> />
)} )}
<p className="presentation-stage__mode">
{demo.state.mode === "replay" ? "Replay" : "Live"} · {demo.state.phase}
</p>
</section> </section>
<aside className="presentation-stage__evidence" aria-label="evidence region"> <aside className="presentation-stage__evidence" aria-label="evidence region">
<EvidenceDrawer records={evidence} mode={composition.evidenceMode} close={closeOverlay} /> <EvidenceDrawer records={evidence} mode={composition.evidenceMode} close={closeOverlay} />
</aside> </aside>
<SceneRail location={state.location} jump={jump} /> {state.location.kind === "main" && (
<SceneProgress location={state.location} />
)}
</div> </div>
</LayoutGroup> </LayoutGroup>
</LazyMotion> </LazyMotion>
@@ -1,48 +0,0 @@
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { PresenterControls } from "./PresenterControls.js";
import { ChatDock } from "./ChatDock.js";
import { initialPresentationState } from "./presentation-state.js";
afterEach(() => cleanup());
describe("PresenterControls", () => {
const props = {
state: initialPresentationState,
next: vi.fn(),
previous: vi.fn(),
jump: vi.fn(),
setStageTheme: vi.fn(),
setChatTheme: vi.fn(),
setChatMode: vi.fn(),
forceReplay: vi.fn(),
resetOverrides: vi.fn(),
resetScene: vi.fn(),
toggleMotion: vi.fn(),
};
it("changes stage and chat themes independently", async () => {
render(<PresenterControls {...props} />);
await userEvent.selectOptions(screen.getByLabelText(/stage theme/i), "night");
await userEvent.selectOptions(screen.getByLabelText(/chat theme/i), "light");
expect(props.setStageTheme).toHaveBeenCalledWith("night");
expect(props.setChatTheme).toHaveBeenCalledWith("light");
});
it("forces replay without changing the current presentation location", async () => {
render(<PresenterControls {...props} />);
await userEvent.click(screen.getByRole("button", { name: /force replay fallback/i }));
expect(props.forceReplay).toHaveBeenCalledTimes(1);
expect(props.jump).not.toHaveBeenCalled();
});
});
describe("ChatDock", () => {
it("opens docked chat by click and keyboard", async () => {
const openChat = vi.fn();
render(<ChatDock openChat={openChat} />);
await userEvent.click(screen.getByRole("button", { name: /open agent chat/i }));
expect(openChat).toHaveBeenCalledTimes(1);
});
});
@@ -1,134 +0,0 @@
import { useEffect, useRef, useState } from "react";
import type { PresentationState } from "./presentation-state.js";
import { compositionForState } from "./presentation-state.js";
import type { ChatMode, ChatTheme, MainLocation, PresentationLocation, StageTheme } from "./storyboard.js";
import { findScene, mainScenes } from "./storyboard.js";
type PresenterControlsProps = {
readonly state: PresentationState;
readonly next: () => void;
readonly previous: () => void;
readonly jump: (location: PresentationLocation) => void;
readonly setStageTheme: (theme: StageTheme | null) => void;
readonly setChatTheme: (theme: ChatTheme | null) => void;
readonly setChatMode: (mode: ChatMode | null) => void;
readonly forceReplay: () => void;
readonly resetOverrides: () => void;
readonly resetScene: () => void;
readonly toggleMotion: () => void;
};
export const PresenterControls = ({
state,
next,
previous,
jump,
setStageTheme,
setChatTheme,
setChatMode,
forceReplay,
resetOverrides,
resetScene,
toggleMotion,
}: PresenterControlsProps) => {
const composition = compositionForState(state);
const isMain = state.location.kind === "main";
const currentScene = isMain ? findScene(state.location.sceneId) : null;
const currentBeat = currentScene?.beats.find((b) => b.id === (state.location as MainLocation).beatId);
const beatIndex = currentScene && isMain
? currentScene.beats.findIndex((b) => b.id === (state.location as MainLocation).beatId)
: -1;
const totalBeats = currentScene?.beats.length ?? 0;
const sceneNumber = currentScene?.number ?? 0;
const totalScenes = mainScenes.length;
const [elapsed, setElapsed] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setElapsed(Math.floor((Date.now() - state.startedAt) / 1000));
}, 1000);
return () => clearInterval(interval);
}, [state.startedAt]);
const formatTime = (seconds: number) => {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m}:${s.toString().padStart(2, "0")}`;
};
const plannedMinutes = 15;
return (
<div className="presenter-controls" role="dialog" aria-label="presenter controls">
<div className="presenter-controls__nav">
<button type="button" onClick={previous}>Previous</button>
<button type="button" onClick={next}>Next</button>
</div>
<div className="presenter-controls__info">
<span>Scene {sceneNumber}/{totalScenes}: {currentScene?.title ?? "Discussion"}</span>
{currentBeat && <span> · {beatIndex + 1}/{totalBeats} {currentBeat.title}</span>}
</div>
<div className="presenter-controls__timer">
<span>Elapsed: {formatTime(elapsed)}</span>
<span> / {plannedMinutes}:00 planned</span>
</div>
<div className="presenter-controls__notes">
<label>
Speaker notes
<textarea
key={currentBeat?.id ?? "none"}
defaultValue={currentBeat?.caption ?? ""}
rows={3}
placeholder="Add notes for this beat..."
/>
</label>
</div>
<div className="presenter-controls__over">
<label>
Stage theme
<select
value={state.stageThemeOverride ?? "scene default"}
onChange={(e) => setStageTheme(e.target.value === "scene default" ? null : e.target.value as StageTheme)}
>
<option value="scene default">scene default</option>
<option value="paper">paper</option>
<option value="night">night</option>
</select>
</label>
<label>
Chat theme
<select
value={state.chatThemeOverride ?? "scene default"}
onChange={(e) => setChatTheme(e.target.value === "scene default" ? null : e.target.value as ChatTheme)}
>
<option value="scene default">scene default</option>
<option value="light">light</option>
<option value="dark">dark</option>
</select>
</label>
<label>
Chat mode
<select
value={state.chatModeOverride ?? "scene default"}
onChange={(e) => setChatMode(e.target.value === "scene default" ? null : e.target.value as ChatMode)}
>
<option value="scene default">scene default</option>
<option value="hidden">hidden</option>
<option value="full">full</option>
<option value="rail">rail</option>
<option value="dock">dock</option>
</select>
</label>
</div>
<div className="presenter-controls__actions">
<span>{state.playbackMode === "replay" ? "Replay" : "Live"}</span>
<button type="button" onClick={forceReplay}>Force replay fallback</button>
<button type="button" onClick={resetScene}>Reset scene</button>
<button type="button" onClick={toggleMotion}>
Motion: {state.motionDisabled ? "off" : "on"}
</button>
<button type="button" onClick={resetOverrides}>Reset overrides</button>
</div>
</div>
);
};
@@ -0,0 +1,49 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { SceneProgress } from "./SceneProgress.js";
import type { MainLocation } from "./storyboard.js";
afterEach(() => cleanup());
describe("SceneProgress", () => {
it("shows scene and beat position for architecture/runtime", () => {
const location: MainLocation = {
kind: "main",
sceneId: "architecture",
beatId: "runtime",
focusPath: [],
};
render(<SceneProgress location={location} />);
expect(screen.getByText("6 / 12")).toBeInTheDocument();
expect(screen.getByText("3 / 4")).toBeInTheDocument();
});
it("shows scene position for workflow-demo/graph", () => {
const location: MainLocation = {
kind: "main",
sceneId: "workflow-demo",
beatId: "graph",
focusPath: [],
};
render(<SceneProgress location={location} />);
expect(screen.getByText("9 / 12")).toBeInTheDocument();
expect(screen.getByText("2 / 3")).toBeInTheDocument();
});
it("has an accessible label", () => {
const location: MainLocation = {
kind: "main",
sceneId: "architecture",
beatId: "runtime",
focusPath: [],
};
render(<SceneProgress location={location} />);
expect(screen.getByLabelText("scene position")).toBeInTheDocument();
});
});
@@ -0,0 +1,28 @@
import { findScene, mainScenes, type MainLocation } from "./storyboard.js";
type SceneProgressProps = {
readonly location: MainLocation;
};
export const SceneProgress = ({ location }: SceneProgressProps) => {
const scene = findScene(location.sceneId);
const sceneIndex = mainScenes.findIndex((s) => s.id === location.sceneId);
const totalScenes = mainScenes.length;
const beatIndex = scene?.beats.findIndex((b) => b.id === location.beatId) ?? -1;
const totalBeats = scene?.beats.length ?? 0;
return (
<p className="scene-progress" aria-label="scene position">
{sceneIndex >= 0 && (
<span className="scene-progress__scene">
{sceneIndex + 1} / {totalScenes}
</span>
)}
{totalBeats > 1 && (
<span className="scene-progress__beat">
{beatIndex >= 0 ? beatIndex + 1 : 1} / {totalBeats}
</span>
)}
</p>
);
};
@@ -1,40 +0,0 @@
import { mainScenes, type MainLocation, type PresentationLocation } from "./storyboard.js";
type SceneRailProps = {
readonly location: PresentationLocation;
readonly jump: (location: MainLocation) => void;
};
export const SceneRail = ({ location, jump }: SceneRailProps) => {
const activeSceneId = location.kind === "main" ? location.sceneId : "positioning";
return (
<nav className="scene-rail" aria-label="presentation scene rail">
{mainScenes.map((scene) => {
const isActive = scene.id === activeSceneId;
const activeBeatId =
isActive && location.kind === "main" ? location.beatId : scene.beats[0]!.id;
return (
<button
key={scene.id}
type="button"
data-active={isActive}
aria-current={isActive ? "step" : undefined}
onClick={() => jump({ kind: "main", sceneId: scene.id as MainLocation["sceneId"], beatId: scene.beats[0]!.id, focusPath: scene.beats[0]!.figure?.focusPath ?? [] })}
className="scene-rail__scene"
>
<span className="scene-rail__number">{scene.number}</span>
<small className="scene-rail__title">{scene.title}</small>
<span className="scene-rail__progress">
{scene.beats.map((beat) => (
<span
key={beat.id}
className={`scene-rail__beat ${beat.id === activeBeatId ? "scene-rail__beat--active" : ""}`}
/>
))}
</span>
</button>
);
})}
</nav>
);
};
@@ -111,14 +111,12 @@ describe("presentationReducer", () => {
expect(nexted.location).toEqual(opened.location); expect(nexted.location).toEqual(opened.location);
}); });
it("derives act and chat composition from the current beat", () => { it("derives chatMode from the current beat", () => {
const state = presentationReducer(initialPresentationState, { const state = presentationReducer(initialPresentationState, {
type: "jump", type: "jump",
location: { kind: "main", sceneId: "workflow-demo", beatId: "graph", focusPath: [] }, location: { kind: "main", sceneId: "workflow-demo", beatId: "graph", focusPath: [] },
}); });
expect(compositionForState(state)).toMatchObject({ expect(compositionForState(state)).toMatchObject({
stageTheme: "night",
chatTheme: "light",
chatMode: "rail", chatMode: "rail",
}); });
}); });
@@ -3,12 +3,10 @@ import {
findDiscussionBranch, findDiscussionBranch,
findScene, findScene,
type ChatMode, type ChatMode,
type ChatTheme,
type DiscussionBranchId, type DiscussionBranchId,
type EvidenceMode, type EvidenceMode,
type MainLocation, type MainLocation,
type PresentationLocation, type PresentationLocation,
type StageTheme,
} from "./storyboard.js"; } from "./storyboard.js";
import { import {
hashForLocation, hashForLocation,
@@ -23,11 +21,6 @@ export type PresentationState = {
readonly selectedNodeId: string | null; readonly selectedNodeId: string | null;
readonly evidenceModeOverride: EvidenceMode | null; readonly evidenceModeOverride: EvidenceMode | null;
readonly playbackMode: "replay" | "live"; readonly playbackMode: "replay" | "live";
readonly stageThemeOverride: StageTheme | null;
readonly chatThemeOverride: ChatTheme | null;
readonly chatModeOverride: ChatMode | null;
readonly controlsOpen: boolean;
readonly discussionIndexOpen: boolean;
readonly motionDisabled: boolean; readonly motionDisabled: boolean;
readonly startedAt: number; readonly startedAt: number;
}; };
@@ -44,12 +37,7 @@ export type PresentationAction =
| { readonly type: "set_evidence_mode"; readonly mode: EvidenceMode } | { readonly type: "set_evidence_mode"; readonly mode: EvidenceMode }
| { readonly type: "close_overlay" } | { readonly type: "close_overlay" }
| { readonly type: "set_playback_mode"; readonly mode: PresentationState["playbackMode"] } | { readonly type: "set_playback_mode"; readonly mode: PresentationState["playbackMode"] }
| { readonly type: "set_stage_theme"; readonly theme: StageTheme | null }
| { readonly type: "set_chat_theme"; readonly theme: ChatTheme | null }
| { readonly type: "set_chat_mode"; readonly mode: ChatMode | null }
| { readonly type: "set_focus_path"; readonly path: readonly string[] } | { readonly type: "set_focus_path"; readonly path: readonly string[] }
| { readonly type: "toggle_controls" }
| { readonly type: "toggle_discussion_index" }
| { readonly type: "toggle_motion" }; | { readonly type: "toggle_motion" };
export const initialPresentationState: PresentationState = { export const initialPresentationState: PresentationState = {
@@ -58,11 +46,6 @@ export const initialPresentationState: PresentationState = {
selectedNodeId: null, selectedNodeId: null,
evidenceModeOverride: null, evidenceModeOverride: null,
playbackMode: "replay", playbackMode: "replay",
stageThemeOverride: null,
chatThemeOverride: null,
chatModeOverride: null,
controlsOpen: false,
discussionIndexOpen: false,
motionDisabled: false, motionDisabled: false,
startedAt: Date.now(), startedAt: Date.now(),
}; };
@@ -70,31 +53,20 @@ export const initialPresentationState: PresentationState = {
const compositionForLocation = ( const compositionForLocation = (
location: PresentationLocation, location: PresentationLocation,
evidenceOverride: EvidenceMode | null, evidenceOverride: EvidenceMode | null,
stageThemeOverride: StageTheme | null,
chatThemeOverride: ChatTheme | null,
chatModeOverride: ChatMode | null,
): { ): {
readonly stageTheme: StageTheme;
readonly chatTheme: ChatTheme;
readonly chatMode: ChatMode; readonly chatMode: ChatMode;
readonly evidenceMode: EvidenceMode; readonly evidenceMode: EvidenceMode;
} => { } => {
if (location.kind === "discussion") { if (location.kind === "discussion") {
const branch = findDiscussionBranch(location.branchId);
const parentScene = branch ? findScene(branch.parentSceneId) : findScene("positioning");
return { return {
stageTheme: stageThemeOverride ?? parentScene?.stageTheme ?? "paper", chatMode: "hidden",
chatTheme: chatThemeOverride ?? "dark",
chatMode: chatModeOverride ?? "hidden",
evidenceMode: evidenceOverride ?? "hidden", evidenceMode: evidenceOverride ?? "hidden",
}; };
} }
const scene = findScene(location.sceneId); const scene = findScene(location.sceneId);
const beat = scene?.beats.find((b) => b.id === location.beatId); const beat = scene?.beats.find((b) => b.id === location.beatId);
return { return {
stageTheme: stageThemeOverride ?? scene?.stageTheme ?? "paper", chatMode: beat?.chatMode ?? "hidden",
chatTheme: chatThemeOverride ?? beat?.chatTheme ?? "dark",
chatMode: chatModeOverride ?? beat?.chatMode ?? "hidden",
evidenceMode: evidenceOverride ?? beat?.evidenceMode ?? "hidden", evidenceMode: evidenceOverride ?? beat?.evidenceMode ?? "hidden",
}; };
}; };
@@ -103,9 +75,6 @@ export const compositionForState = (state: PresentationState) =>
compositionForLocation( compositionForLocation(
state.location, state.location,
state.evidenceModeOverride, state.evidenceModeOverride,
state.stageThemeOverride,
state.chatThemeOverride,
state.chatModeOverride,
); );
const isValidMainLocation = (location: PresentationLocation): location is MainLocation => const isValidMainLocation = (location: PresentationLocation): location is MainLocation =>
@@ -190,8 +159,6 @@ export const presentationReducer = (
return beat?.evidenceMode === "open" || beat?.evidenceMode === "peek"; return beat?.evidenceMode === "open" || beat?.evidenceMode === "peek";
})(); })();
if (isEvidenceVisible) return { ...state, evidenceModeOverride: "hidden" }; if (isEvidenceVisible) return { ...state, evidenceModeOverride: "hidden" };
if (state.discussionIndexOpen) return { ...state, discussionIndexOpen: false };
if (state.controlsOpen) return { ...state, controlsOpen: false };
if (state.location.kind === "discussion") { if (state.location.kind === "discussion") {
return { return {
...state, ...state,
@@ -203,22 +170,12 @@ export const presentationReducer = (
} }
case "set_playback_mode": case "set_playback_mode":
return { ...state, playbackMode: action.mode }; return { ...state, playbackMode: action.mode };
case "set_stage_theme":
return { ...state, stageThemeOverride: action.theme };
case "set_chat_theme":
return { ...state, chatThemeOverride: action.theme };
case "set_chat_mode":
return { ...state, chatModeOverride: action.mode };
case "set_focus_path": case "set_focus_path":
if (state.location.kind !== "main") return state; if (state.location.kind !== "main") return state;
return { return {
...state, ...state,
location: { ...state.location, focusPath: action.path }, location: { ...state.location, focusPath: action.path },
}; };
case "toggle_controls":
return { ...state, controlsOpen: !state.controlsOpen };
case "toggle_discussion_index":
return { ...state, discussionIndexOpen: !state.discussionIndexOpen };
case "toggle_motion": case "toggle_motion":
return { ...state, motionDisabled: !state.motionDisabled }; return { ...state, motionDisabled: !state.motionDisabled };
} }
@@ -18,13 +18,6 @@
--failure: oklch(0.65 0.2 25); --failure: oklch(0.65 0.2 25);
--chat-width: 18rem; --chat-width: 18rem;
--evidence-width: 0rem; --evidence-width: 0rem;
/* Paper theme tokens */
--paper-bg: oklch(0.97 0.005 80);
--paper-text: oklch(0.15 0.01 80);
--paper-muted: oklch(0.5 0.03 80);
--paper-accent: oklch(0.45 0.14 80);
--paper-border: oklch(0.75 0.04 80);
--paper-surface: oklch(0.92 0.01 80);
} }
.presentation-stage { .presentation-stage {
@@ -32,193 +25,14 @@
min-height: 0; min-height: 0;
display: grid; display: grid;
grid-template: grid-template:
"chat primary evidence" minmax(0, 1fr) "chat primary evidence" minmax(0, 1fr) /
"rail rail rail" auto /
var(--chat-width) minmax(0, 1fr) var(--evidence-width); var(--chat-width) minmax(0, 1fr) var(--evidence-width);
} }
/* Stage themes */
.presentation-stage[data-stage-theme="paper"] {
background: var(--paper-bg);
color: var(--paper-text);
}
.presentation-stage[data-stage-theme="night"] {
background: var(--stage-canvas);
color: var(--text-primary);
}
/* Paper theme child-element overrides */
.presentation-stage[data-stage-theme="paper"] .stage-caption__eyebrow {
color: var(--paper-muted);
}
.presentation-stage[data-stage-theme="paper"] .stage-caption h1 {
color: var(--paper-text);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__evidence {
color: var(--paper-muted);
}
.presentation-stage[data-stage-theme="paper"] .scene-rail__scene {
border-color: oklch(0.75 0.04 80);
background: oklch(0.92 0.01 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-rail__scene[data-active="true"] {
background: oklch(0.35 0.12 80);
color: oklch(0.97 0.005 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-rail__beat {
background: oklch(0.7 0.04 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-rail__beat--active {
background: oklch(0.3 0.08 80);
}
.presentation-stage[data-stage-theme="paper"] .presentation-stage__mode {
color: oklch(0.5 0.03 80);
}
.presentation-stage[data-stage-theme="paper"] .presentation-stage__discussion-toggle {
border-color: oklch(0.7 0.05 80);
background: oklch(0.9 0.01 80);
color: oklch(0.2 0.02 80);
}
/* Paper theme scene-body element overrides */
.presentation-stage[data-stage-theme="paper"] .scene-body__positioning-card {
border-color: oklch(0.75 0.04 80);
background: oklch(0.92 0.01 80);
color: oklch(0.15 0.01 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__positioning-card span {
color: oklch(0.4 0.03 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__positioning-card--active {
border-color: oklch(0.45 0.14 80);
background: oklch(0.85 0.04 80);
color: oklch(0.12 0.02 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__boundary-side {
border-color: oklch(0.75 0.04 80);
background: oklch(0.92 0.01 80);
color: oklch(0.15 0.01 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__boundary-side--dim {
opacity: 0.4;
}
.presentation-stage[data-stage-theme="paper"] .scene-body__boundary-divider {
background: oklch(0.5 0.08 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__lifecycle-stage,
.presentation-stage[data-stage-theme="paper"] .scene-body__authoring-step {
border-color: oklch(0.75 0.04 80);
background: oklch(0.92 0.01 80);
color: oklch(0.15 0.01 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__lifecycle-number,
.presentation-stage[data-stage-theme="paper"] .scene-body__authoring-number {
background: oklch(0.45 0.14 80);
color: oklch(0.97 0.005 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__lifecycle-arrow,
.presentation-stage[data-stage-theme="paper"] .scene-body__authoring-arrow {
color: oklch(0.5 0.06 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__lifecycle-stage--active,
.presentation-stage[data-stage-theme="paper"] .scene-body__authoring-step--active {
border-color: oklch(0.45 0.14 80);
background: oklch(0.85 0.04 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__architecture-layer {
border-color: oklch(0.75 0.04 80);
background: oklch(0.92 0.01 80);
color: oklch(0.15 0.01 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__architecture-layer--node {
border-color: oklch(0.45 0.14 80);
background: oklch(0.85 0.04 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__architecture-layer--active {
border-color: oklch(0.45 0.14 80);
background: oklch(0.85 0.04 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__architecture-arrow {
color: oklch(0.5 0.06 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__evaluation-stat {
border-color: oklch(0.75 0.04 80);
background: oklch(0.92 0.01 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__evaluation-stat strong {
color: oklch(0.45 0.14 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__evaluation-stat span {
color: oklch(0.4 0.03 80);
}
.presentation-stage[data-stage-theme="paper"] .scene-body__evaluation-stat--active {
border-color: oklch(0.45 0.14 80);
background: oklch(0.85 0.04 80);
}
/* Chat themes */
.presentation-stage[data-chat-theme="light"] .presentation-stage__chat,
.operator-chat[data-chat-theme="light"] {
background: oklch(0.96 0.005 80);
color: oklch(0.18 0.01 80);
}
.presentation-stage[data-chat-theme="dark"] .presentation-stage__chat,
.operator-chat[data-chat-theme="dark"] {
background: oklch(0.14 0.025 250);
color: oklch(0.92 0.01 250);
}
/* Light chat message overrides */
.presentation-stage[data-chat-theme="light"] .chat-message,
.operator-chat[data-chat-theme="light"] .chat-message {
border-color: oklch(0.75 0.04 80);
background: oklch(0.92 0.01 80);
color: oklch(0.18 0.01 80);
}
/* Light chat tool-part overrides */
.presentation-stage[data-chat-theme="light"] .chat-tool-part,
.operator-chat[data-chat-theme="light"] .chat-tool-part {
border-color: oklch(0.75 0.04 80);
}
.presentation-stage[data-chat-theme="light"] .chat-tool-part span,
.operator-chat[data-chat-theme="light"] .chat-tool-part span {
color: oklch(0.45 0.03 80);
}
/* Chat modes */ /* Chat modes */
.presentation-stage[data-chat-mode="hidden"] { .presentation-stage[data-chat-mode="hidden"] {
grid-template: grid-template:
"primary evidence" minmax(0, 1fr) "primary evidence" minmax(0, 1fr) /
"rail rail" auto /
minmax(0, 1fr) var(--evidence-width); minmax(0, 1fr) var(--evidence-width);
} }
@@ -230,17 +44,6 @@
--chat-width: 16rem; --chat-width: 16rem;
} }
.presentation-stage[data-chat-mode="dock"] {
grid-template:
"primary evidence" minmax(0, 1fr)
"rail rail" auto /
minmax(0, 1fr) var(--evidence-width);
}
.presentation-stage[data-chat-mode="dock"] .presentation-stage__chat {
display: none;
}
/* Evidence modes */ /* Evidence modes */
.presentation-stage[data-evidence-mode="peek"] { .presentation-stage[data-evidence-mode="peek"] {
--evidence-width: 20rem; --evidence-width: 20rem;
@@ -313,59 +116,6 @@
font-size: 0.85rem; font-size: 0.85rem;
} }
.scene-rail {
grid-area: rail;
display: flex;
gap: 0.25rem;
padding: 0.35rem 0.75rem;
overflow-x: auto;
}
.scene-rail__scene {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.15rem;
min-width: 4.5rem;
text-align: center;
border: 1px solid oklch(0.36 0.04 250);
background: oklch(0.16 0.025 250);
color: inherit;
padding: 0.35rem 0.5rem;
border-radius: 0.4rem;
font-size: 0.7rem;
}
.scene-rail__scene[data-active="true"] {
background: oklch(0.7 0.16 195);
color: oklch(0.12 0.025 250);
}
.scene-rail__number {
font-weight: 600;
}
.scene-rail__title {
font-size: 0.6rem;
opacity: 0.8;
}
.scene-rail__progress {
display: flex;
gap: 2px;
}
.scene-rail__beat {
width: 4px;
height: 4px;
border-radius: 50%;
background: oklch(0.5 0.04 250);
}
.scene-rail__beat--active {
background: oklch(0.9 0.06 250);
}
.operation-block { .operation-block {
border: 1px solid var(--stage-line); border: 1px solid var(--stage-line);
background: var(--stage-surface); background: var(--stage-surface);
@@ -601,25 +351,23 @@
font-size: 0.8rem; font-size: 0.8rem;
} }
.presentation-stage__mode { .scene-progress {
flex-shrink: 0; display: flex;
gap: 0.5rem;
font-size: 0.75rem; font-size: 0.75rem;
color: oklch(0.65 0.03 250); color: oklch(0.65 0.03 250);
margin: 0; margin: 0;
padding: 0 1rem;
} }
.presentation-stage__discussion-toggle { .scene-progress__scene {
flex-shrink: 0; font-weight: 600;
align-self: flex-start; }
font-size: 0.8rem;
padding: 0.35rem 0.65rem; .scene-progress__beat {
border: 1px solid oklch(0.4 0.05 250); opacity: 0.8;
background: oklch(0.2 0.03 250);
color: inherit;
border-radius: 0.4rem;
} }
.discussion-index,
.discussion-panel { .discussion-panel {
padding: 1rem; padding: 1rem;
border: 1px solid oklch(0.36 0.04 250); border: 1px solid oklch(0.36 0.04 250);
@@ -627,36 +375,11 @@
border-radius: 0.65rem; border-radius: 0.65rem;
} }
.discussion-index h2,
.discussion-panel h2 { .discussion-panel h2 {
font-size: 1.1rem; font-size: 1.1rem;
margin: 0 0 0.5rem; margin: 0 0 0.5rem;
} }
.discussion-index h3 {
font-size: 0.9rem;
margin: 0.5rem 0 0.25rem;
color: oklch(0.78 0.035 250);
}
.discussion-index ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.discussion-index button {
padding: 0.3rem 0.6rem;
border: 1px solid oklch(0.4 0.05 250);
background: oklch(0.2 0.03 250);
color: inherit;
border-radius: 0.4rem;
font-size: 0.8rem;
}
.discussion-panel__badge { .discussion-panel__badge {
display: inline-block; display: inline-block;
padding: 0.15rem 0.4rem; padding: 0.15rem 0.4rem;
@@ -694,118 +417,6 @@
font-weight: 600; font-weight: 600;
} }
.presenter-controls {
position: fixed;
top: 0.75rem;
left: 0.75rem;
width: min(20rem, calc(100vw - 1.5rem));
padding: 0.75rem;
border: 1px solid oklch(0.4 0.05 250);
background: oklch(0.17 0.025 250);
z-index: 40;
display: flex;
flex-direction: column;
gap: 0.5rem;
font-size: 0.8rem;
}
.presenter-controls__nav {
display: flex;
gap: 0.35rem;
}
.presenter-controls__nav button {
flex: 1;
padding: 0.3rem 0.5rem;
border: 1px solid oklch(0.4 0.05 250);
background: oklch(0.2 0.03 250);
color: inherit;
border-radius: 0.3rem;
}
.presenter-controls__info {
color: oklch(0.78 0.035 250);
}
.presenter-controls__timer {
display: flex;
gap: 0.25rem;
font-family: var(--font-mono, monospace);
font-size: 0.8rem;
color: oklch(0.78 0.035 250);
}
.presenter-controls__notes label {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.presenter-controls__notes textarea {
width: 100%;
padding: 0.35rem;
border: 1px solid oklch(0.36 0.04 250);
background: oklch(0.18 0.025 250);
color: inherit;
border-radius: 0.3rem;
font-size: 0.75rem;
font-family: inherit;
resize: vertical;
}
.presenter-controls__over label {
display: flex;
flex-direction: column;
gap: 0.15rem;
margin-top: 0.25rem;
}
.presenter-controls__over select {
padding: 0.25rem;
border: 1px solid oklch(0.36 0.04 250);
background: oklch(0.18 0.025 250);
color: inherit;
border-radius: 0.3rem;
}
.presenter-controls__actions {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-top: 0.25rem;
}
.presenter-controls__actions button {
padding: 0.25rem 0.5rem;
border: 1px solid oklch(0.4 0.05 250);
background: oklch(0.2 0.03 250);
color: inherit;
border-radius: 0.3rem;
font-size: 0.75rem;
}
.chat-dock {
position: fixed;
bottom: 2.5rem;
left: 0.75rem;
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.45rem 0.75rem;
border: 1px solid oklch(0.4 0.05 250);
background: oklch(0.2 0.03 250);
color: inherit;
border-radius: 0.5rem;
z-index: 25;
font-size: 0.8rem;
cursor: pointer;
}
.chat-dock:hover,
.chat-dock:focus-visible {
background: oklch(0.25 0.04 250);
}
.presentation-route__agent-button { .presentation-route__agent-button {
position: fixed; position: fixed;
bottom: 0.75rem; bottom: 0.75rem;
@@ -1083,15 +694,6 @@
} }
@media (max-height: 760px) { @media (max-height: 760px) {
.scene-rail {
padding: 0.25rem 0.5rem;
}
.scene-rail__scene {
min-width: 3.5rem;
padding: 0.25rem 0.35rem;
}
.stage-caption h1 { .stage-caption h1 {
font-size: clamp(1.4rem, 3vw, 2.2rem); font-size: clamp(1.4rem, 3vw, 2.2rem);
} }
@@ -574,131 +574,6 @@
white-space: pre-wrap; white-space: pre-wrap;
} }
/* Chat remains an app surface while the workflow stage stays cinematic. */
.presentation-stage[data-chat-theme="light"] .presentation-stage__chat {
border-right: 1px solid oklch(0.82 0.015 80);
background: oklch(0.975 0.004 80);
}
.presentation-stage[data-chat-theme="light"] .operator-chat {
gap: 1.05rem;
padding: 1rem 0.85rem;
background: transparent;
}
.presentation-stage[data-chat-theme="light"] .chat-message {
border: 0;
border-radius: 0;
padding: 0;
background: transparent;
box-shadow: none;
color: oklch(0.22 0.015 80);
font-size: 0.78rem;
line-height: 1.45;
}
.presentation-stage[data-chat-theme="light"] .chat-message > strong {
display: block;
margin-bottom: 0.35rem;
color: oklch(0.42 0.025 80);
font: 650 0.6rem/1 var(--font-mono, monospace);
letter-spacing: 0.09em;
text-transform: uppercase;
}
.presentation-stage[data-chat-theme="light"] .chat-message--operator {
margin-left: 1rem;
border-radius: 0.85rem;
padding: 0.65rem 0.75rem;
background: oklch(0.925 0.012 80);
}
.presentation-stage[data-chat-theme="light"] .chat-message p {
margin: 0.2rem 0;
}
.presentation-stage[data-chat-theme="light"] .chat-tool-part {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.3rem;
margin-top: 0.5rem;
border: 1px solid oklch(0.82 0.015 80);
border-radius: 0.55rem;
padding: 0.55rem 0.65rem;
background: oklch(0.95 0.006 80);
}
.presentation-stage[data-chat-theme="light"] .chat-tool-part span {
grid-column: 1 / -1;
color: oklch(0.48 0.04 195);
font: 650 0.56rem/1 var(--font-mono, monospace);
letter-spacing: 0.09em;
}
.presentation-stage[data-chat-theme="light"] .chat-tool-part code {
overflow: hidden;
color: oklch(0.26 0.025 250);
font-size: 0.68rem;
text-overflow: ellipsis;
}
.presentation-stage[data-chat-theme="light"] .chat-tool-part--handoff {
border-color: oklch(0.56 0.13 195 / 0.55);
background: oklch(0.94 0.025 195);
}
.presentation-route .presentation-stage__discussion-toggle {
position: absolute;
right: 1.25rem;
bottom: 0.7rem;
z-index: 8;
border: 0;
padding: 0.25rem 0;
background: transparent;
color: var(--text-muted);
font: 600 0.62rem/1 var(--font-mono, monospace);
cursor: pointer;
}
.presentation-route .presentation-stage__mode {
position: absolute;
bottom: 0.7rem;
left: 1.25rem;
z-index: 8;
margin: 0;
color: var(--text-muted);
font: 600 0.6rem/1 var(--font-mono, monospace);
letter-spacing: 0.06em;
text-transform: uppercase;
}
.presentation-route .scene-rail {
gap: 0.25rem;
min-height: 2.7rem;
border-top: 1px solid oklch(0.36 0.04 250 / 0.7);
padding: 0.35rem 0.45rem;
background: oklch(0.095 0.02 250);
}
.presentation-route .scene-rail__scene {
min-width: 2.5rem;
min-height: 1.9rem;
border: 0;
border-radius: 0.35rem;
padding: 0.25rem 0.35rem;
background: transparent;
color: oklch(0.62 0.025 250);
}
.presentation-route .scene-rail__scene[data-active="true"] {
background: oklch(0.72 0.17 195 / 0.13);
color: var(--text-primary);
}
.presentation-route .scene-rail__title {
display: none;
}
@media (max-width: 1050px) { @media (max-width: 1050px) {
.demo-workflow-stage__graph:has(.interrupt-contract-preview) { .demo-workflow-stage__graph:has(.interrupt-contract-preview) {
grid-template-columns: minmax(0, 1fr) 15rem; grid-template-columns: minmax(0, 1fr) 15rem;