Four enhancements to make the editor fit real school usage:
- Per-document tone (academic/professional/casual/humorous/creative/
persuasive/general): new documents.tone column (migration 0002), threaded
through the docs API, a bilingual ToneSelect dropdown on the title row, and
injected into the grammar-checkpoint LLM prompt so advice fits the register.
The voice pass stays tone-agnostic.
- Right-click word lookup: a new offline `lexicon` package serves definitions
(Wordset, modern ESL-friendly glosses) and synonyms (WordNet synsets first,
then frequency+stopword-ranked Moby for breadth) from gzipped embedded data,
behind /api/word/{word} with light morphology. The WordCard popover shows the
definition and tappable synonym pills that swap the word in place.
- Expanded writing stats: clicking the word count opens a StatsPanel with page
count, sentences, paragraphs, reading time, average word length, word variety,
and Flesch-Kincaid reading level — all computed client-side.
Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
51 lines
1.5 KiB
Go
51 lines
1.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
|
|
}
|
|
|
|
// 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)
|
|
}
|