refactor(chat): extract home helpers, chart library, and starter surfaces
page.tsx still owns the chat main chain, but the first product surfaces now live in their own modules so later splits can land without editing the 4k-line Home. Source-lock tests follow the moved tokens; the orphan user-data contract is aligned and added to the quick gate. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { BirthPlacePicker } from "@/components/birth-place-picker";
|
||||
import type { Profile } from "@/lib/home-types";
|
||||
|
||||
export function BirthLocationFields({ value, onChange }: { value: Profile; onChange: (profile: Profile) => void }) {
|
||||
return (
|
||||
<fieldset className="location-fieldset">
|
||||
<legend>出生地点</legend>
|
||||
<BirthPlacePicker
|
||||
value={{
|
||||
countryCode: value.countryCode,
|
||||
provinceCode: value.provinceCode,
|
||||
cityCode: value.cityCode,
|
||||
districtCode: value.districtCode,
|
||||
timezoneId: value.timezoneId,
|
||||
timezoneOffset: value.timezoneOffset,
|
||||
}}
|
||||
birthDate={value.date}
|
||||
birthTime={value.reportedTime || value.time}
|
||||
onChange={(location) => onChange({
|
||||
...value,
|
||||
countryCode: location?.countryCode || "CN",
|
||||
provinceCode: location?.provinceCode || "",
|
||||
cityCode: location?.cityCode || "",
|
||||
districtCode: location?.districtCode || "",
|
||||
birthPlaceLabel: location?.label || "",
|
||||
birthPlaceType: location?.placeType || "",
|
||||
birthPlaceProvider: location?.provider || "",
|
||||
birthPlaceProviderId: location?.providerPlaceId || "",
|
||||
timezoneId: location?.timezoneId || "",
|
||||
timezoneSource: location ? "iana_historical" : "",
|
||||
latitude: location?.latitude ?? null,
|
||||
longitude: location?.longitude ?? null,
|
||||
timezoneOffset: location?.timezoneOffset ?? null,
|
||||
})}
|
||||
/>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
"use client";
|
||||
|
||||
import type { Dispatch, FormEvent, SetStateAction } from "react";
|
||||
import { ProfileFields } from "@/components/profile-fields";
|
||||
import { activeChartStorageKey, deleteCloudChartProfile, saveCloudChartProfile, updateCloudChartProfile } from "@/lib/home-cloud-sync";
|
||||
import {
|
||||
chartRelationshipLabel,
|
||||
formatChartUpdatedAt,
|
||||
missingOtherProfileStep,
|
||||
profileBirthTimeLabel,
|
||||
profileBirthTimeStatusLabel,
|
||||
profilePlaceLabel,
|
||||
readProfile,
|
||||
upsertSelfChart,
|
||||
} from "@/lib/home-profile";
|
||||
import { emptyProfile, timestamp, type Account, type ChartLibraryRecord, type ChartRelationship, type Profile, type SynastryRelationshipType, type SynastryReportCard } from "@/lib/home-types";
|
||||
|
||||
export type ChartLibraryPanelProps = {
|
||||
readonly account: Account | null;
|
||||
readonly accountId: string | undefined;
|
||||
readonly chartLibrary: ChartLibraryRecord[];
|
||||
readonly setChartLibrary: Dispatch<SetStateAction<ChartLibraryRecord[]>>;
|
||||
readonly profile: Profile;
|
||||
readonly profileDraft: Profile;
|
||||
readonly setProfileDraft: Dispatch<SetStateAction<Profile>>;
|
||||
readonly setProfile: Dispatch<SetStateAction<Profile>>;
|
||||
readonly profileSaving: boolean;
|
||||
readonly profileNotice: string;
|
||||
readonly setProfileNotice: Dispatch<SetStateAction<string>>;
|
||||
readonly setAccountError: Dispatch<SetStateAction<string>>;
|
||||
readonly editingSelfChart: boolean;
|
||||
readonly setEditingSelfChart: Dispatch<SetStateAction<boolean>>;
|
||||
readonly otherProfileDraft: Profile;
|
||||
readonly setOtherProfileDraft: Dispatch<SetStateAction<Profile>>;
|
||||
readonly otherChartRelationship: Exclude<ChartRelationship, "self">;
|
||||
readonly setOtherChartRelationship: Dispatch<SetStateAction<Exclude<ChartRelationship, "self">>>;
|
||||
readonly editingChartId: string | null;
|
||||
readonly setEditingChartId: Dispatch<SetStateAction<string | null>>;
|
||||
readonly synastryRelationshipType: SynastryRelationshipType;
|
||||
readonly setSynastryRelationshipType: Dispatch<SetStateAction<SynastryRelationshipType>>;
|
||||
readonly synastryPendingId: string | null;
|
||||
readonly synastryReportCard: SynastryReportCard | null;
|
||||
readonly setSynastryReportCard: Dispatch<SetStateAction<SynastryReportCard | null>>;
|
||||
readonly synastryHistory: SynastryReportCard[];
|
||||
readonly activeChartId: string;
|
||||
readonly setActiveChartId: Dispatch<SetStateAction<string>>;
|
||||
readonly saveProfile: (event: FormEvent<HTMLFormElement>) => void;
|
||||
readonly draftSynastryQuestionFromChart: (record: ChartLibraryRecord, relationshipType: SynastryRelationshipType) => void;
|
||||
};
|
||||
|
||||
export function ChartLibraryPanel({
|
||||
account,
|
||||
accountId,
|
||||
chartLibrary,
|
||||
setChartLibrary,
|
||||
profile,
|
||||
profileDraft,
|
||||
setProfileDraft,
|
||||
setProfile,
|
||||
profileSaving,
|
||||
profileNotice,
|
||||
setProfileNotice,
|
||||
setAccountError,
|
||||
editingSelfChart,
|
||||
setEditingSelfChart,
|
||||
otherProfileDraft,
|
||||
setOtherProfileDraft,
|
||||
otherChartRelationship,
|
||||
setOtherChartRelationship,
|
||||
editingChartId,
|
||||
setEditingChartId,
|
||||
synastryRelationshipType,
|
||||
setSynastryRelationshipType,
|
||||
synastryPendingId,
|
||||
synastryReportCard,
|
||||
setSynastryReportCard,
|
||||
synastryHistory,
|
||||
activeChartId,
|
||||
setActiveChartId,
|
||||
saveProfile,
|
||||
draftSynastryQuestionFromChart,
|
||||
}: ChartLibraryPanelProps) {
|
||||
async function saveOtherChart(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const nextProfile = { ...otherProfileDraft, name: otherProfileDraft.name.trim(), chartRelationship: otherChartRelationship };
|
||||
|
||||
if (missingOtherProfileStep(nextProfile)) {
|
||||
setAccountError("请补全其他星盘的称呼、出生时间和出生地点。");
|
||||
return;
|
||||
}
|
||||
if (!accountId) return;
|
||||
const record: ChartLibraryRecord = {
|
||||
id: editingChartId || globalThis.crypto.randomUUID(),
|
||||
role: "other",
|
||||
profile: nextProfile,
|
||||
relationship: otherChartRelationship,
|
||||
updatedAt: timestamp(),
|
||||
};
|
||||
try {
|
||||
const saved = editingChartId
|
||||
? await updateCloudChartProfile(record)
|
||||
: await saveCloudChartProfile(record);
|
||||
const selfProfile = account ? readProfile(account.profile) : profile;
|
||||
setChartLibrary((current) => {
|
||||
const others = editingChartId
|
||||
? current.map((item) => item.id === saved.id ? saved : item)
|
||||
: [...current, saved];
|
||||
return upsertSelfChart(others, selfProfile);
|
||||
});
|
||||
setOtherProfileDraft(emptyProfile);
|
||||
setOtherChartRelationship("other");
|
||||
setEditingChartId(null);
|
||||
setAccountError("");
|
||||
setProfileNotice(editingChartId ? "已更新其他人的星盘资料。" : "已保存到云端星盘库。请选择关系类型后点击“用于合盘”。");
|
||||
} catch {
|
||||
setProfileNotice("保存失败,请重试");
|
||||
setAccountError("");
|
||||
}
|
||||
}
|
||||
|
||||
function editOtherChart(record: ChartLibraryRecord) {
|
||||
if (record.role !== "other") return;
|
||||
setOtherProfileDraft(record.profile);
|
||||
setOtherChartRelationship(record.relationship === "self" ? "other" : record.relationship);
|
||||
setEditingChartId(record.id);
|
||||
setAccountError("");
|
||||
setProfileNotice("");
|
||||
}
|
||||
|
||||
async function deleteOtherChart(recordId: string) {
|
||||
if (!accountId || !window.confirm("确定删除这份其他人的星盘资料吗?删除后无法恢复。")) return;
|
||||
try {
|
||||
await deleteCloudChartProfile(recordId);
|
||||
} catch {
|
||||
setProfileNotice("删除失败,请重试");
|
||||
setAccountError("");
|
||||
return;
|
||||
}
|
||||
setChartLibrary((current) => {
|
||||
const next = current.filter((record) => record.id !== recordId || record.role === "self");
|
||||
if (activeChartId === recordId) {
|
||||
setActiveChartId("self");
|
||||
localStorage.setItem(activeChartStorageKey(accountId), "self");
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setAccountError("");
|
||||
setProfileNotice("已从云端星盘库删除。");
|
||||
}
|
||||
|
||||
function makeDefaultChart(record: ChartLibraryRecord) {
|
||||
if (record.role !== "other" || profileSaving || !accountId) return;
|
||||
setActiveChartId(record.id);
|
||||
localStorage.setItem(activeChartStorageKey(accountId), record.id);
|
||||
setProfile(record.profile);
|
||||
setProfileNotice("已设为当前使用资料,账户本人的出生资料未被覆盖。");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chart-library-panel" aria-label="星盘资料管理">
|
||||
<div className="chart-library-group">
|
||||
<div className="chart-library-group-heading">
|
||||
<div><b>我的星盘</b><small>当前账号的默认资料</small></div>
|
||||
<span className="chart-library-count">{chartLibrary.filter((record) => record.role === "self").length}</span>
|
||||
</div>
|
||||
{chartLibrary.filter((record) => record.role === "self").map((record) => (
|
||||
<article className="chart-library-item" key={record.id}>
|
||||
<div className="chart-library-item-main">
|
||||
<div className="chart-library-item-title"><strong>{record.profile.name || "未命名"}</strong><span className="chart-role-badge is-self">本人</span><span className="chart-default-badge">当前默认</span></div>
|
||||
<small>{record.profile.date || "出生日期待补全"} · {profileBirthTimeLabel(record.profile)} · {profilePlaceLabel(record.profile)}</small>
|
||||
<small>{profileBirthTimeStatusLabel(record.profile)} · {formatChartUpdatedAt(record.updatedAt)}</small>
|
||||
</div>
|
||||
<div className="chart-library-actions">
|
||||
<button className="button-secondary" type="button" onClick={() => { setProfileDraft(record.profile); setEditingSelfChart(true); }} disabled={profileSaving || editingChartId !== null}>编辑本人资料</button>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
{editingSelfChart && (
|
||||
<form className="profile-form chart-library-form" onSubmit={saveProfile}>
|
||||
<div className="section-heading"><b>编辑本人星盘</b><small>这份资料会用于默认解盘与新对话。</small></div>
|
||||
<ProfileFields value={profileDraft} onChange={setProfileDraft} nameInputId="self-profile-name" />
|
||||
{profileNotice && <p className="form-success" role="status">{profileNotice}</p>}
|
||||
<div className="dialog-actions">
|
||||
<button className="button-secondary" type="button" onClick={() => { setEditingSelfChart(false); setProfileDraft(profile); }}>取消编辑</button>
|
||||
<button className="button-primary save-profile" type="submit" disabled={!account || profileSaving}>{profileSaving ? "保存中" : "保存本人资料"}</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{chartLibrary.filter((record) => record.role === "self").length === 0 && <p className="empty-library-copy">请先在个人资料中补全你的出生资料。</p>}
|
||||
</div>
|
||||
<div className="chart-library-group">
|
||||
<div className="chart-library-group-heading">
|
||||
<div><b>其他人的星盘</b><small>亲友、伴侣或客户资料</small></div>
|
||||
<span className="chart-library-count">{chartLibrary.filter((record) => record.role === "other").length}</span>
|
||||
</div>
|
||||
{chartLibrary.filter((record) => record.role === "other").length === 0 && <p className="empty-library-copy">还没有其他星盘,先添加一份资料。</p>}
|
||||
{chartLibrary.filter((record) => record.role === "other").map((record) => (
|
||||
<article className="chart-library-item" key={record.id}>
|
||||
<div className="chart-library-item-main">
|
||||
<div className="chart-library-item-title"><strong>{record.profile.name || "未命名"}</strong><span className="chart-role-badge">{chartRelationshipLabel(record.relationship)}</span></div>
|
||||
<small>{record.profile.date || "出生日期待补全"} · {profileBirthTimeLabel(record.profile)} · {profilePlaceLabel(record.profile)}</small>
|
||||
<small>{profileBirthTimeStatusLabel(record.profile)} · {formatChartUpdatedAt(record.updatedAt)}</small>
|
||||
</div>
|
||||
<div className="chart-library-actions">
|
||||
<select aria-label={`${record.profile.name || "其他人"}的关系类型`} value={synastryRelationshipType} onChange={(event) => setSynastryRelationshipType(event.target.value as SynastryRelationshipType)} disabled={synastryPendingId !== null}>
|
||||
<option value="romance">婚恋</option>
|
||||
<option value="business">商业合作</option>
|
||||
<option value="family">亲友/家庭</option>
|
||||
<option value="general">其他关系</option>
|
||||
</select>
|
||||
<button className="button-secondary" type="button" onClick={() => void draftSynastryQuestionFromChart(record, synastryRelationshipType)} disabled={synastryPendingId !== null}>{synastryPendingId === record.id ? "正在计算合盘..." : "用于合盘"}</button>
|
||||
<button className="button-secondary" type="button" onClick={() => editOtherChart(record)} disabled={profileSaving || editingChartId !== null}>编辑</button>
|
||||
<button className="button-secondary" type="button" onClick={() => makeDefaultChart(record)} disabled={profileSaving}>设为默认</button>
|
||||
<button className="button-secondary" type="button" onClick={() => void deleteOtherChart(record.id)}>删除</button>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
{synastryReportCard && (
|
||||
<article className="synastry-report-card" aria-label="合盘结果摘要">
|
||||
<div>
|
||||
<span>合盘结果摘要</span>
|
||||
<strong>{synastryReportCard.partnerName}</strong>
|
||||
<small>Ashtakoot {synastryReportCard.score ?? "?"}/{synastryReportCard.maxScore ?? "?"} · {synastryReportCard.assessment || synastryReportCard.scoreBand || "待解释"}</small>
|
||||
</div>
|
||||
{synastryReportCard.headline && <p>{synastryReportCard.headline}</p>}
|
||||
<details>
|
||||
<summary>查看证据</summary>
|
||||
<ul>
|
||||
{(synastryReportCard.strengths || []).map((item) => <li key={item}>{item}</li>)}
|
||||
{(synastryReportCard.risks || []).map((item) => <li key={item}>{item}</li>)}
|
||||
</ul>
|
||||
<small>下一步证据:{(synastryReportCard.nextEvidence || []).join(" / ") || "双方 Dasha / UL-DK / D9 7宫"}</small>
|
||||
</details>
|
||||
</article>
|
||||
)}
|
||||
{synastryHistory.length > 0 && (
|
||||
<div className="synastry-history-list" aria-label="合盘历史">
|
||||
<b>合盘历史</b>
|
||||
{synastryHistory.slice(0, 5).map((item) => (
|
||||
<button key={item.id} type="button" className="synastry-history-item" onClick={() => setSynastryReportCard(item)}>
|
||||
<span>{item.partnerName}</span>
|
||||
<small>Ashtakoot {item.score ?? "?"}/{item.maxScore ?? "?"} · {item.assessment || item.scoreBand || "待解释"}</small>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<form className="profile-form chart-library-form" onSubmit={saveOtherChart}>
|
||||
<div className="section-heading"><b>{editingChartId ? "编辑其他人的星盘" : "添加其他人的星盘"}</b><small>用于合盘、亲友盘或客户盘。</small></div>
|
||||
<label>
|
||||
<span>关系</span>
|
||||
<select value={otherChartRelationship} onChange={(event) => setOtherChartRelationship(event.target.value as Exclude<ChartRelationship, "self">)}>
|
||||
<option value="partner">伴侣</option>
|
||||
<option value="family">家人</option>
|
||||
<option value="friend">朋友</option>
|
||||
<option value="client">客户</option>
|
||||
<option value="other">其他</option>
|
||||
</select>
|
||||
</label>
|
||||
<ProfileFields value={otherProfileDraft} onChange={setOtherProfileDraft} nameInputId="other-profile-name" />
|
||||
<div className="dialog-actions">
|
||||
{editingChartId && <button className="button-secondary" type="button" onClick={() => { setEditingChartId(null); setOtherProfileDraft(emptyProfile); setOtherChartRelationship("other"); }}>取消编辑</button>}
|
||||
<button className="button-primary save-profile" type="submit" disabled={!account || profileSaving}>{profileSaving ? "保存中" : editingChartId ? "保存修改" : "添加到星盘库"}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { ChatMessageContent } from "@/components/chat-message-content";
|
||||
import { AgentAvatar } from "@/components/chat-message-row";
|
||||
import { protectOnboardingPhrases } from "@/lib/onboarding-copy";
|
||||
import type { Message } from "@/lib/home-types";
|
||||
|
||||
export function OnboardingChatMessage({ role, text, streaming = false, length = text.length, phraseSafe = false }: { role: Message["role"]; text: string; streaming?: boolean; length?: number; phraseSafe?: boolean }) {
|
||||
const visibleText = streaming ? text.slice(0, length) : text;
|
||||
const protectedVisibleText = protectOnboardingPhrases(visibleText);
|
||||
return (
|
||||
<article className={`message message-${role} onboarding-message${phraseSafe ? " is-phrase-safe" : ""}`} aria-label={role === "assistant" ? "Jyotisha" : "你"}>
|
||||
{role === "assistant" && <AgentAvatar />}
|
||||
<div className="message-content">
|
||||
<div className="message-bubble">
|
||||
{role === "assistant" ? (
|
||||
streaming ? (
|
||||
<>
|
||||
<div className={`onboarding-stream ${length >= text.length ? "is-complete" : ""}`} aria-hidden="true"><ChatMessageContent text={protectedVisibleText} /></div>
|
||||
<span className="sr-only" role="status" aria-live="polite" aria-atomic="true">{length >= text.length ? text : ""}</span>
|
||||
</>
|
||||
) : <ChatMessageContent text={protectedVisibleText} />
|
||||
) : <p>{protectedVisibleText}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { BirthTimeIntakeFields } from "@/components/birth-time-intake";
|
||||
import { BirthLocationFields } from "@/components/birth-location-fields";
|
||||
import { CharacterRemaining } from "@/components/character-remaining";
|
||||
import { characterRemainingVisible } from "@/lib/character-remaining";
|
||||
import { applyBirthTimeDraftPatch } from "@/lib/birth-time-intake-model";
|
||||
import { invalidateCandidateAfterLocationChange } from "@/lib/home-profile";
|
||||
import type { Profile } from "@/lib/home-types";
|
||||
|
||||
export function ProfileFields({ value, onChange, nameInputId }: { value: Profile; onChange: (profile: Profile) => void; nameInputId?: string }) {
|
||||
const nameRemainingId = `${nameInputId ?? "profile-name"}-remaining`;
|
||||
return (
|
||||
<>
|
||||
<label>
|
||||
<span className="character-remaining-row">
|
||||
如何称呼你
|
||||
<CharacterRemaining id={nameRemainingId} length={value.name.length} maxLength={80} />
|
||||
</span>
|
||||
<input
|
||||
id={nameInputId}
|
||||
required
|
||||
autoComplete="name"
|
||||
maxLength={80}
|
||||
placeholder="例如:林遥"
|
||||
value={value.name}
|
||||
aria-describedby={characterRemainingVisible(value.name.length, 80) ? nameRemainingId : undefined}
|
||||
onChange={(event) => onChange({ ...value, name: event.target.value })}
|
||||
/>
|
||||
</label>
|
||||
<BirthTimeIntakeFields value={value} onPatch={(patch) => onChange(applyBirthTimeDraftPatch(value, patch))} />
|
||||
<BirthLocationFields
|
||||
value={value}
|
||||
onChange={(next) => onChange(invalidateCandidateAfterLocationChange(value, next))}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import type { DailyStarlanguageState, Theme } from "@/lib/home-types";
|
||||
import type { RectificationEntrySummary } from "@/lib/rectification-entry";
|
||||
|
||||
export type StarterHomeTheme = {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly prompt: string;
|
||||
};
|
||||
|
||||
export type StarterHomeSuggestion = {
|
||||
readonly theme: Theme;
|
||||
readonly text: string;
|
||||
};
|
||||
|
||||
export type StarterHomeProps = {
|
||||
readonly starterGreeting: { readonly salutation: string; readonly question: string };
|
||||
readonly dailyStarlanguage: DailyStarlanguageState;
|
||||
readonly natalMinuteAvailable: boolean;
|
||||
readonly dailyStarlanguageQuestion: string;
|
||||
readonly dailyStarlanguageTrend: string;
|
||||
readonly dailyStarlanguageAction: string;
|
||||
readonly dailyStarlanguageBusy: boolean;
|
||||
readonly productEntrypointsDisabled: boolean;
|
||||
readonly startDailyStarlanguageConsultation: () => void;
|
||||
readonly rectificationCardLabel: string;
|
||||
readonly rectificationCardAction: string;
|
||||
readonly rectificationLoading: boolean;
|
||||
readonly rectificationMutationPending: boolean;
|
||||
readonly openRectificationFromHomepage: () => void;
|
||||
readonly rectificationEntrySummary: RectificationEntrySummary | null;
|
||||
readonly rectificationError: string;
|
||||
readonly rectificationSurfaceOpen: boolean;
|
||||
readonly rectificationErrorMessage: string;
|
||||
readonly starterThemes: readonly StarterHomeTheme[];
|
||||
readonly starterSuggestions: readonly StarterHomeSuggestion[];
|
||||
readonly startSuggestedConsultation: (text: string, theme: Theme) => void;
|
||||
readonly onboardingError: string;
|
||||
};
|
||||
|
||||
export function StarterHome({
|
||||
starterGreeting,
|
||||
natalMinuteAvailable,
|
||||
dailyStarlanguage,
|
||||
dailyStarlanguageQuestion,
|
||||
dailyStarlanguageTrend,
|
||||
dailyStarlanguageAction,
|
||||
dailyStarlanguageBusy,
|
||||
productEntrypointsDisabled,
|
||||
startDailyStarlanguageConsultation,
|
||||
rectificationCardLabel,
|
||||
rectificationCardAction,
|
||||
rectificationLoading,
|
||||
rectificationMutationPending,
|
||||
openRectificationFromHomepage,
|
||||
rectificationEntrySummary,
|
||||
rectificationError,
|
||||
rectificationSurfaceOpen,
|
||||
rectificationErrorMessage,
|
||||
starterThemes,
|
||||
starterSuggestions,
|
||||
startSuggestedConsultation,
|
||||
onboardingError,
|
||||
}: StarterHomeProps) {
|
||||
return (
|
||||
<div className="starter-list starter-workbench" aria-label="Jyotisha 推荐的初始问题">
|
||||
<section className="starter-hero" aria-labelledby="starter-heading">
|
||||
<div className="starter-hero-copy">
|
||||
<p className="starter-greeting">{starterGreeting.salutation}</p>
|
||||
<h1 id="starter-heading">{starterGreeting.question}</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="product-entrypoints" aria-label="常用占星入口">
|
||||
<article className="daily-starlanguage-card product-entrypoint-card" aria-labelledby="daily-starlanguage-title">
|
||||
<button
|
||||
className="product-entrypoint-hitarea"
|
||||
type="button"
|
||||
aria-label={natalMinuteAvailable ? dailyStarlanguageQuestion : "查看今日运势"}
|
||||
disabled={productEntrypointsDisabled}
|
||||
onClick={startDailyStarlanguageConsultation}
|
||||
/>
|
||||
<div className="product-entrypoint-copy">
|
||||
<h2 id="daily-starlanguage-title">{natalMinuteAvailable ? "今日星语" : "每日运势"}</h2>
|
||||
<p
|
||||
aria-busy={dailyStarlanguageBusy}
|
||||
role={natalMinuteAvailable && dailyStarlanguage.kind !== "ready" ? "status" : undefined}
|
||||
>{natalMinuteAvailable ? dailyStarlanguageTrend : "看看今天的整体节奏、适合推进的事和需要留意的地方。"}</p>
|
||||
</div>
|
||||
<div className="product-entrypoint-footer">
|
||||
{natalMinuteAvailable
|
||||
? (dailyStarlanguageAction ? <small>{dailyStarlanguageAction}</small> : null)
|
||||
: <small>不支持的个人判断会明确说明,不会补造出生时间。</small>}
|
||||
<span className="product-entrypoint-action" aria-hidden="true">{natalMinuteAvailable ? dailyStarlanguageQuestion : "查看今日运势"} <ArrowUpRight className="starter-arrow" /></span>
|
||||
</div>
|
||||
</article>
|
||||
<article className="birth-rectification-card product-entrypoint-card" aria-labelledby="birth-rectification-title">
|
||||
<button
|
||||
className="product-entrypoint-hitarea"
|
||||
type="button"
|
||||
aria-label={rectificationCardLabel}
|
||||
disabled={productEntrypointsDisabled || rectificationLoading || rectificationMutationPending}
|
||||
onClick={() => void openRectificationFromHomepage()}
|
||||
/>
|
||||
<div className="product-entrypoint-copy">
|
||||
<h2 id="birth-rectification-title">生时校正</h2>
|
||||
<p>{rectificationEntrySummary?.hasResumableCase
|
||||
? "新建一段独立校正;未完成的记录仍可从左侧历史会话继续。"
|
||||
: rectificationCardAction === "restart"
|
||||
? "上一次校正已经完成,可以基于最新资料再次校正。"
|
||||
: "不确定准确出生时间时,可通过已经发生的人生事件逐步缩小范围。"}</p>
|
||||
</div>
|
||||
<div className="product-entrypoint-footer">
|
||||
<span className="product-entrypoint-action" aria-hidden="true">{rectificationCardLabel} <ArrowUpRight className="starter-arrow" /></span>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
{rectificationError && !rectificationSurfaceOpen && (
|
||||
<p className="form-error rectification-entry-error" role="alert">
|
||||
{rectificationErrorMessage}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<section className="starter-themes" aria-labelledby="starter-themes-heading">
|
||||
<div className="starter-section-heading">
|
||||
<h2 id="starter-themes-heading">从一个主题开始</h2>
|
||||
<p>{natalMinuteAvailable
|
||||
? "选择一个你现在想解决的问题。"
|
||||
: "选择一个你现在想解决的问题;出生时间不足以支持的部分,我会明确说明,不会补造具体分钟。"}</p>
|
||||
</div>
|
||||
<div className="starter-theme-accordion">
|
||||
{starterSuggestions.map((item) => {
|
||||
const theme = starterThemes.find((candidate) => candidate.id === item.theme);
|
||||
return (
|
||||
<button
|
||||
className="starter-theme-card"
|
||||
key={`${item.theme}-${item.text}`}
|
||||
type="button"
|
||||
aria-label={`${theme?.label || "开始"}:${item.text}`}
|
||||
disabled={productEntrypointsDisabled}
|
||||
onClick={() => void startSuggestedConsultation(item.text, item.theme)}
|
||||
>
|
||||
<span className="starter-content">
|
||||
<b>{theme?.label || "开始"}</b>
|
||||
<span>{item.text}</span>
|
||||
</span>
|
||||
<ArrowUpRight className="starter-arrow" aria-hidden="true" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
{onboardingError && <p className="starter-note">个性化问题暂时不可用,已显示安全的默认问题。</p>}
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user