7 Commits

Author SHA1 Message Date
4db65151c8 feat: read-aloud highlights and scrolls to current sentence/word
- Add ReadingMark TipTap extension (transient, never saved to DB) that
  renders the active TTS passage as <span class='reading-word'>
- Build a char→ProseMirror position map on read-start so boundary events
  can pinpoint exact document positions
- Use onstart (fires on every utterance/voice) for reliable sentence-level
  highlight; onboundary overrides with word-level when the voice supports it
- Auto-scroll the highlighted span into view (smooth, centred) on each update
- Strip readingWord marks from JSON alongside lintError before saving
- Guard all mark dispatches with applyingLints flag to suppress spurious
  saves and lint re-checks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 22:48:27 -04:00
c6589b7dcf Fix lint infinite loop, popover usability, debounce timing
Infinite loop (lint hammering server every 2s):
- Add applyingLints ref; set true before clearLintMarks() and
  editor.view.dispatch(tr), false after
- onUpdate returns early when applyingLints is true, so applying
  marks no longer reschedules a lint check or triggers a save

Popover disappearing before you can interact:
- Render via createPortal(…, document.body) — completely outside the
  editor DOM, no stacking context or overflow clipping can interfere
- onMouseDown={e => e.preventDefault()} on the popover prevents the
  editor from losing focus before the button click fires
- Removed setLintPopover(null) from the top of runLint — the popover
  was being force-closed on every auto-check cycle

Debounce: 1.5 s → 3 s — checks after a genuine pause, not mid-word

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 22:00:56 -04:00
e5b9f643e1 Fix TDZ crash: move useEffect hooks to after runLint declaration
useEffect(() => {...}, [runLint]) evaluated [runLint] immediately,
but runLint was declared with const on the next line — temporal dead
zone. Moved both effects to after the useCallback closes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:52:39 -04:00
91071270a4 Auto-check after every word; disable browser spellcheck
- spellCheck={false} on EditorContent — LanguageTool's wavy marks
  take over entirely, no double-squiggle confusion
- onUpdate schedules runLint() 1.5 s after the last keystroke via
  lintDebounce ref; typing resets the timer so it only fires when
  the writer pauses (naturally after finishing a word or sentence)
- runLintRef kept in sync with useEffect so the stale onUpdate
  closure always calls the latest runLint
- Timer cleaned up on unmount

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:48:47 -04:00
55375d2ff0 Built-in spell & grammar checker (LanguageTool)
Server:
- New POST /api/lint/check proxies to LanguageTool API (public free
  endpoint by default; override with LANGUAGETOOL_URL env var for
  self-hosted instance)
- Returns trimmed match list: message, offset, length, replacements,
  kind (spelling|grammar), ruleId
- Disables cosmetic rules that don't suit creative writing
- 20-second timeout; falls back with 502 on error

Frontend:
- LintMark: transient TipTap Mark extension (never saved to DB —
  stripLintMarks() removes them from getJSON() before onChange fires)
- buildTextMap(): walks ProseMirror doc tree, builds parallel plain-
  text string + position array so LanguageTool char offsets map back
  to exact PM positions even across paragraphs / headings
- clearLintMarks() + runLint(): check → clear old marks → apply new
  marks in one transaction, no flicker
- Click on underlined text → fixed-position popover showing the error
  message + up to 6 one-click replacement buttons + Ignore / ✕
- Applying a replacement triggers an automatic re-check after 600 ms
- lintStatus: idle → checking → done → stale (when user edits)
- ABC toolbar button shows count badge when errors found, ✓ when clean
- Wavy red underline = spelling, wavy blue = grammar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:35:38 -04:00
37448be5a8 Add notes panel, font picker, sticky toolbar, and prompt improvements
- Notes: per-story rich-text notes panel (right drawer) with TipTap
  editor, image support, autosave, and full CRUD API
- Font picker: 15 Google Fonts selectable from a floating Aa button,
  persisted to localStorage via --font-body CSS variable
- Sticky toolbar: pulled formatting bar out of overflow:hidden wrapper
  so it sticks below the topbar while scrolling
- Prompts: 100 additional built-in prompts (120 total) in a shuffled
  no-repeat queue; pre-fetch on page load so the AI has time to respond;
  timeout raised to 45s; error logging + /api/prompts/test debug endpoint;
  source badge shows whether prompt came from AI or built-in list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:21:26 -04:00
1f51504de9 Add story writer app with editor, auth, export, and polish features
- React + TipTap editor with formatting toolbar (bold, italic, underline,
  strikethrough, alignment, highlight, scene breaks)
- Custom image node view with resize and alignment controls; server-side
  WebP conversion via sharp
- Express + SQLite backend with JWT auth and admin user management
- Export to PDF, EPUB, and ODT
- Five themes (Midnight, Gothic Night, Enchanted Forest, Aged Manuscript,
  Neon Noir); Lora body font for readability
- Writing streak, daily word goal, milestones, and Ollama writing prompts
- Docker Compose setup for self-hosted deployment behind NPMplus

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 11:47:55 -04:00