feat: agent rename to assistant

This commit is contained in:
kangfenmao 2024-07-03 13:44:05 +08:00
parent 6408762f40
commit 900052e581
34 changed files with 513 additions and 429 deletions

View File

@ -34,9 +34,9 @@
--navbar-height: 42px; --navbar-height: 42px;
--sidebar-width: 68px; --sidebar-width: 68px;
--agents-width: 250px; --assistants-width: 250px;
--topic-list-width: var(--agents-width); --topic-list-width: var(--assistants-width);
--settings-width: var(--agents-width); --settings-width: var(--assistants-width);
--status-bar-height: 40px; --status-bar-height: 40px;
--input-bar-height: 120px; --input-bar-height: 120px;
} }

View File

@ -3,6 +3,7 @@
font-size: 15px; font-size: 15px;
line-height: 1.6; line-height: 1.6;
user-select: text; user-select: text;
margin-top: 4px;
.hljs { .hljs {
background-color: transparent; background-color: transparent;

View File

@ -2,21 +2,21 @@ import { Input, Modal } from 'antd'
import { useState } from 'react' import { useState } from 'react'
import { TopView } from '../TopView' import { TopView } from '../TopView'
import { Box } from '../Layout' import { Box } from '../Layout'
import { Agent } from '@renderer/types' import { Assistant } from '@renderer/types'
import TextArea from 'antd/es/input/TextArea' import TextArea from 'antd/es/input/TextArea'
interface AgentSettingPopupShowParams { interface AssistantSettingPopupShowParams {
agent: Agent assistant: Assistant
} }
interface Props extends AgentSettingPopupShowParams { interface Props extends AssistantSettingPopupShowParams {
resolve: (agent: Agent) => void resolve: (assistant: Assistant) => void
} }
const AgentSettingPopupContainer: React.FC<Props> = ({ agent, resolve }) => { const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve }) => {
const [name, setName] = useState(agent.name) const [name, setName] = useState(assistant.name)
const [description, setDescription] = useState(agent.description) const [description, setDescription] = useState(assistant.description)
const [prompt, setPrompt] = useState(agent.prompt) const [prompt, setPrompt] = useState(assistant.prompt)
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const onOk = () => { const onOk = () => {
@ -28,19 +28,19 @@ const AgentSettingPopupContainer: React.FC<Props> = ({ agent, resolve }) => {
} }
const onClose = () => { const onClose = () => {
resolve({ ...agent, name, description, prompt }) resolve({ ...assistant, name, description, prompt })
} }
return ( return (
<Modal title={agent.name} open={open} onOk={onOk} onCancel={handleCancel} afterClose={onClose}> <Modal title={assistant.name} open={open} onOk={onOk} onCancel={handleCancel} afterClose={onClose}>
<Box mb={8}>Name</Box> <Box mb={8}>Name</Box>
<Input placeholder="Agent Name" value={name} onChange={(e) => setName(e.target.value)} autoFocus /> <Input placeholder="Assistant Name" value={name} onChange={(e) => setName(e.target.value)} autoFocus />
<Box mt={8} mb={8}> <Box mt={8} mb={8}>
Description Description
</Box> </Box>
<TextArea <TextArea
rows={4} rows={4}
placeholder="Agent Description" placeholder="Assistant Description"
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
autoFocus autoFocus
@ -50,7 +50,7 @@ const AgentSettingPopupContainer: React.FC<Props> = ({ agent, resolve }) => {
</Box> </Box>
<TextArea <TextArea
rows={4} rows={4}
placeholder="Agent Prompt" placeholder="Assistant Prompt"
value={prompt} value={prompt}
onChange={(e) => setPrompt(e.target.value)} onChange={(e) => setPrompt(e.target.value)}
autoFocus autoFocus
@ -59,15 +59,15 @@ const AgentSettingPopupContainer: React.FC<Props> = ({ agent, resolve }) => {
) )
} }
export default class AgentSettingPopup { export default class AssistantSettingPopup {
static topviewId = 0 static topviewId = 0
static hide() { static hide() {
TopView.hide(this.topviewId) TopView.hide(this.topviewId)
} }
static show(props: AgentSettingPopupShowParams) { static show(props: AssistantSettingPopupShowParams) {
return new Promise<Agent>((resolve) => { return new Promise<Assistant>((resolve) => {
this.topviewId = TopView.show( this.topviewId = TopView.show(
<AgentSettingPopupContainer <AssistantSettingPopupContainer
{...props} {...props}
resolve={(v) => { resolve={(v) => {
resolve(v) resolve(v)

View File

@ -31,7 +31,7 @@ const NavbarContainer = styled.div`
` `
const NavbarLeftContainer = styled.div` const NavbarLeftContainer = styled.div`
min-width: var(--agents-width); min-width: var(--assistants-width);
border-right: 1px solid var(--color-border); border-right: 1px solid var(--color-border);
padding: 0 16px; padding: 0 16px;
display: flex; display: flex;

View File

@ -24,7 +24,7 @@ const Container = styled.div`
` `
const StatusbarLeft = styled.div` const StatusbarLeft = styled.div`
min-width: var(--sidebar-width) + var(--agents-width); min-width: var(--sidebar-width) + var(--assistants-width);
` `
const StatusbarCenter = styled.div` const StatusbarCenter = styled.div`

View File

@ -1,51 +0,0 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
addTopic as _addTopic,
removeAllTopics as _removeAllTopics,
removeTopic as _removeTopic,
updateTopic as _updateTopic,
addAgent,
removeAgent,
updateAgent
} from '@renderer/store/agents'
import { Agent, Topic } from '@renderer/types'
import localforage from 'localforage'
export default function useAgents() {
const { agents } = useAppSelector((state) => state.agents)
const dispatch = useAppDispatch()
return {
agents,
addAgent: (agent: Agent) => dispatch(addAgent(agent)),
removeAgent: (id: string) => {
dispatch(removeAgent({ id }))
const agent = agents.find((a) => a.id === id)
if (agent) {
agent.topics.forEach((id) => localforage.removeItem(`topic:${id}`))
}
},
updateAgent: (agent: Agent) => dispatch(updateAgent(agent))
}
}
export function useAgent(id: string) {
const agent = useAppSelector((state) => state.agents.agents.find((a) => a.id === id) as Agent)
const dispatch = useAppDispatch()
return {
agent,
addTopic: (topic: Topic) => {
dispatch(_addTopic({ agentId: agent.id, topic }))
},
removeTopic: (topic: Topic) => {
dispatch(_removeTopic({ agentId: agent.id, topic }))
},
updateTopic: (topic: Topic) => {
dispatch(_updateTopic({ agentId: agent.id, topic }))
},
removeAllTopics: () => {
dispatch(_removeAllTopics({ agentId: agent.id }))
}
}
}

View File

@ -0,0 +1,51 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
addTopic as _addTopic,
removeAllTopics as _removeAllTopics,
removeTopic as _removeTopic,
updateTopic as _updateTopic,
addAssistant,
removeAssistant,
updateAssistant
} from '@renderer/store/assistants'
import { Assistant, Topic } from '@renderer/types'
import localforage from 'localforage'
export default function useAssistants() {
const { assistants } = useAppSelector((state) => state.assistants)
const dispatch = useAppDispatch()
return {
assistants,
addAssistant: (assistant: Assistant) => dispatch(addAssistant(assistant)),
removeAssistant: (id: string) => {
dispatch(removeAssistant({ id }))
const assistant = assistants.find((a) => a.id === id)
if (assistant) {
assistant.topics.forEach((id) => localforage.removeItem(`topic:${id}`))
}
},
updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant))
}
}
export function useAssistant(id: string) {
const assistant = useAppSelector((state) => state.assistants.assistants.find((a) => a.id === id) as Assistant)
const dispatch = useAppDispatch()
return {
assistant,
addTopic: (topic: Topic) => {
dispatch(_addTopic({ assistantId: assistant.id, topic }))
},
removeTopic: (topic: Topic) => {
dispatch(_removeTopic({ assistantId: assistant.id, topic }))
},
updateTopic: (topic: Topic) => {
dispatch(_updateTopic({ assistantId: assistant.id, topic }))
},
removeAllTopics: () => {
dispatch(_removeAllTopics({ assistantId: assistant.id }))
}
}
}

View File

@ -1,12 +1,12 @@
import { Agent } from '@renderer/types' import { Assistant } from '@renderer/types'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
export function useActiveTopic(agent: Agent) { export function useActiveTopic(assistant: Assistant) {
const [activeTopic, setActiveTopic] = useState(agent?.topics[0]) const [activeTopic, setActiveTopic] = useState(assistant?.topics[0])
useEffect(() => { useEffect(() => {
agent?.topics && setActiveTopic(agent?.topics[0]) assistant?.topics && setActiveTopic(assistant?.topics[0])
}, [agent]) }, [assistant])
return { activeTopic, setActiveTopic } return { activeTopic, setActiveTopic }
} }

View File

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

View File

@ -1,35 +1,35 @@
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 useAssistants from '@renderer/hooks/useAssistants'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Chat from './components/Chat/Chat' import Chat from './components/Chat/Chat'
import Agents from './components/Agents' import Assistants from './components/Assistants'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
import { getDefaultAgent } from '@renderer/services/agent' import { getDefaultAssistant } from '@renderer/services/assistant'
import { useShowRightSidebar } from '@renderer/hooks/useStore' import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { Tooltip } from 'antd' import { Tooltip } from 'antd'
const HomePage: FC = () => { const HomePage: FC = () => {
const { agents, addAgent } = useAgents() const { assistants, addAssistant } = useAssistants()
const [activeAgent, setActiveAgent] = useState(agents[0]) const [activeAssistant, setActiveAssistant] = useState(assistants[0])
const { showRightSidebar, setShowRightSidebar } = useShowRightSidebar() const { showRightSidebar, setShowRightSidebar } = useShowRightSidebar()
const onCreateAgent = () => { const onCreateAssistant = () => {
const _agent = getDefaultAgent() const _assistant = getDefaultAssistant()
_agent.id = uuid() _assistant.id = uuid()
addAgent(_agent) addAssistant(_assistant)
setActiveAgent(_agent) setActiveAssistant(_assistant)
} }
return ( return (
<Container> <Container>
<Navbar> <Navbar>
<NavbarLeft style={{ justifyContent: 'flex-end', borderRight: 'none' }}> <NavbarLeft style={{ justifyContent: 'flex-end', borderRight: 'none' }}>
<NewButton onClick={onCreateAgent}> <NewButton onClick={onCreateAssistant}>
<i className="iconfont icon-a-addchat"></i> <i className="iconfont icon-a-addchat"></i>
</NewButton> </NewButton>
</NavbarLeft> </NavbarLeft>
<NavbarCenter style={{ border: 'none' }}>{activeAgent?.name}</NavbarCenter> <NavbarCenter style={{ border: 'none' }}>{activeAssistant?.name}</NavbarCenter>
<NavbarRight style={{ justifyContent: 'flex-end', padding: 5 }}> <NavbarRight style={{ justifyContent: 'flex-end', padding: 5 }}>
<Tooltip placement="left" title={showRightSidebar ? 'Hide Topics' : 'Show Topics'} arrow> <Tooltip placement="left" title={showRightSidebar ? 'Hide Topics' : 'Show Topics'} arrow>
<NewButton onClick={setShowRightSidebar}> <NewButton onClick={setShowRightSidebar}>
@ -39,8 +39,8 @@ const HomePage: FC = () => {
</NavbarRight> </NavbarRight>
</Navbar> </Navbar>
<ContentContainer> <ContentContainer>
<Agents activeAgent={activeAgent} onActive={setActiveAgent} /> <Assistants activeAssistant={activeAssistant} onActive={setActiveAssistant} />
<Chat agent={activeAgent} /> <Chat assistant={activeAssistant} />
</ContentContainer> </ContentContainer>
</Container> </Container>
) )

View File

@ -1,147 +0,0 @@
import { FC, useRef } from 'react'
import styled from 'styled-components'
import useAgents from '@renderer/hooks/useAgents'
import { Agent } from '@renderer/types'
import { Dropdown, MenuProps } from 'antd'
import { MoreOutlined } from '@ant-design/icons'
import { last } from 'lodash'
import AgentSettingPopup from '@renderer/components/Popups/AgentSettingPopup'
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'
interface Props {
activeAgent: Agent
onActive: (agent: Agent) => void
}
const Agents: FC<Props> = ({ activeAgent, onActive }) => {
const { agents, removeAgent, updateAgent } = useAgents()
const targetAgent = useRef<Agent | null>(null)
const onDelete = (agent: Agent) => {
removeAgent(agent.id)
setTimeout(() => {
const _agent = last(agents.filter((a) => a.id !== agent.id))
_agent && onActive(_agent)
}, 0)
}
const items: MenuProps['items'] = [
{
label: 'Edit',
key: 'edit',
icon: <EditOutlined />,
async onClick() {
if (targetAgent.current) {
const _agent = await AgentSettingPopup.show({ agent: targetAgent.current })
updateAgent(_agent)
}
}
},
{ type: 'divider' },
{
label: 'Delete',
key: 'delete',
icon: <DeleteOutlined />,
danger: true,
onClick: () => targetAgent.current && onDelete(targetAgent.current)
}
]
return (
<Container>
{agents.map((agent) => (
<AgentItem
data-id={agent.id}
key={agent.id}
onClick={() => onActive(agent)}
className={agent.id === activeAgent?.id ? 'active' : ''}>
<Dropdown
menu={{ items }}
trigger={['click']}
placement="bottom"
arrow
onOpenChange={() => (targetAgent.current = agent)}>
<MenuButton className="menu-button" onClick={(e) => e.stopPropagation()}>
<MoreOutlined />
</MenuButton>
</Dropdown>
<AgentName>{agent.name}</AgentName>
<AgentLastMessage>{agent.description}</AgentLastMessage>
</AgentItem>
))}
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
min-width: var(--agents-width);
max-width: var(--agents-width);
border-right: 0.5px solid var(--color-border);
height: calc(100vh - var(--navbar-height));
overflow-y: scroll;
&::-webkit-scrollbar {
display: none;
}
`
const AgentItem = styled.div`
display: flex;
flex-direction: column;
padding: 10px 15px;
position: relative;
cursor: pointer;
.anticon {
display: none;
}
&:hover {
background-color: var(--color-background-soft);
.anticon {
display: block;
color: var(--color-text-1);
}
}
&.active {
background-color: var(--color-background-mute);
cursor: pointer;
}
`
const AgentName = styled.div`
font-size: 14px;
color: var(--color-text-1);
font-weight: bold;
`
const AgentLastMessage = styled.div`
font-size: 12px;
line-height: 20px;
color: var(--color-text-2);
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 1;
height: 20px;
`
const MenuButton = styled.div`
padding: 5px;
position: absolute;
width: 30px;
height: 30px;
right: 10px;
top: 10px;
font-size: 18px;
border-radius: 50%;
transition: background-color 0.2s ease;
&:hover {
background-color: #ffffff30;
.anticon {
color: white;
}
}
`
export default Agents

View File

@ -0,0 +1,160 @@
import { FC, useRef } from 'react'
import styled from 'styled-components'
import useAssistants from '@renderer/hooks/useAssistants'
import { Assistant } from '@renderer/types'
import { Button, Dropdown, MenuProps } from 'antd'
import { MoreOutlined } from '@ant-design/icons'
import { last } from 'lodash'
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'
interface Props {
activeAssistant: Assistant
onActive: (assistant: Assistant) => void
}
const Assistants: FC<Props> = ({ activeAssistant, onActive }) => {
const { assistants, removeAssistant, updateAssistant } = useAssistants()
const targetAssistant = useRef<Assistant | null>(null)
const menuOpenRef = useRef(false)
const onDelete = (assistant: Assistant) => {
removeAssistant(assistant.id)
setTimeout(() => {
const _assistant = last(assistants.filter((a) => a.id !== assistant.id))
_assistant && onActive(_assistant)
}, 0)
}
const items: MenuProps['items'] = [
{
label: 'Edit',
key: 'edit',
icon: <EditOutlined />,
async onClick() {
if (targetAssistant.current) {
const _assistant = await AssistantSettingPopup.show({ assistant: targetAssistant.current })
updateAssistant(_assistant)
}
}
},
{ type: 'divider' },
{
label: 'Delete',
key: 'delete',
icon: <DeleteOutlined />,
danger: true,
onClick: () => {
targetAssistant.current && onDelete(targetAssistant.current)
}
}
]
return (
<Container>
{assistants.map((assistant) => (
<Dropdown
key={assistant.id}
menu={{ items }}
trigger={['contextMenu']}
onOpenChange={() => (targetAssistant.current = assistant)}>
<AssistantItem
onClick={() => onActive(assistant)}
className={assistant.id === activeAssistant?.id ? 'active' : ''}>
<Dropdown
menu={{ items }}
trigger={['click']}
placement="bottom"
destroyPopupOnHide
arrow
onOpenChange={() => (targetAssistant.current = assistant)}>
<MenuButton type="text" onClick={(e) => e.stopPropagation()}>
<MoreOutlined />
</MenuButton>
</Dropdown>
<AssistantName>{assistant.name}</AssistantName>
<AssistantLastMessage>{assistant.description}</AssistantLastMessage>
</AssistantItem>
</Dropdown>
))}
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
min-width: var(--assistants-width);
max-width: var(--assistants-width);
border-right: 0.5px solid var(--color-border);
height: calc(100vh - var(--navbar-height));
overflow-y: scroll;
&::-webkit-scrollbar {
display: none;
}
`
const AssistantItem = styled.div`
display: flex;
flex-direction: column;
padding: 10px 15px;
position: relative;
cursor: pointer;
.anticon {
display: none;
}
&:hover {
background-color: var(--color-background-soft);
.anticon {
display: block;
color: var(--color-text-1);
}
}
&.active {
background-color: var(--color-background-mute);
cursor: pointer;
}
`
const AssistantName = styled.div`
font-size: 14px;
color: var(--color-text-1);
font-weight: bold;
`
const AssistantLastMessage = styled.div`
font-size: 12px;
line-height: 20px;
color: var(--color-text-2);
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 1;
height: 20px;
`
const MenuButton = styled(Button)`
position: absolute;
width: 28px;
height: 28px;
padding: 0;
right: 6px;
top: 6px;
font-size: 18px;
border-radius: 50%;
transition: background-color 0.2s ease;
z-index: 10;
.anticon {
transition: all 0.3s ease;
color: var(--color-icon);
}
&:hover {
background-color: #ffffff30;
.anticon {
color: white;
}
}
`
export default Assistants

View File

@ -1,32 +1,32 @@
import { Agent } from '@renderer/types' import { Assistant } from '@renderer/types'
import { FC } from 'react' import { FC } 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 { Flex } from 'antd' import { Flex } from 'antd'
import TopicList from './TopicList' import TopicList from './TopicList'
import { useAgent } from '@renderer/hooks/useAgents' import { useAssistant } from '@renderer/hooks/useAssistants'
import { useActiveTopic } from '@renderer/hooks/useTopic' import { useActiveTopic } from '@renderer/hooks/useTopic'
interface Props { interface Props {
agent: Agent assistant: Assistant
} }
const Chat: FC<Props> = (props) => { const Chat: FC<Props> = (props) => {
const { agent } = useAgent(props.agent.id) const { assistant } = useAssistant(props.assistant.id)
const { activeTopic, setActiveTopic } = useActiveTopic(agent) const { activeTopic, setActiveTopic } = useActiveTopic(assistant)
if (!agent) { if (!assistant) {
return null return null
} }
return ( return (
<Container id="chat"> <Container id="chat">
<Flex vertical flex={1} justify="space-between"> <Flex vertical flex={1} justify="space-between">
<Conversations agent={agent} topic={activeTopic} /> <Conversations assistant={assistant} topic={activeTopic} />
<Inputbar agent={agent} setActiveTopic={setActiveTopic} /> <Inputbar assistant={assistant} setActiveTopic={setActiveTopic} />
</Flex> </Flex>
<TopicList agent={agent} activeTopic={activeTopic} setActiveTopic={setActiveTopic} /> <TopicList assistant={assistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
</Container> </Container>
) )
} }

View File

@ -1,5 +1,5 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Agent, Message, Topic } from '@renderer/types' import { Assistant, Message, Topic } from '@renderer/types'
import localforage from 'localforage' import localforage from 'localforage'
import { FC, useCallback, useEffect, useState } from 'react' import { FC, useCallback, useEffect, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@ -7,20 +7,20 @@ import MessageItem from './Message'
import { reverse } from 'lodash' import { reverse } from 'lodash'
import hljs from 'highlight.js' import hljs from 'highlight.js'
import { fetchChatCompletion, fetchConversationSummary } from '@renderer/services/api' import { fetchChatCompletion, fetchConversationSummary } from '@renderer/services/api'
import { useAgent } from '@renderer/hooks/useAgents' import { useAssistant } from '@renderer/hooks/useAssistants'
import { DEFAULT_TOPIC_NAME } from '@renderer/config/constant' import { DEFAULT_TOPIC_NAME } from '@renderer/config/constant'
import { runAsyncFunction } from '@renderer/utils' import { runAsyncFunction } from '@renderer/utils'
import LocalStorage from '@renderer/services/storage' import LocalStorage from '@renderer/services/storage'
interface Props { interface Props {
agent: Agent assistant: Assistant
topic: Topic topic: Topic
} }
const Conversations: FC<Props> = ({ agent, topic }) => { const Conversations: FC<Props> = ({ assistant, topic }) => {
const [messages, setMessages] = useState<Message[]>([]) const [messages, setMessages] = useState<Message[]>([])
const [lastMessage, setLastMessage] = useState<Message | null>(null) const [lastMessage, setLastMessage] = useState<Message | null>(null)
const { updateTopic } = useAgent(agent.id) const { updateTopic } = useAssistant(assistant.id)
const onSendMessage = useCallback( const onSendMessage = useCallback(
(message: Message) => { (message: Message) => {
@ -47,7 +47,7 @@ const Conversations: FC<Props> = ({ agent, topic }) => {
const unsubscribes = [ const unsubscribes = [
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, async (msg: Message) => { EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, async (msg: Message) => {
onSendMessage(msg) onSendMessage(msg)
fetchChatCompletion({ agent, message: msg, topic, onResponse: setLastMessage }) fetchChatCompletion({ assistant, message: msg, topic, onResponse: setLastMessage })
}), }),
EventEmitter.on(EVENT_NAMES.AI_CHAT_COMPLETION, async (msg: Message) => { EventEmitter.on(EVENT_NAMES.AI_CHAT_COMPLETION, async (msg: Message) => {
setLastMessage(null) setLastMessage(null)
@ -62,7 +62,7 @@ const Conversations: FC<Props> = ({ agent, topic }) => {
}) })
] ]
return () => unsubscribes.forEach((unsub) => unsub()) return () => unsubscribes.forEach((unsub) => unsub())
}, [agent, autoRenameTopic, onSendMessage, topic, updateTopic]) }, [assistant, autoRenameTopic, onSendMessage, topic, updateTopic])
useEffect(() => { useEffect(() => {
runAsyncFunction(async () => { runAsyncFunction(async () => {

View File

@ -1,33 +1,33 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Agent, Message, Topic } from '@renderer/types' import { Assistant, Message, Topic } 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'
import { MoreOutlined } from '@ant-design/icons' import { MoreOutlined } from '@ant-design/icons'
import { Button, Popconfirm, Tooltip } from 'antd' import { Button, Popconfirm, Tooltip } from 'antd'
import { useShowRightSidebar } from '@renderer/hooks/useStore' import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { useAgent } from '@renderer/hooks/useAgents' import { useAssistant } from '@renderer/hooks/useAssistants'
import { ClearOutlined, HistoryOutlined, PlusCircleOutlined } from '@ant-design/icons' import { ClearOutlined, HistoryOutlined, PlusCircleOutlined } from '@ant-design/icons'
interface Props { interface Props {
agent: Agent assistant: Assistant
setActiveTopic: (topic: Topic) => void setActiveTopic: (topic: Topic) => void
} }
const Inputbar: FC<Props> = ({ agent, setActiveTopic }) => { const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const [text, setText] = useState('') const [text, setText] = useState('')
const { setShowRightSidebar } = useShowRightSidebar() const { setShowRightSidebar } = useShowRightSidebar()
const { addTopic } = useAgent(agent.id) const { addTopic } = useAssistant(assistant.id)
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => { const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
const topicId = agent.topics[0] ? agent.topics[0] : uuid() const topicId = assistant.topics[0] ? assistant.topics[0] : uuid()
const message: Message = { const message: Message = {
id: uuid(), id: uuid(),
role: 'user', role: 'user',
content: text, content: text,
agentId: agent.id, assistantId: assistant.id,
topicId, topicId,
createdAt: 'now' createdAt: 'now'
} }

View File

@ -9,7 +9,7 @@ const MessageItem: FC<{ message: Message }> = ({ message }) => {
return ( return (
<MessageContainer key={message.id}> <MessageContainer key={message.id}>
<AvatarWrapper> <AvatarWrapper>
{message.role === 'agent' ? <Avatar src={Logo} /> : <Avatar alt="Alice Swift">Y</Avatar>} {message.role === 'assistant' ? <Avatar src={Logo} /> : <Avatar alt="Alice Swift">Y</Avatar>}
</AvatarWrapper> </AvatarWrapper>
<div className="markdown" dangerouslySetInnerHTML={{ __html: marked(message.content) }}></div> <div className="markdown" dangerouslySetInnerHTML={{ __html: marked(message.content) }}></div>
</MessageContainer> </MessageContainer>

View File

@ -1,8 +1,8 @@
import PromptPopup from '@renderer/components/Popups/PromptPopup' import PromptPopup from '@renderer/components/Popups/PromptPopup'
import { useAgent } from '@renderer/hooks/useAgents' import { useAssistant } from '@renderer/hooks/useAssistants'
import { useShowRightSidebar } from '@renderer/hooks/useStore' import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { fetchConversationSummary } from '@renderer/services/api' import { fetchConversationSummary } from '@renderer/services/api'
import { Agent, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { Button, Dropdown, MenuProps, Popconfirm } from 'antd' import { Button, Dropdown, MenuProps, Popconfirm } from 'antd'
import { FC, useRef } from 'react' import { FC, useRef } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@ -10,14 +10,14 @@ import { DeleteOutlined, EditOutlined, SignatureOutlined } from '@ant-design/ico
import LocalStorage from '@renderer/services/storage' import LocalStorage from '@renderer/services/storage'
interface Props { interface Props {
agent: Agent assistant: Assistant
activeTopic: Topic activeTopic: Topic
setActiveTopic: (topic: Topic) => void setActiveTopic: (topic: Topic) => void
} }
const TopicList: FC<Props> = ({ agent, activeTopic, setActiveTopic }) => { const TopicList: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
const { showRightSidebar } = useShowRightSidebar() const { showRightSidebar } = useShowRightSidebar()
const { removeTopic, updateTopic, removeAllTopics } = useAgent(agent.id) const { removeTopic, updateTopic, removeAllTopics } = useAssistant(assistant.id)
const currentTopic = useRef<Topic | null>(null) const currentTopic = useRef<Topic | null>(null)
const topicMenuItems: MenuProps['items'] = [ const topicMenuItems: MenuProps['items'] = [
@ -54,7 +54,7 @@ const TopicList: FC<Props> = ({ agent, activeTopic, setActiveTopic }) => {
} }
] ]
if (agent.topics.length > 1) { if (assistant.topics.length > 1) {
topicMenuItems.push({ type: 'divider' }) topicMenuItems.push({ type: 'divider' })
topicMenuItems.push({ topicMenuItems.push({
label: 'Delete', label: 'Delete',
@ -62,10 +62,10 @@ const TopicList: FC<Props> = ({ agent, activeTopic, setActiveTopic }) => {
key: 'delete', key: 'delete',
icon: <DeleteOutlined />, icon: <DeleteOutlined />,
onClick() { onClick() {
if (agent.topics.length === 1) return if (assistant.topics.length === 1) return
currentTopic.current && removeTopic(currentTopic.current) currentTopic.current && removeTopic(currentTopic.current)
currentTopic.current = null currentTopic.current = null
setActiveTopic(agent.topics[0]) setActiveTopic(assistant.topics[0])
} }
}) })
} }
@ -77,7 +77,7 @@ const TopicList: FC<Props> = ({ agent, activeTopic, setActiveTopic }) => {
return ( return (
<Container className={showRightSidebar ? '' : 'collapsed'}> <Container className={showRightSidebar ? '' : 'collapsed'}>
<TopicTitle> <TopicTitle>
<span>Topics ({agent.topics.length})</span> <span>Topics ({assistant.topics.length})</span>
<Popconfirm <Popconfirm
icon={false} icon={false}
title="Delete all topic?" title="Delete all topic?"
@ -92,7 +92,7 @@ const TopicList: FC<Props> = ({ agent, activeTopic, setActiveTopic }) => {
</DeleteButton> </DeleteButton>
</Popconfirm> </Popconfirm>
</TopicTitle> </TopicTitle>
{agent.topics.map((topic) => ( {assistant.topics.map((topic) => (
<Dropdown <Dropdown
menu={{ items: topicMenuItems }} menu={{ items: topicMenuItems }}
trigger={['contextMenu']} trigger={['contextMenu']}

View File

@ -1,10 +1,10 @@
import { FC } from 'react' import { FC } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
const AboutSetting: FC = () => { const AboutSettings: FC = () => {
return <Container>About</Container> return <Container>About</Container>
} }
const Container = styled.div`` const Container = styled.div``
export default AboutSetting export default AboutSettings

View File

@ -0,0 +1,10 @@
import { FC } from 'react'
import styled from 'styled-components'
const CommonSettings: FC = () => {
return <Container>Common Settings</Container>
}
const Container = styled.div``
export default CommonSettings

View File

@ -1,10 +0,0 @@
import { FC } from 'react'
import styled from 'styled-components'
const DefaultAgentSetting: FC = () => {
return <Container>Default Agent</Container>
}
const Container = styled.div``
export default DefaultAgentSetting

View File

@ -0,0 +1,10 @@
import { FC } from 'react'
import styled from 'styled-components'
const DefaultAssistantSetting: FC = () => {
return <Container>Default Assistant</Container>
}
const Container = styled.div``
export default DefaultAssistantSetting

View File

@ -1,10 +0,0 @@
import { FC } from 'react'
import styled from 'styled-components'
const GeneralSetting: FC = () => {
return <Container>General Settings</Container>
}
const Container = styled.div``
export default GeneralSetting

View File

@ -0,0 +1,36 @@
import { Collapse } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
const LanguageModelsSettings: FC = () => {
return (
<Container>
<Collapse style={{ width: '100%', marginBottom: 10 }}>
<Collapse.Panel header="OpenAI" key="openai">
<p>OpenAI</p>
</Collapse.Panel>
</Collapse>
<Collapse style={{ width: '100%', marginBottom: 10 }}>
<Collapse.Panel header="Silicon" key="silicon">
<p>Silicon</p>
</Collapse.Panel>
</Collapse>
<Collapse style={{ width: '100%', marginBottom: 10 }}>
<Collapse.Panel header="deepseek" key="deepseek">
<p>deepseek</p>
</Collapse.Panel>
</Collapse>
<Collapse style={{ width: '100%', marginBottom: 10 }}>
<Collapse.Panel header="Groq" key="groq">
<p>Groq</p>
</Collapse.Panel>
</Collapse>
</Container>
)
}
const Container = styled.div`
width: 100%;
`
export default LanguageModelsSettings

View File

@ -1,10 +0,0 @@
import { FC } from 'react'
import styled from 'styled-components'
const ModelsSetting: FC = () => {
return <Container>Models</Container>
}
const Container = styled.div``
export default ModelsSetting

View File

@ -2,11 +2,11 @@ import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { FC } from 'react' import { FC } from 'react'
import { Link, Route, Routes, useLocation } from 'react-router-dom' import { Link, Route, Routes, useLocation } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import SettingsHomePage from './GeneralSetting' import CommonSettings from './CommonSettings'
import SettingsDeveloperPage from './DeveloperSetting' import AboutSettings from './AboutSettings'
import SettingsAboutPage from './AboutSetting' import DefaultAssistantSetting from './DefaultAssistantSetting'
import SettingsModelsPage from './ModelsSetting' import SystemAssistantSettings from './SystemAssistantSettings'
import SettingsDefaultAgent from './DefaultAgentSetting' import LanguageModelsSettings from './LanguageModelsSettings'
const SettingsPage: FC = () => { const SettingsPage: FC = () => {
const { pathname } = useLocation() const { pathname } = useLocation()
@ -20,29 +20,29 @@ const SettingsPage: FC = () => {
</Navbar> </Navbar>
<ContentContainer> <ContentContainer>
<SettingMenus> <SettingMenus>
<MenuItemLink to="/settings/general"> <MenuItemLink to="/settings/common">
<MenuItem className={isRoute('/settings/general')}>General</MenuItem> <MenuItem className={isRoute('/settings/common')}>Common Settings</MenuItem>
</MenuItemLink> </MenuItemLink>
<MenuItemLink to="/settings/models"> <MenuItemLink to="/settings/llm">
<MenuItem className={isRoute('/settings/models')}>Language Model</MenuItem> <MenuItem className={isRoute('/settings/llm')}>Language Model</MenuItem>
</MenuItemLink> </MenuItemLink>
<MenuItemLink to="/settings/default-agent"> <MenuItemLink to="/settings/system-assistant">
<MenuItem className={isRoute('/settings/default-agent')}>Default Agent</MenuItem> <MenuItem className={isRoute('/settings/system-assistant')}>System Assistant</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/default-assistant">
<MenuItem className={isRoute('/settings/default-assistant')}>Default Assistant</MenuItem>
</MenuItemLink> </MenuItemLink>
<MenuItemLink to="/settings/about"> <MenuItemLink to="/settings/about">
<MenuItem className={isRoute('/settings/about')}>About</MenuItem> <MenuItem className={isRoute('/settings/about')}>About</MenuItem>
</MenuItemLink> </MenuItemLink>
<MenuItemLink to="/settings/developer">
<MenuItem className={isRoute('/settings/developer')}>Developer</MenuItem>
</MenuItemLink>
</SettingMenus> </SettingMenus>
<SettingContent> <SettingContent>
<Routes> <Routes>
<Route path="general" element={<SettingsHomePage />} /> <Route path="common" element={<CommonSettings />} />
<Route path="models" element={<SettingsModelsPage />} /> <Route path="system-assistant" element={<SystemAssistantSettings />} />
<Route path="default-agent" element={<SettingsDefaultAgent />} /> <Route path="default-assistant" element={<DefaultAssistantSetting />} />
<Route path="about" element={<SettingsAboutPage />} /> <Route path="llm" element={<LanguageModelsSettings />} />
<Route path="developer" element={<SettingsDeveloperPage />} /> <Route path="about" element={<AboutSettings />} />
</Routes> </Routes>
</SettingContent> </SettingContent>
</ContentContainer> </ContentContainer>
@ -65,7 +65,7 @@ const ContentContainer = styled.div`
const SettingMenus = styled.ul` const SettingMenus = styled.ul`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: var(--agents-width); min-width: var(--assistants-width);
border-right: 1px solid var(--color-border); border-right: 1px solid var(--color-border);
padding: 10px; padding: 10px;
` `
@ -84,10 +84,11 @@ const MenuItem = styled.li`
font-size: 14px; font-size: 14px;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
&:hover { &:hover {
background: #213675; background: #135200;
} }
&.active { &.active {
background: #213675; background: #135200;
font-weight: bold;
} }
` `

View File

@ -0,0 +1,10 @@
import { FC } from 'react'
import styled from 'styled-components'
const SystemAssistantSettings: FC = () => {
return <Container>System Assistant</Container>
}
const Container = styled.div``
export default SystemAssistantSettings

View File

@ -1,12 +0,0 @@
import { Agent } from '@renderer/types'
import { getDefaultTopic } from './topic'
export function getDefaultAgent(): Agent {
return {
id: 'default',
name: 'Default Agent',
description: "Hello, I'm Default Agent.",
prompt: '',
topics: [getDefaultTopic()]
}
}

View File

@ -1,4 +1,4 @@
import { Agent, Message, Topic } from '@renderer/types' import { Assistant, Message, Topic } from '@renderer/types'
import { openaiProvider } from './provider' import { openaiProvider } from './provider'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
import { EVENT_NAMES, EventEmitter } from './event' import { EVENT_NAMES, EventEmitter } from './event'
@ -6,16 +6,16 @@ import { ChatCompletionMessageParam, ChatCompletionSystemMessageParam } from 'op
interface FetchChatCompletionParams { interface FetchChatCompletionParams {
message: Message message: Message
agent: Agent assistant: Assistant
topic: Topic topic: Topic
onResponse: (message: Message) => void onResponse: (message: Message) => void
} }
export async function fetchChatCompletion({ message, agent, topic, onResponse }: FetchChatCompletionParams) { export async function fetchChatCompletion({ message, assistant, topic, onResponse }: FetchChatCompletionParams) {
const stream = await openaiProvider.chat.completions.create({ const stream = await openaiProvider.chat.completions.create({
model: 'Qwen/Qwen2-7B-Instruct', model: 'Qwen/Qwen2-7B-Instruct',
messages: [ messages: [
{ role: 'system', content: agent.prompt }, { role: 'system', content: assistant.prompt },
{ role: 'user', content: message.content } { role: 'user', content: message.content }
], ],
stream: true stream: true
@ -23,9 +23,9 @@ export async function fetchChatCompletion({ message, agent, topic, onResponse }:
const _message: Message = { const _message: Message = {
id: uuid(), id: uuid(),
role: 'agent', role: 'assistant',
content: '', content: '',
agentId: agent.id, assistantId: assistant.id,
topicId: topic.id, topicId: topic.id,
createdAt: 'now' createdAt: 'now'
} }

View File

@ -0,0 +1,12 @@
import { Assistant } from '@renderer/types'
import { getDefaultTopic } from './topic'
export function getDefaultAssistant(): Assistant {
return {
id: 'default',
name: 'Default Assistant',
description: "Hello, I'm Default Assistant.",
prompt: '',
topics: [getDefaultTopic()]
}
}

View File

@ -1,77 +0,0 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { getDefaultAgent } from '@renderer/services/agent'
import LocalStorage from '@renderer/services/storage'
import { getDefaultTopic } from '@renderer/services/topic'
import { Agent, Topic } from '@renderer/types'
import { uniqBy } from 'lodash'
export interface AgentsState {
agents: Agent[]
}
const initialState: AgentsState = {
agents: [getDefaultAgent()]
}
const agentsSlice = createSlice({
name: 'agents',
initialState,
reducers: {
addAgent: (state, action: PayloadAction<Agent>) => {
state.agents.push(action.payload)
},
removeAgent: (state, action: PayloadAction<{ id: string }>) => {
state.agents = state.agents.filter((c) => c.id !== action.payload.id)
},
updateAgent: (state, action: PayloadAction<Agent>) => {
state.agents = state.agents.map((c) => (c.id === action.payload.id ? action.payload : c))
},
addTopic: (state, action: PayloadAction<{ agentId: string; topic: Topic }>) => {
state.agents = state.agents.map((agent) =>
agent.id === action.payload.agentId
? {
...agent,
topics: uniqBy([action.payload.topic, ...agent.topics], 'id')
}
: agent
)
},
removeTopic: (state, action: PayloadAction<{ agentId: string; topic: Topic }>) => {
state.agents = state.agents.map((agent) =>
agent.id === action.payload.agentId
? {
...agent,
topics: agent.topics.filter(({ id }) => id !== action.payload.topic.id)
}
: agent
)
},
updateTopic: (state, action: PayloadAction<{ agentId: string; topic: Topic }>) => {
state.agents = state.agents.map((agent) =>
agent.id === action.payload.agentId
? {
...agent,
topics: agent.topics.map((topic) => (topic.id === action.payload.topic.id ? action.payload.topic : topic))
}
: agent
)
},
removeAllTopics: (state, action: PayloadAction<{ agentId: string }>) => {
state.agents = state.agents.map((agent) => {
if (agent.id === action.payload.agentId) {
agent.topics.forEach((topic) => LocalStorage.removeTopic(topic.id))
return {
...agent,
topics: [getDefaultTopic()]
}
}
return agent
})
}
}
})
export const { addAgent, removeAgent, updateAgent, addTopic, removeTopic, updateTopic, removeAllTopics } =
agentsSlice.actions
export default agentsSlice.reducer

View File

@ -0,0 +1,79 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { getDefaultAssistant } from '@renderer/services/assistant'
import LocalStorage from '@renderer/services/storage'
import { getDefaultTopic } from '@renderer/services/topic'
import { Assistant, Topic } from '@renderer/types'
import { uniqBy } from 'lodash'
export interface AssistantsState {
assistants: Assistant[]
}
const initialState: AssistantsState = {
assistants: [getDefaultAssistant()]
}
const assistantsSlice = createSlice({
name: 'assistants',
initialState,
reducers: {
addAssistant: (state, action: PayloadAction<Assistant>) => {
state.assistants.push(action.payload)
},
removeAssistant: (state, action: PayloadAction<{ id: string }>) => {
state.assistants = state.assistants.filter((c) => c.id !== action.payload.id)
},
updateAssistant: (state, action: PayloadAction<Assistant>) => {
state.assistants = state.assistants.map((c) => (c.id === action.payload.id ? action.payload : c))
},
addTopic: (state, action: PayloadAction<{ assistantId: string; topic: Topic }>) => {
state.assistants = state.assistants.map((assistant) =>
assistant.id === action.payload.assistantId
? {
...assistant,
topics: uniqBy([action.payload.topic, ...assistant.topics], 'id')
}
: assistant
)
},
removeTopic: (state, action: PayloadAction<{ assistantId: string; topic: Topic }>) => {
state.assistants = state.assistants.map((assistant) =>
assistant.id === action.payload.assistantId
? {
...assistant,
topics: assistant.topics.filter(({ id }) => id !== action.payload.topic.id)
}
: assistant
)
},
updateTopic: (state, action: PayloadAction<{ assistantId: string; topic: Topic }>) => {
state.assistants = state.assistants.map((assistant) =>
assistant.id === action.payload.assistantId
? {
...assistant,
topics: assistant.topics.map((topic) =>
topic.id === action.payload.topic.id ? action.payload.topic : topic
)
}
: assistant
)
},
removeAllTopics: (state, action: PayloadAction<{ assistantId: string }>) => {
state.assistants = state.assistants.map((assistant) => {
if (assistant.id === action.payload.assistantId) {
assistant.topics.forEach((topic) => LocalStorage.removeTopic(topic.id))
return {
...assistant,
topics: [getDefaultTopic()]
}
}
return assistant
})
}
}
})
export const { addAssistant, removeAssistant, updateAssistant, addTopic, removeTopic, updateTopic, removeAllTopics } =
assistantsSlice.actions
export default assistantsSlice.reducer

View File

@ -2,11 +2,11 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'
import { useDispatch, useSelector, useStore } from 'react-redux' import { useDispatch, useSelector, useStore } from 'react-redux'
import { FLUSH, PAUSE, PERSIST, persistReducer, persistStore, PURGE, REGISTER, REHYDRATE } from 'redux-persist' import { FLUSH, PAUSE, PERSIST, persistReducer, persistStore, PURGE, REGISTER, REHYDRATE } from 'redux-persist'
import storage from 'redux-persist/lib/storage' import storage from 'redux-persist/lib/storage'
import agents from './agents' import assistants from './assistants'
import settings from './settings' import settings from './settings'
const rootReducer = combineReducers({ const rootReducer = combineReducers({
agents, assistants,
settings settings
}) })

View File

@ -0,0 +1,31 @@
import { createSlice } from '@reduxjs/toolkit'
type Provider = {
id: string
name: string
apiKey: string
apiUrl: string
url: string
}
export interface LlmState {
providers: Provider[]
}
const initialState: LlmState = {
providers: []
}
const settingsSlice = createSlice({
name: 'settings',
initialState,
reducers: {
updateProvider: () => {
//
}
}
})
export const { updateProvider } = settingsSlice.actions
export default settingsSlice.reducer

View File

@ -1,4 +1,4 @@
export type Agent = { export type Assistant = {
id: string id: string
name: string name: string
description: string description: string
@ -8,9 +8,9 @@ export type Agent = {
export type Message = { export type Message = {
id: string id: string
role: 'user' | 'agent' role: 'user' | 'assistant'
content: string content: string
agentId: string assistantId: string
topicId: string topicId: string
createdAt: string createdAt: string
} }