开场语从「称呼行 + 追问句」收成 claude.ai 式的单行问候:时段池、 星期池、回访池按顺序拼成一个数组,按 variantSelection 确定性取一条; 名字为空时跳过带称呼的条目。副标题行与 .starter-salutation 下线, h1 降到 clamp(24px, 2.8vw, 30px) / line-height 1.25。追问改由输入框 占位符承担。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
126 lines
5.0 KiB
TypeScript
126 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import { Clock3, Sparkles } from "lucide-react";
|
|
import type { DailyStarlanguageState } from "@/lib/home-types";
|
|
import type { RectificationEntrySummary } from "@/lib/rectification-entry";
|
|
|
|
/**
|
|
* The empty state is split across two slots on purpose.
|
|
*
|
|
* `StarterGreeting` sits in the conversation area, bottom-aligned, so it lands
|
|
* directly above the composer. `StarterEntries` sits inside `.composer-wrap`,
|
|
* below the composer. The composer itself never moves: it keeps the one DOM
|
|
* position it has in a live session, so nothing remounts when the first message
|
|
* arrives and an in-flight draft, focus or queued card survives the transition.
|
|
*
|
|
* Before this, the empty state was a bordered hero card plus two 132px product
|
|
* cards, and the composer sat under roughly 800px of content — the primary
|
|
* action was the last thing on the page.
|
|
*/
|
|
|
|
export type StarterGreetingProps = {
|
|
readonly starterGreeting: { readonly text: string };
|
|
};
|
|
|
|
/* One line. The salutation/question pair it replaces asked the user's question
|
|
for them; the composer placeholder asks now. */
|
|
export function StarterGreeting({ starterGreeting }: StarterGreetingProps) {
|
|
return (
|
|
<section className="starter-greeting-block" aria-labelledby="starter-heading">
|
|
<h1 id="starter-heading">{starterGreeting.text}</h1>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
export type StarterEntriesProps = {
|
|
readonly dailyStarlanguage: DailyStarlanguageState;
|
|
readonly natalMinuteAvailable: boolean;
|
|
readonly dailyStarlanguageQuestion: string;
|
|
readonly dailyStarlanguageTrend: string;
|
|
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;
|
|
};
|
|
|
|
export function StarterEntries({
|
|
natalMinuteAvailable,
|
|
dailyStarlanguage,
|
|
dailyStarlanguageQuestion,
|
|
dailyStarlanguageTrend,
|
|
productEntrypointsDisabled,
|
|
startDailyStarlanguageConsultation,
|
|
rectificationCardLabel,
|
|
rectificationCardAction,
|
|
rectificationLoading,
|
|
rectificationMutationPending,
|
|
openRectificationFromHomepage,
|
|
rectificationEntrySummary,
|
|
rectificationError,
|
|
rectificationSurfaceOpen,
|
|
rectificationErrorMessage,
|
|
}: StarterEntriesProps) {
|
|
const dailyLabel = natalMinuteAvailable ? "今日星语" : "每日运势";
|
|
/* The trend line is the only part that arrives late. It stays a live region so
|
|
a screen reader hears it settle, but it is a tooltip-rank hint now, not a
|
|
card body — the pill is actionable from the first paint either way. */
|
|
const dailyHint = natalMinuteAvailable ? dailyStarlanguageTrend : "看看今天的整体节奏";
|
|
/* Three branches, same as the card it replaces: an unfinished case, a finished
|
|
one that can be redone, and a first run. The label itself is server-driven
|
|
from the entry summary (`rectificationCardAction`), not from the session list. */
|
|
const rectificationHint = rectificationEntrySummary?.hasResumableCase
|
|
? "未完成的那次可以从左侧继续"
|
|
: rectificationCardAction === "restart"
|
|
? "上次已经校正完,可以拿最新资料再来一次"
|
|
: "不确定出生时间时,用记得住的经历一步步缩小范围";
|
|
|
|
return (
|
|
<div className="starter-entries">
|
|
<div className="starter-entry-row">
|
|
<button
|
|
className="starter-entry"
|
|
type="button"
|
|
aria-label={natalMinuteAvailable ? dailyStarlanguageQuestion : "查看今日运势"}
|
|
disabled={productEntrypointsDisabled}
|
|
onClick={startDailyStarlanguageConsultation}
|
|
>
|
|
<Sparkles aria-hidden="true" />
|
|
<span>{dailyLabel}</span>
|
|
</button>
|
|
<button
|
|
className="starter-entry"
|
|
type="button"
|
|
aria-label={rectificationCardLabel}
|
|
data-opening={rectificationLoading ? "true" : undefined}
|
|
disabled={productEntrypointsDisabled || rectificationLoading || rectificationMutationPending}
|
|
onClick={() => void openRectificationFromHomepage()}
|
|
>
|
|
<Clock3 aria-hidden="true" />
|
|
<span>生时校正</span>
|
|
</button>
|
|
</div>
|
|
<p
|
|
className="starter-entry-hint"
|
|
role={natalMinuteAvailable && dailyStarlanguage.kind !== "ready" ? "status" : undefined}
|
|
>
|
|
{dailyHint} · {rectificationHint}
|
|
</p>
|
|
{!natalMinuteAvailable && (
|
|
<p className="starter-entry-boundary">出生时间不足以支持的判断我会说明,不会补造具体分钟。</p>
|
|
)}
|
|
{rectificationError && !rectificationSurfaceOpen && (
|
|
<p className="form-error rectification-entry-error" role="alert">
|
|
{rectificationErrorMessage}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|