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

@@ -5,6 +5,7 @@ import { useCheckpoint } from './hooks/useCheckpoint'
import { DocList } from './components/DocList/DocList'
import { EditorCore, type EditorChange } from './components/Editor/EditorCore'
import { StatusBar } from './components/StatusBar/StatusBar'
import { PetalCompanion } from './components/Companion/PetalCompanion'
export default function App() {
const [docs, setDocs] = useState<DocSummary[]>([])
@@ -16,6 +17,9 @@ export default function App() {
// sidebar. Escape or a click outside the editor canvas restores it.
const [focusMode, setFocusMode] = useState(false)
const canvasRef = useRef<HTMLDivElement>(null)
// Monotonic counters the companion watches to react to writing + accepts.
const [editTick, setEditTick] = useState(0)
const [acceptTick, setAcceptTick] = useState(0)
const { status, schedule, saveNow } = useAutoSave(currentDoc?.id ?? null)
const {
@@ -116,6 +120,7 @@ export default function App() {
const handleEditorChange = useCallback(
(change: EditorChange) => {
setWordCount(change.word_count)
setEditTick((n) => n + 1)
if (currentDoc) {
patchSummary(currentDoc.id, { word_count: change.word_count })
schedule(change)
@@ -130,6 +135,7 @@ export default function App() {
const handleAccept = useCallback(
async (s: Suggestion) => {
removeSuggestion(s.id)
setAcceptTick((n) => n + 1)
try {
await api.acceptSuggestion(s.id)
} catch (err) {
@@ -238,6 +244,13 @@ export default function App() {
)}
</main>
</div>
<PetalCompanion
wordCount={wordCount}
saveStatus={status}
editTick={editTick}
acceptTick={acceptTick}
/>
</div>
)
}