type GreetingPeriod = "morning" | "noon" | "afternoon" | "evening" | "late-night"; /** * One line, two to five words, no follow-up question. Since 2026-10-01 the voice * is a warm older sister's (知心姐姐): it greets and cares, never comments on the * user (no 夜猫子 / 还没睡呀). * * The homepage opening used to be a salutation plus a question ("这么晚还醒着, * {名字}。直接说说最在意的问题吧。") — an office voice that answered for the user * before they had said anything. Asking is the composer placeholder's job now; * the greeting only greets. No metaphors (星 / 月 / 陪), no emoji, no period. */ const NAME_TOKEN = "{名字}"; const periodGreetings: Record = { morning: ["早安", "早安,{名字}", "早呀,今天慢慢来"], noon: ["中午好,记得吃饭", "午安,{名字}"], afternoon: ["下午好,歇口气吧", "下午好,{名字}"], evening: ["今天辛苦了", "晚上好,{名字}", "晚上好,歇一歇"], // Late night never remarks on being awake (产品 2026-10-01: 「你管别人睡不睡」). "late-night": ["这会儿很安静", "夜里好,{名字}", "慢慢说,{名字}"], }; /** Keyed by `Date#getDay()`. Days with nothing worth saying stay out of the pool. */ const weekdayGreetings: Readonly> = { 0: ["周日,慢一点"], 1: ["周一了,别太累"], 5: ["周五了,松口气"], 6: ["周末好好歇歇"], }; /** Only reachable once the account has at least one session behind it. */ const returningGreetings: readonly string[] = [ "又见面了,{名字}", "你来啦,{名字}", "欢迎回来", "最近还好吗,{名字}?", ]; function greetingPeriod(hour: number): GreetingPeriod { if (hour >= 5 && hour < 11) return "morning"; if (hour >= 11 && hour < 14) return "noon"; if (hour >= 14 && hour < 18) return "afternoon"; if (hour >= 18 && hour < 23) return "evening"; return "late-night"; } /** * Every line this moment can produce, in pool order: daypart, weekday, returning. * A blank name drops the lines that would print an empty one; the daypart pool * always keeps a nameless line, so the result is never empty. */ function starterGreetingPool(name: string, now: Date, hasSessions: boolean): readonly string[] { const templates = [ ...periodGreetings[greetingPeriod(now.getHours())], ...(weekdayGreetings[now.getDay()] ?? []), ...(hasSessions ? returningGreetings : []), ]; return name ? templates : templates.filter((template) => !template.includes(NAME_TOKEN)); } export type StartGreetingParts = { readonly text: string; }; export function createStartGreetingParts( name: string, now = new Date(), variantSelection = Math.random(), hasSessions = false, ): StartGreetingParts { const displayName = name.trim(); const pool = starterGreetingPool(displayName, now, hasSessions); const index = Math.min(Math.max(Math.floor(variantSelection * pool.length), 0), pool.length - 1); return { text: pool[index]!.replaceAll(NAME_TOKEN, displayName) }; } export function createStartGreeting( name: string, now = new Date(), variantSelection = Math.random(), hasSessions = false, ): string { return createStartGreetingParts(name, now, variantSelection, hasSessions).text; } export function createOnboardingFallbackGreeting(name: string): string { return `欢迎,${name.trim()}`; }