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
80 lines
2.4 KiB
Go
80 lines
2.4 KiB
Go
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)
|
|
}
|
|
}
|