feat: 深度研究支持独立新报告与原文改写,并完善沙箱、模型管理与全文改写
新报告不再覆盖当前文件卡片,写入时打开沙箱;步骤条仅在改写原文时位于卡片上方。同时补齐改写本地回退、模型管理与会商第三步相关改动。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
11f928bc95
commit
a5a9c952e8
63 files changed
+4113
-929
No files matched your search
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ImagePlusIcon, SparklesIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FilePenLineIcon, ImagePlusIcon, SparklesIcon } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -23,6 +23,20 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { ModelNameSelect } from "@/components/workspace/model-name-select";
|
||||
import type { DocumentRewriteStyle } from "@/open-canvas/api/rewrite";
|
||||
import { useReportStructures } from "@/strategy-components/api/report-structures";
|
||||
|
||||
const KEEP_CURRENT_REPORT_STRUCTURE = "__keep_current_report_structure__";
|
||||
const DEFAULT_REWRITE_INSTRUCTION = "统一表达风格,减少重复内容,改善段落衔接;不新增原文没有依据的事实。";
|
||||
const DEFAULT_GENERATE_INSTRUCTION = "基于已选资料按所选报告结构生成一篇新的完整报告;保持事实可核验,不补充没有依据的信息。";
|
||||
|
||||
export interface DocumentRewriteDialogInput {
|
||||
instruction: string;
|
||||
style: DocumentRewriteStyle;
|
||||
modelName: string | null;
|
||||
reportOutline: string;
|
||||
generateImages: boolean;
|
||||
maxGeneratedImages: number;
|
||||
}
|
||||
|
||||
const STYLE_OPTIONS: { value: DocumentRewriteStyle; label: string; description: string }[] = [
|
||||
{
|
||||
@@ -54,7 +68,13 @@ export function DocumentRewriteDialog({
|
||||
charCount,
|
||||
imageGenerationAvailable = false,
|
||||
maxGeneratedImages = 4,
|
||||
initialReportOutline,
|
||||
operationLabel,
|
||||
operationDescription,
|
||||
submitLabel,
|
||||
operation,
|
||||
onStart,
|
||||
onRewriteOriginal,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@@ -62,64 +82,106 @@ export function DocumentRewriteDialog({
|
||||
charCount: number;
|
||||
imageGenerationAvailable?: boolean;
|
||||
maxGeneratedImages?: number;
|
||||
onStart: (input: {
|
||||
instruction: string;
|
||||
style: DocumentRewriteStyle;
|
||||
modelName: string | null;
|
||||
generateImages: boolean;
|
||||
maxGeneratedImages: number;
|
||||
}) => void;
|
||||
/** A saved report structure selected before the dialog was opened. */
|
||||
initialReportOutline?: string;
|
||||
/** Optional copy for flows that create a new document instead of replacing one. */
|
||||
operationLabel?: string;
|
||||
operationDescription?: string;
|
||||
submitLabel?: string;
|
||||
/** ``generate`` creates an independent report; ``rewrite`` edits this report in place. */
|
||||
operation?: "rewrite" | "generate";
|
||||
onStart: (input: DocumentRewriteDialogInput) => void;
|
||||
/** Optional secondary action for generation flows that rewrites the source report in place. */
|
||||
onRewriteOriginal?: (input: DocumentRewriteDialogInput) => void;
|
||||
}) {
|
||||
const [instruction, setInstruction] = useState(
|
||||
"统一表达风格,减少重复内容,改善段落衔接;不新增原文没有依据的事实。",
|
||||
);
|
||||
const isNewReportGeneration = operation === "generate";
|
||||
const [instruction, setInstruction] = useState(DEFAULT_REWRITE_INSTRUCTION);
|
||||
const [style, setStyle] = useState<DocumentRewriteStyle>("formal_analysis");
|
||||
const [modelName, setModelName] = useState<string | null>(null);
|
||||
const [reportStructureId, setReportStructureId] = useState(KEEP_CURRENT_REPORT_STRUCTURE);
|
||||
const [generateImages, setGenerateImages] = useState(false);
|
||||
const [imageCount, setImageCount] = useState("2");
|
||||
const reportStructuresQuery = useReportStructures({ status: "enabled" });
|
||||
const reportStructures = reportStructuresQuery.data ?? [];
|
||||
const selectedReportStructure = reportStructures.find((item) => item.id === reportStructureId);
|
||||
const initialReportStructureId = useMemo(() => {
|
||||
const outline = initialReportOutline?.trim();
|
||||
if (!outline) return null;
|
||||
return reportStructures.find((item) => item.content.trim() === outline)?.id ?? null;
|
||||
}, [initialReportOutline, reportStructures]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setInstruction(isNewReportGeneration ? DEFAULT_GENERATE_INSTRUCTION : DEFAULT_REWRITE_INSTRUCTION);
|
||||
setStyle("formal_analysis");
|
||||
setModelName(null);
|
||||
setReportStructureId(KEEP_CURRENT_REPORT_STRUCTURE);
|
||||
setGenerateImages(false);
|
||||
setImageCount("2");
|
||||
}, [open]);
|
||||
}, [isNewReportGeneration, open]);
|
||||
|
||||
// The structures query may resolve after the dialog opens. Only seed the
|
||||
// value while the user still has the default selection, so a manual choice
|
||||
// is never overwritten by a query refresh.
|
||||
useEffect(() => {
|
||||
if (!open || !initialReportStructureId) return;
|
||||
setReportStructureId((current) => (
|
||||
current === KEEP_CURRENT_REPORT_STRUCTURE ? initialReportStructureId : current
|
||||
));
|
||||
}, [initialReportStructureId, open]);
|
||||
|
||||
const selectedStyle = STYLE_OPTIONS.find((item) => item.value === style);
|
||||
const canStart = instruction.trim().length > 0;
|
||||
const startWith = (handler: (input: DocumentRewriteDialogInput) => void) => {
|
||||
if (!canStart) return;
|
||||
handler({
|
||||
instruction: instruction.trim(),
|
||||
style,
|
||||
modelName,
|
||||
reportOutline: selectedReportStructure?.content ?? "",
|
||||
generateImages,
|
||||
maxGeneratedImages: Math.min(
|
||||
Math.max(Number(imageCount) || 1, 1),
|
||||
Math.max(Math.min(maxGeneratedImages, 4), 1),
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<SparklesIcon className="size-5 text-primary" />
|
||||
AI 全文改写
|
||||
{operationLabel ?? "AI 全文改写"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
将以当前文件快照生成临时草稿。校验通过前不会覆盖原 Markdown。
|
||||
{operationDescription ?? "将以当前文件快照生成临时草稿。校验通过前不会覆盖原 Markdown。"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="rounded-lg border bg-muted/35 px-3 py-2.5 text-sm">
|
||||
<div className="font-medium">{fileName}</div>
|
||||
<div className="text-muted-foreground mt-1">原文约 {charCount.toLocaleString()} 字</div>
|
||||
<div className="text-muted-foreground mt-1">
|
||||
{isNewReportGeneration ? "来源报告" : "原文"}约 {charCount.toLocaleString()} 字
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="grid gap-2 text-sm font-medium">
|
||||
改写要求
|
||||
{isNewReportGeneration ? "生成要求" : "改写要求"}
|
||||
<Textarea
|
||||
value={instruction}
|
||||
onChange={(event) => setInstruction(event.target.value)}
|
||||
placeholder="例如:统一为正式行业分析语气,减少重复,不新增外部事实。"
|
||||
placeholder={isNewReportGeneration
|
||||
? "例如:突出政策影响,并按所选结构组织章节;仅使用已选资料。"
|
||||
: "例如:统一为正式行业分析语气,减少重复,不新增外部事实。"}
|
||||
rows={4}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<label className="grid gap-2 text-sm font-medium">
|
||||
改写风格
|
||||
{isNewReportGeneration ? "报告风格" : "改写风格"}
|
||||
<Select value={style} onValueChange={(value) => setStyle(value as DocumentRewriteStyle)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
@@ -135,6 +197,36 @@ export function DocumentRewriteDialog({
|
||||
<span className="text-muted-foreground text-xs">{selectedStyle?.description}</span>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-2 text-sm font-medium">
|
||||
报告结构
|
||||
<Select value={reportStructureId} onValueChange={setReportStructureId}>
|
||||
<SelectTrigger aria-label="报告结构">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={KEEP_CURRENT_REPORT_STRUCTURE}>沿用当前结构</SelectItem>
|
||||
{reportStructuresQuery.isLoading && (
|
||||
<SelectItem value="__report_structures_loading__" disabled>
|
||||
正在加载结构…
|
||||
</SelectItem>
|
||||
)}
|
||||
{reportStructuresQuery.isError && (
|
||||
<SelectItem value="__report_structures_error__" disabled>
|
||||
报告结构库加载失败
|
||||
</SelectItem>
|
||||
)}
|
||||
{reportStructures.map((structure) => (
|
||||
<SelectItem key={structure.id} value={structure.id}>
|
||||
{structure.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{selectedReportStructure?.name || "默认保留原文的章节安排。"}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="grid gap-2 text-sm font-medium">
|
||||
模型
|
||||
<ModelNameSelect
|
||||
@@ -143,7 +235,9 @@ export function DocumentRewriteDialog({
|
||||
triggerClassName="h-10 text-sm"
|
||||
defaultLabel="系统默认模型"
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs">仅影响本次全文改写。</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
仅影响本次{isNewReportGeneration ? "新报告生成" : "全文改写"}。
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -155,14 +249,16 @@ export function DocumentRewriteDialog({
|
||||
<ImagePlusIcon className="size-4" />
|
||||
</span>
|
||||
<span className="grid gap-0.5">
|
||||
<span className="text-sm font-medium">改写后补充配图</span>
|
||||
<span className="text-sm font-medium">
|
||||
{isNewReportGeneration ? "为新报告补充配图" : "改写后补充配图"}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs leading-5">
|
||||
根据章节生成并保存为当前沙箱的本地图片,不使用外部图片链接。
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<Switch
|
||||
aria-label="改写后补充配图"
|
||||
aria-label={isNewReportGeneration ? "为新报告补充配图" : "改写后补充配图"}
|
||||
checked={generateImages}
|
||||
id="document-rewrite-images"
|
||||
onCheckedChange={setGenerateImages}
|
||||
@@ -193,24 +289,22 @@ export function DocumentRewriteDialog({
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
{isNewReportGeneration && onRewriteOriginal && (
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={!canStart}
|
||||
onClick={() => startWith(onRewriteOriginal)}
|
||||
>
|
||||
<FilePenLineIcon />
|
||||
改写原文
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
disabled={!canStart}
|
||||
onClick={() => {
|
||||
if (!canStart) return;
|
||||
onStart({
|
||||
instruction: instruction.trim(),
|
||||
style,
|
||||
modelName,
|
||||
generateImages,
|
||||
maxGeneratedImages: Math.min(
|
||||
Math.max(Number(imageCount) || 1, 1),
|
||||
Math.max(Math.min(maxGeneratedImages, 4), 1),
|
||||
),
|
||||
});
|
||||
}}
|
||||
onClick={() => startWith(onStart)}
|
||||
>
|
||||
<SparklesIcon />
|
||||
开始改写
|
||||
{submitLabel ?? "开始改写"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
Reference in new issue
Block a user