From 62e71a467e48d7561e56b58f7f2e710e472df897 Mon Sep 17 00:00:00 2001 From: LAKSHAY2100 Date: Thu, 3 Apr 2025 23:10:56 +0530 Subject: [PATCH 1/2] Enhaced-UI --- react-speech-app/index.html | 2 +- react-speech-app/package-lock.json | 10 + react-speech-app/src/App.css | 179 +++++++++++----- react-speech-app/src/App.jsx | 314 ++++++++++++----------------- react-speech-app/src/main.jsx | 25 ++- 5 files changed, 294 insertions(+), 236 deletions(-) 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..7872bf1 100644 --- a/react-speech-app/src/App.css +++ b/react-speech-app/src/App.css @@ -1,114 +1,197 @@ 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; + background: linear-gradient(135deg, #0c2d57 0%, #1e65a7 100%); color: #ffffff; margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; - height: 100vh; + min-height: 100vh; } - .container { - background: #ffffff; - padding: 2rem; - border-radius: 12px; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + padding: 2.5rem; + border-radius: 16px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); width: 90%; - max-width: 600px; + max-width: 650px; text-align: center; + border: 1px solid rgba(255, 255, 255, 0.2); } + h3 { - font-size: 2em; - margin-bottom: 0rem; + font-size: 2.2em; + margin-bottom: 1rem; color: #ffffff; - font-family: 'Poppins', sans-serif; - font-weight: 600; - text-align: center; + font-family: 'Poppins', sans-serif; + font-weight: 600; + text-align: center; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } - +.text-container { + position: relative; + margin-bottom: 1.5rem; +} textarea { width: 100%; - height: 120px; - padding: 10px; + height: 150px; + padding: 15px; font-size: 16px; - border: 1px solid #ddd; - border-radius: 8px; + border: none; + border-radius: 12px; resize: none; transition: all 0.3s ease-in-out; + background: rgba(255, 255, 255, 0.9); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); + color: #333; + font-family: 'Poppins', sans-serif; } 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; } +.char-count { + position: absolute; + bottom: -20px; + right: 5px; + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.7); +} + .control-container { display: flex; flex-direction: column; - gap: 15px; - margin-top: 1rem; - + gap: 20px; + margin-top: 1.5rem; + margin-bottom: 1.5rem; +} + +.control-label { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 500; + margin-bottom: 8px; + 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; + background: rgba(255, 255, 255, 0.9); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } +select { + appearance: none; + 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='%23000' 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"); + background-repeat: no-repeat; + background-position: right 1rem center; + background-size: 1em; + padding-right: 2.5rem; + color: #333; +} + +input[type="range"] { + -webkit-appearance: none; + height: 8px; + background: #e0e0e0; + border-radius: 4px; + padding: 0; +} + +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); +} button { - margin-top: 20px; - background: #009dff !important; + margin-top: 10px; + background: linear-gradient(to right, #009dff, #0070e0) !important; color: #ffffff !important; - padding: 12px 20px; + padding: 14px 24px; font-size: 16px; font-weight: bold; border: none; - border-radius: 8px; + border-radius: 12px; cursor: pointer; - display: flex; + display: inline-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; + box-shadow: 0 4px 15px rgba(0, 157, 255, 0.4); + position: relative; + overflow: hidden; +} + +button::before { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.2), + transparent + ); + transition: 0.5s; } button:hover { - transform: scale(1.1); - box-shadow: 0 6px 0 #0A2140; + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 157, 255, 0.6); +} + +button:hover::before { + left: 100%; } button:active { - transform: scale(1.05); - box-shadow: 0 2px 0 #0C2D57; + transform: translateY(1px); + box-shadow: 0 2px 10px rgba(0, 157, 255, 0.4); } -button i { - font-size: 18px; +/* Speaking button style */ +button.speaking-btn { + background: linear-gradient(to right, #e74c3c, #c0392b) !important; + box-shadow: 0 4px 15px rgba(231, 76, 60, 0.4); } +button.speaking-btn:hover { + box-shadow: 0 6px 20px rgba(231, 76, 60, 0.6); +} @media (max-width: 670px) { .container { width: 95%; - padding: 1.5rem; + padding: 1.8rem; } textarea { - height: 100px; + height: 120px; } -} + + h3 { + font-size: 1.8em; + } +} \ No newline at end of file diff --git a/react-speech-app/src/App.jsx b/react-speech-app/src/App.jsx index 1c7ab3f..6b770f1 100644 --- a/react-speech-app/src/App.jsx +++ b/react-speech-app/src/App.jsx @@ -1,223 +1,167 @@ -import { useState, useRef, useEffect } from "react"; -import "bootstrap/dist/css/bootstrap.min.css"; -import "./app2.css"; +import React, { useState, useEffect } from 'react'; +import './App.css'; function App() { - const [text, setText] = useState(""); - const [isSpeaking, setIsSpeaking] = useState(false); - const [isPaused, setIsPaused] = useState(false); + // State management + 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 [currentWordIndex, setCurrentWordIndex] = useState(null); + const synth = window.speechSynthesis; + // Load available voices when component mounts useEffect(() => { - const fetchVoices = () => { - const availableVoices = window.speechSynthesis.getVoices(); + const loadVoices = () => { + const availableVoices = synth.getVoices(); setVoices(availableVoices); + + // Set default voice (prefer Microsoft David if available) 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]; + + 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); - } - 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; - - textarea.focus(); - - 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(); + // Handle voices loading which can be asynchronous + if (synth.getVoices().length > 0) { + loadVoices(); } - }; + + synth.onvoiceschanged = loadVoices; - 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); - }; - - const handleRateChange = async (e) => { - const newRate = parseFloat(e.target.value); - setRate(newRate); - if (isSpeaking) { - restartSpeech(lastCharIndex, newRate); - } - }; + // Clean up on unmount + return () => { + synth.onvoiceschanged = null; + if (synth.speaking) synth.cancel(); + }; + }, []); - const toggleSpeaking = () => { - if (!text) { - alert("Please enter text to speak."); + // Handle text-to-speech with word highlighting + const handleSpeak = () => { + if (text.trim() === '') return; + + // Cancel any ongoing speech + if (synth.speaking) { + synth.cancel(); + setIsPlaying(false); + setCurrentWordIndex(null); return; } - if (isSpeaking) { - if (isPaused) { - window.speechSynthesis.resume(); - setIsPaused(false); - } else { - window.speechSynthesis.pause(); - setIsPaused(true); + const utterance = new SpeechSynthesisUtterance(text); + + // Set selected voice + const selectedVoice = voices.find(v => v.name === voice); + if (selectedVoice) utterance.voice = selectedVoice; + + // Set speech rate + utterance.rate = rate; + + // Word boundary detection for highlighting + const words = text.split(/\s+/); + let wordIndex = 0; + + utterance.onboundary = (event) => { + if (event.name === 'word') { + setCurrentWordIndex(wordIndex); + wordIndex = Math.min(wordIndex + 1, words.length - 1); } - } 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); + setCurrentWordIndex(null); }; - - speechRef.current = utterance; - setIsSpeaking(true); - setIsPaused(false); - window.speechSynthesis.speak(utterance); + + // Start speaking + setIsPlaying(true); + setCurrentWordIndex(0); + synth.speak(utterance); }; - const restartSpeech = (startIndex, speechRate = rate) => { - if (isSpeaking) { - window.speechSynthesis.cancel(); - startSpeech(startIndex, speechRate); + // Render text with current word highlighting + const renderText = () => { + if (currentWordIndex === null || !isPlaying) { + return text; } + + const words = text.split(/\s+/); + return ( +
+ {words.map((word, index) => ( + + {word}{' '} + + ))} +
+ ); }; return ( -
- - -

Text Speaking Assistant

- -
+
+

Text Speaking Assistant

+ +