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:
46
internal/llm/rewrite.go
Normal file
46
internal/llm/rewrite.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RewriteMaxRunes caps how much selected text a single rewrite will accept. A
|
||||
// rewrite is a focused "fix this sentence/paragraph" action, not a whole-doc
|
||||
// pass — bounding it keeps latency sane and the model on-task. The handler
|
||||
// rejects longer selections before calling the model.
|
||||
const RewriteMaxRunes = 2000
|
||||
|
||||
// RunRewrite rewrites a selected passage in the requested style (e.g. "natural",
|
||||
// "academic"). It is a one-shot Complete — the result is shown as a preview the
|
||||
// writer accepts or discards, so we want the whole rewrite before rendering.
|
||||
func RunRewrite(ctx context.Context, client LLMClient, text, style string) (string, error) {
|
||||
out, err := client.Complete(ctx, CompletionRequest{
|
||||
Messages: RewriteMessages(text, style),
|
||||
MaxTokens: 1024,
|
||||
Temperature: 0.7,
|
||||
TopP: 0.9,
|
||||
RepetitionPenalty: 1.1,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return cleanRewrite(out), nil
|
||||
}
|
||||
|
||||
// cleanRewrite trims the model's output down to just the rewritten passage. The
|
||||
// prompt asks for no quotes or preamble, but small instruct models occasionally
|
||||
// wrap the answer in matching quotes — strip a single surrounding pair so the
|
||||
// text drops cleanly into the editor.
|
||||
func cleanRewrite(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) >= 2 {
|
||||
first, last := s[0], s[len(s)-1]
|
||||
if (first == '"' && last == '"') ||
|
||||
(first == '\'' && last == '\'') ||
|
||||
(strings.HasPrefix(s, "“") && strings.HasSuffix(s, "”")) {
|
||||
s = strings.TrimSpace(strings.Trim(s, "\"'“”"))
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user