diff --git a/frontend/src/components/Toolbar.jsx b/frontend/src/components/Toolbar.jsx index 2f5c0a8..6a6f96e 100644 --- a/frontend/src/components/Toolbar.jsx +++ b/frontend/src/components/Toolbar.jsx @@ -1,7 +1,50 @@ -import { useRef } from 'react' +import { useRef, useState } from 'react' export default function Toolbar({ editor, onImageUpload, fontSize, onFontSizeChange }) { - const fileRef = useRef() + const fileRef = useRef() + const [isReading, setIsReading] = useState(false) + const isReadingRef = useRef(false) // ref so closure in next() always sees current value + + function startReading() { + if (!('speechSynthesis' in window)) return + + // Grab text from cursor (or selection start) to end of document + const { from } = editor.state.selection + const end = editor.state.doc.content.size + const text = editor.state.doc.textBetween(from, end, '\n', ' ').trim() + if (!text) return + + // Split into sentences — Chrome stops an utterance after ~15 s if it's too long + const chunks = ( + text.match(/[^.!?…]+[.!?…]*['"'"]?\s*/g) + ?.map(s => s.trim()) + .filter(Boolean) + ) || [text] + + speechSynthesis.cancel() // clear any leftover utterance + isReadingRef.current = true + setIsReading(true) + + let idx = 0 + function next() { + if (!isReadingRef.current || idx >= chunks.length) { + isReadingRef.current = false + setIsReading(false) + return + } + const u = new SpeechSynthesisUtterance(chunks[idx++]) + u.onend = next + u.onerror = () => { isReadingRef.current = false; setIsReading(false) } + speechSynthesis.speak(u) + } + next() + } + + function stopReading() { + isReadingRef.current = false + setIsReading(false) + speechSynthesis.cancel() + } if (!editor) return null @@ -83,6 +126,16 @@ export default function Toolbar({ editor, onImageUpload, fontSize, onFontSizeCha {fontSize}px + + + + ) } diff --git a/frontend/src/styles/index.css b/frontend/src/styles/index.css index d94a60f..c6810dd 100644 --- a/frontend/src/styles/index.css +++ b/frontend/src/styles/index.css @@ -1463,6 +1463,21 @@ button { cursor: pointer; font-family: inherit; } font-variant-numeric: tabular-nums; } +/* ── Read Aloud ───────────────────────────────────────── */ + +.read-aloud-btn { font-size: 1rem; line-height: 1; padding: 0.2rem 0.45rem; } + +.read-aloud-btn.reading-active { + color: var(--accent-hi); + border-color: var(--accent); + animation: reading-pulse 1.8s ease-in-out infinite; +} + +@keyframes reading-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.45; } +} + /* ── Writing Prompt ───────────────────────────────────── */ .prompt-wrap { position: relative; }