From ec5632d03925b221e40a8ae9b53cc2a3c8cb66f8 Mon Sep 17 00:00:00 2001 From: Kostandin Angjellari Date: Fri, 2 Oct 2026 12:45:31 +0200 Subject: [PATCH 1/2] Shorten and de-jargon Voice and Brunch panel copy Rewrite the voice consent card, Voice issue notices, connection errors, audio and Labs settings, Ledger notices, Activity tool labels and the experiment draft card in plain language. Notices lead with a short first sentence because the Voice issues popover shows it as the title. Instructions sent to the Live model are unchanged. Co-authored-by: Cursor --- .changeset/voice-settings-copy.md | 5 +++ .../assistant-labs-settings.test.tsx | 3 +- .../assistant-labs-settings.tsx | 8 ++--- ...riment-interactive-tool.authority.test.tsx | 2 +- ...draft-experiment-interactive-tool.test.tsx | 10 +++--- ...unch-draft-experiment-interactive-tool.tsx | 17 ++++------ .../brunch-panel-transport.ts | 4 +-- .../brunch-tool-presentation.test.ts | 8 ++--- .../brunch-tool-presentation.ts | 18 +++++----- .../brunch-workpiece-pane.test.tsx | 10 +++--- .../brunch-workpiece-pane.tsx | 6 ++-- .../live-brunch-bridge.test.ts | 20 +++++------ .../app/voice-interview/live-brunch-bridge.ts | 16 ++++----- .../live-conversation-control.test.tsx | 22 ++++++------ .../live-conversation-control.tsx | 12 +++---- .../voice-interview/live-conversation.test.ts | 8 ++--- .../app/voice-interview/live-conversation.ts | 34 ++++++------------- .../voice-interview-control.test.tsx | 2 +- .../voice-interview-disclosure.tsx | 4 +-- .../voice-session-state.test.ts | 4 +-- .../voice-interview/voice-session-state.ts | 6 ++-- .../server/voice/openai-realtime-call.test.ts | 10 +++--- .../src/voice-diagnostics.ts | 14 ++++---- .../ai-assistant-contents.stories.tsx | 17 +++++----- .../ai-assistant-contents.test.tsx | 10 +++--- .../ai-assistant-contents/voice-dock.test.tsx | 6 ++-- .../audio-popover/settings.test.tsx | 2 +- .../voice-dock/audio-popover/settings.tsx | 11 +++--- 28 files changed, 130 insertions(+), 159 deletions(-) create mode 100644 .changeset/voice-settings-copy.md diff --git a/.changeset/voice-settings-copy.md b/.changeset/voice-settings-copy.md new file mode 100644 index 00000000000..475c9486b21 --- /dev/null +++ b/.changeset/voice-settings-copy.md @@ -0,0 +1,5 @@ +--- +"@hashintel/petrinaut": patch +--- + +Shorten Voice audio settings help: the voice note reads "Applies next session. To preview, mute your mic while the agent is idle." and disabled controls read "Connect Voice to use audio controls." diff --git a/apps/petrinaut-website/src/main/app/local-storage-demo/assistant-labs-settings.test.tsx b/apps/petrinaut-website/src/main/app/local-storage-demo/assistant-labs-settings.test.tsx index 092a87b8064..8c7d7c33213 100644 --- a/apps/petrinaut-website/src/main/app/local-storage-demo/assistant-labs-settings.test.tsx +++ b/apps/petrinaut-website/src/main/app/local-storage-demo/assistant-labs-settings.test.tsx @@ -127,8 +127,7 @@ test.each([ props: { brunchConfigured: false }, control: "Use Brunch", checked: false, - description: - "Brunch is unavailable because this site has no Brunch endpoint configured.", + description: "Brunch isn’t set up on this site.", }, { name: "Voice preference loading", diff --git a/apps/petrinaut-website/src/main/app/local-storage-demo/assistant-labs-settings.tsx b/apps/petrinaut-website/src/main/app/local-storage-demo/assistant-labs-settings.tsx index e8f232fa63a..68bcf4ff04d 100644 --- a/apps/petrinaut-website/src/main/app/local-storage-demo/assistant-labs-settings.tsx +++ b/apps/petrinaut-website/src/main/app/local-storage-demo/assistant-labs-settings.tsx @@ -119,8 +119,8 @@ export const AssistantLabsSettings = ({ const brunchDescription = !assistantReady ? "Loading your assistant preference…" : !brunchConfigured - ? "Brunch is unavailable because this site has no Brunch endpoint configured." - : "Use Brunch instead of the stock Petrinaut assistant."; + ? "Brunch isn’t set up on this site." + : "Use Brunch instead of the default assistant."; const voiceDescription = !voicePreferenceReady ? "Loading your Voice preference…" : !brunchSelected @@ -129,7 +129,7 @@ export const AssistantLabsSettings = ({ ? "Checking whether Voice is available…" : openAIVoiceConfig === null ? "Voice is unavailable in this deployment." - : "Make Voice mode available for Brunch conversations."; + : "Talk to Brunch by voice."; return (
@@ -155,7 +155,7 @@ export const AssistantLabsSettings = ({ /> {brunchSelected && voicePreferenceReady && voiceEnabled && ( { await waitFor(() => expect(submit).toHaveBeenCalledTimes(1)); expect(heading()).toEqual(["Preparing draft"]); expect(screen.getByText(/being prepared/u)).toBeTruthy(); - expect(screen.queryByText(/before this editor was loaded/u)).toBeNull(); + expect(screen.queryByText(/is from an earlier session/u)).toBeNull(); expect(screen.queryByRole("button", { name: "Run" })).toBeNull(); await act(async () => submission.resolve()); @@ -423,7 +423,7 @@ describe("BrunchDraftExperimentWidget", () => { name: "Run", }); expect(run.disabled).toBe(true); - expect(screen.getByRole("alert").textContent).toContain("Run is blocked"); + expect(screen.getByRole("alert").textContent).toContain("Can’t run"); fireEvent.click(run); expect(runExperiment).not.toHaveBeenCalled(); }, @@ -534,7 +534,7 @@ describe("BrunchDraftExperimentWidget", () => { await waitFor(() => expect(submit).toHaveBeenCalledTimes(1)); expect(submit.mock.calls[0]?.[0]).toMatchObject({ status: "invalid" }); expect(submit.mock.calls[0]?.[0].diagnostics[0]).toMatch( - /changed since the canonical read/u, + /changed since Brunch read it/u, ); expect(heading()).toEqual(["Could not be prepared"]); expect(screen.queryByRole("button", { name: "Run" })).toBeNull(); @@ -1441,9 +1441,7 @@ describe("BrunchDraftExperimentWidget", () => { expect(submit).not.toHaveBeenCalled(); expect(heading()).toEqual(["Not retained in this editor"]); - expect( - screen.getByText(/prepared before this editor was loaded/u), - ).toBeTruthy(); + expect(screen.getByText(/draft is from an earlier session/u)).toBeTruthy(); expect(screen.queryByRole("button", { name: "Run" })).toBeNull(); }); diff --git a/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-draft-experiment-interactive-tool.tsx b/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-draft-experiment-interactive-tool.tsx index c94d3851d36..717ab118195 100644 --- a/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-draft-experiment-interactive-tool.tsx +++ b/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-draft-experiment-interactive-tool.tsx @@ -182,9 +182,7 @@ export const resolveDraftAuthorityFromHistory = async ( latest?.toolName !== "getLatestNetDefinition" || latest.state !== "output-available" ) - throw new Error( - "Draft requires the latest settled canonical net read after changes.", - ); + throw new Error("Brunch needs to read the latest model before drafting."); const envelope = latest.output; const metadata = typeof envelope === "object" && envelope !== null && "metadata" in envelope @@ -192,7 +190,7 @@ export const resolveDraftAuthorityFromHistory = async ( : undefined; const revision = metadata?.documentRevision.before; if (revision === undefined) - throw new Error("The latest canonical read has no document revision."); + throw new Error("Brunch’s latest model read has no document revision."); return revision; }; @@ -320,7 +318,7 @@ export const BrunchDraftExperimentWidget = ({ : { prepared: null, error: - "The model changed since the canonical read. Ask Brunch to read the current model and draft again.", + "The model changed since Brunch read it. Ask Brunch to draft it again.", }; } catch (caught) { outcome = { @@ -606,7 +604,7 @@ export const BrunchDraftExperimentWidget = ({ ? preparationFailure.kind === "prepare" ? `The experiment proposal could not be prepared: ${preparationFailure.message}` : `The prepared proposal could not be submitted: ${preparationFailure.message}` - : "This draft was prepared before this editor was loaded. Ask Brunch to draft it again to run it.")} + : "This draft is from an earlier session. Ask Brunch to draft it again to run it.")}

)}

Declared

@@ -620,8 +618,7 @@ export const BrunchDraftExperimentWidget = ({

Not carried into execution

{input.unsupported.length === 0 ? (

- No restrictions were stated. The request carries no constraints, so - none are enforced. + No restrictions were stated, so none are enforced.

) : (
    @@ -639,8 +636,8 @@ export const BrunchDraftExperimentWidget = ({ )} {blocksRun ? (

    - Run is blocked by an unsupported restriction. Ask Brunch to revise the - proposal; a reporting-only exploration needs your explicit acceptance. + Can’t run: a restriction isn’t supported. Ask Brunch to revise the + proposal, or tell it you accept a run that only reports on it.

    ) : null} {optimizationUnavailable !== null ? ( diff --git a/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-panel-transport.ts b/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-panel-transport.ts index 3a4a3f7c526..49a6f0d896d 100644 --- a/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-panel-transport.ts +++ b/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-panel-transport.ts @@ -266,8 +266,8 @@ const summarizeSweepOutput = ( switch (sweep.status) { case "no-settled-range": return { - title: "No settled range to sweep", - detail: "The conversation has no settled user entries.", + title: "Nothing confirmed to sweep yet", + detail: "None of your messages in this conversation are confirmed yet.", }; case "refused": return { diff --git a/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-tool-presentation.test.ts b/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-tool-presentation.test.ts index 5fd3fa06bbe..9ac131c3f39 100644 --- a/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-tool-presentation.test.ts +++ b/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-tool-presentation.test.ts @@ -55,15 +55,15 @@ describe("Brunch tool presentation", () => { test.each([ [ { includeContent: false, sourceIds: ["m1"], locateTexts: ["claim"] }, - "Read settled passages and conversation sources", + "Read confirmed passages and conversation sources", ], [ { includeContent: false, locateTexts: ["claim"] }, - "Read settled passages", + "Read confirmed passages", ], [{ includeContent: false, sourceIds: ["m1"] }, "Read conversation sources"], - [{ includeContent: false, sourceIds: [] }, "Checked Ledger revision"], - [{ includeContent: false }, "Checked Ledger revision"], + [{ includeContent: false, sourceIds: [] }, "Checked for Ledger updates"], + [{ includeContent: false }, "Checked for Ledger updates"], [{ includeContent: true, sourceIds: ["m1"] }, "Read conversation sources"], [{}, "Read ledger"], [undefined, "Read ledger"], diff --git a/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-tool-presentation.ts b/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-tool-presentation.ts index 3f1d5cd241f..eac38e812dc 100644 --- a/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-tool-presentation.ts +++ b/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-tool-presentation.ts @@ -117,16 +117,16 @@ const readWorkpiecePurpose = (input: unknown): LifecycleTitles => { if (hasPassages && hasSources) { return { - pending: "Reading settled passages and conversation sources", - success: "Read settled passages and conversation sources", - error: "Could not read settled passages and conversation sources", + pending: "Reading confirmed passages and conversation sources", + success: "Read confirmed passages and conversation sources", + error: "Could not read confirmed passages and conversation sources", }; } if (hasPassages) { return { - pending: "Reading settled passages", - success: "Read settled passages", - error: "Could not read settled passages", + pending: "Reading confirmed passages", + success: "Read confirmed passages", + error: "Could not read confirmed passages", }; } if (hasSources) { @@ -138,9 +138,9 @@ const readWorkpiecePurpose = (input: unknown): LifecycleTitles => { } if (record.includeContent === false) { return { - pending: "Checking Ledger revision", - success: "Checked Ledger revision", - error: "Could not check Ledger revision", + pending: "Checking for Ledger updates", + success: "Checked for Ledger updates", + error: "Could not check for Ledger updates", }; } return { diff --git a/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-workpiece-pane.test.tsx b/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-workpiece-pane.test.tsx index 3201b11d642..9646d2b1b62 100644 --- a/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-workpiece-pane.test.tsx +++ b/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-workpiece-pane.test.tsx @@ -122,7 +122,7 @@ test("does not display the input of a typed refused settlement or mark it newer" ); expect(html).toContain("

    Settled account

    "); expect(html).not.toContain("Refused silent-shrink account"); - expect(html).not.toContain("A newer Ledger revision exists"); + expect(html).not.toContain("This view is out of date"); }); test("does not display the input of a failed settlement", () => { @@ -150,7 +150,7 @@ test("does not display the input of a failed settlement", () => { ); expect(html).toContain("

    Settled account

    "); expect(html).not.toContain("Refused stale-base account"); - expect(html).not.toContain("A newer Ledger revision exists"); + expect(html).not.toContain("This view is out of date"); }); test("a later settlement replaces the displayed query while retaining the recorded why", () => { @@ -164,9 +164,7 @@ test("a later settlement replaces the displayed query while retaining the record />, ); expect(html).toContain("

    Later account

    "); - expect(html).toContain( - "recorded explanation predates a newer Ledger revision", - ); + expect(html).toContain("This explanation is out of date"); }); test("an explicit later query replaces a recorded settlement", () => { @@ -195,7 +193,7 @@ test("a later settlement whose output is not bound to its call marks the display />, ); expect(html).toContain("

    Earlier account

    "); - expect(html).toContain("A newer Ledger revision exists"); + expect(html).toContain("This view is out of date"); expect(html).not.toContain("Unbound account"); }); diff --git a/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-workpiece-pane.tsx b/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-workpiece-pane.tsx index 0adeba33511..c5c9eeb7e18 100644 --- a/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-workpiece-pane.tsx +++ b/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-workpiece-pane.tsx @@ -70,14 +70,12 @@ export const BrunchWorkpiecePane = ({ > {stateChangedSinceReport && (

    - A newer Ledger revision exists. Ask Brunch to refresh this view before - relying on it. + This view is out of date. Ask Brunch to refresh it.

    )} {whyPredatesSettlement && (

    - The recorded explanation predates a newer Ledger revision. Ask why - again to assess the latest account. + This explanation is out of date. Ask why again for the latest.

    )} {workpiece && typeof workpiece.markdown === "string" && ( diff --git a/apps/petrinaut-website/src/main/app/voice-interview/live-brunch-bridge.test.ts b/apps/petrinaut-website/src/main/app/voice-interview/live-brunch-bridge.test.ts index c99d7624d89..2057a8166aa 100644 --- a/apps/petrinaut-website/src/main/app/voice-interview/live-brunch-bridge.test.ts +++ b/apps/petrinaut-website/src/main/app/voice-interview/live-brunch-bridge.test.ts @@ -358,7 +358,7 @@ test("keeps a dropped utterance's words visible so they can be sent from the com await fixture.bridge.accept(speech("second", "Also check the queue")); expect(fixture.notice).toHaveBeenCalledWith( - expect.stringContaining("not retained"), + expect.stringContaining("Those words weren’t sent"), ); expect( history.project([]).find((message) => message.id === "second")?.parts, @@ -504,7 +504,7 @@ test("does not admit a turn prepared in a conversation that was switched away fr }, ]); expect(fixture.notice).toHaveBeenLastCalledWith( - expect.stringContaining("the conversation changed"), + expect.stringContaining("The conversation changed"), ); expect(fixture.appendInstructions).toHaveBeenCalledWith( expect.stringContaining("not submitted because the conversation changed"), @@ -612,7 +612,7 @@ test("speech that cancels unsent words says they were not sent", async () => { expect(fixture.submit).not.toHaveBeenCalled(); expect(fixture.notice).toHaveBeenCalledExactlyOnceWith( - expect.stringContaining("was not sent"), + expect.stringContaining("You started speaking again"), ); }); @@ -781,7 +781,7 @@ test("speech keeps a submitted turn's composer slot until Brunch admits it", asy expect(prepare).toHaveBeenCalledOnce(); expect(fixture.submit).toHaveBeenCalledOnce(); expect(fixture.notice).toHaveBeenLastCalledWith( - expect.stringContaining("Wait for the pending input"), + expect.stringContaining("when the assistant is ready"), ); }); @@ -963,7 +963,7 @@ test("a finalized snapshot confirms an observed response is textless", async () }); expect(fixture.notice).toHaveBeenLastCalledWith( - "Brunch settled without a spoken answer. Check the conversation.", + "The assistant finished without a reply to speak. Check the conversation.", ); expect(fixture.appendCommentary).not.toHaveBeenCalled(); }); @@ -1106,7 +1106,7 @@ test("duplicates, empty input and one waiting composer submission never create a await fixture.bridge.accept(speech("two", "Follow-up")); expect(fixture.submit).toHaveBeenCalledOnce(); expect(fixture.notice).toHaveBeenLastCalledWith( - expect.stringContaining("not retained"), + expect.stringContaining("Those words weren’t sent"), ); release(); await pending; @@ -1543,7 +1543,7 @@ test("uncertain admission is visible and never automatically replayed", async () await fixture.bridge.accept(speech("one", "First")); expect(fixture.submit).toHaveBeenCalledOnce(); expect(fixture.notice).toHaveBeenLastCalledWith( - expect.stringContaining("Check canonical history"), + expect.stringContaining("Check the conversation before sending it again"), ); }); @@ -1558,7 +1558,7 @@ test("a response failure after confirmed admission does not report uncertain adm expect(fixture.submit).toHaveBeenCalledOnce(); expect(fixture.appendCommentary).not.toHaveBeenCalled(); expect(fixture.notice).toHaveBeenLastCalledWith( - "Your message was admitted, but its response could not be confirmed. Check canonical history; no automatic retry was made.", + "Couldn’t confirm the answer. Your message was sent; check the conversation before sending it again.", ); }); @@ -1592,7 +1592,7 @@ test("admission frees the existing waiting-input slot, but finishing an earlier await fixture.bridge.accept(speech("three", "Another pending input")); expect(fixture.submit).toHaveBeenCalledTimes(2); expect(fixture.notice).toHaveBeenLastCalledWith( - expect.stringContaining("not retained"), + expect.stringContaining("Those words weren’t sent"), ); admitCorrection(); @@ -1861,7 +1861,7 @@ test("a finalized textless unobserved answer cannot fall through to an observed }); expect(fixture.notice).toHaveBeenLastCalledWith( - "Brunch settled without a spoken answer. Check the conversation.", + "The assistant finished without a reply to speak. Check the conversation.", ); expect(fixture.appendInstructions).toHaveBeenCalledExactlyOnceWith( expect.stringContaining("finished without a spoken answer"), diff --git a/apps/petrinaut-website/src/main/app/voice-interview/live-brunch-bridge.ts b/apps/petrinaut-website/src/main/app/voice-interview/live-brunch-bridge.ts index ca89969f732..36903d5cfa4 100644 --- a/apps/petrinaut-website/src/main/app/voice-interview/live-brunch-bridge.ts +++ b/apps/petrinaut-website/src/main/app/voice-interview/live-brunch-bridge.ts @@ -175,7 +175,7 @@ export class LiveBrunchBridge { this.#turns.clear(); if (withdrew) this.#dependencies.notice( - "Your earlier utterance was not sent because you started speaking again. Use the composer to send it.", + "Your earlier words weren’t sent. You started speaking again, so they’re in the composer if you still want them.", ); for (const turn of this.#summarizing) { if (turn.delegationId !== null && !this.#abort.signal.aborted) @@ -221,7 +221,7 @@ export class LiveBrunchBridge { submitted: false, }); this.#dependencies.notice( - "Your earlier utterance was not sent because the conversation changed. Use the composer to send it.", + "Your earlier words weren’t sent. The conversation changed, so they’re in the composer if you still want them.", ); this.#unserved( turn.delegationId, @@ -395,7 +395,7 @@ export class LiveBrunchBridge { // Newer speech already replaced these words; resending them is not asked for. if (input.superseded) return; this.#dependencies.notice( - "That utterance was not retained. Wait for the pending input, then use the composer to send it.", + "Those words weren’t sent. They’re in the composer to send when the assistant is ready.", ); const history = this.#dependencies.mediation.history; const waiting = this.#waitingForComposer; @@ -491,8 +491,8 @@ export class LiveBrunchBridge { }); this.#dependencies.notice( turn.submissionId - ? "Your message was admitted, but its response could not be confirmed. Check canonical history; no automatic retry was made." - : "Voice admission could not be confirmed. Check canonical history before sending again; no automatic retry was made.", + ? "Couldn’t confirm the answer. Your message was sent; check the conversation before sending it again." + : "Couldn’t confirm your message was sent. Check the conversation before sending it again.", ); this.#unconfirmed(turn); } @@ -712,7 +712,7 @@ export class LiveBrunchBridge { delegationId: turn.delegationId, }); this.#dependencies.notice( - "Brunch did not complete this turn. Check the conversation; no result was offered to Live.", + "The assistant didn’t finish. Nothing will be spoken; check the conversation.", ); this.#preparations.delete(turn.preparation); if (settlements.some((settlement) => settlement?.outcome === "aborted")) @@ -880,7 +880,7 @@ export class LiveBrunchBridge { delegationId: turn.delegationId, }); this.#dependencies.notice( - "Brunch settled without a spoken answer. Check the conversation.", + "The assistant finished without a reply to speak. Check the conversation.", ); this.#unserved( turn.delegationId, @@ -953,7 +953,7 @@ export class LiveBrunchBridge { } catch { if (!turn.preparation.signal.aborted && !this.#abort.signal.aborted) { this.#dependencies.notice( - "Brunch finished, but its spoken summary could not be prepared. Read the written answer; no automatic retry was made.", + "Couldn’t prepare a spoken summary. The written answer is in the conversation.", ); this.#unserved( turn.delegationId, diff --git a/apps/petrinaut-website/src/main/app/voice-interview/live-conversation-control.test.tsx b/apps/petrinaut-website/src/main/app/voice-interview/live-conversation-control.test.tsx index e99d153e696..eae3afa69ab 100644 --- a/apps/petrinaut-website/src/main/app/voice-interview/live-conversation-control.test.tsx +++ b/apps/petrinaut-website/src/main/app/voice-interview/live-conversation-control.test.tsx @@ -452,7 +452,7 @@ test("requires renewed consent to save finalized spoken words beyond the browser ).toBeTruthy(); expect( screen.getByText( - /Brunch saves your finalized spoken words, the brief and its answer/, + /Your transcribed words, the request sent to the assistant and its answer are saved/, ), ).toBeTruthy(); expect( @@ -659,7 +659,9 @@ test("reuses setup and reports failure to the host dock and notification surface screen.getByRole("region", { name: "Voice mode consent" }), ).toBeTruthy(); expect(screen.getByText("Start a voice conversation")).toBeTruthy(); - expect(screen.getByText(/OpenAI processes microphone audio/)).toBeTruthy(); + expect( + screen.getByText(/OpenAI processes your microphone audio/), + ).toBeTruthy(); expect(screen.queryByText(/experimental|best-effort/i)).toBeNull(); expect(screen.getByRole("button", { name: "Cancel" })).toBeTruthy(); expect( @@ -733,8 +735,7 @@ test("reuses setup and reports failure to the host dock and notification surface microphoneLevel: 0.12, }), ); - const playbackNotice = - "Audio playback is blocked. Select Play voice audio to hear Live."; + const playbackNotice = "Audio blocked. Select Play to listen."; act(() => onState({ phase: "connected", @@ -773,7 +774,7 @@ test("reuses setup and reports failure to the host dock and notification surface .value as ReturnType; expect(liveSession.retryPlayback).toHaveBeenCalledOnce(); const connectionError = - "live session request failed (HTTP 502, provider HTTP 401). No automatic retry was made."; + "live session request failed (HTTP 502, provider HTTP 401)."; act(() => onState({ phase: "error", @@ -1104,7 +1105,7 @@ test("pins provider, ends through host controls, and never submits or stops cano expect(createLiveConversation).toHaveBeenCalledOnce(); expect( screen.getByText( - "OpenAI processes live audio and speaks the interviewer’s words. Petrinaut saves finalized answers—not audio.", + "OpenAI processes your audio and speaks the interviewer’s questions. Your transcribed answers are saved; audio isn’t.", ), ).toBeTruthy(); expect(screen.getByRole("button", { name: "Start voice" })).toBeTruthy(); @@ -1752,10 +1753,11 @@ test.each(["commentary", "instructions"] as const)( ); } } - for (const [status, text] of [ - ["local-failure", "could not be sent to Live locally"], - ["rejected", "was rejected by Live"], - ] as const) { + const text = + kind === "commentary" + ? "Couldn’t speak the answer." + : "Voice may be out of sync."; + for (const status of ["local-failure", "rejected"] as const) { act(() => call[4]({ ...result, eventId: status, status })); expect(props.reportVoiceSessionState).toHaveBeenLastCalledWith( expect.objectContaining({ diff --git a/apps/petrinaut-website/src/main/app/voice-interview/live-conversation-control.tsx b/apps/petrinaut-website/src/main/app/voice-interview/live-conversation-control.tsx index 4bd632fe8c5..f9c4af77091 100644 --- a/apps/petrinaut-website/src/main/app/voice-interview/live-conversation-control.tsx +++ b/apps/petrinaut-website/src/main/app/voice-interview/live-conversation-control.tsx @@ -304,14 +304,10 @@ export const LiveConversationControl = ({ if (result.status === "unknown" || result.status === "accepted") return; // Quiet interruption context is best effort, not an audible answer. if (result.kind === "thinking") return; - const label = - result.kind === "commentary" ? "answer" : "continuation instruction"; - const outcome = - result.status === "local-failure" - ? "could not be sent to Live locally" - : "was rejected by Live"; setWarningMessage( - `The ${label} ${outcome}. Check the conversation; no automatic retry or replay was made. Acceptance does not confirm playback.`, + result.kind === "commentary" + ? "Couldn’t speak the answer. The written answer is in the conversation." + : "Voice may be out of sync. Check the conversation before relying on what it says.", ); }, audioSettingsStore, @@ -570,7 +566,7 @@ export const LiveConversationControl = ({ setMicrophoneCheck("Microphone ready. No audio was sent."); } catch { setMicrophoneCheck( - "Microphone access was not available. Check your browser permissions and try again.", + "Couldn’t access the microphone. Check your browser permissions.", ); } finally { setCheckingMicrophone(false); diff --git a/apps/petrinaut-website/src/main/app/voice-interview/live-conversation.test.ts b/apps/petrinaut-website/src/main/app/voice-interview/live-conversation.test.ts index 6219e94cc40..3c90b3fd5dc 100644 --- a/apps/petrinaut-website/src/main/app/voice-interview/live-conversation.test.ts +++ b/apps/petrinaut-website/src/main/app/voice-interview/live-conversation.test.ts @@ -1238,16 +1238,14 @@ test.each(["rejects", "throws"] as const)( expect(fixture.onState).toHaveBeenLastCalledWith({ phase: "connected", - message: - "Audio playback is blocked. Select Play voice audio to hear Live.", + message: "Audio blocked. Select Play to listen.", playbackBlocked: true, activity: { microphoneLevel: 0.42, outputActive: false }, }); await vi.advanceTimersByTimeAsync(100); expect(fixture.onState.mock.lastCall?.[0]).toMatchObject({ phase: "connected", - message: - "Audio playback is blocked. Select Play voice audio to hear Live.", + message: "Audio blocked. Select Play to listen.", playbackBlocked: true, }); expect(fixture.input.stop).not.toHaveBeenCalled(); @@ -1391,7 +1389,7 @@ test("reports the failed endpoint and HTTP statuses without reflecting response expect(fixture.onState.mock.lastCall?.[0].message).not.toContain("sensitive"); expect(warning).toHaveBeenCalledExactlyOnceWith( "[Petrinaut Live]", - "live session request failed (HTTP 502, provider HTTP 401). No automatic retry was made.", + "live session request failed (HTTP 502, provider HTTP 401).", ); expect(fixture.input.stop).toHaveBeenCalledOnce(); expect( diff --git a/apps/petrinaut-website/src/main/app/voice-interview/live-conversation.ts b/apps/petrinaut-website/src/main/app/voice-interview/live-conversation.ts index e3044791b60..a690b8cd074 100644 --- a/apps/petrinaut-website/src/main/app/voice-interview/live-conversation.ts +++ b/apps/petrinaut-website/src/main/app/voice-interview/live-conversation.ts @@ -134,9 +134,7 @@ export const createLiveConversation = ( activity?: LiveConversationState["activity"], ): LiveConversationState => ({ phase, - message: playbackBlocked - ? "Audio playback is blocked. Select Play voice audio to hear Live." - : null, + message: playbackBlocked ? "Audio blocked. Select Play to listen." : null, ...(playbackBlocked ? { playbackBlocked: true } : {}), ...(activity ? { activity } : {}), }); @@ -268,10 +266,7 @@ export const createLiveConversation = ( recoveryTimers.set( kind, setTimeout( - () => - fail( - `${label} media connection did not recover. No automatic retry was made.`, - ), + () => fail(`${label} media connection did not recover.`), connectionTimeoutMs, ), ); @@ -372,9 +367,7 @@ export const createLiveConversation = ( ([, previous]) => previous === null, ); if (roots.length > 1) { - fail( - "Transcription item ordering conflicted. No automatic retry was made.", - ); + fail("Transcription item ordering conflicted."); return; } let itemId = roots[0]?.[0]; @@ -490,9 +483,7 @@ export const createLiveConversation = ( conflictingSuccessor || (ancestor !== null && ancestor !== undefined) ) { - fail( - "Transcription item ordering conflicted. No automatic retry was made.", - ); + fail("Transcription item ordering conflicted."); return; } committedPrevious.set(data.item_id, data.previous_item_id); @@ -517,7 +508,7 @@ export const createLiveConversation = ( existing && (existing.id !== input.id || existing.text !== input.text) ) { - fail("Transcription identity conflicted. No automatic retry was made."); + fail("Transcription identity conflicted."); return; } if (!existing) { @@ -532,7 +523,7 @@ export const createLiveConversation = ( data.type === "error" || data.type === "session.error" ) - fail("Transcription failed. No fallback or automatic retry was made."); + fail("Transcription failed."); }; const parseEvent = (kind: ConnectionKind, event: MessageEvent) => { @@ -728,7 +719,7 @@ export const createLiveConversation = ( reportAppendResult({ ...pending, status: "rejected" }); return; } - fail("Live reported an error. No automatic retry was made."); + fail("Live reported an error."); } }; @@ -849,7 +840,7 @@ export const createLiveConversation = ( ? `, provider HTTP ${upstreamStatus}` : ""; fail( - `${kind} session request failed (HTTP ${response.status}${providerStatus}). No automatic retry was made.`, + `${kind} session request failed (HTTP ${response.status}${providerStatus}).`, ); throw new Error("Session creation failed"); } @@ -881,10 +872,7 @@ export const createLiveConversation = ( logLiveDiagnostic("session.starting", { sessionId }); onState(activeState("connecting")); connectionTimer = setTimeout( - () => - fail( - `Voice connections timed out (${connectionProgress()}). No automatic retry was made.`, - ), + () => fail(`Voice connections timed out (${connectionProgress()}).`), connectionTimeoutMs, ); try { @@ -921,9 +909,7 @@ export const createLiveConversation = ( createConnection("transcription", stream), ]); } catch { - fail( - `Voice could not connect (${connectionProgress()}). No automatic retry was made.`, - ); + fail(`Voice could not connect (${connectionProgress()}).`); } }; diff --git a/apps/petrinaut-website/src/main/app/voice-interview/voice-interview-control.test.tsx b/apps/petrinaut-website/src/main/app/voice-interview/voice-interview-control.test.tsx index f42de9c9837..96927df4b25 100644 --- a/apps/petrinaut-website/src/main/app/voice-interview/voice-interview-control.test.tsx +++ b/apps/petrinaut-website/src/main/app/voice-interview/voice-interview-control.test.tsx @@ -388,7 +388,7 @@ describe("voice interview control", () => { ).not.toBeNull(); expect( within(disclosure).getByText( - "OpenAI processes live audio and speaks the interviewer’s words. Petrinaut saves finalized answers—not audio.", + "OpenAI processes your audio and speaks the interviewer’s questions. Your transcribed answers are saved; audio isn’t.", ), ).not.toBeNull(); diff --git a/apps/petrinaut-website/src/main/app/voice-interview/voice-interview-disclosure.tsx b/apps/petrinaut-website/src/main/app/voice-interview/voice-interview-disclosure.tsx index 440d8fdfc22..35663a443ad 100644 --- a/apps/petrinaut-website/src/main/app/voice-interview/voice-interview-disclosure.tsx +++ b/apps/petrinaut-website/src/main/app/voice-interview/voice-interview-disclosure.tsx @@ -98,8 +98,8 @@ export const VoiceInterviewDisclosure = ({ })} > {experimental - ? "OpenAI processes microphone audio for voice and transcription, and text to prepare briefs and summaries. Brunch saves your finalized spoken words, the brief and its answer. Spoken captions stay in this browser; Petrinaut does not save audio." - : "OpenAI processes live audio and speaks the interviewer’s words. Petrinaut saves finalized answers—not audio."} + ? "OpenAI processes your microphone audio and the conversation text. Your transcribed words, the request sent to the assistant and its answer are saved. Audio isn’t saved, and captions stay in this browser." + : "OpenAI processes your audio and speaks the interviewer’s questions. Your transcribed answers are saved; audio isn’t."}

    { test("describes recoverable transcript rejections", () => { expect(mapSnapshot({ inputNotice: "not-heard" })?.notice).toBe( - "We didn't catch that. Please try again.", + "We didn't catch that. Try again.", ); expect(mapSnapshot({ inputNotice: "too-long" })?.notice).toBe( - "That answer is too long. Please try a shorter response.", + "That answer is too long. Try a shorter one.", ); }); diff --git a/apps/petrinaut-website/src/main/app/voice-interview/voice-session-state.ts b/apps/petrinaut-website/src/main/app/voice-interview/voice-session-state.ts index 1a741e8b3d8..7cdcb830574 100644 --- a/apps/petrinaut-website/src/main/app/voice-interview/voice-session-state.ts +++ b/apps/petrinaut-website/src/main/app/voice-interview/voice-session-state.ts @@ -109,11 +109,11 @@ export const toVoiceSessionState = ({ snapshot.inputNotice === "answer-pending" ? "Answer captured. Working on it." : snapshot.inputNotice === "answer-already-pending" - ? "Previous answer waiting. Please try again after it is sent." + ? "Still sending your last answer. Try again in a moment." : snapshot.inputNotice === "not-heard" - ? "We didn't catch that. Please try again." + ? "We didn't catch that. Try again." : snapshot.inputNotice === "too-long" - ? "That answer is too long. Please try a shorter response." + ? "That answer is too long. Try a shorter one." : null, phase: phaseOf(snapshot), speakerMuted: snapshot.speakerMuted, diff --git a/apps/petrinaut-website/src/server/voice/openai-realtime-call.test.ts b/apps/petrinaut-website/src/server/voice/openai-realtime-call.test.ts index 69e67c00490..1091e7ad42d 100644 --- a/apps/petrinaut-website/src/server/voice/openai-realtime-call.test.ts +++ b/apps/petrinaut-website/src/server/voice/openai-realtime-call.test.ts @@ -263,7 +263,7 @@ describe("OpenAI Realtime call handler", () => { expect(response.status).toBe(502); const responseBody = await response.text(); expect(responseBody).toBe( - "The voice connection returned an invalid response. Try again; if it continues, give the diagnostic reference to an operator.", + "The voice connection returned an invalid response. Try again. If it keeps happening, share the diagnostic reference with support.", ); expect(responseBody).not.toContain("secret"); expect(response.headers.get(VOICE_ERROR_CODE_HEADER)).toBe( @@ -288,7 +288,7 @@ describe("OpenAI Realtime call handler", () => { expect(response.status).toBe(502); expect(await response.text()).toBe( - "The voice connection returned an invalid response. Try again; if it continues, give the diagnostic reference to an operator.", + "The voice connection returned an invalid response. Try again. If it keeps happening, share the diagnostic reference with support.", ); }); @@ -307,7 +307,7 @@ describe("OpenAI Realtime call handler", () => { expect(response.status).toBe(400); expect(await response.text()).toBe( - "The voice connection returned an invalid response. Try again; if it continues, give the diagnostic reference to an operator.", + "The voice connection returned an invalid response. Try again. If it keeps happening, share the diagnostic reference with support.", ); expect(response.headers.get(VOICE_ERROR_CODE_HEADER)).toBe( "invalid-response", @@ -360,7 +360,7 @@ describe("OpenAI Realtime call handler", () => { const response = await responsePromise; expect(response.status).toBe(504); expect(await response.text()).toBe( - "The voice connection timed out. Check your connection, then reconnect voice input.", + "The voice connection timed out. Check your connection, then reconnect.", ); expect(response.headers.get(VOICE_ERROR_CODE_HEADER)).toBe("timeout"); expect(response.headers.get("cache-control")).toBe("no-store"); @@ -438,7 +438,7 @@ describe("OpenAI Realtime call handler", () => { expect(response.status).toBe(502); expect(response.headers.get(VOICE_ERROR_CODE_HEADER)).toBe("network"); expect(await response.text()).toBe( - "The voice connection could not be reached. Check your connection, then reconnect voice input.", + "The voice connection could not be reached. Check your connection, then reconnect.", ); expect(JSON.stringify(reportDiagnostic.mock.calls)).not.toContain( "private network diagnostics", diff --git a/apps/petrinaut-website/src/voice-diagnostics.ts b/apps/petrinaut-website/src/voice-diagnostics.ts index 6a64266e10d..23b6a7da24a 100644 --- a/apps/petrinaut-website/src/voice-diagnostics.ts +++ b/apps/petrinaut-website/src/voice-diagnostics.ts @@ -111,18 +111,16 @@ export const voiceErrorMessage = ( code: VoiceErrorCode, ): string => { if (code === "microphone-permission") { - return "Allow microphone access in your browser settings, then reconnect voice input."; + return "Microphone blocked. Allow access in your browser settings, then reconnect."; } if (code === "microphone-device") { - return "No usable microphone was found. Connect or select one, then reconnect voice input."; + return "No microphone found. Connect or select one, then reconnect."; } const visibleTextFallback = - operation === "speech" ? " Read the visible response instead." : ""; + operation === "speech" ? " Read the written answer instead." : ""; const reconnect = - operation === "speech" - ? "" - : " Check your connection, then reconnect voice input."; + operation === "speech" ? "" : " Check your connection, then reconnect."; const subject = operation === "connection" ? "voice connection" : `${operation} service`; @@ -134,9 +132,9 @@ export const voiceErrorMessage = ( case "timeout": return `The ${subject} timed out.${visibleTextFallback}${reconnect}`; case "invalid-response": - return `The ${subject} returned an invalid response.${visibleTextFallback} Try again; if it continues, give the diagnostic reference to an operator.`; + return `The ${subject} returned an invalid response.${visibleTextFallback} Try again. If it keeps happening, share the diagnostic reference with support.`; case "unavailable": - return `The ${subject} preview is unavailable or disabled.${visibleTextFallback} Continue with the text composer.`; + return `The ${subject} is unavailable.${visibleTextFallback} You can keep going in chat.`; } }; diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/ai-assistant-contents.stories.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/ai-assistant-contents.stories.tsx index 03b098083ca..91a967324c5 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/ai-assistant-contents.stories.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/ai-assistant-panel/ai-assistant-contents.stories.tsx @@ -268,9 +268,8 @@ const HostVoiceSlotPreview = () => (
    Voice mode - OpenAI processes live audio to speak the interviewer's questions. - Petrinaut keeps finalized answers in the conversation rather than the - audio. + OpenAI processes your audio and speaks the interviewer’s questions. Your + transcribed answers are saved; audio isn’t.