feat: implement copy message

This commit is contained in:
shadcn 2023-06-13 23:58:59 +04:00
parent 320db5daeb
commit 8255f2d547
5 changed files with 86 additions and 40 deletions

View file

@ -20,7 +20,6 @@ if (!process.env.OPENAI_API_KEY) {
export const POST = auth(async function POST(req: Request) { export const POST = auth(async function POST(req: Request) {
const json = await req.json() const json = await req.json()
// @ts-ignore // @ts-ignore
console.log(req.auth) // todo fix types
const messages = json.messages.map((m: any) => ({ const messages = json.messages.map((m: any) => ({
content: m.content, content: m.content,
role: m.role role: m.role

View file

@ -0,0 +1,38 @@
'use client'
import { Button } from '@/components/ui/button'
import { IconCheck, IconCopy } from '@/components/ui/icons'
import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
import { cn } from '@/lib/utils'
import { type Message } from 'ai-connector'
interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
message: Message
}
export function ChatMessageActions({
message,
className,
...props
}: ChatMessageActionsProps) {
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
return (
<div
className={cn(
'flex items-center justify-end w-full md:absolute md:-top-2 md:-right-10 md:opacity-0 group-hover:opacity-100 transition-opacity',
className
)}
{...props}
>
<Button
variant="ghost"
size="icon"
onClick={() => copyToClipboard(message.content)}
>
{isCopied ? <IconCheck /> : <IconCopy />}
<span className="sr-only">Copy message</span>
</Button>
</div>
)
}

View file

@ -6,6 +6,7 @@ import { cn } from '@/lib/utils'
import { CodeBlock } from '@/components/ui/codeblock' import { CodeBlock } from '@/components/ui/codeblock'
import { MemoizedReactMarkdown } from '@/components/markdown' import { MemoizedReactMarkdown } from '@/components/markdown'
import { IconOpenAI, IconUser } from '@/components/ui/icons' import { IconOpenAI, IconUser } from '@/components/ui/icons'
import { ChatMessageActions } from '@/components/chat-message-actions'
export interface ChatMessageProps { export interface ChatMessageProps {
message: Message message: Message
@ -13,7 +14,10 @@ export interface ChatMessageProps {
export function ChatMessage({ message, ...props }: ChatMessageProps) { export function ChatMessage({ message, ...props }: ChatMessageProps) {
return ( return (
<div className={cn('relative mb-4 flex items-start md:-ml-12')} {...props}> <div
className={cn('relative mb-4 flex items-start group md:-ml-12')}
{...props}
>
<div <div
className={cn( className={cn(
'flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow', 'flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-md border shadow',
@ -24,44 +28,49 @@ export function ChatMessage({ message, ...props }: ChatMessageProps) {
> >
{message.role === 'user' ? <IconUser /> : <IconOpenAI />} {message.role === 'user' ? <IconUser /> : <IconOpenAI />}
</div> </div>
<div className="ml-4 px-1"> <div className="ml-4 px-1 space-y-2 flex-1">
<MemoizedReactMarkdown <div>
className="prose leading-6 dark:prose-invert prose-p:leading-[1.8rem] prose-pre:rounded-md prose-pre:bg-[#282c34]" <MemoizedReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]} className="prose leading-6 dark:prose-invert prose-p:leading-[1.8rem] prose-pre:rounded-md prose-pre:bg-[#282c34]"
components={{ remarkPlugins={[remarkGfm, remarkMath]}
p({ children }) { components={{
return <p className="mb-2 last:mb-0">{children}</p> p({ children }) {
}, return <p className="mb-2 last:mb-0">{children}</p>
code({ node, inline, className, children, ...props }) { },
if (children.length) { code({ node, inline, className, children, ...props }) {
if (children[0] == '▍') { if (children.length) {
return ( if (children[0] == '▍') {
<span className="mt-1 animate-pulse cursor-default"></span> return (
) <span className="mt-1 animate-pulse cursor-default">
</span>
)
}
children[0] = (children[0] as string).replace('`▍`', '▍')
} }
children[0] = (children[0] as string).replace('`▍`', '▍') const match = /language-(\w+)/.exec(className || '')
return !inline ? (
<CodeBlock
key={Math.random()}
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
} }
}}
const match = /language-(\w+)/.exec(className || '') >
{message.content}
return !inline ? ( </MemoizedReactMarkdown>
<CodeBlock </div>
key={Math.random()} <ChatMessageActions message={message} />
language={(match && match[1]) || ''}
value={String(children).replace(/\n$/, '')}
{...props}
/>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}
>
{message.content}
</MemoizedReactMarkdown>
</div> </div>
</div> </div>
) )

View file

@ -4,7 +4,7 @@ import { FC, memo } from 'react'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism' import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-cliipboard' import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
import { IconCheck, IconCopy, IconDownload } from '@/components/ui/icons' import { IconCheck, IconCopy, IconDownload } from '@/components/ui/icons'
interface Props { interface Props {

View file

@ -1,6 +1,6 @@
'use client' 'use client'
import { useState } from 'react' import * as React from 'react'
export interface useCopyToClipboardProps { export interface useCopyToClipboardProps {
timeout?: number timeout?: number
@ -9,7 +9,7 @@ export interface useCopyToClipboardProps {
export function useCopyToClipboard({ export function useCopyToClipboard({
timeout = 2000 timeout = 2000
}: useCopyToClipboardProps) { }: useCopyToClipboardProps) {
const [isCopied, setIsCopied] = useState<Boolean>(false) const [isCopied, setIsCopied] = React.useState<Boolean>(false)
const copyToClipboard = (value: string) => { const copyToClipboard = (value: string) => {
if ( if (