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 && (