Phase 9: ESL superpowers — Chinese gloss + tone-rewrite

Inline Chinese gloss (offline) and a "say it more naturally" / tone-rewrite,
the two ESL features for the Mandarin-speaking writer.

Gloss: embedded English→Chinese dictionary (gloss.json.gz, 57k common words
built from ECDICT via scripts/build_gloss.py). lexicon gains Gloss()/Result.Gloss
and a lightweight GET /api/gloss/{word}; the right-click WordCard leads with the
中文; GlossTip shows it on a 350ms hover (reuses wordAt, so CJK is never glossed).
Offline + instant, works with the LLM down.

Rewrite: selecting text pops a SelectionBubble (更自然 + the tone vocabulary);
picking a style calls POST /api/docs/:id/rewrite (llm.RunRewrite, stateless,
owner-scoped) and shows a RewritePreview (original→rewrite, accept/cancel/retry).
Accept applies it in-editor.

Tests added in lexicon and suggestions. go build/vet/test, tsc, vite all clean;
live smoke vs a fake vLLM verified gloss + rewrite + 400/404/502 paths.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
prosolis
2026-06-26 00:07:26 -07:00
parent 8e1111d768
commit 60eba25fee
20 changed files with 1036 additions and 14 deletions

View File

@@ -51,6 +51,48 @@ func TestLookupUnknownWord(t *testing.T) {
}
}
func TestGloss(t *testing.T) {
l := New()
// A common word carries a Chinese gloss...
res, err := l.Gloss("river")
if err != nil {
t.Fatalf("Gloss: %v", err)
}
if res.Word != "river" {
t.Errorf("Word = %q, want %q", res.Word, "river")
}
if res.Gloss == "" {
t.Errorf("expected a Chinese gloss for %q, got none", "river")
}
// ...and the inflected form resolves via the same de-inflection.
inflected, err := l.Gloss("rivers")
if err != nil {
t.Fatalf("Gloss(rivers): %v", err)
}
if inflected.Gloss == "" {
t.Errorf("expected a gloss for inflected %q, got none", "rivers")
}
// A nonsense word is a clean empty (not an error).
miss, err := l.Gloss("zzzxqqq")
if err != nil {
t.Fatalf("Gloss(miss): %v", err)
}
if miss.Gloss != "" {
t.Errorf("expected empty gloss for nonsense word, got %q", miss.Gloss)
}
}
func TestLookupIncludesGloss(t *testing.T) {
l := New()
res, err := l.Lookup("happy")
if err != nil {
t.Fatalf("Lookup: %v", err)
}
if res.Gloss == "" {
t.Errorf("expected Lookup to include a Chinese gloss for %q", "happy")
}
}
func TestCandidates(t *testing.T) {
cases := map[string]string{
"cats": "cat",