"use client";
import Link from "next/link";
import { RotateCcw, X } from "lucide-react";
import { StatusNote } from "@/components/ui/StatusNote";
import { buttonClass } from "@/lib/ui/variants";
import {
isPinned,
kindLabel,
reasonLabel,
RECENT_N_MAX,
RECENT_N_MIN,
type InjectionEntity,
type InjectionEntityRef,
} from "@/lib/workbench/injection";
import { useInjection } from "@/lib/workbench/useInjection";
interface ChapterAssistantProps {
projectId: string;
chapterNo: number;
}
// 右栏「本章助手」(UX §6.3)。
// 「本章注入(透明)」读 B0 端点,列出确定性选中的设定/角色 + 入选理由徽标,
// 并让作者 📌置顶 / ✕排除 / 调近 N 章(可控版)——「看到的=写章用的」信任牌(不变量 #6)。
// 桌面 aside 包裹;移动端经 Workbench 抽屉复用 AssistantContent。
export function ChapterAssistant({ projectId, chapterNo }: ChapterAssistantProps) {
return (
);
}
export function AssistantContent({ projectId, chapterNo }: ChapterAssistantProps) {
const injection = useInjection(projectId, chapterNo);
const data = injection.data;
const selected = data?.selected ?? [];
const excluded = data?.excluded ?? [];
return (
本章助手
{injection.saving ? (
保存中…
) : null}
本章注入(透明)
{data ? (
) : null}
{injection.loading ? (
加载注入信息…
) : !data ? (
// 首次加载失败:data 仍为空 → 满屏错误框 + 重试。
请检查后端连接后重试。
) : (
<>
{injection.saveError ? (
// 保存失败:悬浮提示,不替换列表;可关闭 + 重试上一次覆盖。
) : null}
{selected.length === 0 ? (
本章暂无注入实体(先在大纲点名设定/角色,或从下方排除项恢复)。
) : (
{selected.map((entity) => (
))}
)}
>
)}
{excluded.length > 0 ? (
已排除(不注入)
{excluded.map((ref) => (
))}
) : null}
四审
一致性 / 伏笔 / 文风 / 节奏,在审稿页逐条裁决。
去审稿 →
);
}
interface EntityRowProps {
entity: InjectionEntity;
pinned: boolean;
disabled: boolean;
onTogglePin: (ref: InjectionEntityRef) => void;
onExclude: (ref: InjectionEntityRef) => void;
}
function EntityRow({ entity, pinned, disabled, onTogglePin, onExclude }: EntityRowProps) {
const ref: InjectionEntityRef = { kind: entity.kind, name: entity.name };
return (
{kindLabel(entity.kind)}
{entity.name}
{(entity.reasons ?? []).map((reason) => (
{reasonLabel(reason)}
))}
);
}
interface ExcludedRowProps {
refItem: InjectionEntityRef;
disabled: boolean;
onRestore: (ref: InjectionEntityRef) => void;
}
function ExcludedRow({ refItem, disabled, onRestore }: ExcludedRowProps) {
return (
{kindLabel(refItem.kind)}
{refItem.name}
);
}
interface RecentStepperProps {
value: number;
disabled: boolean;
onChange: (n: number) => void;
}
// 「关联近 N 章」步进器:覆盖近况摘要回看章数(钳在 [MIN, MAX])。
function RecentStepper({ value, disabled, onChange }: RecentStepperProps) {
return (
近
{value}
章
);
}