人物目录没到时不再当成没建完档(U11)。
目录还没回来时不闪「看谁的盘」,热启动也不因此走冷路径。已知是空的,仍然算没建完档。月亮星座和天空副句的上升,首页没有现成资料,不新发请求,也不编星座。 Co-Authored-By: Grok (agent loop)
This commit is contained in:
co-authored by
Grok (agent loop)
parent
b2a561b34c
commit
d7e8cc46c2
@@ -85,3 +85,21 @@
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 这六处加上登录动作 | `node node_modules/tsx/dist/cli.mjs --test` 盘型、导出、星盘页、账户、外观、登录、登录动作 | 62 通过,0 失败,0 取消,退出码 0 |
|
||||
|
||||
## U11 · 人物目录没到时不当成没建完档
|
||||
|
||||
没有新的产品故障,所以没有新的 BUG 编号。这三处里,目录这一处已经按「未知」处理。月亮星座和天空副句的星座,首页手里没有现成资料,不能新发请求,也不能编一个星座。
|
||||
|
||||
人物目录还没回来时,不再把它当成「没有别人」。只有名字、盘在别人名下的账户,不会在揭幕后闪出「看谁的盘」,热启动也不会因此改走冷路径。目录确实是空的,仍然算没建完档。揭幕会等到目录回来再显示,所以不会先闪一屏再改口。
|
||||
|
||||
起步第一问的「只有月亮」那一档,句子本来就会写。星座目前只从今日星语记住。没有出生分钟时,今日星语不请求。账户、档案、本命盘缓存里都没有月亮星座。星盘页那份结果要有分钟才会有,而且上升和月亮一起都有,对不上「只有月亮」。按任务书不能为了这一问再发请求,也不能临时编一个星座。所以这一档在第一次进首页、又没有分钟时,仍然走不到。有分钟时,今日星语回来后,上升和月亮两档照旧会写上。
|
||||
|
||||
帮别人看的天空副句,句子是「那一刻,{名字}的上升在{星座}」。名字在,上升星座不在,这一行就不显示,原来的天空句子照常播。上升同样只在已有的星座记录里,没有就空着。没有为了这一行再排一次盘。
|
||||
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 目录未知、已知空列表、副句,以及被改到的首页合同 | 在 `frontend` 下 `node node_modules/tsx/dist/cli.mjs --test`:`home-warm-snapshot`、`onboarding-intake-20261022`、`home-first-load-20260930`、`home-bootstrap-run`、`composer-isolation-contract`、`chat-composer-queue`、`birth-time-consultation-consent`、`home-shell-growth-contract` | 75 通过,0 失败,0 取消,退出码 0 |
|
||||
|
||||
断言三栏(引导页是否预载):原值是目录还没到也按空列表预载。新值是已知空列表才预载,未知不预载、步骤停在称呼。原因是未知不能当成没建完档。
|
||||
|
||||
断言三栏(输入框标签和禁用):原值认 `onboardingStep`。新值认 `shownOnboardingStep`,并在目录未到时不收起输入框。原因同上。称呼和发送的两句文案没改。
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
| `TASK-rectification-explicit-open-20261023.md` | `PROGRESS-rectification-explicit-open-20261023.md` | **明说要校正只回一句话(issue #103)**:找会话直接读一张对服务角色撤权的表、必然失败被吞(真正进行中的也续不上);`candidate_accepted` 被算作进行中挡住入口和提议卡;兜底只给一句话。产品选 C;只改入口口径、经 RPC 找会话、删兜底,不改共享状态集合、不动表。BUG-1423 | 已合入 staging | 分支 `codex/rectification-explicit-open-20261023` |
|
||||
| `TASK-chart-table-span-20261023.md` | `PROGRESS-chart-table-span-20261023.md` | **星盘页本命 / 月亮行星表挤进左栏(issue #102)**:#87 给表包了 `ChartSymbolHelp` 后跨栏规则失配;根因修法是 `VedicLayout` 用固定整行容器承载表格,跨栏只认容器;加结构断言防复发。BUG-1422 | 已合入 staging | 分支 `codex/chart-table-span-20261023` |
|
||||
| `TASK-ui-kit-migration-20261023.md` | `PROGRESS-ui-kit-0-base-20261023.md`;`PROGRESS-ui-kit-1-shell-20261023.md`;`PROGRESS-ui-kit-2-chat-20261023.md`;`PROGRESS-ui-kit-3-chart-20261023.md`;`PROGRESS-ui-kit-4-people-20261023.md`;`PROGRESS-ui-kit-5-pages-20261023.md`;`PROGRESS-ui-kit-6-rectification-20261023.md`;`PROGRESS-ui-kit-7-cleanup-20261023.md` | **界面统一换成组件库**:shadcn base-nova(补 `shadcn/tailwind.css`)· Lucide · Motion · react-resizable-panels · dnd-kit · Zustand;lint 拦手写控件(白名单只减不增);对话旁星盘面板;会话、档案拖动排序(动表);长相不变。产品 2026-10-23 拍板 D1~D8;8 个 issue(K0~K7)串行,取代全站动效 #98 #100 #101 | K0–K7 已在测试环境,待验收 | 原型 https://claude.ai/artifact/PGnWsjHhu38KJseEfEMNWG |
|
||||
| `TASK-ui-kit-onboarding-fix-20261026.md` | `PROGRESS-ui-kit-onboarding-fix-20261026.md` | **组件库 K0~K7 与启程(二)~(四)事后验收**:桌面首屏后整页重挂、中英切换看不出选中、提示层每次 DOM 变化全页扫描、启程动效对所有人生效且会藏内容、清单数据只拉一次、拖动排序 N 个请求与部分失败、缺真库重读测试、首屏超 2%、依赖卫生、补回削弱的测试、启程(二)三处未做 | 执行中(#111,U1–U6、U9、U10 已提交) | `codex/ui-kit-onboarding-fix-20261026` |
|
||||
| `TASK-ui-kit-onboarding-fix-20261026.md` | `PROGRESS-ui-kit-onboarding-fix-20261026.md` | **组件库 K0~K7 与启程(二)~(四)事后验收**:桌面首屏后整页重挂、中英切换看不出选中、提示层每次 DOM 变化全页扫描、启程动效对所有人生效且会藏内容、清单数据只拉一次、拖动排序 N 个请求与部分失败、缺真库重读测试、首屏超 2%、依赖卫生、补回削弱的测试、启程(二)三处未做 | 执行中(#111,U1–U6、U9、U10、U11 已提交) | `codex/ui-kit-onboarding-fix-20261026` |
|
||||
| —(门禁修复,无新任务书) | `PROGRESS-birth-journey-contract-20261025.md` | **测试分支过不了门禁,网站停在完整数据版(二)**:BUG-1439 出生时间合同;BUG-1440 测试画布;BUG-1441 侧栏流式检查等第一次补画结束再数帧。页面不改 | 已合入 staging,等门禁 | `codex/gate-birth-journey-contract-20261025` |
|
||||
| `TASK-onboarding-journey-20261022.md` | `PROGRESS-onboarding-1-data-20261022.md`;`PROGRESS-onboarding-2-intake-20261022.md`;`PROGRESS-onboarding-4-motion-20261022.md` | **新用户启程**:建档加「先看谁的盘」(自己 / 帮别人看,本人资料可后补)、人物关系字段、首问免费(服务端原子)、起步三问、启程清单(七项从数据推导,收费项标价,老用户可见)、八条一次性提示(含星盘四页签)、启程流程专用动效。产品 2026-10-22 拍板 D1~D6;动表经产品同意进循环,拆四个 issue 串行。BUG-1420 已被文档去重占用,本轮首问免费记 BUG-1421。(一)T1+T2 在 `codex/onboarding-1-data-20261022`。(二)T3+T4 记 BUG-1431,在 `codex/onboarding-2-intake-20261022`。(四)T7 在 `codex/onboarding-4-motion-20261022` | (一)(二)(三)已在 staging;(四)本分支待验收 | 原型 https://claude.ai/artifact/PcTwMVb9RD611R3qStxv3U |
|
||||
| `TASK-site-motion-20261022.md` | — | **全站动效**:换页按侧栏方向滑入、选中块跟随、页签、盘型切换星体移宫、换人、翻日期、列表补位、弹窗从触发处长出、抽屉、顶部提示、点数滚动、中英交叉淡入;重写 DESIGN §6(推翻 180 ms 上限与 Spatial Instant),不引动画库,先验证 View Transitions。产品 2026-10-22 拍板;三个 issue,排在启程(三)之后、启程(四)之前;**2026-10-23 被 ui-kit-migration 取代** | 已取代 | 原型 https://claude.ai/artifact/7u9goivnmTNSGTNWNeVMQd |
|
||||
|
||||
@@ -43,8 +43,8 @@ import { useHomeShellRegistration } from "@/hooks/use-home-shell-registration";
|
||||
import { useSessionList } from "@/hooks/use-session-list";
|
||||
import { composerDraftSnapshot, setComposerDraft } from "@/lib/composer-draft";
|
||||
import { sessionMatchesSubject } from "@/lib/chat-profile-picker-model";
|
||||
import { bindCurrentSubjectAccount, peekSubjectCatalog, readCurrentSubjectId, subscribeSubjectCatalog, useCurrentSubject, useCurrentSubjectProfile } from "@/lib/current-subject";
|
||||
import { setOnboardingFinishedListener } from "@/lib/onboarding-intake";
|
||||
import { bindCurrentSubjectAccount, peekSubjectCatalog, readCurrentSubjectId, subscribeSubjectCatalog, useCurrentSubject, useCurrentSubjectProfile, useCurrentSubjectReady } from "@/lib/current-subject";
|
||||
import { readOnboardingIntake, setOnboardingFinishedListener } from "@/lib/onboarding-intake";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { notifyOnboardingDailyOpened } from "@/lib/onboarding-signals";
|
||||
@@ -153,6 +153,7 @@ import { useChatProfileCatalog } from "@/hooks/use-chat-profile-catalog";
|
||||
import {
|
||||
birthQuestion,
|
||||
isProfileComplete,
|
||||
missingProfileStep,
|
||||
placeQuestion,
|
||||
readProfile,
|
||||
selfChartComplete,
|
||||
@@ -349,6 +350,7 @@ export default function Home() {
|
||||
() => peekSubjectCatalog() ?? EMPTY_SUBJECT_CATALOG,
|
||||
() => EMPTY_SUBJECT_CATALOG,
|
||||
);
|
||||
const subjectCatalogReady = useCurrentSubjectReady();
|
||||
const currentSubjectId = useCurrentSubject();
|
||||
const starterSignMap = useSyncExternalStore(subscribeStarterSigns, readStarterSignMap, readStarterSignMap);
|
||||
const {
|
||||
@@ -660,7 +662,15 @@ export default function Home() {
|
||||
setChartLibrary((current) => upsertSelfChart(current, profileForLibrary));
|
||||
}, [account, accountId, applyCloudSynastryHistory, beginChatProfileReload, clearSynastryHistory, markChatProfilesDegraded, markChatProfilesReady, profile]);
|
||||
|
||||
const profileComplete = isProfileComplete(profile, subjectCatalog);
|
||||
// An empty stand-in is not a known empty list. Until the people list arrives,
|
||||
// do not treat a name-only holder as unfinished (that flashes 「看谁的盘」).
|
||||
const profileComplete = subjectCatalogReady && isProfileComplete(profile, subjectCatalog);
|
||||
const catalogStep = subjectCatalogReady
|
||||
? missingProfileStep(profile, subjectCatalog, accountId ? readOnboardingIntake(accountId) : null)
|
||||
: null;
|
||||
const shownOnboardingStep = onboardingStep === "name" && catalogStep && catalogStep !== "name"
|
||||
? catalogStep
|
||||
: onboardingStep;
|
||||
const dailyProfile = useCurrentSubjectProfile(profile);
|
||||
const birthTimeRoute = resolveBirthTimeConsultationRoute(dailyProfile ?? { ...profile, time: "", reportedTime: "", birthTimeStatus: "reported", birthTimeSource: "unknown" }, birthTimeConsultationConsent, activeSessionId);
|
||||
const natalMinuteAvailable = birthTimeRoute.kind === "consult"
|
||||
@@ -716,14 +726,14 @@ export default function Home() {
|
||||
}, [hydrated]);
|
||||
const onboardingAudience = intakeTarget === "other" ? "other" : "self";
|
||||
const onboardingSubjectName = onboardingAudience === "other" ? onboardingDraft.name.trim() : profile.name.trim();
|
||||
const currentOnboardingMessage = onboardingStep === "who"
|
||||
const currentOnboardingMessage = shownOnboardingStep === "who"
|
||||
? whoQuestion(profile.name.trim())
|
||||
: onboardingStep === "birth"
|
||||
: shownOnboardingStep === "birth"
|
||||
? birthQuestion(onboardingSubjectName, onboardingAudience)
|
||||
: onboardingStep === "place"
|
||||
: shownOnboardingStep === "place"
|
||||
? placeQuestion(onboardingDraft, onboardingAudience, onboardingSubjectName)
|
||||
: presetOnboardingMessage;
|
||||
const shouldStreamOnboarding = !profileComplete;
|
||||
const shouldStreamOnboarding = subjectCatalogReady && !profileComplete;
|
||||
const presetMessageFinished = !shouldStreamOnboarding || presetMessageLength >= currentOnboardingMessage.length;
|
||||
const onboardingCardReady = presetMessageFinished || birthTimeAssessmentPhase !== null;
|
||||
const starterHomeVisible = profileComplete
|
||||
@@ -737,7 +747,7 @@ export default function Home() {
|
||||
&& !activeSession.messagesHydrated
|
||||
&& pendingSessionId !== activeSession.id,
|
||||
);
|
||||
const onboardingFormActive = !synastryChat && !profileComplete && onboardingStep !== "name";
|
||||
const onboardingFormActive = !synastryChat && subjectCatalogReady && !profileComplete && shownOnboardingStep !== "name";
|
||||
const starterHasPriorSessions = sessions.some((session) => session.id !== activeSession?.id);
|
||||
const starterGreeting = createStartGreetingParts(profile.name, new Date(), starterGreetingSelection, starterHasPriorSessions);
|
||||
const conversationAnchor = useConversationScrollAnchor(
|
||||
@@ -941,10 +951,10 @@ export default function Home() {
|
||||
}, [starterHomeVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hydrated && accountId && !profileComplete && onboardingStep === "name" && presetMessageFinished && activeAccountDialog === null) {
|
||||
if (hydrated && accountId && subjectCatalogReady && !profileComplete && shownOnboardingStep === "name" && presetMessageFinished && activeAccountDialog === null) {
|
||||
composerInput.current?.focus({ preventScroll: true });
|
||||
}
|
||||
}, [accountId, activeAccountDialog, hydrated, onboardingStep, presetMessageFinished, profileComplete]);
|
||||
}, [accountId, activeAccountDialog, hydrated, presetMessageFinished, profileComplete, shownOnboardingStep, subjectCatalogReady]);
|
||||
|
||||
useEffect(() => {
|
||||
setOnboardingFinishedListener((subjectId) => {
|
||||
@@ -1109,7 +1119,7 @@ export default function Home() {
|
||||
function submit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!synastryChat && !profileComplete) {
|
||||
if (onboardingStep === "name" && presetMessageFinished) void saveOnboardingName();
|
||||
if (shownOnboardingStep === "name" && presetMessageFinished) void saveOnboardingName();
|
||||
return;
|
||||
}
|
||||
if (synastryClosed || subjectDeleted) return;
|
||||
@@ -1159,7 +1169,7 @@ export default function Home() {
|
||||
visibleSessions,
|
||||
});
|
||||
|
||||
if (!hydrated || (!account && !accountError)) {
|
||||
if (!hydrated || (!account && !accountError) || (Boolean(account) && !accountError && !subjectCatalogReady)) {
|
||||
const loadingCopy = bootstrapLoadingCopy(bootstrapPhase);
|
||||
return (
|
||||
<main className="app-loading" aria-busy="true" aria-live="polite">
|
||||
@@ -1303,7 +1313,7 @@ export default function Home() {
|
||||
intakeTarget={intakeTarget}
|
||||
onChooseSelf={chooseOnboardingSelf}
|
||||
onChooseOther={chooseOnboardingOther}
|
||||
onboardingStep={onboardingStep}
|
||||
onboardingStep={shownOnboardingStep}
|
||||
presetMessageLength={presetMessageLength}
|
||||
currentOnboardingMessage={currentOnboardingMessage}
|
||||
onboardingJustCompleted={onboardingJustCompleted}
|
||||
@@ -1384,12 +1394,12 @@ export default function Home() {
|
||||
{rectificationComposerReopen ? <RectificationComposerReopen message={rectificationErrorMessage} onReopen={() => void reopenRectificationSession(activeSession?.id || rectificationErrorSessionId || "")} /> : null}
|
||||
<ChatComposer
|
||||
inputRef={composerInput}
|
||||
inputLabel={!synastryChat && !profileComplete && onboardingStep === "name" ? "输入你的称呼" : "输入你的问题"}
|
||||
placeholder={ordinaryComposerPlaceholder({ locked: rectificationComposerLocked, hasAccount: Boolean(account), profileComplete: synastryChat || profileComplete, onboardingName: onboardingStep === "name", presetMessageFinished, creditsEmpty: Boolean(account && account.credits === 0 && !account.activeSubscription && !account.firstConsultFree), subjectDeleted, synastryClosed })}
|
||||
maxLength={!synastryChat && !profileComplete && onboardingStep === "name" ? 80 : 500}
|
||||
inputDisabled={synastryClosed || subjectDeleted || sessionMessagesLoading || rectificationSurfaceOpen || activeRectificationSession || (!synastryChat && !profileComplete && (onboardingStep !== "name" || !presetMessageFinished || profileSaving))}
|
||||
submitLabel={!synastryChat && !profileComplete && onboardingStep === "name" ? "确认称呼" : "发送"}
|
||||
submitBlocked={synastryClosed || subjectDeleted || Boolean(pendingSessionId) || Boolean(queuedDraft) || sessionMessagesLoading || rectificationSurfaceOpen || activeRectificationSession || !account || !modelCatalog || (!synastryChat && !profileComplete && (onboardingStep !== "name" || !presetMessageFinished || profileSaving))}
|
||||
inputLabel={!synastryChat && !profileComplete && shownOnboardingStep === "name" ? "输入你的称呼" : "输入你的问题"}
|
||||
placeholder={ordinaryComposerPlaceholder({ locked: rectificationComposerLocked, hasAccount: Boolean(account), profileComplete: synastryChat || profileComplete, onboardingName: shownOnboardingStep === "name", presetMessageFinished, creditsEmpty: Boolean(account && account.credits === 0 && !account.activeSubscription && !account.firstConsultFree), subjectDeleted, synastryClosed })}
|
||||
maxLength={!synastryChat && !profileComplete && shownOnboardingStep === "name" ? 80 : 500}
|
||||
inputDisabled={synastryClosed || subjectDeleted || sessionMessagesLoading || rectificationSurfaceOpen || activeRectificationSession || (!synastryChat && !profileComplete && (shownOnboardingStep !== "name" || !presetMessageFinished || profileSaving))}
|
||||
submitLabel={!synastryChat && !profileComplete && shownOnboardingStep === "name" ? "确认称呼" : "发送"}
|
||||
submitBlocked={synastryClosed || subjectDeleted || Boolean(pendingSessionId) || Boolean(queuedDraft) || sessionMessagesLoading || rectificationSurfaceOpen || activeRectificationSession || !account || !modelCatalog || (!synastryChat && !profileComplete && (shownOnboardingStep !== "name" || !presetMessageFinished || profileSaving))}
|
||||
stopVisible={isLoading}
|
||||
stopLabel={consultationPhase === "undo" ? "撤回发送,本次不扣点" : activeStreamingText ? "停止回答,保留已生成内容并退回本次点数" : "停止回答并申请退回本次点数"}
|
||||
stopTitle={consultationPhase === "undo" ? "撤回发送,本次不扣点" : activeStreamingText ? "停止回答,保留现有内容并申请退回本次点数" : "停止回答"}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { BIRTH_SKY_PALETTES, drawBirthSky, type BirthSkyCanvas } from "@/lib/bir
|
||||
import type { BirthSkyRevealSky } from "@/lib/birth-sky/reveal";
|
||||
import { birthSkyFactLine, birthSkyHeadline } from "@/lib/birth-sky/sentence";
|
||||
import { peekSubjectCatalog, subscribeSubjectCatalog, useCurrentSubject } from "@/lib/current-subject";
|
||||
import { displaySign, readStarterSigns, subscribeStarterSigns } from "@/lib/starter-questions";
|
||||
import { otherSkySubjectLine, readStarterSigns, subscribeStarterSigns } from "@/lib/starter-questions";
|
||||
|
||||
function useOtherSkySubjectLine(): string {
|
||||
const subjectId = useCurrentSubject();
|
||||
@@ -21,9 +21,7 @@ function useOtherSkySubjectLine(): string {
|
||||
const signs = useSyncExternalStore(subscribeStarterSigns, () => readStarterSigns(subjectId), () => readStarterSigns(subjectId));
|
||||
if (!subjectId || subjectId === "self") return "";
|
||||
const name = catalog?.find((row) => row.id === subjectId)?.profile.name.trim() ?? "";
|
||||
const sign = displaySign(signs.ascendantSign);
|
||||
if (!name || !sign) return "";
|
||||
return `那一刻,${name}的上升在${sign}`;
|
||||
return otherSkySubjectLine(name, signs.ascendantSign);
|
||||
}
|
||||
|
||||
/** Largest 3:4 box that fits the viewport, in CSS pixels. */
|
||||
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
redirectToLogin,
|
||||
resolveLookupBootstrap,
|
||||
} from "@/lib/home-cloud-sync";
|
||||
import { chartSnapshotForSession, isProfileComplete, missingProfileStep, readProfile } from "@/lib/home-profile";
|
||||
import { chartSnapshotForSession, missingProfileStep, onboardingCatalogDecision, readProfile } from "@/lib/home-profile";
|
||||
import { peekSubjectCatalog, readCurrentSubjectId } from "@/lib/current-subject";
|
||||
import { readOnboardingIntake } from "@/lib/onboarding-intake";
|
||||
import { sessionMatchesSubject } from "@/lib/chat-profile-picker-model";
|
||||
@@ -127,6 +127,8 @@ export type HomeBootstrapDeps = {
|
||||
consultationStatusMissingCount: { current: number };
|
||||
isDevelopment: boolean;
|
||||
readSearch: () => string;
|
||||
/** Defaults to the live people list. `null` is "not loaded", not "nobody else". */
|
||||
readSubjectCatalog?: () => ReturnType<typeof peekSubjectCatalog>;
|
||||
storage: HomeBootstrapStorage;
|
||||
restoreConsultationRecovery: HomeBootstrapIo["restoreConsultationRecovery"];
|
||||
setComposerNotice: (notice: string) => void;
|
||||
@@ -219,6 +221,10 @@ async function resolveReservedConsultation(input: {
|
||||
return { reservedConsultation, storedPending };
|
||||
}
|
||||
|
||||
function subjectCatalogForOnboarding(deps: HomeBootstrapDeps) {
|
||||
return deps.readSubjectCatalog ? deps.readSubjectCatalog() : peekSubjectCatalog();
|
||||
}
|
||||
|
||||
export async function runAccountPhase(
|
||||
deps: HomeBootstrapDeps,
|
||||
signal: AbortSignal,
|
||||
@@ -351,9 +357,9 @@ export async function runAccountPhase(
|
||||
const nextProfile = readProfile(nextAccount.profile);
|
||||
// The onboarding shell is no longer in the first-screen bundle (BUG-1130);
|
||||
// someone who will see it gets it before reveal, not a blank frame after.
|
||||
const onboardingOthers = peekSubjectCatalog() ?? [];
|
||||
const onboardingHint = readOnboardingIntake(nextAccount.user.id);
|
||||
if (!isProfileComplete(nextProfile, onboardingOthers, onboardingHint)) await io.preloadOnboardingShell();
|
||||
const onboardingDecision = onboardingCatalogDecision(nextProfile, subjectCatalogForOnboarding(deps), onboardingHint);
|
||||
if (onboardingDecision.known && onboardingDecision.step !== null) await io.preloadOnboardingShell();
|
||||
if (signal.aborted) return { kind: "stop" };
|
||||
const parsedSessions = readSessions(listBoot.rawRows, nextModelCatalog);
|
||||
let nextSessions = parsedSessions.sessions.length > 0 ? parsedSessions.sessions : listBoot.sessions;
|
||||
@@ -444,7 +450,14 @@ export async function resolveLanding(
|
||||
deps.commit.setProfile(ready.nextProfile);
|
||||
deps.commit.setProfileDraft(ready.nextProfile);
|
||||
deps.commit.setStartGreeting(ready.nextProfile.name.trim() ? createStartGreeting(ready.nextProfile.name) : "");
|
||||
deps.commit.setOnboardingStep(missingProfileStep(ready.nextProfile, peekSubjectCatalog() ?? [], readOnboardingIntake(ready.nextAccount.user.id)) ?? "name");
|
||||
const landingDecision = onboardingCatalogDecision(
|
||||
ready.nextProfile,
|
||||
subjectCatalogForOnboarding(deps),
|
||||
readOnboardingIntake(ready.nextAccount.user.id),
|
||||
);
|
||||
// Unknown stays on the name placeholder. It must not commit 「看谁的盘」
|
||||
// from an empty stand-in; the page shows the real step once the list arrives.
|
||||
deps.commit.setOnboardingStep(landingDecision.known ? (landingDecision.step ?? "name") : "name");
|
||||
deps.commit.setSessions(nextSessions);
|
||||
if (bootstrapSelection.urlAction === "new-chat") writeSessionUrl(null, "replace");
|
||||
deps.commit.setActiveSessionId(landingSessionId);
|
||||
|
||||
@@ -401,3 +401,20 @@ export function isProfileComplete(
|
||||
) {
|
||||
return missingProfileStep(profile, others, hint) === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* `others === null` means the people list has not arrived.
|
||||
* That is not an empty list. An empty list means nobody else, so a name-only
|
||||
* holder is still unfinished. Unknown must not be judged unfinished: an
|
||||
* account whose only finished chart belongs to someone else would flash
|
||||
* 「看谁的盘」 and would be refused a warm start.
|
||||
* `step` is only meaningful when `known` is true. `null` then means finished.
|
||||
*/
|
||||
export function onboardingCatalogDecision(
|
||||
profile: Profile,
|
||||
others: readonly OnboardingOtherRecord[] | null,
|
||||
hint: OnboardingIntakeHint | null = null,
|
||||
): { readonly known: boolean; readonly step: OnboardingStep | null } {
|
||||
if (others === null) return { known: false, step: null };
|
||||
return { known: true, step: missingProfileStep(profile, others, hint) };
|
||||
}
|
||||
|
||||
@@ -26,9 +26,8 @@ import { resolveStarterHomeLandingSessionId, starterHomeLandingNeedsConsultation
|
||||
import { createSession, readStoredDailyStarlanguage } from "@/lib/home-cloud-sync";
|
||||
import {
|
||||
chartSnapshotForSession,
|
||||
isProfileComplete,
|
||||
missingOtherProfileStep,
|
||||
missingProfileStep,
|
||||
onboardingCatalogDecision,
|
||||
readProfile,
|
||||
} from "@/lib/home-profile";
|
||||
import {
|
||||
@@ -160,11 +159,12 @@ export function resolveHomeWarmStart(input: HomeWarmStartInput): HomeWarmStart |
|
||||
const snapshot = readHomeWarmSnapshot(account.user.id);
|
||||
if (!isCompleteHomeWarmSnapshot(snapshot)) return null;
|
||||
const profile = readProfile(account.profile);
|
||||
const onboardingOthers = input.subjectCatalog ?? [];
|
||||
const onboardingHint = readOnboardingIntake(account.user.id);
|
||||
// Onboarding keeps the cold path: its streamed questions and birth-time
|
||||
// assessment are not part of the snapshot.
|
||||
if (!isProfileComplete(profile, onboardingOthers, onboardingHint)) return null;
|
||||
// assessment are not part of the snapshot. A list that has not arrived is
|
||||
// not that path — only a known unfinished profile is.
|
||||
const catalogDecision = onboardingCatalogDecision(profile, input.subjectCatalog, onboardingHint);
|
||||
if (catalogDecision.known && catalogDecision.step !== null) return null;
|
||||
const catalog = snapshot.modelCatalog;
|
||||
const landing = resolveWarmLanding({
|
||||
sessions: input.sessions,
|
||||
@@ -182,7 +182,7 @@ export function resolveHomeWarmStart(input: HomeWarmStartInput): HomeWarmStart |
|
||||
modelCatalog: catalog,
|
||||
profile,
|
||||
startGreeting: profile.name.trim() ? createStartGreeting(profile.name) : "",
|
||||
onboardingStep: missingProfileStep(profile, onboardingOthers, onboardingHint) ?? "name",
|
||||
onboardingStep: catalogDecision.step ?? "name",
|
||||
entrySummary: snapshot.entrySummary,
|
||||
daily: warmDaily(account.user.id, input.subjectId, subjectProfile(input.subjectId, profile, input.subjectCatalog), input.now),
|
||||
sessionsCursor: input.boot === snapshot.listBoot ? snapshot.sessionsCursor : input.boot.cursor,
|
||||
|
||||
@@ -248,7 +248,8 @@ export function SessionListProvider({ children }: { children: ReactNode }) {
|
||||
// no Home is mounted, fill the snapshot in the background (read-only GETs).
|
||||
// Home mounting, sign-out, an account switch or a list reload cancels it.
|
||||
const onboardingCatalog = useSyncExternalStore(subscribeSubjectCatalog, peekSubjectCatalog, () => null);
|
||||
const prefetchAccountId = settled && !signedOut && !registration && account && isProfileComplete(readProfile(account.profile), onboardingCatalog ?? [])
|
||||
const prefetchAccountId = settled && !signedOut && !registration && account && onboardingCatalog !== null
|
||||
&& isProfileComplete(readProfile(account.profile), onboardingCatalog)
|
||||
? account.user.id
|
||||
: null;
|
||||
useEffect(() => {
|
||||
|
||||
@@ -15,6 +15,14 @@ const EMPTY_SIGNS: StarterChartSigns = { ascendantSign: null, moonSign: null };
|
||||
|
||||
const SIGN_LABELS: Readonly<Record<string, string>> = SIGN_ZH;
|
||||
|
||||
/** Sky reveal subtitle for someone else's chart. Empty until both the name and the ascendant are known. */
|
||||
export function otherSkySubjectLine(name: string, ascendantSign: string | null | undefined): string {
|
||||
const trimmed = name.trim();
|
||||
const sign = displaySign(ascendantSign);
|
||||
if (!trimmed || !sign) return "";
|
||||
return `那一刻,${trimmed}的上升在${sign}`;
|
||||
}
|
||||
|
||||
/** English engine names become the short Chinese sign. An already-Chinese label is kept, without 座. */
|
||||
export function displaySign(value: string | null | undefined): string | null {
|
||||
const text = value?.trim() ?? "";
|
||||
|
||||
@@ -243,10 +243,10 @@ test("onboarding form steps hide the dead composer and keep source choices in a
|
||||
const page = homeSurface;
|
||||
const css = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
|
||||
|
||||
// 原值: const onboardingFormActive = !profileComplete && onboardingStep !== "name"
|
||||
// 新值: const onboardingFormActive = !synastryChat && !profileComplete && onboardingStep !== "name"
|
||||
// 原因: 合盘追问可以跳过引导表单。普通对话仍是资料未完成、且不在称呼步时才收起输入框。
|
||||
assert.match(page, /const onboardingFormActive = !synastryChat && !profileComplete && onboardingStep !== "name"/);
|
||||
// 原值: const onboardingFormActive = !synastryChat && !profileComplete && onboardingStep !== "name"
|
||||
// 新值: const onboardingFormActive = !synastryChat && subjectCatalogReady && !profileComplete && shownOnboardingStep !== "name"
|
||||
// 原因: 人物目录未到时不收起输入框、也不闪「看谁的盘」。目录已知且资料未完成、又不在称呼步时,仍收起。合盘追问仍跳过引导表单。
|
||||
assert.match(page, /const onboardingFormActive = !synastryChat && subjectCatalogReady && !profileComplete && shownOnboardingStep !== "name"/);
|
||||
assert.match(page, /!rectificationSurfaceOpen && !onboardingFormActive && <div className=\{`composer-wrap/);
|
||||
assert.match(page, /birthTimeDraftReadyHint\(profileDraft\)/);
|
||||
assert.match(css, /\.birth-time-source-list \{ grid-template-columns: 1fr/);
|
||||
|
||||
@@ -54,12 +54,12 @@ test("generating does not disable the textarea; Enter queues instead of dropping
|
||||
// 原值:`inputDisabled={sessionMessagesLoading || rectificationSurfaceOpen || activeRectificationSession || (!profileComplete && ...)}`
|
||||
// 新值:表达式前增加 `subjectDeleted ||`,其余结构性禁用条件不变。
|
||||
// 原因:BUG-1000 已删除人物的历史会话只读;BUG-1014 同步合同,不改生成中仍可输入的性质。
|
||||
// 原值:subjectDeleted 开头,资料不全仍禁用
|
||||
// 新值:合盘已删除时禁用;合盘追问不因查看者资料不全而禁用
|
||||
// 原因:#83 报告删除后只读,追问不要求查看者自己的出生资料
|
||||
// 原值:合盘已删除时禁用;判断称呼步用 onboardingStep
|
||||
// 新值:同一串禁用条件,称呼步改认 shownOnboardingStep
|
||||
// 原因:人物目录未到时不把空目录当成称呼已过。生成中仍可输入,合盘追问仍不因查看者资料不全而禁用。
|
||||
assert.match(
|
||||
page,
|
||||
/inputDisabled=\{synastryClosed \|\| subjectDeleted \|\| sessionMessagesLoading \|\| rectificationSurfaceOpen \|\| activeRectificationSession \|\| \(!synastryChat && !profileComplete && \(onboardingStep !== "name" \|\| !presetMessageFinished \|\| profileSaving\)\)\}/,
|
||||
/inputDisabled=\{synastryClosed \|\| subjectDeleted \|\| sessionMessagesLoading \|\| rectificationSurfaceOpen \|\| activeRectificationSession \|\| \(!synastryChat && !profileComplete && \(shownOnboardingStep !== "name" \|\| !presetMessageFinished \|\| profileSaving\)\)\}/,
|
||||
);
|
||||
assert.doesNotMatch(page, /inputDisabled=\{isLoading \|\| sessionMessagesLoading \|\| cancellationPending/);
|
||||
assert.match(page, /if \(isLoading \|\| cancellationPending\) return void enqueueQueuedDraft\(composerDraftSnapshot\(\)\);/);
|
||||
|
||||
@@ -100,11 +100,11 @@ test("the composer keeps its Chinese input, focus and accessibility contract", (
|
||||
assert.match(composerSource, /title=\{stopTitle\}/);
|
||||
assert.match(composerSource, /aria-label=\{submitLabel\} disabled=/);
|
||||
assert.equal(composerSource.match(/size="icon"/g)?.length, 2);
|
||||
// 原值: 资料未完成且停在称呼步时,标签是「输入你的称呼 / 确认称呼」
|
||||
// 新值: 合盘追问不走这套称呼标签
|
||||
// 原因: #83 追问要发出预填问题,不能把发送按钮改成确认称呼
|
||||
assert.match(pageSource, /inputLabel=\{!synastryChat && !profileComplete && onboardingStep === "name" \? "输入你的称呼" : "输入你的问题"\}/);
|
||||
assert.match(pageSource, /submitLabel=\{!synastryChat && !profileComplete && onboardingStep === "name" \? "确认称呼" : "发送"\}/);
|
||||
// 原值: 资料未完成且停在称呼步时,标签是「输入你的称呼 / 确认称呼」,判断用 onboardingStep
|
||||
// 新值: 同一对标签,判断用 shownOnboardingStep;合盘追问仍不走这套称呼标签
|
||||
// 原因: 人物目录未到时不能把空目录当成「看谁的盘」。称呼步的两句文案不变,#83 的合盘追问也不改成确认称呼。
|
||||
assert.match(pageSource, /inputLabel=\{!synastryChat && !profileComplete && shownOnboardingStep === "name" \? "输入你的称呼" : "输入你的问题"\}/);
|
||||
assert.match(pageSource, /submitLabel=\{!synastryChat && !profileComplete && shownOnboardingStep === "name" \? "确认称呼" : "发送"\}/);
|
||||
});
|
||||
|
||||
test("every external draft writer keeps working through the page-owned setters", () => {
|
||||
|
||||
@@ -413,6 +413,9 @@ test("the home first screen no longer imports the city table by value (T7)", ()
|
||||
});
|
||||
|
||||
test("an incomplete profile waits for the onboarding shell before reveal; a complete one does not load it (T7)", async () => {
|
||||
// 原值:人物目录还没到时用空数组,只有名字的账户也会预载引导页。
|
||||
// 新值:目录用已知的空列表才预载;目录还没到(null)不预载,也不把步骤写成「看谁的盘」。
|
||||
// 原因:目录未到是未知,不是「没有别人」。已知的空列表仍表示没建完档。
|
||||
installWindow();
|
||||
const restoreStorage = installStorage({});
|
||||
for (const complete of [false, true]) {
|
||||
@@ -433,6 +436,7 @@ test("an incomplete profile waits for the onboarding shell before reveal; a comp
|
||||
consultationStatusMissingCount: { current: 0 },
|
||||
isDevelopment: false,
|
||||
readSearch: () => "",
|
||||
readSubjectCatalog: () => [],
|
||||
storage: { getItem: (key: string) => storage.get(key) ?? null, removeItem: (key: string) => { storage.delete(key); } },
|
||||
restoreConsultationRecovery: noop,
|
||||
setComposerNotice: noop,
|
||||
@@ -462,6 +466,41 @@ test("an incomplete profile waits for the onboarding shell before reveal; a comp
|
||||
await run;
|
||||
assert.deepEqual(phases, ["prepare"]);
|
||||
}
|
||||
let unknownStep = "";
|
||||
let unknownPreloads = 0;
|
||||
const unknownProfile = { ...emptyProfile, name: "Synthetic" };
|
||||
const unknownBoot: SessionListBoot = { sessions: [], rawRows: [], cursor: null, account: { ...account, profile: unknownProfile }, signedOut: false };
|
||||
const unknownDeps: HomeBootstrapDeps = {
|
||||
sessionListReady: Promise.resolve(),
|
||||
sessionListBoot: () => unknownBoot,
|
||||
uiPreview: { current: false }, uiPreviewMode: { current: null },
|
||||
consultationStatusMissingCount: { current: 0 },
|
||||
isDevelopment: false,
|
||||
readSearch: () => "",
|
||||
readSubjectCatalog: () => null,
|
||||
storage: { getItem: () => null, removeItem: () => undefined },
|
||||
restoreConsultationRecovery: () => undefined,
|
||||
setComposerNotice: () => undefined,
|
||||
commit: {
|
||||
setAccount: () => undefined, setAccountError: () => undefined, setActiveSessionId: () => undefined,
|
||||
setBirthTimeAssessmentPhase: () => undefined, setBirthTimeJourney: () => undefined,
|
||||
setBootstrapPhase: () => undefined, setGuidedJourneyPreview: () => undefined,
|
||||
setHydrated: () => undefined, setModelCatalog: () => undefined,
|
||||
setOnboardingStep: (step) => { unknownStep = step; },
|
||||
setProfile: () => undefined, setProfileDraft: () => undefined,
|
||||
setSessions: () => undefined, setSessionsCursor: () => undefined, setStartGreeting: () => undefined,
|
||||
},
|
||||
io: {
|
||||
fetchModelCatalog: async () => previewModelCatalog,
|
||||
fetchActiveConsultationStatus: async () => null,
|
||||
startEntrySummaryRead: () => undefined,
|
||||
preloadOnboardingShell: () => { unknownPreloads += 1; return Promise.resolve(); },
|
||||
resolveLookupBootstrap: async (input) => ({ selection: { sessionId: input.defaultSessionId, urlAction: "none", missing: false }, sessions: input.sessions, notice: null }),
|
||||
},
|
||||
};
|
||||
await runHomeBootstrap(unknownDeps, new AbortController().signal);
|
||||
assert.equal(unknownPreloads, 0, "an unknown people list does not preload the who-step shell");
|
||||
assert.equal(unknownStep, "name", "an unknown people list does not commit 看谁的盘");
|
||||
restoreStorage();
|
||||
});
|
||||
|
||||
|
||||
@@ -201,6 +201,9 @@ test("warm start is refused whenever a piece is missing", () => {
|
||||
assert.equal(resolveHomeWarmStart(input({ boot: { sessions: [], rawRows: [], cursor: null, account: null, signedOut: false, error: "session_list_unavailable" } })), null, "list error");
|
||||
assert.equal(resolveHomeWarmStart(input({ previewRequested: true })), null, "development preview");
|
||||
assert.equal(resolveHomeWarmStart(input({ account: account(USER, { name: "林遥" }) })), null, "onboarding profile");
|
||||
const unknownCatalog = resolveHomeWarmStart(input({ account: account(USER, { name: "林遥" }), subjectCatalog: null }));
|
||||
assert.ok(unknownCatalog, "a people list that has not arrived is not an unfinished profile");
|
||||
assert.equal(unknownCatalog.onboardingStep, "name", "unknown does not commit 看谁的盘");
|
||||
assert.equal(resolveHomeWarmStart(input({ search: "?c=99999999-9999-4999-8999-999999999999" })), null, "a ?c= not in memory needs the cold lookup");
|
||||
writeHomeWarmSnapshot(snapshotFor(USER, { entrySummarySettled: false }));
|
||||
assert.equal(resolveHomeWarmStart(input()), null, "summary never settled");
|
||||
|
||||
@@ -3,9 +3,9 @@ import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { readOnboardingIntake } from "../src/lib/onboarding-intake.ts";
|
||||
import { missingProfileStep } from "../src/lib/home-profile.ts";
|
||||
import { missingProfileStep, onboardingCatalogDecision } from "../src/lib/home-profile.ts";
|
||||
import { emptyProfile, type Profile } from "../src/lib/home-types.ts";
|
||||
import { starterQuestions, type StarterChartSigns } from "../src/lib/starter-questions.ts";
|
||||
import { otherSkySubjectLine, starterQuestions, type StarterChartSigns } from "../src/lib/starter-questions.ts";
|
||||
|
||||
/** Fictional chart. 1990-01-02 and 杭州 are not a real person's birth data. */
|
||||
function chart(name: string, ready: boolean): Profile {
|
||||
@@ -49,6 +49,17 @@ test("missingProfileStep: self complete, only someone else complete, neither, an
|
||||
), "place");
|
||||
assert.equal(missingProfileStep(selfName, [otherPlace], { target: "other", otherId: "other-1" }), "place");
|
||||
assert.equal(missingProfileStep({ ...emptyProfile }, []), "name");
|
||||
assert.equal(onboardingCatalogDecision(selfName, null).known, false);
|
||||
assert.equal(onboardingCatalogDecision(selfName, []).known, true);
|
||||
assert.equal(onboardingCatalogDecision(selfName, []).step, "who");
|
||||
assert.equal(onboardingCatalogDecision(selfName, [otherDone]).step, null);
|
||||
});
|
||||
|
||||
test("someone else's sky subtitle names them and their ascendant, and stays blank without either", () => {
|
||||
assert.equal(otherSkySubjectLine("小舟", "Taurus"), "那一刻,小舟的上升在金牛");
|
||||
assert.equal(otherSkySubjectLine("小舟", "金牛座"), "那一刻,小舟的上升在金牛");
|
||||
assert.equal(otherSkySubjectLine("小舟", null), "");
|
||||
assert.equal(otherSkySubjectLine(" ", "Taurus"), "");
|
||||
});
|
||||
|
||||
test("starter questions use the chart signs already on hand, for both people", () => {
|
||||
|
||||
Reference in New Issue
Block a user