Files
writer-work-flow/apps/web/components/workbench/ChapterAssistant.tsx
Yaojia Wang 8058cfb11a feat(ux): B0 注入透明读端点 + F1 写作页右栏真面板
兑现「看到的=写章用的」信任牌(不变量 #6),替换写作页右栏过时假占位
(「M1 暂未接 / M2 开放」)。

后端:
- GET /projects/{id}/chapters/{no}/injection → InjectionResponse(selected
  实体 + 入选理由 + recent_n);调既有 assemble() 回放确定性 SelectionTrace,
  无 LLM / 无 commit / 无 DDL;项目不存在→404,无大纲→selected:[]。
- 新 schemas/injection.py;tests/test_injection.py(3 测)。

前端:
- gen:api 纳入端点;纯逻辑 lib/workbench/injection.ts(理由/类型→中文徽标
  + 4 vitest)+ useInjection 读 hook。
- ChapterAssistant 改 client 组件:列出选中实体 + 理由徽标 + 空/载/错三态,
  四审段指向审稿页;Workbench 传 projectId/chapterNo。

门禁:后端 ruff/format/mypy 159/alembic 无漂移/pytest 454;
前端 lint/typecheck/vitest 167/build。

B0 可控版(PUT override pin/排除/recent_n + selection 加参 + draft 同读
override + 持久化)+ F1 可控控件待后续。
2026-06-20 10:57:03 +02:00

95 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import Link from "next/link";
import {
kindLabel,
reasonLabel,
type InjectionEntity,
} from "@/lib/workbench/injection";
import { useInjection } from "@/lib/workbench/useInjection";
interface ChapterAssistantProps {
projectId: string;
chapterNo: number;
}
// 右栏「本章助手」UX §6.3)。
// 「本章注入(透明)」读 B0 端点,列出确定性选中的设定/角色 + 入选理由徽标——
// 这是「看到的=写章用的」的信任牌(不变量 #6。「四审」指向审稿页入口。
export function ChapterAssistant({
projectId,
chapterNo,
}: ChapterAssistantProps) {
const injection = useInjection(projectId, chapterNo);
const selected = injection.data?.selected ?? [];
return (
<aside className="hidden border-l border-line bg-panel p-4 lg:block">
<h2 className="text-sm font-semibold text-ink"></h2>
<section className="mt-4">
<h3 className="text-xs font-semibold uppercase tracking-wide text-ink-soft">
</h3>
{injection.loading ? (
<p className="mt-2 text-xs text-ink-soft"></p>
) : injection.error ? (
<p className="mt-2 rounded border border-dashed border-line bg-bg p-3 text-xs text-ink-soft">
{injection.error}
</p>
) : selected.length === 0 ? (
<p className="mt-2 rounded border border-dashed border-line bg-bg p-3 text-xs text-ink-soft">
/
</p>
) : (
<ul className="mt-2 space-y-2">
{selected.map((entity) => (
<EntityRow key={`${entity.kind}:${entity.name}`} entity={entity} />
))}
</ul>
)}
</section>
<section className="mt-6">
<h3 className="text-xs font-semibold uppercase tracking-wide text-ink-soft">
</h3>
<p className="mt-2 text-xs text-ink-soft">
/ / / 稿
</p>
<Link
href={`/projects/${projectId}/review?chapter=${chapterNo}`}
className="mt-2 inline-block rounded border border-cinnabar px-3 py-1.5 text-xs text-cinnabar hover:bg-[var(--color-cinnabar-wash)]"
>
稿
</Link>
</section>
</aside>
);
}
function EntityRow({ entity }: { entity: InjectionEntity }) {
return (
<li className="rounded border border-line bg-bg p-2">
<div className="flex items-center gap-2">
<span className="rounded bg-panel px-1.5 py-0.5 text-[10px] text-ink-soft">
{kindLabel(entity.kind)}
</span>
<span className="text-sm text-ink">{entity.name}</span>
</div>
<div className="mt-1.5 flex flex-wrap gap-1">
{(entity.reasons ?? []).map((reason) => (
<span
key={reason}
className="rounded bg-[var(--color-cinnabar-wash)] px-1.5 py-0.5 text-[10px] text-cinnabar"
>
{reasonLabel(reason)}
</span>
))}
</div>
</li>
);
}