Phase 14: bedtime nag + night mode (dark theme + falling stars)
Companion warmth: - ENCOURAGEMENTS grown 5->10; new BEDTIME lines (warm/playful, zh-first) - useCompanion heartbeat gains a bedtime branch (>=11pm, while actively writing) gated by a 30min cooldown; new 'bedtime' BubbleTone paces it Night mode (auto at ~11pm, local clock): - lib/night.ts centralizes isBedtime() + the window, shared with the nag - useNightMode toggles a `petal-night` class on <html>; index.css re-points only the palette tokens, so every Tailwind color utility flips via var() with no component edits (600ms dusk fade; print stays white) - PetalFall gains a `night` prop: chunky cartoon power stars (makeCartoonStar, 5 candy colors) mixed ~70/30 with twinkle sparkles; each star spins at its own rate (~0.5-2.2 rad/s, random direction), falls straight down, shimmers Verified: go vet/test, tsc, vitest 51/51, vite build; real-browser screenshots/video (clock mocked to 23:30). Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
@@ -15,6 +15,11 @@ export const ENCOURAGEMENTS: Line[] = [
|
||||
{ zh: '我很喜欢这个改法 💕', en: 'I really like that change.' },
|
||||
{ zh: '继续保持,加油!', en: 'Keep going — you’ve got this!' },
|
||||
{ zh: '嗯嗯,这样清楚多了 👍', en: 'Mm, that’s much clearer.' },
|
||||
{ zh: '这个词用得真好 🌷', en: 'That’s such a good word choice.' },
|
||||
{ zh: '哇,这一段读起来真舒服 ☁️', en: 'Ooh, that paragraph flows so nicely.' },
|
||||
{ zh: '看你越写越有信心,真好 💛', en: 'I love watching you write with more confidence.' },
|
||||
{ zh: '一点点进步,都是了不起的进步 🌱', en: 'Every little bit of progress counts.' },
|
||||
{ zh: '今天的你,文字闪闪发光 ✨', en: 'Your words are sparkling today.' },
|
||||
]
|
||||
|
||||
// Gentle, generic writing/ESL tips — the fallback when the rules-based prose
|
||||
@@ -36,6 +41,16 @@ export const BREAKS: Line[] = [
|
||||
{ zh: '看看远方,放松一下眼睛 🌿', en: 'Look into the distance for a moment — give your eyes a break. 🌿' },
|
||||
]
|
||||
|
||||
// Shown when she's still writing late at night (≥11pm). Caring, a little
|
||||
// playful — the kitten is always asleep, so "you should be too" lands as a gag,
|
||||
// never a scold. zh stays gentle; the English subtitle carries the wink.
|
||||
export const BEDTIME: Line[] = [
|
||||
{ zh: '你的床在想你了哦 🛏️', en: 'I bet your bed is missing you right now.' },
|
||||
{ zh: '太累可写不出好文字呀,早点歇着吧 🌙', en: 'A tired writer is a bad writer — get some rest.' },
|
||||
{ zh: '好好睡一觉,灵感自己会来 ✨', en: 'Sleep is a wondrous enabler.' },
|
||||
{ zh: '听见了吗?没有吧——大家都睡了,你也该睡啦 😴', en: "Hear that? No… you don't, because everyone is sleeping and you should be too." },
|
||||
]
|
||||
|
||||
// First hello when the app opens.
|
||||
export const GREETING: Line = {
|
||||
zh: '嗨~我在这儿陪你写作哦 🐱',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import type { SaveStatus } from '../../hooks/useAutoSave'
|
||||
import {
|
||||
BEDTIME,
|
||||
BREAKS,
|
||||
ENCOURAGEMENTS,
|
||||
ERRORS,
|
||||
@@ -14,12 +15,13 @@ import {
|
||||
} from './tips'
|
||||
import { analyzeProse } from './prose'
|
||||
import { playPop, playSound, type SoundName } from '../../audio/sounds'
|
||||
import { isBedtime } from '../../lib/night'
|
||||
|
||||
// The kitten's expression. Maps to a Lottie animation when assets are present,
|
||||
// otherwise to an emoji placeholder (see PetalCompanion).
|
||||
export type Mood = 'idle' | 'happy' | 'talking' | 'sleeping' | 'celebrate'
|
||||
|
||||
export type BubbleTone = 'cheer' | 'tip' | 'break' | 'error'
|
||||
export type BubbleTone = 'cheer' | 'tip' | 'break' | 'error' | 'bedtime'
|
||||
export interface Bubble extends Line {
|
||||
tone: BubbleTone
|
||||
}
|
||||
@@ -44,6 +46,9 @@ const IDLE_MS = 75_000 // no edits → kitten naps
|
||||
const BREAK_MS = 25 * 60_000 // continuous writing → suggest a break
|
||||
const TIP_MIN_GAP = 4 * 60_000 // at most one spontaneous tip per this window
|
||||
const PROACTIVE_GAP = 40_000 // floor between any two unsolicited bubbles
|
||||
const BEDTIME_GAP = 30 * 60_000 // at most one "go to bed" nudge per this window
|
||||
// The late-night window itself (isBedtime) lives in ../../lib/night so the
|
||||
// companion nag and the night-mode theme/starfall share one definition.
|
||||
// How long a bubble lingers. These are *floors* — readBubbleMs extends them by
|
||||
// how much there is to read, since she reads both the Mandarin and the English
|
||||
// (and tips now quote a slice of her own sentence, so they run longer).
|
||||
@@ -58,7 +63,7 @@ const now = () => Date.now()
|
||||
// length of the Mandarin + English lines so denser advice stays up long enough
|
||||
// to actually finish reading.
|
||||
function readBubbleMs(b: Bubble): number {
|
||||
const base = b.tone === 'cheer' ? CHEER_MS : BUBBLE_MS
|
||||
const base = b.tone === 'cheer' ? CHEER_MS : b.tone === 'bedtime' ? BUBBLE_MS + 4_000 : BUBBLE_MS
|
||||
const chars = b.zh.length + b.en.length
|
||||
return Math.min(MAX_BUBBLE_MS, base + chars * READ_MS_PER_CHAR)
|
||||
}
|
||||
@@ -75,6 +80,7 @@ export function useCompanion({ wordCount, saveStatus, llmDown, editTick, acceptT
|
||||
const lastProactive = useRef(0)
|
||||
const lastTip = useRef(0)
|
||||
const lastBreak = useRef(0)
|
||||
const lastBedtime = useRef(0)
|
||||
const nextMilestone = useRef(0) // index into MILESTONES
|
||||
const sleeping = useRef(false)
|
||||
|
||||
@@ -262,6 +268,15 @@ export function useCompanion({ wordCount, saveStatus, llmDown, editTick, acceptT
|
||||
return
|
||||
}
|
||||
|
||||
// Burning the midnight oil? Gently suggest bed — caring, low-frequency,
|
||||
// and only while she's actually still at it (the idle branch above already
|
||||
// returned if she's away/napping).
|
||||
if (isBedtime() && t - lastBedtime.current > BEDTIME_GAP) {
|
||||
lastBedtime.current = t
|
||||
say({ ...pick(BEDTIME), tone: 'bedtime' }, { proactive: true })
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise an occasional tip — context-aware when her text gives us
|
||||
// something concrete to gently point at, generic warmth otherwise.
|
||||
if (t - lastTip.current > TIP_MIN_GAP) {
|
||||
|
||||
Reference in New Issue
Block a user