feat(home): write the daily card with the Agent and vary the starter heading
The daily starlanguage card claimed to be personal but was a four-card rotation picked by hashing the date and birth place, with the same pool duplicated as a client fallback. It now collects chart, Vimshottari and Narayana dasha, D9/D10 and today's transits, hands that evidence to a dedicated Agent, and keeps the result per account per day in process. A failed generation says so instead of printing generic advice. The starter heading is drawn from a pool on each visit, and the rectification card drops its fine print. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,22 +1,79 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
dailyStarlanguageCacheKey,
|
||||
dailyStarlanguageEvidence,
|
||||
dailyStarlanguagePrompt,
|
||||
parseDailyStarlanguageText,
|
||||
type DailyStarlanguageCard,
|
||||
} from "@/lib/daily-starlanguage";
|
||||
import {
|
||||
dailyProfilePayload,
|
||||
type GlobalBirthProfile as Profile,
|
||||
type GlobalBirthProfile,
|
||||
} from "@/lib/global-birth-payloads";
|
||||
import { loadLanguageModelCatalog } from "@/lib/model-catalog";
|
||||
import { createServerSupabaseClient } from "@/lib/supabase/server";
|
||||
import { getDailyStarlanguageAgent } from "@/mastra";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const maxDuration = 60;
|
||||
|
||||
type Profile = GlobalBirthProfile & { birthTimeStatus?: string };
|
||||
type BirthPayload = NonNullable<Awaited<ReturnType<typeof dailyProfilePayload>>>;
|
||||
type CacheEntry = { readonly day: string; readonly card: DailyStarlanguageCard };
|
||||
|
||||
const jyotishApiBase = process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
|
||||
const engineTimeoutMs = 8_000;
|
||||
const agentTimeoutMs = 30_000;
|
||||
const cacheLimit = 500;
|
||||
|
||||
const cards = [
|
||||
{ trend: "先收束,再推进。适合把一个悬而未决的问题拆小。", action: "选一件最重要的事,给它留出 45 分钟不被打断的时间。", caution: "避免在情绪最满时做承诺。" },
|
||||
{ trend: "适合整理关系与边界。越清楚,越不容易被外界节奏带走。", action: "把今天要回复的人和要推迟的事分开列出来。", caution: "不要把暂时的沉默误读成最终答案。" },
|
||||
{ trend: "执行力比灵感更重要。小步完成会比大计划更有力量。", action: "先完成一个可交付版本,再考虑优化。", caution: "别让完美感拖慢开始。" },
|
||||
{ trend: "适合观察资源流向:时间、注意力、金钱都算。", action: "检查一个正在消耗你的习惯,并给它设上限。", caution: "不要为了短期安心做长期成本高的选择。" },
|
||||
];
|
||||
const state = globalThis as typeof globalThis & {
|
||||
jyotishaDailyStarlanguageCache?: Map<string, CacheEntry>;
|
||||
jyotishaDailyStarlanguagePending?: Map<string, Promise<DailyStarlanguageCard | null>>;
|
||||
};
|
||||
|
||||
function pickCard(profile: Profile, today: string) {
|
||||
const seed = `${today}-${profile.date ?? ""}-${profile.time ?? ""}-${profile.provinceCode ?? ""}-${profile.cityCode ?? ""}`;
|
||||
const index = Array.from(seed).reduce((sum, char) => sum + char.charCodeAt(0), 0) % cards.length;
|
||||
return cards[index];
|
||||
function cache() {
|
||||
state.jyotishaDailyStarlanguageCache ??= new Map();
|
||||
return state.jyotishaDailyStarlanguageCache;
|
||||
}
|
||||
|
||||
function pending() {
|
||||
state.jyotishaDailyStarlanguagePending ??= new Map();
|
||||
return state.jyotishaDailyStarlanguagePending;
|
||||
}
|
||||
|
||||
function readCache(key: string, today: string): DailyStarlanguageCard | null {
|
||||
const entry = cache().get(key);
|
||||
if (!entry) return null;
|
||||
if (entry.day !== today) {
|
||||
cache().delete(key);
|
||||
return null;
|
||||
}
|
||||
return entry.card;
|
||||
}
|
||||
|
||||
function writeCache(key: string, today: string, card: DailyStarlanguageCard) {
|
||||
const store = cache();
|
||||
for (const [storedKey, entry] of store) {
|
||||
if (entry.day !== today) store.delete(storedKey);
|
||||
}
|
||||
while (store.size >= cacheLimit) {
|
||||
const oldest = store.keys().next();
|
||||
if (oldest.done) break;
|
||||
store.delete(oldest.value);
|
||||
}
|
||||
store.set(key, { day: today, card });
|
||||
}
|
||||
|
||||
async function fetchEngine(path: string, body: Record<string, unknown>) {
|
||||
const response = await fetch(`${jyotishApiBase}${path}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(engineTimeoutMs),
|
||||
});
|
||||
if (!response.ok) throw new Error(`jyotish_api_${response.status}`);
|
||||
return await response.json() as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function chartPoints(chart: Record<string, unknown>) {
|
||||
@@ -27,57 +84,92 @@ function chartPoints(chart: Record<string, unknown>) {
|
||||
return planets && ascendant ? { planets, ascendant } : null;
|
||||
}
|
||||
|
||||
async function fetchJson(path: string, body: Record<string, unknown>) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 2500);
|
||||
try {
|
||||
const response = await fetch(`${jyotishApiBase}${path}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
cache: "no-store",
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) throw new Error(`jyotish_api_${response.status}`);
|
||||
return await response.json() as Record<string, unknown>;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function transitBackedCard(profile: Profile, today: string) {
|
||||
const payload = await dailyProfilePayload(profile, today);
|
||||
if (!payload) return null;
|
||||
const chart = await fetchJson("/api/chart", payload);
|
||||
async function collectLayers(payload: BirthPayload, today: string) {
|
||||
const chart = await fetchEngine("/api/chart", payload);
|
||||
const points = chartPoints(chart);
|
||||
if (!points) return null;
|
||||
const tomorrow = new Date(`${today}T00:00:00.000Z`);
|
||||
tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);
|
||||
const transit = await fetchJson("/api/transit", {
|
||||
natal_planets: points.planets,
|
||||
ascendant: points.ascendant,
|
||||
start: today,
|
||||
end: tomorrow.toISOString().slice(0, 10),
|
||||
planets_to_check: ["Saturn", "Jupiter", "Rahu", "Ketu"],
|
||||
});
|
||||
const summary = transit.summary && typeof transit.summary === "object" ? transit.summary as Record<string, unknown> : {};
|
||||
const total = Number(summary.total_triggers ?? 0);
|
||||
return {
|
||||
trend: total > 0 ? `今日有 ${total} 个可观察过境触发点,适合把它当作时间窗口观察。` : "今日未发现强精确过境触发,适合按本命节奏稳步推进。",
|
||||
action: "把今日计划压缩到一件主事,并记录实际发生的触发点。",
|
||||
caution: "过境触发不能单独定事件,需与 Dasha、分盘和本命承诺交叉确认。",
|
||||
};
|
||||
const withPoints = { ...payload, planets: points.planets, ascendant: points.ascendant };
|
||||
const [vimshottari, narayana, varga, transit] = await Promise.all([
|
||||
fetchEngine("/api/dasha", { ...withPoints, dasha: "vimshottari", today }).catch(() => null),
|
||||
fetchEngine("/api/dasha", { ...withPoints, dasha: "narayana", today }).catch(() => null),
|
||||
fetchEngine("/api/varga_full", { ...withPoints, divisions: ["D9", "D10"] }).catch(() => null),
|
||||
fetchEngine("/api/transit", {
|
||||
natal_planets: points.planets,
|
||||
ascendant: points.ascendant,
|
||||
start: today,
|
||||
end: tomorrow.toISOString().slice(0, 10),
|
||||
planets_to_check: ["Saturn", "Jupiter", "Rahu", "Ketu", "Mars", "Sun"],
|
||||
}).catch(() => null),
|
||||
]);
|
||||
return { chart, vimshottari, narayana, varga, transit };
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json().catch(() => null) as { profile?: Profile; today?: string } | null;
|
||||
const profile = body?.profile ?? {};
|
||||
const today = body?.today || new Date().toISOString().slice(0, 10);
|
||||
const transitCard = await transitBackedCard(profile, today).catch(() => null);
|
||||
async function generateCard(payload: BirthPayload, profile: Profile, today: string) {
|
||||
const layers = await collectLayers(payload, today);
|
||||
if (!layers) return null;
|
||||
const modelId = process.env.DAILY_STARLANGUAGE_MODEL_ID?.trim() || "deepseek-v4-flash";
|
||||
const catalog = await loadLanguageModelCatalog();
|
||||
const model = catalog.models.find((entry) => entry.id === modelId)
|
||||
?? catalog.models.find((entry) => entry.id === catalog.defaultModelId)
|
||||
?? null;
|
||||
if (!model) return null;
|
||||
const evidence = dailyStarlanguageEvidence(layers, today, profile.birthTimeStatus === "confirmed");
|
||||
const result = await getDailyStarlanguageAgent(model).generate(
|
||||
[{ role: "user", content: dailyStarlanguagePrompt(evidence) }],
|
||||
{ abortSignal: AbortSignal.timeout(agentTimeoutMs) },
|
||||
);
|
||||
return parseDailyStarlanguageText(result.text);
|
||||
}
|
||||
|
||||
function unavailable(reason: string) {
|
||||
return NextResponse.json({
|
||||
status: "ok",
|
||||
card: transitCard ?? pickCard(profile, today),
|
||||
source: transitCard ? "jyotish_api_transit_lite" : "calculation_lite",
|
||||
status: "unavailable",
|
||||
reason,
|
||||
claim_status: "exploratory_unvalidated",
|
||||
boundary: "not_deterministic_prediction",
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const supabase = await createServerSupabaseClient();
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return NextResponse.json({ status: "unauthenticated" }, { status: 401 });
|
||||
|
||||
const body = await request.json().catch(() => null) as { profile?: Profile; today?: string } | null;
|
||||
const profile = body?.profile ?? {};
|
||||
const today = body?.today || new Date().toISOString().slice(0, 10);
|
||||
const payload = await dailyProfilePayload(profile, today).catch(() => null);
|
||||
if (!payload) return unavailable("birth_profile_incomplete");
|
||||
|
||||
const key = dailyStarlanguageCacheKey(user.id, JSON.stringify(payload), today);
|
||||
const cached = readCache(key, today);
|
||||
if (cached) {
|
||||
return NextResponse.json({
|
||||
status: "ok",
|
||||
card: cached,
|
||||
source: "agent_cache",
|
||||
claim_status: "exploratory_unvalidated",
|
||||
boundary: "not_deterministic_prediction",
|
||||
});
|
||||
}
|
||||
|
||||
const inFlight = pending().get(key) ?? generateCard(payload, profile, today)
|
||||
.catch((error: unknown) => {
|
||||
console.warn("daily_starlanguage_generation_failed", error);
|
||||
return null;
|
||||
})
|
||||
.finally(() => pending().delete(key));
|
||||
pending().set(key, inFlight);
|
||||
|
||||
const card = await inFlight;
|
||||
if (!card) return unavailable("agent_generation_failed");
|
||||
writeCache(key, today, card);
|
||||
return NextResponse.json({
|
||||
status: "ok",
|
||||
card,
|
||||
source: "agent",
|
||||
claim_status: "exploratory_unvalidated",
|
||||
boundary: "not_deterministic_prediction",
|
||||
});
|
||||
|
||||
@@ -1211,6 +1211,7 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
gap: var(--space-1);
|
||||
color: var(--color-action);
|
||||
font-size: var(--type-caption);
|
||||
|
||||
+41
-35
@@ -115,6 +115,7 @@ import {
|
||||
resolveSessionModelId,
|
||||
type PublicLanguageModelCatalog,
|
||||
} from "@/lib/public-models";
|
||||
import { createStarterPrompt } from "@/lib/starter-prompt";
|
||||
import { selfHostedOtpActions } from "@/modules/identity/client";
|
||||
|
||||
const BirthTimeRectification = dynamic(
|
||||
@@ -227,12 +228,16 @@ type OnboardingStep = "name" | "birth" | "place" | "rectification";
|
||||
type AccountDialog = "profile" | "logout";
|
||||
type DailyStarlanguageCard = { trend: string; action: string; caution: string };
|
||||
type DailyStarlanguageApiResponse = {
|
||||
status?: "ok";
|
||||
status?: "ok" | "unavailable" | "unauthenticated";
|
||||
card?: DailyStarlanguageCard;
|
||||
source?: "calculation_lite";
|
||||
source?: "agent" | "agent_cache";
|
||||
claim_status?: "exploratory_unvalidated";
|
||||
boundary?: "not_deterministic_prediction";
|
||||
};
|
||||
type DailyStarlanguageState =
|
||||
| { kind: "pending" }
|
||||
| { kind: "ready"; card: DailyStarlanguageCard }
|
||||
| { kind: "unavailable" };
|
||||
type SessionReadResult = { readonly sessions: ChatSession[]; readonly fallbackSessionIds: string[] };
|
||||
type ConsultationStatus = {
|
||||
readonly requestId: string;
|
||||
@@ -284,13 +289,6 @@ const previewModelCatalog = parsePublicModelCatalog({
|
||||
|
||||
const presetOnboardingMessage = "你好,我是 Jyotisha。\n开始前,我想先认识你。\n请问我该怎么称呼你?";
|
||||
|
||||
const dailyStarlanguageCards: DailyStarlanguageCard[] = [
|
||||
{ trend: "先收束,再推进。适合把一个悬而未决的问题拆小。", action: "选一件最重要的事,给它留出 45 分钟不被打断的时间。", caution: "避免在情绪最满时做承诺。" },
|
||||
{ trend: "适合整理关系与边界。越清楚,越不容易被外界节奏带走。", action: "把今天要回复的人和要推迟的事分开列出来。", caution: "不要把暂时的沉默误读成最终答案。" },
|
||||
{ trend: "执行力比灵感更重要。小步完成会比大计划更有力量。", action: "先完成一个可交付版本,再考虑优化。", caution: "别让完美感拖慢开始。" },
|
||||
{ trend: "适合观察资源流向:时间、注意力、金钱都算。", action: "检查一个正在消耗你的习惯,并给它设上限。", caution: "不要为了短期安心做长期成本高的选择。" },
|
||||
];
|
||||
|
||||
function greetingForHour(hour: number): string {
|
||||
if (hour < 11) return "早上好";
|
||||
if (hour < 18) return "下午好";
|
||||
@@ -512,23 +510,17 @@ function buildSynastryQuestion(selfProfile: Profile, partnerProfile: Profile, re
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function buildDailyStarlanguageCard(profile: Profile) {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const seed = `${today}-${profile.date}-${profile.time}-${profile.provinceCode}-${profile.cityCode}`;
|
||||
const index = Array.from(seed).reduce((sum, char) => sum + char.charCodeAt(0), 0) % dailyStarlanguageCards.length;
|
||||
return dailyStarlanguageCards[index];
|
||||
}
|
||||
|
||||
async function fetchDailyStarlanguage(profile: Profile) {
|
||||
async function fetchDailyStarlanguage(profile: Profile, signal: AbortSignal): Promise<DailyStarlanguageState> {
|
||||
const response = await fetch("/api/daily-starlanguage", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ profile }),
|
||||
signal,
|
||||
});
|
||||
if (!response.ok) throw new Error("daily_starlanguage_unavailable");
|
||||
if (!response.ok) return { kind: "unavailable" };
|
||||
const payload = await response.json().catch(() => null) as DailyStarlanguageApiResponse | null;
|
||||
if (payload?.status !== "ok" || !payload.card) throw new Error("daily_starlanguage_invalid");
|
||||
return payload.card;
|
||||
if (payload?.status !== "ok" || !payload.card) return { kind: "unavailable" };
|
||||
return { kind: "ready", card: payload.card };
|
||||
}
|
||||
|
||||
function missingProfileStep(profile: Profile): OnboardingStep | null {
|
||||
@@ -994,7 +986,7 @@ export default function Home() {
|
||||
const [otherProfileDraft, setOtherProfileDraft] = useState<Profile>(emptyProfile);
|
||||
const [synastryReportCard, setSynastryReportCard] = useState<SynastryReportCard | null>(null);
|
||||
const [synastryHistory, setSynastryHistory] = useState<SynastryReportCard[]>([]);
|
||||
const [dailyStarlanguageCard, setDailyStarlanguageCard] = useState<DailyStarlanguageCard | null>(null);
|
||||
const [dailyStarlanguage, setDailyStarlanguage] = useState<DailyStarlanguageState>({ kind: "pending" });
|
||||
const [profileNotice, setProfileNotice] = useState("");
|
||||
const [avatarNotice, setAvatarNotice] = useState("");
|
||||
const [avatarSaving, setAvatarSaving] = useState(false);
|
||||
@@ -1043,6 +1035,7 @@ export default function Home() {
|
||||
const [birthTimeError, setBirthTimeError] = useState("");
|
||||
const [birthTimeAssessmentPhase, setBirthTimeAssessmentPhase] = useState<BirthTimeAssessmentPhase | null>(null);
|
||||
const [startGreeting, setStartGreeting] = useState("");
|
||||
const [starterPrompt, setStarterPrompt] = useState(() => createStarterPrompt());
|
||||
const [presetMessageLength, setPresetMessageLength] = useState(0);
|
||||
const conversation = useRef<HTMLDivElement>(null);
|
||||
const accountTrigger = useRef<HTMLButtonElement>(null);
|
||||
@@ -1050,6 +1043,7 @@ export default function Home() {
|
||||
const dialogReturnTarget = useRef<HTMLButtonElement | null>(null);
|
||||
const closeButton = useRef<HTMLButtonElement>(null);
|
||||
const onboardingPaywallShown = useRef(false);
|
||||
const starterPromptUnseen = useRef(true);
|
||||
const composerInput = useRef<HTMLTextAreaElement>(null);
|
||||
const pendingConsultation = useRef<PendingConsultation | null>(null);
|
||||
const cancellationRequests = useRef(new Map<string, Promise<void>>());
|
||||
@@ -1230,7 +1224,12 @@ export default function Home() {
|
||||
const personalChartAvailable = birthTimeRoute.kind === "consult" && birthTimeRoute.mode !== "general_no_birth_time";
|
||||
|
||||
const starterThemes = personalChartAvailable ? themes : generalGuidedJyotishTopics;
|
||||
const dailyStarlanguage = dailyStarlanguageCard ?? (profileComplete ? buildDailyStarlanguageCard(profile) : null);
|
||||
const dailyStarlanguageTrend = dailyStarlanguage.kind === "ready"
|
||||
? dailyStarlanguage.card.trend
|
||||
: dailyStarlanguage.kind === "pending"
|
||||
? "正在结合你的星盘写今天的星语。"
|
||||
: "今天的星语没能生成,稍后再回来看看;这里不会用通用文案顶替。";
|
||||
const dailyStarlanguageAction = dailyStarlanguage.kind === "ready" ? dailyStarlanguage.card.action : "";
|
||||
const onboardingPending = profileComplete && !onboarding && !onboardingError;
|
||||
const currentOnboardingMessage = onboardingJustCompleted
|
||||
? startGreeting || completedOnboardingMessage(profileDraft.name.trim())
|
||||
@@ -1724,20 +1723,28 @@ export default function Home() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydrated || !profileComplete || birthTimeDisplayState(profile)) return;
|
||||
let cancelled = false;
|
||||
setDailyStarlanguageCard(null);
|
||||
void fetchDailyStarlanguage(profile)
|
||||
.then((card) => {
|
||||
if (!cancelled) setDailyStarlanguageCard(card);
|
||||
const controller = new AbortController();
|
||||
setDailyStarlanguage({ kind: "pending" });
|
||||
void fetchDailyStarlanguage(profile, controller.signal)
|
||||
.then((next) => {
|
||||
if (!controller.signal.aborted) setDailyStarlanguage(next);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setDailyStarlanguageCard(buildDailyStarlanguageCard(profile));
|
||||
if (!controller.signal.aborted) setDailyStarlanguage({ kind: "unavailable" });
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
return () => controller.abort();
|
||||
}, [hydrated, profile, profileComplete]);
|
||||
|
||||
useEffect(() => {
|
||||
if (starterHomeVisible) {
|
||||
starterPromptUnseen.current = false;
|
||||
return;
|
||||
}
|
||||
if (starterPromptUnseen.current) return;
|
||||
starterPromptUnseen.current = true;
|
||||
setStarterPrompt(createStarterPrompt());
|
||||
}, [starterHomeVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (starterHomeVisible) return;
|
||||
const container = conversation.current;
|
||||
@@ -3381,7 +3388,7 @@ export default function Home() {
|
||||
<section className="starter-hero" aria-labelledby="starter-heading">
|
||||
<div className="starter-hero-copy">
|
||||
<p className="starter-greeting">{daypartGreeting},{profile.name.trim()}。</p>
|
||||
<h1 id="starter-heading">今天想先理清什么?</h1>
|
||||
<h1 id="starter-heading">{starterPrompt}</h1>
|
||||
<p className="starter-hero-note">{personalChartAvailable
|
||||
? "从此刻最在意的事开始,我会结合你的星盘证据,帮你把问题拆得更清楚。"
|
||||
: "从你现在最关心的事开始;出生时间不足以支持的部分,我会明确说明,不会补造具体分钟。"}</p>
|
||||
@@ -3399,10 +3406,10 @@ export default function Home() {
|
||||
/>
|
||||
<div className="product-entrypoint-copy">
|
||||
<h2 id="daily-starlanguage-title">{personalChartAvailable ? "今日星语" : "每日运势"}</h2>
|
||||
<p>{personalChartAvailable ? dailyStarlanguage?.trend : "看看今天的整体节奏、适合推进的事和需要留意的地方。"}</p>
|
||||
<p aria-busy={personalChartAvailable && dailyStarlanguage.kind === "pending"}>{personalChartAvailable ? dailyStarlanguageTrend : "看看今天的整体节奏、适合推进的事和需要留意的地方。"}</p>
|
||||
</div>
|
||||
<div className="product-entrypoint-footer">
|
||||
<small>{personalChartAvailable ? dailyStarlanguage?.action : "不支持的个人判断会明确说明,不会补造出生时间。"}</small>
|
||||
<small>{personalChartAvailable ? dailyStarlanguageAction : "不支持的个人判断会明确说明,不会补造出生时间。"}</small>
|
||||
<span className="product-entrypoint-action" aria-hidden="true">{personalChartAvailable ? "深入看今日" : "查看今日运势"} <ArrowUpRight className="starter-arrow" /></span>
|
||||
</div>
|
||||
</article>
|
||||
@@ -3423,7 +3430,6 @@ export default function Home() {
|
||||
: "不确定准确出生时间时,可通过已经发生的人生事件逐步缩小范围。"}</p>
|
||||
</div>
|
||||
<div className="product-entrypoint-footer">
|
||||
<small>进度会自动保存;结果只作为候选范围,不会改写已填报出生时间。</small>
|
||||
<span className="product-entrypoint-action" aria-hidden="true">{rectificationCardLabel} <ArrowUpRight className="starter-arrow" /></span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
Reference in New Issue
Block a user