feat: use ant.design
This commit is contained in:
parent
33dbc88d60
commit
9313452490
@ -1,14 +1,17 @@
|
||||
module.exports = {
|
||||
plugins: ['unused-imports'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:react-hooks/recommended',
|
||||
'@electron-toolkit/eslint-config-ts/recommended',
|
||||
'@electron-toolkit/eslint-config-prettier'
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'sort-imports': [
|
||||
'error',
|
||||
{
|
||||
|
||||
@ -21,18 +21,21 @@
|
||||
"build:linux": "electron-vite build && electron-builder --linux"
|
||||
},
|
||||
"dependencies": {
|
||||
"@douyinfe/semi-ui": "^2.60.0",
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@fontsource/inter": "^5.0.18",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"antd": "^5.18.3",
|
||||
"electron-updater": "^6.1.7",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"emittery": "^1.0.3",
|
||||
"highlight.js": "^11.9.0",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^13.0.1",
|
||||
"openai": "^4.52.1",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router": "6",
|
||||
"react-router-dom": "6",
|
||||
@ -54,7 +57,9 @@
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-vite": "^2.0.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",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@ -23,6 +23,7 @@ function createWindow(): void {
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
transparent: true,
|
||||
trafficLightPosition: { x: 8, y: 8 },
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta
|
||||
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>
|
||||
|
||||
<body theme-mode="dark">
|
||||
|
||||
@ -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;
|
||||
} */
|
||||
@ -7,9 +7,19 @@ import Sidebar from './components/app/Sidebar'
|
||||
import AppsPage from './pages/apps/AppsPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import { ConfigProvider, theme } from 'antd'
|
||||
|
||||
function App(): JSX.Element {
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: '#00b96b',
|
||||
borderRadius: 5,
|
||||
colorBgContainer: '#f6ffed'
|
||||
},
|
||||
algorithm: [theme.darkAlgorithm, theme.compactAlgorithm]
|
||||
}}>
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<BrowserRouter>
|
||||
@ -22,6 +32,7 @@ function App(): JSX.Element {
|
||||
</BrowserRouter>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
</ConfigProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
87
src/renderer/src/assets/styles/index.scss
Normal file
87
src/renderer/src/assets/styles/index.scss
Normal 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;
|
||||
}
|
||||
96
src/renderer/src/assets/styles/markdown.scss
Normal file
96
src/renderer/src/assets/styles/markdown.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
@ -25,6 +25,7 @@ const NavbarContainer = styled.div`
|
||||
flex-direction: row;
|
||||
min-height: var(--navbar-height);
|
||||
max-height: var(--navbar-height);
|
||||
background-color: #111;
|
||||
border-bottom: 1px solid #ffffff20;
|
||||
-webkit-app-region: drag;
|
||||
`
|
||||
|
||||
@ -45,8 +45,7 @@ const Container = styled.div`
|
||||
padding: 16px 0;
|
||||
min-width: var(--sidebar-width);
|
||||
min-height: 100%;
|
||||
border-top: 1px solid #ffffff20;
|
||||
border-right: 1px solid #ffffff20;
|
||||
background: #262626;
|
||||
padding-top: 40px;
|
||||
padding-bottom: 10px;
|
||||
-webkit-app-region: drag !important;
|
||||
|
||||
@ -7,17 +7,13 @@ import {
|
||||
updateAgent
|
||||
} from '@renderer/store/agents'
|
||||
import { Agent } from '@renderer/types'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function useAgents() {
|
||||
const { agents } = useAppSelector((state) => state.agents)
|
||||
const [agentId, setAgentId] = useState(agents[0]?.id)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
agents,
|
||||
agent: agents.find((t) => t.id === agentId),
|
||||
setAgent: (agent: Agent) => setAgentId(agent.id),
|
||||
addAgent: (agent: Agent) => dispatch(addAgent(agent)),
|
||||
removeAgent: (id: string) => dispatch(removeAgent({ id })),
|
||||
updateAgent: (agent: Agent) => dispatch(updateAgent(agent)),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import './App.css'
|
||||
import './assets/styles/index.scss'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
|
||||
@ -6,7 +6,7 @@ const AppsPage: FC = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter>APP</NavbarCenter>
|
||||
<NavbarCenter>Agent Market</NavbarCenter>
|
||||
</Navbar>
|
||||
</Container>
|
||||
)
|
||||
|
||||
@ -1,17 +1,16 @@
|
||||
import { Navbar, NavbarCenter, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import useAgents from '@renderer/hooks/useAgents'
|
||||
import { FC, useEffect } from 'react'
|
||||
import { FC, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Chat from './components/Chat'
|
||||
import Agents from './components/Agents'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { Agent } from '@renderer/types'
|
||||
import { last } from 'lodash'
|
||||
|
||||
const HomePage: FC = () => {
|
||||
const { agents, agent, setAgent, addAgent } = useAgents()
|
||||
|
||||
useEffect(() => {
|
||||
!agent && agents[0] && setAgent(agents[0])
|
||||
}, [agent, agents])
|
||||
const { agents, addAgent } = useAgents()
|
||||
const [activeAgent, setActiveAgent] = useState(agents[0])
|
||||
|
||||
const onCreateConversation = () => {
|
||||
const _agent = {
|
||||
@ -23,18 +22,23 @@ const HomePage: FC = () => {
|
||||
conversations: []
|
||||
}
|
||||
addAgent(_agent)
|
||||
setAgent(_agent)
|
||||
setActiveAgent(_agent)
|
||||
}
|
||||
|
||||
const onRemoveAgent = (agent: Agent) => {
|
||||
const _agent = last(agents.filter((a) => a.id !== agent.id))
|
||||
_agent && setActiveAgent(_agent)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarLeft style={{ justifyContent: 'flex-end' }}>
|
||||
<NavbarLeft style={{ justifyContent: 'flex-end', borderRight: 'none' }}>
|
||||
<NewButton onClick={onCreateConversation}>
|
||||
<i className="iconfont icon-a-addchat"></i>
|
||||
</NewButton>
|
||||
</NavbarLeft>
|
||||
<NavbarCenter style={{ border: 'none' }}>{agent?.name}</NavbarCenter>
|
||||
<NavbarCenter style={{ border: 'none' }}>{activeAgent?.name}</NavbarCenter>
|
||||
<NavbarRight style={{ justifyContent: 'flex-end', padding: 5 }}>
|
||||
<NewButton>
|
||||
<i className="iconfont icon-showsidebarhoriz"></i>
|
||||
@ -42,8 +46,8 @@ const HomePage: FC = () => {
|
||||
</NavbarRight>
|
||||
</Navbar>
|
||||
<ContentContainer>
|
||||
<Agents />
|
||||
{agent && <Chat agent={agent} />}
|
||||
<Agents activeAgent={activeAgent} onActive={setActiveAgent} onRemove={onRemoveAgent} />
|
||||
<Chat agent={activeAgent} />
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
)
|
||||
|
||||
@ -1,25 +1,56 @@
|
||||
import { FC } from 'react'
|
||||
import { FC, useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { IconMore } from '@douyinfe/semi-icons'
|
||||
import { Dropdown } from '@douyinfe/semi-ui'
|
||||
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 = () => {
|
||||
const { agents, setAgent, removeAgent } = useAgents()
|
||||
interface Props {
|
||||
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 (
|
||||
<Container>
|
||||
{agents.map((agent) => (
|
||||
<AgentItem key={agent.id} onClick={() => setAgent(agent)} className={agent.id === agent?.id ? 'active' : ''}>
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
stopPropagation
|
||||
render={
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item onClick={() => removeAgent(agent.id)}>Delete</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
}>
|
||||
<IconMore style={{ position: 'absolute', right: 12, top: 12 }} />
|
||||
<AgentItem
|
||||
key={agent.id}
|
||||
onClick={() => onActive(agent)}
|
||||
className={agent.id === activeAgent?.id ? 'active' : ''}>
|
||||
<Dropdown menu={{ items }} trigger={['click']} placement="bottom">
|
||||
<EllipsisOutlined
|
||||
style={{ position: 'absolute', right: 12, top: 12 }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
targetAgent.current = agent
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
<AgentName>{agent.name}</AgentName>
|
||||
<AgentLastMessage>{agent.lastMessage}</AgentLastMessage>
|
||||
@ -35,9 +66,8 @@ const Container = styled.div`
|
||||
flex-direction: column;
|
||||
min-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));
|
||||
padding: 10px;
|
||||
overflow-y: scroll;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
@ -50,12 +80,12 @@ const AgentItem = styled.div`
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
.semi-icon {
|
||||
.anticon {
|
||||
display: none;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
.semi-icon {
|
||||
.anticon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@ -63,8 +93,6 @@ const AgentItem = styled.div`
|
||||
background-color: var(--color-background-mute);
|
||||
cursor: pointer;
|
||||
}
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
|
||||
const AgentTime = styled.div`
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
import { Message, Agent } from '@renderer/types'
|
||||
import { FC, useState } from 'react'
|
||||
import { Agent } from '@renderer/types'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Inputbar from './Inputbar'
|
||||
import Conversations from './Conversations'
|
||||
import useAgents from '@renderer/hooks/useAgents'
|
||||
import { isEmpty } from 'lodash'
|
||||
import localforage from 'localforage'
|
||||
import { uuid } from '@renderer/utils'
|
||||
|
||||
interface Props {
|
||||
@ -13,10 +10,18 @@ interface Props {
|
||||
}
|
||||
|
||||
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 (
|
||||
<Container>
|
||||
<Container id="chat">
|
||||
<Conversations agent={agent} conversationId={conversationId} />
|
||||
<Inputbar agent={agent} />
|
||||
</Container>
|
||||
@ -28,7 +33,7 @@ const Container = styled.div`
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
border-right: 1px solid #ffffff20;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
export default Chat
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import { Avatar } from '@douyinfe/semi-ui'
|
||||
import useAgents from '@renderer/hooks/useAgents'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { Conversation, Message, Agent } from '@renderer/types'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { openaiProvider } from '@renderer/services/provider'
|
||||
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 { isEmpty } from 'lodash'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import MessageItem from './Message'
|
||||
import { reverse } from 'lodash'
|
||||
|
||||
interface Props {
|
||||
agent: Agent
|
||||
@ -15,44 +17,81 @@ interface Props {
|
||||
|
||||
const Conversations: FC<Props> = ({ agent, conversationId }) => {
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
const [lastMessage, setLastMessage] = useState<Message | null>(null)
|
||||
const { addConversation } = useAgents()
|
||||
|
||||
const onSendMessage = (message: Message) => {
|
||||
setMessages([...messages, message])
|
||||
|
||||
if (isEmpty(agent?.conversations)) {
|
||||
const onSendMessage = useCallback(
|
||||
(message: Message) => {
|
||||
const _messages = [...messages, message]
|
||||
setMessages(_messages)
|
||||
addConversation(agent.id, conversationId)
|
||||
}
|
||||
|
||||
localforage.setItem<Conversation>(`conversation:${conversationId}`, {
|
||||
id: conversationId,
|
||||
messages: [...messages, message]
|
||||
messages: _messages
|
||||
})
|
||||
},
|
||||
[addConversation, agent.id, conversationId, messages]
|
||||
)
|
||||
|
||||
const fetchChatCompletion = useCallback(
|
||||
async (message: 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(() => {
|
||||
runAsyncFunction(async () => {
|
||||
const conversation = await localforage.getItem<Conversation>(`conversation:${conversationId}`)
|
||||
conversation && setMessages(conversation.messages)
|
||||
console.debug('conversation', conversation)
|
||||
setMessages(conversation ? conversation.messages : [])
|
||||
})
|
||||
}, [conversationId])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, onSendMessage)
|
||||
return () => unsubscribe()
|
||||
}, [onSendMessage])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{messages.map((message) => (
|
||||
<ConversationItem key={message.id}>
|
||||
<AvatarWrapper>
|
||||
<Avatar size="small" alt="Alice Swift">
|
||||
Y
|
||||
</Avatar>
|
||||
</AvatarWrapper>
|
||||
<div>{message.content}</div>
|
||||
</ConversationItem>
|
||||
<Container id="conversations">
|
||||
{lastMessage && <MessageItem message={lastMessage} />}
|
||||
{reverse([...messages]).map((message) => (
|
||||
<MessageItem message={message} key={message.id} />
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
@ -61,26 +100,11 @@ const Conversations: FC<Props> = ({ agent, conversationId }) => {
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
flex-direction: column-reverse;
|
||||
&::-webkit-scrollbar {
|
||||
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
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 { FC, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
@ -44,14 +44,15 @@ const Inputbar: FC<Props> = ({ agent }) => {
|
||||
const Textarea = styled.textarea`
|
||||
padding: 15px;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
height: 120px;
|
||||
min-height: 120px;
|
||||
border: none;
|
||||
outline: none;
|
||||
resize: none;
|
||||
font-size: 14px;
|
||||
color: var(--color-text);
|
||||
background-color: transparent;
|
||||
border-top: 1px solid #ffffff20;
|
||||
border-top: 0.5px solid #ffffff20;
|
||||
`
|
||||
|
||||
export default Inputbar
|
||||
|
||||
34
src/renderer/src/pages/home/components/Message.tsx
Normal file
34
src/renderer/src/pages/home/components/Message.tsx
Normal 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
|
||||
@ -3,5 +3,6 @@ import Emittery from 'emittery'
|
||||
export const EventEmitter = new Emittery()
|
||||
|
||||
export const EVENT_NAMES = {
|
||||
SEND_MESSAGE: 'SEND_MESSAGE'
|
||||
SEND_MESSAGE: 'SEND_MESSAGE',
|
||||
AI_CHAT_COMPLETION: 'AI_CHAT_COMPLETION'
|
||||
}
|
||||
|
||||
7
src/renderer/src/services/provider.ts
Normal file
7
src/renderer/src/services/provider.ts
Normal 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'
|
||||
})
|
||||
@ -4,6 +4,10 @@ import { FLUSH, PAUSE, PERSIST, persistReducer, persistStore, PURGE, REGISTER, R
|
||||
import storage from 'redux-persist/lib/storage'
|
||||
import agents from './agents'
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
agents
|
||||
})
|
||||
|
||||
const store = configureStore({
|
||||
reducer: persistReducer(
|
||||
{
|
||||
@ -11,9 +15,7 @@ const store = configureStore({
|
||||
storage,
|
||||
version: 1
|
||||
},
|
||||
combineReducers({
|
||||
agents
|
||||
})
|
||||
rootReducer
|
||||
),
|
||||
middleware: (getDefaultMiddleware) => {
|
||||
return getDefaultMiddleware({
|
||||
@ -25,13 +27,12 @@ const store = configureStore({
|
||||
devTools: true
|
||||
})
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export type RootState = ReturnType<typeof rootReducer>
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
|
||||
export const persistor = persistStore(store)
|
||||
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
|
||||
export const useAppSelector = useSelector.withTypes<RootState>()
|
||||
export const useAppStore = useStore.withTypes<typeof store>()
|
||||
// export const dispatch: AppDispatch = useDispatch()
|
||||
|
||||
export default store
|
||||
|
||||
@ -26,12 +26,3 @@ export type User = {
|
||||
avatar: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export type Agent = {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
avatar: string
|
||||
model: string
|
||||
default: boolean
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user