105 lines
3.4 KiB
TypeScript
105 lines
3.4 KiB
TypeScript
"use client";
|
||
|
||
import { CheckCircle2, X } from "lucide-react";
|
||
import { useEffect } from "react";
|
||
|
||
import { ThinkingIndicator } from "@/components/ThinkingIndicator";
|
||
import { Button } from "@/components/ui/Button";
|
||
import { Badge } from "@/components/ui/Badge";
|
||
import { useRefine } from "@/lib/style/useRefine";
|
||
|
||
interface RefineViewProps {
|
||
projectId: string;
|
||
chapterNo: number;
|
||
// 选中漂移段的原文(从终稿按段切出;空则提示先编辑终稿)。
|
||
segment: string;
|
||
// 采纳:把 refined 合入终稿(经审稿页 finalText → 既有 draft 自动保存路径)。
|
||
onAdopt: (original: string, refined: string) => void;
|
||
onClose: () => void;
|
||
}
|
||
|
||
// 回炉 diff(UX §8.3):POST .../refine → 展示原段/重写段对比 → 采纳合入正文。
|
||
// 不另写库(不变量#3);采纳经既有 draft 自动保存合入。
|
||
export function RefineView({
|
||
projectId,
|
||
chapterNo,
|
||
segment,
|
||
onAdopt,
|
||
onClose,
|
||
}: RefineViewProps) {
|
||
const refiner = useRefine();
|
||
|
||
// 进入即触发回炉(段非空)。
|
||
useEffect(() => {
|
||
if (segment.trim().length > 0) {
|
||
void refiner.refine(projectId, chapterNo, segment);
|
||
}
|
||
// 仅在段变化时触发。
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [segment, projectId, chapterNo]);
|
||
|
||
return (
|
||
<div
|
||
className="rounded border border-cinnabar bg-panel p-3 text-xs"
|
||
role="dialog"
|
||
aria-label="回炉对比"
|
||
>
|
||
<div className="mb-2 flex items-center justify-between">
|
||
<h3 className="font-semibold text-ink">回炉对比</h3>
|
||
<button
|
||
type="button"
|
||
onClick={onClose}
|
||
className="rounded border border-transparent p-1 text-ink-soft hover:border-line hover:text-cinnabar"
|
||
aria-label="关闭回炉对比"
|
||
>
|
||
<X className="h-4 w-4" aria-hidden="true" />
|
||
</button>
|
||
</div>
|
||
|
||
{segment.trim().length === 0 ? (
|
||
<Badge variant="warning">
|
||
该段在终稿中为空,请先在终稿中保留该段正文。
|
||
</Badge>
|
||
) : refiner.status === "refining" ? (
|
||
<p className="text-info">
|
||
<ThinkingIndicator label="回炉中" />
|
||
</p>
|
||
) : refiner.status === "error" ? (
|
||
<p className="text-conflict">回炉失败,请稍后重试。</p>
|
||
) : refiner.result ? (
|
||
<div className="space-y-2">
|
||
<div>
|
||
<p className="mb-1 text-ink-soft">原段</p>
|
||
<p className="whitespace-pre-wrap rounded border border-line bg-bg p-2 text-ink-soft line-through">
|
||
{refiner.result.original}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p className="mb-1 text-cinnabar">重写段</p>
|
||
<p className="whitespace-pre-wrap rounded border border-cinnabar bg-bg p-2 text-ink">
|
||
{refiner.result.refined}
|
||
</p>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<Button
|
||
onClick={() => {
|
||
if (refiner.result) {
|
||
onAdopt(refiner.result.original, refiner.result.refined);
|
||
}
|
||
}}
|
||
variant="primary"
|
||
size="sm"
|
||
>
|
||
<CheckCircle2 className="h-4 w-4" aria-hidden="true" />
|
||
采纳合入正文
|
||
</Button>
|
||
<Button onClick={onClose} variant="secondary" size="sm">
|
||
放弃
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|