fix(web): 账户弹窗移出 inert;校正快照失败不再静默(BUG-968/969)
账户弹窗与入门付费墙 portal 到 document.body,SidebarInset 的 inert 不再罩住弹窗。 GET 校正快照每个非 200 打 JSON warn;客户端失败显示「这一问还没读到」和重新读取。 下一题是可渲染选择题时,确定性回复正文带题干,挂卡后去重。
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { ChevronRight, Settings, UserRound, Users, WalletCards, X } from "lucide-react";
|
||||
import { memo, type MutableRefObject, type ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
export type AccountSettingsDialog = "profile" | "chart-library" | "billing" | "general";
|
||||
|
||||
@@ -60,7 +61,7 @@ export const AccountDialogOverlay = memo(function AccountDialogOverlay({
|
||||
return model.renderGeneral();
|
||||
};
|
||||
|
||||
return (
|
||||
const overlay = (
|
||||
<div className="account-modal-overlay" onMouseDown={model.close}>
|
||||
<section
|
||||
className={`account-modal ${model.dialogClass}`}
|
||||
@@ -110,4 +111,9 @@ export const AccountDialogOverlay = memo(function AccountDialogOverlay({
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
// BUG-968: SidebarInset is inert while the dialog is open. Portal to body so
|
||||
// the dialog is not inside that subtree. SSR keeps the in-tree markup so
|
||||
// renderToString contracts still see the dialog.
|
||||
if (typeof document === "undefined") return overlay;
|
||||
return createPortal(overlay, document.body);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { Gift, X } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { FormEvent } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { keepFocusWithin } from "@/lib/focus-trap";
|
||||
import { notifyBalanceChanged, redeemErrorMessage } from "@/lib/membership";
|
||||
|
||||
@@ -72,7 +73,7 @@ export function OnboardingRedeemPaywall({
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
const overlay = (
|
||||
<div className="account-modal-overlay" onMouseDown={close}>
|
||||
<section
|
||||
className="account-modal paywall-modal"
|
||||
@@ -133,4 +134,6 @@ export function OnboardingRedeemPaywall({
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
if (typeof document === "undefined") return overlay;
|
||||
return createPortal(overlay, document.body);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ import {
|
||||
RECTIFICATION_QUESTION_RETRY_INTERVAL_MS,
|
||||
RECTIFICATION_QUESTION_RETRY_LIMIT,
|
||||
RECTIFICATION_QUESTION_UNAVAILABLE_COPY,
|
||||
RECTIFICATION_SNAPSHOT_RETRY_LABEL,
|
||||
RECTIFICATION_SNAPSHOT_UNAVAILABLE_COPY,
|
||||
RECTIFICATION_COLLECT_WAITING_PLACEHOLDER,
|
||||
RECTIFICATION_DELIVERED_COPY,
|
||||
isAbortError,
|
||||
@@ -74,6 +76,7 @@ import {
|
||||
interviewSessionOutcomeFromSnapshot,
|
||||
birthTimeSourceFromSnapshot,
|
||||
type RectificationCaseSnapshotPayload,
|
||||
type RectificationSnapshotFailure,
|
||||
} from "@/lib/rectification-surface-state";
|
||||
import { RectificationTimeline } from "@/components/rectification-timeline";
|
||||
import {
|
||||
@@ -430,6 +433,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const [questionRetryAttempts, setQuestionRetryAttempts] = useState(0);
|
||||
const [questionRepairAttempts, setQuestionRepairAttempts] = useState(0);
|
||||
const [questionRepairing, setQuestionRepairing] = useState(false);
|
||||
const [snapshotFailure, setSnapshotFailure] = useState<RectificationSnapshotFailure | null>(null);
|
||||
const [openingRequested, setOpeningRequested] = useState(false);
|
||||
const [acceptingCandidateId, setAcceptingCandidateId] = useState<string | null>(null);
|
||||
const [feedback, setFeedback] = useState<Record<string, "up" | "down" | undefined>>({});
|
||||
@@ -633,11 +637,16 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
`/api/rectification/cases/${encodeURIComponent(caseId)}?sessionId=${encodeURIComponent(sessionId)}`,
|
||||
{ cache: "no-store", signal: controller.signal },
|
||||
);
|
||||
if (!response.ok) return undefined;
|
||||
if (controller.signal.aborted) return undefined;
|
||||
if (!response.ok) {
|
||||
setSnapshotFailure({ status: response.status });
|
||||
return undefined;
|
||||
}
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (controller.signal.aborted) return undefined;
|
||||
const turns = snapshotTurns(payload);
|
||||
startTransition(() => {
|
||||
setSnapshotFailure(null);
|
||||
applyCaseSnapshot(payload);
|
||||
mergeMessages?.(turns);
|
||||
});
|
||||
@@ -645,8 +654,9 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
question: currentQuestionFromSnapshot(payload?.current_question),
|
||||
turns,
|
||||
};
|
||||
} catch {
|
||||
// Snapshot refresh is best-effort; the durable Case remains on the server.
|
||||
} catch (error) {
|
||||
if (isAbortError(error) || controller.signal.aborted) return undefined;
|
||||
setSnapshotFailure({ status: "network" });
|
||||
return undefined;
|
||||
} finally {
|
||||
if (snapshotAbort.current === controller) snapshotAbort.current = null;
|
||||
@@ -1779,7 +1789,15 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
{RECTIFICATION_DELIVERED_COPY}
|
||||
</p>
|
||||
)}
|
||||
{questionGap === "unavailable" && (
|
||||
{snapshotFailure && (
|
||||
<div className="rectification-message-wrap rectification-message-entry rectification-question-gap" role="status">
|
||||
<p className="rectification-question-gap__copy">{RECTIFICATION_SNAPSHOT_UNAVAILABLE_COPY}</p>
|
||||
<Button type="button" variant="outline" onClick={() => void refetchQuestion()}>
|
||||
{RECTIFICATION_SNAPSHOT_RETRY_LABEL}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{questionGap === "unavailable" && !snapshotFailure && (
|
||||
<div className="rectification-message-wrap rectification-message-entry rectification-question-gap" role="status">
|
||||
{questionRepairAttempts >= RECTIFICATION_QUESTION_REPAIR_LIMIT ? (
|
||||
<p className="rectification-question-gap__copy">{RECTIFICATION_QUESTION_REPAIR_FAILED_COPY}</p>
|
||||
@@ -1828,7 +1846,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
? "点上面的选项即可;想补一句细节再写"
|
||||
: (liveQuestionOnMessages || questionGap === "persisted_question") && collectSpokenPrompt
|
||||
? "请回答上面的问题…"
|
||||
: questionGap === "collect_waiting"
|
||||
: !snapshotFailure && questionGap === "collect_waiting"
|
||||
? RECTIFICATION_COLLECT_WAITING_PLACEHOLDER
|
||||
: "继续说你记得的人生经历,或回答刚才的问题…"}
|
||||
maxLength={RECTIFICATION_COMPOSER_MAX_LENGTH}
|
||||
|
||||
Reference in New Issue
Block a user