diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4087e4f5..9973bb9e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3075,6 +3075,24 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/easy-speech": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/easy-speech/-/easy-speech-2.4.0.tgz", + "integrity": "sha512-wpMv29DEoeP/eyXr4aXpDqd9DvlXl7aQs7BgfKbjGVxqkmQPgNmpbF5YULaTH5bc/5qrteg5MDfCD2Zd0qr4rQ==", + "funding": [ + { + "type": "GitHub", + "url": "https://github.com/sponsors/jankapunkt" + }, + { + "type": "PayPal", + "url": "https://paypal.me/kuesterjan" + } + ], + "engines": { + "node": ">= 14.x" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.11", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.11.tgz", diff --git a/frontend/src/assets/Loading.svg b/frontend/src/assets/Loading.svg new file mode 100644 index 00000000..84a604f9 --- /dev/null +++ b/frontend/src/assets/Loading.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/speaker.svg b/frontend/src/assets/speaker.svg new file mode 100644 index 00000000..6c379177 --- /dev/null +++ b/frontend/src/assets/speaker.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/stopspeech.svg b/frontend/src/assets/stopspeech.svg new file mode 100644 index 00000000..f77a235b --- /dev/null +++ b/frontend/src/assets/stopspeech.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/components/TextToSpeechButton.tsx b/frontend/src/components/TextToSpeechButton.tsx new file mode 100644 index 00000000..2cb9e8f8 --- /dev/null +++ b/frontend/src/components/TextToSpeechButton.tsx @@ -0,0 +1,94 @@ +import { useState, useRef } from 'react'; +import Speaker from '../assets/speaker.svg?react'; +import Stopspeech from '../assets/stopspeech.svg?react'; +import LoadingIcon from '../assets/Loading.svg?react'; // Add a loading icon SVG here +const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com'; + +export default function SpeakButton({ + text, + colorLight, + colorDark, +}: { + text: string; + colorLight?: string; + colorDark?: string; +}) { + const [isSpeaking, setIsSpeaking] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isSpeakHovered, setIsSpeakHovered] = useState(false); + const audioRef = useRef(null); + + const handleSpeakClick = async () => { + if (isSpeaking) { + // Stop audio if it's currently playing + audioRef.current?.pause(); + audioRef.current = null; + setIsSpeaking(false); + return; + } + + try { + // Set loading state and initiate TTS request + setIsLoading(true); + + const response = await fetch(apiHost + '/api/tts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text }), + }); + + const data = await response.json(); + + if (data.success && data.audio_base64) { + // Create and play the audio + const audio = new Audio(`data:audio/mp3;base64,${data.audio_base64}`); + audioRef.current = audio; + + audio.play().then(() => { + setIsSpeaking(true); + setIsLoading(false); + + // Reset when audio ends + audio.onended = () => { + setIsSpeaking(false); + audioRef.current = null; + }; + }); + } else { + console.error('Failed to retrieve audio.'); + setIsLoading(false); + } + } catch (error) { + console.error('Error fetching audio from TTS endpoint', error); + setIsLoading(false); + } + }; + + return ( +
+ {isLoading ? ( + + ) : isSpeaking ? ( + setIsSpeakHovered(true)} + onMouseLeave={() => setIsSpeakHovered(false)} + /> + ) : ( + setIsSpeakHovered(true)} + onMouseLeave={() => setIsSpeakHovered(false)} + /> + )} +
+ ); +} diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx index 2ccf1ca3..a9a05168 100644 --- a/frontend/src/conversation/ConversationBubble.tsx +++ b/frontend/src/conversation/ConversationBubble.tsx @@ -7,7 +7,6 @@ import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; import 'katex/dist/katex.min.css'; - import DocsGPT3 from '../assets/cute_docsgpt3.svg'; import Dislike from '../assets/dislike.svg?react'; import Document from '../assets/document.svg'; @@ -23,6 +22,7 @@ import { } from '../preferences/preferenceSlice'; import classes from './ConversationBubble.module.css'; import { FEEDBACK, MESSAGE_TYPE } from './conversationModels'; +import SpeakButton from '../components/TextToSpeechButton'; const DisableSourceFE = import.meta.env.VITE_DISABLE_SOURCE_FE || false; @@ -336,6 +336,14 @@ const ConversationBubble = forwardRef< +
+
+ {/* Add SpeakButton here */} +
+
{type === 'ERROR' && (
{retryBtn}