Add companion kitten — reactive corner mascot

A cozy bottom-right mascot that reacts to the writing session: cheers on
accepted suggestions and word-count milestones, drops Mandarin-first writing
tips, suggests screen breaks after long stretches, and naps when idle.

- useCompanion: library-agnostic behavior engine (priority + cooldown paced
  so it never nags); tips.ts holds all copy, bilingual zh-first.
- LottiePlayer wraps lottie-web's light build (offline, no eval/CDN) so it
  bundles into the Go binary. Ships with an emoji-kitten placeholder per mood;
  dropping a Lottie cat JSON into animations/index.ts is the only swap needed.
- Speech bubble uses the CJK-first font stack (spec Note #17).

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
prosolis
2026-06-25 21:43:58 -07:00
parent 534f7262ab
commit 8a32dde587
10 changed files with 465 additions and 8 deletions

View File

@@ -0,0 +1,63 @@
// Bilingual companion copy — Mandarin first (she writes/reads in Mandarin), with
// a warm English subtitle. Kept gentle and encouraging, never scolding. The
// bubble renders zh prominently and en underneath. (Spec north star: CJK is
// first-class; Ask Petal & companion answer in Mandarin.)
export interface Line {
zh: string
en: string
}
// Played when a suggestion is accepted or a milestone hits — pure warmth.
export const ENCOURAGEMENTS: Line[] = [
{ zh: '好棒!这一句更顺了 🌸', en: 'Lovely — that reads so much smoother now.' },
{ zh: '你写得越来越好了 ✨', en: "You're getting better and better." },
{ zh: '我很喜欢这个改法 💕', en: 'I really like that change.' },
{ zh: '继续保持,加油!', en: 'Keep going — youve got this!' },
{ zh: '嗯嗯,这样清楚多了 👍', en: 'Mm, thats much clearer.' },
]
// Gentle, periodic writing/ESL tips. Surfaced only when nothing else is showing.
export const TIPS: Line[] = [
{ zh: '小贴士:英文句子短一点,会更清楚哦。', en: 'Tip: shorter English sentences often read clearer.' },
{ zh: '别忘了冠词 “the” 和 “a” 哦。', en: "Don't forget articles like “the” and “a”." },
{ zh: '过去的事情用过去式go → went。', en: 'For the past, use past tense: go → went.' },
{ zh: '读出声音,能帮你发现奇怪的地方。', en: 'Reading aloud helps you catch awkward spots.' },
{ zh: '一个段落讲一个想法就好。', en: 'One idea per paragraph keeps it tidy.' },
{ zh: '不确定的地方,问问我就好啦 ✨', en: 'Not sure about something? Just ask me. ✨' },
{ zh: '复数别忘了加 stwo apples 🍎', en: 'Plurals take an “s”: two apples 🍎' },
]
// Shown after a long stretch of continuous writing.
export const BREAKS: Line[] = [
{ zh: '写了好一会儿啦,起来走走,让眼睛休息一下 🍵', en: "You've been writing a while — stretch and rest your eyes. 🍵" },
{ zh: '喝口水,休息五分钟好不好?', en: 'Sip some water and take five?' },
{ zh: '看看远方,放松一下眼睛 🌿', en: 'Look into the distance for a moment — give your eyes a break. 🌿' },
]
// First hello when the app opens.
export const GREETING: Line = {
zh: '嗨~我在这儿陪你写作哦 🐱',
en: "Hi! I'm right here keeping you company. 🐱",
}
// Welcome-back nudge after she returns from an idle pause.
export const WELCOME_BACK: Line = {
zh: '欢迎回来 ✨ 我们继续吧!',
en: 'Welcome back ✨ lets keep going!',
}
// Word-count milestones worth a little cheer.
export const MILESTONES = [50, 100, 250, 500, 1000, 2000]
export function milestoneLine(n: number): Line {
return {
zh: `哇!已经 ${n} 个词了,太厉害了 🎉`,
en: `Wow — ${n} words already! Amazing. 🎉`,
}
}
// Deterministic-enough random pick (Math.random is fine in the browser runtime).
export function pick<T>(arr: T[]): T {
return arr[Math.floor(Math.random() * arr.length)]
}