refactor: make birth time rectification conversational
This commit is contained in:
@@ -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
@@ -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" : ""}`}>
|
||||
|
||||
Reference in New Issue
Block a user