diff --git a/react-speech-app/index.html b/react-speech-app/index.html index 30e2e9d..d4c3f10 100644 --- a/react-speech-app/index.html +++ b/react-speech-app/index.html @@ -43,4 +43,4 @@
- + \ No newline at end of file diff --git a/react-speech-app/package-lock.json b/react-speech-app/package-lock.json index 307ceb0..2c7c3f1 100644 --- a/react-speech-app/package-lock.json +++ b/react-speech-app/package-lock.json @@ -2229,6 +2229,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rollup/plugin-node-resolve": { "version": "15.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", diff --git a/react-speech-app/src/App.css b/react-speech-app/src/App.css index f1e8da6..c0fab74 100644 --- a/react-speech-app/src/App.css +++ b/react-speech-app/src/App.css @@ -1,114 +1,309 @@ body { font-family: 'Poppins', sans-serif; -/* background-image: url('images/10.jpg'); */ - background-image: url('10.jpg'); - background-size: cover; - background-position: center; - background-repeat: no-repeat; - color: #ffffff; + transition: background 0.5s ease-in-out, color 0.5s ease-in-out; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; - height: 100vh; + min-height: 100vh; } +.page-title { + position: absolute; + top: 40px; + left: 50%; + transform: translateX(-50%); + font-size: 2.2rem; + font-weight: bold; + text-align: center; + transition: color 0.5s ease-in-out; + margin-bottom: 0; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} -.container { +body.light-mode { + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + color: #333333; +} + +body.light-mode .page-title { + color: #0C2D57; +} + +body.dark-mode { + background: linear-gradient(135deg, #0c2d57 0%, #1e65a7 100%); + color: #ffffff; +} + +body.dark-mode .page-title { + color: #ffffff; +} + +.toggle-button { + position: absolute; + top: 20px; + right: 20px; + padding: 12px 18px; + font-size: 15px; + font-weight: 600; + border: none; + border-radius: 30px; + cursor: pointer; + transition: all 0.3s ease; + z-index: 100; + display: flex; + align-items: center; + gap: 8px; +} + +body.light-mode .toggle-button { background: #ffffff; - padding: 2rem; - border-radius: 12px; + color: #0C2D57; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); - width: 90%; - max-width: 600px; - text-align: center; } -h3 { - font-size: 2em; - margin-bottom: 0rem; + +body.dark-mode .toggle-button { + background: rgba(255, 255, 255, 0.1); color: #ffffff; - font-family: 'Poppins', sans-serif; - font-weight: 600; - text-align: center; + backdrop-filter: blur(5px); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.2); } +.toggle-button:hover { + transform: translateY(-2px); +} +.container { + padding: 2.5rem; + border-radius: 16px; + width: 100%; + max-width: 650px; + text-align: center; + margin-top: 60px; + transition: background 0.5s ease-in-out, box-shadow 0.5s ease-in-out; +} + +body.light-mode .container { + background: rgba(255, 255, 255, 0.9); + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1); +} + +body.dark-mode .container { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.2); +} textarea { width: 100%; - height: 120px; - padding: 10px; + height: 200px; + padding: 15px; font-size: 16px; - border: 1px solid #ddd; - border-radius: 8px; + border-radius: 12px; resize: none; transition: all 0.3s ease-in-out; + border: none; +} + +body.light-mode textarea { + background: #f8f9fa; + color: #333; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05); +} + +body.dark-mode textarea { + background: rgba(255, 255, 255, 0.9); + color: #333; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); } textarea:focus { - border-color: #007BFF; - box-shadow: 0 0 6px rgba(0, 123, 255, 0.2); + box-shadow: 0 0 8px rgba(0, 157, 255, 0.5), inset 0 2px 4px rgba(0, 0, 0, 0.1); outline: none; } .control-container { display: flex; flex-direction: column; - gap: 15px; - margin-top: 1rem; - + gap: 20px; + margin-top: 1.5rem; +} + +.control-label { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 500; + margin-bottom: 5px; +} + +body.light-mode .control-label { + color: #333; +} + +body.dark-mode .control-label { + color: rgba(255, 255, 255, 0.9); } select, input[type="range"] { width: 100%; - padding: 8px; - font-size: 14px; - border-radius: 6px; - border: 1px solid #ccc; + padding: 12px; + font-size: 15px; + border-radius: 10px; + border: none; + transition: all 0.3s ease-in-out; +} + +select { + appearance: none; + background-repeat: no-repeat; + background-position: right 1rem center; + background-size: 1em; + padding-right: 2.5rem; + cursor: pointer; } +body.light-mode select { + background-color: #f8f9fa; + color: #333; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23333333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); +} + +body.dark-mode select { + background-color: rgba(255, 255, 255, 0.9); + color: #333; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23333333' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); +} -button { - margin-top: 20px; - background: #009dff !important; - color: #ffffff !important; - padding: 12px 20px; +input[type="range"] { + -webkit-appearance: none; + height: 8px; + border-radius: 4px; + padding: 0; +} + +body.light-mode input[type="range"] { + background: #e0e0e0; +} + +body.dark-mode input[type="range"] { + background: rgba(255, 255, 255, 0.3); +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + height: 20px; + width: 20px; + border-radius: 50%; + background: #009dff; + cursor: pointer; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.speak-button { + margin-top: 25px; + padding: 14px 24px; font-size: 16px; font-weight: bold; border: none; - border-radius: 8px; + border-radius: 12px; cursor: pointer; display: flex; align-items: center; justify-content: center; - gap: 8px; - transition: transform 0.3s ease-out, box-shadow 0.3s ease-in-out; - box-shadow: 0 4px 0 #0C2D57; + gap: 10px; + transition: all 0.3s ease; + margin-left: auto; + margin-right: auto; } -button:hover { - transform: scale(1.1); - box-shadow: 0 6px 0 #0A2140; +body.light-mode .speak-button { + background: linear-gradient(to right, #009dff, #0070e0); + color: #ffffff; + box-shadow: 0 4px 15px rgba(0, 157, 255, 0.3); +} + +body.dark-mode .speak-button { + background: linear-gradient(to right, #009dff, #0070e0); + color: #ffffff; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4); } -button:active { - transform: scale(1.05); - box-shadow: 0 2px 0 #0C2D57; +.speak-button:hover { + transform: translateY(-2px); } -button i { - font-size: 18px; +body.light-mode .speak-button:hover { + box-shadow: 0 6px 20px rgba(0, 157, 255, 0.4); } +body.dark-mode .speak-button:hover { + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5); +} -@media (max-width: 670px) { +.speak-button:active { + transform: translateY(1px); +} + +.speak-button.playing { + background: linear-gradient(to right, #e74c3c, #c0392b); +} + +body.light-mode .speak-button.playing { + box-shadow: 0 4px 15px rgba(231, 76, 60, 0.3); +} + +body.dark-mode .speak-button.playing { + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4); +} + +body.light-mode .speak-button.playing:hover { + box-shadow: 0 6px 20px rgba(231, 76, 60, 0.4); +} + +@media (max-width: 768px) { .container { - width: 95%; - padding: 1.5rem; + width: 90%; + padding: 1.8rem; } textarea { - height: 100px; + height: 120px; + } + + .page-title { + font-size: 1.8rem; + top: 15px; + } + + .toggle-button { + top: 15px; + right: 15px; + padding: 10px 15px; + font-size: 14px; } } + +@media (max-width: 576px) { + .container { + width: 95%; + padding: 1.5rem; + margin-top: 80px; + } + + .page-title { + font-size: 1.6rem; + } + + .toggle-button { + top: 60px; + right: 50%; + transform: translateX(50%); + font-size: 12px; + } +} \ No newline at end of file diff --git a/react-speech-app/src/App.jsx b/react-speech-app/src/App.jsx index 1c7ab3f..cd728ea 100644 --- a/react-speech-app/src/App.jsx +++ b/react-speech-app/src/App.jsx @@ -1,223 +1,164 @@ -import { useState, useRef, useEffect } from "react"; +import React, { useState, useEffect } from 'react'; import "bootstrap/dist/css/bootstrap.min.css"; -import "./app2.css"; +// import "./app2.css"; +import './App.css'; function App() { - const [text, setText] = useState(""); - const [isSpeaking, setIsSpeaking] = useState(false); - const [isPaused, setIsPaused] = useState(false); + const [text, setText] = useState(''); + const [voice, setVoice] = useState(''); const [voices, setVoices] = useState([]); - const [selectedVoice, setSelectedVoice] = useState(""); const [rate, setRate] = useState(1); - const [lastCharIndex, setLastCharIndex] = useState(0); - const [isDarkMode, setIsDarkMode] = useState(() => { - return localStorage.getItem("darkMode") === "true"; - }); - - const speechRef = useRef(null); + const [isPlaying, setIsPlaying] = useState(false); + const [isDarkMode, setIsDarkMode] = useState(true); + const [highlightedText, setHighlightedText] = useState(''); + const [currentWord, setCurrentWord] = useState(0); + const synth = window.speechSynthesis; useEffect(() => { - const fetchVoices = () => { - const availableVoices = window.speechSynthesis.getVoices(); + const loadVoices = () => { + const availableVoices = synth.getVoices(); setVoices(availableVoices); - if (availableVoices.length > 0) { - setSelectedVoice(availableVoices[0].name); - } + const defaultVoice = availableVoices.find(v => + v.name.includes('David') || + (v.lang.includes('en') && v.default) + ) || availableVoices[0]; + + if (defaultVoice) setVoice(defaultVoice.name); }; - if (window.speechSynthesis.onvoiceschanged !== undefined) { - window.speechSynthesis.onvoiceschanged = fetchVoices; - } - fetchVoices(); - }, []); - - useEffect(() => { - document.body.className = isDarkMode ? "dark-mode" : "light-mode"; - }, [isDarkMode]); - - // Get the word of a string given the string and index - const getWordAt = (str, pos) => { - str = String(str); - pos = Number(pos) >>> 0; - - let left = str.slice(0, pos + 1).search(/\S+$/), - right = str.slice(pos).search(/\s/); - - if (right < 0) { - return str.slice(left); + if (synth.getVoices().length > 0) { + loadVoices(); } - return str.slice(left, right + pos); - }; - - // Get the position of the beginning of the word - const getWordStart = (str, pos) => { - str = String(str); - pos = Number(pos) >>> 0; - let start = str.slice(0, pos + 1).search(/\S+$/); - return start; - }; - const wordHighlightAndAutoScroll = (event,text) => { - let textarea = document.getElementById("textarea"); - let value = text; - let index = event.charIndex; - console.log(index); - let word = getWordAt(value, index); - console.log(word); - let anchorPosition = (textarea.value.length-value.length) + getWordStart(value, index); - let activePosition = anchorPosition + word.length; + synth.onvoiceschanged = loadVoices; - textarea.focus(); + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + setIsDarkMode(prefersDark); + document.body.className = prefersDark ? 'dark-mode' : 'light-mode'; - const fullText = textarea.value; - textarea.value = fullText.substring(0, activePosition+250); - textarea.scrollTop = textarea.scrollHeight; - textarea.value = fullText; - - if (textarea.setSelectionRange) { - textarea.setSelectionRange(anchorPosition, activePosition); - } else { - let range = textarea.createTextRange(); - range.collapse(true); - range.moveEnd("character", activePosition); - range.moveStart("character", anchorPosition); - range.select(); - } - }; - - const toggleDarkMode = () => { - const newMode = !isDarkMode; - setIsDarkMode(newMode); - localStorage.setItem("darkMode", newMode); - }; - - const handleTextChange = (e) => setText(e.target.value); - const handleVoiceChange = (e) => { - setSelectedVoice(e.target.value); - if (isSpeaking) restartSpeech(lastCharIndex); - }; + return () => { + synth.onvoiceschanged = null; + if (synth.speaking) synth.cancel(); + }; + }, []); - const handleRateChange = async (e) => { - const newRate = parseFloat(e.target.value); - setRate(newRate); - if (isSpeaking) { - restartSpeech(lastCharIndex, newRate); - } + const toggleTheme = () => { + setIsDarkMode(!isDarkMode); + document.body.className = !isDarkMode ? 'dark-mode' : 'light-mode'; }; - const toggleSpeaking = () => { - if (!text) { - alert("Please enter text to speak."); - return; - } - - if (isSpeaking) { - if (isPaused) { - window.speechSynthesis.resume(); - setIsPaused(false); - } else { - window.speechSynthesis.pause(); - setIsPaused(true); + // Handle text highlighting during speech + const handleTextHighlighting = (utterance, words) => { + setCurrentWord(0); + + utterance.onboundary = (event) => { + if (event.name === 'word') { + setCurrentWord((prevWord) => { + const nextWord = prevWord + 1; + return nextWord < words.length ? nextWord : prevWord; + }); } - } else { - startSpeech(lastCharIndex, rate); - } - }; - - const startSpeech = (startIndex, speechRate) => { - window.speechSynthesis.cancel(); - const utterance = new SpeechSynthesisUtterance(text.substring(startIndex)); - const voice = voices.find((v) => v.name === selectedVoice); - if (voice) utterance.voice = voice; - utterance.rate = speechRate; - - utterance.onboundary = async (event) => { - await setLastCharIndex(startIndex + event.charIndex); - await wordHighlightAndAutoScroll(event,utterance.text); }; - - utterance.onpause = (event) => { - setLastCharIndex(event.charIndex); - setIsPaused(true); - }; - + utterance.onend = () => { - setIsSpeaking(false); - setIsPaused(false); - setLastCharIndex(0); + setIsPlaying(false); + setHighlightedText(''); + setCurrentWord(0); }; - - speechRef.current = utterance; - setIsSpeaking(true); - setIsPaused(false); - window.speechSynthesis.speak(utterance); }; - const restartSpeech = (startIndex, speechRate = rate) => { - if (isSpeaking) { - window.speechSynthesis.cancel(); - startSpeech(startIndex, speechRate); + const speak = () => { + if (text.trim() === '') return; + + if (synth.speaking) { + synth.cancel(); + setIsPlaying(false); + return; } + + const utterance = new SpeechSynthesisUtterance(text); + + const selectedVoice = voices.find(v => v.name === voice); + if (selectedVoice) utterance.voice = selectedVoice; + + utterance.rate = rate; + + const words = text.split(/\s+/); + setHighlightedText(text); + handleTextHighlighting(utterance, words); + + setIsPlaying(true); + synth.speak(utterance); }; return ( -
- - + <>

Text Speaking Assistant

- + + +
-