diff --git a/package-lock.json b/package-lock.json index 5599022..9632e02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "@eslint/js": "^9.17.0", "@tailwindcss/postcss": "^4.1.11", "@tailwindcss/vite": "^4.1.11", + "@types/dom-speech-recognition": "^0.0.6", "@types/node": "^22.13.1", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", @@ -3399,6 +3400,13 @@ "@types/ms": "*" } }, + "node_modules/@types/dom-speech-recognition": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.6.tgz", + "integrity": "sha512-o7pAVq9UQPJL5RDjO1f/fcpfFHdgiMnR4PoIU2N/ZQrYOS3C5rzdOJMsrpqeBCbii2EE9mERXgqspQqPDdPahw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", diff --git a/package.json b/package.json index 90e6c4a..5d3985a 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@eslint/js": "^9.17.0", "@tailwindcss/postcss": "^4.1.11", "@tailwindcss/vite": "^4.1.11", + "@types/dom-speech-recognition": "^0.0.6", "@types/node": "^22.13.1", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", diff --git a/src/components/ChatInput.tsx b/src/components/ChatInput.tsx index e82e63c..1af993f 100644 --- a/src/components/ChatInput.tsx +++ b/src/components/ChatInput.tsx @@ -1,7 +1,13 @@ import { memo, useCallback, useEffect, useMemo } from 'react'; import toast from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; -import { LuArrowUp, LuPaperclip, LuSquare } from 'react-icons/lu'; +import { + LuArrowUp, + LuCircleStop, + LuMic, + LuPaperclip, + LuSquare, +} from 'react-icons/lu'; import { TbAdjustmentsHorizontal } from 'react-icons/tb'; import { useNavigate } from 'react-router'; import { useChatContext } from '../context/chat'; @@ -10,6 +16,10 @@ import { useFileUpload } from '../hooks/useFileUpload'; import { MessageExtra } from '../types'; import { classNames, cleanCurrentUrl } from '../utils'; import { DropzoneArea } from './DropzoneArea'; +import SpeechToText, { + IS_SPEECH_RECOGNITION_SUPPORTED, + SpeechRecordCallback, +} from './SpeechToText'; /** * If the current URL contains "?m=...", prefill the message input with the value. @@ -50,6 +60,11 @@ export const ChatInput = memo( stopGenerating(convId); }, [convId, stopGenerating]); + const handleRecord: SpeechRecordCallback = useCallback( + (text: string) => textarea.setValue(text), + [textarea] + ); + const sendNewMessage = async () => { const lastInpMsg = textarea.value(); if (lastInpMsg.trim().length === 0) { @@ -145,6 +160,35 @@ export const ChatInput = memo(
+ {IS_SPEECH_RECOGNITION_SUPPORTED && !isPending && ( + + {({ isRecording, startRecording, stopRecording }) => ( + <> + {!isRecording && ( + + )} + {isRecording && ( + + )} + + )} + + )} + {isPending && (