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 ( + + + ); +} 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 => {