feat: show presentation live replay status
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { cleanup, render, screen, within } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { EvidenceRecord } from "../app/state.js";
|
||||
import type { PresentationTargetHealth } from "./presentation-target-status.js";
|
||||
import { PresentationFooter } from "./PresentationFooter.js";
|
||||
|
||||
afterEach(() => cleanup());
|
||||
@@ -16,6 +17,11 @@ describe("PresentationFooter", () => {
|
||||
response: { result: { status: "completed" } },
|
||||
durationMs: 34,
|
||||
};
|
||||
const replayHealth: PresentationTargetHealth = {
|
||||
kind: "replay",
|
||||
label: "Replay evidence",
|
||||
detail: "reviewed recording",
|
||||
};
|
||||
render(
|
||||
<PresentationFooter
|
||||
location={{
|
||||
@@ -25,6 +31,7 @@ describe("PresentationFooter", () => {
|
||||
focusPath: [],
|
||||
}}
|
||||
evidence={[evidence]}
|
||||
targetStatus={replayHealth}
|
||||
showEvidenceReceipt
|
||||
inspectEvidence={vi.fn()}
|
||||
/>,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { EvidenceRecord } from "../app/state.js";
|
||||
import { SceneProgress } from "./SceneProgress.js";
|
||||
import { EvidenceReceipt } from "./evidence/EvidenceReceipt.js";
|
||||
import { PresentationTruthBadge } from "./PresentationTruthBadge.js";
|
||||
import type { PresentationTargetHealth } from "./presentation-target-status.js";
|
||||
import type { MainLocation } from "./storyboard.js";
|
||||
|
||||
type PresentationFooterProps = {
|
||||
readonly location: MainLocation;
|
||||
readonly evidence: readonly EvidenceRecord[];
|
||||
readonly targetStatus: PresentationTargetHealth;
|
||||
readonly showEvidenceReceipt: boolean;
|
||||
readonly inspectEvidence: () => void;
|
||||
};
|
||||
@@ -13,11 +16,13 @@ type PresentationFooterProps = {
|
||||
export const PresentationFooter = ({
|
||||
location,
|
||||
evidence,
|
||||
targetStatus,
|
||||
showEvidenceReceipt,
|
||||
inspectEvidence,
|
||||
}: PresentationFooterProps) => (
|
||||
<footer className="presentation-footer" aria-label="presentation footer">
|
||||
<SceneProgress location={location} />
|
||||
<PresentationTruthBadge status={targetStatus} />
|
||||
<EvidenceReceipt
|
||||
records={evidence}
|
||||
visible={showEvidenceReceipt}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useReducer, useState } from "react";
|
||||
import type { EvidenceRecord } from "../app/state.js";
|
||||
import { resolvePresentationTarget } from "./live-target.js";
|
||||
import { usePresentationTargetStatus } from "./usePresentationTargetStatus.js";
|
||||
import { useTimelineAgent } from "../demo/agent/timelineAgent.js";
|
||||
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
|
||||
import { useDemoTimeline } from "../demo/useDemoTimeline.js";
|
||||
@@ -52,6 +53,7 @@ export const PresentationRoute = () => {
|
||||
|
||||
const presentationTarget = useMemo(() => resolvePresentationTarget(), []);
|
||||
const demo = useDemoTimeline(presentationTarget.target, recordEvidence, recording);
|
||||
const targetStatus = usePresentationTargetStatus(presentationTarget, demo.state);
|
||||
const timelineAgent = useTimelineAgent(
|
||||
demo,
|
||||
presentationTarget.mode === "live" ? "live" : "replay",
|
||||
@@ -197,6 +199,7 @@ export const PresentationRoute = () => {
|
||||
evidence={evidence}
|
||||
timelineAgent={timelineAgent}
|
||||
approvalActions={approvalActions}
|
||||
targetStatus={targetStatus}
|
||||
jump={handleJump}
|
||||
selectNode={(nodeId) => dispatch({ type: "select_node", nodeId })}
|
||||
openEvidence={() => dispatch({ type: "set_evidence_presentation", presentation: "inspector" })}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { OperatorChat } from "./OperatorChat.js";
|
||||
import { PresentationFooter } from "./PresentationFooter.js";
|
||||
import type { PresentationState } from "./presentation-state.js";
|
||||
import { compositionForState } from "./presentation-state.js";
|
||||
import type { PresentationTargetHealth } from "./presentation-target-status.js";
|
||||
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
||||
import { findScene, type MainLocation } from "./storyboard.js";
|
||||
|
||||
@@ -22,6 +23,7 @@ type PresentationStageProps = {
|
||||
readonly approvalActions?: DemoApprovalActions | undefined;
|
||||
readonly onApprove?: (() => void) | undefined;
|
||||
readonly onDeny?: (() => void) | undefined;
|
||||
readonly targetStatus: PresentationTargetHealth;
|
||||
readonly jump: (location: MainLocation) => void;
|
||||
readonly selectNode: (nodeId: string | null) => void;
|
||||
readonly openEvidence: () => void;
|
||||
@@ -39,6 +41,7 @@ export const PresentationStage = ({
|
||||
approvalActions,
|
||||
onApprove,
|
||||
onDeny,
|
||||
targetStatus,
|
||||
jump,
|
||||
selectNode,
|
||||
openEvidence,
|
||||
@@ -90,6 +93,7 @@ export const PresentationStage = ({
|
||||
<PresentationFooter
|
||||
location={state.location}
|
||||
evidence={evidence}
|
||||
targetStatus={targetStatus}
|
||||
showEvidenceReceipt={composition.evidencePresentation !== "hidden"}
|
||||
inspectEvidence={openEvidence}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { PresentationTruthBadge } from "./PresentationTruthBadge.js";
|
||||
|
||||
describe("PresentationTruthBadge", () => {
|
||||
it("renders status label and detail", () => {
|
||||
render(
|
||||
<PresentationTruthBadge
|
||||
status={{
|
||||
kind: "ready",
|
||||
target: "http://127.0.0.1:8765/rpc",
|
||||
label: "Live target ready",
|
||||
detail: "127.0.0.1:8765",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("presentation evidence mode")).toHaveAttribute("data-status", "ready");
|
||||
expect(screen.getByText("Live target ready")).toBeInTheDocument();
|
||||
expect(screen.getByText("127.0.0.1:8765")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { PresentationTargetHealth } from "./presentation-target-status.js";
|
||||
|
||||
export const PresentationTruthBadge = ({
|
||||
status,
|
||||
}: {
|
||||
readonly status: PresentationTargetHealth;
|
||||
}) => (
|
||||
<aside
|
||||
className="presentation-truth-badge"
|
||||
data-status={status.kind}
|
||||
aria-label="presentation evidence mode"
|
||||
>
|
||||
<strong>{status.label}</strong>
|
||||
<span>{status.detail}</span>
|
||||
</aside>
|
||||
);
|
||||
@@ -494,6 +494,36 @@
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.presentation-truth-badge {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 0.45rem;
|
||||
min-width: 0;
|
||||
padding: 0.3rem 0.55rem;
|
||||
border: 1px solid var(--stage-line);
|
||||
border-radius: 999px;
|
||||
background: color-mix(in oklch, var(--stage-surface) 88%, transparent);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
|
||||
.presentation-truth-badge span {
|
||||
color: var(--text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.presentation-truth-badge[data-status="failed"] {
|
||||
border-color: color-mix(in oklch, var(--accent-amber) 70%, var(--stage-line));
|
||||
}
|
||||
|
||||
.presentation-truth-badge[data-status="active"],
|
||||
.presentation-truth-badge[data-status="ready"] {
|
||||
border-color: color-mix(in oklch, var(--accent-cyan) 65%, var(--stage-line));
|
||||
}
|
||||
|
||||
.evidence-receipt:focus-visible,
|
||||
.evidence-inspector button:focus-visible,
|
||||
.evidence-inspector select:focus-visible {
|
||||
|
||||
Reference in New Issue
Block a user