diff --git a/src/App.tsx b/src/App.tsx index 3406c34..0367d4c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -36,6 +36,7 @@ import Config from './lib/config'; import Storage from './lib/storage'; import Voice from './lib/voice'; import useVoices from './hooks/useVoices'; +import textToSpeech from "./lib/eleven_labs"; interface CreateChatGPTMessageResponse { answer: string; @@ -238,7 +239,7 @@ function App() { ...oldMessages, { type: 'response', text: res.answer }, ]); - speak(res.answer); + textToSpeech(res.answer); }) .catch((err: unknown) => { console.warn(err); diff --git a/src/design_system/Message.tsx b/src/design_system/Message.tsx index 3ad4e53..b29c62e 100644 --- a/src/design_system/Message.tsx +++ b/src/design_system/Message.tsx @@ -1,5 +1,6 @@ import { KeyboardEventHandler } from 'react'; import { DollarSign, Terminal } from 'react-feather'; +import Typewriter from './typewriter'; interface MessageProps { type: 'prompt' | 'response'; @@ -47,7 +48,13 @@ export default function Message({ )} -
{text}
+
+ {type === 'response' ? ( + + ) : ( + {text} + )} +
); } diff --git a/src/design_system/typewriter.tsx b/src/design_system/typewriter.tsx new file mode 100644 index 0000000..a71a079 --- /dev/null +++ b/src/design_system/typewriter.tsx @@ -0,0 +1,30 @@ +import React, { useEffect, useState } from 'react'; +import styles from './typing.module.css'; + +const Typewriter = ({ text = "", typingDelay = 100, blinkingCursor = true }) => { + const [typewriterText, setTypewriterText] = useState(""); + const [index, setIndex] = useState(0); + const words = text.replace(/\n/g, '
').split(' '); + + useEffect(() => { + setTypewriterText(""); + setIndex(0); + }, [text]); + + useEffect(() => { + if (index < words.length) { + const timerId = setInterval(() => { + setTypewriterText((prevText) => prevText + (index > 0 ? ' ' : '') + words[index]); + setIndex((prevIndex) => prevIndex + 1); + }, typingDelay); + + return () => clearInterval(timerId); + } + }, [index, words, typingDelay]); + + return ( + + ); +}; + +export default Typewriter; diff --git a/src/design_system/typing.module.css b/src/design_system/typing.module.css new file mode 100644 index 0000000..5188bef --- /dev/null +++ b/src/design_system/typing.module.css @@ -0,0 +1,7 @@ +.blinking-cursor { + animation: blink 0.75s step-end infinite; +} + +@keyframes blink { + 50% { opacity: 0 } +} diff --git a/src/lib/eleven_labs.ts b/src/lib/eleven_labs.ts new file mode 100644 index 0000000..8a5ae80 --- /dev/null +++ b/src/lib/eleven_labs.ts @@ -0,0 +1,36 @@ +// Define your Eleven Labs API key +const ELEVEN_LABS_API_KEY = ""; // Add your Eleven Labs API key here +const sVoiceId = "21m00Tcm4TlvDq8ikWAM"; // This is the default voiceId. You can change it if needed. + +// Function for making a request to Eleven Labs API and playing the response +export default async function textToSpeech(text: string, voiceId: string = sVoiceId): Promise { + // Preparing the request + const url = `https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`; + const headers = new Headers(); + headers.append("Accept", "audio/mpeg"); + headers.append("Content-Type", "application/json"); + headers.append("xi-api-key", ELEVEN_LABS_API_KEY); + const data = { + text: text, + voice_settings: { stability: 0, similarity_boost: 0 } + }; + const requestOptions: RequestInit = { + method: 'POST', + headers: headers, + body: JSON.stringify(data) + }; + + // Making the request + const response = await fetch(url, requestOptions); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + // Reading the response as Blob + const blob = await response.blob(); + + // Creating object URL and audio element to play the sound + const audioURL = URL.createObjectURL(blob); + const audio = new Audio(audioURL); + audio.play(); +}