Commit Graph

12 Commits

Author SHA1 Message Date
prosolis
660f00d452 Phase 7: browser-side spell check (nspell, en-US)
Vendored Hunspell en.aff/en.dic into web/public/dictionaries/en/ (served as a
static asset + embedded in the binary, kept out of the JS bundle). dictionary-en
moved to a devDep — only used to source the files.

- useSpellChecker (App-level, loads once/session): fetches the dict, builds an
  nspell instance, replays a localStorage personal word list; addWord persists
  and bumps a version so consumers re-decorate. Ambient types in
  src/types/nspell.d.ts (the package ships none).
- SpellCheck Tiptap extension: misspellings as ProseMirror decorations (no
  stored marks), recomputed on edit / caret move / checker swap. Latin-only
  tokenizer so CJK is never flagged; skips short tokens + all-caps acronyms;
  exempts the caret word to avoid mid-typing jitter. Reuses mapOffset (now
  exported from SuggestionHighlight); wordAt resolves the exact span on click.
- MisspellCard: soft rose wavy underline, bilingual popover with up to 5 nspell
  corrections (click to replace) + add-to-dictionary. Closes on outside-pointer,
  edit, or doc switch.

Chinese spell check intentionally omitted — nspell is dictionary-based and
English-only; Chinese typos (homophone 别字) need an LLM, out of v1 scope.

tsc/vite/go build+vet clean; live server serves both dict files; nspell
behavior smoke-tested.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-25 22:18:21 -07:00
prosolis
183219b5de Companion: selectable companions with a picker
Click the mascot to open a bilingual picker and switch between the Sleepy Cat
(always asleep, still talks in her sleep) and the Happy Dog (awake/bouncy).
Choice persists in localStorage.

- companions.ts is the roster; add one by dropping a pure-vector Lottie JSON
  in animations/ and appending a COMPANIONS entry.
- napping = companion.alwaysAsleep || mood === 'sleeping' drives the sway/zzz.
- Removed the old single MOOD_ANIMATIONS map (animations/index.ts).

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-25 22:07:48 -07:00
prosolis
97f99b27e4 Companion: wire real Lottie assets — the always-sleeping cat
- LottiePlayer now auto-crops each asset to its content bbox (union getBBox
  across 6 frames -> square viewBox), so stock files with empty artboard
  padding fill the badge instead of floating tiny in the middle.
- Wire sleeping-cat.json to every mood: she's always asleep in the corner yet
  still mumbles tips and cheers through the bubble. napping hardcoded true so
  she keeps the calm sway + drifting zzz even while "talking".
- happy-dog.json parked in the folder as an awake-mascot alternate.
- Enable resolveJsonModule for the JSON imports.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-25 22:03:02 -07:00
prosolis
8a32dde587 Add companion kitten — reactive corner mascot
A cozy bottom-right mascot that reacts to the writing session: cheers on
accepted suggestions and word-count milestones, drops Mandarin-first writing
tips, suggests screen breaks after long stretches, and naps when idle.

- useCompanion: library-agnostic behavior engine (priority + cooldown paced
  so it never nags); tips.ts holds all copy, bilingual zh-first.
- LottiePlayer wraps lottie-web's light build (offline, no eval/CDN) so it
  bundles into the Go binary. Ships with an emoji-kitten placeholder per mood;
  dropping a Lottie cat JSON into animations/index.ts is the only swap needed.
- Speech bubble uses the CJK-first font stack (spec Note #17).

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-25 21:43:58 -07:00
prosolis
534f7262ab Phase 6: design polish — accept confetti + distraction-free mode
- Accept confetti: CSS-only 4-dot burst spawned at the accepted card,
  trajectory via inline --dx/--dy, auto-cleared after 720ms.
- Distraction-free mode: editor focus collapses the doc-list sidebar
  (width→0 slide), canvas re-centers full-width; Escape or a click
  outside the canvas (header/gutter/status bar) restores it.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-25 21:21:46 -07:00
prosolis
0fa70979a0 Phase 5: voice consistency pass
Tier-1 voice-consistency pass: whole-document LLM review surfacing passages
that read tonally out of place (formal/over-polished/paraphrased-too-closely),
as honey-decorated `voice` flags with no correction (awareness-only).

- internal/llm/voice.go: RunVoice sends the whole document (no TruncateDoc),
  MaxTokens 2048, 20s per-doc floor (VoiceInterval). Standalone voice prompt
  in prompts.go (not bundled with the grammar checkpoint, per spec).
- internal/suggestions: POST /api/docs/:id/voice. replacePending is now
  family-scoped (pendingScope) so grammar and voice never clobber each other's
  pending flags; both passes return the unified pending set. check/voice share
  one runPass helper. TestVoicePassCoexists covers both directions.
- Frontend: api.voiceDoc, useCheckpoint voicing/runVoice, honey "Check my
  voice" toolbar pill, breathing honey dot in StatusBar.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-25 21:16:53 -07:00
prosolis
3c5f3ecb96 Phase 4: Ask Petal SSE chat
Conversational follow-up on a suggestion, streamed token-by-token.

Backend (interface-only; handlers never touch a concrete LLM client):
- internal/llm/chat.go: StreamAskPetal with conversational sampling
  (max_tokens 512, temp 0.7, rep 1.15, top_p 0.92, stop "\n\n\n"),
  reusing AskPetalSystemPrompt + TrimHistory.
- internal/suggestions/chat.go: POST /api/suggestions/:id/chat. One
  user-scoped join loads the suggestion + parent content_text;
  surroundingParagraph extracts the \n\n-bounded paragraph at from_pos
  (whole-doc fallback when unlocated) and injects it server-side.
  Streams event: token / event: done SSE frames with JSON-encoded data
  so token newlines can't break framing; real http.Flusher per chunk.
  LLM-unreachable -> 502 before SSE headers; unknown suggestion -> 404.

Frontend:
- streamSuggestionChat: fetch + ReadableStream SSE parser (not
  EventSource, needs POST), abortable.
- AskPetal.tsx: whole conversation in component state (no persistence,
  cleared on close), Petal's first bubble pre-seeded with the
  explanation, rose/lavender bubbles, CJK font stack on the bubbles
  only (Note #17), streaming caret.
- SuggestionCard "Ask Petal" pill pins the card open while chatting
  (hover-close suppressed, click-away closes) and widens it to 340px.

Tests: chat_test.go covers streamed-text concat + done event,
server-side context injection on the system message, sampling params,
404, and surroundingParagraph. go build/vet/test clean, tsc clean,
vite build OK. Live SSE smoke-tested against a fake streaming vLLM:
tokens flushed individually through the chi middleware stack, done
terminator, 502 on LLM-down, 404 on unknown suggestion.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-25 21:05:39 -07:00
prosolis
3f7e705028 llm/ollama: disable thinking so reasoning models return content
Qwen 3.5 — the spec's recommended model — is a reasoning model. With
thinking on, Ollama streams its chain-of-thought into a separate
`thinking` field and hits num_predict before emitting any answer into
`content`, so Complete() got an empty string and the checkpoint failed
with "no JSON object in model output". Sending `"think": false` on every
/api/chat request fixes it; non-thinking models (qwen2.5) ignore the flag.

Validated end-to-end on deployment hardware (Ollama, qwen3.5:9b): the
grammar checkpoint now caught all five ESL errors in a 3-sentence sample
with correct JSON and string-anchoring, ~8.5s warm.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-25 20:53:01 -07:00
prosolis
a4069d5755 Phase 3: LLM grammar checkpoint
Backend (internal/llm): backend-agnostic LLMClient interface + factory
with vLLM (OpenAI-compat) and Ollama (native) clients, each Complete +
Stream. prompts.go holds the checkpoint and Ask Petal templates;
checkpoint.go salvages JSON from model output (brace-matched), enforces a
per-doc 30s RateLimiter, and truncates the doc to a latency cap.

internal/suggestions: POST /api/docs/:id/check runs a checkpoint and
replaces the doc's pending suggestions in one tx (accepted/rejected kept
as history); GET /api/docs/:id/suggestions lists pending;
POST /api/suggestions/:id/{accept,dismiss} resolves one. Throttled checks
return the current set rather than erroring.

Frontend: useCheckpoint (4s debounce, loads existing on open, stale-guard
tokens); SuggestionHighlight renders ProseMirror decorations re-anchored
by the `original` string on every doc change (not stored marks), with
precise textblock-offset→PM-position mapping; SuggestionCard shows the
type tag + diff + explanation and applies the replacement in-editor on
accept; breathing rose checkpoint dot in the StatusBar; fade-float +
breathe animations.

Tests: llm parse/rate-limit/truncate; suggestions full flow + rate-limit
over httptest with a stub client. Smoke-tested end-to-end against a fake
vLLM endpoint (anchoring verified) and the LLM-unreachable 502 path.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-25 20:45:30 -07:00
prosolis
5e00cdce88 Phase 2: document CRUD + auto-save
Backend internal/docs: chi sub-router (list/create/get/update/delete)
mounted at /api/docs, scoped to the local user. Create uses RETURNING;
update is a COALESCE partial-update so rename and full editor save share
one PUT. JSON 404/400 errors; handlers_test.go walks the lifecycle.

Frontend: api/client.ts, useAutoSave (1.5s debounce + saveNow flush
before doc switch), EditorCore (Tiptap StarterKit/Underline/TextAlign/
Placeholder/CharacterCount) + Toolbar, DocList/DocListItem, StatusBar,
and an App.tsx that orchestrates load/select/create/delete with
optimistic sidebar patching. content + content_text + word_count are
emitted together on every edit. .petal-prose styling (Lora body, Nunito
headings).

Verified: tsc clean, vite build, go build, full CRUD smoke test incl.
CJK title round-trip and SPA serve.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2026-06-25 20:30:39 -07:00
prosolis
9c98e97030 Phase 1: data layer (SQLite, migrations, models, seed) 2026-06-25 20:25:14 -07:00
prosolis
e72d48d64e Phase 0: project scaffold
- Go module + chi server with embedded SPA serving and /api/health
- internal/config env loader (local-dev defaults; auth/copyleaks deferred)
- React 19 + Vite 6 + Tailwind v4 frontend with full Petal design tokens
- Frontend embedded into the binary via web/embed.go (go:embed all:dist)
- README dev workflow, .env.example, BUILD_PLAN progress tracker
- Verified end-to-end: binary serves health, embedded SPA, and SPA fallback
2026-06-25 20:20:04 -07:00