diff --git a/apps/web/components/ThinkingIndicator.tsx b/apps/web/components/ThinkingIndicator.tsx
new file mode 100644
index 0000000..d778d11
--- /dev/null
+++ b/apps/web/components/ThinkingIndicator.tsx
@@ -0,0 +1,32 @@
+interface ThinkingIndicatorProps {
+ // 状态文案(如「审稿进行中」「验收中」「回炉中」「生成中」)。
+ label: string;
+ className?: string;
+}
+
+// AI 推理中动画提示:三点波动(朱砂/继承色),让长等待「看得见在工作」。
+// motion-safe:尊重 prefers-reduced-motion,减动偏好下三点静止为「…」仍可读(a11y §10)。
+// role=status + aria-live:屏读播报状态变化。
+export function ThinkingIndicator({
+ label,
+ className = "",
+}: ThinkingIndicatorProps) {
+ return (
+
+
+ {[0, 1, 2].map((i) => (
+
+ ))}
+
+ {label}
+
+ );
+}
diff --git a/apps/web/components/review/AcceptPanel.tsx b/apps/web/components/review/AcceptPanel.tsx
index d6a75ae..ca361ea 100644
--- a/apps/web/components/review/AcceptPanel.tsx
+++ b/apps/web/components/review/AcceptPanel.tsx
@@ -2,6 +2,7 @@
import type { AcceptResponse } from "@/lib/api/types";
import { buildAcceptPreview } from "@/lib/review/accept-preview";
+import { ThinkingIndicator } from "@/components/ThinkingIndicator";
interface AcceptPanelProps {
conflictCount: number;
@@ -101,7 +102,11 @@ export function AcceptPanel({
aria-disabled={blocked || accepting}
className="w-full rounded bg-cinnabar px-4 py-2 text-sm text-panel hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40"
>
- {accepting ? "验收中…" : "全部处理完 → 验收本章"}
+ {accepting ? (
+
+ ) : (
+ "全部处理完 → 验收本章"
+ )}
);
diff --git a/apps/web/components/review/ReviewReport.tsx b/apps/web/components/review/ReviewReport.tsx
index 6e6fdf8..fddc1b7 100644
--- a/apps/web/components/review/ReviewReport.tsx
+++ b/apps/web/components/review/ReviewReport.tsx
@@ -30,6 +30,7 @@ import { useReviewStream } from "@/lib/review/useReviewStream";
import type { ReviewConflict, StyleDriftSegment } from "@/lib/review/sse";
import { normalizeStyleDrift } from "@/lib/style/style";
import { useToast } from "@/components/Toast";
+import { ThinkingIndicator } from "@/components/ThinkingIndicator";
import { AcceptPanel } from "./AcceptPanel";
import { AnnotatedText } from "./AnnotatedText";
import { ConflictCard } from "./ConflictCard";
@@ -429,8 +430,8 @@ function SectionStatusLine({
}: SectionStatusLineProps) {
if (reviewing) {
return (
-
- 审稿进行中…
+
+
);
}
diff --git a/apps/web/components/style/RefineView.tsx b/apps/web/components/style/RefineView.tsx
index a226696..4900627 100644
--- a/apps/web/components/style/RefineView.tsx
+++ b/apps/web/components/style/RefineView.tsx
@@ -3,6 +3,8 @@
import { useRefine } from "@/lib/style/useRefine";
import { useEffect } from "react";
+import { ThinkingIndicator } from "@/components/ThinkingIndicator";
+
interface RefineViewProps {
projectId: string;
chapterNo: number;
@@ -54,8 +56,8 @@ export function RefineView({
{segment.trim().length === 0 ? (
该段在终稿中为空,请先在终稿中保留该段正文。
) : refiner.status === "refining" ? (
-
- 回炉中…
+
+
) : refiner.status === "error" ? (
回炉失败,请稍后重试。
diff --git a/apps/web/components/workbench/Workbench.tsx b/apps/web/components/workbench/Workbench.tsx
index f881563..de854ae 100644
--- a/apps/web/components/workbench/Workbench.tsx
+++ b/apps/web/components/workbench/Workbench.tsx
@@ -36,7 +36,7 @@ export function Workbench({ project, initialText = "" }: WorkbenchProps) {
// 本章指令(T4-b):自由文本 + 风格预设 → 写本章时组装传入 draft 生成。
const [directive, setDirective] = useState("");
const [presetIds, setPresetIds] = useState([]);
- const autosave = useAutosave(project.id, chapterNo);
+ const autosave = useAutosave(project.id, chapterNo, initialText);
const stream = useDraftStream();
const lastStreamText = useRef("");
diff --git a/apps/web/lib/autosave/useAutosave.ts b/apps/web/lib/autosave/useAutosave.ts
index c58e2da..8410a42 100644
--- a/apps/web/lib/autosave/useAutosave.ts
+++ b/apps/web/lib/autosave/useAutosave.ts
@@ -22,14 +22,24 @@ export interface UseAutosave {
// 防抖自动保存草稿:乐观置 saving → PUT 成功置 saved(落「保存于 hh:mm」);
// 失败回滚状态 + toast,不丢正文(已在编辑器里)。
+// `initialSavedText`:进页种入的已存草稿——非空时初始即「已保存」,修「加载已存草稿却显示尚未保存」的误导。
export function useAutosave(
projectId: string,
chapterNo: number,
+ initialSavedText?: string,
): UseAutosave {
- const [status, setStatus] = useState("idle");
- const [savedLabel, setSavedLabel] = useState(null);
+ const hasInitialDraft = (initialSavedText ?? "").length > 0;
+ const [status, setStatus] = useState(
+ hasInitialDraft ? "saved" : "idle",
+ );
+ const [savedLabel, setSavedLabel] = useState(
+ hasInitialDraft ? "已保存" : null,
+ );
const toast = useToast();
- const lastSavedRef = useRef(null);
+ // 种入已存草稿为基线:用户未改动时不触发冗余保存(save 早退)。
+ const lastSavedRef = useRef(
+ hasInitialDraft ? (initialSavedText as string) : null,
+ );
const save = useCallback(
async (text: string): Promise => {