From 42f55251f23f803b1ba58527cf424ab0d4151a84 Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Wed, 24 Jun 2026 18:31:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai=E5=86=99=E4=BD=9C):=20=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E5=9B=9E=E7=9C=8B=E5=8F=B3=E4=BE=A7=E6=88=90=E7=A8=BF?= =?UTF-8?q?=E5=8F=AF=E7=BC=96=E8=BE=91=E5=B9=B6=E4=BF=9D=E5=AD=98=E5=9B=9E?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端新增 PUT /api/ai-writing/sessions/{id}/draft(属主校验,draft_title 可选) - 前端历史模式放开只读,工具栏新增「保存」按钮持久化修改 - 实时写作完成态保持原样(仅导出,不落库) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../open-canvas/api/ai-writing-sessions.ts | 21 +++++++ .../ai-writing/AIWritingDraftPanel.tsx | 3 +- .../artifact/TextArtifactRenderer.tsx | 5 +- .../components/artifact/WritingToolbar.tsx | 55 ++++++++++++++++++- .../contexts/AIWritingHistoryContext.tsx | 17 +++++- offline-backend-20260512/backend/CLAUDE.md | 2 + .../backend/app/gateway/routers/ai_writing.py | 29 ++++++++++ 7 files changed, 126 insertions(+), 6 deletions(-) diff --git a/frontend-web/src/open-canvas/api/ai-writing-sessions.ts b/frontend-web/src/open-canvas/api/ai-writing-sessions.ts index 36b2abad..a3a9b1f4 100644 --- a/frontend-web/src/open-canvas/api/ai-writing-sessions.ts +++ b/frontend-web/src/open-canvas/api/ai-writing-sessions.ts @@ -82,6 +82,27 @@ export async function renameAIWritingSession(id: string, title: string): Promise return resp.json(); } +/** + * 更新历史会话的成稿正文(历史回看时用户在右侧编辑器里直接修改并保存)。 + * draftTitle 省略时后端不动原标题。 + */ +export async function updateAIWritingDraft( + id: string, + draftMarkdown: string, + draftTitle?: string, +): Promise { + const resp = await apiFetch(`${BASE()}/${id}/draft`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + draft_markdown: draftMarkdown, + ...(draftTitle === undefined ? {} : { draft_title: draftTitle }), + }), + }); + if (!resp.ok) throw new Error(`保存修改失败: ${resp.status}`); + return resp.json(); +} + export async function deleteAIWritingSession(id: string): Promise { const resp = await apiFetch(`${BASE()}/${id}`, { method: 'DELETE' }); if (!resp.ok) throw new Error(`删除失败: ${resp.status}`); diff --git a/frontend-web/src/open-canvas/components/ai-writing/AIWritingDraftPanel.tsx b/frontend-web/src/open-canvas/components/ai-writing/AIWritingDraftPanel.tsx index 55dbbbe8..216ad991 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/AIWritingDraftPanel.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/AIWritingDraftPanel.tsx @@ -102,7 +102,8 @@ export function AIWritingDraftPanel() { diff --git a/frontend-web/src/open-canvas/components/artifact/TextArtifactRenderer.tsx b/frontend-web/src/open-canvas/components/artifact/TextArtifactRenderer.tsx index 68c245f1..d7fc8365 100644 --- a/frontend-web/src/open-canvas/components/artifact/TextArtifactRenderer.tsx +++ b/frontend-web/src/open-canvas/components/artifact/TextArtifactRenderer.tsx @@ -24,6 +24,8 @@ interface TextArtifactRendererProps { isStreaming?: boolean; /** 完成态只读(如历史回看):保留目录 + 排版,禁用编辑与 AI 润色 */ readOnly?: boolean; + /** 历史回看可编辑场景:提供后工具栏出现「保存」按钮,把修改持久化回会话 */ + onSaveDraft?: (markdown: string, title?: string) => Promise; } interface PanelPos { @@ -40,7 +42,7 @@ interface LastAction { customInstruction?: string; } -export function TextArtifactRenderer({ content, isStreaming, readOnly = false }: TextArtifactRendererProps) { +export function TextArtifactRenderer({ content, isStreaming, readOnly = false, onSaveDraft }: TextArtifactRendererProps) { const { artifact, setArtifact } = useCanvasGraphContext(); const { writingSelection } = useCanvasUIContext(); @@ -280,6 +282,7 @@ export function TextArtifactRenderer({ content, isStreaming, readOnly = false }: markdown={content.fullMarkdown} title={content.title} readOnly={readOnly} + onSave={onSaveDraft} />
diff --git a/frontend-web/src/open-canvas/components/artifact/WritingToolbar.tsx b/frontend-web/src/open-canvas/components/artifact/WritingToolbar.tsx index 0d1dbe7a..d4a04f35 100644 --- a/frontend-web/src/open-canvas/components/artifact/WritingToolbar.tsx +++ b/frontend-web/src/open-canvas/components/artifact/WritingToolbar.tsx @@ -14,6 +14,7 @@ import { Pilcrow, Download, Loader2, + Save, } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; @@ -71,14 +72,32 @@ interface WritingToolbarProps { title?: string; /** 历史回看等场景:仅保留导出,隐藏格式工具 */ readOnly?: boolean; + /** 提供后显示「保存」按钮,把当前正文持久化(历史回看可编辑场景) */ + onSave?: (markdown: string, title?: string) => Promise; } -export function WritingToolbar({ editor, markdown, title, readOnly = false }: WritingToolbarProps) { +export function WritingToolbar({ editor, markdown, title, readOnly = false, onSave }: WritingToolbarProps) { const [exporting, setExporting] = useState(false); + const [saving, setSaving] = useState(false); if (!editor) return null; const canExport = !exporting && !!markdown?.trim(); + const canSave = !!onSave && !saving && !!markdown?.trim(); + + const handleSave = async () => { + if (!onSave || saving || !markdown?.trim()) return; + setSaving(true); + try { + await onSave(markdown, title); + toast.success("已保存修改"); + } catch (err) { + console.error("[writing-save] failed", err); + toast.error("保存失败,请稍后重试"); + } finally { + setSaving(false); + } + }; const handleExport = async () => { if (!canExport) return; @@ -240,8 +259,40 @@ export function WritingToolbar({ editor, markdown, title, readOnly = false }: Wr - {/* Right side: export + model selector */} + {/* Right side: save + export + model selector */}
+ {onSave && ( + <> + + + + + + 保存修改到历史记录 + + + + + )}