fix(chat): choose the chart person before the first message

This commit is contained in:
jesse-ux
2026-09-22 12:57:04 +08:00
parent 6d06fca0d8
commit 2a3013f224
22 changed files with 973 additions and 27 deletions
+63 -5
View File
@@ -34,6 +34,7 @@ import {
type ChatMessageFeedback,
} from "@/components/chat-message-actions";
import { ChatTranscript, type ChatTranscriptActions } from "@/components/chat-transcript";
import { ChatProfilePicker } from "@/components/chat-profile-picker";
import { ModelSelector } from "@/components/model-selector";
import { OnboardingRedeemPaywall } from "@/components/onboarding-redeem-paywall";
import { ChatComposer } from "@/components/chat-composer";
@@ -174,6 +175,8 @@ import {
type StreamingReply,
type Theme,
} from "@/lib/home-types";
import { chatSubjectSendBlocked } from "@/lib/chat-profile-picker-model";
import { useChatProfileCatalog } from "@/hooks/use-chat-profile-catalog";
import {
birthQuestion,
chartSnapshotForSession,
@@ -454,6 +457,8 @@ export default function Home() {
togglePinnedSession,
shareSession,
startNewChat,
bindEmptyChatSubject,
openChatBoundToProfile,
selectSession, selectSessionModel, loadMoreSessions,
} = useSessionManagement({
account, accountId, activeChartId, activeSession, activeSessionId, activeSessionIdRef,
@@ -563,6 +568,36 @@ export default function Home() {
queueMicrotask(() => setActiveChartId(storedChartId));
}, [accountId, hydrated]);
const {
status: chatProfileStatus,
markReady: markChatProfilesReady,
markFailed: markChatProfilesFailed,
beginReload: beginChatProfileReload,
} = useChatProfileCatalog(accountId);
const subjectDeleted = chatSubjectSendBlocked({
session: activeSession,
status: chatProfileStatus,
library: chartLibrary,
});
function reloadChatProfiles() {
if (!accountId) return;
const requested = accountId;
const profileForLibrary: Profile = activeChartId === "self" ? profile : account ? readProfile(account.profile) : profile;
beginChatProfileReload();
void fetchCloudChartLibrary()
.then((cloudLibrary) => {
if (chartLibraryLoadedAccount.current !== requested) return;
setChartLibrary(chartLibraryFromCloudOthers(cloudLibrary, profileForLibrary, upsertSelfChart));
markChatProfilesReady(requested);
})
.catch(() => {
if (chartLibraryLoadedAccount.current !== requested) return;
markChatProfilesFailed(requested);
setComposerNotice("星盘库暂时无法读取,请重试。");
});
}
useEffect(() => {
const branch = chartLibrarySessionBranch(accountId, chartLibraryLoadedAccount.current);
if (branch === "clear" || !accountId) {
@@ -582,17 +617,26 @@ export default function Home() {
clearSynastryHistory();
void fetchCloudChartLibrary()
.then((cloudLibrary) => {
if (chartLibraryLoadedAccount.current !== accountId) return;
setChartLibrary(chartLibraryFromCloudOthers(cloudLibrary, profileForLibrary, upsertSelfChart));
markChatProfilesReady(accountId);
})
.catch(() => {
if (chartLibraryLoadedAccount.current !== accountId) return;
markChatProfilesFailed(accountId);
setComposerNotice("星盘库暂时无法读取,请重试。", {
label: "重试",
onClick: () => {
beginChatProfileReload();
void fetchCloudChartLibrary()
.then((cloudLibrary) => {
if (chartLibraryLoadedAccount.current !== accountId) return;
setChartLibrary(chartLibraryFromCloudOthers(cloudLibrary, profileForLibrary, upsertSelfChart));
markChatProfilesReady(accountId);
})
.catch(() => {
if (chartLibraryLoadedAccount.current !== accountId) return;
markChatProfilesFailed(accountId);
setComposerNotice("星盘库暂时无法读取,请重试。");
});
},
@@ -618,7 +662,7 @@ export default function Home() {
});
}
setChartLibrary((current) => upsertSelfChart(current, profileForLibrary));
}, [account, accountId, activeChartId, applyCloudSynastryHistory, clearSynastryHistory, profile]);
}, [account, accountId, activeChartId, applyCloudSynastryHistory, beginChatProfileReload, clearSynastryHistory, markChatProfilesFailed, markChatProfilesReady, profile]);
useEffect(() => {
if (!hydrated || !accountId || chartLibrary.length === 0) return;
@@ -747,6 +791,7 @@ export default function Home() {
pendingConsultation,
pendingSessionId,
profile,
subjectDeleted,
sessions,
setAccount,
setActiveSessionId,
@@ -1426,6 +1471,7 @@ export default function Home() {
if (onboardingStep === "name" && presetMessageFinished) void saveOnboardingName();
return;
}
if (subjectDeleted) return;
if (isLoading || cancellationPending) return void enqueueQueuedDraft(composerDraftSnapshot());
void send(composerDraftSnapshot(), draftTheme.current ?? undefined, draftEntrypoint.current);
}
@@ -1566,7 +1612,19 @@ export default function Home() {
<SidebarTrigger placement="inset" />
<div className="chat-header-title">
<strong>{activeSession?.title || "新对话"}</strong>
{activeSession ? <span className="chat-header-chart">{sessionChartLabel(activeSession, chartLibrary)}</span> : null}
{activeSession?.sessionType === "consultation" ? (
<ChatProfilePicker
session={activeSession}
library={chartLibrary}
status={chatProfileStatus}
requestInFlight={Boolean(pendingSessionId) || cancellationPending}
onBindEmpty={bindEmptyChatSubject}
onOpenNew={openChatBoundToProfile}
onReload={reloadChatProfiles}
/>
) : activeSession ? (
<span className="chat-header-chart">{sessionChartLabel(activeSession, chartLibrary)}</span>
) : null}
</div>
<div className="chat-header-actions">
<div className="chat-header-rectification" data-rectification-header-slot="" ref={setRectificationHeaderSlot} />
@@ -1711,11 +1769,11 @@ export default function Home() {
<ChatComposer
inputRef={composerInput}
inputLabel={!profileComplete && onboardingStep === "name" ? "输入你的称呼" : "输入你的问题"}
placeholder={ordinaryComposerPlaceholder({ locked: rectificationComposerLocked, hasAccount: Boolean(account), profileComplete, onboardingName: onboardingStep === "name", presetMessageFinished, creditsEmpty: Boolean(account && account.credits === 0 && !account.activeSubscription) })}
placeholder={ordinaryComposerPlaceholder({ locked: rectificationComposerLocked, hasAccount: Boolean(account), profileComplete, onboardingName: onboardingStep === "name", presetMessageFinished, creditsEmpty: Boolean(account && account.credits === 0 && !account.activeSubscription), subjectDeleted })}
maxLength={!profileComplete && onboardingStep === "name" ? 80 : 500}
inputDisabled={sessionMessagesLoading || rectificationSurfaceOpen || activeRectificationSession || (!profileComplete && (onboardingStep !== "name" || !presetMessageFinished || profileSaving))}
inputDisabled={subjectDeleted || sessionMessagesLoading || rectificationSurfaceOpen || activeRectificationSession || (!profileComplete && (onboardingStep !== "name" || !presetMessageFinished || profileSaving))}
submitLabel={!profileComplete && onboardingStep === "name" ? "确认称呼" : "发送"}
submitBlocked={Boolean(pendingSessionId) || Boolean(queuedDraft) || sessionMessagesLoading || rectificationSurfaceOpen || activeRectificationSession || !account || !modelCatalog || (!profileComplete && (onboardingStep !== "name" || !presetMessageFinished || profileSaving))}
submitBlocked={subjectDeleted || Boolean(pendingSessionId) || Boolean(queuedDraft) || sessionMessagesLoading || rectificationSurfaceOpen || activeRectificationSession || !account || !modelCatalog || (!profileComplete && (onboardingStep !== "name" || !presetMessageFinished || profileSaving))}
stopVisible={isLoading}
stopLabel={consultationPhase === "undo" ? "撤回发送,本次不扣点" : activeStreamingText ? "停止回答,保留已生成内容并退回本次点数" : "停止回答并申请退回本次点数"}
stopTitle={consultationPhase === "undo" ? "撤回发送,本次不扣点" : activeStreamingText ? "停止回答,保留现有内容并申请退回本次点数" : "停止回答"}
+29 -5
View File
@@ -642,6 +642,7 @@ button:disabled { cursor: default; opacity: .45; }
.chat-header > div { min-width: 0; flex: 1; }
.chat-header { padding: 0 var(--space-3); }
.chat-header-chart { display: none; }
button.chat-profile-picker-trigger { display: inline-flex; }
.onboarding-card { padding: 16px; }
.message-assistant .message-content { max-width: 100%; }
@@ -678,7 +679,7 @@ button:disabled { cursor: default; opacity: .45; }
width: 8px !important;
height: 8px !important;
}
.model-selector-popup[data-starting-style], .model-selector-popup[data-ending-style], .session-actions[data-starting-style], .session-actions[data-ending-style], .account-menu-popup[data-starting-style], .account-menu-popup[data-ending-style], [role="tooltip"][data-starting-style], [role="tooltip"][data-ending-style] { transform: none; }
.model-selector-popup[data-starting-style], .model-selector-popup[data-ending-style], .chat-profile-picker-popup[data-starting-style], .chat-profile-picker-popup[data-ending-style], .session-actions[data-starting-style], .session-actions[data-ending-style], .account-menu-popup[data-starting-style], .account-menu-popup[data-ending-style], [role="tooltip"][data-starting-style], [role="tooltip"][data-ending-style] { transform: none; }
.auth-step { transform: none; transition: opacity 80ms linear !important; }
@starting-style { .auth-step { opacity: 0; transform: none; } }
}
@@ -906,12 +907,33 @@ button:disabled { cursor: default; opacity: .45; }
.chat-panel.is-rectification { grid-template-rows: 46px minmax(0, 1fr); }
.chat-header { z-index: 2; min-width: 0; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: var(--space-3); border-bottom: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent); padding: 0 var(--space-4); background: var(--color-frosted); backdrop-filter: saturate(130%) blur(20px); text-align: left; }
.chat-header > div { min-width: 0; }
/* One line, 46px tall. The former second line read `分析对象:…`; the chart it
analyses is now a quiet chip beside the title, and drops out on narrow
viewports rather than wrapping the header to two rows. */
.chat-header-title { min-width: 0; display: flex; align-items: baseline; gap: var(--space-3); }
/* One line, 46px tall. The chart chip stays beside the title and ellipsizes
instead of wrapping the header. Narrow viewports hide the static note; the
ordinary-chat person button stays, because it is the only person entry. */
.chat-header-title { min-width: 0; display: flex; align-items: center; gap: var(--space-3); }
.chat-header strong { min-width: 0; overflow: hidden; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; font-size: var(--type-body-md); font-weight: 500; letter-spacing: -.2px; }
.chat-header-chart { flex: 0 1 auto; min-width: 0; overflow: hidden; color: var(--color-ink-tertiary); font-size: var(--type-caption); text-overflow: ellipsis; white-space: nowrap; }
.chat-profile-picker-trigger { max-width: 46%; min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; margin: 0; padding: 0 var(--space-2); border: 0; border-radius: var(--radius-md); background: transparent; color: var(--color-ink-tertiary); cursor: pointer; font: inherit; font-size: var(--type-caption); line-height: 1.35; text-align: left; transition: background-color 120ms ease-out, color 120ms ease-out; }
.chat-profile-picker-trigger:focus-visible { outline: 3px solid color-mix(in srgb, var(--color-focus) 56%, transparent); outline-offset: 1px; }
.chat-profile-picker-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-profile-picker-positioner { z-index: 30; outline: 0; }
.chat-profile-picker-popup { width: max-content; min-width: 220px; max-width: min(320px, calc(100vw - var(--space-6))); border: 1px solid var(--color-border); padding: var(--space-3); border-radius: var(--radius-lg); background: var(--color-canvas); box-shadow: var(--shadow-elevated); transform-origin: var(--transform-origin); transition: opacity 120ms ease-out, transform 120ms var(--ease-out); }
.chat-profile-picker-popup[data-starting-style], .chat-profile-picker-popup[data-ending-style] { opacity: 0; transform: translateY(calc(var(--space-1) * -1)); }
.chat-profile-picker-title { margin: 0; color: var(--color-ink); font-family: var(--font-display); font-size: var(--type-title-sm); font-weight: 500; }
.chat-profile-picker-status { margin: var(--space-2) 0 0; color: var(--color-ink-secondary); font-size: var(--type-caption); line-height: 1.45; }
.chat-profile-picker-status p { margin: 0; }
.chat-profile-picker-retry { min-height: 44px; margin-top: var(--space-2); padding: 0 var(--space-3); border: 0; border-radius: var(--radius-md); background: transparent; color: var(--color-action); cursor: pointer; font: inherit; font-size: var(--type-caption); }
.chat-profile-picker-options { display: grid; gap: var(--space-1); margin: var(--space-2) 0 0; }
.chat-profile-picker-option { width: 100%; min-height: 44px; display: grid; grid-template-columns: minmax(0, 1fr) 20px; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-3); border: 1px solid transparent; border-radius: var(--radius-md); background: transparent; color: var(--color-ink-secondary); cursor: pointer; text-align: left; font: inherit; transition: border-color 120ms ease-out, background-color 120ms ease-out, color 120ms ease-out; }
.chat-profile-picker-option[aria-disabled="true"] { cursor: default; color: var(--color-ink-tertiary); }
.chat-profile-picker-option[data-selected] { border-color: var(--color-border); background: var(--color-selected); color: var(--color-ink); }
.chat-profile-picker-option:focus-visible { outline: 3px solid color-mix(in srgb, var(--color-focus) 56%, transparent); outline-offset: 1px; }
.chat-profile-picker-option:disabled { cursor: default; }
.chat-profile-picker-copy { min-width: 0; display: grid; gap: 2px; }
.chat-profile-picker-copy b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: inherit; font-size: var(--type-body-sm); font-weight: 500; line-height: 1.35; }
.chat-profile-picker-copy small { color: var(--color-ink-tertiary); font-size: var(--type-caption); line-height: 1.35; }
.chat-profile-picker-check { width: 18px; height: 18px; opacity: 0; color: var(--color-action); }
.chat-profile-picker-option[data-selected] .chat-profile-picker-check { opacity: 1; }
.chat-header-actions { min-width: max-content; display: flex; flex: 0 0 auto; align-items: center; gap: var(--space-2); white-space: nowrap; }
.chat-header-actions > * { flex: 0 0 auto; }
/* The page header is 46px tall, so its actions are 32px — the button primitives
@@ -1981,6 +2003,8 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
.composer button:not(:disabled):hover, .button-primary:not(:disabled):hover { background: var(--color-action-hover); }
.model-selector-trigger:not(:disabled):hover { background: var(--color-canvas-muted); color: var(--color-ink); }
.model-selector-option:hover { background: var(--color-canvas-soft); color: var(--color-ink); }
.chat-profile-picker-trigger:hover { background: var(--color-canvas-muted); color: var(--color-ink); }
.chat-profile-picker-option:not(:disabled):not([aria-disabled="true"]):hover { background: var(--color-canvas-soft); color: var(--color-ink); }
.button-secondary:not(:disabled):hover { background: var(--color-canvas-muted); }
.danger-primary:not(:disabled):hover { background: color-mix(in srgb, var(--color-danger) 88%, var(--color-ink)); }
.birth-time-source-option:hover, .birth-time-answer-list button:not(:disabled):hover { border-color: var(--color-action); }