feat: implement copy message
This commit is contained in:
parent
320db5daeb
commit
8255f2d547
5 changed files with 86 additions and 40 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
38
components/chat-message-actions.tsx
Normal file
38
components/chat-message-actions.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
Loading…
Add table
Add a link
Reference in a new issue