From b18310bcb7058a52c3ac300764937125d04d9a9b Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Sat, 20 Jun 2026 11:02:27 +0200 Subject: [PATCH] =?UTF-8?q?feat(ux):=20F3=20=E6=B5=81=E5=BC=8F=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E6=84=9F=20+=20F4=20=E5=8F=8B=E5=A5=BD=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=96=87=E6=A1=88=EF=BC=88=E7=BA=AF=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F4:新 lib/errors/messages.ts —— friendlyError(code, fallback) 把错误码映射为 中文文案 + 可选「去设置提供商」动作;覆盖后端 ErrorCode + 前端 NETWORK/ STREAM_FAILED;未知码回退原 message。写作页与审稿页错误块改用之,不再暴露 raw error code。+4 vitest。 F3:写作页工具条字数计数器流式时显示「生成中… N 字」+ 朱砂脉冲 (motion-safe:animate-pulse,尊重 prefers-reduced-motion),停后回「字数 N」。 纯 text.length,无后端改动。 前端门禁绿:lint/typecheck/vitest 171/build。无契约变更。 --- PROGRESS.md | 1 + apps/web/components/review/ReviewReport.tsx | 42 ++++++++++++------- apps/web/components/workbench/Workbench.tsx | 44 ++++++++++++++++---- apps/web/lib/errors/messages.test.ts | 28 +++++++++++++ apps/web/lib/errors/messages.ts | 46 +++++++++++++++++++++ apps/web/tsconfig.tsbuildinfo | 2 +- 6 files changed, 139 insertions(+), 24 deletions(-) create mode 100644 apps/web/lib/errors/messages.test.ts create mode 100644 apps/web/lib/errors/messages.ts diff --git a/PROGRESS.md b/PROGRESS.md index 99c34c5..4c7397f 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -90,6 +90,7 @@ T0.1 monorepo 骨架 ✅ @devops · T0.2 16 MVP 表迁移(无漂移,users st > 格式:`- [YYYY-MM-DD] @skill 完成/进展 Txx — 一句话结果 + 影响的契约/文件` +- [2026-06-20] UX计划 F3+F4 — **✅ 流式进度感 + 友好错误文案(纯前端)**。F4:新 `apps/web/lib/errors/messages.ts`(`friendlyError(code,fallback)`→中文文案 + 可选「去设置提供商」动作;覆盖后端 `ErrorCode` + 前端 `NETWORK/STREAM_FAILED`;未知码回退原 message;+4 vitest);写作页 `Workbench` 与审稿页 `ReviewReport` 的错误块改用之,不再直接暴露 raw error code。F3:`Workbench` 工具条字数计数器流式时变「生成中… N 字」+ 朱砂 `motion-safe:animate-pulse`(尊重 prefers-reduced-motion),停后回「字数 N」;纯 `text.length`,无后端改动。前端门禁绿:lint/typecheck/vitest **171**/build。文件:`lib/errors/{messages,messages.test}.ts` · `components/workbench/Workbench.tsx` · `components/review/ReviewReport.tsx`。无契约变更。 - [2026-06-20] UX计划 B0(读)+F1 — **✅ 本章注入透明(读端点 + 右栏真面板)**。兑现差异化信任牌「看到的=写章用的」(不变量 #6):把先前写作页右栏「M1 暂未接 / M2 开放」的过时假占位换成真数据。**后端**(@backend):`GET /projects/{id}/chapters/{no}/injection` → `InjectionResponse{project_id,chapter_no,selected:[{kind,name,reasons[]}],recent_n}`,调既有 `assemble()` 回放确定性 `SelectionTrace`(**无 LLM、无 commit、无 DDL**);项目不存在→404,无大纲→`selected:[]`。文件:`schemas/injection.py`(新) · `routers/projects.py`(+GET+2 import) · `tests/test_injection.py`(新,3 测)。**前端**(@frontend):`gen:api` 纳入端点;纯逻辑 `lib/workbench/injection.ts`(理由/类型→中文徽标 + 4 vitest) + `useInjection` 读 hook;`ChapterAssistant` 改 client 组件列出选中实体 + 入选理由徽标(本章点名/主角常驻/近章出现/伏笔窗口)+ 空/载/错三态,「四审」段改指向审稿页;`Workbench` 传 `projectId/chapterNo`。**全仓门禁绿**:后端 ruff/format/mypy **159 Success**/alembic 无漂移/pytest **454 passed**;前端 lint/typecheck/vitest **167 passed**/build。记 contracts C3扩。**B0 余项(可控版:PUT override pin/排除/recent_n + selection 加参 + draft 同读 override + 持久化)+ F1 可控控件待后续。** - [2026-06-20] @frontend — **✅ 接上两个新读端点:大纲页 / 写作工作台重访时回显已存内容**。`pnpm gen:api` 纳入 `GET .../outline` + `GET .../chapters/{no}/draft` + schema `DraftView`;`lib/api/types.ts` 加 `DraftView` 别名。新增两个 RSC 读 helper(仿 `fetchForeshadow`/`fetchProject` 服务端读、无 CORS):`fetchOutline(projectId)`→`OutlineChapterView[]`(200 解包 chapters,**任何错误/空降级为 `[]`** 不阻塞进页),`fetchDraft(projectId,chapterNo)`→`DraftView|null`(复用 `getJsonOrNull`,**404→null 当空编辑器**,不抛)。**大纲页**(`outline/page.tsx`) 进页 `fetchOutline` 作 `initialChapters` 种入 `OutlineEditor`(`useOutline` 已据非空初值置 `ready`)→ 重访显已生成大纲;生成流照常覆盖所显章节。**写作工作台**(`write/page.tsx` + `Workbench`) 进页 `fetchDraft` 拿 `content` 作编辑器初值(`useState(initialText)`)→ 重访重载已写正文;404/新章 → `""` 空编辑器;**SSE 流式写 + AbortController + PUT 自动保存全不动**(流起始空、仅 streaming/done/aborted 变化才覆盖,初值不反扑活跃流);导出 `WORKBENCH_CHAPTER_NO`(原 `M1_CHAPTER_NO`)供页面拉对应章草稿。新增 `lib/api/server.test.ts`(5 vitest,mock fetch 验解包/降级/404→null)。前端门禁全绿:gen:api / lint / typecheck / vitest **160**(155→+5) / build。文件:`lib/api/{types,server,server.test}.ts` · `app/projects/[id]/{outline,write}/page.tsx` · `components/workbench/Workbench.tsx`。消费 contracts C3扩(@backend 两读端点)。 - [2026-06-20] @backend — **✅ `GET /projects/{id}/chapters/{no}/draft` 草稿读端点(补缺口,镜像 GET /outline)**。写作工作台先前只有 `POST .../draft`(SSE 流写)+`PUT .../draft`(自动保存) 无读端点 → 重访工作台编辑器空白、看似「未保存」(其实 `chapters` 行在库)。补对称读侧:`GET /projects/{id}/chapters/{no}/draft` → 200 `DraftView{project_id,chapter_no,volume,status,version,content,length}`(**比 PUT 的 `DraftResponse` 多 `content`**——读侧需正文重建编辑器);无草稿行/正文空白 → **404 `NOT_FOUND`**(工作台据此呈现空编辑器,仿其它缺资源端点)。**直接复用既有 `chapter_repo.get_draft`**(同续审 `_resolve_review_draft` 读缝,`ChapterDraftView` 已含全字段,无新 repo 方法/view);只读不写库;多版本固定返草稿版次(`DRAFT_VERSION=1`)。文件:`schemas/projects.py`(+`DraftView`) · `routers/projects.py`(+GET endpoint+import) · `tests/test_projects.py`(+2:回灌正文 / 无草稿 404)。**全仓门禁绿**:ruff/format 干净、mypy **154 Success**、alembic **无漂移**、pytest **444 passed**(442→+2)。OpenAPI 新增 1 端点 → **@frontend 须 `cd apps/web && pnpm gen:api`** + 工作台初次加载拉已保存草稿回灌编辑器。记 contracts C3扩。 diff --git a/apps/web/components/review/ReviewReport.tsx b/apps/web/components/review/ReviewReport.tsx index 7da4032..1e9b632 100644 --- a/apps/web/components/review/ReviewReport.tsx +++ b/apps/web/components/review/ReviewReport.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { AppShell } from "@/components/AppShell"; import type { ProjectResponse, ReviewHistoryItem } from "@/lib/api/types"; +import { friendlyError } from "@/lib/errors/messages"; import { allResolved, emptyDecisions, @@ -226,21 +227,7 @@ export function ReviewReport({ {review.state.error ? ( -

- 审稿失败({review.state.error.code}): - {review.state.error.message} - {review.state.error.code === "LLM_UNAVAILABLE" ? ( - <> - {" "} - - 去设置提供商 - - - ) : null} -

+ ) : null}
@@ -346,6 +333,31 @@ export function ReviewReport({ ); } +// 审稿失败提示:友好文案(不暴露 raw code)+ 可选「去设置」动作(F4)。 +function ReviewErrorNote({ + error, +}: { + error: { code: string; message: string }; +}) { + const friendly = friendlyError(error.code, error.message); + return ( +

+ {friendly.text} + {friendly.actionHref ? ( + <> + {" "} + + {friendly.actionLabel} + + + ) : null} +

+ ); +} + interface SectionStatusLineProps { reviewing: boolean; done: boolean; diff --git a/apps/web/components/workbench/Workbench.tsx b/apps/web/components/workbench/Workbench.tsx index 238e0a7..35dcdec 100644 --- a/apps/web/components/workbench/Workbench.tsx +++ b/apps/web/components/workbench/Workbench.tsx @@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from "react"; import { AppShell } from "@/components/AppShell"; import type { ProjectResponse } from "@/lib/api/types"; +import { friendlyError } from "@/lib/errors/messages"; import { useAutosave } from "@/lib/autosave/useAutosave"; import { useDraftStream } from "@/lib/stream/useDraftStream"; import { WORKBENCH_CHAPTER_NO } from "@/lib/workbench/chapter"; @@ -93,6 +94,31 @@ export function Workbench({ project, initialText = "" }: WorkbenchProps) { ); } +// 写章失败提示:友好文案(不暴露 raw code)+ 可选「去设置」动作(F4)。 +function StreamErrorNote({ + streamError, +}: { + streamError: { code: string; message: string }; +}) { + const friendly = friendlyError(streamError.code, streamError.message); + return ( +

+ {friendly.text} + {friendly.actionHref ? ( + <> + {" "} + + {friendly.actionLabel} + + + ) : null} +

+ ); +} + interface ToolbarProps { projectId: string; chapterNo: number; @@ -118,11 +144,7 @@ function Toolbar({ }: ToolbarProps) { return (
- {streamError ? ( -

- 写章失败({streamError.code}):{streamError.message} -

- ) : null} + {streamError ? : null}
{streaming ? (