Editor: document tone, right-click word lookup, expanded stats

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
This commit is contained in:
prosolis
2026-06-25 23:22:55 -07:00
parent 95123e8c49
commit 4c288834c0
23 changed files with 1021 additions and 32 deletions

View File

@@ -31,9 +31,9 @@ type checkpointResponse struct {
// RunCheckpoint sends the grammar checkpoint and parses the JSON result. It
// applies the latency-guard truncation and the checkpoint sampling parameters
// from the spec.
func RunCheckpoint(ctx context.Context, client LLMClient, contentText string) ([]RawSuggestion, error) {
func RunCheckpoint(ctx context.Context, client LLMClient, contentText, tone string) ([]RawSuggestion, error) {
raw, err := client.Complete(ctx, CompletionRequest{
Messages: CheckpointMessages(TruncateDoc(contentText)),
Messages: CheckpointMessages(TruncateDoc(contentText), tone),
MaxTokens: 1024,
Temperature: 0.3,
RepetitionPenalty: 1.15,

View File

@@ -8,7 +8,7 @@ const checkpointSystemPrompt = `You are a warm, encouraging writing assistant he
`Analyze the text below and identify up to 5 issues: grammar errors, unnatural phrasing, ` +
`incorrect idiom usage, or unclear sentences that are common ESL patterns.
Be specific, friendly, and explain WHY each suggestion improves the writing.
Be specific, friendly, and explain WHY each suggestion improves the writing.%s
Respond ONLY with valid JSON. No preamble, no markdown fences. Format:
{
@@ -24,11 +24,32 @@ Respond ONLY with valid JSON. No preamble, no markdown fences. Format:
If the writing looks good, return: {"suggestions": []}`
// toneGuidance returns a sentence steering the checkpoint toward the writer's
// chosen tone, or "" for the neutral default. The clause is appended to the
// checkpoint instructions so the model's phrasing suggestions fit the target
// register (e.g. an academic essay vs a casual journal). Unknown values fall
// back to no steering, so a stray tone string is harmless.
func toneGuidance(tone string) string {
clause, ok := map[string]string{
"academic": "formal, academic, and objective — suited to a school essay or research paper",
"professional": "polished and professional — suited to a workplace email or report",
"casual": "relaxed, friendly, and conversational",
"humorous": "light, playful, and good-humored",
"creative": "vivid, expressive, and imaginative — suited to a story or personal narrative",
"persuasive": "confident and persuasive — suited to an argument or opinion piece",
}[tone]
if !ok {
return ""
}
return "\n\nThe writer wants this document to read as " + clause + ". When phrasing could " +
"be improved, prefer suggestions that fit that tone, and gently flag wording that clashes with it."
}
// CheckpointMessages builds the message array for a grammar checkpoint over the
// given (already-truncated) document text.
func CheckpointMessages(contentText string) []Message {
// given (already-truncated) document text, steered toward the document's tone.
func CheckpointMessages(contentText, tone string) []Message {
return []Message{
{Role: "system", Content: checkpointSystemPrompt},
{Role: "system", Content: fmt.Sprintf(checkpointSystemPrompt, toneGuidance(tone))},
{Role: "user", Content: contentText},
}
}

View File

@@ -16,7 +16,10 @@ const VoiceInterval = 20 * time.Second
// for a Tier-1 voice pass and parses the JSON result. It reuses the checkpoint's
// tolerant parser and a larger token budget, since one pass may flag several
// passages. Each flag carries a null replacement (awareness-only).
func RunVoice(ctx context.Context, client LLMClient, contentText string) ([]RawSuggestion, error) {
// The tone argument is accepted for a uniform pass signature but ignored: voice
// consistency is judged against the document's own established voice, not an
// externally-chosen register.
func RunVoice(ctx context.Context, client LLMClient, contentText, _ string) ([]RawSuggestion, error) {
raw, err := client.Complete(ctx, CompletionRequest{
Messages: VoiceMessages(contentText),
MaxTokens: 2048,