11import React from "react" ;
22import { Accordion , AccordionContent , AccordionItem , AccordionTrigger } from "@/components/ui/accordion" ;
33import { Badge } from "@/components/ui/badge" ;
4- import { Spin } from "./icons" ;
4+ import { Button } from "@/components/ui/button" ;
5+ import { Copy , Check , Spin } from "./icons" ;
56import Image from "next/image" ;
67import { Source } from "@/components/types" ;
78import ReactMarkdown from "react-markdown" ;
89import remarkGfm from "remark-gfm" ;
910import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" ;
1011import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism" ;
11- import type { Options } from "react-markdown" ;
1212
1313// Define interface for the UIMessage - matching what our useMorphikChat hook returns
1414export interface UIMessage {
@@ -68,6 +68,33 @@ const renderContent = (content: string, contentType: string) => {
6868 }
6969} ;
7070
71+ // Copy button component
72+ function CopyButton ( { content } : { content : string } ) {
73+ const [ copied , setCopied ] = React . useState ( false ) ;
74+
75+ const handleCopy = async ( ) => {
76+ try {
77+ await navigator . clipboard . writeText ( content ) ;
78+ setCopied ( true ) ;
79+ setTimeout ( ( ) => setCopied ( false ) , 2000 ) ;
80+ } catch ( err ) {
81+ console . error ( "Failed to copy text: " , err ) ;
82+ }
83+ } ;
84+
85+ return (
86+ < Button
87+ variant = "ghost"
88+ size = "sm"
89+ className = "h-8 w-8 p-0"
90+ onClick = { handleCopy }
91+ title = { copied ? "Copied!" : "Copy message" }
92+ >
93+ { copied ? < Check className = "h-4 w-4" /> : < Copy className = "h-4 w-4" /> }
94+ </ Button >
95+ ) ;
96+ }
97+
7198export function PreviewMessage ( { message } : Pick < MessageProps , "message" > ) {
7299 const sources = message . experimental_customData ?. sources ;
73100
@@ -77,47 +104,50 @@ export function PreviewMessage({ message }: Pick<MessageProps, "message">) {
77104 < div className = "flex w-full max-w-3xl items-start gap-4" >
78105 < div className = { `flex-1 space-y-2 overflow-hidden ${ message . role === "user" ? "" : "" } ` } >
79106 < div
80- className = { `rounded-xl p-4 ${
107+ className = { `relative rounded-xl p-4 ${
81108 message . role === "user" ? "ml-auto bg-primary text-primary-foreground" : "bg-muted"
82109 } `}
83110 >
111+ { message . role === "assistant" && (
112+ < div className = "absolute right-2 top-2" >
113+ < CopyButton content = { message . content } />
114+ </ div >
115+ ) }
84116 < div className = "prose prose-sm dark:prose-invert max-w-none break-words" >
85117 { message . role === "assistant" ? (
86118 < ReactMarkdown
87119 remarkPlugins = { [ remarkGfm ] }
88- components = {
89- {
90- code ( props ) {
91- const { children, className, ...rest } = props ;
92- const inline = ! className ?. includes ( "language-" ) ;
93- const match = / l a n g u a g e - ( \w + ) / . exec ( className || "" ) ;
120+ components = { {
121+ code ( props ) {
122+ const { children, className, ...rest } = props ;
123+ const inline = ! className ?. includes ( "language-" ) ;
124+ const match = / l a n g u a g e - ( \w + ) / . exec ( className || "" ) ;
94125
95- if ( ! inline && match ) {
96- const language = match [ 1 ] ;
97- return (
98- < div className = "my-4 overflow-hidden rounded-md" >
99- < SyntaxHighlighter style = { oneDark } language = { language } PreTag = "div" className = "!my-0" >
100- { String ( children ) . replace ( / \n $ / , "" ) }
101- </ SyntaxHighlighter >
102- </ div >
103- ) ;
104- } else if ( ! inline ) {
105- return (
106- < div className = "my-4 overflow-hidden rounded-md" >
107- < SyntaxHighlighter style = { oneDark } language = "text" PreTag = "div" className = "!my-0" >
108- { String ( children ) . replace ( / \n $ / , "" ) }
109- </ SyntaxHighlighter >
110- </ div >
111- ) ;
112- }
126+ if ( ! inline && match ) {
127+ const language = match [ 1 ] ;
128+ return (
129+ < div className = "my-4 overflow-hidden rounded-md" >
130+ < SyntaxHighlighter style = { oneDark } language = { language } PreTag = "div" className = "!my-0" >
131+ { String ( children ) . replace ( / \n $ / , "" ) }
132+ </ SyntaxHighlighter >
133+ </ div >
134+ ) ;
135+ } else if ( ! inline ) {
113136 return (
114- < code className = "rounded bg-muted px-1.5 py-0.5 font-mono text-sm" { ...rest } >
115- { children }
116- </ code >
137+ < div className = "my-4 overflow-hidden rounded-md" >
138+ < SyntaxHighlighter style = { oneDark } language = "text" PreTag = "div" className = "!my-0" >
139+ { String ( children ) . replace ( / \n $ / , "" ) }
140+ </ SyntaxHighlighter >
141+ </ div >
117142 ) ;
118- } ,
119- } as Options [ "components" ]
120- }
143+ }
144+ return (
145+ < code className = "rounded bg-muted px-1 py-0.5 font-mono text-sm" { ...rest } >
146+ { children }
147+ </ code >
148+ ) ;
149+ } ,
150+ } }
121151 >
122152 { message . content }
123153 </ ReactMarkdown >
0 commit comments