refactor: make birth time rectification conversational

This commit is contained in:
Jesse_Chen
2026-07-27 22:31:11 +08:00
parent b569cf7825
commit a550b98da7
25 changed files with 756 additions and 1788 deletions
+2 -45
View File
@@ -840,20 +840,8 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background:
.conversational-composer-footer button, .conversational-confirmation button, .conversational-original-question button { width: 100%; }
}
/* Conversational rectification shell */
.conversation.is-rectification { overflow: hidden; padding-bottom: 0; }
.rectification-chat { height: 100%; min-height: 0; display: flex; flex-direction: column; color: var(--color-ink); }
.rectification-message-list { min-height: 0; flex: 1; overflow-y: auto; overscroll-behavior: contain; padding-bottom: var(--space-6); }
.rectification-correction-target button { min-height: 44px; padding-inline: var(--space-3); border: 0; background: transparent; color: var(--color-action); cursor: pointer; }
.rectification-composer-wrap { position: static; flex: 0 0 auto; }
.rectification-correction-target { width: min(760px, 100%); display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); margin: 0 auto var(--space-2); color: var(--color-ink-secondary); font-size: var(--type-caption); }
.rectification-correction-target p { margin: 0; }
.conversational-empty-state { width: min(760px, 100%); margin: var(--space-8) auto; color: var(--color-ink-secondary); }
.conversational-loading { min-height: min(52vh, 460px); display: grid; place-items: center; padding: var(--space-6); }
@media (max-width: 430px) {
.rectification-correction-target { align-items: stretch; display: grid; }
}
/* Rectification reuses the ordinary session message list and composer. */
.rectification-chat { display: contents; }
/* Starter workbench */
.welcome {
@@ -1666,34 +1654,3 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background:
.birth-time-clock-menu.select-content { width: 108px; min-width: 108px; }
.birth-time-clock-menu .select-item { justify-content: flex-start; }
/* Birth-time rectification V4 */
.rectification-v4-panel { width: min(860px, 100%); margin: 0 auto; padding: clamp(20px, 4vw, 40px); display: grid; gap: 20px; overflow-y: auto; }
.rectification-v4-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
.rectification-v4-header h2 { margin: 4px 0 8px; font-size: clamp(24px, 4vw, 36px); letter-spacing: -0.035em; }
.rectification-v4-header p, .rectification-v4-context p, .rectification-v4-processing p, .rectification-v4-result p { margin: 0; color: var(--color-ink-secondary); line-height: 1.65; }
.rectification-v4-eyebrow { color: var(--color-action) !important; font-size: var(--type-caption); font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
.rectification-v4-context, .rectification-v4-processing, .rectification-v4-result, .rectification-v4-notice { padding: 18px; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas); }
.rectification-v4-context { display: grid; gap: 6px; }
.rectification-v4-processing { display: grid; gap: 8px; }
.rectification-v4-ranges { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin: 14px 0 18px; }
.rectification-v4-ranges > div { display: grid; gap: 5px; padding: 16px; border-radius: var(--radius-md); background: var(--color-surface); }
.rectification-v4-ranges span { color: var(--color-ink-secondary); font-size: var(--type-caption); }
.rectification-v4-ranges strong { font-size: 24px; letter-spacing: -.02em; }
.rectification-v4-evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.rectification-v4-evidence-grid h3 { margin: 0 0 8px; font-size: 15px; }
.rectification-v4-evidence-grid ul { margin: 0; padding-left: 18px; color: var(--color-ink-secondary); line-height: 1.6; }
.rectification-v4-uncertainty { margin-top: 16px !important; padding-top: 14px; border-top: 1px solid var(--color-border); font-size: var(--type-caption); }
.rectification-v4-actions, .rectification-v4-footer { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 16px; }
.rectification-v4-saved { display: inline-flex; align-items: center; gap: 7px; color: var(--color-action); font-weight: 700; }
.rectification-v4-saved svg, .rectification-v4-footer svg { width: 16px; height: 16px; }
.rectification-v4-composer { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; align-items: end; }
.rectification-v4-composer label { grid-column: 1 / -1; font-weight: 650; line-height: 1.55; }
.rectification-v4-composer textarea { min-height: 112px; resize: vertical; }
.rectification-v4-footer button { display: inline-flex; align-items: center; gap: 7px; min-height: 40px; border: 0; background: transparent; color: var(--color-ink-secondary); cursor: pointer; }
.rectification-v4-footer button:disabled { cursor: default; opacity: .5; }
@media (max-width: 680px) {
.rectification-v4-panel { padding: 18px 14px 24px; }
.rectification-v4-header { display: grid; }
.rectification-v4-ranges, .rectification-v4-evidence-grid { grid-template-columns: 1fr; }
}
+4 -117
View File
@@ -54,13 +54,11 @@ import {
type RectificationCardAction,
} from "@/lib/birth-time-consultation-consent";
import type { ConsultationBirthTimeMode } from "@/lib/consultation-birth-time-mode";
import type { ConversationalRectificationTurn } from "@/lib/conversational-rectification/contracts";
import { claimRectificationV4Handoff } from "@/lib/rectification-v4/client";
import {
createRectificationQuestionHandoffCoordinator,
} from "@/lib/rectification-question-handoff";
import { useBirthTimeGuidedJourney } from "@/hooks/use-birth-time-guided-journey";
import type { ConversationalRectificationMessage } from "@/hooks/use-conversational-rectification";
import {
requestBirthTimeAssessment,
type JourneyClientResponse,
@@ -169,13 +167,6 @@ type ChatSession = {
rectificationCaseId: string | null;
};
function durableRectificationMessages(
messages: readonly ConversationalRectificationMessage[],
): Message[] {
const durable = messages.map(({ role, text }) => ({ role, text }));
if (durable.length <= 500) return durable;
return [durable[0]!, ...durable.slice(-(500 - 1))];
}
type RequestError = { sessionId: string; message: string };
type StreamingReply = { sessionId: string; text: string };
type BirthPlace = {
@@ -956,8 +947,6 @@ export default function Home() {
);
const [rectificationSessionId, setRectificationSessionId] = useState<string | null>(null);
const [rectificationReturnSessionId, setRectificationReturnSessionId] = useState<string | null>(null);
const [rectificationInitialTurn, setRectificationInitialTurn] = useState<ConversationalRectificationTurn | null>(null);
const [rectificationOpeningAssistantText, setRectificationOpeningAssistantText] = useState("");
const [rectificationPendingQuestion, setRectificationPendingQuestion] = useState<string | null>(null);
const [rectificationLoading, setRectificationLoading] = useState(false);
const [rectificationMutationPending, setRectificationMutationPending] = useState(false);
@@ -1015,9 +1004,6 @@ export default function Home() {
const activeSession = sessions.find((session) => session.id === activeSessionId) ?? sessions[0];
const activeRectificationSession = activeSession?.sessionType === "birth_time_rectification";
const visibleRectificationTurn = activeSession?.id === rectificationSessionId
? rectificationInitialTurn
: null;
const rectificationSurfaceOpen = activeRectificationSession
&& activeSession.id === rectificationSessionId;
const visibleSessions = sessions
@@ -1612,7 +1598,7 @@ export default function Home() {
setComposerNotice("");
if (nextSession?.sessionType === "birth_time_rectification") {
setRectificationError("");
if (nextSession.id !== rectificationSessionId || !rectificationInitialTurn) {
if (nextSession.id !== rectificationSessionId) {
resumeRectificationSession.current(nextSession);
}
}
@@ -2041,20 +2027,6 @@ export default function Home() {
);
}
function synchronizeRectificationQuestion(
turn: ConversationalRectificationTurn,
fallbackSession: ChatSession | undefined = activeSession,
) {
if (!turn.pendingConsultationQuestion) return;
if (!rectificationQuestionHandoff.current.peek() && fallbackSession) {
rectificationQuestionHandoff.current.synchronizeDurableQuestion(
turn.pendingConsultationQuestion,
{ sessionId: fallbackSession.id, theme: fallbackSession.theme },
);
}
setRectificationPendingQuestion(turn.pendingConsultationQuestion);
}
async function openBirthTimeRectification(
pendingConsultationQuestion: string | null = null,
sourceSessionOverride: ChatSession | null = null,
@@ -2072,8 +2044,6 @@ export default function Home() {
rectificationOpenInFlight.current = true;
setRectificationLoading(true);
setRectificationError("");
setRectificationInitialTurn(null);
setRectificationOpeningAssistantText("");
setRectificationPendingQuestion(requestedQuestion);
setDraft("");
setDraftTheme(null);
@@ -2103,78 +2073,6 @@ export default function Home() {
void openBirthTimeRectification(null, session);
};
function handleConversationalRectificationTurn(
turn: ConversationalRectificationTurn,
messages: readonly ConversationalRectificationMessage[],
) {
const requestIdentity = accountRefreshGuard.current.begin();
setRectificationInitialTurn(turn);
synchronizeRectificationQuestion(turn);
if (activeSession?.sessionType === "birth_time_rectification") {
const boundSession = {
...activeSession,
messages: durableRectificationMessages(messages),
rectificationCaseId: turn.caseId,
updatedAt: timestamp(),
};
updateSession(activeSession.id, () => boundSession);
void rectificationPersistence.current.enqueue(
boundSession.id,
() => persistSession(boundSession),
).catch(() => {
setComposerNotice("校正进度已经保留,但会话关联暂时未同步到云端。");
});
}
setAccount((current) => current ? {
...current,
hasConfirmedBirthTime: current.hasConfirmedBirthTime
|| (turn.status === "completed" && turn.candidate.status === "confirmed"),
rectificationCase: ["active", "paused", "confirming"].includes(turn.status)
? {
caseId: turn.caseId,
journeyProtocol: "conversational-evidence-v3",
status: turn.status,
turnVersion: turn.turnVersion,
isRevision: current.rectificationCase?.caseId === turn.caseId
? current.rectificationCase.isRevision
: current.hasConfirmedBirthTime,
preservesActiveTime: current.rectificationCase?.caseId === turn.caseId
? current.rectificationCase.preservesActiveTime
: current.hasConfirmedBirthTime,
}
: current.rectificationCase?.caseId === turn.caseId
? null
: current.rectificationCase,
} : current);
if (turn.status === "completed"
&& turn.candidate.status === "confirmed"
&& turn.candidate.representativeTime) {
setProfile((current) => ({
...current,
time: turn.candidate.representativeTime ?? current.time,
birthTimeStatus: "confirmed",
rectificationCaseId: turn.caseId,
}));
setProfileDraft((current) => ({
...current,
time: turn.candidate.representativeTime ?? current.time,
birthTimeStatus: "confirmed",
rectificationCaseId: turn.caseId,
}));
}
void fetchAccount()
.then((latest) => {
if (!accountRefreshGuard.current.isCurrent(requestIdentity)) return;
setAccount((current) => {
if (turn.status === "completed" || turn.status === "abandoned") return latest;
if (latest.rectificationCase?.caseId !== turn.caseId
|| latest.rectificationCase.turnVersion < turn.turnVersion) return current;
return latest;
});
})
.catch(() => undefined);
}
async function draftSynastryQuestionFromChart(record: ChartLibraryRecord, relationshipType: SynastryRelationshipType) {
if (record.role !== "other") return;
if (synastryPendingId) return;
@@ -2943,7 +2841,7 @@ export default function Home() {
</button>
</header>
<div className={`conversation ${activeSession?.messages.length ? "" : "is-empty"} ${rectificationSurfaceOpen ? "is-rectification" : ""}`}>
<div className={`conversation ${rectificationSurfaceOpen || activeSession?.messages.length ? "" : "is-empty"}`}>
{!rectificationSurfaceOpen && (!activeSession?.messages.length ? (
<div className="welcome">
{!profileComplete ? (
@@ -3127,28 +3025,17 @@ export default function Home() {
<div ref={conversationEnd} />
</div>
))}
{rectificationSurfaceOpen && (!visibleRectificationTurn && rectificationError ? (
<div className="conversational-rectification-start-error" role="alert">
<p>{rectificationError}</p>
<button type="button" disabled={rectificationLoading} onClick={() => void openBirthTimeRectification(null, activeSession ?? null)}>
{rectificationLoading ? "正在重试…" : "重新建立校正记录"}
</button>
</div>
) : (
{rectificationSurfaceOpen && (
<ConversationalBirthTimeRectification
initialTurn={visibleRectificationTurn}
initialMessages={activeSession?.messages ?? []}
openingAssistantText={rectificationOpeningAssistantText}
models={modelCatalog?.models ?? []}
selectedModelId={activeSession?.modelId ?? ""}
onSelectModel={(modelId) => void selectSessionModel(modelId)}
pendingConsultationQuestion={rectificationPendingQuestion}
continuationPending={rectificationContinuationPending}
onPendingChange={setRectificationMutationPending}
onTurn={handleConversationalRectificationTurn}
onContinueOriginalQuestion={(continuation) => void continueRectificationOriginalQuestion(continuation)}
/>
))}
)}
</div>
{!rectificationSurfaceOpen && <div className={`composer-wrap ${starterHomeVisible ? "composer-wrap-starter" : ""}`}>