Files
deerflow-code/frontend-web/src/open-canvas/components/ai-writing/AIWritingDraftPanel.tsx
T
fengxiangdebaoziandClaude Opus 4.8 c117e296cb feat(ai写作): 草稿局部修改只改命中章节,支持改章节标题,修改时右侧流式重写
用户在草稿确认填修改意见(user_revise)时,按章节定位只重写命中章节、
其余字节级保留;新增范围识别区分「改标题」与「改正文」,改标题同步回写
outline 使其持久生效(修复改章节标题不生效);整体性/结构性意见自动回退
整篇重写。前端新增 isRevising:修改时右侧先恢复初始占位(清空旧稿),
重写流到达后实时展示被改章节的流式重写。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 19:45:00 +08:00

132 lines
5.5 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.
import { FileText, PenLine } from 'lucide-react';
import { useAIWritingOptional } from '../../contexts/AIWritingContext';
import { useAIWritingHistoryOptional } from '../../contexts/AIWritingHistoryContext';
import { useCanvasGraphContext } from '../../contexts/CanvasGraphContext';
import { useCanvasUIContext } from '../../contexts/CanvasUIContext';
import { assembleStreamingDraftMarkdown } from '../../hooks/useAIWritingStream';
import { getCurrentArtifactContent, isCodeArtifact } from '../../utils/artifact';
import { TextArtifactRenderer } from '../artifact/TextArtifactRenderer';
import { DraftStreamingPreview } from './DraftStreamingPreview';
import type { CanvasArtifactText } from '../../types/artifact';
function DraftWaitingPlaceholder() {
return (
<div className="h-full flex flex-col items-center justify-center gap-5 select-none">
<div className="relative">
<div className="w-16 h-16 rounded-2xl bg-muted/60 flex items-center justify-center dark:bg-[#022248] ">
<FileText className="w-8 h-8 text-muted-foreground/40 dark:text-[#55A6FE] " />
</div>
<div className="absolute -bottom-1.5 -right-1.5 w-6 h-6 rounded-full bg-primary/10 flex items-center justify-center dark:bg-[#102D5C] ">
<PenLine className="w-3.5 h-3.5 text-primary/60 dark:text-[#2580FF] " />
</div>
</div>
<div className="text-center space-y-1.5 max-w-[200px]">
<p className="text-sm font-medium text-foreground/60 dark:text-[#AACEFF]">草稿将在此处显示</p>
<p className="text-[11px] text-muted-foreground leading-relaxed dark:text-[#55A6FE]">
完成写作流程后,AI 生成的内容将实时出现在这里
</p>
</div>
</div>
);
}
export function AIWritingDraftPanel() {
const writing = useAIWritingOptional();
const history = useAIWritingHistoryOptional();
const isHistoryMode = history?.isHistoryMode ?? false;
const { artifact } = useCanvasGraphContext();
const { isDraftEditable } = useCanvasUIContext();
// 历史回看与写作完成态一致:Tiptap + 目录;流式生成中仍走 Markdown 预览
const showCompletedDraftView = isDraftEditable || isHistoryMode;
const writingStatus = writing?.state.status;
const streamingSectionCount = writing?.state.streamingDraftSections.length ?? 0;
// 按用户意见修改中、且重写流还没开始(无任何流式章节):右侧先恢复初始空占位,
// 把旧稿清掉;一旦重写流式 chunk 到达(streamingDraftSections 非空)就走下面的
// 流式预览,实时展示重写过程;draft_ready 后 isRevising 置回 false 显示新成稿。
if (writing?.state.isRevising && streamingSectionCount === 0) {
return (
<div className="h-full bg-muted/10 border-l">
<DraftWaitingPlaceholder />
</div>
);
}
// 仅当章节仍在累积时走流式 Markdown;draft_ready 清空 sections 后即使 status 尚未切走也进画布
const isActivelyStreamingDraft =
(writingStatus === 'writing_draft' || writingStatus === 'revising') &&
streamingSectionCount > 0;
const streamingMarkdown = isActivelyStreamingDraft && writing
? assembleStreamingDraftMarkdown(
writing.state.streamingDraftSections,
writing.state.streamingDraftTitle || writing.state.outline?.title || '',
)
: '';
// 流式生成中:只读 Markdown 预览
if (isActivelyStreamingDraft) {
return (
<div className="h-full flex flex-col overflow-hidden border-l">
<div className="h-10 border-b flex items-center gap-2 px-4 shrink-0 bg-muted/20">
<FileText className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
<span className="text-sm font-medium truncate flex-1">草稿生成中…</span>
</div>
<div className="flex-1 overflow-hidden">
{streamingMarkdown ? (
<DraftStreamingPreview markdown={streamingMarkdown} />
) : (
<DraftWaitingPlaceholder />
)}
</div>
</div>
);
}
const artifactContent = artifact ? getCurrentArtifactContent(artifact) : null;
const textFromArtifact =
artifactContent && !isCodeArtifact(artifactContent)
? (artifactContent as CanvasArtifactText)
: null;
// draft_ready 后 DraftSync 写 artifact 可能晚一帧;先用 context 里的成稿兜底,避免闪占位符
const textContent: CanvasArtifactText | null =
textFromArtifact ??
(writing?.state.currentDraftMarkdown
? {
index: 0,
type: 'text',
title: writing.state.currentDraftTitle || '草稿',
fullMarkdown: writing.state.currentDraftMarkdown,
}
: null);
if (textContent) {
return (
<div className="h-full flex flex-col overflow-hidden border-l">
<div className="h-10 border-b flex items-center gap-2 px-4 shrink-0 bg-muted/20">
<FileText className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
<span className="text-sm font-medium truncate flex-1">
{textContent.title || '草稿预览'}
</span>
</div>
<div className="flex-1 overflow-hidden">
<TextArtifactRenderer
content={textContent}
isStreaming={!showCompletedDraftView}
readOnly={false}
onSaveDraft={isHistoryMode ? history?.saveHistoryDraft : undefined}
/>
</div>
</div>
);
}
return (
<div className="h-full bg-muted/10 border-l">
<DraftWaitingPlaceholder />
</div>
);
}