feat: Agents 页面改版 #198

This commit is contained in:
kangfenmao 2024-10-17 13:44:52 +08:00
parent a3a005b946
commit a8ccaf6847
12 changed files with 309 additions and 245 deletions

View File

@ -9,6 +9,7 @@ import Sidebar from './components/app/Sidebar'
import TopViewContainer from './components/TopView' import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider' import AntdProvider from './context/AntdProvider'
import { ThemeProvider } from './context/ThemeProvider' import { ThemeProvider } from './context/ThemeProvider'
import AgentEditPage from './pages/agents/AgentEditPage'
import AgentsPage from './pages/agents/AgentsPage' import AgentsPage from './pages/agents/AgentsPage'
import AppsPage from './pages/apps/AppsPage' import AppsPage from './pages/apps/AppsPage'
import FilesPage from './pages/files/FilesPage' import FilesPage from './pages/files/FilesPage'
@ -30,6 +31,7 @@ function App(): JSX.Element {
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/files" element={<FilesPage />} /> <Route path="/files" element={<FilesPage />} />
<Route path="/agents" element={<AgentsPage />} /> <Route path="/agents" element={<AgentsPage />} />
<Route path="/agents/:id" element={<AgentEditPage />} />
<Route path="/translate" element={<TranslatePage />} /> <Route path="/translate" element={<TranslatePage />} />
<Route path="/apps" element={<AppsPage />} /> <Route path="/apps" element={<AppsPage />} />
<Route path="/messages/*" element={<HistoryPage />} /> <Route path="/messages/*" element={<HistoryPage />} />

View File

@ -54,7 +54,7 @@ const Sidebar: FC = () => {
</Icon> </Icon>
</StyledLink> </StyledLink>
<StyledLink onClick={() => to('/agents')}> <StyledLink onClick={() => to('/agents')}>
<Icon className={isRoute('/agents')}> <Icon className={isRoutes('/agents')}>
<i className="iconfont icon-business-smart-assistant" /> <i className="iconfont icon-business-smart-assistant" />
</Icon> </Icon>
</StyledLink> </StyledLink>

View File

@ -15,3 +15,14 @@ export function useAgents() {
updateAgents: (agents: Agent[]) => dispatch(updateAgents(agents)) updateAgents: (agents: Agent[]) => dispatch(updateAgents(agents))
} }
} }
export function useAgent(id: string) {
const agents = useSelector((state: RootState) => state.agents.agents)
const dispatch = useDispatch()
const agent = agents.find((a) => a.id === id)
return {
agent,
updateAgent: (agent: Agent) => dispatch(updateAgent(agent))
}
}

View File

@ -28,7 +28,8 @@
"warning": "Warning", "warning": "Warning",
"back": "Back", "back": "Back",
"chat": "Chat", "chat": "Chat",
"close": "Close" "close": "Close",
"cancel": "Cancel"
}, },
"button": { "button": {
"add": "Add", "add": "Add",

View File

@ -28,7 +28,8 @@
"warning": "警告", "warning": "警告",
"back": "返回", "back": "返回",
"chat": "聊天", "chat": "聊天",
"close": "关闭" "close": "关闭",
"cancel": "取消"
}, },
"button": { "button": {
"add": "添加", "add": "添加",

View File

@ -28,7 +28,8 @@
"warning": "警告", "warning": "警告",
"back": "返回", "back": "返回",
"chat": "聊天", "chat": "聊天",
"close": "關閉" "close": "關閉",
"cancel": "取消"
}, },
"button": { "button": {
"add": "添加", "add": "添加",

View File

@ -0,0 +1,157 @@
import { LoadingOutlined, ThunderboltOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import EmojiPicker from '@renderer/components/EmojiPicker'
import { useAgent, useAgents } from '@renderer/hooks/useAgents'
import { fetchGenerate } from '@renderer/services/api'
import { syncAgentToAssistant } from '@renderer/services/assistant'
import { Agent } from '@renderer/types'
import { getLeadingEmoji } from '@renderer/utils'
import { Button, Form, FormInstance, Input, Popover } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { FC, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate, useParams } from 'react-router'
import styled from 'styled-components'
type FieldType = {
id: string
name: string
prompt: string
}
const AgentEditPage: FC = () => {
const { t } = useTranslation()
const { id } = useParams()
const { agent } = useAgent(id!)
const [form] = Form.useForm()
const formRef = useRef<FormInstance>(null)
const { addAgent, updateAgent } = useAgents()
const [emoji, setEmoji] = useState(agent?.emoji)
const [loading, setLoading] = useState(false)
const navigate = useNavigate()
const onFinish = (values: FieldType) => {
const _emoji = emoji || getLeadingEmoji(values.name)
if (values.name.trim() === '' || values.prompt.trim() === '') {
return
}
const _agent = {
...agent,
name: values.name,
emoji: _emoji,
prompt: values.prompt
} as Agent
updateAgent(_agent)
syncAgentToAssistant(_agent)
navigate(-1)
}
const handleButtonClick = async () => {
const prompt = `你是一个专业的 prompt 优化助手我会给你一段prompt你需要帮我优化它仅回复优化后的 prompt 不要添加任何解释,使用 [CRISPE提示框架] 回复。`
const name = formRef.current?.getFieldValue('name')
const content = formRef.current?.getFieldValue('prompt')
const promptText = content || name
if (!promptText) {
return
}
if (content) {
navigator.clipboard.writeText(content)
}
setLoading(true)
try {
const prefixedContent = `请帮我优化下面这段 prompt使用 CRISPE 提示框架,请使用 Markdown 格式回复,不要使用 codeblock: ${promptText}`
const generatedText = await fetchGenerate({ prompt, content: prefixedContent })
formRef.current?.setFieldValue('prompt', generatedText)
} catch (error) {
console.error('Error fetching data:', error)
}
setLoading(false)
}
useEffect(() => {
if (agent) {
form.setFieldsValue({
name: agent.name,
prompt: agent.prompt
})
}
}, [agent, form])
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('agents.edit.title')}</NavbarCenter>
</Navbar>
<ContentContainer id="content-container">
<Form
ref={formRef}
layout="vertical"
form={form}
labelAlign="left"
colon={false}
style={{ width: '100%' }}
onFinish={onFinish}>
<Form.Item name="name" label="Emoji">
<Popover content={<EmojiPicker onEmojiClick={setEmoji} />} arrow placement="rightBottom">
<Button icon={emoji && <span style={{ fontSize: 20 }}>{emoji}</span>}>{t('common.select')}</Button>
</Popover>
</Form.Item>
<Form.Item name="name" label={t('agents.add.name')} rules={[{ required: true }]}>
<Input placeholder={t('agents.add.name.placeholder')} spellCheck={false} allowClear />
</Form.Item>
<div style={{ position: 'relative' }}>
<Form.Item
name="prompt"
label={t('agents.add.prompt')}
rules={[{ required: true }]}
style={{ position: 'relative' }}>
<TextArea placeholder={t('agents.add.prompt.placeholder')} spellCheck={false} rows={10} />
</Form.Item>
<Button
icon={loading ? <LoadingOutlined /> : <ThunderboltOutlined />}
onClick={handleButtonClick}
style={{ position: 'absolute', top: 8, right: 8 }}
disabled={loading}
/>
</div>
<Form.Item wrapperCol={{ span: 16 }}>
<Button type="primary" htmlType="submit">
{t('common.save')}
</Button>
<Button type="link" onClick={() => navigate(-1)}>
{t('common.cancel')}
</Button>
</Form.Item>
</Form>
<div style={{ minHeight: 50 }} />
</ContentContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
padding: 20px;
overflow-y: scroll;
`
export default AgentEditPage

View File

@ -1,8 +1,6 @@
import { UnorderedListOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout' import { VStack } from '@renderer/components/Layout'
import Agents from '@renderer/config/agents.json' import Agents from '@renderer/config/agents.json'
import { useAgents } from '@renderer/hooks/useAgents'
import { useAssistants } from '@renderer/hooks/useAssistant' import { useAssistants } from '@renderer/hooks/useAssistant'
import { covertAgentToAssistant } from '@renderer/services/assistant' import { covertAgentToAssistant } from '@renderer/services/assistant'
import { Agent } from '@renderer/types' import { Agent } from '@renderer/types'
@ -13,14 +11,12 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import AgentCard from './components/AgentCard' import AgentCard from './components/AgentCard'
import ManageAgentsPopup from './components/ManageAgentsPopup' import MyAgents from './components/MyAgents'
import UserAgents from './components/UserAgents'
const { Title } = Typography const { Title } = Typography
const AppsPage: FC = () => { const AppsPage: FC = () => {
const { assistants, addAssistant } = useAssistants() const { assistants, addAssistant } = useAssistants()
const { agents } = useAgents()
const agentGroups = groupBy(Agents, 'group') const agentGroups = groupBy(Agents, 'group')
const { t } = useTranslation() const { t } = useTranslation()
@ -55,31 +51,29 @@ const AppsPage: FC = () => {
<NavbarCenter style={{ borderRight: 'none' }}>{t('agents.title')}</NavbarCenter> <NavbarCenter style={{ borderRight: 'none' }}>{t('agents.title')}</NavbarCenter>
</Navbar> </Navbar>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<MyAgents onClick={onAddAgentConfirm} />
<AssistantsContainer> <AssistantsContainer>
<HStack alignItems="center" style={{ marginBottom: 16 }}> <VStack style={{ flex: 1 }}>
<Title level={4}>{t('agents.my_agents')}</Title> {Object.keys(agentGroups)
{agents.length > 0 && <ManageIcon onClick={ManageAgentsPopup.show} />} .reverse()
</HStack> .map((group) => (
<UserAgents onAdd={onAddAgentConfirm} /> <div key={group}>
{Object.keys(agentGroups) <Title level={5} key={group} style={{ marginBottom: 16 }}>
.reverse() {group}
.map((group) => ( </Title>
<div key={group}> <Row gutter={16}>
<Title level={4} key={group} style={{ marginBottom: 16 }}> {agentGroups[group].map((agent, index) => {
{group} return (
</Title> <Col span={8} key={group + index}>
<Row gutter={16}> <AgentCard onClick={() => onAddAgentConfirm(agent)} agent={agent as any} />
{agentGroups[group].map((agent, index) => { </Col>
return ( )
<Col span={8} key={group + index}> })}
<AgentCard onClick={() => onAddAgentConfirm(agent)} agent={agent as any} /> </Row>
</Col> </div>
) ))}
})} <div style={{ minHeight: 20 }} />
</Row> </VStack>
</div>
))}
<div style={{ minHeight: 20 }} />
</AssistantsContainer> </AssistantsContainer>
</ContentContainer> </ContentContainer>
</Container> </Container>
@ -99,24 +93,15 @@ const ContentContainer = styled.div`
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
overflow-y: scroll;
` `
const AssistantsContainer = styled.div` const AssistantsContainer = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: row;
height: calc(100vh - var(--navbar-height)); height: calc(100vh - var(--navbar-height));
padding: 20px; padding: 15px 20px;
max-width: 1000px; overflow-y: scroll;
`
const ManageIcon = styled(UnorderedListOutlined)`
font-size: 18px;
color: var(--color-icon);
cursor: pointer;
margin-bottom: 0.5em;
margin-left: 0.5em;
` `
export default AppsPage export default AppsPage

View File

@ -5,16 +5,14 @@ import EmojiPicker from '@renderer/components/EmojiPicker'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { useAgents } from '@renderer/hooks/useAgents' import { useAgents } from '@renderer/hooks/useAgents'
import { fetchGenerate } from '@renderer/services/api' import { fetchGenerate } from '@renderer/services/api'
import { syncAgentToAssistant } from '@renderer/services/assistant'
import { Agent } from '@renderer/types' import { Agent } from '@renderer/types'
import { getLeadingEmoji, uuid } from '@renderer/utils' import { getLeadingEmoji, uuid } from '@renderer/utils'
import { Button, Form, FormInstance, Input, Modal, Popover } from 'antd' import { Button, Form, FormInstance, Input, Modal, Popover } from 'antd'
import TextArea from 'antd/es/input/TextArea' import TextArea from 'antd/es/input/TextArea'
import { useEffect, useRef, useState } from 'react' import { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
interface Props { interface Props {
agent?: Agent
resolve: (data: Agent | null) => void resolve: (data: Agent | null) => void
} }
@ -24,13 +22,13 @@ type FieldType = {
prompt: string prompt: string
} }
const PopupContainer: React.FC<Props> = ({ agent, resolve }) => { const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const [form] = Form.useForm() const [form] = Form.useForm()
const { t } = useTranslation() const { t } = useTranslation()
const { addAgent, updateAgent } = useAgents() const { addAgent } = useAgents()
const formRef = useRef<FormInstance>(null) const formRef = useRef<FormInstance>(null)
const [emoji, setEmoji] = useState(agent?.emoji) const [emoji, setEmoji] = useState('')
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const onFinish = (values: FieldType) => { const onFinish = (values: FieldType) => {
@ -40,20 +38,6 @@ const PopupContainer: React.FC<Props> = ({ agent, resolve }) => {
return return
} }
if (agent) {
const _agent = {
...agent,
name: values.name,
emoji: _emoji,
prompt: values.prompt
}
updateAgent(_agent)
syncAgentToAssistant(_agent)
resolve(_agent)
setOpen(false)
return
}
const _agent = { const _agent = {
id: uuid(), id: uuid(),
name: values.name, name: values.name,
@ -75,15 +59,6 @@ const PopupContainer: React.FC<Props> = ({ agent, resolve }) => {
resolve(null) resolve(null)
} }
useEffect(() => {
if (agent) {
form.setFieldsValue({
name: agent.name,
prompt: agent.prompt
})
}
}, [agent, form])
const handleButtonClick = async () => { const handleButtonClick = async () => {
const prompt = `你是一个专业的 prompt 优化助手我会给你一段prompt你需要帮我优化它仅回复优化后的 prompt 不要添加任何解释,使用 [CRISPE提示框架] 回复。` const prompt = `你是一个专业的 prompt 优化助手我会给你一段prompt你需要帮我优化它仅回复优化后的 prompt 不要添加任何解释,使用 [CRISPE提示框架] 回复。`
@ -114,13 +89,13 @@ const PopupContainer: React.FC<Props> = ({ agent, resolve }) => {
return ( return (
<Modal <Modal
title={agent ? t('agents.edit.title') : t('agents.add.title')} title={t('agents.add.title')}
open={open} open={open}
onOk={() => formRef.current?.submit()} onOk={() => formRef.current?.submit()}
onCancel={onCancel} onCancel={onCancel}
maskClosable={false} maskClosable={false}
afterClose={onClose} afterClose={onClose}
okText={agent ? t('common.save') : t('agents.add.button')} okText={t('agents.add.button')}
centered> centered>
<Form <Form
ref={formRef} ref={formRef}
@ -163,11 +138,10 @@ export default class AddAgentPopup {
static hide() { static hide() {
TopView.hide('AddAgentPopup') TopView.hide('AddAgentPopup')
} }
static show(agent?: Agent) { static show() {
return new Promise<Agent | null>((resolve) => { return new Promise<Agent | null>((resolve) => {
TopView.show( TopView.show(
<PopupContainer <PopupContainer
agent={agent}
resolve={(v) => { resolve={(v) => {
resolve(v) resolve(v)
this.hide() this.hide()

View File

@ -1,109 +0,0 @@
import { DeleteOutlined, EditOutlined, MenuOutlined } from '@ant-design/icons'
import DragableList from '@renderer/components/DragableList'
import { Box, HStack } from '@renderer/components/Layout'
import { TopView } from '@renderer/components/TopView'
import { useAgents } from '@renderer/hooks/useAgents'
import { Empty, Modal, Popconfirm } from 'antd'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import AddAgentPopup from './AddAgentPopup'
const PopupContainer: React.FC = () => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const { agents, removeAgent, updateAgents } = useAgents()
const onOk = () => {
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = async () => {
ManageAgentsPopup.hide()
}
useEffect(() => {
if (agents.length === 0) {
setOpen(false)
}
}, [agents])
return (
<Modal
title={t('agents.manage.title')}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
footer={null}
centered>
<Container>
{agents.length > 0 && (
<DragableList list={agents} onUpdate={updateAgents}>
{(item) => (
<AgentItem>
<Box mr={8}>
{item.emoji} {item.name}
</Box>
<HStack gap="15px">
<Popconfirm
title={t('agents.delete.popup.content')}
okButtonProps={{ danger: true }}
onConfirm={() => removeAgent(item)}>
<DeleteOutlined style={{ color: 'var(--color-error)' }} />
</Popconfirm>
<EditOutlined style={{ cursor: 'pointer' }} onClick={() => AddAgentPopup.show(item)} />
<MenuOutlined style={{ cursor: 'move' }} />
</HStack>
</AgentItem>
)}
</DragableList>
)}
{agents.length === 0 && <Empty description="" />}
</Container>
</Modal>
)
}
const Container = styled.div`
padding: 12px 0;
height: 50vh;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
`
const AgentItem = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 8px;
border-radius: 8px;
user-select: none;
background-color: var(--color-background-soft);
margin-bottom: 8px;
.anticon {
font-size: 16px;
color: var(--color-icon);
}
&:hover {
background-color: var(--color-background-mute);
}
`
export default class ManageAgentsPopup {
static topviewId = 0
static hide() {
TopView.hide('ManageAgentsPopup')
}
static show() {
TopView.show(<PopupContainer />, 'ManageAgentsPopup')
}
}

View File

@ -0,0 +1,98 @@
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'
import DragableList from '@renderer/components/DragableList'
import { Box, HStack } from '@renderer/components/Layout'
import { useAgents } from '@renderer/hooks/useAgents'
import { Agent } from '@renderer/types'
import { Button, Popconfirm, Typography } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
import AddAgentPopup from './AddAgentPopup'
const { Title } = Typography
interface Props {
onClick: (agent: Agent) => void
}
const MyAssistants: React.FC<Props> = ({ onClick }) => {
const { t } = useTranslation()
const { agents, removeAgent, updateAgents } = useAgents()
const [dragging, setDragging] = useState(false)
const navigate = useNavigate()
return (
<Container style={{ paddingBottom: dragging ? 30 : 0 }}>
<Title level={5} style={{ marginLeft: 10 }}>
{t('agents.my_agents')}
</Title>
{agents.length > 0 && (
<DragableList
list={agents}
onUpdate={updateAgents}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(agent) => (
<AgentItem onClick={() => onClick(agent)}>
<Box mr={8}>
{agent.emoji} {agent.name}
</Box>
<HStack gap="15px" onClick={(e) => e.stopPropagation()}>
<Popconfirm
title={t('agents.delete.popup.content')}
placement="bottom"
okButtonProps={{ danger: true }}
onConfirm={() => removeAgent(agent)}>
<DeleteOutlined style={{ color: 'var(--color-error)' }} />
</Popconfirm>
<EditOutlined style={{ cursor: 'pointer' }} onClick={() => navigate(`/agents/${agent.id}`)} />
</HStack>
</AgentItem>
)}
</DragableList>
)}
{!dragging && (
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() => AddAgentPopup.show()}
style={{ borderRadius: 20, height: 34 }}>
{t('agents.add.title')}
</Button>
)}
</Container>
)
}
const Container = styled.div`
padding: 15px 10px;
display: flex;
flex-direction: column;
width: var(--assistants-width);
height: calc(100vh - var(--navbar-height));
border-right: 0.5px solid var(--color-border);
overflow-y: scroll;
`
const AgentItem = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-radius: 20px;
user-select: none;
background-color: var(--color-background-soft);
margin-bottom: 8px;
.anticon {
font-size: 16px;
color: var(--color-icon);
}
&:hover {
background-color: var(--color-background-mute);
}
`
export default MyAssistants

View File

@ -1,57 +0,0 @@
import { PlusOutlined } from '@ant-design/icons'
import { useAgents } from '@renderer/hooks/useAgents'
import { Agent } from '@renderer/types'
import { Col, Row } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
import AddAssistantPopup from './AddAgentPopup'
import AgentCard from './AgentCard'
interface Props {
onAdd: (agent: Agent) => void
}
const UserAgents: FC<Props> = ({ onAdd }) => {
const { agents } = useAgents()
const onAddMyAgentClick = () => {
AddAssistantPopup.show()
}
return (
<Row gutter={16} style={{ marginBottom: 16 }}>
{agents.map((agent) => (
<Col span={8} key={agent.id}>
<AgentCard agent={agent} onClick={() => onAdd(agent)} />
</Col>
))}
<Col span={8}>
<AssistantCardContainer style={{ borderStyle: 'dashed' }} onClick={onAddMyAgentClick}>
<PlusOutlined />
</AssistantCardContainer>
</Col>
</Row>
)
}
const AssistantCardContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
border: 1px dashed var(--color-border-soft);
border-radius: 10px;
cursor: pointer;
min-height: 72px;
.anticon {
font-size: 16px;
color: var(--color-icon);
}
&:hover {
background-color: var(--color-background-soft);
}
`
export default UserAgents