From f8a2990acdfe8c603e163bdc9932816bbed314ac Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 5 Nov 2025 13:38:51 +0000 Subject: [PATCH] feat: add voice-to-text input functionality for AI agent interactions - Add useSpeechRecognition hook with Web Speech API support - Integrate microphone button into ChatTextArea component - Add visual feedback for active recording state - Display interim transcripts during speech input - Add comprehensive localization support for voice features - Include error handling for various speech recognition scenarios - Add comprehensive test coverage for the speech recognition hook Fixes #9052 --- .../src/components/chat/ChatTextArea.tsx | 89 +++- .../__tests__/useSpeechRecognition.spec.tsx | 426 ++++++++++++++++++ .../chat/hooks/useSpeechRecognition.ts | 210 +++++++++ webview-ui/src/i18n/locales/en/chat.json | 13 + 4 files changed, 737 insertions(+), 1 deletion(-) create mode 100644 webview-ui/src/components/chat/hooks/__tests__/useSpeechRecognition.spec.tsx create mode 100644 webview-ui/src/components/chat/hooks/useSpeechRecognition.ts diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index c7813372fa..acd7864666 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -1,7 +1,7 @@ import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react" import { useEvent } from "react-use" import DynamicTextArea from "react-textarea-autosize" -import { VolumeX, Image, WandSparkles, SendHorizontal, MessageSquareX } from "lucide-react" +import { VolumeX, Image, WandSparkles, SendHorizontal, MessageSquareX, Mic, MicOff } from "lucide-react" import { mentionRegex, mentionRegexGlobal, commandRegexGlobal, unescapeSpaces } from "@roo/context-mentions" import { WebviewMessage } from "@roo/WebviewMessage" @@ -31,6 +31,7 @@ import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" import { IndexingStatusBadge } from "./IndexingStatusBadge" import { usePromptHistory } from "./hooks/usePromptHistory" +import { useSpeechRecognition } from "./hooks/useSpeechRecognition" import { CloudAccountSwitcher } from "../cloud/CloudAccountSwitcher" interface ChatTextAreaProps { @@ -214,6 +215,49 @@ export const ChatTextArea = forwardRef( const [isEnhancingPrompt, setIsEnhancingPrompt] = useState(false) const [isFocused, setIsFocused] = useState(false) + // Use custom hook for speech recognition + const { + isListening, + isSupported: isSpeechRecognitionSupported, + transcript, + interimTranscript, + error: speechError, + toggleListening, + clearTranscript, + } = useSpeechRecognition() + + // Update input value when transcript changes + useEffect(() => { + if (transcript && !isListening) { + // Append transcript to existing input value with a space if needed + const needsSpace = inputValue.length > 0 && !inputValue.endsWith(" ") + const newValue = inputValue + (needsSpace ? " " : "") + transcript + setInputValue(newValue) + // Clear the transcript after using it + clearTranscript() + + // Set cursor position to the end + const newCursorPosition = newValue.length + setCursorPosition(newCursorPosition) + setIntendedCursorPosition(newCursorPosition) + + // Focus the textarea + setTimeout(() => { + if (textAreaRef.current) { + textAreaRef.current.focus() + textAreaRef.current.setSelectionRange(newCursorPosition, newCursorPosition) + } + }, 0) + } + }, [transcript, isListening, inputValue, setInputValue, clearTranscript]) + + // Show speech error if any + useEffect(() => { + if (speechError) { + console.error("Speech recognition error:", speechError) + } + }, [speechError]) + // Use custom hook for prompt history navigation const { handleHistoryNavigation, resetHistoryNavigation, resetOnInputChange } = usePromptHistory({ clineMessages, @@ -1084,6 +1128,19 @@ export const ChatTextArea = forwardRef( onScroll={() => updateHighlights()} /> + {/* Show interim transcript overlay when listening */} + {isListening && interimTranscript && ( +
+
+ {interimTranscript} +
+
+ )} +
+ {isSpeechRecognitionSupported && ( + + + + )} {isEditMode && (