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
82 lines
2.5 KiB
Go
82 lines
2.5 KiB
Go
package lexicon
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// Handler serves the word-lookup endpoint backed by a single shared Lexicon.
|
|
type Handler struct {
|
|
Lex *Lexicon
|
|
}
|
|
|
|
// New constructs a Handler with a fresh (lazily-loaded) Lexicon.
|
|
func NewHandler() *Handler { return &Handler{Lex: New()} }
|
|
|
|
// Routes returns the router mounted at /api/word. The word is a path segment so
|
|
// "/api/word/happy" reads naturally; it's URL-decoded to tolerate the rare
|
|
// punctuated token.
|
|
func (h *Handler) Routes() chi.Router {
|
|
r := chi.NewRouter()
|
|
r.Get("/{word}", h.lookup)
|
|
return r
|
|
}
|
|
|
|
// GlossRoutes returns the router mounted at /api/gloss — the lightweight
|
|
// Chinese-only lookup behind the inline hover/select gloss. It shares the
|
|
// Handler's Lexicon, so the datasets still load just once.
|
|
func (h *Handler) GlossRoutes() chi.Router {
|
|
r := chi.NewRouter()
|
|
r.Get("/{word}", h.gloss)
|
|
return r
|
|
}
|
|
|
|
// lookup returns the definition + synonyms for one word. A word found in neither
|
|
// dataset still returns 200 with empty lists, so the popover can show a friendly
|
|
// "nothing found" rather than an error state.
|
|
func (h *Handler) lookup(w http.ResponseWriter, r *http.Request) {
|
|
word := chi.URLParam(r, "word")
|
|
if decoded, err := url.PathUnescape(word); err == nil {
|
|
word = decoded
|
|
}
|
|
|
|
res, err := h.Lex.Lookup(word)
|
|
if err != nil {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
// Word lookups are static for the life of the build; let the browser cache
|
|
// them so repeated right-clicks on the same word are instant.
|
|
w.Header().Set("Cache-Control", "public, max-age=86400")
|
|
_ = json.NewEncoder(w).Encode(res)
|
|
}
|
|
|
|
// gloss returns just the Chinese translation for one word. Like lookup, a miss
|
|
// is a 200 with an empty gloss so the hover tooltip can quietly skip rather than
|
|
// error.
|
|
func (h *Handler) gloss(w http.ResponseWriter, r *http.Request) {
|
|
word := chi.URLParam(r, "word")
|
|
if decoded, err := url.PathUnescape(word); err == nil {
|
|
word = decoded
|
|
}
|
|
|
|
res, err := h.Lex.Gloss(word)
|
|
if err != nil {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Cache-Control", "public, max-age=86400")
|
|
_ = json.NewEncoder(w).Encode(res)
|
|
}
|