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}, } } // collocationSystemPrompt drives the collocation coach — the single most // valuable polish for an ESL writer. It flags word PAIRINGS that are not wrong, // just non-native ("do a decision" → "make a decision", "strong rain" → "heavy // rain"), and explicitly DEFERS real grammar/spelling errors to the grammar // checkpoint so the two families don't overlap. Every explanation is framed as a // warm "natives usually say…" note with a short gloss in the writer's own // language — never "error/wrong" — because these are stylistic, not mistakes. It is a distinct // pass from the grammar checkpoint (do not bundle them). `replacement` carries // the natural pairing the writer can accept in one tap. const collocationSystemPrompt = `You are a warm, encouraging writing assistant helping someone who speaks English as a second language. ` + `You are reviewing a COMPLETE document for COLLOCATIONS only — the natural word pairings native speakers use. A collocation is a pair or short group of words that native speakers habitually say together. ESL writers ` + `often choose words that are grammatically correct but sound non-native: "do a decision" instead of "make a decision", ` + `"strong rain" instead of "heavy rain", "say a joke" instead of "tell a joke". These are NOT grammar mistakes — they ` + `are just not what a native speaker would naturally say. Identify up to 5 such non-native word pairings. For each, give the natural pairing a native speaker would use. ` + `Be gentle and specific. Do NOT flag grammar errors, spelling mistakes, or unclear sentences — those are handled ` + `elsewhere. Only flag word pairings that are correct but sound non-native.%s Phrase every explanation warmly as "Natives usually say…" and include a brief %s gloss in parentheses. ` + `Never use the words "error", "wrong", or "mistake" — these are friendly polish, not corrections. Respond ONLY with valid JSON. No preamble, no markdown fences. Format: { "suggestions": [ { "original": "exact word pairing from the document", "replacement": "the natural native pairing", "explanation": "friendly note, e.g. 'Natives usually say \"make a decision\" rather than \"do a decision\" (native usage / 地道说法).'", "type": "collocation" } ] } If every pairing already sounds natural, return: {"suggestions": []}` // CollocationMessages builds the message array for a collocation pass over the // WHOLE document (no truncation), gently steered toward the document's tone so a // hint can prefer a register-appropriate pairing. The parenthetical gloss is // written in the writer's own language. func CollocationMessages(contentText, tone string, lang Lang) []Message { return []Message{ {Role: "system", Content: fmt.Sprintf(collocationSystemPrompt, toneGuidance(tone), lang.Name)}, {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. // // The reply is bilingual, the pair language first. Until UX item 6 it mirrored // the language of the question instead — self-consistent, but it meant asking in // one language cost you the other, and the writer doesn't always know which one // the answer will be clearer in. Which half is the safety net and which is the // lesson depends on who is writing: the pair is (English + X) either way, and an // English speaker learning French wants the French half for the same reason a // Mandarin speaker learning English wants the English one. Petal cannot tell // them apart from a chat message, and doesn't need to — every other explanation // surface already gives both (the card's English body, the seeded bubble in the // pair language). The answer that goes deepest into the "why" was the one place // that didn't. // // The blank line between the halves is a contract with the client: AskPetal.tsx // splits on the first one to render her language prominently and the English // beneath it, mirroring the companion's bubble. A model that ignores the // instruction and writes one language degrades to a single plain block — the // answer is still readable, which is why the split is a rendering nicety and // never a parse the reply depends on. 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: "%[1]s" - Suggested replacement: "%[2]s" - Issue type: %[3]s - Initial explanation: "%[4]s" - Surrounding paragraph: "%[5]s" The user wants to understand this suggestion better. Answer in BOTH languages, every time, ` + `whichever language they asked their question in: first the whole answer in %[6]s, then the ` + `same answer again in English. Separate the two with a single blank line. Do not label them, ` + `do not use a blank line anywhere else, and do not mix the two languages within one half — ` + `each half is complete on its own. One of those two languages is the one they are surest in and the other is the one they are ` + `working in — you do not know which way round, so give both and let them choose. Both halves ` + `say the same thing: do not put a point in one that is missing from the other. Explain clearly and kindly. Use simple language appropriate to the user's message. Give examples ` + `when helpful. If they ask "why" (or "%[7]s"), explain the grammar rule or idiom behind it. ` + `If they suggest an alternative phrasing, evaluate it honestly. Keep each half concise (2-3 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 and // the writer's pair language, which is the one she may ask her question in. func AskPetalSystemPrompt(original, replacement, suggestionType, explanation, paragraph string, lang Lang) string { return fmt.Sprintf(askPetalSystemTemplate, original, replacement, suggestionType, explanation, paragraph, lang.Name, lang.Why) } // rewriteSystemTemplate drives the "say it more naturally" / tone-rewrite tool. // The writer selects a passage and picks a style; the model rewrites that // passage in place. The instruction is deliberately strict about returning ONLY // the rewritten passage so the result can be dropped straight into the editor — // no quotes, no preamble, no commentary to strip. const rewriteSystemTemplate = `You are Petal, a warm English writing assistant helping someone who speaks English ` + `as a second language. Rewrite the passage the user sends so that it %s, while preserving its original ` + `meaning. Fix any grammar mistakes and awkward phrasing along the way. Keep it about the same length — ` + `do not add new ideas, explanations, or commentary. Respond with ONLY the rewritten passage. No quotation marks around it, no preamble, no notes — just the ` + `rewritten English text, ready to drop back into the document.` // styleGuidance maps a rewrite style onto the clause describing the target // register. "natural" is the default "say it more naturally" action; the rest // mirror the document-tone vocabulary (see toneGuidance / the ToneSelect UI). // An unknown style falls back to the natural rewrite. func styleGuidance(style string) string { switch style { case "academic": return "reads as formal, academic English suited to a school essay or research paper" case "professional": return "reads as polished, professional English suited to a workplace email or report" case "casual": return "sounds relaxed, friendly, and conversational" case "humorous": return "has a light, playful, good-humored tone" case "creative": return "is vivid, expressive, and imaginative" case "persuasive": return "is confident and persuasive" default: // "natural" return "sounds natural and fluent, the way a native English speaker would naturally say it" } } // RewriteMessages builds the message array for a tone-rewrite: the styled system // instruction plus the passage to rewrite as the user turn. func RewriteMessages(text, style string) []Message { return []Message{ {Role: "system", Content: fmt.Sprintf(rewriteSystemTemplate, styleGuidance(style))}, {Role: "user", Content: text}, } } // translateSystemPrompt drives the explanation translator: it renders a // suggestion's English explanation into the writer's own language so an ESL // reader sees the "why" in her first language. Strict about returning ONLY the // translation (no quotes, no romanisation, no English echo) so it can drop // straight into the chat bubble. Kept warm and plain — these are short, friendly // one-liners. const translateSystemPrompt = `You are Petal, a warm writing assistant. Translate the English text the user ` + `sends into natural, friendly %[1]s. It is a short explanation of a writing ` + `suggestion, written for a native %[1]s speaker learning English. Respond with ONLY the %[1]s translation. No quotation marks, no romanisation, no English, no preamble — ` + `just the translated sentence.` // TranslateMessages builds the message array for translating one short English // explanation into the writer's own language. func TranslateMessages(text string, lang Lang) []Message { return []Message{ {Role: "system", Content: fmt.Sprintf(translateSystemPrompt, lang.Name)}, {Role: "user", Content: text}, } }