feat: show presentation operation blocks
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user