fix: address presentation review findings
This commit is contained in:
+14
-11
@@ -204,8 +204,8 @@ Presentation visual audit, July 11:
|
|||||||
|
|
||||||
## Completed: Presentation Recomposition And Authoring Story
|
## Completed: Presentation Recomposition And Authoring Story
|
||||||
|
|
||||||
Scenes 8 and 9 are now implemented as authentic external-agent request and
|
Scenes 8 and 9 now present an external-agent request through a scripted
|
||||||
factual prepared-authoring proof. Scene 8 is one local request-to-first-turn
|
presentation surface and factual prepared-authoring proof. Scene 8 is one local request-to-first-turn
|
||||||
beat; its obsolete `/handoff` route fails closed. Scene 9 keeps a persistent
|
beat; its obsolete `/handoff` route fails closed. Scene 9 keeps a persistent
|
||||||
prepared assistant pane beside five factual phase visuals in an adaptive
|
prepared assistant pane beside five factual phase visuals in an adaptive
|
||||||
approximately 35/65 split. The obsolete receipt/trace modal and lower chat dock
|
approximately 35/65 split. The obsolete receipt/trace modal and lower chat dock
|
||||||
@@ -244,8 +244,9 @@ Recommended next visual slices:
|
|||||||
[`presentation evaluation and closing`](superpowers/specs/2026-07-10-presentation-evaluation-closing-design.md).
|
[`presentation evaluation and closing`](superpowers/specs/2026-07-10-presentation-evaluation-closing-design.md).
|
||||||
Implementation:
|
Implementation:
|
||||||
[`presentation evaluation and closing plan`](historical/superpowers/plans/2026-07-10-presentation-evaluation-closing.md).
|
[`presentation evaluation and closing plan`](historical/superpowers/plans/2026-07-10-presentation-evaluation-closing.md).
|
||||||
6. Completed: Presentation agent authoring story rebuilds Scenes 8 and 9 as
|
6. Completed: Presentation agent authoring story rebuilds Scenes 8 and 9 as a
|
||||||
authentic external-agent handoff and factual prepared-authoring proof.
|
scripted surface representing an external-agent handoff and factual
|
||||||
|
prepared-authoring proof.
|
||||||
Implementation:
|
Implementation:
|
||||||
[`presentation agent authoring story plan`](historical/superpowers/plans/2026-07-11-presentation-agent-authoring-story.md).
|
[`presentation agent authoring story plan`](historical/superpowers/plans/2026-07-11-presentation-agent-authoring-story.md).
|
||||||
7. Completed: Scene 1 now introduces the agent-shaped product goal through
|
7. Completed: Scene 1 now introduces the agent-shaped product goal through
|
||||||
@@ -261,7 +262,7 @@ Recommended next visual slices:
|
|||||||
Implementation:
|
Implementation:
|
||||||
[`defense presentation rehearsal`](historical/superpowers/plans/2026-07-12-defense-presentation-rehearsal.md).
|
[`defense presentation rehearsal`](historical/superpowers/plans/2026-07-12-defense-presentation-rehearsal.md).
|
||||||
9. Completed: Scenes 7-9 now use one dominant factual artifact per beat on the
|
9. Completed: Scenes 7-9 now use one dominant factual artifact per beat on the
|
||||||
Editorial Canvas: authoring/repair evidence, an authentic light agent chat,
|
Editorial Canvas: authoring/repair evidence, a scripted light agent chat,
|
||||||
and a light prepared lifecycle canvas with synchronized secondary chat.
|
and a light prepared lifecycle canvas with synchronized secondary chat.
|
||||||
Implementation:
|
Implementation:
|
||||||
[`Scenes 7-9 editorial focal proof`](historical/superpowers/plans/2026-07-12-scenes-7-9-editorial-focal-proof.md).
|
[`Scenes 7-9 editorial focal proof`](historical/superpowers/plans/2026-07-12-scenes-7-9-editorial-focal-proof.md).
|
||||||
@@ -287,12 +288,14 @@ separate activity after these surfaces are stable.
|
|||||||
turns, and Deployment Send records a truthful local run request without
|
turns, and Deployment Send records a truthful local run request without
|
||||||
execution or RPC. Implementation:
|
execution or RPC. Implementation:
|
||||||
[`Scene 9 staged message box`](historical/superpowers/plans/2026-07-12-scene-9-staged-message-box.md).
|
[`Scene 9 staged message box`](historical/superpowers/plans/2026-07-12-scene-9-staged-message-box.md).
|
||||||
The boundary is explicit: Scenes 10–12 own run activation, approval,
|
The boundary is explicit: the compact demo footer may start the prepared run
|
||||||
resume, output, and trace evidence.
|
from Scenes 8–12, while Scenes 10–12 own run, decision, resume, output, and
|
||||||
3. **Completed: Live/replay truth and run activation:** Scene 10 exposes the
|
trace evidence.
|
||||||
prepared-run action outside the hidden chat, retries the existing health
|
3. **Completed: Live/replay truth and run activation:** the compact footer rail
|
||||||
probe, starts live execution explicitly, and keeps direct links and failed
|
across Scenes 8–12 exposes the prepared-run action, retries the existing
|
||||||
services on truthful replay evidence. Implementation:
|
health probe, starts live execution explicitly, and keeps direct links and
|
||||||
|
failed services on truthful replay evidence. Scene 10 remains the semantic
|
||||||
|
start of execution evidence. Implementation:
|
||||||
[`presentation live/replay activation`](historical/superpowers/plans/2026-07-12-presentation-live-replay-activation.md).
|
[`presentation live/replay activation`](historical/superpowers/plans/2026-07-12-presentation-live-replay-activation.md).
|
||||||
4. **Completed: Scene 11 compression:** reduce the typed-human-boundary scene to two
|
4. **Completed: Scene 11 compression:** reduce the typed-human-boundary scene to two
|
||||||
beats: interrupt context and approval decision. Cancellation remains a
|
beats: interrupt context and approval decision. Cancellation remains a
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ can be shown after selecting replay with the runbook's session-storage switch.
|
|||||||
| `positioning/landscape` | Related systems solve different parts of the tool, script, workflow, and agent problem. | Related-systems landscape | Problem contracts | hidden | replay-only; Thesis Positioning and Related Systems | `positioning/landscape` |
|
| `positioning/landscape` | Related systems solve different parts of the tool, script, workflow, and agent problem. | Related-systems landscape | Problem contracts | hidden | replay-only; Thesis Positioning and Related Systems | `positioning/landscape` |
|
||||||
| `positioning/lda-position` | lda.chat occupies the typed, provider-neutral workflow substrate niche for external agents. | lda.chat position map | Related-systems landscape | hidden | replay-only; Thesis Positioning and Related Systems | `positioning/lda-position` |
|
| `positioning/lda-position` | lda.chat occupies the typed, provider-neutral workflow substrate niche for external agents. | lda.chat position map | Related-systems landscape | hidden | replay-only; Thesis Positioning and Related Systems | `positioning/lda-position` |
|
||||||
| `planner-runtime/planner` | An external planner proposes and revises workflow structure. | Planner side of boundary | Typed operations | hidden | replay-only; Thesis Architecture Overview | `planner-runtime/planner` |
|
| `planner-runtime/planner` | An external planner proposes and revises workflow structure. | Planner side of boundary | Typed operations | hidden | replay-only; Thesis Architecture Overview | `planner-runtime/planner` |
|
||||||
| `planner-runtime/runtime` | The runtime validates, executes, records, and resumes the proposal deterministically. | Runtime side of boundary | Lifecycle records | hidden | replay-only; Thesis Architecture Overview | `planner-runtime/runtime` |
|
| `planner-runtime/runtime` | For fixed handler results, the core validates, executes, records, and resumes the proposal deterministically. | Runtime side of boundary | Lifecycle records | hidden | replay-only; Thesis Architecture Overview | `planner-runtime/runtime` |
|
||||||
| `planner-runtime/boundary` | Typed CLI and JSON-RPC operations make the planner/runtime boundary explicit. | Boundary diagram | CLI and RPC surface | hidden | replay-only; Thesis Architecture Overview | `planner-runtime/boundary` |
|
| `planner-runtime/boundary` | Typed CLI and JSON-RPC operations make the planner/runtime boundary explicit. | Boundary diagram | CLI and RPC surface | hidden | replay-only; Thesis Architecture Overview | `planner-runtime/boundary` |
|
||||||
| `lifecycle/draft` | Draft is mutable iterative authoring state. | Lifecycle stage: Draft | State transition strip | hidden | replay-only; Thesis Workflow Lifecycle | `lifecycle/draft` |
|
| `lifecycle/draft` | Draft is mutable iterative authoring state. | Lifecycle stage: Draft | State transition strip | hidden | replay-only; Thesis Workflow Lifecycle | `lifecycle/draft` |
|
||||||
| `lifecycle/artifact` | Artifact is the immutable saved workflow definition. | Lifecycle stage: Artifact | State transition strip | hidden | replay-only; Thesis Workflow Lifecycle | `lifecycle/artifact` |
|
| `lifecycle/artifact` | Artifact is the immutable saved workflow definition. | Lifecycle stage: Artifact | State transition strip | hidden | replay-only; Thesis Workflow Lifecycle | `lifecycle/artifact` |
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ pnpm --filter @lda/console build
|
|||||||
3. 1280x720 uses a 1280x720 logical canvas
|
3. 1280x720 uses a 1280x720 logical canvas
|
||||||
4. Ratios outside 4:3 through 16:9 letterbox at the nearest supported ratio
|
4. Ratios outside 4:3 through 16:9 letterbox at the nearest supported ratio
|
||||||
5. Warm Editorial Canvas unchanged across Scene 1 and Scene 6
|
5. Warm Editorial Canvas unchanged across Scene 1 and Scene 6
|
||||||
6. No scene rail, replay label, discussion button, or presenter controls
|
6. Non-demo routes have no demo rail, replay label, discussion button, or presenter controls; Scenes 8-12 retain the approved compact demo footer rail
|
||||||
7. Scene 6 root has one primary figure and no competing card grid
|
7. Scene 6 root has one primary figure and no competing card grid
|
||||||
8. Runtime & providers expands in place with breadcrumb
|
8. Runtime & providers expands in place with breadcrumb
|
||||||
9. Configured providers expands second level
|
9. Configured providers expands second level
|
||||||
|
|||||||
@@ -26,9 +26,10 @@ when `wf-rpc-server` is running.
|
|||||||
|
|
||||||
### Launch surface
|
### Launch surface
|
||||||
|
|
||||||
Expose a compact prepared-run control on Scene 10's `operation` beat. It is a
|
Expose one compact prepared-run control in `PresentationFooter` throughout
|
||||||
scene-owned control, not a reintroduced chat rail. It must remain visible while
|
Scenes 8-12. It is demo chrome, not a reintroduced chat rail or a scene-owned
|
||||||
the target is checking, ready, failed, or the live run is active.
|
launch panel. Scene 10 remains the semantic execution start even when the
|
||||||
|
presenter starts or prepares the run from an earlier demo scene.
|
||||||
|
|
||||||
The control communicates the current action:
|
The control communicates the current action:
|
||||||
|
|
||||||
@@ -83,8 +84,8 @@ discussion views should not gain a persistent live-service badge or run action.
|
|||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
||||||
1. With `wf-rpc-server` and the web server running, Scene 10's operation beat
|
1. With `wf-rpc-server` and the web server running, the compact footer rail on
|
||||||
visibly exposes `Run prepared workflow` and starts a live timeline.
|
Scenes 8-12 exposes `Run prepared workflow` and starts a live timeline.
|
||||||
2. A live run records deployment inspection, run start, interrupt, resume, and
|
2. A live run records deployment inspection, run start, interrupt, resume, and
|
||||||
trace evidence using the existing `callOperation` path.
|
trace evidence using the existing `callOperation` path.
|
||||||
3. The typed approval form remains the only way to provide the resume decision;
|
3. The typed approval form remains the only way to provide the resume decision;
|
||||||
@@ -97,8 +98,9 @@ discussion views should not gain a persistent live-service badge or run action.
|
|||||||
recovered with retry, without reloading the page or reconnecting manually.
|
recovered with retry, without reloading the page or reconnecting manually.
|
||||||
7. The launch control cannot issue duplicate starts while an operation is in
|
7. The launch control cannot issue duplicate starts while an operation is in
|
||||||
flight or a live run is already active.
|
flight or a live run is already active.
|
||||||
8. Scene 8 and Scene 9 remain deterministic authoring replay and do not call
|
8. Scene 8 chat Send and Scene 9 staged messages remain deterministic authoring
|
||||||
workflow authoring RPC operations.
|
replay and do not call workflow authoring RPC operations. Their separate
|
||||||
|
footer control may start the prepared run.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
|
|||||||
@@ -44,8 +44,10 @@ The old five-step phase rail is removed from Scene 8. The lifecycle sequence is
|
|||||||
shown by Scene 9, where the authoring canvas is the primary artifact and the
|
shown by Scene 9, where the authoring canvas is the primary artifact and the
|
||||||
conversation becomes a secondary modal.
|
conversation becomes a secondary modal.
|
||||||
|
|
||||||
The standalone `Run prepared workflow` action is removed from Scene 8. The
|
The standalone in-scene `Run prepared workflow` action is removed from Scene 8.
|
||||||
workflow-run action belongs to Slice 3 and will be exposed in Scene 10.
|
The later demo-chrome slice owns one compact footer rail across Scenes 8-12.
|
||||||
|
That separate footer may expose the prepared-run action, while chat Send remains
|
||||||
|
local and never starts a workflow.
|
||||||
|
|
||||||
### Chat Surface Boundary
|
### Chat Surface Boundary
|
||||||
|
|
||||||
@@ -77,7 +79,8 @@ On the Scene 8 request beat, render:
|
|||||||
- a composer with the request text prefilled:
|
- a composer with the request text prefilled:
|
||||||
`We need to author a report workflow for the lda_report scenario. What sources and capabilities are available?`;
|
`We need to author a report workflow for the lda_report scenario. What sources and capabilities are available?`;
|
||||||
- an enabled Send button;
|
- an enabled Send button;
|
||||||
- no workflow operation, run ID, deployment ID, or live target action.
|
- no workflow operation, run ID, deployment ID, or live target action inside
|
||||||
|
the chat surface. The separate demo footer rail may show target and run state.
|
||||||
|
|
||||||
The prefilled text is editable. The scene does not require the presenter to
|
The prefilled text is editable. The scene does not require the presenter to
|
||||||
retype the request during the defense.
|
retype the request during the defense.
|
||||||
@@ -110,8 +113,9 @@ The Scene 8 entry state is local presentation state. Reloading the request beat
|
|||||||
returns to the empty composer. The canonical authoring recording remains the
|
returns to the empty composer. The canonical authoring recording remains the
|
||||||
source for all revealed turns.
|
source for all revealed turns.
|
||||||
|
|
||||||
Scene 8 must work with no workflow server and must not change the live/replay
|
Scene 8 chat must work with no workflow server and must not change live/replay
|
||||||
target badge. The target badge remains scoped by the later demo truth slice.
|
state. The later demo truth slice owns the separate footer badge and launch
|
||||||
|
control across the demo arc.
|
||||||
|
|
||||||
### Responsive Behavior
|
### Responsive Behavior
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
param(
|
param(
|
||||||
[string]$BaseUrl = "http://127.0.0.1:5173",
|
[string]$BaseUrl = "http://127.0.0.1:5173",
|
||||||
[string]$OutputRoot = "web/apps/console/.visual-smoke/rehearsal",
|
[string]$OutputRoot = "web/apps/console/.visual-smoke/rehearsal",
|
||||||
[string[]]$Viewports = @("1280,720", "1024,768")
|
[string[]]$Viewports = @("1280,720", "1024,768"),
|
||||||
|
[string]$ReadySelector = 'main[data-presentation-ready="true"]'
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -40,11 +41,17 @@ foreach ($Viewport in $Viewports) {
|
|||||||
Write-Host "$Viewport $Url -> $OutputPath"
|
Write-Host "$Viewport $Url -> $OutputPath"
|
||||||
& pnpx --yes playwright screenshot `
|
& pnpx --yes playwright screenshot `
|
||||||
--viewport-size $Viewport `
|
--viewport-size $Viewport `
|
||||||
--wait-for-timeout 800 `
|
--wait-for-selector $ReadySelector `
|
||||||
$Url `
|
$Url `
|
||||||
$OutputPath
|
$OutputPath
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
throw "Screenshot capture failed for $Url at $Viewport (exit code $LASTEXITCODE)"
|
throw "Screenshot capture failed for $Url at $Viewport (exit code $LASTEXITCODE)"
|
||||||
}
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $OutputPath -PathType Leaf)) {
|
||||||
|
throw "Screenshot command reported success but did not write $OutputPath"
|
||||||
|
}
|
||||||
|
if ((Get-Item -LiteralPath $OutputPath).Length -le 0) {
|
||||||
|
throw "Screenshot command wrote an empty PNG: $OutputPath"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,34 @@ describe("useTimelineAgent", () => {
|
|||||||
expect(start).toHaveBeenCalledWith("live");
|
expect(start).toHaveBeenCalledWith("live");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not advertise live launch when the timeline cannot start", () => {
|
||||||
|
const demo = demoController({ canStart: false });
|
||||||
|
const { result } = renderHook(() => useTimelineAgent(demo, {
|
||||||
|
mode: "live",
|
||||||
|
status: readyStatus,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(result.current.canRunLive).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses target health for an explicit live launch regardless of selected mode", async () => {
|
||||||
|
const start = vi.fn();
|
||||||
|
const demo = demoController({
|
||||||
|
state: { ...initialDemoTimelineState, mode: "replay" },
|
||||||
|
start,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useTimelineAgent(demo, {
|
||||||
|
mode: "replay",
|
||||||
|
status: readyStatus,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(result.current.canRunLive).toBe(true);
|
||||||
|
await act(async () => result.current.runPreparedWorkflow("live"));
|
||||||
|
|
||||||
|
expect(start).toHaveBeenCalledWith("live");
|
||||||
|
});
|
||||||
|
|
||||||
it("does not offer an explicit live launch when health has failed", () => {
|
it("does not offer an explicit live launch when health has failed", () => {
|
||||||
const start = vi.fn();
|
const start = vi.fn();
|
||||||
const demo = demoController({ start });
|
const demo = demoController({ start });
|
||||||
|
|||||||
@@ -91,9 +91,15 @@ export const useTimelineAgent = (
|
|||||||
|
|
||||||
const runLabel = modeLabel === "live" ? "Run prepared workflow" : "Run replay walkthrough";
|
const runLabel = modeLabel === "live" ? "Run prepared workflow" : "Run replay walkthrough";
|
||||||
const canRun = demo.canStart && !demo.inFlight && demo.state.phase !== "running";
|
const canRun = demo.canStart && !demo.inFlight && demo.state.phase !== "running";
|
||||||
const canRunLive = (options.liveTargetReady ?? (options.mode === "live" && (
|
// Live capability is about the target and timeline, not the selected display
|
||||||
|
// mode; callers can request a live run from a replay-backed slide explicitly.
|
||||||
|
const liveTargetReady = options.liveTargetReady ?? (
|
||||||
options.status.kind === "ready" || options.status.kind === "active"
|
options.status.kind === "ready" || options.status.kind === "active"
|
||||||
))) && !demo.inFlight && demo.state.phase !== "running";
|
);
|
||||||
|
const canRunLive = liveTargetReady
|
||||||
|
&& demo.canStart
|
||||||
|
&& !demo.inFlight
|
||||||
|
&& demo.state.phase !== "running";
|
||||||
|
|
||||||
const runPreparedWorkflow = useCallback(async (requestedMode?: TimelineAgentMode) => {
|
const runPreparedWorkflow = useCallback(async (requestedMode?: TimelineAgentMode) => {
|
||||||
const mode = requestedMode ?? modeLabel;
|
const mode = requestedMode ?? modeLabel;
|
||||||
|
|||||||
@@ -111,6 +111,15 @@ describe("PresentationRoute", () => {
|
|||||||
15000,
|
15000,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it("exposes an application-owned readiness signal for rehearsal capture", async () => {
|
||||||
|
window.location.hash = "#scene/thesis/title";
|
||||||
|
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||||
|
render(<PresentationRoute />);
|
||||||
|
|
||||||
|
const route = await screen.findByRole("main", { name: /lda\.chat presentation/i });
|
||||||
|
await waitFor(() => expect(route).toHaveAttribute("data-presentation-ready", "true"));
|
||||||
|
});
|
||||||
|
|
||||||
const visualRouteContracts = [
|
const visualRouteContracts = [
|
||||||
{ hash: "#scene/thesis/title", heading: "Design and Implementation of lda.chat", primary: "thesis opening" },
|
{ hash: "#scene/thesis/title", heading: "Design and Implementation of lda.chat", primary: "thesis opening" },
|
||||||
{ hash: "#scene/lifecycle/draft", heading: "Workflow Lifecycle", primary: "workflow lifecycle rail" },
|
{ hash: "#scene/lifecycle/draft", heading: "Workflow Lifecycle", primary: "workflow lifecycle rail" },
|
||||||
|
|||||||
@@ -43,6 +43,29 @@ export const PresentationRoute = () => {
|
|||||||
{ type: "jump_hash", hash: initialHash },
|
{ type: "jump_hash", hash: initialHash },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const [presentationReady, setPresentationReady] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
let firstFrame = 0;
|
||||||
|
let secondFrame = 0;
|
||||||
|
setPresentationReady(false);
|
||||||
|
// Font metrics and two paint opportunities let measured diagrams settle
|
||||||
|
// before the rehearsal runner accepts the route as screenshot-ready.
|
||||||
|
const markReady = async () => {
|
||||||
|
await (document.fonts?.ready ?? Promise.resolve());
|
||||||
|
if (cancelled) return;
|
||||||
|
firstFrame = window.requestAnimationFrame(() => {
|
||||||
|
secondFrame = window.requestAnimationFrame(() => setPresentationReady(true));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
void markReady();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
window.cancelAnimationFrame(firstFrame);
|
||||||
|
window.cancelAnimationFrame(secondFrame);
|
||||||
|
};
|
||||||
|
}, [state.location]);
|
||||||
|
|
||||||
const recording = useMemo(() => loadCanonicalDemoRecording(), []);
|
const recording = useMemo(() => loadCanonicalDemoRecording(), []);
|
||||||
const replayEvidence = useMemo(() => projectRecordingToEvidence(recording), [recording]);
|
const replayEvidence = useMemo(() => projectRecordingToEvidence(recording), [recording]);
|
||||||
@@ -260,7 +283,12 @@ export const PresentationRoute = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="presentation-route" aria-label="lda.chat presentation" data-motion={state.motionDisabled ? "disabled" : "enabled"}>
|
<main
|
||||||
|
className="presentation-route"
|
||||||
|
aria-label="lda.chat presentation"
|
||||||
|
data-motion={state.motionDisabled ? "disabled" : "enabled"}
|
||||||
|
data-presentation-ready={presentationReady ? "true" : "false"}
|
||||||
|
>
|
||||||
<PresentationCanvas>
|
<PresentationCanvas>
|
||||||
<PresentationStage
|
<PresentationStage
|
||||||
state={state}
|
state={state}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import { cleanup, render, screen } from "@testing-library/react";
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import { SceneProgress } from "./SceneProgress.js";
|
import { SceneProgress, shouldShowBeatCounter } from "./SceneProgress.js";
|
||||||
import type { MainLocation } from "./storyboard.js";
|
import type { MainLocation } from "./storyboard.js";
|
||||||
|
|
||||||
afterEach(() => cleanup());
|
afterEach(() => cleanup());
|
||||||
|
|
||||||
describe("SceneProgress", () => {
|
describe("SceneProgress", () => {
|
||||||
|
it("uses scene metadata for a single-beat counter", () => {
|
||||||
|
expect(shouldShowBeatCounter({ alwaysShowBeatCounter: true }, 1)).toBe(true);
|
||||||
|
expect(shouldShowBeatCounter({ alwaysShowBeatCounter: false }, 1)).toBe(false);
|
||||||
|
expect(shouldShowBeatCounter(undefined, 2)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("shows scene and beat position for architecture/runtime", () => {
|
it("shows scene and beat position for architecture/runtime", () => {
|
||||||
const location: MainLocation = {
|
const location: MainLocation = {
|
||||||
kind: "main",
|
kind: "main",
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { findScene, mainScenes, type MainLocation } from "./storyboard.js";
|
import { findScene, mainScenes, type MainLocation } from "./storyboard.js";
|
||||||
|
import type { SceneDefinition } from "./storyboard.js";
|
||||||
|
|
||||||
type SceneProgressProps = {
|
type SceneProgressProps = {
|
||||||
readonly location: MainLocation;
|
readonly location: MainLocation;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const shouldShowBeatCounter = (
|
||||||
|
scene: Pick<SceneDefinition, "alwaysShowBeatCounter"> | undefined,
|
||||||
|
totalBeats: number,
|
||||||
|
): boolean => totalBeats > 1 || scene?.alwaysShowBeatCounter === true;
|
||||||
|
|
||||||
export const SceneProgress = ({ location }: SceneProgressProps) => {
|
export const SceneProgress = ({ location }: SceneProgressProps) => {
|
||||||
const scene = findScene(location.sceneId);
|
const scene = findScene(location.sceneId);
|
||||||
const sceneIndex = mainScenes.findIndex((s) => s.id === location.sceneId);
|
const sceneIndex = mainScenes.findIndex((s) => s.id === location.sceneId);
|
||||||
@@ -18,7 +24,7 @@ export const SceneProgress = ({ location }: SceneProgressProps) => {
|
|||||||
{sceneIndex + 1} / {totalScenes}
|
{sceneIndex + 1} / {totalScenes}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{(totalBeats > 1 || location.sceneId === "agent-handoff") && (
|
{shouldShowBeatCounter(scene, totalBeats) && (
|
||||||
<span className="scene-progress__beat">
|
<span className="scene-progress__beat">
|
||||||
{beatIndex >= 0 ? beatIndex + 1 : 1} / {totalBeats}
|
{beatIndex >= 0 ? beatIndex + 1 : 1} / {totalBeats}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+34
-4
@@ -1,15 +1,16 @@
|
|||||||
import { cleanup, render, screen } from "@testing-library/react";
|
import { cleanup, render, screen, within } from "@testing-library/react";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { findBeat, findScene } from "../storyboard.js";
|
import { findBeat, findScene } from "../storyboard.js";
|
||||||
import { PreparedAuthoringLifecycleScene } from "./PreparedAuthoringLifecycleScene.js";
|
import { PreparedAuthoringLifecycleScene } from "./PreparedAuthoringLifecycleScene.js";
|
||||||
|
|
||||||
afterEach(() => cleanup());
|
afterEach(() => cleanup());
|
||||||
|
|
||||||
const renderBeat = (beatId: string) => {
|
const renderBeat = (beatId: string, onAdvance?: () => void) => {
|
||||||
const scene = findScene("prepared-lifecycle");
|
const scene = findScene("prepared-lifecycle");
|
||||||
const beat = findBeat("prepared-lifecycle", beatId);
|
const beat = findBeat("prepared-lifecycle", beatId);
|
||||||
if (!scene || !beat) throw new Error(`missing prepared-lifecycle/${beatId}`);
|
if (!scene || !beat) throw new Error(`missing prepared-lifecycle/${beatId}`);
|
||||||
return render(<PreparedAuthoringLifecycleScene scene={scene} beat={beat} />);
|
return render(<PreparedAuthoringLifecycleScene scene={scene} beat={beat} onAdvance={onAdvance} />);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("PreparedAuthoringLifecycleScene", () => {
|
describe("PreparedAuthoringLifecycleScene", () => {
|
||||||
@@ -85,6 +86,35 @@ describe("PreparedAuthoringLifecycleScene", () => {
|
|||||||
expect(workspace.querySelectorAll('[data-visual-role="lifecycle-primary"]')).toHaveLength(1);
|
expect(workspace.querySelectorAll('[data-visual-role="lifecycle-primary"]')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("projects a custom discover submission into the prepared conversation", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderBeat("discover");
|
||||||
|
|
||||||
|
const input = screen.getByRole("textbox", { name: /message to authoring assistant/i });
|
||||||
|
await user.type(input, "Inspect the report source first.");
|
||||||
|
await user.click(screen.getByRole("button", { name: /send message/i }));
|
||||||
|
|
||||||
|
const conversation = screen.getByRole("log", { name: "prepared authoring conversation" });
|
||||||
|
expect(within(conversation).getByText("Inspect the report source first.")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["draft", true],
|
||||||
|
["artifact", true],
|
||||||
|
["validate", false],
|
||||||
|
["deployment", false],
|
||||||
|
] as const)("%s submission advances only when its beat owns the transition", async (beatId, advances) => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onAdvance = vi.fn();
|
||||||
|
renderBeat(beatId, onAdvance);
|
||||||
|
|
||||||
|
const input = screen.getByRole("textbox", { name: /message to authoring assistant/i });
|
||||||
|
if (beatId === "validate") await user.type(input, "Review the validation result.");
|
||||||
|
await user.click(screen.getByRole("button", { name: /send message/i }));
|
||||||
|
|
||||||
|
expect(onAdvance).toHaveBeenCalledTimes(advances ? 1 : 0);
|
||||||
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
["discover", "Discover"],
|
["discover", "Discover"],
|
||||||
["draft", "Draft"],
|
["draft", "Draft"],
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ export const PreparedAuthoringLifecycleScene = ({ scene, beat, onAdvance }: Prep
|
|||||||
onDraftChange={(draft) => dispatch({ type: "draft_edited", draft })}
|
onDraftChange={(draft) => dispatch({ type: "draft_edited", draft })}
|
||||||
onSubmit={(submittedText) => {
|
onSubmit={(submittedText) => {
|
||||||
dispatch({ type: "draft_edited", draft: submittedText });
|
dispatch({ type: "draft_edited", draft: submittedText });
|
||||||
|
if (beatId === "discover") {
|
||||||
|
dispatch({ type: "discover_submitted" });
|
||||||
|
}
|
||||||
if (beatId === "draft") {
|
if (beatId === "draft") {
|
||||||
dispatch({ type: "draft_submitted" });
|
dispatch({ type: "draft_submitted" });
|
||||||
onAdvance?.();
|
onAdvance?.();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState, type FormEvent, type KeyboardEvent } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button } from "../../components/ui/button.js";
|
import { Button } from "../../components/ui/button.js";
|
||||||
import { Textarea } from "../../components/ui/textarea.js";
|
import { Textarea } from "../../components/ui/textarea.js";
|
||||||
import { AuthoringConversation } from "./AuthoringConversation.js";
|
import { AuthoringConversation } from "./AuthoringConversation.js";
|
||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
Scene9MessageProjection,
|
Scene9MessageProjection,
|
||||||
Scene9SubmittedOverrides,
|
Scene9SubmittedOverrides,
|
||||||
} from "./scene9-message-state.js";
|
} from "./scene9-message-state.js";
|
||||||
|
import { PREPARED_COMPOSER_HELP, usePreparedComposerSubmit } from "./usePreparedComposerSubmit.js";
|
||||||
|
|
||||||
export type PresentationAssistantPaneProps = {
|
export type PresentationAssistantPaneProps = {
|
||||||
readonly phase: AuthoringPhaseId;
|
readonly phase: AuthoringPhaseId;
|
||||||
@@ -52,16 +53,10 @@ export const PresentationAssistantPane = ({
|
|||||||
setDraft(nextDraft);
|
setDraft(nextDraft);
|
||||||
onDraftChange(nextDraft);
|
onDraftChange(nextDraft);
|
||||||
};
|
};
|
||||||
const submit = (event?: FormEvent<HTMLFormElement>) => {
|
const { submit, handleKeyDown } = usePreparedComposerSubmit(
|
||||||
event?.preventDefault();
|
canSubmit,
|
||||||
if (canSubmit) onSubmit(draft);
|
() => onSubmit(draft),
|
||||||
};
|
);
|
||||||
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (event.key === "Enter" && !event.shiftKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
submit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
@@ -104,7 +99,7 @@ export const PresentationAssistantPane = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p id="scene9-authoring-message-help" className="presentation-assistant-pane__composer-help">
|
<p id="scene9-authoring-message-help" className="presentation-assistant-pane__composer-help">
|
||||||
Shift+Enter adds a new line. This is a deterministic prepared replay, not a live model request.
|
{PREPARED_COMPOSER_HELP}
|
||||||
</p>
|
</p>
|
||||||
{runRequested !== null ? (
|
{runRequested !== null ? (
|
||||||
<p role="status" className="presentation-assistant-pane__run-status">
|
<p role="status" className="presentation-assistant-pane__run-status">
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { FormEvent } from "react";
|
|
||||||
import { Button } from "../../components/ui/button.js";
|
import { Button } from "../../components/ui/button.js";
|
||||||
import { Textarea } from "../../components/ui/textarea.js";
|
import { Textarea } from "../../components/ui/textarea.js";
|
||||||
import { AuthoringConversation } from "./AuthoringConversation.js";
|
import { AuthoringConversation } from "./AuthoringConversation.js";
|
||||||
@@ -7,6 +6,7 @@ import {
|
|||||||
type Scene8EntryAction,
|
type Scene8EntryAction,
|
||||||
type Scene8EntryState,
|
type Scene8EntryState,
|
||||||
} from "./scene8-entry-state.js";
|
} from "./scene8-entry-state.js";
|
||||||
|
import { PREPARED_COMPOSER_HELP, usePreparedComposerSubmit } from "./usePreparedComposerSubmit.js";
|
||||||
|
|
||||||
type Scene8ChatEntryProps = {
|
type Scene8ChatEntryProps = {
|
||||||
readonly state: Scene8EntryState;
|
readonly state: Scene8EntryState;
|
||||||
@@ -17,11 +17,10 @@ type Scene8ChatEntryProps = {
|
|||||||
export const Scene8ChatEntry = ({ state, dispatch }: Scene8ChatEntryProps) => {
|
export const Scene8ChatEntry = ({ state, dispatch }: Scene8ChatEntryProps) => {
|
||||||
const submitted = state.phase === "submitted";
|
const submitted = state.phase === "submitted";
|
||||||
const canSubmit = canSubmitScene8Entry(state);
|
const canSubmit = canSubmitScene8Entry(state);
|
||||||
|
const { submit, handleKeyDown } = usePreparedComposerSubmit(
|
||||||
const submit = (event?: FormEvent<HTMLFormElement>) => {
|
canSubmit,
|
||||||
event?.preventDefault();
|
() => dispatch({ type: "submit" }),
|
||||||
if (canSubmit) dispatch({ type: "submit" });
|
);
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
@@ -43,12 +42,7 @@ export const Scene8ChatEntry = ({ state, dispatch }: Scene8ChatEntryProps) => {
|
|||||||
value={state.draft}
|
value={state.draft}
|
||||||
disabled={submitted}
|
disabled={submitted}
|
||||||
onChange={(event) => dispatch({ type: "draft_changed", draft: event.target.value })}
|
onChange={(event) => dispatch({ type: "draft_changed", draft: event.target.value })}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={handleKeyDown}
|
||||||
if (event.key === "Enter" && !event.shiftKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
submit();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
aria-describedby="scene8-authoring-request-help"
|
aria-describedby="scene8-authoring-request-help"
|
||||||
/>
|
/>
|
||||||
<div className="agent-handoff-scene__composer-actions">
|
<div className="agent-handoff-scene__composer-actions">
|
||||||
@@ -57,7 +51,7 @@ export const Scene8ChatEntry = ({ state, dispatch }: Scene8ChatEntryProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p id="scene8-authoring-request-help" className="agent-handoff-scene__composer-help">
|
<p id="scene8-authoring-request-help" className="agent-handoff-scene__composer-help">
|
||||||
Shift+Enter adds a new line. This is a deterministic prepared replay, not a live model request.
|
{PREPARED_COMPOSER_HELP}
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
{submitted ? (
|
{submitted ? (
|
||||||
|
|||||||
@@ -205,4 +205,20 @@ describe("projectPreparedAuthoringThread", () => {
|
|||||||
canonical.filter((message) => message.id.endsWith("-tools")),
|
canonical.filter((message) => message.id.endsWith("-tools")),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps each staged override scoped to its phase", () => {
|
||||||
|
const messages = projectPreparedAuthoringThread("deployment", undefined, {
|
||||||
|
validate: "Edited validation request",
|
||||||
|
deployment: "Edited deployment request",
|
||||||
|
});
|
||||||
|
const userText = messages
|
||||||
|
.filter((message) => message.role === "user")
|
||||||
|
.map((message) => message.parts[0]?.type === "text" ? message.parts[0].text : "");
|
||||||
|
|
||||||
|
expect(userText).toEqual([
|
||||||
|
"We need to author a report workflow for the lda_report scenario. What sources and capabilities are available?",
|
||||||
|
"Edited validation request",
|
||||||
|
"Edited deployment request",
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -281,28 +281,40 @@ export const authoringToolGroupId = (phase: AuthoringPhaseId): string =>
|
|||||||
export const projectPreparedAuthoringThread = (
|
export const projectPreparedAuthoringThread = (
|
||||||
throughPhase: AuthoringPhaseId = "deployment",
|
throughPhase: AuthoringPhaseId = "deployment",
|
||||||
requestOverride?: string,
|
requestOverride?: string,
|
||||||
requestOverrides?: Readonly<Partial<Record<"validate" | "deployment", string>>>,
|
requestOverrides?: Readonly<Partial<Record<"discover" | "validate" | "deployment", string>>>,
|
||||||
): readonly AgentMessage[] => {
|
): readonly AgentMessage[] => {
|
||||||
const finalPhaseIndex = recording.findIndex(({ phase }) => phase === throughPhase);
|
const finalPhaseIndex = recording.findIndex(({ phase }) => phase === throughPhase);
|
||||||
if (finalPhaseIndex < 0) throw new Error(`unknown phase: ${throughPhase}`);
|
if (finalPhaseIndex < 0) throw new Error(`unknown phase: ${throughPhase}`);
|
||||||
|
|
||||||
let requestReplaced = false;
|
let requestReplaced = false;
|
||||||
|
const phaseOverrideApplied = new Set<AuthoringPhaseId>();
|
||||||
return recording.slice(0, finalPhaseIndex + 1).flatMap((phase) => {
|
return recording.slice(0, finalPhaseIndex + 1).flatMap((phase) => {
|
||||||
const phaseOverride =
|
const phaseOverride =
|
||||||
phase.phase === "validate"
|
phase.phase === "discover"
|
||||||
|
? requestOverrides?.discover
|
||||||
|
: phase.phase === "validate"
|
||||||
? requestOverrides?.validate
|
? requestOverrides?.validate
|
||||||
: phase.phase === "deployment"
|
: phase.phase === "deployment"
|
||||||
? requestOverrides?.deployment
|
? requestOverrides?.deployment
|
||||||
: undefined;
|
: undefined;
|
||||||
const conversation = phase.conversation.map((turn, index) => {
|
const conversation = phase.conversation.map((turn, index) => {
|
||||||
const shouldReplaceRequest =
|
let replacement: string | undefined;
|
||||||
turn.role === "user" &&
|
if (turn.role === "user" && phaseOverride !== undefined && !phaseOverrideApplied.has(phase.phase)) {
|
||||||
(phaseOverride !== undefined || (requestOverride !== undefined && !requestReplaced));
|
replacement = phaseOverride;
|
||||||
if (shouldReplaceRequest && phaseOverride === undefined) requestReplaced = true;
|
phaseOverrideApplied.add(phase.phase);
|
||||||
|
} else if (
|
||||||
|
turn.role === "user"
|
||||||
|
&& phaseOverride === undefined
|
||||||
|
&& requestOverride !== undefined
|
||||||
|
&& !requestReplaced
|
||||||
|
) {
|
||||||
|
replacement = requestOverride;
|
||||||
|
requestReplaced = true;
|
||||||
|
}
|
||||||
return agentTextMessage(
|
return agentTextMessage(
|
||||||
`authoring-${phase.phase}-message-${index}`,
|
`authoring-${phase.phase}-message-${index}`,
|
||||||
turn.role,
|
turn.role,
|
||||||
shouldReplaceRequest ? (phaseOverride ?? requestOverride ?? turn.text) : turn.text,
|
replacement ?? turn.text,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
const groupId = authoringToolGroupId(phase.phase);
|
const groupId = authoringToolGroupId(phase.phase);
|
||||||
|
|||||||
@@ -69,6 +69,19 @@ describe("scene 9 staged message state", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("stores discover submissions under the discover destination", () => {
|
||||||
|
const state = scene9MessageReducer(initialScene9MessageState, {
|
||||||
|
type: "draft_edited",
|
||||||
|
draft: "Inspect the report source first.",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(scene9MessageReducer(state, { type: "discover_submitted" })).toEqual({
|
||||||
|
draft: state.draft,
|
||||||
|
submittedOverrides: { discover: state.draft },
|
||||||
|
runRequested: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("ignores blank submits and keeps duplicate submits idempotent", () => {
|
it("ignores blank submits and keeps duplicate submits idempotent", () => {
|
||||||
const blank = { ...initialScene9MessageState, draft: " \n\t" };
|
const blank = { ...initialScene9MessageState, draft: " \n\t" };
|
||||||
expect(scene9MessageReducer(blank, { type: "draft_submitted" })).toBe(blank);
|
expect(scene9MessageReducer(blank, { type: "draft_submitted" })).toBe(blank);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { AuthoringPhaseId } from "./authoring-recording.js";
|
import type { AuthoringPhaseId } from "./authoring-recording.js";
|
||||||
|
|
||||||
export type Scene9MessagePhase = AuthoringPhaseId;
|
export type Scene9MessagePhase = AuthoringPhaseId;
|
||||||
export type Scene9DestinationPhase = "validate" | "deployment";
|
export type Scene9DestinationPhase = "discover" | "validate" | "deployment";
|
||||||
|
|
||||||
export const SCENE9_PHASE_PROMPTS: Readonly<Record<Scene9MessagePhase, string>> = {
|
export const SCENE9_PHASE_PROMPTS: Readonly<Record<Scene9MessagePhase, string>> = {
|
||||||
discover: "",
|
discover: "",
|
||||||
@@ -38,6 +38,7 @@ export const initialScene9MessageState: Scene9MessageState = {
|
|||||||
|
|
||||||
export type Scene9MessageAction =
|
export type Scene9MessageAction =
|
||||||
| { readonly type: "draft_edited"; readonly draft: string }
|
| { readonly type: "draft_edited"; readonly draft: string }
|
||||||
|
| { readonly type: "discover_submitted" }
|
||||||
| { readonly type: "draft_submitted" }
|
| { readonly type: "draft_submitted" }
|
||||||
| { readonly type: "artifact_submitted" }
|
| { readonly type: "artifact_submitted" }
|
||||||
| { readonly type: "run_requested" };
|
| { readonly type: "run_requested" };
|
||||||
@@ -66,6 +67,8 @@ export const scene9MessageReducer = (
|
|||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "draft_edited":
|
case "draft_edited":
|
||||||
return state.runRequested === null ? { ...state, draft: action.draft } : state;
|
return state.runRequested === null ? { ...state, draft: action.draft } : state;
|
||||||
|
case "discover_submitted":
|
||||||
|
return submitOverride(state, "discover");
|
||||||
case "draft_submitted":
|
case "draft_submitted":
|
||||||
return submitOverride(state, "validate");
|
return submitOverride(state, "validate");
|
||||||
case "artifact_submitted":
|
case "artifact_submitted":
|
||||||
@@ -92,7 +95,7 @@ export const projectScene9Message = (
|
|||||||
placeholder: SCENE9_PHASE_PLACEHOLDERS[phase],
|
placeholder: SCENE9_PHASE_PLACEHOLDERS[phase],
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Keeps the transcript-facing map limited to the two staged handoff destinations. */
|
/** Returns the transcript-facing request overrides by destination phase. */
|
||||||
export const projectScene9SubmittedOverrides = (
|
export const projectScene9SubmittedOverrides = (
|
||||||
state: Scene9MessageState,
|
state: Scene9MessageState,
|
||||||
): Scene9SubmittedOverrides => state.submittedOverrides;
|
): Scene9SubmittedOverrides => state.submittedOverrides;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import type { FormEvent, KeyboardEvent } from "react";
|
||||||
|
|
||||||
|
type PreparedComposerSubmitHandlers = {
|
||||||
|
readonly submit: (event?: FormEvent<HTMLFormElement>) => void;
|
||||||
|
readonly handleKeyDown: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PREPARED_COMPOSER_HELP =
|
||||||
|
"Shift+Enter adds a new line. This is a deterministic prepared replay, not a live model request.";
|
||||||
|
|
||||||
|
/** Shares the prepared-replay composer keyboard and form submission contract. */
|
||||||
|
export const usePreparedComposerSubmit = (
|
||||||
|
canSubmit: boolean,
|
||||||
|
onSubmit: () => void,
|
||||||
|
): PreparedComposerSubmitHandlers => {
|
||||||
|
const submit = (event?: FormEvent<HTMLFormElement>) => {
|
||||||
|
event?.preventDefault();
|
||||||
|
if (canSubmit) onSubmit();
|
||||||
|
};
|
||||||
|
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (event.key === "Enter" && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
submit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { submit, handleKeyDown };
|
||||||
|
};
|
||||||
@@ -172,7 +172,13 @@ describe("AssistantOperatorThread", () => {
|
|||||||
|
|
||||||
it("can keep a first-stage group anchored at the start of the transcript", async () => {
|
it("can keep a first-stage group anchored at the start of the transcript", async () => {
|
||||||
const setScrollTop = vi.fn();
|
const setScrollTop = vi.fn();
|
||||||
const originalDescriptor = Object.getOwnPropertyDescriptor(HTMLDivElement.prototype, "scrollTop");
|
const descriptors = {
|
||||||
|
scrollTop: Object.getOwnPropertyDescriptor(HTMLDivElement.prototype, "scrollTop"),
|
||||||
|
scrollHeight: Object.getOwnPropertyDescriptor(HTMLDivElement.prototype, "scrollHeight"),
|
||||||
|
clientHeight: Object.getOwnPropertyDescriptor(HTMLDivElement.prototype, "clientHeight"),
|
||||||
|
offsetTop: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetTop"),
|
||||||
|
offsetHeight: Object.getOwnPropertyDescriptor(HTMLElement.prototype, "offsetHeight"),
|
||||||
|
};
|
||||||
const messages: ReadonlyArray<AgentMessage> = [
|
const messages: ReadonlyArray<AgentMessage> = [
|
||||||
{
|
{
|
||||||
id: "assistant-start-scroll-text",
|
id: "assistant-start-scroll-text",
|
||||||
@@ -192,10 +198,14 @@ describe("AssistantOperatorThread", () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Object.defineProperty(HTMLDivElement.prototype, "scrollTop", {
|
Object.defineProperties(HTMLDivElement.prototype, {
|
||||||
configurable: true,
|
scrollTop: { configurable: true, get: () => 0, set: setScrollTop },
|
||||||
get: () => 0,
|
scrollHeight: { configurable: true, get: () => 400 },
|
||||||
set: setScrollTop,
|
clientHeight: { configurable: true, get: () => 80 },
|
||||||
|
});
|
||||||
|
Object.defineProperties(HTMLElement.prototype, {
|
||||||
|
offsetTop: { configurable: true, get: () => 120 },
|
||||||
|
offsetHeight: { configurable: true, get: () => 40 },
|
||||||
});
|
});
|
||||||
render(
|
render(
|
||||||
<AssistantOperatorThread
|
<AssistantOperatorThread
|
||||||
@@ -209,10 +219,15 @@ describe("AssistantOperatorThread", () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(setScrollTop).toHaveBeenCalledWith(0));
|
await waitFor(() => expect(setScrollTop).toHaveBeenCalledWith(0));
|
||||||
} finally {
|
} finally {
|
||||||
if (originalDescriptor) {
|
for (const [name, descriptor] of Object.entries(descriptors)) {
|
||||||
Object.defineProperty(HTMLDivElement.prototype, "scrollTop", originalDescriptor);
|
const prototype = name === "offsetTop" || name === "offsetHeight"
|
||||||
} else {
|
? HTMLElement.prototype
|
||||||
delete (HTMLDivElement.prototype as { scrollTop?: number }).scrollTop;
|
: HTMLDivElement.prototype;
|
||||||
|
if (descriptor) {
|
||||||
|
Object.defineProperty(prototype, name, descriptor);
|
||||||
|
} else {
|
||||||
|
delete (prototype as unknown as Record<string, unknown>)[name];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,34 @@ import { describe, expect, it } from "vitest";
|
|||||||
const css = readFileSync(join(import.meta.dirname, "presentation.css"), "utf8").replace(/\r\n/g, "\n");
|
const css = readFileSync(join(import.meta.dirname, "presentation.css"), "utf8").replace(/\r\n/g, "\n");
|
||||||
const demoWorkflowCss = readFileSync(join(import.meta.dirname, "styles", "demo-workflow.css"), "utf8").replace(/\r\n/g, "\n");
|
const demoWorkflowCss = readFileSync(join(import.meta.dirname, "styles", "demo-workflow.css"), "utf8").replace(/\r\n/g, "\n");
|
||||||
|
|
||||||
|
const cssBlocks = (source: string, selector: string): readonly string[] => {
|
||||||
|
const blocks: string[] = [];
|
||||||
|
let searchFrom = 0;
|
||||||
|
while (searchFrom < source.length) {
|
||||||
|
const selectorStart = source.indexOf(selector, searchFrom);
|
||||||
|
if (selectorStart < 0) break;
|
||||||
|
const openingBrace = source.indexOf("{", selectorStart);
|
||||||
|
if (openingBrace < 0) break;
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
for (let index = openingBrace; index < source.length; index += 1) {
|
||||||
|
if (source[index] === "{") depth += 1;
|
||||||
|
if (source[index] !== "}") continue;
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) {
|
||||||
|
blocks.push(source.slice(openingBrace + 1, index));
|
||||||
|
searchFrom = index + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (searchFrom <= selectorStart) break;
|
||||||
|
}
|
||||||
|
return blocks;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cssBlock = (source: string, selector: string): string | undefined =>
|
||||||
|
cssBlocks(source, selector).at(-1);
|
||||||
|
|
||||||
describe("presentation.css", () => {
|
describe("presentation.css", () => {
|
||||||
it("keeps the demo footer rail compact and removes the old launch control", () => {
|
it("keeps the demo footer rail compact and removes the old launch control", () => {
|
||||||
const railBlock = css.match(/\.presentation-demo-rail\s*\{(?<body>[\s\S]*?)\n\}/)?.groups?.body;
|
const railBlock = css.match(/\.presentation-demo-rail\s*\{(?<body>[\s\S]*?)\n\}/)?.groups?.body;
|
||||||
@@ -80,16 +108,30 @@ describe("presentation.css", () => {
|
|||||||
|
|
||||||
it("bounds Scene 9 conversation scrolling and recenters Scene 8 at compact stage widths", () => {
|
it("bounds Scene 9 conversation scrolling and recenters Scene 8 at compact stage widths", () => {
|
||||||
expect(css.match(/\.presentation-stage\[data-scene-view="agent"\] \.presentation-stage__primary\s*\{/g)).toHaveLength(2);
|
expect(css.match(/\.presentation-stage\[data-scene-view="agent"\] \.presentation-stage__primary\s*\{/g)).toHaveLength(2);
|
||||||
expect(css).toMatch(
|
expect(cssBlocks(css, ".presentation-stage__primary > .agent-handoff-scene")
|
||||||
/\.presentation-stage__primary > \.agent-handoff-scene\s*\{[\s\S]*?margin-inline:\s*auto;/,
|
.some((body) => body.includes("margin-inline: auto;"))).toBe(true);
|
||||||
|
expect(cssBlocks(css, ".agent-handoff-scene__intro")
|
||||||
|
.some((body) => body.includes("width: min(calc(100% - 3rem), 72rem);"))).toBe(true);
|
||||||
|
expect(cssBlocks(css, ".agent-handoff-scene__composer")
|
||||||
|
.some((body) => body.includes("width: min(calc(100% - 3rem), 72rem);"))).toBe(true);
|
||||||
|
const conversation = cssBlock(
|
||||||
|
css,
|
||||||
|
'.prepared-lifecycle-scene[data-presentation-surface="editorial"] .presentation-assistant-pane__conversation',
|
||||||
);
|
);
|
||||||
expect(css).toMatch(/\.agent-handoff-scene__intro\s*\{[\s\S]*?width:\s*min\(calc\(100% - 3rem\), 72rem\);/);
|
expect(conversation).toContain("flex: 1 1 auto;");
|
||||||
expect(css).toMatch(/\.agent-handoff-scene__composer\s*\{[\s\S]*?width:\s*min\(calc\(100% - 3rem\), 72rem\);/);
|
expect(conversation).toContain("min-height: 0;");
|
||||||
expect(css).toMatch(
|
expect(conversation).toContain("overflow: auto;");
|
||||||
/\.prepared-lifecycle-scene\[data-presentation-surface="editorial"\] \.presentation-assistant-pane__conversation\s*\{[\s\S]*?flex:\s*1 1 auto;[\s\S]*?min-height:\s*0;[\s\S]*?overflow:\s*auto;/,
|
const compactAgentPrimary = cssBlock(
|
||||||
|
cssBlock(css, "@media (max-width: 1100px)") ?? "",
|
||||||
|
'.presentation-stage[data-scene-view="agent"] .presentation-stage__primary',
|
||||||
);
|
);
|
||||||
expect(css).toMatch(
|
expect(compactAgentPrimary).toContain("align-items: center;");
|
||||||
/@media \(max-width: 1100px\)[\s\S]*?\.presentation-stage\[data-scene-view="agent"\] \.presentation-stage__primary\s*\{[\s\S]*?align-items:\s*center;[\s\S]*?justify-content:\s*center;/,
|
expect(compactAgentPrimary).toContain("justify-content: center;");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not keep an empty findings campaign-strip ruleset", () => {
|
||||||
|
expect(css).not.toContain(
|
||||||
|
'.evaluation-board[data-evaluation-focus="findings"] .evaluation-board__campaign-strip {\n /*',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -188,11 +188,6 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.evaluation-board[data-evaluation-focus="findings"] .evaluation-board__campaign-strip {
|
|
||||||
/* grid-template-columns: minmax(12rem, 0.7fr) minmax(20rem, 1.3fr); */
|
|
||||||
/* difference between two beats -> shifting */
|
|
||||||
}
|
|
||||||
|
|
||||||
.evaluation-board[data-evaluation-focus="cohort"] .evaluation-board__cohort {
|
.evaluation-board[data-evaluation-focus="cohort"] .evaluation-board__cohort {
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
padding-block: 1.35rem 0.8rem;
|
padding-block: 1.35rem 0.8rem;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export type SceneDefinition = {
|
|||||||
readonly evidencePointer: string;
|
readonly evidencePointer: string;
|
||||||
readonly view: SceneView;
|
readonly view: SceneView;
|
||||||
readonly beats: readonly SceneBeatDefinition[];
|
readonly beats: readonly SceneBeatDefinition[];
|
||||||
|
readonly alwaysShowBeatCounter?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defineScenes = <const Scenes extends readonly SceneDefinition[]>(scenes: Scenes): Scenes => scenes;
|
const defineScenes = <const Scenes extends readonly SceneDefinition[]>(scenes: Scenes): Scenes => scenes;
|
||||||
@@ -158,6 +159,7 @@ export const mainScenes = defineScenes([
|
|||||||
claimClass: "implemented",
|
claimClass: "implemented",
|
||||||
evidencePointer: "Constrained demo agent and prepared replay recipe",
|
evidencePointer: "Constrained demo agent and prepared replay recipe",
|
||||||
view: "agent",
|
view: "agent",
|
||||||
|
alwaysShowBeatCounter: true,
|
||||||
beats: [
|
beats: [
|
||||||
sceneBeat("request", "Operator request", "A thin agent interface receives the report request.", { chatMode: "hidden", chatTheme: "light" }),
|
sceneBeat("request", "Operator request", "A thin agent interface receives the report request.", { chatMode: "hidden", chatTheme: "light" }),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user