From 4dfdb83ee4bec1126dff2a5b1e8299d39e4b1e38 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 17:14:53 +0000 Subject: [PATCH 1/4] feat(chat): add a microphone button to dictate messages to the AI Add a microphone button in the chat composer so the user can dictate a message to Gladys instead of typing it, as requested on the community forum. Everything happens in the browser with the Web Speech API: no audio is recorded or uploaded by Gladys, and no server change is needed. The transcription fills the message input as the user speaks, and the message is never sent automatically so the user can review it first. The recognition locale follows the language selected in Gladys, falling back to the browser locale. Browsers without the Web Speech API (Firefox) or pages served over plain HTTP simply don't get the button. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013yxguVaLdJ8ZKmw5x3HePT --- front/src/actions/message.js | 5 + front/src/config/i18n/de.json | 10 + front/src/config/i18n/en.json | 10 + front/src/config/i18n/fr.json | 10 + front/src/routes/chat/ChatPage.js | 31 +++- .../src/routes/chat/ChatVoiceInputButton.jsx | 123 +++++++++++++ front/src/routes/chat/style.css | 54 ++++++ front/src/utils/speechRecognition.js | 173 ++++++++++++++++++ 8 files changed, 414 insertions(+), 2 deletions(-) create mode 100644 front/src/routes/chat/ChatVoiceInputButton.jsx create mode 100644 front/src/utils/speechRecognition.js diff --git a/front/src/actions/message.js b/front/src/actions/message.js index 564ca8edba..45b91d433c 100644 --- a/front/src/actions/message.js +++ b/front/src/actions/message.js @@ -42,6 +42,11 @@ function createActions(store) { currentMessageTextInput: e.target.value }); }, + setMessageTextInput(state, text) { + store.setState({ + currentMessageTextInput: text + }); + }, syncMessage(state, message) { let newMessages = store.getState().messages; // Check if message is already in the list diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json index 4703487ea2..7de7a51568 100644 --- a/front/src/config/i18n/de.json +++ b/front/src/config/i18n/de.json @@ -5708,6 +5708,16 @@ "auto": "Auto", "priceLegend": "€ günstig · €€ mittel · €€€ premium" }, + "voiceInput": { + "startListening": "Nachricht diktieren", + "stopListening": "Diktat beenden", + "error": "Die Spracherkennung ist fehlgeschlagen. Bitte versuche es erneut.", + "errorNoSpeech": "Keine Stimme erkannt. Prüfe, ob dein Mikrofon aktiviert ist, und versuche es erneut.", + "errorPermissionDenied": "Der Mikrofonzugriff wurde verweigert. Erlaube ihn in den Einstellungen deines Browsers, um eine Nachricht zu diktieren.", + "errorNoMicrophone": "Kein Mikrofon erkannt. Schließe eines an oder aktiviere es in den Systemeinstellungen.", + "errorNetwork": "Die Spracherkennung ist nicht verfügbar, dein Browser konnte seinen Sprachdienst nicht erreichen.", + "errorNotSupported": "Spracherkennung ist in diesem Browser nicht verfügbar." + }, "sidebar": { "title": "Wie funktioniert der Chat?", "intro": "Der Gladys-Chat nutzt die Gladys-Plus-Künstliche Intelligenz, damit du natürlich mit deinem vernetzten Zuhause kommunizieren kannst.", diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 3080417573..791534d582 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -5719,6 +5719,16 @@ "label": "AI model", "auto": "Auto", "priceLegend": "€ cheap · €€ mid · €€€ premium" + }, + "voiceInput": { + "startListening": "Dictate a message", + "stopListening": "Stop dictation", + "error": "Speech recognition failed. Please try again.", + "errorNoSpeech": "No voice detected. Check that your microphone is enabled, then try again.", + "errorPermissionDenied": "Microphone access was denied. Allow it in your browser settings to dictate a message.", + "errorNoMicrophone": "No microphone detected. Plug one in or enable it in system settings.", + "errorNetwork": "Speech recognition is unavailable, your browser could not reach its speech service.", + "errorNotSupported": "Speech recognition is not available in this browser." } }, "history": { diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 152327809a..72e8a36cc9 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -5719,6 +5719,16 @@ "label": "Modèle IA", "auto": "Auto", "priceLegend": "€ économique · €€ moyen · €€€ premium" + }, + "voiceInput": { + "startListening": "Dicter un message", + "stopListening": "Arrêter la dictée", + "error": "La reconnaissance vocale a échoué. Veuillez réessayer.", + "errorNoSpeech": "Aucune voix détectée. Vérifiez que votre micro est activé, puis réessayez.", + "errorPermissionDenied": "L'accès au micro a été refusé. Autorisez-le dans les réglages de votre navigateur pour dicter un message.", + "errorNoMicrophone": "Aucun micro détecté. Branchez ou activez un micro dans les réglages système.", + "errorNetwork": "La reconnaissance vocale est indisponible, votre navigateur n'a pas pu joindre son service vocal.", + "errorNotSupported": "La reconnaissance vocale n'est pas disponible dans ce navigateur." } }, "history": { diff --git a/front/src/routes/chat/ChatPage.js b/front/src/routes/chat/ChatPage.js index 48f56fd56c..b9f2ce9a38 100644 --- a/front/src/routes/chat/ChatPage.js +++ b/front/src/routes/chat/ChatPage.js @@ -4,9 +4,11 @@ import { useEffect, useRef, useState } from 'preact/hooks'; import { connect } from 'unistore/preact'; import actions from '../../actions/message'; import { RequestStatus } from '../../utils/consts'; +import { isSpeechRecognitionSupported } from '../../utils/speechRecognition'; import ChatItems from './ChatItems'; import EmptyChat from './EmptyChat'; import AiModelSelector from './AiModelSelector'; +import ChatVoiceInputButton from './ChatVoiceInputButton'; import style from './style.css'; const IntegrationPage = connect( @@ -19,14 +21,20 @@ const IntegrationPage = connect( MessageGetStatus, currentMessageTextInput, updateMessageTextInput, + setMessageTextInput, onKeyPress, sendMessage, gladysIsTyping, httpClient }) => { const textareaRef = useRef(null); + const voiceInputRef = useRef(null); const [selectedModel, setSelectedModel] = useState('auto'); const [gladysPlusConfigured, setGladysPlusConfigured] = useState(null); + const [voiceInputError, setVoiceInputError] = useState(null); + // Browsers without the Web Speech API (Firefox for example) simply don't + // get a microphone button. + const [voiceInputSupported] = useState(isSpeechRecognitionSupported); const hasMessageToSend = Boolean(currentMessageTextInput && currentMessageTextInput.trim().length > 0); useEffect(() => { @@ -63,13 +71,16 @@ const IntegrationPage = connect( }; const handleSendMessage = () => { + if (voiceInputRef.current) { + voiceInputRef.current.cancelListening(); + } sendMessage(selectedModel); }; const handleKeyPress = e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); - sendMessage(selectedModel); + handleSendMessage(); return; } onKeyPress(e); @@ -109,13 +120,24 @@ const IntegrationPage = connect(