import React, { useState, useRef, useEffect } from 'react'; import { Box, IconButton, Tooltip } from '@mui/material'; import { PlayArrow as PlayIcon, Pause as PauseIcon, Stop as StopIcon } from '@mui/icons-material'; /** * MessageContent Component * Renders message text with inline word highlighting during TTS playback */ function MessageContent({ content, isPlaying, currentWordIndex, formatMarkdown }) { if (!isPlaying || currentWordIndex < 0) { // Normal rendering without highlighting return ( ); } // Parse content into words for highlighting const words = content.split(/(\s+)/); let wordCount = 0; return ( {words.map((word, idx) => { if (word.trim().length === 0) { return {word}; } const thisWordIndex = wordCount; wordCount++; const isCurrentWord = thisWordIndex === currentWordIndex; const isPastWord = thisWordIndex < currentWordIndex; return ( {word} ); })} ); } /** * InlineTTSControls Component * Simple play/pause/stop controls for TTS - no progress bar, no loaders */ function InlineTTSControls({ isPlaying, onPlay, onPause, onStop }) { return ( {!isPlaying ? ( ) : ( <> )} ); } /** * useTTS Hook * Handles Web Speech API with word boundary tracking */ export function useTTS(text) { const [isPlaying, setIsPlaying] = useState(false); const [isPaused, setIsPaused] = useState(false); const [currentWordIndex, setCurrentWordIndex] = useState(-1); const utteranceRef = useRef(null); const wordsRef = useRef([]); useEffect(() => { if (text) { wordsRef.current = text.split(/\s+/).filter(w => w.length > 0); } return () => { if (window.speechSynthesis) { window.speechSynthesis.cancel(); } }; }, [text]); const play = () => { if (isPaused && window.speechSynthesis) { window.speechSynthesis.resume(); setIsPlaying(true); setIsPaused(false); return; } if ('speechSynthesis' in window) { window.speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(text); utteranceRef.current = utterance; utterance.rate = 1.0; utterance.pitch = 1.0; // Track word boundaries utterance.onboundary = (event) => { if (event.name === 'word') { const charIndex = event.charIndex; let wordIdx = 0; let charCount = 0; for (let i = 0; i < wordsRef.current.length; i++) { if (charCount >= charIndex) { wordIdx = i; break; } charCount += wordsRef.current[i].length + 1; } setCurrentWordIndex(wordIdx); } }; utterance.onstart = () => { setIsPlaying(true); setIsPaused(false); setCurrentWordIndex(0); }; utterance.onend = () => { setIsPlaying(false); setIsPaused(false); setCurrentWordIndex(-1); }; utterance.onerror = () => { setIsPlaying(false); setIsPaused(false); setCurrentWordIndex(-1); }; window.speechSynthesis.speak(utterance); } }; const pause = () => { if (window.speechSynthesis) { window.speechSynthesis.pause(); setIsPlaying(false); setIsPaused(true); } }; const stop = () => { if (window.speechSynthesis) { window.speechSynthesis.cancel(); } setIsPlaying(false); setIsPaused(false); setCurrentWordIndex(-1); }; return { isPlaying, isPaused, currentWordIndex, play, pause, stop }; } export { MessageContent, InlineTTSControls };