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:
79
internal/suggestions/rewrite_test.go
Normal file
79
internal/suggestions/rewrite_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package suggestions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.parodia.dev/drwily/petal/internal/llm"
|
||||
)
|
||||
|
||||
// recordingClient captures the last Complete request so the rewrite test can
|
||||
// assert the styled system prompt and the selected passage reached the model.
|
||||
type recordingClient struct {
|
||||
response string
|
||||
last llm.CompletionRequest
|
||||
}
|
||||
|
||||
func (c *recordingClient) Complete(_ context.Context, req llm.CompletionRequest) (string, error) {
|
||||
c.last = req
|
||||
return c.response, nil
|
||||
}
|
||||
|
||||
func (c *recordingClient) Stream(_ context.Context, _ llm.CompletionRequest) (<-chan string, error) {
|
||||
ch := make(chan string)
|
||||
close(ch)
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func TestRewrite(t *testing.T) {
|
||||
// The model wraps its answer in quotes; cleanRewrite should strip them.
|
||||
client := &recordingClient{response: `"I have two apples."`}
|
||||
srv, docID, _ := newTestServer(t, client)
|
||||
|
||||
rec := do(t, srv, http.MethodPost, "/docs/"+docID+"/rewrite",
|
||||
`{"text":"I has two apple.","style":"academic"}`)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("rewrite: code=%d body=%s", rec.Code, rec.Body)
|
||||
}
|
||||
var resp rewriteResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if resp.Rewrite != "I have two apples." {
|
||||
t.Fatalf("rewrite = %q, want the de-quoted text", resp.Rewrite)
|
||||
}
|
||||
|
||||
// The passage rode in as the user turn, and the style steered the system turn.
|
||||
msgs := client.last.Messages
|
||||
if len(msgs) != 2 || msgs[0].Role != "system" || msgs[1].Role != "user" {
|
||||
t.Fatalf("unexpected message shape: %+v", msgs)
|
||||
}
|
||||
if msgs[1].Content != "I has two apple." {
|
||||
t.Fatalf("passage not forwarded: %q", msgs[1].Content)
|
||||
}
|
||||
if !strings.Contains(msgs[0].Content, "academic") {
|
||||
t.Fatalf("system prompt missing academic style guidance:\n%s", msgs[0].Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteEmptyText(t *testing.T) {
|
||||
client := &recordingClient{response: "anything"}
|
||||
srv, docID, _ := newTestServer(t, client)
|
||||
rec := do(t, srv, http.MethodPost, "/docs/"+docID+"/rewrite", `{"text":" ","style":"natural"}`)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("empty text: want 400, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteUnknownDoc(t *testing.T) {
|
||||
client := &recordingClient{response: "anything"}
|
||||
srv, _, _ := newTestServer(t, client)
|
||||
rec := do(t, srv, http.MethodPost, "/docs/does-not-exist/rewrite",
|
||||
`{"text":"hello there","style":"natural"}`)
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Fatalf("unknown doc: want 404, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user