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
123 lines
5.9 KiB
Go
123 lines
5.9 KiB
Go
package llm
|
|
|
|
import "fmt"
|
|
|
|
// checkpointSystemPrompt is the grammar-checkpoint instruction. It asks for
|
|
// strict JSON (no fences, no preamble) so Complete's output parses directly.
|
|
const checkpointSystemPrompt = `You are a warm, encouraging writing assistant helping someone who speaks English as a second language. ` +
|
|
`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.%s
|
|
|
|
Respond ONLY with valid JSON. No preamble, no markdown fences. Format:
|
|
{
|
|
"suggestions": [
|
|
{
|
|
"original": "exact text from the document that needs fixing",
|
|
"replacement": "corrected version",
|
|
"explanation": "friendly one-sentence explanation",
|
|
"type": "grammar|phrasing|idiom|clarity"
|
|
}
|
|
]
|
|
}
|
|
|
|
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, steered toward the document's tone.
|
|
func CheckpointMessages(contentText, tone string) []Message {
|
|
return []Message{
|
|
{Role: "system", Content: fmt.Sprintf(checkpointSystemPrompt, toneGuidance(tone))},
|
|
{Role: "user", Content: contentText},
|
|
}
|
|
}
|
|
|
|
// voiceSystemPrompt drives the Tier-1 voice-consistency pass. It is a distinct
|
|
// pass from the grammar checkpoint (spec: "do not bundle them") — the model
|
|
// reads the whole document to learn the writer's natural voice, then flags
|
|
// passages that read as tonally out of place. `replacement` is null: these are
|
|
// awareness-only, with no correction to apply.
|
|
const voiceSystemPrompt = `You are a warm, encouraging writing assistant helping someone who speaks English as a second language. ` +
|
|
`You are reviewing a COMPLETE document for VOICE CONSISTENCY only — not grammar.
|
|
|
|
Read the whole document to learn the writer's natural voice, then identify any passages (2 or more sentences) ` +
|
|
`that feel tonally inconsistent with the surrounding writing — unusually formal, unusually polished, or phrased ` +
|
|
`in a way that differs from the writer's established voice elsewhere in the document. These often signal text ` +
|
|
`that was paraphrased too closely from another source. Do not flag the first paragraph (there is no baseline yet). ` +
|
|
`Do not flag grammar or spelling mistakes — only voice.
|
|
|
|
Respond ONLY with valid JSON. No preamble, no markdown fences. Format:
|
|
{
|
|
"suggestions": [
|
|
{
|
|
"original": "exact passage from the document that feels inconsistent",
|
|
"replacement": null,
|
|
"explanation": "friendly one-sentence note, e.g. 'This passage sounds more formal than the rest of your writing — worth reviewing.'",
|
|
"type": "voice"
|
|
}
|
|
]
|
|
}
|
|
|
|
If the voice is consistent throughout, return: {"suggestions": []}`
|
|
|
|
// VoiceMessages builds the message array for a voice-consistency pass. Unlike
|
|
// the checkpoint, the caller passes the WHOLE document (no truncation) — voice
|
|
// consistency is judged against the established voice everywhere else.
|
|
func VoiceMessages(contentText string) []Message {
|
|
return []Message{
|
|
{Role: "system", Content: voiceSystemPrompt},
|
|
{Role: "user", Content: contentText},
|
|
}
|
|
}
|
|
|
|
// askPetalSystemTemplate is the Ask Petal tutor prompt. The suggestion context
|
|
// is interpolated in; the user's own messages are appended after this system
|
|
// turn by the caller.
|
|
const askPetalSystemTemplate = `You are Petal, a warm and patient English writing tutor helping someone who is learning English ` +
|
|
`as a second language. You are currently discussing a specific writing suggestion.
|
|
|
|
Suggestion context:
|
|
- Original text: "%s"
|
|
- Suggested replacement: "%s"
|
|
- Issue type: %s
|
|
- Initial explanation: "%s"
|
|
- Surrounding paragraph: "%s"
|
|
|
|
The user wants to understand this suggestion better. Detect the language of the user's message ` +
|
|
`and respond in that same language. If they write in Mandarin Chinese, respond entirely in ` +
|
|
`Mandarin. If they write in English, respond in English. Never mix languages in a single response.
|
|
|
|
Explain clearly and kindly. Use simple language appropriate to the user's message. Give examples ` +
|
|
`when helpful. If they ask "why" (or "为什么"), explain the grammar rule or idiom behind it. ` +
|
|
`If they suggest an alternative phrasing, evaluate it honestly.
|
|
|
|
Keep responses concise (2-4 sentences). This is a chat, not an essay. Be encouraging — ` +
|
|
`learning a language is hard and they're doing great.`
|
|
|
|
// AskPetalSystemPrompt fills the tutor prompt with one suggestion's context.
|
|
func AskPetalSystemPrompt(original, replacement, suggestionType, explanation, paragraph string) string {
|
|
return fmt.Sprintf(askPetalSystemTemplate, original, replacement, suggestionType, explanation, paragraph)
|
|
}
|