feat: add copy button on message footer
This commit is contained in:
parent
fa1f00f4f5
commit
e7f7f8509e
@ -8,6 +8,7 @@ import {
|
||||
PauseCircleOutlined,
|
||||
QuestionCircleOutlined
|
||||
} from '@ant-design/icons'
|
||||
import db from '@renderer/databases'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useRuntime, useShowTopics } from '@renderer/hooks/useStore'
|
||||
@ -120,6 +121,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
const topic = getDefaultTopic()
|
||||
addTopic(topic)
|
||||
setActiveTopic(topic)
|
||||
db.topics.add({ id: topic.id, messages: [] })
|
||||
}, [addTopic, setActiveTopic])
|
||||
|
||||
const clearTopic = async () => {
|
||||
|
||||
@ -3,7 +3,7 @@ import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { initMermaid } from '@renderer/init'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import React, { useState } from 'react'
|
||||
import React, { memo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { atomDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
@ -17,34 +17,23 @@ interface CodeBlockProps {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) => {
|
||||
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
const [copied, setCopied] = useState(false)
|
||||
const showFooterCopyButton = children && children.length > 500
|
||||
const { theme } = useTheme()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onCopy = () => {
|
||||
navigator.clipboard.writeText(children)
|
||||
window.message.success({ content: t('message.copied'), key: 'copy-code' })
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
if (match && match[1] === 'mermaid') {
|
||||
initMermaid(theme)
|
||||
return <Mermaid chart={children} />
|
||||
}
|
||||
|
||||
return match ? (
|
||||
<>
|
||||
<div className="code-block">
|
||||
<CodeHeader>
|
||||
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
|
||||
{!copied && <CopyIcon className="copy" onClick={onCopy} />}
|
||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||
<CopyButton text={children} />
|
||||
</CodeHeader>
|
||||
<SyntaxHighlighter
|
||||
{...rest}
|
||||
language={match[1]}
|
||||
style={theme === ThemeMode.dark ? atomDark : oneLight}
|
||||
wrapLongLines={true}
|
||||
@ -56,11 +45,32 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) =
|
||||
}}>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
</>
|
||||
{showFooterCopyButton && (
|
||||
<CodeFooter>
|
||||
<CopyButton text={children} style={{ marginTop: -40, marginRight: 10 }} />
|
||||
</CodeFooter>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<code {...rest} className={className}>
|
||||
{children}
|
||||
</code>
|
||||
<code className={className}>{children}</code>
|
||||
)
|
||||
}
|
||||
|
||||
const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ text, style }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onCopy = () => {
|
||||
navigator.clipboard.writeText(text)
|
||||
window.message.success({ content: t('message.copied'), key: 'copy-code' })
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
return copied ? (
|
||||
<CheckOutlined style={{ color: 'var(--color-primary)', ...style }} />
|
||||
) : (
|
||||
<CopyIcon className="copy" style={style} onClick={onCopy} />
|
||||
)
|
||||
}
|
||||
|
||||
@ -90,4 +100,19 @@ const CodeLanguage = styled.div`
|
||||
font-weight: bold;
|
||||
`
|
||||
|
||||
export default CodeBlock
|
||||
const CodeFooter = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
.copy {
|
||||
cursor: pointer;
|
||||
color: var(--color-text-3);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
.copy:hover {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
`
|
||||
|
||||
export default memo(CodeBlock)
|
||||
|
||||
@ -4,7 +4,7 @@ import { Message } from '@renderer/types'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { FC, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import ReactMarkdown, { Components } from 'react-markdown'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
@ -16,6 +16,14 @@ interface Props {
|
||||
message: Message
|
||||
}
|
||||
|
||||
const rehypePlugins = [rehypeKatex]
|
||||
const remarkPlugins = [remarkGfm, remarkMath]
|
||||
|
||||
const components = {
|
||||
code: CodeBlock,
|
||||
a: Link
|
||||
}
|
||||
|
||||
const Markdown: FC<Props> = ({ message }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -26,22 +34,20 @@ const Markdown: FC<Props> = ({ message }) => {
|
||||
return content
|
||||
}, [message.content, message.status, t])
|
||||
|
||||
return useMemo(() => {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className="markdown"
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
remarkPlugins={[remarkMath, remarkGfm]}
|
||||
remarkRehypeOptions={{
|
||||
footnoteLabel: t('common.footnotes'),
|
||||
footnoteLabelTagName: 'h4',
|
||||
footnoteBackContent: ' '
|
||||
}}
|
||||
components={{ code: CodeBlock as any, a: Link as any }}>
|
||||
{messageContent}
|
||||
</ReactMarkdown>
|
||||
)
|
||||
}, [messageContent, t])
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className="markdown"
|
||||
rehypePlugins={rehypePlugins}
|
||||
remarkPlugins={remarkPlugins}
|
||||
components={components as Partial<Components>}
|
||||
remarkRehypeOptions={{
|
||||
footnoteLabel: t('common.footnotes'),
|
||||
footnoteLabelTagName: 'h4',
|
||||
footnoteBackContent: ' '
|
||||
}}>
|
||||
{messageContent}
|
||||
</ReactMarkdown>
|
||||
)
|
||||
}
|
||||
|
||||
export default Markdown
|
||||
|
||||
@ -107,34 +107,6 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
[t, message]
|
||||
)
|
||||
|
||||
const MessageItem = useCallback(() => {
|
||||
if (message.status === 'sending') {
|
||||
return (
|
||||
<MessageContentLoading>
|
||||
<SyncOutlined spin size={24} />
|
||||
</MessageContentLoading>
|
||||
)
|
||||
}
|
||||
|
||||
if (message.status === 'error') {
|
||||
return (
|
||||
<Alert
|
||||
message={<div style={{ fontSize: 14 }}>{t('error.chat.response')}</div>}
|
||||
description={<Markdown message={message} />}
|
||||
type="error"
|
||||
style={{ marginBottom: 15, padding: 10, fontSize: 12 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Markdown message={message} />
|
||||
<MessageAttachments message={message} />
|
||||
</>
|
||||
)
|
||||
}, [message, t])
|
||||
|
||||
const showMiniApp = () => model?.provider && startMinAppById(model?.provider)
|
||||
|
||||
if (message.type === 'clear') {
|
||||
@ -175,8 +147,8 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
</UserWrap>
|
||||
</AvatarWrapper>
|
||||
</MessageHeader>
|
||||
<MessageContent style={{ fontFamily, fontSize }}>
|
||||
<MessageItem />
|
||||
<MessageContentContainer style={{ fontFamily, fontSize }}>
|
||||
<MessageContent message={message} />
|
||||
<MessageFooter style={{ border: messageBorder }}>
|
||||
{showMenu && (
|
||||
<MenusBar className={`menubar ${isLastMessage && 'show'} ${(!isLastMessage || isUserMessage) && 'user'}`}>
|
||||
@ -229,11 +201,41 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
</MessageMetadata>
|
||||
)}
|
||||
</MessageFooter>
|
||||
</MessageContent>
|
||||
</MessageContentContainer>
|
||||
</MessageContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const MessageContent: React.FC<{ message: Message }> = ({ message }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (message.status === 'sending') {
|
||||
return (
|
||||
<MessageContentLoading>
|
||||
<SyncOutlined spin size={24} />
|
||||
</MessageContentLoading>
|
||||
)
|
||||
}
|
||||
|
||||
if (message.status === 'error') {
|
||||
return (
|
||||
<Alert
|
||||
message={<div style={{ fontSize: 14 }}>{t('error.chat.response')}</div>}
|
||||
description={<Markdown message={message} />}
|
||||
type="error"
|
||||
style={{ marginBottom: 15, padding: 10, fontSize: 12 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Markdown message={message} />
|
||||
<MessageAttachments message={message} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const MessageContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -290,7 +292,7 @@ const MessageTime = styled.div`
|
||||
color: var(--color-text-3);
|
||||
`
|
||||
|
||||
const MessageContent = styled.div`
|
||||
const MessageContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
@ -38,7 +38,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
(message: Message) => {
|
||||
const _messages = [...messages, message]
|
||||
setMessages(_messages)
|
||||
db.topics.add({ id: topic.id, messages: _messages })
|
||||
db.topics.put({ id: topic.id, messages: _messages })
|
||||
},
|
||||
[messages, topic]
|
||||
)
|
||||
@ -142,7 +142,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
return (
|
||||
<Container id="messages" key={assistant.id} ref={containerRef}>
|
||||
<Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} />
|
||||
{lastMessage && <MessageItem message={lastMessage} />}
|
||||
{lastMessage && <MessageItem key={lastMessage.id} message={lastMessage} />}
|
||||
{reverse([...messages]).map((message, index) => (
|
||||
<MessageItem key={message.id} message={message} showMenu index={index} onDeleteMessage={onDeleteMessage} />
|
||||
))}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user