fix: bind onboarding greeting to profile
This commit is contained in:
@@ -46,6 +46,7 @@ import { chatMessageViews, type ChatMessage } from "@/lib/chat-message-view";
|
||||
import {
|
||||
OnboardingAuthenticationError,
|
||||
type OnboardingContent,
|
||||
createStartGreeting,
|
||||
isCurrentOnboardingRequest,
|
||||
onboardingProfileFingerprint,
|
||||
onboardingRequestIdentity,
|
||||
@@ -118,7 +119,6 @@ type StreamingReply = { sessionId: string; text: string };
|
||||
type BirthPlace = { label: string; lat: number; lon: number; tz: number };
|
||||
type Account = { user: { id: string; email: string | null }; credits: number; isAdmin: boolean };
|
||||
type OnboardingStep = "name" | "birth" | "place" | "rectification";
|
||||
type GreetingPeriod = "morning" | "noon" | "afternoon" | "evening" | "late-night";
|
||||
type AccountDialog = "profile" | "redeem" | "logout";
|
||||
type DailyStarlanguageCard = { trend: string; action: string; caution: string };
|
||||
type DailyStarlanguageApiResponse = {
|
||||
@@ -188,48 +188,6 @@ const dailyStarlanguageCards: DailyStarlanguageCard[] = [
|
||||
{ trend: "适合观察资源流向:时间、注意力、金钱都算。", action: "检查一个正在消耗你的习惯,并给它设上限。", caution: "不要为了短期安心做长期成本高的选择。" },
|
||||
];
|
||||
|
||||
const greetingVariants: Record<GreetingPeriod, Array<(name: string) => string>> = {
|
||||
morning: [
|
||||
(name) => `早上好,${name}。今天最想先看什么?`,
|
||||
(name) => `${name},早安。今天最该关注哪件事?`,
|
||||
(name) => `早上好,${name}。想从哪个问题开始?`,
|
||||
],
|
||||
noon: [
|
||||
(name) => `中午好,${name}。现在最想理清哪件事?`,
|
||||
(name) => `${name},中午好。什么问题最需要方向?`,
|
||||
(name) => `午间好,${name}。事业、关系或选择,想先聊哪个?`,
|
||||
],
|
||||
afternoon: [
|
||||
(name) => `${name},下午好。现在最想推进哪件事?`,
|
||||
(name) => `下午好,${name}。今天想先理清什么?`,
|
||||
(name) => `${name},下午好。事业、关系或选择,想先聊哪个?`,
|
||||
],
|
||||
evening: [
|
||||
(name) => `晚上好,${name}。今天最挂心的是哪件事?`,
|
||||
(name) => `${name},晚上好。此刻最想聊哪件事?`,
|
||||
(name) => `晚上好,${name}。把心里的问题告诉我吧。`,
|
||||
],
|
||||
"late-night": [
|
||||
(name) => `夜深了,${name}。此刻最想问什么?`,
|
||||
(name) => `${name},还没休息吗?想从哪件事说起?`,
|
||||
(name) => `这么晚还醒着,${name}。直接说说最在意的问题吧。`,
|
||||
],
|
||||
};
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
function createStartGreeting(name: string, now = new Date()) {
|
||||
const displayName = name.trim() || "你好";
|
||||
const variants = greetingVariants[greetingPeriod(now.getHours())];
|
||||
return variants[Math.floor(Math.random() * variants.length)](displayName);
|
||||
}
|
||||
|
||||
const emptyProfile: Profile = {
|
||||
name: "",
|
||||
date: "",
|
||||
@@ -1119,7 +1077,7 @@ export default function Home() {
|
||||
const requestIdentity = onboardingRequestIdentity(accountId, onboardingFingerprint);
|
||||
if (isCurrentOnboardingRequest(activeOnboardingRequestIdentity.current, requestIdentity)) return;
|
||||
activeOnboardingRequestIdentity.current = requestIdentity;
|
||||
const presentation = { name: profile.name, startGreeting };
|
||||
const presentationName = profile.name;
|
||||
const controller = new AbortController();
|
||||
setOnboarding(null);
|
||||
setOnboardingError("");
|
||||
@@ -1134,7 +1092,7 @@ export default function Home() {
|
||||
|| !isCurrentOnboardingRequest(activeOnboardingRequestIdentity.current, requestIdentity)) return;
|
||||
setOnboarding({
|
||||
...content,
|
||||
greeting: presentation.startGreeting || createStartGreeting(presentation.name),
|
||||
greeting: createStartGreeting(presentationName),
|
||||
});
|
||||
setOnboardingError("");
|
||||
})
|
||||
@@ -1153,7 +1111,7 @@ export default function Home() {
|
||||
}
|
||||
controller.abort();
|
||||
};
|
||||
}, [accountId, hydrated, onboardingFingerprint, profile.name, profileComplete, startGreeting]);
|
||||
}, [accountId, hydrated, onboardingFingerprint, profile.name, profileComplete]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydrated || !profileComplete || birthTimeDisplayState(profile)) return;
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
import { z } from "zod";
|
||||
|
||||
type GreetingPeriod = "morning" | "noon" | "afternoon" | "evening" | "late-night";
|
||||
|
||||
const greetingVariants: Record<GreetingPeriod, readonly ((name: string) => string)[]> = {
|
||||
morning: [
|
||||
(name) => `早上好,${name}。今天最想先看什么?`,
|
||||
(name) => `${name},早安。今天最该关注哪件事?`,
|
||||
(name) => `早上好,${name}。想从哪个问题开始?`,
|
||||
],
|
||||
noon: [
|
||||
(name) => `中午好,${name}。现在最想理清哪件事?`,
|
||||
(name) => `${name},中午好。什么问题最需要方向?`,
|
||||
(name) => `午间好,${name}。事业、关系或选择,想先聊哪个?`,
|
||||
],
|
||||
afternoon: [
|
||||
(name) => `${name},下午好。现在最想推进哪件事?`,
|
||||
(name) => `下午好,${name}。今天想先理清什么?`,
|
||||
(name) => `${name},下午好。事业、关系或选择,想先聊哪个?`,
|
||||
],
|
||||
evening: [
|
||||
(name) => `晚上好,${name}。今天最挂心的是哪件事?`,
|
||||
(name) => `${name},晚上好。此刻最想聊哪件事?`,
|
||||
(name) => `晚上好,${name}。把心里的问题告诉我吧。`,
|
||||
],
|
||||
"late-night": [
|
||||
(name) => `夜深了,${name}。此刻最想问什么?`,
|
||||
(name) => `${name},还没休息吗?想从哪件事说起?`,
|
||||
(name) => `这么晚还醒着,${name}。直接说说最在意的问题吧。`,
|
||||
],
|
||||
};
|
||||
|
||||
const onboardingResponseSchema = z.object({
|
||||
greeting: z.string().transform((value) => value.replace(/\s+/g, " ").trim().slice(0, 180)).pipe(z.string().min(8)),
|
||||
suggestions: z.tuple([
|
||||
@@ -56,6 +86,24 @@ export type OnboardingContent = {
|
||||
readonly suggestions: readonly OnboardingSuggestion[];
|
||||
};
|
||||
|
||||
export function createStartGreeting(
|
||||
name: string,
|
||||
now = new Date(),
|
||||
variantSelection = Math.random(),
|
||||
): string {
|
||||
const displayName = name.trim() || "你好";
|
||||
const hour = now.getHours();
|
||||
const period: GreetingPeriod = hour >= 5 && hour < 11
|
||||
? "morning"
|
||||
: hour >= 11 && hour < 14
|
||||
? "noon"
|
||||
: hour >= 14 && hour < 18
|
||||
? "afternoon"
|
||||
: hour >= 18 && hour < 23 ? "evening" : "late-night";
|
||||
const variants = greetingVariants[period];
|
||||
return variants[Math.floor(variantSelection * variants.length)](displayName);
|
||||
}
|
||||
|
||||
export function onboardingProfileFingerprint(profile: OnboardingProfileFingerprintInput): string {
|
||||
return JSON.stringify([
|
||||
profile.name,
|
||||
|
||||
@@ -3,6 +3,7 @@ import test from "node:test";
|
||||
import {
|
||||
OnboardingAuthenticationError,
|
||||
OnboardingRequestError,
|
||||
createStartGreeting,
|
||||
isCurrentOnboardingRequest,
|
||||
onboardingProfileFingerprint,
|
||||
onboardingRequestIdentity,
|
||||
@@ -32,11 +33,13 @@ test("rejects a stale completion after a complete profile changes", () => {
|
||||
// When: a presentation-affecting profile field changes before that request completes.
|
||||
const changedFingerprint = onboardingProfileFingerprint({ ...completeProfile, name: "周宁" });
|
||||
const currentIdentity = onboardingRequestIdentity("account-1", changedFingerprint);
|
||||
const committedGreeting = createStartGreeting("周宁", new Date("2026-07-19T08:00:00+08:00"), 0);
|
||||
|
||||
// Then: the new profile has a distinct request identity and the old completion is stale.
|
||||
// Then: the new profile has a distinct identity and its greeting cannot retain the old name.
|
||||
assert.notEqual(currentIdentity, firstIdentity);
|
||||
assert.equal(isCurrentOnboardingRequest(currentIdentity, firstIdentity), false);
|
||||
assert.equal(isCurrentOnboardingRequest(currentIdentity, currentIdentity), true);
|
||||
assert.match(committedGreeting, /周宁/);
|
||||
assert.doesNotMatch(committedGreeting, /林遥/);
|
||||
});
|
||||
|
||||
test("returns personalized cache content after a timeout and pending response", async () => {
|
||||
@@ -285,10 +288,8 @@ test("parent cancellation interrupts a pending-response retry delay", async () =
|
||||
const originalFetch = globalThis.fetch;
|
||||
const parent = new AbortController();
|
||||
let requestCount = 0;
|
||||
let markFetched: (() => void) | null = null;
|
||||
const fetched = new Promise<void>((resolve) => { markFetched = resolve; });
|
||||
globalThis.fetch = () => {
|
||||
requestCount += 1; markFetched?.();
|
||||
requestCount += 1;
|
||||
return Promise.resolve(Response.json({ ...personalizedOnboarding, source: "pending" }));
|
||||
};
|
||||
|
||||
@@ -297,7 +298,6 @@ test("parent cancellation interrupts a pending-response retry delay", async () =
|
||||
const request = requestOnboardingWithRecovery(parent.signal, () => undefined, {
|
||||
requestTimeoutMs: 10_000, retryDelayMs: 10_000, maxAttempts: 3,
|
||||
});
|
||||
await fetched;
|
||||
await new Promise<void>((resolve) => setImmediate(resolve));
|
||||
parent.abort(new DOMException("page unmounted", "AbortError"));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user