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 ''
+}