开场语从「称呼行 + 追问句」收成 claude.ai 式的单行问候:时段池、 星期池、回访池按顺序拼成一个数组,按 variantSelection 确定性取一条; 名字为空时跳过带称呼的条目。副标题行与 .starter-salutation 下线, h1 降到 clamp(24px, 2.8vw, 30px) / line-height 1.25。追问改由输入框 占位符承担。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
87 lines
3.1 KiB
TypeScript
87 lines
3.1 KiB
TypeScript
type GreetingPeriod = "morning" | "noon" | "afternoon" | "evening" | "late-night";
|
||
|
||
/**
|
||
* One line, two to five words, no follow-up question.
|
||
*
|
||
* 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<GreetingPeriod, readonly string[]> = {
|
||
morning: ["早上好", "早上好,{名字}", "早呀,{名字}"],
|
||
noon: ["中午好", "中午好,{名字}"],
|
||
afternoon: ["下午好", "下午好,{名字}"],
|
||
evening: ["晚上好", "晚上好,{名字}", "今天过得怎么样?"],
|
||
"late-night": ["夜猫子,晚上好", "还没睡呀,{名字}", "晚安前再聊一会"],
|
||
};
|
||
|
||
/** Keyed by `Date#getDay()`. Days with nothing worth saying stay out of the pool. */
|
||
const weekdayGreetings: Readonly<Record<number, readonly string[]>> = {
|
||
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()}`;
|
||
}
|