import { useEffect, useState } from 'react' import { annotate, renderAnnotations, wordCard, saveLesson, updateLesson, renameLesson, deleteLesson, getLesson, } from '../api' import { speak } from '../tts' import WordCard from '../components/WordCard' import BlockEditor from '../components/BlockEditor' import { stripMarkdown } from '../blocks' const SAMPLE_TEXT = 'Le petit chat noir dort sur la grande table. ' + "La femme mange une pomme délicieuse avec son ami. " + 'Le livre est ouvert sur le bureau.' export default function Notebook({ openLessonId, onLessonOpened, onTextChange }) { const [lessonId, setLessonId] = useState(null) const [title, setTitle] = useState('') const [text, setText] = useState(SAMPLE_TEXT) const [ann, setAnn] = useState({ tokens: [], meanings: {} }) const [html, setHtml] = useState('') const [colorsOn, setColorsOn] = useState(true) const [status, setStatus] = useState('') const [wordCardData, setWordCardData] = useState(null) const [wordCardLoading, setWordCardLoading] = useState(false) const [confirmDelete, setConfirmDelete] = useState(false) // Show a colored sample on first load, like the old Blocks app did. useEffect(() => { annotate(SAMPLE_TEXT, true) .then((data) => { setAnn({ tokens: data.tokens, meanings: data.meanings }) setHtml(data.html) }) .catch(() => {}) }, []) // Let App.jsx know the current lesson text, so other screens (Exercises, // Chat) can use it as context without lifting the whole editor state. // Markdown markers are stripped so the LLM/spaCy see clean prose. useEffect(() => { onTextChange?.(stripMarkdown(text)) }, [text, onTextChange]) // Load a lesson opened from the Lessons browser. useEffect(() => { if (!openLessonId) return let cancelled = false getLesson(openLessonId) .then((page) => { if (cancelled) return const a = page.annotations && page.annotations.tokens ? page.annotations : { tokens: [], meanings: {} } setLessonId(page.id) setTitle(page.title || '') setText(page.raw_text || '') setAnn(a) setStatus('') setWordCardData(null) setConfirmDelete(false) return renderAnnotations(a, colorsOn) }) .then((r) => { if (r && !cancelled) setHtml(r.html) }) .catch((e) => setStatus(`⚠ Could not load page: ${e.message}`)) .finally(() => onLessonOpened?.()) return () => { cancelled = true } // eslint-disable-next-line react-hooks/exhaustive-deps }, [openLessonId]) // Annotate just the highlighted text if there's a selection, otherwise // fall back to the whole note. Reading window.getSelection() here relies // on the click not having collapsed it first — see the Annotate button's // onMouseDown below. const handleAnnotate = async () => { const sel = window.getSelection() const selected = sel && !sel.isCollapsed ? sel.toString().trim() : '' const target = selected || stripMarkdown(text) if (!target) { setStatus('Nothing to annotate — select some French text or write a lesson first.') return } try { const data = await annotate(target, colorsOn) setAnn({ tokens: data.tokens, meanings: data.meanings }) setHtml(data.html) setWordCardData(null) } catch (e) { setStatus(`⚠ Could not annotate: ${e.message}`) } } const handleToggleColors = async (checked) => { setColorsOn(checked) try { const r = await renderAnnotations(ann, checked) setHtml(r.html) } catch { /* ignore */ } } const handleContainerClick = async (e) => { const tok = e.target.closest('[data-token]') if (!tok) return const tokenInfo = { text: tok.dataset.text, gender: tok.dataset.gender || '', pos: tok.dataset.pos || '', lemma: tok.dataset.lemma || tok.dataset.text, } speak(tokenInfo.text) setWordCardLoading(true) setWordCardData({ ...tokenInfo, meaning: '', grammar: '' }) try { const data = await wordCard(tokenInfo, ann.meanings) setAnn((prev) => ({ ...prev, meanings: data.meanings })) setWordCardData(data) } catch (err) { setWordCardData({ ...tokenInfo, meaning: `⚠ ${err.message}`, grammar: '' }) } finally { setWordCardLoading(false) } } const handleSave = async () => { if (!text.trim()) { setStatus('Nothing to save — type or paste some French text first.'); return } try { const { id, title: newTitle } = await saveLesson(text, ann) setLessonId(id) setTitle(newTitle) setStatus(`✅ Saved as ${newTitle}`) } catch (e) { setStatus(`⚠ Could not save: ${e.message}`) } } const handleUpdate = async () => { if (!lessonId) return if (!text.trim()) { setStatus('Nothing to save.'); return } try { const { title: t } = await updateLesson(lessonId, text, ann) setTitle(t) setStatus(`✅ Updated ${t}`) } catch (e) { setStatus(`⚠ Could not update: ${e.message}`) } } const handleRename = async () => { if (!lessonId) return if (!title.trim()) { setStatus("⚠ Title can't be empty."); return } try { const { title: t } = await renameLesson(lessonId, title) setTitle(t) setStatus(`✅ Renamed to ${t}`) } catch (e) { setStatus(`⚠ Could not rename: ${e.message}`) } } const handleDelete = async () => { if (!lessonId) return try { await deleteLesson(lessonId) setLessonId(null) setTitle('') setText('') setAnn({ tokens: [], meanings: {} }) setHtml('') setWordCardData(null) setConfirmDelete(false) setStatus('🗑️ Page deleted.') } catch (e) { setStatus(`⚠ Could not delete: ${e.message}`) } } return (