diff --git a/apps/desktop/src/components/Composer.tsx b/apps/desktop/src/components/Composer.tsx index b84d93f62..2ffc3cfc8 100644 --- a/apps/desktop/src/components/Composer.tsx +++ b/apps/desktop/src/components/Composer.tsx @@ -56,7 +56,6 @@ import { import { useComposerAttachments } from "../features/chat/composer/hooks/useComposerAttachments"; import { useComposerDraft } from "../features/chat/composer/hooks/useComposerDraft"; import { useComposerSubmit } from "../features/chat/composer/hooks/useComposerSubmit"; -import { useComposerSpeech } from "../features/chat/composer/hooks/useComposerSpeech"; import { ComposerInput } from "../features/chat/composer/ComposerInput"; import { useComposerModelMenu } from "../features/chat/composer/hooks/useComposerModelMenu"; import { ComposerToolbar } from "../features/chat/composer/ComposerToolbar"; @@ -437,16 +436,6 @@ export function Composer({ submit, } = submitController; - const speech = useComposerSpeech({ - activeSessionId, - value, - enhancementDraft, - activeFileReferences, - applyEditorDraft, - t, - showToast, - controlsBlocked, - }); const composerAc = useComposerAutocomplete({ value, cursor, @@ -617,12 +606,6 @@ export function Composer({ clearEnhancementError={clearEnhancementError} runActive={runActive} hasDraftContent={hasDraftContent} - transcribeEnabled={speech.transcribeEnabled} - speakEnabled={speech.speakEnabled} - transcribing={speech.transcribing} - speaking={speech.speaking} - transcribe={speech.transcribe} - speak={speech.speak} abort={abort} submit={submit} /> diff --git a/apps/desktop/src/features/chat/composer/ComposerToolbar.tsx b/apps/desktop/src/features/chat/composer/ComposerToolbar.tsx index 587e3b886..d2c899052 100644 --- a/apps/desktop/src/features/chat/composer/ComposerToolbar.tsx +++ b/apps/desktop/src/features/chat/composer/ComposerToolbar.tsx @@ -19,8 +19,6 @@ import { IconSparkles, IconStop, IconUndo2, - IconMic, - IconAudio, } from "../../../components/icons"; import { ModeIcon } from "./ComposerModeIcon"; import { ComposerModelPicker } from "./ComposerModelPicker"; @@ -64,12 +62,6 @@ export type ComposerToolbarProps = { clearEnhancementError: () => void; runActive: boolean; hasDraftContent: boolean; - transcribeEnabled: boolean; - speakEnabled: boolean; - transcribing: boolean; - speaking: boolean; - transcribe: () => Promise; - speak: () => Promise; abort: AppState["abort"]; submit: () => Promise; }; @@ -105,12 +97,6 @@ export function ComposerToolbar({ clearEnhancementError, runActive, hasDraftContent, - transcribeEnabled, - speakEnabled, - transcribing, - speaking, - transcribe, - speak, abort, submit, }: ComposerToolbarProps) { @@ -257,28 +243,6 @@ export function ComposerToolbar({ controlsBlocked={controlsBlocked} onCloseOtherMenus={() => setPermissionOpen(false)} /> - void transcribe()} - > - {transcribing ? - void speak()} - > - {speaking ? void; - -export function useComposerSpeech({ - activeSessionId, - value, - enhancementDraft, - activeFileReferences, - applyEditorDraft, - t, - showToast, - controlsBlocked, -}: { - activeSessionId: string | null | undefined; - value: string; - enhancementDraft: string; - activeFileReferences: ComposerFileReference[]; - applyEditorDraft: (next: string, refs: ComposerFileReference[], cursor: number) => void; - - t: TFunction; - showToast: SpeechToast; - controlsBlocked: boolean; -}) { - const [status, setStatus] = useState(null); - const [transcribing, setTranscribing] = useState(false); - const [speaking, setSpeaking] = useState(false); - const playbackRef = useRef(null); - - useEffect(() => { - let cancelled = false; - void api.speechStatus().then( - (next) => { - if (!cancelled) setStatus(next); - }, - () => { - if (!cancelled) setStatus(null); - }, - ); - return () => { - cancelled = true; - playbackRef.current?.pause(); - playbackRef.current = null; - }; - }, []); - - const audioAttachments = activeFileReferences.filter(isComposerAudioReference); - const transcribeEnabled = Boolean(status?.transcribe.available) && audioAttachments.length > 0 && !controlsBlocked; - const speakEnabled = Boolean(status?.synthesize.available) && Boolean(enhancementDraft.trim()) && !controlsBlocked; - - const transcribe = async () => { - if (!transcribeEnabled || transcribing) return; - setTranscribing(true); - try { - const sessionId = activeSessionId ?? (await materializeDraftSession()); - if (!sessionId) throw new Error("session unavailable"); - const chunks: string[] = []; - for (const file of audioAttachments) { - const result = await api.speechTranscribe({ - sessionId, - path: file.path, - mimeType: file.mimeType, - }); - if (result.text.trim()) chunks.push(result.text.trim()); - } - if (!chunks.length) return; - const insertion = chunks.join("\n"); - const prefix = value.trim() ? `${value.replace(/\s+$/, "")}\n` : ""; - const next = `${prefix}${insertion}`; - applyEditorDraft(next, activeFileReferences, next.length); - } catch (error) { - showToast(error instanceof Error ? error.message : t("chat.transcribeFailed"), { variant: "error" }); - } finally { - setTranscribing(false); - } - }; - - const speak = async () => { - if (!speakEnabled || speaking) return; - const text = stripInlineComposerFileReferenceTokens(enhancementDraft, activeFileReferences).trim(); - if (!text) return; - setSpeaking(true); - try { - const sessionId = activeSessionId ?? (await materializeDraftSession()); - if (!sessionId) throw new Error("session unavailable"); - const result = await api.speechSynthesize({ sessionId, text }); - if (result.dataUrl) { - const audio = new Audio(result.dataUrl); - playbackRef.current = audio; - await audio.play(); - } else { - showToast(t("chat.speakSaved", { path: result.path }), { variant: "info" }); - } - } catch (error) { - showToast(error instanceof Error ? error.message : t("chat.speakFailed"), { variant: "error" }); - } finally { - setSpeaking(false); - } - }; - - return { - transcribeEnabled, - speakEnabled, - transcribing, - speaking, - transcribe, - speak, - }; -} diff --git a/apps/desktop/test/speech-capability.test.mjs b/apps/desktop/test/speech-capability.test.mjs index 4537dea0d..19d92d621 100644 --- a/apps/desktop/test/speech-capability.test.mjs +++ b/apps/desktop/test/speech-capability.test.mjs @@ -1,4 +1,3 @@ -import { readComposerSource } from "./helpers/source-contracts.mjs"; import assert from "node:assert/strict"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { register } from "node:module"; @@ -28,15 +27,6 @@ test("speech IPC is on the typed whitelist and renderer API", async () => { assert.match(registerSrc, /registerSpeechIpc/); }); -test("Composer disables voice actions until speech is configured", async () => { - const composer = await readComposerSource(); - assert.match(composer, /status\?\.transcribe\.available/); - assert.match(composer, /status\?\.synthesize\.available/); - assert.match(composer, /disabled=\{!transcribeEnabled \|\| transcribing \|\| speaking\}/); - assert.match(composer, /disabled=\{!speakEnabled \|\| speaking \|\| transcribing\}/); - assert.match(composer, /api\.speechTranscribe/); - assert.match(composer, /api\.speechSynthesize/); -}); test("settings reject an illegal speech protocol id", async () => { const { validateSpeechSettings } = await import("@pi-desktop/shared");