feat: agent rename to assistant
This commit is contained in:
parent
6408762f40
commit
900052e581
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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)
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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`
|
||||||
|
|||||||
@ -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 }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
51
src/renderer/src/hooks/useAssistants.ts
Normal file
51
src/renderer/src/hooks/useAssistants.ts
Normal 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 }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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
|
|
||||||
160
src/renderer/src/pages/home/components/Assistants.tsx
Normal file
160
src/renderer/src/pages/home/components/Assistants.tsx
Normal 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
|
||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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']}
|
||||||
|
|||||||
@ -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
|
||||||
10
src/renderer/src/pages/settings/CommonSettings.tsx
Normal file
10
src/renderer/src/pages/settings/CommonSettings.tsx
Normal 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
|
||||||
@ -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
|
|
||||||
10
src/renderer/src/pages/settings/DefaultAssistantSetting.tsx
Normal file
10
src/renderer/src/pages/settings/DefaultAssistantSetting.tsx
Normal 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
|
||||||
@ -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
|
|
||||||
36
src/renderer/src/pages/settings/LanguageModelsSettings.tsx
Normal file
36
src/renderer/src/pages/settings/LanguageModelsSettings.tsx
Normal 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
|
||||||
@ -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
|
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|||||||
10
src/renderer/src/pages/settings/SystemAssistantSettings.tsx
Normal file
10
src/renderer/src/pages/settings/SystemAssistantSettings.tsx
Normal 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
|
||||||
@ -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()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/renderer/src/services/assistant.ts
Normal file
12
src/renderer/src/services/assistant.ts
Normal 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()]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
79
src/renderer/src/store/assistants.ts
Normal file
79
src/renderer/src/store/assistants.ts
Normal 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
|
||||||
@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
31
src/renderer/src/store/llm.ts
Normal file
31
src/renderer/src/store/llm.ts
Normal 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
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user