diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index e7bc36cf..c3d2697c 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -549,7 +549,7 @@ export default function ChatPage() { {/* MARK: 开发测试:iframe 通信功能测试面板 */} - {/* */} + ); diff --git a/frontend/src/components/ai-elements/code-block.tsx b/frontend/src/components/ai-elements/code-block.tsx index c0460238..0f227cc2 100644 --- a/frontend/src/components/ai-elements/code-block.tsx +++ b/frontend/src/components/ai-elements/code-block.tsx @@ -1,7 +1,7 @@ "use client"; import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; +import { cn, copyToClipboard } from "@/lib/utils"; import { CheckIcon, CopyIcon } from "lucide-react"; import { type ComponentProps, @@ -146,14 +146,9 @@ export const CodeBlockCopyButton = ({ const [isCopied, setIsCopied] = useState(false); const { code } = useContext(CodeBlockContext); - const copyToClipboard = async () => { - if (typeof window === "undefined" || !navigator?.clipboard?.writeText) { - onError?.(new Error("Clipboard API not available")); - return; - } - + const handleCopy = async () => { try { - await navigator.clipboard.writeText(code); + await copyToClipboard(code); setIsCopied(true); onCopy?.(); setTimeout(() => setIsCopied(false), timeout); @@ -167,7 +162,7 @@ export const CodeBlockCopyButton = ({ return ( ( className={cn( "group flex w-full flex-col gap-2 rounded-[10px] p-[20px]", from === "user" - ? "is-user ml-auto justify-end px-0" + ? "is-user ml-auto justify-end px-0 pb-0" : "is-assistant bg-[#ffffff]", className, )} diff --git a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx index 437ee6ac..25be179d 100644 --- a/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -48,7 +48,7 @@ import { installSkill } from "@/core/skills/api"; import { streamdownPlugins } from "@/core/streamdown"; import { checkCodeFile, getFileName } from "@/core/utils/files"; import { env } from "@/env"; -import { cn } from "@/lib/utils"; +import { cn, copyToClipboard } from "@/lib/utils"; import { CitationLink } from "../citations/citation-link"; import { Tooltip } from "../tooltip"; @@ -240,7 +240,7 @@ export function ArtifactFileDetail({ disabled={!content} onClick={async () => { try { - await navigator.clipboard.writeText(displayContent ?? ""); + await copyToClipboard(displayContent ?? ""); toast.success(t.clipboard.copiedToClipboard); } catch (error) { toast.error("Failed to copy to clipboard"); diff --git a/frontend/src/components/workspace/copy-button.tsx b/frontend/src/components/workspace/copy-button.tsx index 13d331b5..b279a56b 100644 --- a/frontend/src/components/workspace/copy-button.tsx +++ b/frontend/src/components/workspace/copy-button.tsx @@ -3,6 +3,7 @@ import { useCallback, useState, type ComponentProps } from "react"; import { Button } from "@/components/ui/button"; import { useI18n } from "@/core/i18n/hooks"; +import { copyToClipboard } from "@/lib/utils"; import { Tooltip } from "./tooltip"; @@ -15,7 +16,7 @@ export function CopyButton({ const { t } = useI18n(); const [copied, setCopied] = useState(false); const handleCopy = useCallback(() => { - void navigator.clipboard.writeText(clipboardData); + void copyToClipboard(clipboardData); setCopied(true); setTimeout(() => setCopied(false), 2000); }, [clipboardData]); diff --git a/frontend/src/components/workspace/iframe-test-panel.tsx b/frontend/src/components/workspace/iframe-test-panel.tsx index eca8527c..0967bde9 100644 --- a/frontend/src/components/workspace/iframe-test-panel.tsx +++ b/frontend/src/components/workspace/iframe-test-panel.tsx @@ -5,6 +5,7 @@ import { useState } from "react"; import { Button } from "@/components/ui/button"; import { useIframeSkill } from "@/hooks/use-iframe-skill"; +import { copyToClipboard } from "@/lib/utils"; import { cn } from "@/lib/utils"; /** @@ -55,6 +56,15 @@ export function IframeTestPanel() { iframeSkill.clearSkill(); addLog("clearSkill 已调用,postMessage → skill_id=0"); } + + function handleTestClipboardCopy() { + const testText = "测试复制内容 - " + new Date().toISOString(); + copyToClipboard(testText); + addLog(`copyToClipboard → "${testText.slice(0, 30)}..."`); + } + + // 检测是否在 iframe 中 + const isInIframe = typeof window !== "undefined" && window.self !== window.top; if (!open) { return ( + {/* 场景 4:剪贴板复制(iframe 通信) */} + + + + ④ 剪贴板复制(iframe 通信) + + + {isInIframe ? "iframe 模式" : "独立页面"} + + + + + 📋 测试复制到剪贴板 + + + {isInIframe + ? "将通过 postMessage 请求父页面复制" + : "将直接调用 navigator.clipboard"} + + + + {/* 日志 */} {log.length > 0 && ( diff --git a/frontend/src/components/workspace/recent-chat-list.tsx b/frontend/src/components/workspace/recent-chat-list.tsx index 220aee22..546b1f46 100644 --- a/frontend/src/components/workspace/recent-chat-list.tsx +++ b/frontend/src/components/workspace/recent-chat-list.tsx @@ -39,6 +39,7 @@ import { } from "@/core/threads/hooks"; import { pathOfThread, titleOfThread } from "@/core/threads/utils"; import { env } from "@/env"; +import { copyToClipboard } from "@/lib/utils"; export function RecentChatList() { const { t } = useI18n(); @@ -102,7 +103,7 @@ export function RecentChatList() { const baseUrl = isLocalhost ? VERCEL_URL : window.location.origin; const shareUrl = `${baseUrl}/workspace/chats/${threadId}`; try { - await navigator.clipboard.writeText(shareUrl); + await copyToClipboard(shareUrl); toast.success(t.clipboard.linkCopied); } catch { toast.error(t.clipboard.failedToCopyToClipboard); diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index f8ff63a2..991b94bf 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -10,3 +10,26 @@ export const externalLinkClass = "text-primary underline underline-offset-2 hover:no-underline"; /** Link style without underline by default (e.g. for streaming/loading). */ export const externalLinkClassNoUnderline = "text-primary hover:underline"; + +/** + * Copy text to clipboard, using postMessage when in iframe. + * In iframe context, sends message to parent window to handle clipboard operation. + */ +export async function copyToClipboard(text: string): Promise { + const isInIframe = window.self !== window.top; + const message = { + type: "copyToClipboard", + data: text, + }; + + if (isInIframe && window.parent) { + // Request parent window to copy + window.parent.postMessage(message, "*"); + console.log("[copyToClipboard] iframe mode → postMessage to parent", message); + return; + } + + // Direct clipboard access when not in iframe + console.log("[copyToClipboard] direct mode", message); + await navigator.clipboard.writeText(text); +}