feat: use ant.design

This commit is contained in:
kangfenmao 2024-06-30 14:03:13 +08:00
parent 33dbc88d60
commit 9313452490
24 changed files with 1192 additions and 481 deletions

View File

@ -1,14 +1,17 @@
module.exports = { module.exports = {
plugins: ['unused-imports'],
extends: [ extends: [
'eslint:recommended', 'eslint:recommended',
'plugin:react/recommended', 'plugin:react/recommended',
'plugin:react/jsx-runtime', 'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
'@electron-toolkit/eslint-config-ts/recommended', '@electron-toolkit/eslint-config-ts/recommended',
'@electron-toolkit/eslint-config-prettier' '@electron-toolkit/eslint-config-prettier'
], ],
rules: { rules: {
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'unused-imports/no-unused-imports': 'error',
'sort-imports': [ 'sort-imports': [
'error', 'error',
{ {

View File

@ -21,18 +21,21 @@
"build:linux": "electron-vite build && electron-builder --linux" "build:linux": "electron-vite build && electron-builder --linux"
}, },
"dependencies": { "dependencies": {
"@douyinfe/semi-ui": "^2.60.0",
"@electron-toolkit/preload": "^3.0.0", "@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5", "@emotion/styled": "^11.11.5",
"@fontsource/inter": "^5.0.18", "@fontsource/inter": "^5.0.18",
"@reduxjs/toolkit": "^2.2.5", "@reduxjs/toolkit": "^2.2.5",
"antd": "^5.18.3",
"electron-updater": "^6.1.7", "electron-updater": "^6.1.7",
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"emittery": "^1.0.3", "emittery": "^1.0.3",
"highlight.js": "^11.9.0",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "^13.0.1",
"openai": "^4.52.1",
"react-redux": "^9.1.2", "react-redux": "^9.1.2",
"react-router": "6", "react-router": "6",
"react-router-dom": "6", "react-router-dom": "6",
@ -54,7 +57,9 @@
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-vite": "^2.0.0", "electron-vite": "^2.0.0",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^4.0.0",
"prettier": "^3.2.4", "prettier": "^3.2.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@ -23,6 +23,7 @@ function createWindow(): void {
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
titleBarStyle: 'hiddenInset', titleBarStyle: 'hiddenInset',
transparent: true,
trafficLightPosition: { x: 8, y: 8 }, trafficLightPosition: { x: 8, y: 8 },
...(process.platform === 'linux' ? { icon } : {}), ...(process.platform === 'linux' ? { icon } : {}),
webPreferences: { webPreferences: {

View File

@ -7,7 +7,7 @@
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta <meta
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://at.alicdn.com; font-src 'self' https://at.alicdn.com; img-src 'self' data:" /> content="default-src 'self'; connect-src *; script-src 'self'; style-src 'self' 'unsafe-inline' *; font-src 'self' *; img-src 'self' data:" />
</head> </head>
<body theme-mode="dark"> <body theme-mode="dark">

View File

@ -1,129 +0,0 @@
@import 'https://at.alicdn.com/t/c/font_4563475_yuh5d3ftmm.css';
:root {
--ev-c-white: #ffffff;
--ev-c-white-soft: #f8f8f8;
--ev-c-white-mute: #f2f2f2;
--ev-c-black: #1b1b1f;
--ev-c-black-soft: #222222;
--ev-c-black-mute: #282828;
--ev-c-gray-1: #515c67;
--ev-c-gray-2: #414853;
--ev-c-gray-3: #32363f;
--ev-c-text-1: rgba(255, 255, 245, 0.86);
--ev-c-text-2: rgba(235, 235, 245, 0.6);
--ev-c-text-3: rgba(235, 235, 245, 0.38);
--ev-button-alt-border: transparent;
--ev-button-alt-text: var(--ev-c-text-1);
--ev-button-alt-bg: var(--ev-c-gray-3);
--ev-button-alt-hover-border: transparent;
--ev-button-alt-hover-text: var(--ev-c-text-1);
--ev-button-alt-hover-bg: var(--ev-c-gray-2);
--navbar-height: 40px;
--sidebar-width: 70px;
--conversations-width: 240px;
--settings-width: 280px;
--status-bar-height: 40px;
--color-background: #181818;
--color-background-soft: #222222;
--color-background-mute: var(--ev-c-black-mute);
--color-text: var(--ev-c-text-1);
--color-text-2: var(--ev-c-text-2);
--color-icon: #ffffff99;
--color-icon-white: #ffffff;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
ul {
list-style: none;
}
body {
display: flex;
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
line-height: 1.6;
overflow: hidden;
background-size: cover;
user-select: none;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-weight: 600;
padding: 3px 5px;
border-radius: 2px;
background-color: var(--color-background-mute);
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
font-size: 85%;
}
html,
body,
#root {
height: 100%;
margin: 0;
}
#root {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
flex: 1;
}
/*
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track {
background-color: #292f34;
}
::-webkit-scrollbar-thumb {
background-color: #444c51;
}
::-webkit-scrollbar-thumb:hover {
background-color: #444c51;
} */

View File

@ -7,21 +7,32 @@ import Sidebar from './components/app/Sidebar'
import AppsPage from './pages/apps/AppsPage' import AppsPage from './pages/apps/AppsPage'
import HomePage from './pages/home/HomePage' import HomePage from './pages/home/HomePage'
import SettingsPage from './pages/settings/SettingsPage' import SettingsPage from './pages/settings/SettingsPage'
import { ConfigProvider, theme } from 'antd'
function App(): JSX.Element { function App(): JSX.Element {
return ( return (
<Provider store={store}> <ConfigProvider
<PersistGate loading={null} persistor={persistor}> theme={{
<BrowserRouter> token: {
<Sidebar /> colorPrimary: '#00b96b',
<Routes> borderRadius: 5,
<Route path="" element={<HomePage />} /> colorBgContainer: '#f6ffed'
<Route path="/apps" element={<AppsPage />} /> },
<Route path="/settings/*" element={<SettingsPage />} /> algorithm: [theme.darkAlgorithm, theme.compactAlgorithm]
</Routes> }}>
</BrowserRouter> <Provider store={store}>
</PersistGate> <PersistGate loading={null} persistor={persistor}>
</Provider> <BrowserRouter>
<Sidebar />
<Routes>
<Route path="" element={<HomePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
</Routes>
</BrowserRouter>
</PersistGate>
</Provider>
</ConfigProvider>
) )
} }

View File

@ -0,0 +1,87 @@
@import 'https://at.alicdn.com/t/c/font_4563475_yuh5d3ftmm.css';
@import './markdown.scss';
:root {
--color-white: #ffffff;
--color-white-soft: #f8f8f8;
--color-white-mute: #f2f2f2;
--color-black: #1b1b1f;
--color-black-soft: #303030;
--color-black-mute: #363636;
--color-gray-1: #515c67;
--color-gray-2: #414853;
--color-gray-3: #32363f;
--color-text-1: rgba(255, 255, 245, 0.86);
--color-text-2: rgba(235, 235, 245, 0.6);
--color-text-3: rgba(235, 235, 245, 0.38);
--color-background: #1e1e1e;
--color-background-soft: var(--color-black-soft);
--color-background-mute: var(--color-black-mute);
--color-text: var(--color-text-1);
--color-icon: #ffffff99;
--color-icon-white: #ffffff;
--navbar-height: 40px;
--sidebar-width: 70px;
--conversations-width: 240px;
--settings-width: 280px;
--status-bar-height: 40px;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
ul {
list-style: none;
}
body {
display: flex;
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
line-height: 1.6;
overflow: hidden;
background-size: cover;
user-select: none;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html,
body,
#root {
height: 100%;
margin: 0;
}
#root {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
flex: 1;
}

View File

@ -0,0 +1,96 @@
.markdown {
width: 100%;
max-width: 800px;
color: #fff;
font-size: 14px;
line-height: 1.6;
user-select: text;
.hljs {
background-color: transparent;
border: 1px solid #333;
border-radius: 3px;
}
p:first-of-type {
margin-top: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 1.5em 0 0.5em 0;
font-weight: 800;
}
h1 {
font-size: 2em;
color: #fff;
}
h2 {
font-size: 1.5em;
color: #fff;
}
h3 {
font-size: 1.2em;
color: #fff;
}
h4 {
font-size: 1em;
color: #fff;
}
h5 {
font-size: 0.9em;
color: #fff;
}
h6 {
font-size: 0.8em;
color: #fff;
}
p {
margin: 1em 0;
color: #ccc;
}
ul,
ol {
padding-left: 1.5em;
margin: 1em 0;
color: #ccc;
}
li {
margin-bottom: 0.5em;
}
hr {
border: none;
border-top: 1px solid #555;
margin: 20px 0;
background-color: #555;
}
code {
font-weight: 600;
padding: 3px 5px;
border-radius: 2px;
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
font-size: 80%;
}
}

View File

@ -25,6 +25,7 @@ const NavbarContainer = styled.div`
flex-direction: row; flex-direction: row;
min-height: var(--navbar-height); min-height: var(--navbar-height);
max-height: var(--navbar-height); max-height: var(--navbar-height);
background-color: #111;
border-bottom: 1px solid #ffffff20; border-bottom: 1px solid #ffffff20;
-webkit-app-region: drag; -webkit-app-region: drag;
` `

View File

@ -45,8 +45,7 @@ const Container = styled.div`
padding: 16px 0; padding: 16px 0;
min-width: var(--sidebar-width); min-width: var(--sidebar-width);
min-height: 100%; min-height: 100%;
border-top: 1px solid #ffffff20; background: #262626;
border-right: 1px solid #ffffff20;
padding-top: 40px; padding-top: 40px;
padding-bottom: 10px; padding-bottom: 10px;
-webkit-app-region: drag !important; -webkit-app-region: drag !important;

View File

@ -7,17 +7,13 @@ import {
updateAgent updateAgent
} from '@renderer/store/agents' } from '@renderer/store/agents'
import { Agent } from '@renderer/types' import { Agent } from '@renderer/types'
import { useState } from 'react'
export default function useAgents() { export default function useAgents() {
const { agents } = useAppSelector((state) => state.agents) const { agents } = useAppSelector((state) => state.agents)
const [agentId, setAgentId] = useState(agents[0]?.id)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
return { return {
agents, agents,
agent: agents.find((t) => t.id === agentId),
setAgent: (agent: Agent) => setAgentId(agent.id),
addAgent: (agent: Agent) => dispatch(addAgent(agent)), addAgent: (agent: Agent) => dispatch(addAgent(agent)),
removeAgent: (id: string) => dispatch(removeAgent({ id })), removeAgent: (id: string) => dispatch(removeAgent({ id })),
updateAgent: (agent: Agent) => dispatch(updateAgent(agent)), updateAgent: (agent: Agent) => dispatch(updateAgent(agent)),

View File

@ -1,4 +1,4 @@
import './App.css' import './assets/styles/index.scss'
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App' import App from './App'

View File

@ -6,7 +6,7 @@ const AppsPage: FC = () => {
return ( return (
<Container> <Container>
<Navbar> <Navbar>
<NavbarCenter>APP</NavbarCenter> <NavbarCenter>Agent Market</NavbarCenter>
</Navbar> </Navbar>
</Container> </Container>
) )

View File

@ -1,17 +1,16 @@
import { Navbar, NavbarCenter, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import useAgents from '@renderer/hooks/useAgents' import useAgents from '@renderer/hooks/useAgents'
import { FC, useEffect } from 'react' import { FC, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Chat from './components/Chat' import Chat from './components/Chat'
import Agents from './components/Agents' import Agents from './components/Agents'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
import { Agent } from '@renderer/types'
import { last } from 'lodash'
const HomePage: FC = () => { const HomePage: FC = () => {
const { agents, agent, setAgent, addAgent } = useAgents() const { agents, addAgent } = useAgents()
const [activeAgent, setActiveAgent] = useState(agents[0])
useEffect(() => {
!agent && agents[0] && setAgent(agents[0])
}, [agent, agents])
const onCreateConversation = () => { const onCreateConversation = () => {
const _agent = { const _agent = {
@ -23,18 +22,23 @@ const HomePage: FC = () => {
conversations: [] conversations: []
} }
addAgent(_agent) addAgent(_agent)
setAgent(_agent) setActiveAgent(_agent)
}
const onRemoveAgent = (agent: Agent) => {
const _agent = last(agents.filter((a) => a.id !== agent.id))
_agent && setActiveAgent(_agent)
} }
return ( return (
<Container> <Container>
<Navbar> <Navbar>
<NavbarLeft style={{ justifyContent: 'flex-end' }}> <NavbarLeft style={{ justifyContent: 'flex-end', borderRight: 'none' }}>
<NewButton onClick={onCreateConversation}> <NewButton onClick={onCreateConversation}>
<i className="iconfont icon-a-addchat"></i> <i className="iconfont icon-a-addchat"></i>
</NewButton> </NewButton>
</NavbarLeft> </NavbarLeft>
<NavbarCenter style={{ border: 'none' }}>{agent?.name}</NavbarCenter> <NavbarCenter style={{ border: 'none' }}>{activeAgent?.name}</NavbarCenter>
<NavbarRight style={{ justifyContent: 'flex-end', padding: 5 }}> <NavbarRight style={{ justifyContent: 'flex-end', padding: 5 }}>
<NewButton> <NewButton>
<i className="iconfont icon-showsidebarhoriz"></i> <i className="iconfont icon-showsidebarhoriz"></i>
@ -42,8 +46,8 @@ const HomePage: FC = () => {
</NavbarRight> </NavbarRight>
</Navbar> </Navbar>
<ContentContainer> <ContentContainer>
<Agents /> <Agents activeAgent={activeAgent} onActive={setActiveAgent} onRemove={onRemoveAgent} />
{agent && <Chat agent={agent} />} <Chat agent={activeAgent} />
</ContentContainer> </ContentContainer>
</Container> </Container>
) )

View File

@ -1,25 +1,56 @@
import { FC } from 'react' import { FC, useRef } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { IconMore } from '@douyinfe/semi-icons'
import { Dropdown } from '@douyinfe/semi-ui'
import useAgents from '@renderer/hooks/useAgents' import useAgents from '@renderer/hooks/useAgents'
import { Agent } from '@renderer/types'
import { Dropdown, MenuProps } from 'antd'
import { EllipsisOutlined } from '@ant-design/icons'
const Agents: FC = () => { interface Props {
const { agents, setAgent, removeAgent } = useAgents() activeAgent: Agent
onActive: (agent: Agent) => void
onRemove: (agent: Agent) => void
}
const Agents: FC<Props> = ({ activeAgent, onActive, onRemove }) => {
const { agents, removeAgent } = useAgents()
const targetAgent = useRef<Agent | null>(null)
const onDelete = (agent: Agent) => {
removeAgent(agent.id)
onRemove(agent)
}
const items: MenuProps['items'] = [
{
label: 'Edit',
key: 'edit'
},
{
label: 'Favorite',
key: 'favorite'
},
{
label: 'Delete',
key: 'delete',
onClick: () => targetAgent.current && onDelete(targetAgent.current)
}
]
return ( return (
<Container> <Container>
{agents.map((agent) => ( {agents.map((agent) => (
<AgentItem key={agent.id} onClick={() => setAgent(agent)} className={agent.id === agent?.id ? 'active' : ''}> <AgentItem
<Dropdown key={agent.id}
trigger="click" onClick={() => onActive(agent)}
stopPropagation className={agent.id === activeAgent?.id ? 'active' : ''}>
render={ <Dropdown menu={{ items }} trigger={['click']} placement="bottom">
<Dropdown.Menu> <EllipsisOutlined
<Dropdown.Item onClick={() => removeAgent(agent.id)}>Delete</Dropdown.Item> style={{ position: 'absolute', right: 12, top: 12 }}
</Dropdown.Menu> onClick={(e) => {
}> e.stopPropagation()
<IconMore style={{ position: 'absolute', right: 12, top: 12 }} /> targetAgent.current = agent
}}
/>
</Dropdown> </Dropdown>
<AgentName>{agent.name}</AgentName> <AgentName>{agent.name}</AgentName>
<AgentLastMessage>{agent.lastMessage}</AgentLastMessage> <AgentLastMessage>{agent.lastMessage}</AgentLastMessage>
@ -35,9 +66,8 @@ const Container = styled.div`
flex-direction: column; flex-direction: column;
min-width: var(--conversations-width); min-width: var(--conversations-width);
max-width: var(--conversations-width); max-width: var(--conversations-width);
border-right: 1px solid #ffffff20; border-right: 0.5px solid #ffffff20;
height: calc(100vh - var(--navbar-height)); height: calc(100vh - var(--navbar-height));
padding: 10px;
overflow-y: scroll; overflow-y: scroll;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
@ -50,12 +80,12 @@ const AgentItem = styled.div`
padding: 10px; padding: 10px;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
.semi-icon { .anticon {
display: none; display: none;
} }
&:hover { &:hover {
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
.semi-icon { .anticon {
display: block; display: block;
} }
} }
@ -63,8 +93,6 @@ const AgentItem = styled.div`
background-color: var(--color-background-mute); background-color: var(--color-background-mute);
cursor: pointer; cursor: pointer;
} }
border-radius: 8px;
margin-bottom: 10px;
` `
const AgentTime = styled.div` const AgentTime = styled.div`

View File

@ -1,11 +1,8 @@
import { Message, Agent } from '@renderer/types' import { Agent } from '@renderer/types'
import { FC, useState } from 'react' import { FC, useEffect, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Inputbar from './Inputbar' import Inputbar from './Inputbar'
import Conversations from './Conversations' import Conversations from './Conversations'
import useAgents from '@renderer/hooks/useAgents'
import { isEmpty } from 'lodash'
import localforage from 'localforage'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
interface Props { interface Props {
@ -13,10 +10,18 @@ interface Props {
} }
const Chat: FC<Props> = ({ agent }) => { const Chat: FC<Props> = ({ agent }) => {
const [conversationId] = useState<string>(agent.conversations[0] || uuid()) const [conversationId, setConversationId] = useState<string>(agent?.conversations[0] || uuid())
useEffect(() => {
setConversationId(agent?.conversations[0] || uuid())
}, [agent])
if (!agent) {
return null
}
return ( return (
<Container> <Container id="chat">
<Conversations agent={agent} conversationId={conversationId} /> <Conversations agent={agent} conversationId={conversationId} />
<Inputbar agent={agent} /> <Inputbar agent={agent} />
</Container> </Container>
@ -28,7 +33,7 @@ const Container = styled.div`
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
flex: 1; flex: 1;
border-right: 1px solid #ffffff20; justify-content: space-between;
` `
export default Chat export default Chat

View File

@ -1,12 +1,14 @@
import { Avatar } from '@douyinfe/semi-ui'
import useAgents from '@renderer/hooks/useAgents' import useAgents from '@renderer/hooks/useAgents'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Conversation, Message, Agent } from '@renderer/types' import { openaiProvider } from '@renderer/services/provider'
import { runAsyncFunction } from '@renderer/utils' import { Agent, Conversation, Message } from '@renderer/types'
import { runAsyncFunction, uuid } from '@renderer/utils'
import 'highlight.js/styles/github-dark.css'
import localforage from 'localforage' import localforage from 'localforage'
import { isEmpty } from 'lodash' import { FC, useCallback, useEffect, useState } from 'react'
import { FC, useEffect, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import MessageItem from './Message'
import { reverse } from 'lodash'
interface Props { interface Props {
agent: Agent agent: Agent
@ -15,44 +17,81 @@ interface Props {
const Conversations: FC<Props> = ({ agent, conversationId }) => { const Conversations: FC<Props> = ({ agent, conversationId }) => {
const [messages, setMessages] = useState<Message[]>([]) const [messages, setMessages] = useState<Message[]>([])
const [lastMessage, setLastMessage] = useState<Message | null>(null)
const { addConversation } = useAgents() const { addConversation } = useAgents()
const onSendMessage = (message: Message) => { const onSendMessage = useCallback(
setMessages([...messages, message]) (message: Message) => {
const _messages = [...messages, message]
if (isEmpty(agent?.conversations)) { setMessages(_messages)
addConversation(agent.id, conversationId) addConversation(agent.id, conversationId)
} localforage.setItem<Conversation>(`conversation:${conversationId}`, {
id: conversationId,
messages: _messages
})
},
[addConversation, agent.id, conversationId, messages]
)
localforage.setItem<Conversation>(`conversation:${conversationId}`, { const fetchChatCompletion = useCallback(
id: conversationId, async (message: Message) => {
messages: [...messages, message] const stream = await openaiProvider.chat.completions.create({
}) model: 'Qwen/Qwen2-7B-Instruct',
} messages: [{ role: 'user', content: message.content }],
stream: true
})
const _message: Message = {
id: uuid(),
content: '',
agentId: agent.id,
conversationId,
createdAt: 'now'
}
let content = ''
for await (const chunk of stream) {
content = content + (chunk.choices[0]?.delta?.content || '')
setLastMessage({ ..._message, content })
}
_message.content = content
EventEmitter.emit(EVENT_NAMES.AI_CHAT_COMPLETION, _message)
return _message
},
[agent.id, conversationId]
)
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, async (msg: Message) => {
onSendMessage(msg)
fetchChatCompletion(msg)
}),
EventEmitter.on(EVENT_NAMES.AI_CHAT_COMPLETION, async (msg: Message) => {
setLastMessage(null)
onSendMessage(msg)
})
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [fetchChatCompletion, onSendMessage])
useEffect(() => { useEffect(() => {
runAsyncFunction(async () => { runAsyncFunction(async () => {
const conversation = await localforage.getItem<Conversation>(`conversation:${conversationId}`) const conversation = await localforage.getItem<Conversation>(`conversation:${conversationId}`)
conversation && setMessages(conversation.messages) console.debug('conversation', conversation)
setMessages(conversation ? conversation.messages : [])
}) })
}, [conversationId]) }, [conversationId])
useEffect(() => {
const unsubscribe = EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, onSendMessage)
return () => unsubscribe()
}, [onSendMessage])
return ( return (
<Container> <Container id="conversations">
{messages.map((message) => ( {lastMessage && <MessageItem message={lastMessage} />}
<ConversationItem key={message.id}> {reverse([...messages]).map((message) => (
<AvatarWrapper> <MessageItem message={message} key={message.id} />
<Avatar size="small" alt="Alice Swift">
Y
</Avatar>
</AvatarWrapper>
<div>{message.content}</div>
</ConversationItem>
))} ))}
</Container> </Container>
) )
@ -61,26 +100,11 @@ const Conversations: FC<Props> = ({ agent, conversationId }) => {
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1;
overflow-y: scroll; overflow-y: scroll;
flex-direction: column-reverse;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
} }
` `
const ConversationItem = styled.div`
display: flex;
flex-direction: row;
padding: 10px 15px;
position: relative;
cursor: pointer;
&:hover {
background-color: var(--color-background-soft);
}
`
const AvatarWrapper = styled.div`
margin-right: 10px;
`
export default Conversations export default Conversations

View File

@ -1,5 +1,5 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Message, Agent } from '@renderer/types' import { Agent, Message } from '@renderer/types'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@ -44,14 +44,15 @@ const Inputbar: FC<Props> = ({ agent }) => {
const Textarea = styled.textarea` const Textarea = styled.textarea`
padding: 15px; padding: 15px;
width: 100%; width: 100%;
height: 100px; height: 120px;
min-height: 120px;
border: none; border: none;
outline: none; outline: none;
resize: none; resize: none;
font-size: 14px; font-size: 14px;
color: var(--color-text); color: var(--color-text);
background-color: transparent; background-color: transparent;
border-top: 1px solid #ffffff20; border-top: 0.5px solid #ffffff20;
` `
export default Inputbar export default Inputbar

View File

@ -0,0 +1,34 @@
import { Message } from '@renderer/types'
import { Avatar } from 'antd'
import hljs from 'highlight.js'
import { marked } from 'marked'
import { FC, useEffect } from 'react'
import styled from 'styled-components'
const MessageItem: FC<{ message: Message }> = ({ message }) => {
useEffect(() => {
hljs.highlightAll()
})
return (
<MessageContainer key={message.id}>
<AvatarWrapper>
<Avatar alt="Alice Swift">Y</Avatar>
</AvatarWrapper>
<div className="markdown" dangerouslySetInnerHTML={{ __html: marked(message.content) }}></div>
</MessageContainer>
)
}
const MessageContainer = styled.div`
display: flex;
flex-direction: row;
padding: 10px 15px;
position: relative;
`
const AvatarWrapper = styled.div`
margin-right: 10px;
`
export default MessageItem

View File

@ -3,5 +3,6 @@ import Emittery from 'emittery'
export const EventEmitter = new Emittery() export const EventEmitter = new Emittery()
export const EVENT_NAMES = { export const EVENT_NAMES = {
SEND_MESSAGE: 'SEND_MESSAGE' SEND_MESSAGE: 'SEND_MESSAGE',
AI_CHAT_COMPLETION: 'AI_CHAT_COMPLETION'
} }

View File

@ -0,0 +1,7 @@
import OpenAI from 'openai'
export const openaiProvider = new OpenAI({
dangerouslyAllowBrowser: true,
apiKey: 'sk-cmxcwkapuoxpddlytqpuxxszyqymqgrcxremulcdlgcgabtq',
baseURL: 'https://api.siliconflow.cn/v1'
})

View File

@ -4,6 +4,10 @@ import { FLUSH, PAUSE, PERSIST, persistReducer, persistStore, PURGE, REGISTER, R
import storage from 'redux-persist/lib/storage' import storage from 'redux-persist/lib/storage'
import agents from './agents' import agents from './agents'
const rootReducer = combineReducers({
agents
})
const store = configureStore({ const store = configureStore({
reducer: persistReducer( reducer: persistReducer(
{ {
@ -11,9 +15,7 @@ const store = configureStore({
storage, storage,
version: 1 version: 1
}, },
combineReducers({ rootReducer
agents
})
), ),
middleware: (getDefaultMiddleware) => { middleware: (getDefaultMiddleware) => {
return getDefaultMiddleware({ return getDefaultMiddleware({
@ -25,13 +27,12 @@ const store = configureStore({
devTools: true devTools: true
}) })
export type RootState = ReturnType<typeof store.getState> export type RootState = ReturnType<typeof rootReducer>
export type AppDispatch = typeof store.dispatch export type AppDispatch = typeof store.dispatch
export const persistor = persistStore(store) export const persistor = persistStore(store)
export const useAppDispatch = useDispatch.withTypes<AppDispatch>() export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>() export const useAppSelector = useSelector.withTypes<RootState>()
export const useAppStore = useStore.withTypes<typeof store>() export const useAppStore = useStore.withTypes<typeof store>()
// export const dispatch: AppDispatch = useDispatch()
export default store export default store

View File

@ -26,12 +26,3 @@ export type User = {
avatar: string avatar: string
email: string email: string
} }
export type Agent = {
id: string
name: string
description: string
avatar: string
model: string
default: boolean
}

993
yarn.lock

File diff suppressed because it is too large Load Diff