feat: redesign starter homepage

This commit is contained in:
Jesse_Chen
2026-07-22 17:21:31 +08:00
parent c07eab2941
commit d40c16369d
5 changed files with 725 additions and 53 deletions
+18
View File
@@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"@base-ui/react": "^1.6.0",
"@gsap/react": "^2.1.2",
"@mastra/core": "^1.50.1",
"@supabase/ssr": "^0.12.3",
"@supabase/supabase-js": "^2.110.5",
@@ -18,6 +19,7 @@
"clsx": "^2.1.1",
"date-fns": "^4.4.0",
"drizzle-orm": "^0.45.2",
"gsap": "^3.15.0",
"lucide-react": "^1.24.0",
"next": "16.2.10",
"pg": "^8.22.0",
@@ -1286,6 +1288,16 @@
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
"license": "MIT"
},
"node_modules/@gsap/react": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/@gsap/react/-/react-2.1.2.tgz",
"integrity": "sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw==",
"license": "SEE LICENSE AT https://gsap.com/standard-license",
"peerDependencies": {
"gsap": "^3.12.5",
"react": ">=17"
}
},
"node_modules/@hono/node-server": {
"version": "1.19.14",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
@@ -6565,6 +6577,12 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/gsap": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz",
"integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==",
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
},
"node_modules/has-bigints": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+2
View File
@@ -17,6 +17,7 @@
},
"dependencies": {
"@base-ui/react": "^1.6.0",
"@gsap/react": "^2.1.2",
"@mastra/core": "^1.50.1",
"@supabase/ssr": "^0.12.3",
"@supabase/supabase-js": "^2.110.5",
@@ -26,6 +27,7 @@
"clsx": "^2.1.1",
"date-fns": "^4.4.0",
"drizzle-orm": "^0.45.2",
"gsap": "^3.15.0",
"lucide-react": "^1.24.0",
"next": "16.2.10",
"pg": "^8.22.0",
+571
View File
@@ -785,3 +785,574 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background:
.conversational-composer-footer { align-items: stretch; flex-direction: column; }
.conversational-composer-footer button, .conversational-confirmation button, .conversational-original-question button { width: 100%; }
}
/* Starter workbench */
.welcome {
width: min(1040px, 100%);
padding: var(--space-8) 0 var(--space-12);
}
.starter-list {
width: min(1040px, 100%);
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: var(--space-6);
border: 0;
}
.starter-hero,
.product-entrypoints,
.starter-themes {
width: 100%;
min-width: 0;
}
.starter-hero {
position: relative;
min-height: 220px;
overflow: hidden;
display: grid;
grid-template-columns: minmax(0, 1fr) 250px;
align-items: center;
gap: var(--space-8);
padding: var(--space-10);
border: 1px solid color-mix(in srgb, var(--color-action) 14%, var(--color-border));
border-radius: var(--radius-xl);
background: radial-gradient(circle at 82% 36%, rgba(133, 67, 47, .12), transparent 33%), linear-gradient(135deg, #fbfaf7 0%, #f5f1eb 100%);
}
.starter-hero::after {
position: absolute;
inset: 0;
opacity: .2;
pointer-events: none;
background-image: radial-gradient(rgba(29, 29, 31, .24) .45px, transparent .45px);
background-size: 6px 6px;
content: "";
mask-image: linear-gradient(to right, transparent 8%, #000 72%);
}
.starter-hero-copy {
position: relative;
z-index: 1;
max-width: 720px;
}
.starter-greeting {
margin: 0 0 var(--space-3);
color: var(--color-action);
font-size: var(--type-body-sm);
font-weight: 600;
letter-spacing: .02em;
}
.starter-hero h1 {
max-width: 980px;
margin: 0;
color: var(--color-ink);
font-family: var(--font-display);
font-size: clamp(42px, 5.4vw, 68px);
font-weight: 400;
letter-spacing: -.055em;
line-height: .98;
text-wrap: balance;
}
.starter-hero-note {
max-width: 590px;
margin: var(--space-5) 0 0;
color: var(--color-ink-secondary);
font-size: var(--type-body-sm);
line-height: 1.7;
text-wrap: pretty;
}
.starter-celestial-visual {
position: relative;
z-index: 1;
width: 232px;
height: 178px;
justify-self: end;
}
.starter-orbit {
position: absolute;
border: 1px solid color-mix(in srgb, var(--color-action) 34%, transparent);
border-radius: 50%;
}
.starter-orbit-outer {
inset: 8px 10px;
transform: rotate(-12deg);
}
.starter-orbit-inner {
inset: 38px 50px;
border-color: color-mix(in srgb, var(--color-ink-tertiary) 42%, transparent);
transform: rotate(20deg);
}
.starter-orbit-dot {
position: absolute;
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--color-action);
box-shadow: 0 0 0 5px color-mix(in srgb, var(--color-action-soft) 72%, transparent);
}
.starter-orbit-dot-primary {
top: 26px;
right: 37px;
}
.starter-orbit-dot-secondary {
bottom: 31px;
left: 31px;
width: 6px;
height: 6px;
background: var(--color-ink-tertiary);
box-shadow: none;
}
.starter-celestial-mark {
position: absolute;
top: 50%;
left: 50%;
width: 54px;
height: 54px;
border: 1px solid color-mix(in srgb, var(--color-action) 18%, var(--color-border));
border-radius: 50%;
background: color-mix(in srgb, var(--color-canvas) 88%, transparent) url("/jyotish-logo.png") center / 32px 32px no-repeat;
box-shadow: 0 14px 36px -24px rgba(29, 29, 31, .48);
transform: translate(-50%, -50%);
}
.product-entrypoints {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0;
overflow: hidden;
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
background: var(--color-canvas);
}
.product-entrypoints small {
color: var(--color-ink-secondary);
line-height: 1.5;
font-size: var(--type-caption);
}
.daily-starlanguage-card,
.birth-rectification-card {
min-height: 228px;
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
gap: var(--space-6);
padding: var(--space-8);
border: 0;
border-radius: 0;
background: transparent;
}
.birth-rectification-card {
border-left: 1px solid var(--color-border);
}
.product-entrypoint-card {
position: relative;
overflow: hidden;
transition: background-color 180ms ease-out;
}
.product-entrypoint-card::after {
position: absolute;
inset: 0;
opacity: 0;
pointer-events: none;
background: linear-gradient(112deg, transparent 22%, rgba(255, 255, 255, .54) 46%, transparent 68%);
content: "";
transform: translateX(-48%);
transition: opacity 220ms ease-out, transform 520ms var(--ease-out);
}
.product-entrypoint-hitarea {
position: absolute;
z-index: 3;
inset: 0;
width: 100%;
min-height: 0;
padding: 0;
border: 0;
border-radius: inherit;
background: transparent;
cursor: pointer;
}
.product-entrypoint-card > :not(.product-entrypoint-hitarea) {
position: relative;
z-index: 1;
pointer-events: none;
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover) {
border-color: transparent;
background: color-mix(in srgb, var(--color-action-soft) 58%, var(--color-canvas));
transform: none;
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover)::after {
opacity: .65;
transform: translateX(48%);
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover) .product-entrypoint-action {
transform: translateX(3px);
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:focus-visible) {
outline: 3px solid color-mix(in srgb, var(--color-focus) 56%, transparent);
outline-offset: -3px;
}
.product-entrypoint-card:has(.product-entrypoint-hitarea:disabled) {
opacity: .65;
}
.product-entrypoint-hitarea:disabled {
cursor: not-allowed;
}
.product-entrypoint-copy {
max-width: 520px;
}
.product-entrypoint-kicker {
display: block;
margin-bottom: var(--space-4);
color: var(--color-action);
font-size: var(--type-overline);
font-weight: 600;
letter-spacing: .12em;
}
.product-entrypoint-copy h2 {
margin: 0;
color: var(--color-ink);
font-family: var(--font-display);
font-size: clamp(26px, 2.6vw, 34px);
font-weight: 400;
letter-spacing: -.035em;
line-height: 1.05;
}
.product-entrypoint-copy p {
max-width: 460px;
margin: var(--space-3) 0 0;
color: var(--color-ink-secondary);
font-size: var(--type-body-sm);
line-height: 1.65;
text-wrap: pretty;
}
.product-entrypoint-footer {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: var(--space-5);
padding-top: var(--space-5);
border-top: 1px solid color-mix(in srgb, var(--color-border) 76%, transparent);
}
.product-entrypoint-footer small {
min-width: 0;
flex: 1;
}
.product-entrypoint-action {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: flex-end;
gap: var(--space-1);
color: var(--color-action);
font-size: var(--type-caption);
font-weight: 600;
white-space: nowrap;
transition: transform 180ms var(--ease-out);
}
.product-entrypoint-action .starter-arrow {
width: 15px;
height: 15px;
color: currentColor;
}
.starter-themes {
display: grid;
gap: var(--space-4);
}
.starter-section-heading {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-5);
}
.starter-section-heading h2 {
margin: 0;
color: var(--color-ink);
font-family: var(--font-display);
font-size: var(--type-title-lg);
font-weight: 400;
letter-spacing: -.025em;
}
.starter-section-heading p {
margin: 0;
color: var(--color-ink-tertiary);
font-size: var(--type-caption);
}
.starter-theme-accordion {
display: flex;
min-width: 0;
overflow: hidden;
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
background: var(--color-canvas-soft);
}
.starter-theme-card {
position: relative;
min-width: 0;
min-height: 170px;
flex: 1 1 0;
display: grid;
grid-template-columns: minmax(0, 1fr) 20px;
align-items: end;
gap: var(--space-3);
padding: var(--space-5);
border: 0;
border-right: 1px solid var(--color-border);
background: transparent;
cursor: pointer;
text-align: left;
transition: background-color 220ms ease-out, flex-grow 360ms var(--ease-out);
}
.starter-theme-card:last-child {
border-right: 0;
}
.starter-theme-card:not(:disabled):hover,
.starter-theme-card:focus-visible {
flex-grow: 1.62;
background: color-mix(in srgb, var(--color-action-soft) 56%, var(--color-canvas));
}
.starter-theme-accordion:has(.starter-theme-card:not(:disabled):hover) .starter-theme-card:not(:hover),
.starter-theme-accordion:has(.starter-theme-card:focus-visible) .starter-theme-card:not(:focus-visible) {
flex-grow: .78;
}
.starter-theme-card:not(:disabled):hover .starter-arrow,
.starter-theme-card:focus-visible .starter-arrow {
color: var(--color-action);
transform: translate(2px, -2px);
}
.starter-content {
min-width: 0;
display: grid;
gap: var(--space-3);
}
.starter-content b {
color: var(--color-action);
font-size: var(--type-body-sm);
font-weight: 600;
letter-spacing: .04em;
}
.starter-content span {
max-width: 245px;
overflow: visible;
color: var(--color-ink);
font-family: var(--font-display);
font-size: var(--type-title-md);
line-height: 1.38;
text-overflow: clip;
text-wrap: pretty;
white-space: normal;
}
.starter-arrow {
width: 17px;
height: 17px;
color: var(--color-ink-secondary);
transition: color 180ms ease-out, transform 180ms var(--ease-out);
}
.starter-loading {
width: min(1040px, 100%);
margin-left: 0;
padding: var(--space-6);
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
background: var(--color-canvas-soft);
color: var(--color-ink-secondary);
font-size: var(--type-body-sm);
}
.starter-note {
margin: calc(var(--space-2) * -1) 0 0;
color: var(--color-ink-secondary);
line-height: 1.5;
font-size: var(--type-caption);
}
.composer-wrap-starter .composer,
.composer-wrap-starter .composer-footer,
.composer-wrap-starter .composer-suggestions {
width: min(1040px, 100%);
}
@media (min-width: 768px) and (max-width: 900px) {
.starter-hero {
grid-template-columns: minmax(0, 1fr) 180px;
padding: var(--space-8);
}
.starter-celestial-visual {
width: 180px;
height: 150px;
}
.product-entrypoints {
grid-template-columns: 1fr;
}
.birth-rectification-card {
border-top: 1px solid var(--color-border);
border-left: 0;
}
.starter-theme-accordion {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.starter-theme-card {
min-height: 136px;
border-bottom: 1px solid var(--color-border);
}
.starter-theme-card:nth-child(2n) {
border-right: 0;
}
.starter-theme-card:nth-last-child(-n + 2) {
border-bottom: 0;
}
}
@media (max-width: 767px) {
.welcome {
padding: var(--space-4) 0 var(--space-8);
}
.starter-list {
grid-template-columns: 1fr;
grid-template-rows: auto;
gap: var(--space-5);
}
.starter-hero {
min-height: 0;
grid-template-columns: 1fr;
padding: var(--space-8) var(--space-6);
}
.starter-hero h1 {
font-size: clamp(38px, 11vw, 54px);
}
.starter-celestial-visual {
position: absolute;
top: var(--space-4);
right: calc(var(--space-8) * -1);
width: 180px;
height: 150px;
opacity: .34;
}
.product-entrypoints {
grid-template-columns: 1fr;
}
.daily-starlanguage-card,
.birth-rectification-card {
min-height: 210px;
padding: var(--space-6);
}
.birth-rectification-card {
border-top: 1px solid var(--color-border);
border-left: 0;
}
.starter-section-heading {
display: grid;
gap: var(--space-1);
}
.starter-theme-accordion {
display: grid;
}
.starter-theme-card {
min-height: 116px;
border-right: 0;
border-bottom: 1px solid var(--color-border);
}
.starter-theme-card:last-child {
border-bottom: 0;
}
.starter-theme-accordion:has(.starter-theme-card:not(:disabled):hover) .starter-theme-card:not(:hover),
.starter-theme-accordion:has(.starter-theme-card:focus-visible) .starter-theme-card:not(:focus-visible),
.starter-theme-card:not(:disabled):hover,
.starter-theme-card:focus-visible {
flex-grow: 1;
}
.starter-content span {
max-width: 440px;
}
}
@media (max-width: 480px) {
.starter-hero {
padding: var(--space-6) var(--space-5);
}
.starter-hero-note {
margin-top: var(--space-4);
}
.product-entrypoint-footer {
display: grid;
gap: var(--space-4);
}
.product-entrypoint-action {
justify-self: start;
}
.starter-content span {
font-size: var(--type-title-sm);
}
}
+116 -53
View File
@@ -3,6 +3,8 @@
import Link from "next/link";
import dynamic from "next/dynamic";
import { ArrowUp, ArrowUpRight, Sparkles, Square, X } from "lucide-react";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { useEffect, useRef, useState } from "react";
import type { FormEvent, KeyboardEvent } from "react";
import { AppSidebar } from "@/components/app-sidebar";
@@ -71,7 +73,6 @@ import { consultationReportMarkdown } from "@/lib/consultation-report-export";
import {
OnboardingAuthenticationError,
type OnboardingContent,
createOnboardingFallbackGreeting,
createStartGreeting,
isCurrentOnboardingRequest,
onboardingProfileFingerprint,
@@ -90,6 +91,8 @@ import {
} from "@/lib/public-models";
import { createBrowserSupabaseClient } from "@/lib/supabase/client";
gsap.registerPlugin(useGSAP);
const BirthTimeRectification = dynamic(
() => import("@/components/birth-time-rectification").then((module) => module.BirthTimeRectification),
{
@@ -235,6 +238,12 @@ const dailyStarlanguageCards: DailyStarlanguageCard[] = [
{ trend: "适合观察资源流向:时间、注意力、金钱都算。", action: "检查一个正在消耗你的习惯,并给它设上限。", caution: "不要为了短期安心做长期成本高的选择。" },
];
function greetingForHour(hour: number): string {
if (hour < 11) return "早上好";
if (hour < 18) return "下午好";
return "晚上好";
}
const emptyProfile: Profile = {
name: "",
date: "",
@@ -806,6 +815,7 @@ export default function Home() {
const closeButton = useRef<HTMLButtonElement>(null);
const redeemInput = useRef<HTMLInputElement>(null);
const composerInput = useRef<HTMLTextAreaElement>(null);
const starterWorkbench = useRef<HTMLDivElement>(null);
const pendingConsultation = useRef<PendingConsultation | null>(null);
const cancellationRequests = useRef(new Map<string, Promise<void>>());
const cancellationFeedbackRequest = useRef<string | null>(null);
@@ -974,7 +984,6 @@ export default function Home() {
}, [accountId, profile]);
const profileComplete = isProfileComplete(profile);
const birthTimeDisplay = birthTimeDisplayState(profile);
const dailyStarlanguage = dailyStarlanguageCard ?? (profileComplete ? buildDailyStarlanguageCard(profile) : null);
const onboardingPending = profileComplete && !onboarding && !onboardingError;
const currentOnboardingMessage = onboardingJustCompleted
@@ -989,6 +998,14 @@ export default function Home() {
const shouldStreamOnboarding = !profileComplete;
const presetMessageFinished = !shouldStreamOnboarding || presetMessageLength >= currentOnboardingMessage.length;
const onboardingCardReady = presetMessageFinished || birthTimeAssessmentPhase !== null;
const starterSuggestions = themes.map((theme) => onboarding?.suggestions.find((item) => item.theme === theme.id)
?? { theme: theme.id, text: theme.prompt });
const starterHomeVisible = profileComplete
&& presetMessageFinished
&& !onboardingPending
&& !rectificationSurfaceOpen
&& !activeSession?.messages.length;
const daypartGreeting = greetingForHour(new Date().getHours());
useEffect(() => {
const controller = new AbortController();
@@ -2684,6 +2701,39 @@ export default function Home() {
continueRectificationQuestion.current(question);
}, [rectificationInitialTurn, rectificationLoading, rectificationMutationPending]);
useGSAP(() => {
if (!starterHomeVisible || !starterWorkbench.current) return;
const motion = gsap.matchMedia();
motion.add("(prefers-reduced-motion: no-preference)", () => {
const timeline = gsap.timeline({ defaults: { ease: "power3.out" } });
timeline
.from(".starter-hero-copy > *", {
autoAlpha: 0,
duration: 0.62,
stagger: 0.07,
y: 18,
})
.from(".starter-celestial-visual", {
autoAlpha: 0,
duration: 0.76,
scale: 0.94,
}, 0.08)
.from(".product-entrypoint-card", {
autoAlpha: 0,
duration: 0.54,
stagger: 0.08,
y: 20,
}, 0.2)
.from(".starter-theme-card", {
autoAlpha: 0,
duration: 0.46,
stagger: 0.055,
y: 14,
}, 0.34);
});
return () => motion.revert();
}, { dependencies: [activeSession?.id, starterHomeVisible], scope: starterWorkbench });
function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!profileComplete) {
@@ -2839,12 +2889,7 @@ export default function Home() {
{profileComplete && onboardingJustCompleted && selectedBirthPlace(profileDraft) && <OnboardingChatMessage role="user" text={selectedBirthPlace(profileDraft)?.label ?? ""} />}
{profileComplete && onboardingJustCompleted && <OnboardingChatMessage role="assistant" text={currentOnboardingMessage} streaming length={presetMessageLength} />}
</>
) : (
<OnboardingChatMessage role="assistant" text={onboarding?.greeting
|| (onboardingPending
? `${profile.name.trim()},稍等一下,我正在准备几个适合开始的问题。`
: createOnboardingFallbackGreeting(profile.name))} />
)}
) : null}
{!profileComplete && onboardingStep === "birth" && onboardingCardReady && (
<div className="onboarding-card-reveal">
@@ -2903,9 +2948,24 @@ export default function Home() {
{profileComplete && presetMessageFinished && !rectificationSurfaceOpen && (onboardingPending ? (
<div className="starter-loading" role="status"></div>
) : (
<div className="starter-list" aria-label="Jyotisha 推荐的初始问题">
<div className="product-entrypoints" aria-label="常用占星入口">
<article className="daily-starlanguage-card product-entrypoint-card" aria-label="今日星语">
<div className="starter-list starter-workbench" ref={starterWorkbench} aria-label="Jyotisha 推荐的初始问题">
<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>
<p className="starter-hero-note"></p>
</div>
<div className="starter-celestial-visual" aria-hidden="true">
<span className="starter-orbit starter-orbit-outer" />
<span className="starter-orbit starter-orbit-inner" />
<span className="starter-orbit-dot starter-orbit-dot-primary" />
<span className="starter-orbit-dot starter-orbit-dot-secondary" />
<span className="starter-celestial-mark" />
</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"
@@ -2913,20 +2973,17 @@ export default function Home() {
disabled={productEntrypointsDisabled}
onClick={draftDailyStarlanguageQuestion}
/>
<div className="daily-starlanguage-heading">
<span></span>
<div className="product-entrypoint-copy">
<span className="product-entrypoint-kicker"></span>
<h2 id="daily-starlanguage-title"></h2>
<p>{dailyStarlanguage?.trend}</p>
</div>
<dl>
<div><dt></dt><dd>{dailyStarlanguage?.trend}</dd></div>
<div><dt></dt><dd>{dailyStarlanguage?.action}</dd></div>
<div><dt></dt><dd>{dailyStarlanguage?.caution}</dd></div>
</dl>
<div className="product-entrypoint-footer">
<small></small>
<small>{dailyStarlanguage?.action}</small>
<span className="product-entrypoint-action" aria-hidden="true"> <ArrowUpRight className="starter-arrow" /></span>
</div>
</article>
<article className="birth-rectification-card product-entrypoint-card" aria-label="生时校正">
<article className="birth-rectification-card product-entrypoint-card" aria-labelledby="birth-rectification-title">
<button
className="product-entrypoint-hitarea"
type="button"
@@ -2934,44 +2991,50 @@ export default function Home() {
disabled={productEntrypointsDisabled || rectificationLoading || rectificationMutationPending}
onClick={() => void openBirthTimeRectification()}
/>
<div className="daily-starlanguage-heading">
<span></span>
<div className="product-entrypoint-copy">
<span className="product-entrypoint-kicker"></span>
<h2 id="birth-rectification-title"></h2>
<p>{rectificationCardAction === "resume"
? "你有一段未完成的校正记录,可以从上次的位置继续。"
: "不确定准确出生时间时,可通过已经发生的人生事件逐步缩小范围。"}</p>
</div>
<dl>
{birthTimeDisplay ? (
<>
<div><dt>{birthTimeDisplay.kind === "candidate" ? "待验证候选时间" : "当前排盘时间"}</dt><dd>{birthTimeDisplay.activeTime}</dd></div>
<div><dt></dt><dd>{birthTimeDisplay.kind === "candidate" ? `未确认;${birthTimeConsultationOptionsCopy(profile)}` : "已确认"}</dd></div>
<div><dt></dt><dd>{birthTimeDisplay.reportedLabel}</dd></div>
</>
) : (
<>
<div><dt></dt><dd>{formatBirthMoment(profile)}</dd></div>
<div><dt></dt><dd>{rectificationCardAction === "resume" ? "已有进度,可继续" : "尚未建立进行中的校正案例"}</dd></div>
<div><dt></dt><dd> + </dd></div>
</>
)}
</dl>
<div className="product-entrypoint-footer">
<small>{rectificationCardAction === "resume"
? account.rectificationCase?.preservesActiveTime
? "新校正进行中;旧确认时间继续有效,继续同一案例不重复收费。"
: "继续同一案例不重复收费。"
: `固定费用 ${account.rectificationPriceCredits} 点;首轮有效分析生成后收取。`}</small>
? "继续同一案例,不重复收费。"
: `固定费用 ${account.rectificationPriceCredits} 点;首轮有效分析后收取。`}</small>
<span className="product-entrypoint-action" aria-hidden="true">{rectificationCardLabel} <ArrowUpRight className="starter-arrow" /></span>
</div>
</article>
</div>
{(onboarding?.suggestions ?? themes.map((item) => ({ theme: item.id, text: item.prompt }))).map((item) => {
const theme = themes.find((candidate) => candidate.id === item.theme);
return (
<button key={`${item.theme}-${item.text}`} type="button" disabled={!hydrated || Boolean(pendingSessionId) || cancellationPending || !account || !modelCatalog} onClick={() => chooseSuggestedQuestion(item.text, item.theme)}>
<span className="starter-content"><b>{theme?.label || "开始"}</b><span>{item.text}</span>{theme && <small>{theme.evidencePreview.join(" / ")} · {theme.claimBoundary}</small>}</span>
<ArrowUpRight className="starter-arrow" aria-hidden="true" />
</button>
);
})}
{onboardingError && <p className="starter-note">Agent </p>}
</section>
<section className="starter-themes" aria-labelledby="starter-themes-heading">
<div className="starter-section-heading">
<h2 id="starter-themes-heading"></h2>
<p></p>
</div>
<div className="starter-theme-accordion">
{starterSuggestions.map((item) => {
const theme = themes.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={!hydrated || Boolean(pendingSessionId) || cancellationPending || !account || !modelCatalog}
onClick={() => chooseSuggestedQuestion(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>
))}
<div ref={conversationEnd} />
@@ -3000,7 +3063,7 @@ export default function Home() {
)}
</div>
{!rectificationSurfaceOpen && <div className="composer-wrap">
{!rectificationSurfaceOpen && <div className={`composer-wrap ${starterHomeVisible ? "composer-wrap-starter" : ""}`}>
{activeSuggestions.length > 0 && (
<div className="composer-suggestions" aria-label="推荐继续提问">
{activeSuggestions.map((question) => (
@@ -245,3 +245,21 @@ test("homepage entrypoints use two whole-card native actions", () => {
assert.equal(wholeCardActions.length, 2);
assert.doesNotMatch(source, /className="daily-starlanguage-heading">[\s\S]{0,180}<button/);
});
test("starter homepage stays editorial and hides technical chart parameters", () => {
const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const start = source.indexOf('<div className="starter-list starter-workbench"');
const end = source.indexOf("{onboardingError &&", start);
const starterHomepage = source.slice(start, end);
assert.ok(start >= 0 && end > start);
assert.match(starterHomepage, /className="starter-hero"/);
assert.match(starterHomepage, /className="starter-theme-accordion"/);
assert.match(starterHomepage, /starterSuggestions\.map/);
assert.doesNotMatch(starterHomepage, /evidencePreview|birthTimeDisplay|Vimshottari|D1|D9/);
assert.match(source, /const starterSuggestions = themes\.map/);
assert.match(source, /composer-wrap-starter/);
assert.match(styles, /\/\* Starter workbench \*\/[\s\S]*?\.starter-list \{[\s\S]*?grid-template-columns: minmax\(0, 1fr\);/);
assert.match(styles, /\.starter-hero,[\s\S]*?\.product-entrypoints,[\s\S]*?\.starter-themes \{[\s\S]*?width: 100%;/);
});