refactor: markdown render

This commit is contained in:
kangfenmao 2024-07-09 15:49:28 +08:00
parent 88aefb6ad1
commit 232892b71c
14 changed files with 977 additions and 78 deletions

View File

@ -30,43 +30,45 @@
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^1.0.1",
"@electron-toolkit/tsconfig": "^1.0.1",
"@fontsource/inter": "^5.0.18",
"@reduxjs/toolkit": "^2.2.5",
"@types/lodash": "^4.17.5",
"@types/node": "^18.19.9",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
"electron": "^28.2.0",
"electron-builder": "^24.9.1",
"electron-devtools-installer": "^3.2.0",
"electron-vite": "^2.0.0",
"eslint": "^8.56.0",
"eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^4.0.0",
"prettier": "^3.2.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.77.2",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"@fontsource/inter": "^5.0.18",
"@reduxjs/toolkit": "^2.2.5",
"ahooks": "^3.8.0",
"antd": "^5.18.3",
"browser-image-compression": "^2.0.2",
"dayjs": "^1.11.11",
"electron": "^28.2.0",
"electron-builder": "^24.9.1",
"electron-devtools-installer": "^3.2.0",
"electron-vite": "^2.0.0",
"emittery": "^1.0.3",
"highlight.js": "^11.9.0",
"eslint": "^8.56.0",
"eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^4.0.0",
"i18next": "^23.11.5",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"marked": "^13.0.1",
"openai": "^4.52.1",
"prettier": "^3.2.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.2",
"react-markdown": "^9.0.1",
"react-redux": "^9.1.2",
"react-router": "6",
"react-router-dom": "6",
"react-syntax-highlighter": "^15.5.0",
"redux-persist": "^6.0.0",
"sass": "^1.77.2",
"styled-components": "^6.1.11",
"uuid": "^10.0.0"
"typescript": "^5.3.3",
"uuid": "^10.0.0",
"vite": "^5.0.12"
},
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0",

View File

@ -10,6 +10,7 @@ import SettingsPage from './pages/settings/SettingsPage'
import { ConfigProvider } from 'antd'
import TopViewContainer from './components/TopView'
import { AntdThemeConfig } from './config/antd'
import './i18n'
function App(): JSX.Element {
return (

View File

@ -5,12 +5,6 @@
user-select: text;
margin-top: 4px;
.hljs {
background-color: transparent;
border: 1px solid #333;
border-radius: 3px;
}
p:first-of-type {
margin-top: 0;
}
@ -78,27 +72,7 @@
background-color: #555;
}
pre {
white-space: pre-wrap;
}
span {
word-break: break-all;
}
code {
font-weight: 600;
padding: 3px 5px;
border-radius: 2px;
font-size: 90%;
display: inline-block;
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
}
}

View File

@ -0,0 +1,40 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
const resources = {
'en-US': {
translation: {
settings: {
title: 'Settings',
general: 'General',
provider: 'Model Provider',
model: 'Model Settings',
assistant: 'Default Assistant',
about: 'About'
}
}
},
'zh-CN': {
translation: {
settings: {
title: '设置',
general: '常规',
provider: '模型提供商',
model: '模型设置',
assistant: '默认助手',
about: '关于'
}
}
}
}
i18n.use(initReactI18next).init({
resources,
lng: 'en-US',
fallbackLng: 'en-US',
interpolation: {
escapeValue: false
}
})
export default i18n

View File

@ -4,7 +4,7 @@ import styled from 'styled-components'
import Inputbar from './Inputbar'
import Messages from './Messages'
import { Flex } from 'antd'
import TopicList from './TopicList'
import Topics from './Topics'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useActiveTopic } from '@renderer/hooks/useTopic'
@ -26,7 +26,7 @@ const Chat: FC<Props> = (props) => {
<Messages assistant={assistant} topic={activeTopic} />
<Inputbar assistant={assistant} setActiveTopic={setActiveTopic} />
</Flex>
<TopicList assistant={assistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
<Topics assistant={assistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
</Container>
)
}

View File

@ -0,0 +1,68 @@
import React from 'react'
import SyntaxHighlighter from 'react-syntax-highlighter'
import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
import styled from 'styled-components'
import { CopyOutlined } from '@ant-design/icons'
interface CodeBlockProps {
children: string
className?: string
[key: string]: any
}
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) => {
const match = /language-(\w+)/.exec(className || '')
const onCopy = () => {
navigator.clipboard.writeText(children)
window.message.success({ content: 'Copied!', key: 'copy-code' })
}
return match ? (
<div>
<CodeHeader>
<CodeLanguage>{match[1]}</CodeLanguage>
<CopyOutlined className="copy" onClick={onCopy} />
</CodeHeader>
<SyntaxHighlighter
{...rest}
language={match[1]}
style={atomDark}
customStyle={{ borderTopLeftRadius: 0, borderTopRightRadius: 0, marginTop: 0 }}>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
</div>
) : (
<code {...rest} className={className}>
{children}
</code>
)
}
const CodeHeader = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
font-size: 14px;
font-weight: bold;
background-color: #323232;
height: 40px;
padding: 0 10px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
.copy {
cursor: pointer;
color: var(--color-text-3);
transition: color 0.3s;
}
.copy:hover {
color: var(--color-text-1);
}
`
const CodeLanguage = styled.div`
font-weight: bold;
`
export default CodeBlock

View File

@ -37,7 +37,8 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
content: text,
assistantId: assistant.id,
topicId: assistant.topics[0].id || uuid(),
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss')
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
status: 'success'
}
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
@ -70,9 +71,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
setActiveTopic(topic)
}, [addTopic, setActiveTopic])
const clearTopic = () => {
EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES)
}
const clearTopic = () => EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES)
// Command or Ctrl + N create new topic
useEffect(() => {

View File

@ -1,11 +1,12 @@
import { Message } from '@renderer/types'
import { Avatar } from 'antd'
import { marked } from 'marked'
import { FC } from 'react'
import styled from 'styled-components'
import Logo from '@renderer/assets/images/logo.png'
import useAvatar from '@renderer/hooks/useAvatar'
import { CopyOutlined, DeleteOutlined } from '@ant-design/icons'
import Markdown from 'react-markdown'
import CodeBlock from './CodeBlock'
interface Props {
message: Message
@ -36,7 +37,10 @@ const MessageItem: FC<Props> = ({ message, showMenu, onDeleteMessage }) => {
<MessageContainer key={message.id}>
<AvatarWrapper>{message.role === 'assistant' ? <Avatar src={Logo} /> : <Avatar src={avatar} />}</AvatarWrapper>
<MessageContent>
<div className="markdown" dangerouslySetInnerHTML={{ __html: marked(message.content) }} />
<Markdown className="markdown" components={{ code: CodeBlock as any }}>
{message.content}
</Markdown>
{/* <Markdown className="markdown" dangerouslySetInnerHTML={{ __html: marked(message.content) }} /> */}
{showMenu && (
<MenusBar className="menubar">
<CopyOutlined onClick={onCopy} />
@ -60,6 +64,8 @@ const AvatarWrapper = styled.div`
margin-right: 10px;
`
// const Markdown = styled.div``
const MessageContent = styled.div`
display: flex;
flex-direction: column;

View File

@ -5,7 +5,6 @@ import { FC, useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
import MessageItem from './Message'
import { reverse } from 'lodash'
import hljs from 'highlight.js'
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { DEFAULT_TOPIC_NAME } from '@renderer/config/constant'
@ -91,8 +90,6 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
})
}, [topic.id])
useEffect(() => hljs.highlightAll(), [messages, lastMessage])
useEffect(() => {
messagesRef.current?.scrollTo({ top: 100000, behavior: 'auto' })
}, [messages])

View File

@ -15,7 +15,7 @@ interface Props {
setActiveTopic: (topic: Topic) => void
}
const TopicList: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
const { showRightSidebar } = useShowRightSidebar()
const { removeTopic, updateTopic, removeAllTopics } = useAssistant(assistant.id)
const currentTopic = useRef<Topic | null>(null)
@ -162,4 +162,4 @@ const DeleteIcon = styled(DeleteOutlined)`
font-size: 16px;
`
export default TopicList
export default Topics

View File

@ -7,33 +7,35 @@ import AboutSettings from './AboutSettings'
import AssistantSettings from './AssistantSettings'
import ModelSettings from './ModelSettings'
import ProviderSettings from './ProviderSettings'
import { useTranslation } from 'react-i18next'
const SettingsPage: FC = () => {
const { pathname } = useLocation()
const { t } = useTranslation()
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
return (
<Container>
<Navbar>
<NavbarCenter>Settings</NavbarCenter>
<NavbarCenter>{t('settings.title')}</NavbarCenter>
</Navbar>
<ContentContainer>
<SettingMenus>
<MenuItemLink to="/settings/general">
<MenuItem className={isRoute('/settings/general')}>General</MenuItem>
<MenuItem className={isRoute('/settings/general')}>{t('settings.general')}</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/provider">
<MenuItem className={isRoute('/settings/provider')}>Model Provider</MenuItem>
<MenuItem className={isRoute('/settings/provider')}>{t('settings.provider')}</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/model">
<MenuItem className={isRoute('/settings/model')}>Model Settings</MenuItem>
<MenuItem className={isRoute('/settings/model')}>{t('settings.model')}</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/assistant">
<MenuItem className={isRoute('/settings/assistant')}>Default Assistant</MenuItem>
<MenuItem className={isRoute('/settings/assistant')}>{t('settings.assistant')}</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/about">
<MenuItem className={isRoute('/settings/about')}>About</MenuItem>
<MenuItem className={isRoute('/settings/about')}>{t('settings.about')}</MenuItem>
</MenuItemLink>
</SettingMenus>
<SettingContent>

View File

@ -35,7 +35,8 @@ export async function fetchChatCompletion({ messages, topic, assistant, onRespon
assistantId: assistant.id,
topicId: topic.id,
modelId: model.id,
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss')
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
status: 'pending'
}
try {
@ -60,6 +61,8 @@ export async function fetchChatCompletion({ messages, topic, assistant, onRespon
_message.content = `Error: ${error.message}`
}
_message.status = 'success'
EventEmitter.emit(EVENT_NAMES.AI_CHAT_COMPLETION, _message)
return _message

View File

@ -15,6 +15,7 @@ export type Message = {
topicId: string
modelId?: string
createdAt: string
status: 'pending' | 'success' | 'error'
}
export type Topic = {

832
yarn.lock

File diff suppressed because it is too large Load Diff