diff --git a/apps/sim/app/chat/components/message/components/markdown-renderer.tsx b/apps/sim/app/chat/components/message/components/markdown-renderer.tsx index 88a34da84e8..a62c901ae2f 100644 --- a/apps/sim/app/chat/components/message/components/markdown-renderer.tsx +++ b/apps/sim/app/chat/components/message/components/markdown-renderer.tsx @@ -2,6 +2,8 @@ import React, { type HTMLAttributes, memo, type ReactNode, useMemo } from 'react import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' import { Tooltip } from '@/components/emcn' +import { CopyCodeButton } from '@/components/ui/copy-code-button' +import { extractTextContent } from '@/lib/core/utils/react-node-text' export function LinkWithPreview({ href, children }: { href: string; children: React.ReactNode }) { return ( @@ -102,6 +104,10 @@ function createCustomComponents(LinkComponent: typeof LinkWithPreview) { {codeProps.className?.replace('language-', '') || 'code'} +
             {codeContent}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx
index 8249422b73b..018967deae2 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx
@@ -9,7 +9,9 @@ import 'prismjs/components/prism-css'
 import 'prismjs/components/prism-markup'
 import '@/components/emcn/components/code/code.css'
 import { Checkbox, highlight, languages } from '@/components/emcn'
+import { CopyCodeButton } from '@/components/ui/copy-code-button'
 import { cn } from '@/lib/core/utils/cn'
+import { extractTextContent } from '@/lib/core/utils/react-node-text'
 import {
   PendingTagIndicator,
   parseSpecialTags,
@@ -33,16 +35,6 @@ const LANG_ALIASES: Record = {
   py: 'python',
 }
 
-function extractTextContent(node: React.ReactNode): string {
-  if (typeof node === 'string') return node
-  if (typeof node === 'number') return String(node)
-  if (!node) return ''
-  if (Array.isArray(node)) return node.map(extractTextContent).join('')
-  if (isValidElement(node))
-    return extractTextContent((node.props as { children?: React.ReactNode }).children)
-  return ''
-}
-
 const PROSE_CLASSES = cn(
   'prose prose-base dark:prose-invert max-w-none',
   'font-[family-name:var(--font-inter)] antialiased break-words font-[430] tracking-[0]',
@@ -125,11 +117,13 @@ const MARKDOWN_COMPONENTS: React.ComponentProps['component
 
     return (
       
- {language && ( -
- {language} -
- )} +
+ {language || 'code'} + +
 | null>(null)
+
+  const handleCopy = useCallback(async () => {
+    await navigator.clipboard.writeText(code)
+    setCopied(true)
+    if (timerRef.current) clearTimeout(timerRef.current)
+    timerRef.current = setTimeout(() => setCopied(false), 2000)
+  }, [code])
+
+  useEffect(
+    () => () => {
+      if (timerRef.current) clearTimeout(timerRef.current)
+    },
+    []
+  )
+
+  return (
+    
+  )
+}
diff --git a/apps/sim/lib/core/utils/react-node-text.ts b/apps/sim/lib/core/utils/react-node-text.ts
new file mode 100644
index 00000000000..c59e865a9b2
--- /dev/null
+++ b/apps/sim/lib/core/utils/react-node-text.ts
@@ -0,0 +1,14 @@
+import { isValidElement, type ReactNode } from 'react'
+
+/**
+ * Recursively extracts plain text content from a React node tree.
+ */
+export function extractTextContent(node: ReactNode): string {
+  if (typeof node === 'string') return node
+  if (typeof node === 'number') return String(node)
+  if (!node) return ''
+  if (Array.isArray(node)) return node.map(extractTextContent).join('')
+  if (isValidElement(node))
+    return extractTextContent((node.props as { children?: ReactNode }).children)
+  return ''
+}