feat: add presentation problem visual

This commit is contained in:
lda
2026-07-10 03:41:20 +07:00 Verified
parent 9a42e2bfc3
commit e40fd67ca2
5 changed files with 174 additions and 0 deletions
@@ -0,0 +1,33 @@
import { cleanup, render, screen, within } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { findBeat, findScene } from "../storyboard.js";
import { ProblemLoopScene } from "./ProblemLoopScene.js";
const problemScene = findScene("problem")!;
afterEach(() => cleanup());
describe("ProblemLoopScene", () => {
it("renders the action sequence as useful but insufficient", () => {
render(<ProblemLoopScene scene={problemScene} beat={findBeat("problem", "direct-actions")!} />);
const action = screen.getByRole("group", { name: "Action sequence" });
expect(within(action).getByText("think")).toBeInTheDocument();
expect(within(action).getAllByText("tool")).toHaveLength(2);
expect(within(action).getByText("observe")).toBeInTheDocument();
expect(within(action).getByText("done")).toBeInTheDocument();
expect(screen.getByText(/useful once/i)).toBeInTheDocument();
});
it("renders reusable automation with simple verbs only", () => {
render(<ProblemLoopScene scene={problemScene} beat={findBeat("problem", "missing-contracts")!} />);
const automation = screen.getByRole("group", { name: "Reusable automation" });
for (const label of ["design", "save", "connect", "run", "inspect"]) {
expect(within(automation).getByText(label)).toBeInTheDocument();
}
for (const formalName of ["Draft", "Artifact", "Deployment", "Trace"]) {
expect(screen.queryByText(formalName)).not.toBeInTheDocument();
}
});
});
@@ -0,0 +1,47 @@
import { StageCaption } from "../StageCaption.js";
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
import { ConceptNode, ConceptRail } from "./ConceptPrimitives.js";
type ProblemLoopSceneProps = {
readonly scene: SceneDefinition;
readonly beat: SceneBeatDefinition;
};
export const ProblemLoopScene = ({ scene, beat }: ProblemLoopSceneProps) => {
const automationBeat = beat.id === "missing-contracts";
return (
<>
<StageCaption eyebrow="Problem shape" title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
<section className="problem-loop-scene" aria-label="action sequence versus reusable automation">
<div className="problem-loop-scene__side" data-problem-active={automationBeat ? "false" : "true"}>
<h2>Action sequence</h2>
<ConceptRail label="Action sequence">
<ConceptNode title="think" icon="think" emphasis={automationBeat ? "muted" : "normal"} />
<ConceptNode title="tool" icon="toolCall" emphasis={automationBeat ? "muted" : "normal"} />
<ConceptNode title="observe" icon="observe" emphasis={automationBeat ? "muted" : "normal"} />
<ConceptNode title="tool" icon="toolCall" emphasis={automationBeat ? "muted" : "normal"} />
<ConceptNode title="done" icon="done" emphasis={automationBeat ? "muted" : "normal"} />
</ConceptRail>
<p>Useful once. Hard to reuse.</p>
</div>
<div className="problem-loop-scene__bridge" aria-hidden="true"></div>
<div className="problem-loop-scene__side" data-problem-active={automationBeat ? "true" : "false"}>
<h2>Reusable automation</h2>
<ConceptRail label="Reusable automation">
<ConceptNode title="design" icon="design" emphasis={automationBeat ? "primary" : "normal"} />
<ConceptNode title="save" icon="save" emphasis={automationBeat ? "primary" : "normal"} />
<ConceptNode title="connect" icon="connect" emphasis={automationBeat ? "primary" : "normal"} />
<ConceptNode title="run" icon="run" emphasis={automationBeat ? "primary" : "normal"} />
<ConceptNode title="inspect" icon="inspect" emphasis={automationBeat ? "primary" : "normal"} />
</ConceptRail>
<p>The platform makes work reusable.</p>
</div>
</section>
<p className="scene-body__evidence">{scene.evidencePointer}</p>
</>
);
};