refactor: markdown render
This commit is contained in:
parent
88aefb6ad1
commit
232892b71c
40
package.json
40
package.json
@ -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",
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
40
src/renderer/src/i18n/index.ts
Normal file
40
src/renderer/src/i18n/index.ts
Normal 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
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
68
src/renderer/src/pages/home/components/CodeBlock.tsx
Normal file
68
src/renderer/src/pages/home/components/CodeBlock.tsx
Normal 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
|
||||
@ -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(() => {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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])
|
||||
|
||||
@ -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
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -15,6 +15,7 @@ export type Message = {
|
||||
topicId: string
|
||||
modelId?: string
|
||||
createdAt: string
|
||||
status: 'pending' | 'success' | 'error'
|
||||
}
|
||||
|
||||
export type Topic = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user