refactor(rectification): split the chat component into hooks, parameter functions and blocks

Move-only (TASK-rectification-code-split-20260926 T1/T4). No behavior, API,
copy, DB, Skill or scoring change.

- rectification-agentic-chat.tsx: 2043 -> 737 lines; component body
  1625 -> 630; useState 35 -> 12, useRef 16 -> 9, effects 9 -> 5,
  useCallback 12 -> 6.
- Snapshot sync -> hooks/use-rectification-case-snapshot.ts; board layout
  and live label clock -> two small hooks.
- send / submitStructuredChoice / acceptCandidate bodies ->
  lib/rectification-chat-{turn,choice,accept}-run.ts parameter functions
  (bodies verbatim, deps destructured to the same names); copy/regenerate,
  question repair, pure transcript and snapshot helpers and the per-render
  view derivation -> lib/rectification-chat-*.ts (no React hooks).
- Question-gap blocks and the read-only range line ->
  components/rectification-question-gap-notices.tsx.
- Dependency arrays kept exactly as before (lint warnings +3, listed in
  PROGRESS); no dep added to appease the linter.
- Tests: whole-source contracts read tests/rectification-chat-surface.ts
  (container + split files, like home-surface.ts); slices of moved code now
  call the extracted functions or render the extracted block, each with a
  原值/新值/原因 comment. New tests/rectification-growth-contract.test.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-09-26 19:23:52 +08:00
co-authored by Claude Opus 5.5
parent 12cbe6f868
commit 3a66c39f48
61 changed files with 3402 additions and 1731 deletions
+17 -17
View File
@@ -14,13 +14,13 @@ import {
} from "../src/lib/queued-draft.ts";
import { chatReplyAnnouncer } from "../src/lib/chat-reply-announcement.ts";
import { RECTIFICATION_STOPPED_NOTICE } from "../src/lib/rectification-surface-state.ts";
import { rectificationChatSurface } from "./rectification-chat-surface.ts";
import { abortWiringOf } from "./rectification-chat-run-fixtures.ts";
const page = readFileSync(new URL("../src/app/(app)/page.tsx", import.meta.url), "utf8");
const composer = readFileSync(new URL("../src/components/chat-composer.tsx", import.meta.url), "utf8");
const rectification = readFileSync(
new URL("../src/components/rectification-agentic-chat.tsx", import.meta.url),
"utf8",
);
// 原值: 读 rectification-agentic-chat.tsx 单文件。新值: rectificationChatSurface(容器 + 拆出的 hook / 参数函数 / 块,见 rectification-chat-surface.ts)。原因: TASK-rectification-code-split-20260926 只搬不改,整文件断言跟着代码走;切片断言另行改为调用函数。
const rectification = rectificationChatSurface;
const consultationRun = readFileSync(
new URL("../src/hooks/use-consultation-run.ts", import.meta.url),
"utf8",
@@ -136,7 +136,7 @@ test("a queued card renders above the live textarea", () => {
assert.match(html, /还想再问一句/);
});
test("rectification abort settles as stopped, not a failed alert", () => {
test("rectification abort settles as stopped, not a failed alert", async () => {
assert.match(rectification, /stopped: true/);
assert.match(rectification, /failed: false,\s*stopped: true/);
assert.doesNotMatch(rectification, /if \(raw\.trim\(\)\) setError\(RECTIFICATION_STOPPED_NOTICE\)/);
@@ -149,18 +149,18 @@ test("rectification abort settles as stopped, not a failed alert", () => {
assert.match(messageRow, /className="message-stopped-notice"/);
assert.equal(RECTIFICATION_STOPPED_NOTICE, "已停止,已生成的内容保留;本次不会扣点。");
assert.match(rectification, /signal: abortController\.signal/);
const choiceFetch = rectification.slice(
rectification.indexOf("const submitStructuredChoice"),
rectification.indexOf("const acceptCandidate"),
);
assert.match(choiceFetch, /runAbort\.current = abortController/);
assert.match(choiceFetch, /signal: abortController\.signal/);
const adoptFetch = rectification.slice(
rectification.indexOf("const acceptCandidate"),
rectification.indexOf("async function copyMessage"),
);
assert.match(adoptFetch, /runAbort\.current = abortController/);
assert.match(adoptFetch, /signal: abortController\.signal/);
// 原值: 从 chat 源码切 submitStructuredChoice / acceptCandidate 两段,各匹配 `runAbort.current = abortController` 与 `signal: abortController.signal`。
// 新值: 调用 runRectificationStructuredChoice / runRectificationCandidateAccept(假 fetch):请求带的 signal
// 就是 runAbort 里那个控制器的(停止按钮能掐断它),结束后 runAbort 清空。
// 原因: TASK-rectification-code-split-20260926 两段函数体原样搬进参数函数;行为断言覆盖接线本身。
const choiceWiring = await abortWiringOf("choice");
assert.equal(choiceWiring.url, "/api/rectification/agent");
assert.equal(choiceWiring.signalMatchesRunAbort, true);
assert.equal(choiceWiring.runAbortCleared, true);
const adoptWiring = await abortWiringOf("accept");
assert.match(adoptWiring.url, /\/api\/rectification\/cases\/[^/]+\/candidates\/accept$/);
assert.equal(adoptWiring.signalMatchesRunAbort, true);
assert.equal(adoptWiring.runAbortCleared, true);
assert.equal(chatReplyAnnouncer("stopped"), "chat_notice_toast");
assert.doesNotMatch(
rectification.slice(rectification.indexOf("{error &&"), rectification.indexOf("{readonly &&")),