From de9f6b67112c3f6525ac4403c53846d8da449ff3 Mon Sep 17 00:00:00 2001 From: likun Date: Fri, 15 May 2026 16:22:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=86=99=E4=BD=9C?= =?UTF-8?q?=E7=94=BB=E5=B8=83=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../artifacts/artifact-file-detail.tsx | 205 ++++++++++++------ .../src/components/workspace/code-editor.tsx | 51 +++-- .../src/components/workspace/input-box.tsx | 58 +++-- .../workspace/messages/message-group.tsx | 62 +++--- frontend-web/src/core/artifacts/api.ts | 40 ++++ frontend-web/src/core/artifacts/hooks.ts | 117 ++++++++-- frontend-web/src/core/artifacts/index.ts | 5 +- frontend-web/src/core/settings/local.ts | 11 +- frontend-web/src/core/threads/types.ts | 8 +- frontend-web/src/pages/ChatPage.tsx | 50 +++-- .../backend/app/gateway/routers/artifacts.py | 70 +++++- .../backend/app/gateway/services.py | 6 +- .../deerflow/agents/lead_agent/agent.py | 51 +++-- .../deerflow/agents/lead_agent/prompt.py | 63 ++++-- 14 files changed, 580 insertions(+), 217 deletions(-) create mode 100644 frontend-web/src/core/artifacts/api.ts diff --git a/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx index eea8d2a3..c45a68a6 100644 --- a/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -2,15 +2,16 @@ import { ArrowDownIcon, - Code2Icon, - CopyIcon, - DownloadIcon, - EyeIcon, - LoaderIcon, - PackageIcon, - SquareArrowOutUpRightIcon, - XIcon, -} from "lucide-react"; + Code2Icon, + CopyIcon, + DownloadIcon, + EyeIcon, + LoaderIcon, + PackageIcon, + SaveIcon, + SquareArrowOutUpRightIcon, + XIcon, +} from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; import { Streamdown } from "streamdown"; @@ -33,11 +34,14 @@ import { import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { CodeEditor } from "@/components/workspace/code-editor"; import { apiFetch } from "@/core/api"; -import { - downloadArtifactFile, - openArtifactInNewTab, -} from "@/core/artifacts/download"; -import { useArtifactContent } from "@/core/artifacts/hooks"; +import { + downloadArtifactFile, + openArtifactInNewTab, +} from "@/core/artifacts/download"; +import { + useArtifactContent, + useUpdateArtifactContent, +} from "@/core/artifacts/hooks"; import { urlOfArtifact } from "@/core/artifacts/utils"; import { useI18n } from "@/core/i18n/hooks"; import { installSkill } from "@/core/skills/api"; @@ -113,13 +117,18 @@ export function ArtifactFileDetail({ const isSupportPreview = useMemo(() => { return language === "html" || language === "markdown"; }, [language]); - const { content } = useArtifactContent({ - threadId, - filepath: filepathFromProps, - enabled: isCodeFile && !isWriteFile, - }); - - const displayContent = content ?? ""; + const { content, isApplyingEdit } = useArtifactContent({ + threadId, + filepath: filepathFromProps, + enabled: isCodeFile && !isWriteFile, + }); + + const canEditArtifact = + language === "markdown" && !isWriteFile && !isSkillFile && !isMock; + const [draftContent, setDraftContent] = useState(""); + const [hasLocalEdits, setHasLocalEdits] = useState(false); + const updateArtifact = useUpdateArtifactContent(); + const displayContent = canEditArtifact ? draftContent : (content ?? ""); const isHtmlStreaming = language === "html" && thread.isLoading; const hasMultipleArtifacts = (artifacts?.length ?? 0) > 1; const fileTypeLabel = useMemo(() => { @@ -139,8 +148,20 @@ export function ArtifactFileDetail({ const [binaryPreviewUrl, setBinaryPreviewUrl] = useState( null, ); - const artifactContentRef = useRef(null); - const resumeButtonTimeoutRef = useRef(null); + const artifactContentRef = useRef(null); + const resumeButtonTimeoutRef = useRef(null); + + useEffect(() => { + setDraftContent(content ?? ""); + setHasLocalEdits(false); + }, [filepathFromProps]); + + useEffect(() => { + if (!canEditArtifact || hasLocalEdits) { + return; + } + setDraftContent(content ?? ""); + }, [canEditArtifact, content, hasLocalEdits]); useEffect(() => { setAllowAutoViewSwitch(true); @@ -308,7 +329,7 @@ export function ArtifactFileDetail({ }, 1600); }, [scrollArtifactToBottom]); - const handleInstallSkill = useCallback(async () => { + const handleInstallSkill = useCallback(async () => { if (isInstalling) return; setIsInstalling(true); @@ -328,7 +349,33 @@ export function ArtifactFileDetail({ } finally { setIsInstalling(false); } - }, [threadId, filepath, isInstalling]); + }, [threadId, filepath, isInstalling]); + + const handleSaveArtifact = useCallback(async () => { + if (!canEditArtifact || updateArtifact.isPending) { + return; + } + try { + await updateArtifact.mutateAsync({ + filepath, + threadId, + isMock, + content: draftContent, + }); + setHasLocalEdits(false); + toast.success("Markdown 已保存"); + } catch (error) { + console.error("Failed to save artifact:", error); + toast.error(error instanceof Error ? error.message : "Markdown 保存失败"); + } + }, [ + canEditArtifact, + draftContent, + filepath, + isMock, + threadId, + updateArtifact, + ]); return ( @@ -405,10 +452,19 @@ export function ArtifactFileDetail({ )} -
- - {!isWriteFile && filepath.endsWith(".skill") && ( - +
+ + {canEditArtifact && ( + + )} + {!isWriteFile && filepath.endsWith(".skill") && ( +
- - {showResumeAutoScroll && ( -
+ )} {!isCodeFile && (binaryPreviewUrl ? (