Phase 12 + 13: collocation coach + vocabulary garden
Phase 12 — collocation coach: a third suggestion family for gentle
"natives usually say…" hints on non-native word pairings, reusing the
existing runPass/pendingScope/rail machinery.
- llm/collocation.go (RunCollocation, 25s floor, reuses ParseCheckpoint)
+ collocationSystemPrompt/CollocationMessages (warm, Mandarin gloss,
defers grammar/spelling to the grammar family)
- migration 0005 rebuilds the suggestions table to extend the type CHECK
(SQLite can't ALTER a CHECK)
- collocationScope + CollocationLimit + POST /{id}/collocation
- fix: grammarScope was `type != 'voice'` and would wipe the new
collocation flags; now `type NOT IN ('voice','collocation')`
- frontend: --color-blossom, "Make it sound natural 🌸" pill,
collocating/runCollocation in useCheckpoint, StatusBar dot
Phase 13 — vocabulary garden: capture looked-up words and surface them
for gentle spaced repetition.
- new internal/vocab package: migration 0006 (vocab_words, SM-2-lite
columns, doc_id ON DELETE SET NULL, UNIQUE(user_id,word)),
scheduler.go (Leitner ladder 1/3/7/16/35 then geometric; gentle
"again", no streak-shaming), handlers (capture-upsert/list/due/
review/delete, owner-scoped, SQLite-side datetime math)
- auto-capture on word lookup (dictionary-known words only, captures
the surrounding sentence + doc_id) + 🤍/💚 toggle on WordCard
- GardenPanel: blossom grid (bloom by reps), flashcard review (sentence
blanked, flip, again/good/easy, recognition↔production), sleepy-kitten
footer; opened from a global 🌷 header button
Tests: TestCollocationPassCoexists, vocab scheduler + handlers, db CHECK
extended. go build/vet/test + tsc + vite + vitest (51/51) clean;
migration verified against a copy of the live DB; live backend smoke
walked the full vocab lifecycle + the warm-502 collocation path.
Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
@@ -23,19 +23,22 @@ import (
|
||||
// grammar checkpoint and the voice pass each get their own per-document rate
|
||||
// limiter — they are independent passes with different cadences.
|
||||
type Handler struct {
|
||||
DB *db.DB
|
||||
Client llm.LLMClient
|
||||
Limit *llm.RateLimiter // grammar checkpoint floor
|
||||
VoiceLimit *llm.RateLimiter // voice-consistency floor
|
||||
DB *db.DB
|
||||
Client llm.LLMClient
|
||||
Limit *llm.RateLimiter // grammar checkpoint floor
|
||||
VoiceLimit *llm.RateLimiter // voice-consistency floor
|
||||
CollocationLimit *llm.RateLimiter // collocation-coach floor
|
||||
}
|
||||
|
||||
// New constructs a Handler with per-document checkpoint and voice rate limiters.
|
||||
// New constructs a Handler with per-document checkpoint, voice, and collocation
|
||||
// rate limiters.
|
||||
func New(database *db.DB, client llm.LLMClient) *Handler {
|
||||
return &Handler{
|
||||
DB: database,
|
||||
Client: client,
|
||||
Limit: llm.NewRateLimiter(llm.CheckpointInterval),
|
||||
VoiceLimit: llm.NewRateLimiter(llm.VoiceInterval),
|
||||
DB: database,
|
||||
Client: client,
|
||||
Limit: llm.NewRateLimiter(llm.CheckpointInterval),
|
||||
VoiceLimit: llm.NewRateLimiter(llm.VoiceInterval),
|
||||
CollocationLimit: llm.NewRateLimiter(llm.CollocationInterval),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +47,7 @@ func New(database *db.DB, client llm.LLMClient) *Handler {
|
||||
func (h *Handler) RegisterDocRoutes(r chi.Router) {
|
||||
r.Post("/{id}/check", h.check)
|
||||
r.Post("/{id}/voice", h.voice)
|
||||
r.Post("/{id}/collocation", h.collocation)
|
||||
r.Post("/{id}/rewrite", h.rewrite)
|
||||
r.Get("/{id}/suggestions", h.listForDoc)
|
||||
}
|
||||
@@ -70,6 +74,13 @@ func (h *Handler) voice(w http.ResponseWriter, r *http.Request) {
|
||||
h.runPass(w, r, h.VoiceLimit, llm.RunVoice, voiceScope)
|
||||
}
|
||||
|
||||
// collocation runs the collocation coach over the whole document, flagging
|
||||
// non-native word pairings. Explicit-action pass; replaces only the pending
|
||||
// collocation flags.
|
||||
func (h *Handler) collocation(w http.ResponseWriter, r *http.Request) {
|
||||
h.runPass(w, r, h.CollocationLimit, llm.RunCollocation, collocationScope)
|
||||
}
|
||||
|
||||
// pass is the signature shared by the grammar checkpoint and the voice pass:
|
||||
// given the document text and the document's tone it returns the model's raw
|
||||
// suggestions. The voice pass ignores tone (see llm.RunVoice).
|
||||
@@ -151,10 +162,14 @@ type pendingScope struct {
|
||||
}
|
||||
|
||||
var (
|
||||
// grammarScope owns the grammar/phrasing/idiom/clarity flags (everything but voice).
|
||||
grammarScope = pendingScope{deleteWhere: "type != 'voice'", forceType: ""}
|
||||
// grammarScope owns the grammar/phrasing/idiom/clarity flags — everything but
|
||||
// the explicit-action families (voice, collocation), which run on their own
|
||||
// cadence and must survive a grammar checkpoint.
|
||||
grammarScope = pendingScope{deleteWhere: "type NOT IN ('voice','collocation')", forceType: ""}
|
||||
// voiceScope owns the voice flags only.
|
||||
voiceScope = pendingScope{deleteWhere: "type = 'voice'", forceType: db.SuggestionTypeVoice}
|
||||
// collocationScope owns the collocation flags only.
|
||||
collocationScope = pendingScope{deleteWhere: "type = 'collocation'", forceType: db.SuggestionTypeCollocation}
|
||||
)
|
||||
|
||||
// replacePending swaps a document's pending suggestions within one family for a
|
||||
@@ -320,7 +335,7 @@ func locate(contentText, original string) (int, int) {
|
||||
// defaulting unknown values to grammar so a stray label never trips the CHECK.
|
||||
func normalizeType(t string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(t)) {
|
||||
case db.SuggestionTypeGrammar, db.SuggestionTypePhrasing, db.SuggestionTypeIdiom, db.SuggestionTypeClarity:
|
||||
case db.SuggestionTypeGrammar, db.SuggestionTypePhrasing, db.SuggestionTypeIdiom, db.SuggestionTypeClarity, db.SuggestionTypeCollocation:
|
||||
return strings.ToLower(strings.TrimSpace(t))
|
||||
default:
|
||||
return db.SuggestionTypeGrammar
|
||||
|
||||
Reference in New Issue
Block a user