feat: split authoring diagnosis and repair beats

This commit is contained in:
lda
2026-07-13 12:16:53 +07:00 Verified
parent 708445a8f1
commit 3eef7e35c0
7 changed files with 191 additions and 90 deletions
@@ -4,14 +4,18 @@ import {
projectPreparedAuthoringThread, projectPreparedAuthoringThread,
type AuthoringPhaseId, type AuthoringPhaseId,
} from "./authoring-recording.js"; } from "./authoring-recording.js";
import type { Scene9SubmittedOverrides } from "./scene9-message-state.js"; import {
recordingPhaseForStep,
type PreparedLifecycleStepId,
} from "./authoring-projection.js";
import type { PreparedLifecycleSubmittedOverrides } from "./prepared-lifecycle-message-state.js";
type AuthoringConversationProps = { type AuthoringConversationProps = {
readonly throughPhase: AuthoringPhaseId; readonly throughPhase: PreparedLifecycleStepId | AuthoringPhaseId;
readonly activePhase: AuthoringPhaseId; readonly activePhase: PreparedLifecycleStepId | AuthoringPhaseId;
readonly surface: "stage" | "dock"; readonly surface: "stage" | "dock";
readonly requestOverride?: string | undefined; readonly requestOverride?: string | undefined;
readonly requestOverrides?: Scene9SubmittedOverrides | undefined; readonly requestOverrides?: PreparedLifecycleSubmittedOverrides | undefined;
readonly scrollMode?: "active" | "start" | undefined; readonly scrollMode?: "active" | "start" | undefined;
}; };
@@ -27,8 +31,12 @@ export const AuthoringConversation = ({
<AssistantOperatorThread <AssistantOperatorThread
mode={surface === "stage" ? "full" : "dock"} mode={surface === "stage" ? "full" : "dock"}
surface={surface} surface={surface}
messages={projectPreparedAuthoringThread(throughPhase, requestOverride, requestOverrides)} messages={projectPreparedAuthoringThread(
activeToolGroupId={authoringToolGroupId(activePhase)} recordingPhaseForStep(throughPhase),
requestOverride,
requestOverrides,
)}
activeToolGroupId={authoringToolGroupId(recordingPhaseForStep(activePhase))}
scrollMode={scrollMode} scrollMode={scrollMode}
ariaLabel="prepared authoring conversation" ariaLabel="prepared authoring conversation"
/> />
@@ -4,16 +4,16 @@ import type { ComponentProps } from "react";
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { PresentationAssistantPane } from "./PresentationAssistantPane.js"; import { PresentationAssistantPane } from "./PresentationAssistantPane.js";
import { import {
SCENE9_PHASE_PLACEHOLDERS, PREPARED_LIFECYCLE_PHASE_PLACEHOLDERS,
SCENE9_PHASE_PROMPTS, PREPARED_LIFECYCLE_PHASE_PROMPTS,
initialScene9MessageState, initialPreparedLifecycleMessageState,
projectScene9Message, projectPreparedLifecycleMessage,
} from "./scene9-message-state.js"; } from "./prepared-lifecycle-message-state.js";
afterEach(cleanup); afterEach(cleanup);
const renderPane = ( const renderPane = (
phase: Parameters<typeof projectScene9Message>[1], phase: Parameters<typeof projectPreparedLifecycleMessage>[1],
options: Partial<ComponentProps<typeof PresentationAssistantPane>> = {}, options: Partial<ComponentProps<typeof PresentationAssistantPane>> = {},
) => { ) => {
const onDraftChange = options.onDraftChange ?? (() => undefined); const onDraftChange = options.onDraftChange ?? (() => undefined);
@@ -22,7 +22,7 @@ const renderPane = (
return render( return render(
<PresentationAssistantPane <PresentationAssistantPane
phase={phase} phase={phase}
message={projectScene9Message(initialScene9MessageState, phase)} message={projectPreparedLifecycleMessage(initialPreparedLifecycleMessageState, phase)}
submittedOverrides={{}} submittedOverrides={{}}
runRequested={null} runRequested={null}
onDraftChange={onDraftChange} onDraftChange={onDraftChange}
@@ -69,7 +69,7 @@ describe("PresentationAssistantPane", () => {
const input = screen.getByRole("textbox", { name: /message to authoring assistant/i }); const input = screen.getByRole("textbox", { name: /message to authoring assistant/i });
expect(input).toHaveValue(""); expect(input).toHaveValue("");
expect(input).toHaveAttribute("placeholder", SCENE9_PHASE_PLACEHOLDERS[phase]); expect(input).toHaveAttribute("placeholder", PREPARED_LIFECYCLE_PHASE_PLACEHOLDERS[phase]);
expect(screen.getByRole("button", { name: /send message/i })).toBeDisabled(); expect(screen.getByRole("button", { name: /send message/i })).toBeDisabled();
} }
}); });
@@ -80,7 +80,7 @@ describe("PresentationAssistantPane", () => {
renderPane(phase); renderPane(phase);
expect(screen.getByRole("textbox", { name: /message to authoring assistant/i })) expect(screen.getByRole("textbox", { name: /message to authoring assistant/i }))
.toHaveValue(SCENE9_PHASE_PROMPTS[phase]); .toHaveValue(PREPARED_LIFECYCLE_PHASE_PROMPTS[phase]);
} }
}); });
@@ -4,34 +4,37 @@ import { Textarea } from "../../components/ui/textarea.js";
import { AuthoringConversation } from "./AuthoringConversation.js"; import { AuthoringConversation } from "./AuthoringConversation.js";
import type { AuthoringPhaseId } from "./authoring-recording.js"; import type { AuthoringPhaseId } from "./authoring-recording.js";
import type { import type {
Scene9MessageProjection, PreparedLifecycleMessageProjection,
Scene9SubmittedOverrides, PreparedLifecycleSubmittedOverrides,
} from "./scene9-message-state.js"; } from "./prepared-lifecycle-message-state.js";
import type { PreparedLifecycleStepId } from "./authoring-projection.js";
import { PREPARED_COMPOSER_HELP, usePreparedComposerSubmit } from "./usePreparedComposerSubmit.js"; import { PREPARED_COMPOSER_HELP, usePreparedComposerSubmit } from "./usePreparedComposerSubmit.js";
export type PresentationAssistantPaneProps = { export type PresentationAssistantPaneProps = {
readonly phase: AuthoringPhaseId; readonly phase: PreparedLifecycleStepId | AuthoringPhaseId;
readonly visualRole?: "support"; readonly visualRole?: "support";
readonly message: Scene9MessageProjection; readonly message: PreparedLifecycleMessageProjection;
readonly submittedOverrides: Scene9SubmittedOverrides; readonly submittedOverrides: PreparedLifecycleSubmittedOverrides;
readonly runRequested: string | null; readonly runRequested: string | null;
readonly onDraftChange: (draft: string) => void; readonly onDraftChange: (draft: string) => void;
readonly onSubmit: (message: string) => void; readonly onSubmit: (message: string) => void;
}; };
const phaseLabels: Readonly<Record<AuthoringPhaseId, string>> = { const phaseLabels: Readonly<Record<PreparedLifecycleStepId | AuthoringPhaseId, string>> = {
discover: "Discover", discover: "Discover",
draft: "Draft", draft: "Draft",
diagnose: "Diagnose",
repair: "Repair",
validate: "Validate", validate: "Validate",
artifact: "Artifact", artifact: "Artifact",
deployment: "Deployment", deployment: "Deployment",
}; };
/** /**
* Stable Scene 9 boundary for the prepared assistant surface. * Stable prepared lifecycle boundary for the prepared assistant surface.
* *
* The pane owns only the transient composer buffer. Submitted text is handed * The pane owns only the transient composer buffer. Submitted text is handed
* back to the Scene 9 controller so the replay can project it later. * back to the lifecycle controller so the replay can project it later.
*/ */
export const PresentationAssistantPane = ({ export const PresentationAssistantPane = ({
phase, phase,
@@ -83,22 +86,22 @@ export const PresentationAssistantPane = ({
/> />
</div> </div>
<form className="presentation-assistant-pane__composer" onSubmit={submit}> <form className="presentation-assistant-pane__composer" onSubmit={submit}>
<label htmlFor="scene9-authoring-message">Message to authoring assistant</label> <label htmlFor="prepared-lifecycle-authoring-message">Message to authoring assistant</label>
<Textarea <Textarea
id="scene9-authoring-message" id="prepared-lifecycle-authoring-message"
value={draft} value={draft}
placeholder={message.placeholder} placeholder={message.placeholder}
disabled={runRequested !== null} disabled={runRequested !== null}
onChange={(event) => updateDraft(event.target.value)} onChange={(event) => updateDraft(event.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
aria-describedby="scene9-authoring-message-help" aria-describedby="prepared-lifecycle-authoring-message-help"
/> />
<div className="presentation-assistant-pane__composer-actions"> <div className="presentation-assistant-pane__composer-actions">
<Button type="submit" disabled={!canSubmit}> <Button type="submit" disabled={!canSubmit}>
Send message Send message
</Button> </Button>
</div> </div>
<p id="scene9-authoring-message-help" className="presentation-assistant-pane__composer-help"> <p id="prepared-lifecycle-authoring-message-help" className="presentation-assistant-pane__composer-help">
{PREPARED_COMPOSER_HELP} {PREPARED_COMPOSER_HELP}
</p> </p>
{runRequested !== null ? ( {runRequested !== null ? (
@@ -1,6 +1,10 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { projectPreparedAuthoring } from "./authoring-recording.js"; import { projectPreparedAuthoring } from "./authoring-recording.js";
import { projectPreparedAuthoringPhase } from "./authoring-projection.js"; import {
projectPreparedAuthoringPhase,
projectPreparedLifecycleStep,
recordingPhaseForStep,
} from "./authoring-projection.js";
describe("projectPreparedAuthoringPhase", () => { describe("projectPreparedAuthoringPhase", () => {
it("projects the discover phase with sources, capabilities, schema", () => { it("projects the discover phase with sources, capabilities, schema", () => {
@@ -64,4 +68,37 @@ describe("projectPreparedAuthoringPhase", () => {
expect(["success", "diagnostic"]).toContain(cmd.result); expect(["success", "diagnostic"]).toContain(cmd.result);
} }
}); });
it("splits one recorded validate phase into diagnosis and repair presentation steps", () => {
const diagnose = projectPreparedLifecycleStep("diagnose");
const repair = projectPreparedLifecycleStep("repair");
expect(diagnose.recordingPhase).toBe("validate");
expect(repair.recordingPhase).toBe("validate");
expect(diagnose.focus).toBe("diagnose");
expect(repair.focus).toBe("repair");
expect(diagnose.primaryCommand.title).toBe("workflow.draft_workspaces.validate");
expect(repair.primaryCommand.title).toBe("workflow.draft_workspaces.set_step_output_map");
});
it("projects six presentation steps from five recorded phases", () => {
const steps = [
"discover",
"draft",
"diagnose",
"repair",
"artifact",
"deployment",
] as const;
expect(steps.map(recordingPhaseForStep)).toEqual([
"discover",
"draft",
"validate",
"validate",
"artifact",
"deployment",
]);
expect(steps.map((step) => projectPreparedLifecycleStep(step).step)).toEqual(steps);
});
}); });
@@ -1,5 +1,13 @@
import { projectPreparedAuthoring, type AuthoringPhaseId, type PreparedAuthoringCommand } from "./authoring-recording.js"; import { projectPreparedAuthoring, type AuthoringPhaseId, type PreparedAuthoringCommand } from "./authoring-recording.js";
export type PreparedLifecycleStepId =
| "discover"
| "draft"
| "diagnose"
| "repair"
| "artifact"
| "deployment";
export type AuthoringPhaseProjection = { export type AuthoringPhaseProjection = {
readonly phase: AuthoringPhaseId; readonly phase: AuthoringPhaseId;
readonly beatId: string; readonly beatId: string;
@@ -10,6 +18,13 @@ export type AuthoringPhaseProjection = {
readonly visual: AuthoringPhaseVisualModel; readonly visual: AuthoringPhaseVisualModel;
}; };
export type PreparedLifecycleStepProjection = AuthoringPhaseProjection & {
readonly step: PreparedLifecycleStepId;
readonly recordingPhase: AuthoringPhaseId;
readonly focus: "full" | "diagnose" | "repair";
readonly primaryCommand: PreparedAuthoringCommand;
};
export type AuthoringPhaseVisualModel = export type AuthoringPhaseVisualModel =
| { | {
readonly kind: "inventory"; readonly kind: "inventory";
@@ -111,3 +126,34 @@ export const projectPreparedAuthoringPhase = (
visual: visualForPhase(found.phase), visual: visualForPhase(found.phase),
}; };
}; };
export const recordingPhaseForStep = (
step: PreparedLifecycleStepId | AuthoringPhaseId,
): AuthoringPhaseId => {
if (step === "diagnose" || step === "repair") return "validate";
return step;
};
export const projectPreparedLifecycleStep = (
step: PreparedLifecycleStepId,
): PreparedLifecycleStepProjection => {
const recordingPhase = recordingPhaseForStep(step);
const phase = projectPreparedAuthoringPhase(recordingPhase);
// Diagnose and repair are presentation choreography over one factual
// recording phase; they select distinct evidence without duplicating it.
const commandIndex = step === "repair" ? 1 : 0;
const primaryCommand = phase.commands[commandIndex];
if (primaryCommand === undefined) {
throw new Error(
`recorded phase "${recordingPhase}" has no command at index ${commandIndex} for presentation step "${step}"`,
);
}
return {
...phase,
step,
recordingPhase,
focus: step === "diagnose" || step === "repair" ? step : "full",
primaryCommand,
};
};
@@ -1,15 +1,15 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
SCENE9_PHASE_PROMPTS, PREPARED_LIFECYCLE_PHASE_PROMPTS,
initialScene9MessageState, initialPreparedLifecycleMessageState,
projectScene9Message, projectPreparedLifecycleMessage,
projectScene9SubmittedOverrides, projectPreparedLifecycleSubmittedOverrides,
scene9MessageReducer, preparedLifecycleMessageReducer,
} from "./scene9-message-state.js"; } from "./prepared-lifecycle-message-state.js";
describe("scene 9 staged message state", () => { describe("prepared lifecycle staged message state", () => {
it("defines the exact prompt for every phase", () => { it("defines the exact prompt for every phase", () => {
expect(SCENE9_PHASE_PROMPTS).toEqual({ expect(PREPARED_LIFECYCLE_PHASE_PROMPTS).toEqual({
discover: "", discover: "",
draft: "Is the draft valid? Can you check and fix any issues?", draft: "Is the draft valid? Can you check and fix any issues?",
validate: "", validate: "",
@@ -20,12 +20,12 @@ describe("scene 9 staged message state", () => {
}); });
it("projects empty phases with a useful placeholder", () => { it("projects empty phases with a useful placeholder", () => {
expect(projectScene9Message(initialScene9MessageState, "discover")).toMatchObject({ expect(projectPreparedLifecycleMessage(initialPreparedLifecycleMessageState, "discover")).toMatchObject({
draft: "", draft: "",
prefill: "", prefill: "",
placeholder: expect.any(String), placeholder: expect.any(String),
}); });
expect(projectScene9Message(initialScene9MessageState, "validate")).toMatchObject({ expect(projectPreparedLifecycleMessage(initialPreparedLifecycleMessageState, "diagnose")).toMatchObject({
draft: "", draft: "",
prefill: "", prefill: "",
placeholder: expect.any(String), placeholder: expect.any(String),
@@ -33,23 +33,23 @@ describe("scene 9 staged message state", () => {
}); });
it("projects the exact phase prefill without changing the draft", () => { it("projects the exact phase prefill without changing the draft", () => {
expect(projectScene9Message(initialScene9MessageState, "draft")).toMatchObject({ expect(projectPreparedLifecycleMessage(initialPreparedLifecycleMessageState, "draft")).toMatchObject({
draft: "", draft: "",
prefill: SCENE9_PHASE_PROMPTS.draft, prefill: PREPARED_LIFECYCLE_PHASE_PROMPTS.draft,
}); });
expect(projectScene9Message(initialScene9MessageState, "artifact")).toMatchObject({ expect(projectPreparedLifecycleMessage(initialPreparedLifecycleMessageState, "artifact")).toMatchObject({
draft: "", draft: "",
prefill: SCENE9_PHASE_PROMPTS.artifact, prefill: PREPARED_LIFECYCLE_PHASE_PROMPTS.artifact,
}); });
}); });
it("preserves edited draft text exactly when submitting draft", () => { it("preserves edited draft text exactly when submitting draft", () => {
const edited = scene9MessageReducer(initialScene9MessageState, { const edited = preparedLifecycleMessageReducer(initialPreparedLifecycleMessageState, {
type: "draft_edited", type: "draft_edited",
draft: " Check only the report binding. ", draft: " Check only the report binding. ",
}); });
expect(scene9MessageReducer(edited, { type: "draft_submitted" })).toEqual({ expect(preparedLifecycleMessageReducer(edited, { type: "draft_submitted" })).toEqual({
draft: edited.draft, draft: edited.draft,
submittedOverrides: { validate: edited.draft }, submittedOverrides: { validate: edited.draft },
runRequested: null, runRequested: null,
@@ -57,12 +57,12 @@ describe("scene 9 staged message state", () => {
}); });
it("stores artifact submissions under the deployment destination", () => { it("stores artifact submissions under the deployment destination", () => {
const state = scene9MessageReducer(initialScene9MessageState, { const state = preparedLifecycleMessageReducer(initialPreparedLifecycleMessageState, {
type: "draft_edited", type: "draft_edited",
draft: "Save this edited deployment request", draft: "Save this edited deployment request",
}); });
expect(scene9MessageReducer(state, { type: "artifact_submitted" })).toEqual({ expect(preparedLifecycleMessageReducer(state, { type: "artifact_submitted" })).toEqual({
draft: state.draft, draft: state.draft,
submittedOverrides: { deployment: state.draft }, submittedOverrides: { deployment: state.draft },
runRequested: null, runRequested: null,
@@ -70,12 +70,12 @@ describe("scene 9 staged message state", () => {
}); });
it("stores discover submissions under the discover destination", () => { it("stores discover submissions under the discover destination", () => {
const state = scene9MessageReducer(initialScene9MessageState, { const state = preparedLifecycleMessageReducer(initialPreparedLifecycleMessageState, {
type: "draft_edited", type: "draft_edited",
draft: "Inspect the report source first.", draft: "Inspect the report source first.",
}); });
expect(scene9MessageReducer(state, { type: "discover_submitted" })).toEqual({ expect(preparedLifecycleMessageReducer(state, { type: "discover_submitted" })).toEqual({
draft: state.draft, draft: state.draft,
submittedOverrides: { discover: state.draft }, submittedOverrides: { discover: state.draft },
runRequested: null, runRequested: null,
@@ -83,43 +83,43 @@ describe("scene 9 staged message state", () => {
}); });
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 = { ...initialPreparedLifecycleMessageState, draft: " \n\t" };
expect(scene9MessageReducer(blank, { type: "draft_submitted" })).toBe(blank); expect(preparedLifecycleMessageReducer(blank, { type: "draft_submitted" })).toBe(blank);
expect(scene9MessageReducer(blank, { type: "artifact_submitted" })).toBe(blank); expect(preparedLifecycleMessageReducer(blank, { type: "artifact_submitted" })).toBe(blank);
expect(scene9MessageReducer(blank, { type: "run_requested" })).toBe(blank); expect(preparedLifecycleMessageReducer(blank, { type: "run_requested" })).toBe(blank);
const submitted = scene9MessageReducer( const submitted = preparedLifecycleMessageReducer(
scene9MessageReducer(initialScene9MessageState, { preparedLifecycleMessageReducer(initialPreparedLifecycleMessageState, {
type: "draft_edited", type: "draft_edited",
draft: "A draft override", draft: "A draft override",
}), }),
{ type: "draft_submitted" }, { type: "draft_submitted" },
); );
expect(scene9MessageReducer(submitted, { type: "draft_submitted" })).toBe(submitted); expect(preparedLifecycleMessageReducer(submitted, { type: "draft_submitted" })).toBe(submitted);
}); });
it("records the final run request without claiming execution", () => { it("records the final run request without claiming execution", () => {
const state = scene9MessageReducer(initialScene9MessageState, { const state = preparedLifecycleMessageReducer(initialPreparedLifecycleMessageState, {
type: "draft_edited", type: "draft_edited",
draft: "Run this deployment", draft: "Run this deployment",
}); });
const requested = scene9MessageReducer(state, { type: "run_requested" }); const requested = preparedLifecycleMessageReducer(state, { type: "run_requested" });
expect(requested.runRequested).toBe("Run this deployment"); expect(requested.runRequested).toBe("Run this deployment");
expect(requested.submittedOverrides).toEqual({}); expect(requested.submittedOverrides).toEqual({});
expect(scene9MessageReducer(requested, { type: "run_requested" })).toBe(requested); expect(preparedLifecycleMessageReducer(requested, { type: "run_requested" })).toBe(requested);
}); });
it("projects only destination-phase overrides for the prepared thread", () => { it("projects only destination-phase overrides for the prepared thread", () => {
const state = { const state = {
...initialScene9MessageState, ...initialPreparedLifecycleMessageState,
submittedOverrides: { submittedOverrides: {
validate: "Edited validation request", validate: "Edited validation request",
deployment: "Edited deployment request", deployment: "Edited deployment request",
}, },
}; };
expect(projectScene9SubmittedOverrides(state)).toEqual({ expect(projectPreparedLifecycleSubmittedOverrides(state)).toEqual({
validate: "Edited validation request", validate: "Edited validation request",
deployment: "Edited deployment request", deployment: "Edited deployment request",
}); });
@@ -1,9 +1,13 @@
import type { AuthoringPhaseId } from "./authoring-recording.js"; import type { AuthoringPhaseId } from "./authoring-recording.js";
import {
recordingPhaseForStep,
type PreparedLifecycleStepId,
} from "./authoring-projection.js";
export type Scene9MessagePhase = AuthoringPhaseId; export type PreparedLifecycleMessagePhase = PreparedLifecycleStepId | AuthoringPhaseId;
export type Scene9DestinationPhase = "discover" | "validate" | "deployment"; export type PreparedLifecycleDestinationPhase = "discover" | "validate" | "deployment";
export const SCENE9_PHASE_PROMPTS: Readonly<Record<Scene9MessagePhase, string>> = { export const PREPARED_LIFECYCLE_PHASE_PROMPTS: Readonly<Record<AuthoringPhaseId, string>> = {
discover: "", discover: "",
draft: "Is the draft valid? Can you check and fix any issues?", draft: "Is the draft valid? Can you check and fix any issues?",
validate: "", validate: "",
@@ -12,7 +16,7 @@ export const SCENE9_PHASE_PROMPTS: Readonly<Record<Scene9MessagePhase, string>>
"The deployment lda_report_case_study.default is saved and valid. Shall we run it now?", "The deployment lda_report_case_study.default is saved and valid. Shall we run it now?",
}; };
export const SCENE9_PHASE_PLACEHOLDERS: Readonly<Record<Scene9MessagePhase, string>> = { export const PREPARED_LIFECYCLE_PHASE_PLACEHOLDERS: Readonly<Record<AuthoringPhaseId, string>> = {
discover: "Ask about the workflow authoring process.", discover: "Ask about the workflow authoring process.",
draft: "Review the prepared draft.", draft: "Review the prepared draft.",
validate: "Ask about the draft validation results.", validate: "Ask about the draft validation results.",
@@ -20,23 +24,23 @@ export const SCENE9_PHASE_PLACEHOLDERS: Readonly<Record<Scene9MessagePhase, stri
deployment: "Ask whether the saved deployment should run.", deployment: "Ask whether the saved deployment should run.",
}; };
export type Scene9SubmittedOverrides = Readonly< export type PreparedLifecycleSubmittedOverrides = Readonly<
Partial<Record<Scene9DestinationPhase, string>> Partial<Record<PreparedLifecycleDestinationPhase, string>>
>; >;
export type Scene9MessageState = { export type PreparedLifecycleMessageState = {
readonly draft: string; readonly draft: string;
readonly submittedOverrides: Scene9SubmittedOverrides; readonly submittedOverrides: PreparedLifecycleSubmittedOverrides;
readonly runRequested: string | null; readonly runRequested: string | null;
}; };
export const initialScene9MessageState: Scene9MessageState = { export const initialPreparedLifecycleMessageState: PreparedLifecycleMessageState = {
draft: "", draft: "",
submittedOverrides: {}, submittedOverrides: {},
runRequested: null, runRequested: null,
}; };
export type Scene9MessageAction = export type PreparedLifecycleMessageAction =
| { readonly type: "draft_edited"; readonly draft: string } | { readonly type: "draft_edited"; readonly draft: string }
| { readonly type: "discover_submitted" } | { readonly type: "discover_submitted" }
| { readonly type: "draft_submitted" } | { readonly type: "draft_submitted" }
@@ -46,9 +50,9 @@ export type Scene9MessageAction =
const hasText = (text: string): boolean => text.trim().length > 0; const hasText = (text: string): boolean => text.trim().length > 0;
const submitOverride = ( const submitOverride = (
state: Scene9MessageState, state: PreparedLifecycleMessageState,
destination: Scene9DestinationPhase, destination: PreparedLifecycleDestinationPhase,
): Scene9MessageState => { ): PreparedLifecycleMessageState => {
if (!hasText(state.draft) || state.submittedOverrides[destination] !== undefined) return state; if (!hasText(state.draft) || state.submittedOverrides[destination] !== undefined) return state;
return { return {
@@ -60,10 +64,10 @@ const submitOverride = (
}; };
}; };
export const scene9MessageReducer = ( export const preparedLifecycleMessageReducer = (
state: Scene9MessageState, state: PreparedLifecycleMessageState,
action: Scene9MessageAction, action: PreparedLifecycleMessageAction,
): Scene9MessageState => { ): PreparedLifecycleMessageState => {
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;
@@ -80,22 +84,25 @@ export const scene9MessageReducer = (
} }
}; };
export type Scene9MessageProjection = { export type PreparedLifecycleMessageProjection = {
readonly draft: string; readonly draft: string;
readonly prefill: string; readonly prefill: string;
readonly placeholder: string; readonly placeholder: string;
}; };
export const projectScene9Message = ( export const projectPreparedLifecycleMessage = (
state: Scene9MessageState, state: PreparedLifecycleMessageState,
phase: Scene9MessagePhase, step: PreparedLifecycleMessagePhase,
): Scene9MessageProjection => ({ ): PreparedLifecycleMessageProjection => {
draft: state.draft, const phase = recordingPhaseForStep(step);
prefill: SCENE9_PHASE_PROMPTS[phase], return {
placeholder: SCENE9_PHASE_PLACEHOLDERS[phase], draft: state.draft,
}); prefill: PREPARED_LIFECYCLE_PHASE_PROMPTS[phase],
placeholder: PREPARED_LIFECYCLE_PHASE_PLACEHOLDERS[phase],
};
};
/** Returns the transcript-facing request overrides by destination phase. */ /** Returns the transcript-facing request overrides by destination phase. */
export const projectScene9SubmittedOverrides = ( export const projectPreparedLifecycleSubmittedOverrides = (
state: Scene9MessageState, state: PreparedLifecycleMessageState,
): Scene9SubmittedOverrides => state.submittedOverrides; ): PreparedLifecycleSubmittedOverrides => state.submittedOverrides;