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 {
|
||||
initialPresentationState,
|
||||
presentationReducer,
|
||||
compositionForState,
|
||||
} from "./presentation-state.js";
|
||||
import { hashForLocation } from "./storyboard-navigation.js";
|
||||
import { findScene } from "./storyboard.js";
|
||||
import type { PresentationLocation } from "./storyboard.js";
|
||||
import "./presentation.css";
|
||||
|
||||
@@ -154,7 +156,8 @@ export const PresentationRoute = () => {
|
||||
|
||||
const handleForceReplay = useCallback(() => {
|
||||
demo.setMode("replay");
|
||||
}, [demo]);
|
||||
setEvidence(replayEvidence);
|
||||
}, [demo, replayEvidence]);
|
||||
|
||||
const handleResetOverrides = useCallback(() => {
|
||||
dispatch({ type: "set_stage_theme", theme: null });
|
||||
@@ -162,7 +165,15 @@ export const PresentationRoute = () => {
|
||||
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 (
|
||||
<main className="presentation-route" aria-label="lda.chat presentation">
|
||||
@@ -179,6 +190,7 @@ export const PresentationRoute = () => {
|
||||
closeOverlay={() => dispatch({ type: "close_overlay" })}
|
||||
openDiscussion={handleOpenDiscussion}
|
||||
closeDiscussion={handleCloseDiscussion}
|
||||
toggleDiscussionIndex={() => dispatch({ type: "toggle_discussion_index" })}
|
||||
/>
|
||||
{showChatDock && <ChatDock openChat={() => dispatch({ type: "set_chat_mode", mode: "rail" })} />}
|
||||
{state.controlsOpen && (
|
||||
@@ -191,8 +203,8 @@ export const PresentationRoute = () => {
|
||||
setChatTheme={(theme) => dispatch({ type: "set_chat_theme", theme })}
|
||||
setChatMode={(mode) => dispatch({ type: "set_chat_mode", mode })}
|
||||
forceReplay={handleForceReplay}
|
||||
openDiscussionIndex={() => dispatch({ type: "toggle_controls" })}
|
||||
resetOverrides={handleResetOverrides}
|
||||
resetScene={handleResetScene}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useState } from "react";
|
||||
import type { EvidenceRecord } from "../app/state.js";
|
||||
import type { AgentMessage } from "../demo/agent/events.js";
|
||||
import { SceneBody } from "./SceneBody.js";
|
||||
@@ -25,6 +24,7 @@ type PresentationStageProps = {
|
||||
readonly closeOverlay: () => void;
|
||||
readonly openDiscussion: (branchId: string) => void;
|
||||
readonly closeDiscussion: () => void;
|
||||
readonly toggleDiscussionIndex: () => void;
|
||||
};
|
||||
|
||||
export const PresentationStage = ({
|
||||
@@ -40,9 +40,9 @@ export const PresentationStage = ({
|
||||
closeOverlay,
|
||||
openDiscussion,
|
||||
closeDiscussion,
|
||||
toggleDiscussionIndex,
|
||||
}: PresentationStageProps) => {
|
||||
const composition = compositionForState(state);
|
||||
const [indexOpen, setIndexOpen] = useState(false);
|
||||
|
||||
const isMainScene = state.location.kind === "main";
|
||||
const currentScene = isMainScene ? findScene(state.location.sceneId) : null;
|
||||
@@ -75,17 +75,17 @@ export const PresentationStage = ({
|
||||
<button
|
||||
type="button"
|
||||
className="presentation-stage__discussion-toggle"
|
||||
onClick={() => setIndexOpen(true)}
|
||||
onClick={toggleDiscussionIndex}
|
||||
>
|
||||
Open discussion topics
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{indexOpen && (
|
||||
{state.discussionIndexOpen && (
|
||||
<DiscussionIndex
|
||||
onSelect={(branchId) => {
|
||||
setIndexOpen(false);
|
||||
toggleDiscussionIndex();
|
||||
openDiscussion(branchId);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -17,8 +17,8 @@ describe("PresenterControls", () => {
|
||||
setChatTheme: vi.fn(),
|
||||
setChatMode: vi.fn(),
|
||||
forceReplay: vi.fn(),
|
||||
openDiscussionIndex: vi.fn(),
|
||||
resetOverrides: vi.fn(),
|
||||
resetScene: vi.fn(),
|
||||
};
|
||||
|
||||
it("changes stage and chat themes independently", async () => {
|
||||
|
||||
@@ -13,8 +13,8 @@ type PresenterControlsProps = {
|
||||
readonly setChatTheme: (theme: ChatTheme | null) => void;
|
||||
readonly setChatMode: (mode: ChatMode | null) => void;
|
||||
readonly forceReplay: () => void;
|
||||
readonly openDiscussionIndex: () => void;
|
||||
readonly resetOverrides: () => void;
|
||||
readonly resetScene: () => void;
|
||||
};
|
||||
|
||||
export const PresenterControls = ({
|
||||
@@ -26,13 +26,19 @@ export const PresenterControls = ({
|
||||
setChatTheme,
|
||||
setChatMode,
|
||||
forceReplay,
|
||||
openDiscussionIndex,
|
||||
resetOverrides,
|
||||
resetScene,
|
||||
}: 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;
|
||||
|
||||
return (
|
||||
<div className="presenter-controls" role="dialog" aria-label="presenter controls">
|
||||
@@ -41,8 +47,8 @@ export const PresenterControls = ({
|
||||
<button type="button" onClick={next}>Next</button>
|
||||
</div>
|
||||
<div className="presenter-controls__info">
|
||||
<span>{currentScene?.title ?? "Discussion"}</span>
|
||||
{currentBeat && <span> · {currentBeat.title}</span>}
|
||||
<span>Scene {sceneNumber}/{totalScenes}: {currentScene?.title ?? "Discussion"}</span>
|
||||
{currentBeat && <span> · {beatIndex + 1}/{totalBeats} {currentBeat.title}</span>}
|
||||
</div>
|
||||
<div className="presenter-controls__over">
|
||||
<label>
|
||||
@@ -84,7 +90,7 @@ export const PresenterControls = ({
|
||||
<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={openDiscussionIndex}>Open discussion index</button>
|
||||
<button type="button" onClick={resetScene}>Reset scene</button>
|
||||
<button type="button" onClick={resetOverrides}>Reset overrides</button>
|
||||
</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 }) => (
|
||||
<>
|
||||
<StageCaption eyebrow="Agent handoff" title={scene.title}>
|
||||
@@ -80,14 +208,17 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode }: SceneB
|
||||
|
||||
switch (scene.view) {
|
||||
case "narrative":
|
||||
case "positioning":
|
||||
case "boundary":
|
||||
case "lifecycle":
|
||||
case "architecture":
|
||||
case "authoring":
|
||||
case "evaluation":
|
||||
case "conclusion":
|
||||
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":
|
||||
return <AgentHandoffScene scene={scene} beat={beat} />;
|
||||
case "demo":
|
||||
@@ -100,6 +231,10 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode }: SceneB
|
||||
selectNode={selectNode}
|
||||
/>
|
||||
);
|
||||
case "evaluation":
|
||||
return <EvaluationScene scene={scene} beat={beat} />;
|
||||
case "conclusion":
|
||||
return <NarrativeScene scene={scene} beat={beat} />;
|
||||
default:
|
||||
return assertNever(scene.view);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export type PresentationState = {
|
||||
readonly chatThemeOverride: ChatTheme | null;
|
||||
readonly chatModeOverride: ChatMode | null;
|
||||
readonly controlsOpen: boolean;
|
||||
readonly discussionIndexOpen: boolean;
|
||||
};
|
||||
|
||||
export type PresentationAction =
|
||||
@@ -44,7 +45,8 @@ export type PresentationAction =
|
||||
| { 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: "toggle_controls" };
|
||||
| { readonly type: "toggle_controls" }
|
||||
| { readonly type: "toggle_discussion_index" };
|
||||
|
||||
export const initialPresentationState: PresentationState = {
|
||||
location: defaultMainLocation,
|
||||
@@ -56,6 +58,7 @@ export const initialPresentationState: PresentationState = {
|
||||
chatThemeOverride: null,
|
||||
chatModeOverride: null,
|
||||
controlsOpen: false,
|
||||
discussionIndexOpen: false,
|
||||
};
|
||||
|
||||
const compositionForLocation = (
|
||||
@@ -173,6 +176,8 @@ export const presentationReducer = (
|
||||
case "close_overlay": {
|
||||
if (state.selectedNodeId !== null) return { ...state, selectedNodeId: 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") {
|
||||
return {
|
||||
...state,
|
||||
@@ -192,5 +197,7 @@ export const presentationReducer = (
|
||||
return { ...state, chatModeOverride: action.mode };
|
||||
case "toggle_controls":
|
||||
return { ...state, controlsOpen: !state.controlsOpen };
|
||||
case "toggle_discussion_index":
|
||||
return { ...state, discussionIndexOpen: !state.discussionIndexOpen };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
--evidence-width: 0rem;
|
||||
}
|
||||
|
||||
html, body {
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.presentation-route {
|
||||
@@ -28,6 +26,92 @@ html, body {
|
||||
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__primary,
|
||||
.presentation-stage__evidence {
|
||||
@@ -451,6 +535,168 @@ html, body {
|
||||
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) {
|
||||
.scene-rail {
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
@@ -23,6 +23,11 @@ describe("storyboard navigation", () => {
|
||||
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", () => {
|
||||
expect(nextMainLocation({ kind: "main", sceneId: "thesis", beatId: "title" })).toEqual({
|
||||
kind: "main",
|
||||
|
||||
@@ -22,8 +22,14 @@ export const locationFromHash = (hash: string): PresentationLocation => {
|
||||
const raw = hash.replace(/^#/, "");
|
||||
const sceneMatch = raw.match(/^scene\/([^/]+)\/(.+)$/);
|
||||
if (sceneMatch) {
|
||||
const sceneId = decodeURIComponent(sceneMatch[1]!);
|
||||
const beatId = decodeURIComponent(sceneMatch[2]!);
|
||||
let sceneId: string;
|
||||
let beatId: string;
|
||||
try {
|
||||
sceneId = decodeURIComponent(sceneMatch[1]!);
|
||||
beatId = decodeURIComponent(sceneMatch[2]!);
|
||||
} catch {
|
||||
return defaultMainLocation;
|
||||
}
|
||||
const scene = findScene(sceneId);
|
||||
if (scene && scene.beats.some((b) => b.id === 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\/(.+)$/);
|
||||
if (discussMatch) {
|
||||
const branchId = decodeURIComponent(discussMatch[1]!);
|
||||
let branchId: string;
|
||||
try {
|
||||
branchId = decodeURIComponent(discussMatch[1]!);
|
||||
} catch {
|
||||
return defaultMainLocation;
|
||||
}
|
||||
if (findDiscussionBranch(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");
|
||||
});
|
||||
|
||||
it("defines the five positioning discussion branches", () => {
|
||||
expect(discussionBranches.map((branch) => branch.id)).toEqual([
|
||||
it("defines discussion branches across multiple scenes", () => {
|
||||
expect(discussionBranches.length).toBeGreaterThanOrEqual(5);
|
||||
const positioningBranches = discussionBranches.filter((b) => b.parentSceneId === "positioning");
|
||||
expect(positioningBranches.map((b) => b.id)).toEqual([
|
||||
"direct-orchestration",
|
||||
"generated-scripts",
|
||||
"hosted-automation",
|
||||
"durable-agent-graphs",
|
||||
"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", () => {
|
||||
|
||||
@@ -284,6 +284,70 @@ export const discussionBranches = defineDiscussionBranches([
|
||||
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.",
|
||||
},
|
||||
{
|
||||
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"];
|
||||
|
||||
Reference in New Issue
Block a user