feat: show presentation operation blocks

This commit is contained in:
lda
2026-07-03 05:24:45 +07:00 Verified
parent e0c51dd0a4
commit 7ab3cb0bf8
4 changed files with 112 additions and 0 deletions
@@ -0,0 +1,39 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import type { DemoEvent } from "../demo/timeline/models.js";
import { OperationBlock } from "./OperationBlock.js";
afterEach(() => cleanup());
const event: DemoEvent = {
id: "recorded-1-run-start",
sequence: 1,
stage: "run_start",
operation: "workflow.runs.start",
reason: "Start the prepared report workflow.",
equivalentCli: "uv run wf run start lda_report_case_study.default --input '<json>'",
params: { deployment_id: "lda_report_case_study.default" },
rawResponse: { result: { status: "interrupted" } },
interpreted: {
status: "interrupted",
interrupt: { kind: "issue_review" },
},
durationMs: 88,
resultingIds: {
deploymentId: "lda_report_case_study.default",
runId: "run_demo",
},
recordedAt: "2026-07-03T00:00:01.000Z",
};
describe("OperationBlock", () => {
it("shows command, raw response, and interpreted result", () => {
render(<OperationBlock event={event} />);
expect(screen.getByText(/workflow.runs.start/i)).toBeInTheDocument();
expect(screen.getByText(/uv run wf run start/i)).toBeInTheDocument();
expect(screen.getByText("Raw")).toBeInTheDocument();
expect(screen.getByText("Interpreted")).toBeInTheDocument();
expect(screen.getByText(/run_demo/i)).toBeInTheDocument();
});
});
@@ -0,0 +1,33 @@
import type { DemoEvent } from "../demo/timeline/models.js";
const formatJson = (value: unknown): string => JSON.stringify(value, null, 2);
type OperationBlockProps = {
readonly event: DemoEvent;
};
export const OperationBlock = ({ event }: OperationBlockProps) => (
<section className="operation-block" aria-label={`${event.stage} operation`}>
<header>
<p>{event.operation ?? event.stage}</p>
<small>{event.durationMs} ms</small>
</header>
{event.equivalentCli && (
<pre className="operation-block__command"><code>{event.equivalentCli}</code></pre>
)}
<div className="operation-block__grid">
<section>
<h3>Raw</h3>
<pre><code>{formatJson(event.rawResponse)}</code></pre>
</section>
<section>
<h3>Interpreted</h3>
<pre><code>{formatJson(event.interpreted)}</code></pre>
</section>
</div>
<footer>
<span>{event.resultingIds.deploymentId}</span>
{event.resultingIds.runId && <span>{event.resultingIds.runId}</span>}
</footer>
</section>
);
@@ -1,5 +1,6 @@
import { presentationBeats, type BeatId } from "./beats.js";
import { BeatRail } from "./BeatRail.js";
import { OperationBlock } from "./OperationBlock.js";
import { OperatorChat } from "./OperatorChat.js";
import { StageCaption } from "./StageCaption.js";
import type { PresentationState } from "./presentation-state.js";
@@ -14,6 +15,11 @@ type PresentationStageProps = {
export const PresentationStage = ({ state, demo, jump }: PresentationStageProps) => {
const beat = presentationBeats.find((candidate) => candidate.id === state.beat) ?? presentationBeats[0]!;
const operationEvent =
demo.state.events.find((event) => event.stage === "run_start") ??
demo.state.events.find((event) => event.operation !== null) ??
null;
return (
<div className="presentation-stage" data-beat={state.beat}>
<OperatorChat state={state} />
@@ -21,6 +27,7 @@ export const PresentationStage = ({ state, demo, jump }: PresentationStageProps)
<StageCaption eyebrow="lda.chat defense" title={beat.title}>
<p>{beat.caption}</p>
</StageCaption>
{operationEvent && <OperationBlock event={operationEvent} />}
<p className="presentation-stage__mode">
{demo.state.mode === "replay" ? "Replay" : "Live"} · {demo.state.phase}
</p>
@@ -55,3 +55,36 @@
background: oklch(0.7 0.16 195);
color: oklch(0.12 0.025 250);
}
.operation-block {
border: 1px solid oklch(0.36 0.04 250);
background: oklch(0.16 0.025 250);
padding: 1rem;
border-radius: 0.85rem;
}
.operation-block header,
.operation-block footer {
display: flex;
justify-content: space-between;
gap: 1rem;
color: oklch(0.78 0.035 250);
}
.operation-block__command,
.operation-block pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.operation-block__grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
@media (max-width: 800px) {
.operation-block__grid {
grid-template-columns: 1fr;
}
}