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; }