diff --git a/crates/buzz-relay/src/api/transcribe.rs b/crates/buzz-relay/src/api/transcribe.rs index ff8af8a99..f66c584e4 100644 --- a/crates/buzz-relay/src/api/transcribe.rs +++ b/crates/buzz-relay/src/api/transcribe.rs @@ -82,8 +82,7 @@ pub async fn create_transcribe_session( .header("Content-Type", "application/json") .json(&serde_json::json!({ "session": { - "model": "gpt-4o-mini-realtime-preview", - "modalities": ["text"], + "type": "transcription", "input_audio_transcription": { "model": model, }, diff --git a/desktop/src/features/dictation/hooks/useComposerDictation.ts b/desktop/src/features/dictation/hooks/useComposerDictation.ts index d907062be..808787059 100644 --- a/desktop/src/features/dictation/hooks/useComposerDictation.ts +++ b/desktop/src/features/dictation/hooks/useComposerDictation.ts @@ -1,10 +1,13 @@ import type * as React from "react"; +import { useRef } from "react"; import { useDictation } from "./useDictation"; interface UseComposerDictationOptions { - contentRef: React.MutableRefObject; - disabled: boolean; - isSending: boolean; + /** Ref to a function that syncs contentRef from the Tiptap editor and returns it. */ + syncContentRef: React.MutableRefObject<() => string>; + disabledRef: React.MutableRefObject; + isSendingRef: React.MutableRefObject; + isUploadingRef: React.MutableRefObject; /** Updates contentRef + isContentEmpty state. */ setComposerContent: (text: string) => void; /** Ref to a function that updates the Tiptap editor document. */ @@ -14,18 +17,23 @@ interface UseComposerDictationOptions { /** * Thin wrapper around `useDictation` pre-wired for the MessageComposer's - * state management (contentRef, setComposerContent, editor, submitMessageRef). + * state management (syncContentRef, setComposerContent, editor, submitMessageRef). */ export function useComposerDictation({ - contentRef, - disabled, - isSending, + syncContentRef, + disabledRef, + isSendingRef, + isUploadingRef, setComposerContent, setEditorContentRef, submitMessageRef, }: UseComposerDictationOptions) { + const isSendBlockedRef = useRef(false); + isSendBlockedRef.current = + disabledRef.current || isSendingRef.current || isUploadingRef.current; + return useDictation({ - text: contentRef.current, + getText: () => syncContentRef.current(), setText: (text) => { setComposerContent(text); setEditorContentRef.current(text); @@ -38,6 +46,6 @@ export function useComposerDictation({ // holds the dictated text. submitMessageRef.current(); }, - sendDisabled: disabled || isSending, + isSendBlockedRef, }); } diff --git a/desktop/src/features/dictation/hooks/useDictation.ts b/desktop/src/features/dictation/hooks/useDictation.ts index abf687704..ad42ef569 100644 --- a/desktop/src/features/dictation/hooks/useDictation.ts +++ b/desktop/src/features/dictation/hooks/useDictation.ts @@ -1,3 +1,4 @@ +import type * as React from "react"; import { useCallback, useMemo, useRef } from "react"; import { DEFAULT_AUTO_SUBMIT_PHRASE, @@ -8,35 +9,33 @@ import { import { useRealtimeDictation } from "./useRealtimeDictation"; interface UseDictationOptions { - /** Current composer text */ - text: string; + /** Returns the current composer text (must be fresh — synced from editor). */ + getText: () => string; /** Set composer text */ setText: (value: string) => void; /** Send the message */ onSend: (text: string) => void; - /** Whether sending is currently blocked */ - sendDisabled?: boolean; + /** Ref that is `true` when sending is blocked (uploading, preparing mention, etc.) */ + isSendBlockedRef?: React.MutableRefObject; } export function useDictation({ - text, + getText, setText, onSend, - sendDisabled = false, + isSendBlockedRef, }: UseDictationOptions) { const autoSubmitPhrases = useMemo( () => parseAutoSubmitPhrases(DEFAULT_AUTO_SUBMIT_PHRASE), [], ); const stopRecordingRef = useRef<() => void>(() => {}); - const textRef = useRef(text); - textRef.current = text; const lastTranscriptRef = useRef(""); const handleTranscript = useCallback( (transcript: string) => { const previous = lastTranscriptRef.current; - const latest = textRef.current; + const latest = getText(); const merged = replaceTrailingTranscribedText( latest, previous, @@ -46,7 +45,6 @@ export function useDictation({ if (!match) { setText(merged); - textRef.current = merged; lastTranscriptRef.current = transcript; return; } @@ -60,18 +58,16 @@ export function useDictation({ stopRecordingRef.current(); - if (sendDisabled) { + if (isSendBlockedRef?.current) { setText(textWithoutPhrase); - textRef.current = textWithoutPhrase; return; } onSend(textWithoutPhrase.trim()); setText(""); - textRef.current = ""; lastTranscriptRef.current = ""; }, - [autoSubmitPhrases, onSend, sendDisabled, setText], + [autoSubmitPhrases, getText, onSend, isSendBlockedRef, setText], ); const dictation = useRealtimeDictation({ diff --git a/desktop/src/features/messages/ui/MessageComposer.tsx b/desktop/src/features/messages/ui/MessageComposer.tsx index 96a5003c5..84b84ce11 100644 --- a/desktop/src/features/messages/ui/MessageComposer.tsx +++ b/desktop/src/features/messages/ui/MessageComposer.tsx @@ -266,17 +266,17 @@ function MessageComposerImpl({ emojiAutocomplete.isEmojiAutocompleteOpen; const submitMessageRef = React.useRef<() => void>(() => {}); - const setEditorContentRef = React.useRef<(t: string) => void>(() => {}); + const setEditorContentRef = React.useRef<(text: string) => void>(() => {}); const dictation = useComposerDictation({ - contentRef, - disabled, - isSending, + syncContentRef: syncContentRefFromEditorRef, + disabledRef, + isSendingRef, + isUploadingRef, setComposerContent, setEditorContentRef, submitMessageRef, }); const composerScrollRef = React.useRef(null); - // Set after `useLinkEditor` exists below; the editor's link-click handler // delegates through this ref to break the hook ordering cycle (the editor // needs `onEditLink`, but the link editor needs the editor's `richText`).