用户在草稿确认填修改意见(user_revise)时,按章节定位只重写命中章节、 其余字节级保留;新增范围识别区分「改标题」与「改正文」,改标题同步回写 outline 使其持久生效(修复改章节标题不生效);整体性/结构性意见自动回退 整篇重写。前端新增 isRevising:修改时右侧先恢复初始占位(清空旧稿), 重写流到达后实时展示被改章节的流式重写。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
132 lines
5.5 KiB
TypeScript
132 lines
5.5 KiB
TypeScript
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>
|
||
);
|
||
}
|