Merge pull request #157 from cynarAI/claude/composer-mic-send-toggle-jbg7wv

Composer: mic sits in the send slot until there is something to send
This commit is contained in:
cynarAI 2026-10-05 15:38:32 +02:00 committed by GitHub
commit 568d37c48c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 68 additions and 213 deletions

View File

@ -78,7 +78,7 @@ test("sidebar Now and Recent surface active and terminal runs", async ({ page },
await captureActivitySidebar(page, testInfo, "58-activity-now"); await captureActivitySidebar(page, testInfo, "58-activity-now");
await page.getByRole("button", { name: "Stop", exact: true }).click(); await page.getByRole("button", { name: "Stop", exact: true }).click();
await expect(page.getByRole("button", { name: "Send" })).toBeVisible({ timeout: 30_000 }); await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible({ timeout: 30_000 });
await page.reload(); await page.reload();
await expect(activityToggle).toHaveAttribute("aria-pressed", "true"); await expect(activityToggle).toHaveAttribute("aria-pressed", "true");

View File

@ -195,14 +195,23 @@ test("chat composer controls are vertically centered", async ({ page }) => {
await signup(page, `composer-layout-${stamp}@rakazo.test`, "password12", "Composer Layout"); await signup(page, `composer-layout-${stamp}@rakazo.test`, "password12", "Composer Layout");
await completeOnboarding(page); await completeOnboarding(page);
const centers = await page.getByTestId("composer-bar").evaluate((composer) => const bar = page.getByTestId("composer-bar");
["Add", "Message", "Voice", "Send"].map((label) => { const centersOf = (labels: string[]) =>
const element = composer.querySelector<HTMLElement>(`[aria-label="${label}"]`); bar.evaluate(
if (!element) throw new Error(`Missing composer control: ${label}`); (composer, controls) =>
const box = element.getBoundingClientRect(); controls.map((label) => {
return box.top + box.height / 2; const element = composer.querySelector<HTMLElement>(`[aria-label="${label}"]`);
}), if (!element) throw new Error(`Missing composer control: ${label}`);
); const box = element.getBoundingClientRect();
return box.top + box.height / 2;
}),
labels,
);
const idle = await centersOf(["Add", "Message", "Voice"]);
await page.getByRole("combobox", { name: "Message" }).fill("hello");
const typing = await centersOf(["Add", "Message", "Send"]);
const centers = [...idle, ...typing];
expect(Math.max(...centers) - Math.min(...centers)).toBeLessThanOrEqual(1); expect(Math.max(...centers) - Math.min(...centers)).toBeLessThanOrEqual(1);
}); });

View File

@ -378,12 +378,14 @@ test("sign-in, spawn, and stop work in the shell", async ({ page }, testInfo) =>
await expect(page.getByText("Messages sent now guide the next turn.")).toHaveCount(0); await expect(page.getByText("Messages sent now guide the next turn.")).toHaveCount(0);
await expect(page.getByText(/^Steer /)).toHaveCount(0); await expect(page.getByText(/^Steer /)).toHaveCount(0);
await expect(composer).toHaveAttribute("placeholder", "Message…"); await expect(composer).toHaveAttribute("placeholder", "Message…");
await expect(page.getByRole("button", { name: "Send", exact: true })).toBeVisible(); // An empty draft offers voice in the send slot; typing turns it into Send.
const voice = page
.getByTestId("composer-bar")
.getByRole("button", { name: "Voice", exact: true });
await expect(voice).toBeVisible();
await expect(page.getByRole("button", { name: "Send", exact: true })).toHaveCount(0);
await composer.fill("Use the newer report and keep the answer short."); await composer.fill("Use the newer report and keep the answer short.");
await page.keyboard.press("Tab"); await expect(voice).toHaveCount(0);
await expect(
page.getByTestId("composer-bar").getByRole("button", { name: "Voice", exact: true }),
).toBeFocused();
await page.keyboard.press("Tab"); await page.keyboard.press("Tab");
await expect(page.getByRole("button", { name: "Send", exact: true })).toBeFocused(); await expect(page.getByRole("button", { name: "Send", exact: true })).toBeFocused();
await page.keyboard.press("Enter"); await page.keyboard.press("Enter");
@ -393,7 +395,7 @@ test("sign-in, spawn, and stop work in the shell", async ({ page }, testInfo) =>
await expect(page.getByRole("button", { name: "Stop", exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: "Stop", exact: true })).toBeVisible();
await captureScreenshot(page, testInfo, "14-active-bot-work"); await captureScreenshot(page, testInfo, "14-active-bot-work");
await page.setViewportSize({ width: 390, height: 844 }); await page.setViewportSize({ width: 390, height: 844 });
await expect(page.getByRole("button", { name: "Send", exact: true })).toBeVisible(); await expect(voice).toBeVisible();
await expect(page.getByRole("button", { name: "Stop", exact: true })).toBeVisible(); await expect(page.getByRole("button", { name: "Stop", exact: true })).toBeVisible();
await expect(composer).toHaveAttribute("placeholder", "Message…"); await expect(composer).toHaveAttribute("placeholder", "Message…");
await captureScreenshot(page, testInfo, "14-active-bot-work-mobile"); await captureScreenshot(page, testInfo, "14-active-bot-work-mobile");
@ -414,14 +416,13 @@ test("sign-in, spawn, and stop work in the shell", async ({ page }, testInfo) =>
}); });
await page.getByRole("button", { name: "Stop", exact: true }).click(); await page.getByRole("button", { name: "Stop", exact: true }).click();
await stopRequestStarted; await stopRequestStarted;
await expect(page.getByRole("button", { name: "Send", exact: true })).toBeDisabled(); await expect(voice).toBeVisible();
await expect(page.getByRole("button", { name: "Stop", exact: true })).toBeDisabled(); await expect(page.getByRole("button", { name: "Stop", exact: true })).toBeDisabled();
releaseStopRequest(); releaseStopRequest();
// Idle Send stays disabled with an empty draft; wait for Stop to leave instead.
await expect(page.getByRole("button", { name: "Stop", exact: true })).toHaveCount(0, { await expect(page.getByRole("button", { name: "Stop", exact: true })).toHaveCount(0, {
timeout: 30_000, timeout: 30_000,
}); });
await expect(page.getByRole("button", { name: "Send", exact: true })).toBeVisible(); await expect(voice).toBeVisible();
await page.context().clearCookies(); await page.context().clearCookies();
await page.goto("/sign-in"); await page.goto("/sign-in");

View File

@ -45,7 +45,7 @@ test("shows peer chips in transcript and opens view-only peer chat", async ({ pa
) )
.toBe(true); .toBe(true);
await expect(page.getByRole("button", { name: "Send" })).toBeVisible({ timeout: 60_000 }); await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible({ timeout: 60_000 });
const transcript = page.getByTestId("transcript"); const transcript = page.getByTestId("transcript");
const chip = transcript const chip = transcript

View File

@ -150,7 +150,7 @@ test("routine test-run completes and survives reload", async ({ page }, testInfo
await routine.click(); await routine.click();
await page.getByRole("button", { name: "Test run" }).click(); await page.getByRole("button", { name: "Test run" }).click();
await expect(page.getByText(/routine-run-now-ok/i).first()).toBeVisible({ timeout: 30_000 }); await expect(page.getByText(/routine-run-now-ok/i).first()).toBeVisible({ timeout: 30_000 });
await expect(page.getByRole("button", { name: "Send" })).toBeVisible({ timeout: 30_000 }); await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible({ timeout: 30_000 });
await captureScreenshot(page, testInfo, "34-routine-run-completed"); await captureScreenshot(page, testInfo, "34-routine-run-completed");
await page.reload(); await page.reload();

View File

@ -54,6 +54,10 @@ test("voice settings connect a key, speak a reply, and open a call", async ({ pa
const composer = page.getByPlaceholder(/Message/); const composer = page.getByPlaceholder(/Message/);
await composer.fill("say hello"); await composer.fill("say hello");
await expect(
page.getByTestId("composer-bar").getByRole("button", { name: "Voice", exact: true }),
).toHaveCount(0);
await captureScreenshot(page, testInfo, "voice-composer-typing");
await page.keyboard.press("Enter"); await page.keyboard.press("Enter");
// A reply can render more than one text bubble; speak the latest one. // A reply can render more than one text bubble; speak the latest one.
const speakReply = page.getByRole("button", { name: "Speak this reply" }).last(); const speakReply = page.getByRole("button", { name: "Speak this reply" }).last();

View File

@ -24,7 +24,6 @@ import {
import { personaIdlePrompt, personaThinkingLines } from "../../lib/voice-persona-copy"; import { personaIdlePrompt, personaThinkingLines } from "../../lib/voice-persona-copy";
import { VoiceAwarenessStrip } from "../voice/VoiceAwarenessStrip"; import { VoiceAwarenessStrip } from "../voice/VoiceAwarenessStrip";
import { StageHero } from "./StageHero"; import { StageHero } from "./StageHero";
import { StageVoiceEntry } from "./StageVoiceEntry";
import { TextStageCanvas } from "./TextStageCanvas"; import { TextStageCanvas } from "./TextStageCanvas";
export function OoStage({ export function OoStage({
@ -37,8 +36,6 @@ export function OoStage({
snapshot, snapshot,
running, running,
hasMessages, hasMessages,
voiceReady = false,
onStartVoice,
}: { }: {
botId: string; botId: string;
botName: string; botName: string;
@ -49,8 +46,6 @@ export function OoStage({
snapshot: ThreadSnapshot | null; snapshot: ThreadSnapshot | null;
running: boolean; running: boolean;
hasMessages: boolean; hasMessages: boolean;
voiceReady?: boolean;
onStartVoice?: () => void;
}) { }) {
const { t } = useLingui(); const { t } = useLingui();
const [reducedMotion, setReducedMotion] = useState(false); const [reducedMotion, setReducedMotion] = useState(false);
@ -242,13 +237,6 @@ export function OoStage({
> >
{subtitle} {subtitle}
</p> </p>
{onStartVoice ? (
<StageVoiceEntry
voiceReady={voiceReady}
botColor={botColor}
onStartVoice={onStartVoice}
/>
) : null}
</> </>
) : ( ) : (
<p className="mt-1 w-full max-w-xl truncate text-center text-ui-caption text-muted-foreground"> <p className="mt-1 w-full max-w-xl truncate text-center text-ui-caption text-muted-foreground">
@ -306,14 +294,10 @@ export function OoGroupStage({
groupName, groupName,
snapshot, snapshot,
enrichMember, enrichMember,
voiceReady = false,
onStartVoice,
}: { }: {
groupName?: string; groupName?: string;
snapshot: ThreadSnapshot | null; snapshot: ThreadSnapshot | null;
enrichMember?: (botId: string) => Pick<GroupStageMember, "title" | "description"> | undefined; enrichMember?: (botId: string) => Pick<GroupStageMember, "title" | "description"> | undefined;
voiceReady?: boolean;
onStartVoice?: () => void;
}) { }) {
const { t } = useLingui(); const { t } = useLingui();
const [reducedMotion, setReducedMotion] = useState(false); const [reducedMotion, setReducedMotion] = useState(false);
@ -552,14 +536,6 @@ export function OoGroupStage({
{subtitle} {subtitle}
</p> </p>
)} )}
{onStartVoice && !hasMessages ? (
<StageVoiceEntry
voiceReady={voiceReady}
botColor={speaker?.color}
onStartVoice={onStartVoice}
/>
) : null}
</div> </div>
</section> </section>
); );

View File

@ -1,43 +0,0 @@
import { Trans, useLingui } from "@lingui/react/macro";
import { Button } from "@rakazo/ui-web";
import { Mic } from "lucide-react";
import { VoiceAuroraRim } from "../voice/VoiceAurora";
export function StageVoiceEntry({
voiceReady,
botColor,
onStartVoice,
}: {
voiceReady: boolean;
botColor?: string;
onStartVoice: () => void;
}) {
const { t } = useLingui();
return (
<div className="mt-5 flex w-full max-w-md flex-col items-center gap-2">
<Button
type="button"
variant="outline"
onClick={onStartVoice}
data-testid="stage-voice-primary"
aria-label={voiceReady ? t`Start talking` : t`Set up voice`}
title={voiceReady ? t`Start talking` : t`Set up voice`}
className={`tactile-press h-control-lg w-full max-w-sm gap-2.5 rounded-full px-6 text-ui-body font-semibold transition-all ${
voiceReady
? "relative overflow-hidden border-border bg-foreground/5 text-foreground hover:bg-foreground/10"
: "border-border bg-foreground/5 text-foreground/80 hover:bg-foreground/10"
}`}
>
{voiceReady ? (
<VoiceAuroraRim botColor={botColor} className="inset-x-[12%] h-0.5 blur-[3px]" />
) : null}
<Mic strokeWidth={2} className="size-icon-lg shrink-0" aria-hidden="true" />
{voiceReady ? <Trans>Start talking</Trans> : <Trans>Set up voice</Trans>}
</Button>
<p className="text-center text-ui-caption text-muted-foreground">
<Trans>Voice first — transcript below</Trans>
</p>
</div>
);
}

View File

@ -4567,7 +4567,6 @@ msgstr "Servername"
msgid "Server URL" msgid "Server URL"
msgstr "Server-URL" msgstr "Server-URL"
#: src/components/stage/StageVoiceEntry.tsx
#: src/pages/shell/composer.tsx #: src/pages/shell/composer.tsx
msgid "Set up voice" msgid "Set up voice"
msgstr "Stimme einrichten" msgstr "Stimme einrichten"
@ -4822,10 +4821,6 @@ msgstr "Chat starten"
msgid "Start recording" msgid "Start recording"
msgstr "Aufnahme starten" msgstr "Aufnahme starten"
#: src/components/stage/StageVoiceEntry.tsx
msgid "Start talking"
msgstr "Sprechen starten"
#: src/pages/shell/transcript.tsx #: src/pages/shell/transcript.tsx
msgid "Start the conversation — {activeName} is on stage above." msgid "Start the conversation — {activeName} is on stage above."
msgstr "Starte die Unterhaltung — {activeName} steht oben auf der Bühne." msgstr "Starte die Unterhaltung — {activeName} steht oben auf der Bühne."
@ -5346,10 +5341,6 @@ msgstr "Stimme"
msgid "Voice \"{0}\" not found." msgid "Voice \"{0}\" not found."
msgstr "Stimme „{0}“ nicht gefunden." msgstr "Stimme „{0}“ nicht gefunden."
#: src/components/stage/StageVoiceEntry.tsx
msgid "Voice first — transcript below"
msgstr "Primär per Stimme — Transkript unten"
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
msgid "Voice is not available yet. You can turn it on later in settings." msgid "Voice is not available yet. You can turn it on later in settings."
msgstr "Die Stimme ist noch nicht verfügbar. Du kannst sie später in den Einstellungen aktivieren." msgstr "Die Stimme ist noch nicht verfügbar. Du kannst sie später in den Einstellungen aktivieren."

View File

@ -4567,7 +4567,6 @@ msgstr "Server name"
msgid "Server URL" msgid "Server URL"
msgstr "Server URL" msgstr "Server URL"
#: src/components/stage/StageVoiceEntry.tsx
#: src/pages/shell/composer.tsx #: src/pages/shell/composer.tsx
msgid "Set up voice" msgid "Set up voice"
msgstr "Set up voice" msgstr "Set up voice"
@ -4822,10 +4821,6 @@ msgstr "Start chat"
msgid "Start recording" msgid "Start recording"
msgstr "Start recording" msgstr "Start recording"
#: src/components/stage/StageVoiceEntry.tsx
msgid "Start talking"
msgstr "Start talking"
#: src/pages/shell/transcript.tsx #: src/pages/shell/transcript.tsx
msgid "Start the conversation — {activeName} is on stage above." msgid "Start the conversation — {activeName} is on stage above."
msgstr "Start the conversation — {activeName} is on stage above." msgstr "Start the conversation — {activeName} is on stage above."
@ -5346,10 +5341,6 @@ msgstr "Voice"
msgid "Voice \"{0}\" not found." msgid "Voice \"{0}\" not found."
msgstr "Voice \"{0}\" not found." msgstr "Voice \"{0}\" not found."
#: src/components/stage/StageVoiceEntry.tsx
msgid "Voice first — transcript below"
msgstr "Voice first — transcript below"
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
msgid "Voice is not available yet. You can turn it on later in settings." msgid "Voice is not available yet. You can turn it on later in settings."
msgstr "Voice is not available yet. You can turn it on later in settings." msgstr "Voice is not available yet. You can turn it on later in settings."

View File

@ -4567,7 +4567,6 @@ msgstr "Nombre del servidor"
msgid "Server URL" msgid "Server URL"
msgstr "URL del servidor" msgstr "URL del servidor"
#: src/components/stage/StageVoiceEntry.tsx
#: src/pages/shell/composer.tsx #: src/pages/shell/composer.tsx
msgid "Set up voice" msgid "Set up voice"
msgstr "" msgstr ""
@ -4822,10 +4821,6 @@ msgstr ""
msgid "Start recording" msgid "Start recording"
msgstr "Iniciar grabación" msgstr "Iniciar grabación"
#: src/components/stage/StageVoiceEntry.tsx
msgid "Start talking"
msgstr ""
#: src/pages/shell/transcript.tsx #: src/pages/shell/transcript.tsx
msgid "Start the conversation — {activeName} is on stage above." msgid "Start the conversation — {activeName} is on stage above."
msgstr "" msgstr ""
@ -5346,10 +5341,6 @@ msgstr "Voz"
msgid "Voice \"{0}\" not found." msgid "Voice \"{0}\" not found."
msgstr "" msgstr ""
#: src/components/stage/StageVoiceEntry.tsx
msgid "Voice first — transcript below"
msgstr ""
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
msgid "Voice is not available yet. You can turn it on later in settings." msgid "Voice is not available yet. You can turn it on later in settings."
msgstr "" msgstr ""

View File

@ -4567,7 +4567,6 @@ msgstr "सर्वर का नाम"
msgid "Server URL" msgid "Server URL"
msgstr "सर्वर URL" msgstr "सर्वर URL"
#: src/components/stage/StageVoiceEntry.tsx
#: src/pages/shell/composer.tsx #: src/pages/shell/composer.tsx
msgid "Set up voice" msgid "Set up voice"
msgstr "" msgstr ""
@ -4822,10 +4821,6 @@ msgstr ""
msgid "Start recording" msgid "Start recording"
msgstr "रिकॉर्डिंग शुरू करें" msgstr "रिकॉर्डिंग शुरू करें"
#: src/components/stage/StageVoiceEntry.tsx
msgid "Start talking"
msgstr ""
#: src/pages/shell/transcript.tsx #: src/pages/shell/transcript.tsx
msgid "Start the conversation — {activeName} is on stage above." msgid "Start the conversation — {activeName} is on stage above."
msgstr "" msgstr ""
@ -5346,10 +5341,6 @@ msgstr "आवाज़"
msgid "Voice \"{0}\" not found." msgid "Voice \"{0}\" not found."
msgstr "" msgstr ""
#: src/components/stage/StageVoiceEntry.tsx
msgid "Voice first — transcript below"
msgstr ""
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
msgid "Voice is not available yet. You can turn it on later in settings." msgid "Voice is not available yet. You can turn it on later in settings."
msgstr "" msgstr ""

View File

@ -4567,7 +4567,6 @@ msgstr "서버 이름"
msgid "Server URL" msgid "Server URL"
msgstr "서버 URL" msgstr "서버 URL"
#: src/components/stage/StageVoiceEntry.tsx
#: src/pages/shell/composer.tsx #: src/pages/shell/composer.tsx
msgid "Set up voice" msgid "Set up voice"
msgstr "" msgstr ""
@ -4822,10 +4821,6 @@ msgstr ""
msgid "Start recording" msgid "Start recording"
msgstr "작업 녹화 시작" msgstr "작업 녹화 시작"
#: src/components/stage/StageVoiceEntry.tsx
msgid "Start talking"
msgstr ""
#: src/pages/shell/transcript.tsx #: src/pages/shell/transcript.tsx
msgid "Start the conversation — {activeName} is on stage above." msgid "Start the conversation — {activeName} is on stage above."
msgstr "" msgstr ""
@ -5346,10 +5341,6 @@ msgstr "음성"
msgid "Voice \"{0}\" not found." msgid "Voice \"{0}\" not found."
msgstr "" msgstr ""
#: src/components/stage/StageVoiceEntry.tsx
msgid "Voice first — transcript below"
msgstr ""
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
msgid "Voice is not available yet. You can turn it on later in settings." msgid "Voice is not available yet. You can turn it on later in settings."
msgstr "" msgstr ""

View File

@ -4567,7 +4567,6 @@ msgstr "Nome do servidor"
msgid "Server URL" msgid "Server URL"
msgstr "URL do servidor" msgstr "URL do servidor"
#: src/components/stage/StageVoiceEntry.tsx
#: src/pages/shell/composer.tsx #: src/pages/shell/composer.tsx
msgid "Set up voice" msgid "Set up voice"
msgstr "" msgstr ""
@ -4822,10 +4821,6 @@ msgstr ""
msgid "Start recording" msgid "Start recording"
msgstr "Começar a gravar" msgstr "Começar a gravar"
#: src/components/stage/StageVoiceEntry.tsx
msgid "Start talking"
msgstr ""
#: src/pages/shell/transcript.tsx #: src/pages/shell/transcript.tsx
msgid "Start the conversation — {activeName} is on stage above." msgid "Start the conversation — {activeName} is on stage above."
msgstr "" msgstr ""
@ -5346,10 +5341,6 @@ msgstr "Voz"
msgid "Voice \"{0}\" not found." msgid "Voice \"{0}\" not found."
msgstr "" msgstr ""
#: src/components/stage/StageVoiceEntry.tsx
msgid "Voice first — transcript below"
msgstr ""
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
msgid "Voice is not available yet. You can turn it on later in settings." msgid "Voice is not available yet. You can turn it on later in settings."
msgstr "" msgstr ""

View File

@ -4567,7 +4567,6 @@ msgstr "Имя сервера"
msgid "Server URL" msgid "Server URL"
msgstr "URL-адрес сервера" msgstr "URL-адрес сервера"
#: src/components/stage/StageVoiceEntry.tsx
#: src/pages/shell/composer.tsx #: src/pages/shell/composer.tsx
msgid "Set up voice" msgid "Set up voice"
msgstr "" msgstr ""
@ -4822,10 +4821,6 @@ msgstr ""
msgid "Start recording" msgid "Start recording"
msgstr "Начать запись" msgstr "Начать запись"
#: src/components/stage/StageVoiceEntry.tsx
msgid "Start talking"
msgstr ""
#: src/pages/shell/transcript.tsx #: src/pages/shell/transcript.tsx
msgid "Start the conversation — {activeName} is on stage above." msgid "Start the conversation — {activeName} is on stage above."
msgstr "" msgstr ""
@ -5346,10 +5341,6 @@ msgstr "Голос"
msgid "Voice \"{0}\" not found." msgid "Voice \"{0}\" not found."
msgstr "" msgstr ""
#: src/components/stage/StageVoiceEntry.tsx
msgid "Voice first — transcript below"
msgstr ""
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
msgid "Voice is not available yet. You can turn it on later in settings." msgid "Voice is not available yet. You can turn it on later in settings."
msgstr "" msgstr ""

View File

@ -4567,7 +4567,6 @@ msgstr "Sunucu adı"
msgid "Server URL" msgid "Server URL"
msgstr "Sunucu URL'si" msgstr "Sunucu URL'si"
#: src/components/stage/StageVoiceEntry.tsx
#: src/pages/shell/composer.tsx #: src/pages/shell/composer.tsx
msgid "Set up voice" msgid "Set up voice"
msgstr "" msgstr ""
@ -4822,10 +4821,6 @@ msgstr ""
msgid "Start recording" msgid "Start recording"
msgstr "Kaydı başlat" msgstr "Kaydı başlat"
#: src/components/stage/StageVoiceEntry.tsx
msgid "Start talking"
msgstr ""
#: src/pages/shell/transcript.tsx #: src/pages/shell/transcript.tsx
msgid "Start the conversation — {activeName} is on stage above." msgid "Start the conversation — {activeName} is on stage above."
msgstr "" msgstr ""
@ -5346,10 +5341,6 @@ msgstr "Ses"
msgid "Voice \"{0}\" not found." msgid "Voice \"{0}\" not found."
msgstr "" msgstr ""
#: src/components/stage/StageVoiceEntry.tsx
msgid "Voice first — transcript below"
msgstr ""
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
msgid "Voice is not available yet. You can turn it on later in settings." msgid "Voice is not available yet. You can turn it on later in settings."
msgstr "" msgstr ""

View File

@ -4567,7 +4567,6 @@ msgstr "服务器名称"
msgid "Server URL" msgid "Server URL"
msgstr "服务器 URL" msgstr "服务器 URL"
#: src/components/stage/StageVoiceEntry.tsx
#: src/pages/shell/composer.tsx #: src/pages/shell/composer.tsx
msgid "Set up voice" msgid "Set up voice"
msgstr "" msgstr ""
@ -4822,10 +4821,6 @@ msgstr ""
msgid "Start recording" msgid "Start recording"
msgstr "开始录制" msgstr "开始录制"
#: src/components/stage/StageVoiceEntry.tsx
msgid "Start talking"
msgstr ""
#: src/pages/shell/transcript.tsx #: src/pages/shell/transcript.tsx
msgid "Start the conversation — {activeName} is on stage above." msgid "Start the conversation — {activeName} is on stage above."
msgstr "" msgstr ""
@ -5346,10 +5341,6 @@ msgstr "语音"
msgid "Voice \"{0}\" not found." msgid "Voice \"{0}\" not found."
msgstr "" msgstr ""
#: src/components/stage/StageVoiceEntry.tsx
msgid "Voice first — transcript below"
msgstr ""
#: src/components/onboarding/OnboardingVoiceStep.tsx #: src/components/onboarding/OnboardingVoiceStep.tsx
msgid "Voice is not available yet. You can turn it on later in settings." msgid "Voice is not available yet. You can turn it on later in settings."
msgstr "" msgstr ""

View File

@ -3323,8 +3323,6 @@ export function ShellPage() {
snapshot={activeSnapshot} snapshot={activeSnapshot}
running={transcriptRunning} running={transcriptRunning}
hasMessages={transcriptMessages.length > 0} hasMessages={transcriptMessages.length > 0}
voiceReady={Boolean(voiceStatus?.ready)}
onStartVoice={startVoiceEncounter}
/> />
) : null} ) : null}
{!callOpen && inGroup ? ( {!callOpen && inGroup ? (
@ -3332,8 +3330,6 @@ export function ShellPage() {
groupName={activeGroup?.name ?? activeSnapshot?.groupName} groupName={activeGroup?.name ?? activeSnapshot?.groupName}
snapshot={activeSnapshot} snapshot={activeSnapshot}
enrichMember={enrichStageMember} enrichMember={enrichStageMember}
voiceReady={Boolean(voiceStatus?.ready)}
onStartVoice={startVoiceEncounter}
/> />
) : null} ) : null}
<Transcript <Transcript

View File

@ -135,6 +135,38 @@ export const Composer = memo(function Composer({
selectedSkill !== null || selectedSkill !== null ||
selectedMentions.length > 0 || selectedMentions.length > 0 ||
pendingAttachments.length > 0; pendingAttachments.length > 0;
// One slot, two jobs: an empty draft talks, a written one sends.
const voiceInSlot = Boolean(onVoice) && !canSend;
const primaryAction = voiceInSlot ? (
<Button
key="voice"
size="icon"
aria-label={t`Voice`}
title={voiceReady ? t`Start voice conversation` : t`Set up voice`}
disabled={disabled}
onClick={onVoice}
data-testid="composer-voice-primary"
className="tactile-press relative size-10 shrink-0 overflow-hidden rounded-full transition-all active:scale-90 animate-in fade-in zoom-in-75 duration-150 flex items-center justify-center cursor-pointer"
>
{voiceReady ? (
<VoiceAuroraRim botColor={voiceColor} className="inset-x-[22%] h-0.5 blur-[3px]" />
) : null}
<Mic strokeWidth={2.2} className="size-icon-lg" />
</Button>
) : (
<Button
key="send"
size="icon"
aria-label={t`Send`}
disabled={!canSend || disabled}
onClick={send}
className={`tactile-press size-10 shrink-0 rounded-full transition-all active:scale-90 disabled:bg-foreground/[0.08] disabled:text-foreground/20 flex items-center justify-center cursor-pointer ${
onVoice ? "animate-in fade-in zoom-in-75 duration-150" : ""
}`}
>
<ArrowUp strokeWidth={2.6} className="size-icon-lg" />
</Button>
);
useEffect(() => { useEffect(() => {
if (!runError || !runErrorId) return; if (!runError || !runErrorId) return;
@ -743,40 +775,9 @@ export const Composer = memo(function Composer({
className="max-h-36 min-h-[28px] min-w-[8rem] flex-1 resize-none overflow-y-auto bg-transparent py-1 text-[16.5px] leading-[1.55] text-foreground outline-none placeholder:text-muted-foreground/75 disabled:opacity-40 font-normal" className="max-h-36 min-h-[28px] min-w-[8rem] flex-1 resize-none overflow-y-auto bg-transparent py-1 text-[16.5px] leading-[1.55] text-foreground outline-none placeholder:text-muted-foreground/75 disabled:opacity-40 font-normal"
/> />
</div> </div>
{onVoice ? (
<Button
variant="outline"
aria-label={t`Voice`}
title={voiceReady ? t`Start voice conversation` : t`Set up voice`}
disabled={disabled}
onClick={onVoice}
data-testid="composer-voice-primary"
className={`tactile-press h-control-lg shrink-0 gap-2 rounded-full px-4 sm:px-5 flex items-center justify-center cursor-pointer transition-all ${
voiceReady
? "relative overflow-hidden border-border bg-foreground/5 text-foreground hover:bg-foreground/10"
: "border-border bg-foreground/5 text-foreground/80 hover:bg-foreground/10"
}`}
>
{voiceReady ? (
<VoiceAuroraRim botColor={voiceColor} className="inset-x-[14%] h-0.5 blur-[3px]" />
) : null}
<Mic strokeWidth={1.9} className="size-icon-lg shrink-0" />
<span className="hidden text-ui-body font-semibold sm:inline">
<Trans>Voice</Trans>
</span>
</Button>
) : null}
{running ? ( {running ? (
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
<Button {primaryAction}
size="icon"
aria-label={t`Send`}
disabled={!canSend || disabled}
onClick={send}
className="tactile-press size-10 rounded-full transition-transform active:scale-90 flex items-center justify-center cursor-pointer"
>
<ArrowUp strokeWidth={2.6} className="size-icon-lg" />
</Button>
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
@ -789,15 +790,7 @@ export const Composer = memo(function Composer({
</Button> </Button>
</div> </div>
) : ( ) : (
<Button primaryAction
size="icon"
aria-label={t`Send`}
disabled={!canSend || disabled}
onClick={send}
className="tactile-press size-10 shrink-0 rounded-full transition-all active:scale-90 disabled:bg-foreground/[0.08] disabled:text-foreground/20 flex items-center justify-center cursor-pointer"
>
<ArrowUp strokeWidth={2.6} className="size-icon-lg" />
</Button>
)} )}
</div> </div>
</div> </div>