diff --git a/.gitignore b/.gitignore index af5de8c..482874a 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,8 @@ yarn-error.log* next-env.d.ts push-via-ec2.sh .claude/settings.local.json + +next +next-ai-draw-io@0.2.0 +object +starting diff --git a/components/chat-message-display.tsx b/components/chat-message-display.tsx index cab4f68..badc44f 100644 --- a/components/chat-message-display.tsx +++ b/components/chat-message-display.tsx @@ -7,6 +7,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import ExamplePanel from "./chat-example-panel"; import { UIMessage } from "ai"; import { convertToLegalXml, replaceNodes } from "@/lib/utils"; +import { Copy, Check } from "lucide-react"; import { useDiagram } from "@/contexts/diagram-context"; @@ -30,6 +31,50 @@ export function ChatMessageDisplay({ const [expandedTools, setExpandedTools] = useState>( {} ); + const [copiedMessageId, setCopiedMessageId] = useState(null); + + // 复制消息到剪贴板,支持非 HTTPS 环境的降级处理 + const copyMessageToClipboard = async (messageId: string, text: string) => { + try { + // 优先使用 Clipboard API(需要 HTTPS 或 localhost) + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text); + } else { + // 降级方案:使用传统的 execCommand 方法(兼容 HTTP 环境) + const textArea = document.createElement("textarea"); + textArea.value = text; + // 设置样式避免影响页面布局 + textArea.style.position = "fixed"; + textArea.style.left = "-9999px"; + textArea.style.top = "-9999px"; + textArea.style.opacity = "0"; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + const successful = document.execCommand("copy"); + document.body.removeChild(textArea); + + if (!successful) { + throw new Error("execCommand copy failed"); + } + } + setCopiedMessageId(messageId); + setTimeout(() => { + setCopiedMessageId(null); + }, 2000); + } catch (err) { + console.error("Failed to copy message:", err); + } + }; + + const getMessageTextContent = (message: UIMessage): string => { + if (!message.parts) return ""; + return message.parts + .filter((part: any) => part.type === "text") + .map((part: any) => part.text) + .join("\n"); + }; const handleDisplayChart = useCallback( (xml: string) => { const currentXml = xml || ""; @@ -160,51 +205,67 @@ export function ChatMessageDisplay({ {messages.length === 0 ? ( ) : ( - messages.map((message) => ( -
+ messages.map((message) => { + const userMessageText = message.role === "user" ? getMessageTextContent(message) : ""; + return (
- {message.parts?.map((part: any, index: number) => { - switch (part.type) { - case "text": - return ( -
{part.text}
- ); - case "file": - return ( -
- {`Uploaded -
- ); - default: - if (part.type?.startsWith("tool-")) { - return renderToolPart(part); - } - return null; - } - })} +
+ {message.parts?.map((part: any, index: number) => { + switch (part.type) { + case "text": + return ( +
{part.text}
+ ); + case "file": + return ( +
+ {`Uploaded +
+ ); + default: + if (part.type?.startsWith("tool-")) { + return renderToolPart(part); + } + return null; + } + })} +
+ {userMessageText && ( +
+ +
+ )}
-
- )) + ) + }) )} {error && (