fix: address review findings for defense compositor
- S1: Remove global html/body overflow:hidden; scope to .presentation-route - S2+P1: Add CSS selectors for data-stage-theme, data-chat-theme, data-chat-mode - S3: Add distinct visual treatments per SceneView type (positioning, boundary, lifecycle, architecture, authoring, evaluation) - S4+P2: Move discussion index to reducer state; Escape closes all overlays; remove miswired button - P3: Add 8 missing Q&A discussion branches across lifecycle, authoring, architecture, evaluation, workflow-demo - P4: Force replay now restores replay evidence - P5: Add scene progress display and scene reset to presenter controls - P6: Fix default dock mode to use composition.chatMode - P7: Wrap decodeURIComponent in try-catch for malformed hashes
This commit is contained in:
@@ -10,8 +10,10 @@ 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 type { PresentationLocation } from "./storyboard.js";
|
import type { PresentationLocation } from "./storyboard.js";
|
||||||
import "./presentation.css";
|
import "./presentation.css";
|
||||||
|
|
||||||
@@ -154,7 +156,8 @@ export const PresentationRoute = () => {
|
|||||||
|
|
||||||
const handleForceReplay = useCallback(() => {
|
const handleForceReplay = useCallback(() => {
|
||||||
demo.setMode("replay");
|
demo.setMode("replay");
|
||||||
}, [demo]);
|
setEvidence(replayEvidence);
|
||||||
|
}, [demo, replayEvidence]);
|
||||||
|
|
||||||
const handleResetOverrides = useCallback(() => {
|
const handleResetOverrides = useCallback(() => {
|
||||||
dispatch({ type: "set_stage_theme", theme: null });
|
dispatch({ type: "set_stage_theme", theme: null });
|
||||||
@@ -162,7 +165,15 @@ export const PresentationRoute = () => {
|
|||||||
dispatch({ type: "set_chat_mode", mode: null });
|
dispatch({ type: "set_chat_mode", mode: null });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const showChatDock = state.chatModeOverride === "dock" && state.location.kind !== "discussion";
|
const handleResetScene = useCallback(() => {
|
||||||
|
if (state.location.kind !== "main") return;
|
||||||
|
const scene = findScene(state.location.sceneId);
|
||||||
|
if (!scene || scene.beats.length === 0) return;
|
||||||
|
dispatch({ type: "jump", location: { kind: "main", sceneId: state.location.sceneId, beatId: scene.beats[0]!.id } });
|
||||||
|
}, [state.location]);
|
||||||
|
|
||||||
|
const composition = compositionForState(state);
|
||||||
|
const showChatDock = composition.chatMode === "dock" && state.location.kind !== "discussion";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="presentation-route" aria-label="lda.chat presentation">
|
<main className="presentation-route" aria-label="lda.chat presentation">
|
||||||
@@ -179,6 +190,7 @@ 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" })}
|
||||||
/>
|
/>
|
||||||
{showChatDock && <ChatDock openChat={() => dispatch({ type: "set_chat_mode", mode: "rail" })} />}
|
{showChatDock && <ChatDock openChat={() => dispatch({ type: "set_chat_mode", mode: "rail" })} />}
|
||||||
{state.controlsOpen && (
|
{state.controlsOpen && (
|
||||||
@@ -191,8 +203,8 @@ export const PresentationRoute = () => {
|
|||||||
setChatTheme={(theme) => dispatch({ type: "set_chat_theme", theme })}
|
setChatTheme={(theme) => dispatch({ type: "set_chat_theme", theme })}
|
||||||
setChatMode={(mode) => dispatch({ type: "set_chat_mode", mode })}
|
setChatMode={(mode) => dispatch({ type: "set_chat_mode", mode })}
|
||||||
forceReplay={handleForceReplay}
|
forceReplay={handleForceReplay}
|
||||||
openDiscussionIndex={() => dispatch({ type: "toggle_controls" })}
|
|
||||||
resetOverrides={handleResetOverrides}
|
resetOverrides={handleResetOverrides}
|
||||||
|
resetScene={handleResetScene}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useState } from "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";
|
||||||
@@ -25,6 +24,7 @@ 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 = ({
|
||||||
@@ -40,9 +40,9 @@ export const PresentationStage = ({
|
|||||||
closeOverlay,
|
closeOverlay,
|
||||||
openDiscussion,
|
openDiscussion,
|
||||||
closeDiscussion,
|
closeDiscussion,
|
||||||
|
toggleDiscussionIndex,
|
||||||
}: PresentationStageProps) => {
|
}: PresentationStageProps) => {
|
||||||
const composition = compositionForState(state);
|
const composition = compositionForState(state);
|
||||||
const [indexOpen, setIndexOpen] = useState(false);
|
|
||||||
|
|
||||||
const isMainScene = state.location.kind === "main";
|
const isMainScene = state.location.kind === "main";
|
||||||
const currentScene = isMainScene ? findScene(state.location.sceneId) : null;
|
const currentScene = isMainScene ? findScene(state.location.sceneId) : null;
|
||||||
@@ -75,17 +75,17 @@ export const PresentationStage = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="presentation-stage__discussion-toggle"
|
className="presentation-stage__discussion-toggle"
|
||||||
onClick={() => setIndexOpen(true)}
|
onClick={toggleDiscussionIndex}
|
||||||
>
|
>
|
||||||
Open discussion topics
|
Open discussion topics
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{indexOpen && (
|
{state.discussionIndexOpen && (
|
||||||
<DiscussionIndex
|
<DiscussionIndex
|
||||||
onSelect={(branchId) => {
|
onSelect={(branchId) => {
|
||||||
setIndexOpen(false);
|
toggleDiscussionIndex();
|
||||||
openDiscussion(branchId);
|
openDiscussion(branchId);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ describe("PresenterControls", () => {
|
|||||||
setChatTheme: vi.fn(),
|
setChatTheme: vi.fn(),
|
||||||
setChatMode: vi.fn(),
|
setChatMode: vi.fn(),
|
||||||
forceReplay: vi.fn(),
|
forceReplay: vi.fn(),
|
||||||
openDiscussionIndex: vi.fn(),
|
|
||||||
resetOverrides: vi.fn(),
|
resetOverrides: vi.fn(),
|
||||||
|
resetScene: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
it("changes stage and chat themes independently", async () => {
|
it("changes stage and chat themes independently", async () => {
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ type PresenterControlsProps = {
|
|||||||
readonly setChatTheme: (theme: ChatTheme | null) => void;
|
readonly setChatTheme: (theme: ChatTheme | null) => void;
|
||||||
readonly setChatMode: (mode: ChatMode | null) => void;
|
readonly setChatMode: (mode: ChatMode | null) => void;
|
||||||
readonly forceReplay: () => void;
|
readonly forceReplay: () => void;
|
||||||
readonly openDiscussionIndex: () => void;
|
|
||||||
readonly resetOverrides: () => void;
|
readonly resetOverrides: () => void;
|
||||||
|
readonly resetScene: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PresenterControls = ({
|
export const PresenterControls = ({
|
||||||
@@ -26,13 +26,19 @@ export const PresenterControls = ({
|
|||||||
setChatTheme,
|
setChatTheme,
|
||||||
setChatMode,
|
setChatMode,
|
||||||
forceReplay,
|
forceReplay,
|
||||||
openDiscussionIndex,
|
|
||||||
resetOverrides,
|
resetOverrides,
|
||||||
|
resetScene,
|
||||||
}: PresenterControlsProps) => {
|
}: PresenterControlsProps) => {
|
||||||
const composition = compositionForState(state);
|
const composition = compositionForState(state);
|
||||||
const isMain = state.location.kind === "main";
|
const isMain = state.location.kind === "main";
|
||||||
const currentScene = isMain ? findScene(state.location.sceneId) : null;
|
const currentScene = isMain ? findScene(state.location.sceneId) : null;
|
||||||
const currentBeat = currentScene?.beats.find((b) => b.id === (state.location as MainLocation).beatId);
|
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;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="presenter-controls" role="dialog" aria-label="presenter controls">
|
<div className="presenter-controls" role="dialog" aria-label="presenter controls">
|
||||||
@@ -41,8 +47,8 @@ export const PresenterControls = ({
|
|||||||
<button type="button" onClick={next}>Next</button>
|
<button type="button" onClick={next}>Next</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="presenter-controls__info">
|
<div className="presenter-controls__info">
|
||||||
<span>{currentScene?.title ?? "Discussion"}</span>
|
<span>Scene {sceneNumber}/{totalScenes}: {currentScene?.title ?? "Discussion"}</span>
|
||||||
{currentBeat && <span> · {currentBeat.title}</span>}
|
{currentBeat && <span> · {beatIndex + 1}/{totalBeats} {currentBeat.title}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="presenter-controls__over">
|
<div className="presenter-controls__over">
|
||||||
<label>
|
<label>
|
||||||
@@ -84,7 +90,7 @@ export const PresenterControls = ({
|
|||||||
<div className="presenter-controls__actions">
|
<div className="presenter-controls__actions">
|
||||||
<span>{state.playbackMode === "replay" ? "Replay" : "Live"}</span>
|
<span>{state.playbackMode === "replay" ? "Replay" : "Live"}</span>
|
||||||
<button type="button" onClick={forceReplay}>Force replay fallback</button>
|
<button type="button" onClick={forceReplay}>Force replay fallback</button>
|
||||||
<button type="button" onClick={openDiscussionIndex}>Open discussion index</button>
|
<button type="button" onClick={resetScene}>Reset scene</button>
|
||||||
<button type="button" onClick={resetOverrides}>Reset overrides</button>
|
<button type="button" onClick={resetOverrides}>Reset overrides</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,6 +29,134 @@ const NarrativeScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBe
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const PositioningScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
|
||||||
|
<>
|
||||||
|
<StageCaption eyebrow={`Act I · ${scene.claimClass}`} title={scene.title}>
|
||||||
|
<p>{beat.caption}</p>
|
||||||
|
</StageCaption>
|
||||||
|
<div className="scene-body__positioning-grid">
|
||||||
|
<div className="scene-body__positioning-card">
|
||||||
|
<strong>Tool loops</strong>
|
||||||
|
<span>Direct action, no lifecycle</span>
|
||||||
|
</div>
|
||||||
|
<div className="scene-body__positioning-card">
|
||||||
|
<strong>Scripts</strong>
|
||||||
|
<span>Simple, debuggable</span>
|
||||||
|
</div>
|
||||||
|
<div className="scene-body__positioning-card scene-body__positioning-card--active">
|
||||||
|
<strong>lda.chat</strong>
|
||||||
|
<span>Typed lifecycle contracts</span>
|
||||||
|
</div>
|
||||||
|
<div className="scene-body__positioning-card">
|
||||||
|
<strong>Agent graphs</strong>
|
||||||
|
<span>Shared durability</span>
|
||||||
|
</div>
|
||||||
|
<div className="scene-body__positioning-card">
|
||||||
|
<strong>MCP</strong>
|
||||||
|
<span>Capability protocol</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="scene-body__evidence">{scene.evidencePointer}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const BoundaryScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
|
||||||
|
<>
|
||||||
|
<StageCaption eyebrow="Act II · implemented" title={scene.title}>
|
||||||
|
<p>{beat.caption}</p>
|
||||||
|
</StageCaption>
|
||||||
|
<div className="scene-body__boundary">
|
||||||
|
<div className="scene-body__boundary-side">
|
||||||
|
<h3>Planner</h3>
|
||||||
|
<p>External LLM proposes and revises workflow structure.</p>
|
||||||
|
</div>
|
||||||
|
<div className="scene-body__boundary-divider" />
|
||||||
|
<div className="scene-body__boundary-side">
|
||||||
|
<h3>Runtime</h3>
|
||||||
|
<p>Validates, executes, records, and resumes deterministically.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="scene-body__evidence">{scene.evidencePointer}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LifecycleScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
|
||||||
|
<>
|
||||||
|
<StageCaption eyebrow="Act II · implemented" title={scene.title}>
|
||||||
|
<p>{beat.caption}</p>
|
||||||
|
</StageCaption>
|
||||||
|
<div className="scene-body__lifecycle">
|
||||||
|
{["Draft", "Artifact", "Deployment", "Run"].map((stage, i) => (
|
||||||
|
<div key={stage} className="scene-body__lifecycle-stage">
|
||||||
|
<span className="scene-body__lifecycle-number">{i + 1}</span>
|
||||||
|
<strong>{stage}</strong>
|
||||||
|
{i < 3 && <span className="scene-body__lifecycle-arrow">→</span>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="scene-body__evidence">{scene.evidencePointer}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ArchitectureScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
|
||||||
|
<>
|
||||||
|
<StageCaption eyebrow="Act II · implemented" title={scene.title}>
|
||||||
|
<p>{beat.caption}</p>
|
||||||
|
</StageCaption>
|
||||||
|
<div className="scene-body__architecture">
|
||||||
|
<div className="scene-body__architecture-layer">Client operations</div>
|
||||||
|
<div className="scene-body__architecture-arrow">↓</div>
|
||||||
|
<div className="scene-body__architecture-layer">Transport / JSON-RPC</div>
|
||||||
|
<div className="scene-body__architecture-arrow">↓</div>
|
||||||
|
<div className="scene-body__architecture-layer">Runtime & providers</div>
|
||||||
|
<div className="scene-body__architecture-arrow">↓</div>
|
||||||
|
<div className="scene-body__architecture-layer scene-body__architecture-layer--node">NodeUse</div>
|
||||||
|
</div>
|
||||||
|
<p className="scene-body__evidence">{scene.evidencePointer}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const AuthoringScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
|
||||||
|
<>
|
||||||
|
<StageCaption eyebrow="Act II · implemented" title={scene.title}>
|
||||||
|
<p>{beat.caption}</p>
|
||||||
|
</StageCaption>
|
||||||
|
<div className="scene-body__authoring">
|
||||||
|
{["Discover", "Author", "Diagnose", "Repair"].map((step, i) => (
|
||||||
|
<div key={step} className="scene-body__authoring-step">
|
||||||
|
<span className="scene-body__authoring-number">{i + 1}</span>
|
||||||
|
<strong>{step}</strong>
|
||||||
|
{i < 3 && <span className="scene-body__authoring-arrow">→</span>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="scene-body__evidence">{scene.evidencePointer}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const EvaluationScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
|
||||||
|
<>
|
||||||
|
<StageCaption eyebrow={`Act I · ${scene.claimClass}`} title={scene.title}>
|
||||||
|
<p>{beat.caption}</p>
|
||||||
|
</StageCaption>
|
||||||
|
<div className="scene-body__evaluation">
|
||||||
|
<div className="scene-body__evaluation-stat">
|
||||||
|
<strong>36</strong>
|
||||||
|
<span>trials</span>
|
||||||
|
</div>
|
||||||
|
<div className="scene-body__evaluation-stat">
|
||||||
|
<strong>2</strong>
|
||||||
|
<span>challenges</span>
|
||||||
|
</div>
|
||||||
|
<div className="scene-body__evaluation-stat">
|
||||||
|
<strong>3</strong>
|
||||||
|
<span>waves</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="scene-body__evidence">{scene.evidencePointer}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const AgentHandoffScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
|
const AgentHandoffScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
|
||||||
<>
|
<>
|
||||||
<StageCaption eyebrow="Agent handoff" title={scene.title}>
|
<StageCaption eyebrow="Agent handoff" title={scene.title}>
|
||||||
@@ -80,14 +208,17 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode }: SceneB
|
|||||||
|
|
||||||
switch (scene.view) {
|
switch (scene.view) {
|
||||||
case "narrative":
|
case "narrative":
|
||||||
case "positioning":
|
|
||||||
case "boundary":
|
|
||||||
case "lifecycle":
|
|
||||||
case "architecture":
|
|
||||||
case "authoring":
|
|
||||||
case "evaluation":
|
|
||||||
case "conclusion":
|
|
||||||
return <NarrativeScene scene={scene} beat={beat} />;
|
return <NarrativeScene scene={scene} beat={beat} />;
|
||||||
|
case "positioning":
|
||||||
|
return <PositioningScene scene={scene} beat={beat} />;
|
||||||
|
case "boundary":
|
||||||
|
return <BoundaryScene scene={scene} beat={beat} />;
|
||||||
|
case "lifecycle":
|
||||||
|
return <LifecycleScene scene={scene} beat={beat} />;
|
||||||
|
case "architecture":
|
||||||
|
return <ArchitectureScene scene={scene} beat={beat} />;
|
||||||
|
case "authoring":
|
||||||
|
return <AuthoringScene scene={scene} beat={beat} />;
|
||||||
case "agent":
|
case "agent":
|
||||||
return <AgentHandoffScene scene={scene} beat={beat} />;
|
return <AgentHandoffScene scene={scene} beat={beat} />;
|
||||||
case "demo":
|
case "demo":
|
||||||
@@ -100,6 +231,10 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode }: SceneB
|
|||||||
selectNode={selectNode}
|
selectNode={selectNode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case "evaluation":
|
||||||
|
return <EvaluationScene scene={scene} beat={beat} />;
|
||||||
|
case "conclusion":
|
||||||
|
return <NarrativeScene scene={scene} beat={beat} />;
|
||||||
default:
|
default:
|
||||||
return assertNever(scene.view);
|
return assertNever(scene.view);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export type PresentationState = {
|
|||||||
readonly chatThemeOverride: ChatTheme | null;
|
readonly chatThemeOverride: ChatTheme | null;
|
||||||
readonly chatModeOverride: ChatMode | null;
|
readonly chatModeOverride: ChatMode | null;
|
||||||
readonly controlsOpen: boolean;
|
readonly controlsOpen: boolean;
|
||||||
|
readonly discussionIndexOpen: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PresentationAction =
|
export type PresentationAction =
|
||||||
@@ -44,7 +45,8 @@ export type PresentationAction =
|
|||||||
| { readonly type: "set_stage_theme"; readonly theme: StageTheme | null }
|
| { readonly type: "set_stage_theme"; readonly theme: StageTheme | null }
|
||||||
| { readonly type: "set_chat_theme"; readonly theme: ChatTheme | null }
|
| { readonly type: "set_chat_theme"; readonly theme: ChatTheme | null }
|
||||||
| { readonly type: "set_chat_mode"; readonly mode: ChatMode | null }
|
| { readonly type: "set_chat_mode"; readonly mode: ChatMode | null }
|
||||||
| { readonly type: "toggle_controls" };
|
| { readonly type: "toggle_controls" }
|
||||||
|
| { readonly type: "toggle_discussion_index" };
|
||||||
|
|
||||||
export const initialPresentationState: PresentationState = {
|
export const initialPresentationState: PresentationState = {
|
||||||
location: defaultMainLocation,
|
location: defaultMainLocation,
|
||||||
@@ -56,6 +58,7 @@ export const initialPresentationState: PresentationState = {
|
|||||||
chatThemeOverride: null,
|
chatThemeOverride: null,
|
||||||
chatModeOverride: null,
|
chatModeOverride: null,
|
||||||
controlsOpen: false,
|
controlsOpen: false,
|
||||||
|
discussionIndexOpen: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const compositionForLocation = (
|
const compositionForLocation = (
|
||||||
@@ -173,6 +176,8 @@ export const presentationReducer = (
|
|||||||
case "close_overlay": {
|
case "close_overlay": {
|
||||||
if (state.selectedNodeId !== null) return { ...state, selectedNodeId: null };
|
if (state.selectedNodeId !== null) return { ...state, selectedNodeId: null };
|
||||||
if (state.evidenceModeOverride !== null) return { ...state, evidenceModeOverride: null };
|
if (state.evidenceModeOverride !== null) return { ...state, evidenceModeOverride: null };
|
||||||
|
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,
|
||||||
@@ -192,5 +197,7 @@ export const presentationReducer = (
|
|||||||
return { ...state, chatModeOverride: action.mode };
|
return { ...state, chatModeOverride: action.mode };
|
||||||
case "toggle_controls":
|
case "toggle_controls":
|
||||||
return { ...state, controlsOpen: !state.controlsOpen };
|
return { ...state, controlsOpen: !state.controlsOpen };
|
||||||
|
case "toggle_discussion_index":
|
||||||
|
return { ...state, discussionIndexOpen: !state.discussionIndexOpen };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,11 +3,9 @@
|
|||||||
--evidence-width: 0rem;
|
--evidence-width: 0rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow: hidden;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.presentation-route {
|
.presentation-route {
|
||||||
@@ -28,6 +26,92 @@ html, body {
|
|||||||
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: oklch(0.97 0.005 80);
|
||||||
|
color: oklch(0.15 0.01 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-stage[data-stage-theme="night"] {
|
||||||
|
background: oklch(0.12 0.025 250);
|
||||||
|
color: oklch(0.96 0.01 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Paper theme child-element overrides */
|
||||||
|
.presentation-stage[data-stage-theme="paper"] .stage-caption__eyebrow {
|
||||||
|
color: oklch(0.45 0.04 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-stage[data-stage-theme="paper"] .scene-body__evidence {
|
||||||
|
color: oklch(0.5 0.03 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat themes */
|
||||||
|
.presentation-stage[data-chat-theme="light"] .presentation-stage__chat {
|
||||||
|
background: oklch(0.96 0.005 80);
|
||||||
|
color: oklch(0.18 0.01 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-stage[data-chat-theme="dark"] .presentation-stage__chat {
|
||||||
|
background: oklch(0.14 0.025 250);
|
||||||
|
color: oklch(0.92 0.01 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat modes */
|
||||||
|
.presentation-stage[data-chat-mode="hidden"] {
|
||||||
|
grid-template:
|
||||||
|
"primary evidence" minmax(0, 1fr)
|
||||||
|
"rail rail" auto /
|
||||||
|
minmax(0, 1fr) var(--evidence-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-stage[data-chat-mode="hidden"] .presentation-stage__chat {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-stage[data-chat-mode="rail"] {
|
||||||
|
--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;
|
||||||
|
}
|
||||||
|
|
||||||
.presentation-stage__chat,
|
.presentation-stage__chat,
|
||||||
.presentation-stage__primary,
|
.presentation-stage__primary,
|
||||||
.presentation-stage__evidence {
|
.presentation-stage__evidence {
|
||||||
@@ -451,6 +535,168 @@ html, body {
|
|||||||
color: oklch(0.65 0.2 25);
|
color: oklch(0.65 0.2 25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scene body visual treatments */
|
||||||
|
.scene-body__positioning-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__positioning-card {
|
||||||
|
padding: 0.65rem 0.75rem;
|
||||||
|
border: 1px solid oklch(0.36 0.04 250);
|
||||||
|
background: oklch(0.16 0.025 250);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__positioning-card strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__positioning-card span {
|
||||||
|
color: oklch(0.72 0.03 250);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__positioning-card--active {
|
||||||
|
border-color: oklch(0.7 0.16 195);
|
||||||
|
background: oklch(0.2 0.06 195);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__boundary {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__boundary-side {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid oklch(0.36 0.04 250);
|
||||||
|
background: oklch(0.16 0.025 250);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__boundary-side h3 {
|
||||||
|
margin: 0 0 0.35rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__boundary-side p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: oklch(0.78 0.03 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__boundary-divider {
|
||||||
|
width: 2px;
|
||||||
|
background: oklch(0.5 0.08 195);
|
||||||
|
border-radius: 1px;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__lifecycle,
|
||||||
|
.scene-body__authoring {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__lifecycle-stage,
|
||||||
|
.scene-body__authoring-step {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid oklch(0.36 0.04 250);
|
||||||
|
background: oklch(0.16 0.025 250);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__lifecycle-number,
|
||||||
|
.scene-body__authoring-number {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.4rem;
|
||||||
|
height: 1.4rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: oklch(0.7 0.16 195);
|
||||||
|
color: oklch(0.12 0.025 250);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__lifecycle-arrow,
|
||||||
|
.scene-body__authoring-arrow {
|
||||||
|
color: oklch(0.5 0.06 250);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__architecture {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__architecture-layer {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 20rem;
|
||||||
|
padding: 0.55rem 0.75rem;
|
||||||
|
border: 1px solid oklch(0.36 0.04 250);
|
||||||
|
background: oklch(0.16 0.025 250);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__architecture-layer--node {
|
||||||
|
border-color: oklch(0.7 0.16 195);
|
||||||
|
background: oklch(0.2 0.06 195);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__architecture-arrow {
|
||||||
|
color: oklch(0.5 0.06 250);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__evaluation {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__evaluation-stat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.65rem 1rem;
|
||||||
|
border: 1px solid oklch(0.36 0.04 250);
|
||||||
|
background: oklch(0.16 0.025 250);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__evaluation-stat strong {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
color: oklch(0.7 0.16 195);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-body__evaluation-stat span {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: oklch(0.72 0.03 250);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-height: 760px) {
|
@media (max-height: 760px) {
|
||||||
.scene-rail {
|
.scene-rail {
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ describe("storyboard navigation", () => {
|
|||||||
expect(locationFromHash("#discuss/nope")).toEqual(defaultMainLocation);
|
expect(locationFromHash("#discuss/nope")).toEqual(defaultMainLocation);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("falls back for malformed percent-encoded hashes", () => {
|
||||||
|
expect(locationFromHash("#scene/%ZZ/%ZZ")).toEqual(defaultMainLocation);
|
||||||
|
expect(locationFromHash("#discuss/%ZZ")).toEqual(defaultMainLocation);
|
||||||
|
});
|
||||||
|
|
||||||
it("advances within a scene before advancing to the next scene", () => {
|
it("advances within a scene before advancing to the next scene", () => {
|
||||||
expect(nextMainLocation({ kind: "main", sceneId: "thesis", beatId: "title" })).toEqual({
|
expect(nextMainLocation({ kind: "main", sceneId: "thesis", beatId: "title" })).toEqual({
|
||||||
kind: "main",
|
kind: "main",
|
||||||
|
|||||||
@@ -22,8 +22,14 @@ export const locationFromHash = (hash: string): PresentationLocation => {
|
|||||||
const raw = hash.replace(/^#/, "");
|
const raw = hash.replace(/^#/, "");
|
||||||
const sceneMatch = raw.match(/^scene\/([^/]+)\/(.+)$/);
|
const sceneMatch = raw.match(/^scene\/([^/]+)\/(.+)$/);
|
||||||
if (sceneMatch) {
|
if (sceneMatch) {
|
||||||
const sceneId = decodeURIComponent(sceneMatch[1]!);
|
let sceneId: string;
|
||||||
const beatId = decodeURIComponent(sceneMatch[2]!);
|
let beatId: string;
|
||||||
|
try {
|
||||||
|
sceneId = decodeURIComponent(sceneMatch[1]!);
|
||||||
|
beatId = decodeURIComponent(sceneMatch[2]!);
|
||||||
|
} catch {
|
||||||
|
return defaultMainLocation;
|
||||||
|
}
|
||||||
const scene = findScene(sceneId);
|
const scene = findScene(sceneId);
|
||||||
if (scene && scene.beats.some((b) => b.id === beatId)) {
|
if (scene && scene.beats.some((b) => b.id === beatId)) {
|
||||||
return { kind: "main", sceneId: scene.id as MainLocation["sceneId"], beatId };
|
return { kind: "main", sceneId: scene.id as MainLocation["sceneId"], beatId };
|
||||||
@@ -32,7 +38,12 @@ export const locationFromHash = (hash: string): PresentationLocation => {
|
|||||||
}
|
}
|
||||||
const discussMatch = raw.match(/^discuss\/(.+)$/);
|
const discussMatch = raw.match(/^discuss\/(.+)$/);
|
||||||
if (discussMatch) {
|
if (discussMatch) {
|
||||||
const branchId = decodeURIComponent(discussMatch[1]!);
|
let branchId: string;
|
||||||
|
try {
|
||||||
|
branchId = decodeURIComponent(discussMatch[1]!);
|
||||||
|
} catch {
|
||||||
|
return defaultMainLocation;
|
||||||
|
}
|
||||||
if (findDiscussionBranch(branchId)) {
|
if (findDiscussionBranch(branchId)) {
|
||||||
return { kind: "discussion", branchId: branchId as DiscussionLocation["branchId"] };
|
return { kind: "discussion", branchId: branchId as DiscussionLocation["branchId"] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,15 +41,21 @@ describe("defense storyboard catalog", () => {
|
|||||||
expect(findBeat("interrupt-evidence", "trace")?.chatMode).toBe("dock");
|
expect(findBeat("interrupt-evidence", "trace")?.chatMode).toBe("dock");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defines the five positioning discussion branches", () => {
|
it("defines discussion branches across multiple scenes", () => {
|
||||||
expect(discussionBranches.map((branch) => branch.id)).toEqual([
|
expect(discussionBranches.length).toBeGreaterThanOrEqual(5);
|
||||||
|
const positioningBranches = discussionBranches.filter((b) => b.parentSceneId === "positioning");
|
||||||
|
expect(positioningBranches.map((b) => b.id)).toEqual([
|
||||||
"direct-orchestration",
|
"direct-orchestration",
|
||||||
"generated-scripts",
|
"generated-scripts",
|
||||||
"hosted-automation",
|
"hosted-automation",
|
||||||
"durable-agent-graphs",
|
"durable-agent-graphs",
|
||||||
"mcp-agent-scale",
|
"mcp-agent-scale",
|
||||||
]);
|
]);
|
||||||
expect(discussionBranches.every((branch) => branch.parentSceneId === "positioning")).toBe(true);
|
for (const branch of discussionBranches) {
|
||||||
|
expect(branch.title.length).toBeGreaterThan(0);
|
||||||
|
expect(branch.summary.length).toBeGreaterThan(0);
|
||||||
|
expect(branch.evidencePointer.length).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exposes a valid default location", () => {
|
it("exposes a valid default location", () => {
|
||||||
|
|||||||
@@ -284,6 +284,70 @@ export const discussionBranches = defineDiscussionBranches([
|
|||||||
evidencePointer: "Thesis: Model Context Protocol; Cloudflare Code Mode; Anthropic code execution with MCP",
|
evidencePointer: "Thesis: Model Context Protocol; Cloudflare Code Mode; Anthropic code execution with MCP",
|
||||||
summary: "MCP is a capability protocol; progressive discovery addresses large agent-facing surfaces.",
|
summary: "MCP is a capability protocol; progressive discovery addresses large agent-facing surfaces.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "lifecycle-states",
|
||||||
|
parentSceneId: "lifecycle",
|
||||||
|
title: "Lifecycle state machine",
|
||||||
|
claimClass: "implemented",
|
||||||
|
evidencePointer: "Thesis Workflow Lifecycle; lifecycle state transitions",
|
||||||
|
summary: "Draft, artifact, deployment, and run form a linear lifecycle with typed transitions.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "raw-plan-import",
|
||||||
|
parentSceneId: "authoring",
|
||||||
|
title: "Raw plan import",
|
||||||
|
claimClass: "implemented",
|
||||||
|
evidencePointer: "Thesis Authoring; raw plan import and compilation",
|
||||||
|
summary: "Raw plans are compiled into validated workflow definitions with typed operations.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "validation-diagnostics",
|
||||||
|
parentSceneId: "authoring",
|
||||||
|
title: "Validation and diagnostics",
|
||||||
|
claimClass: "implemented",
|
||||||
|
evidencePointer: "Thesis Validation; diagnostic repair hints",
|
||||||
|
summary: "Structured diagnostics identify invalid state and suggest repair hints.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "typed-interrupts",
|
||||||
|
parentSceneId: "interrupt-evidence",
|
||||||
|
title: "Typed interrupt contracts",
|
||||||
|
claimClass: "implemented",
|
||||||
|
evidencePointer: "Thesis Typed Interrupts; interrupt schemas",
|
||||||
|
summary: "Interrupts are typed resume-request payloads with schema-backed review boundaries.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "run-persistence",
|
||||||
|
parentSceneId: "architecture",
|
||||||
|
title: "Run persistence and inspection",
|
||||||
|
claimClass: "implemented",
|
||||||
|
evidencePointer: "Thesis Runtime; run persistence and trace events",
|
||||||
|
summary: "Persisted runs store output, status, and trace for post-hoc inspection.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "provider-security",
|
||||||
|
parentSceneId: "architecture",
|
||||||
|
title: "Provider-neutral security",
|
||||||
|
claimClass: "implemented",
|
||||||
|
evidencePointer: "Thesis Architecture; provider-neutral capability resolution",
|
||||||
|
summary: "Provider-neutral capabilities avoid vendor lock-in while maintaining security boundaries.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "evaluation-validity",
|
||||||
|
parentSceneId: "evaluation",
|
||||||
|
title: "Evaluation validity bounds",
|
||||||
|
claimClass: "evaluated",
|
||||||
|
evidencePointer: "Thesis Evaluation; bounded validity",
|
||||||
|
summary: "Manual audit separates task completion from valid product-surface evidence.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "replay-provenance",
|
||||||
|
parentSceneId: "workflow-demo",
|
||||||
|
title: "Replay provenance",
|
||||||
|
claimClass: "implemented",
|
||||||
|
evidencePointer: "Thesis Prepared Replay; replay evidence chain",
|
||||||
|
summary: "Replay evidence is projected from the canonical recording for deterministic demonstration.",
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type DiscussionBranchId = (typeof discussionBranches)[number]["id"];
|
export type DiscussionBranchId = (typeof discussionBranches)[number]["id"];
|
||||||
|
|||||||
Reference in New Issue
Block a user