refactor: thread -> agent

This commit is contained in:
kangfenmao 2024-06-28 15:19:04 +08:00
parent 182d631dd6
commit 33dbc88d60
12 changed files with 133 additions and 136 deletions

View File

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

View File

@ -1,31 +0,0 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
addConversationToThread,
addThread,
removeConversationFromThread,
removeThread,
updateThread
} from '@renderer/store/threads'
import { Thread } from '@renderer/types'
import { useState } from 'react'
export default function useThreads() {
const { threads } = useAppSelector((state) => state.threads)
const [threadId, setThreadId] = useState(threads[0]?.id)
const dispatch = useAppDispatch()
return {
threads,
thread: threads.find((t) => t.id === threadId),
setThread: (thread: Thread) => setThreadId(thread.id),
addThread: (thread: Thread) => dispatch(addThread(thread)),
removeThread: (id: string) => dispatch(removeThread({ id })),
updateThread: (thread: Thread) => dispatch(updateThread(thread)),
addConversation: (threadId: string, conversationId: string) => {
dispatch(addConversationToThread({ threadId, conversationId }))
},
removeConversation: (threadId: string, conversationId: string) => {
dispatch(removeConversationFromThread({ threadId, conversationId }))
}
}
}

View File

@ -1,20 +1,20 @@
import { Navbar, NavbarCenter, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import useThreads from '@renderer/hooks/useThreads' import useAgents from '@renderer/hooks/useAgents'
import { FC, useEffect } from 'react' import { FC, useEffect } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Chat from './components/Chat' import Chat from './components/Chat'
import Threads from './components/Threads' import Agents from './components/Agents'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
const HomePage: FC = () => { const HomePage: FC = () => {
const { threads, thread, setThread, addThread } = useThreads() const { agents, agent, setAgent, addAgent } = useAgents()
useEffect(() => { useEffect(() => {
!thread && threads[0] && setThread(threads[0]) !agent && agents[0] && setAgent(agents[0])
}, [thread, threads]) }, [agent, agents])
const onCreateConversation = () => { const onCreateConversation = () => {
const _thread = { const _agent = {
id: uuid(), id: uuid(),
name: 'New conversation', name: 'New conversation',
avatar: 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&f=y', avatar: 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&f=y',
@ -22,8 +22,8 @@ const HomePage: FC = () => {
lastMessageAt: 'now', lastMessageAt: 'now',
conversations: [] conversations: []
} }
addThread(_thread) addAgent(_agent)
setThread(_thread) setAgent(_agent)
} }
return ( return (
@ -34,7 +34,7 @@ const HomePage: FC = () => {
<i className="iconfont icon-a-addchat"></i> <i className="iconfont icon-a-addchat"></i>
</NewButton> </NewButton>
</NavbarLeft> </NavbarLeft>
<NavbarCenter style={{ border: 'none' }}>{thread?.name}</NavbarCenter> <NavbarCenter style={{ border: 'none' }}>{agent?.name}</NavbarCenter>
<NavbarRight style={{ justifyContent: 'flex-end', padding: 5 }}> <NavbarRight style={{ justifyContent: 'flex-end', padding: 5 }}>
<NewButton> <NewButton>
<i className="iconfont icon-showsidebarhoriz"></i> <i className="iconfont icon-showsidebarhoriz"></i>
@ -42,8 +42,8 @@ const HomePage: FC = () => {
</NavbarRight> </NavbarRight>
</Navbar> </Navbar>
<ContentContainer> <ContentContainer>
<Threads /> <Agents />
{thread && <Chat thread={thread} />} {agent && <Chat agent={agent} />}
</ContentContainer> </ContentContainer>
</Container> </Container>
) )

View File

@ -2,32 +2,29 @@ import { FC } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { IconMore } from '@douyinfe/semi-icons' import { IconMore } from '@douyinfe/semi-icons'
import { Dropdown } from '@douyinfe/semi-ui' import { Dropdown } from '@douyinfe/semi-ui'
import useThreads from '@renderer/hooks/useThreads' import useAgents from '@renderer/hooks/useAgents'
const Threads: FC = () => { const Agents: FC = () => {
const { threads, thread, setThread, removeThread } = useThreads() const { agents, setAgent, removeAgent } = useAgents()
return ( return (
<Container> <Container>
{threads.map((thread) => ( {agents.map((agent) => (
<ThreadItem <AgentItem key={agent.id} onClick={() => setAgent(agent)} className={agent.id === agent?.id ? 'active' : ''}>
key={thread.id}
onClick={() => setThread(thread)}
className={thread.id === thread?.id ? 'active' : ''}>
<Dropdown <Dropdown
trigger="click" trigger="click"
stopPropagation stopPropagation
render={ render={
<Dropdown.Menu> <Dropdown.Menu>
<Dropdown.Item onClick={() => removeThread(thread.id)}>Delete</Dropdown.Item> <Dropdown.Item onClick={() => removeAgent(agent.id)}>Delete</Dropdown.Item>
</Dropdown.Menu> </Dropdown.Menu>
}> }>
<IconMore style={{ position: 'absolute', right: 12, top: 12 }} /> <IconMore style={{ position: 'absolute', right: 12, top: 12 }} />
</Dropdown> </Dropdown>
<ThreadName>{thread.name}</ThreadName> <AgentName>{agent.name}</AgentName>
<ThreadLastMessage>{thread.lastMessage}</ThreadLastMessage> <AgentLastMessage>{agent.lastMessage}</AgentLastMessage>
<ThreadTime>{thread.lastMessageAt}</ThreadTime> <AgentTime>{agent.lastMessageAt}</AgentTime>
</ThreadItem> </AgentItem>
))} ))}
</Container> </Container>
) )
@ -47,7 +44,7 @@ const Container = styled.div`
} }
` `
const ThreadItem = styled.div` const AgentItem = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 10px; padding: 10px;
@ -70,18 +67,18 @@ const ThreadItem = styled.div`
margin-bottom: 10px; margin-bottom: 10px;
` `
const ThreadTime = styled.div` const AgentTime = styled.div`
font-size: 12px; font-size: 12px;
color: var(--color-text-2); color: var(--color-text-2);
` `
const ThreadName = styled.div` const AgentName = styled.div`
font-size: 14px; font-size: 14px;
color: var(--color-text-1); color: var(--color-text-1);
font-weight: bold; font-weight: bold;
` `
const ThreadLastMessage = styled.div` const AgentLastMessage = styled.div`
font-size: 12px; font-size: 12px;
line-height: 20px; line-height: 20px;
color: var(--color-text-2); color: var(--color-text-2);
@ -93,4 +90,4 @@ const ThreadLastMessage = styled.div`
height: 20px; height: 20px;
` `
export default Threads export default Agents

View File

@ -1,24 +1,24 @@
import { Message, Thread } from '@renderer/types' import { Message, Agent } from '@renderer/types'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Inputbar from './Inputbar' import Inputbar from './Inputbar'
import Conversations from './Conversations' import Conversations from './Conversations'
import useThreads from '@renderer/hooks/useThreads' import useAgents from '@renderer/hooks/useAgents'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import localforage from 'localforage' import localforage from 'localforage'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
interface Props { interface Props {
thread: Thread agent: Agent
} }
const Chat: FC<Props> = ({ thread }) => { const Chat: FC<Props> = ({ agent }) => {
const [conversationId] = useState<string>(thread.conversations[0] || uuid()) const [conversationId] = useState<string>(agent.conversations[0] || uuid())
return ( return (
<Container> <Container>
<Conversations thread={thread} conversationId={conversationId} /> <Conversations agent={agent} conversationId={conversationId} />
<Inputbar thread={thread} /> <Inputbar agent={agent} />
</Container> </Container>
) )
} }

View File

@ -1,7 +1,7 @@
import { Avatar } from '@douyinfe/semi-ui' import { Avatar } from '@douyinfe/semi-ui'
import useThreads from '@renderer/hooks/useThreads' import useAgents from '@renderer/hooks/useAgents'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Conversation, Message, Thread } from '@renderer/types' import { Conversation, Message, Agent } from '@renderer/types'
import { runAsyncFunction } from '@renderer/utils' import { runAsyncFunction } from '@renderer/utils'
import localforage from 'localforage' import localforage from 'localforage'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
@ -9,19 +9,19 @@ import { FC, useEffect, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
interface Props { interface Props {
thread: Thread agent: Agent
conversationId: string conversationId: string
} }
const Conversations: FC<Props> = ({ thread, conversationId }) => { const Conversations: FC<Props> = ({ agent, conversationId }) => {
const [messages, setMessages] = useState<Message[]>([]) const [messages, setMessages] = useState<Message[]>([])
const { addConversation } = useThreads() const { addConversation } = useAgents()
const onSendMessage = (message: Message) => { const onSendMessage = (message: Message) => {
setMessages([...messages, message]) setMessages([...messages, message])
if (isEmpty(thread?.conversations)) { if (isEmpty(agent?.conversations)) {
addConversation(thread.id, conversationId) addConversation(agent.id, conversationId)
} }
localforage.setItem<Conversation>(`conversation:${conversationId}`, { localforage.setItem<Conversation>(`conversation:${conversationId}`, {

View File

@ -1,24 +1,24 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Message, Thread } from '@renderer/types' import { Message, Agent } 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'
interface Props { interface Props {
thread: Thread agent: Agent
} }
const Inputbar: FC<Props> = ({ thread }) => { const Inputbar: FC<Props> = ({ agent }) => {
const [text, setText] = useState('') const [text, setText] = useState('')
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => { const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
const conversationId = thread.conversations[0] ? thread.conversations[0] : uuid() const conversationId = agent.conversations[0] ? agent.conversations[0] : uuid()
const message: Message = { const message: Message = {
id: uuid(), id: uuid(),
content: text, content: text,
threadId: thread.id, agentId: agent.id,
conversationId, conversationId,
createdAt: 'now' createdAt: 'now'
} }

View File

@ -1,4 +1,4 @@
export function getDefaultThread() { export function getDefaultAgent() {
return { return {
id: 'default', id: 'default',
name: 'Chat Assistant', name: 'Chat Assistant',

View File

@ -0,0 +1,52 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { getDefaultAgent } from '@renderer/services/agent'
import { Agent } from '@renderer/types'
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))
},
addConversationToAgent: (state, action: PayloadAction<{ agentId: string; conversationId: string }>) => {
state.agents = state.agents.map((c) =>
c.id === action.payload.agentId
? {
...c,
conversations: [...c.conversations, action.payload.conversationId]
}
: c
)
},
removeConversationFromAgent: (state, action: PayloadAction<{ agentId: string; conversationId: string }>) => {
state.agents = state.agents.map((c) =>
c.id === action.payload.agentId
? {
...c,
conversations: c.conversations.filter((id) => id !== action.payload.conversationId)
}
: c
)
}
}
})
export const { addAgent, removeAgent, updateAgent, addConversationToAgent, removeConversationFromAgent } =
agentsSlice.actions
export default agentsSlice.reducer

View File

@ -2,7 +2,7 @@ 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 threads from './threads' import agents from './agents'
const store = configureStore({ const store = configureStore({
reducer: persistReducer( reducer: persistReducer(
@ -12,7 +12,7 @@ const store = configureStore({
version: 1 version: 1
}, },
combineReducers({ combineReducers({
threads agents
}) })
), ),
middleware: (getDefaultMiddleware) => { middleware: (getDefaultMiddleware) => {

View File

@ -1,52 +0,0 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { getDefaultThread } from '@renderer/services/thread'
import { Thread } from '@renderer/types'
export interface ThreadsState {
threads: Thread[]
}
const initialState: ThreadsState = {
threads: [getDefaultThread()]
}
const threadsSlice = createSlice({
name: 'threads',
initialState,
reducers: {
addThread: (state, action: PayloadAction<Thread>) => {
state.threads.push(action.payload)
},
removeThread: (state, action: PayloadAction<{ id: string }>) => {
state.threads = state.threads.filter((c) => c.id !== action.payload.id)
},
updateThread: (state, action: PayloadAction<Thread>) => {
state.threads = state.threads.map((c) => (c.id === action.payload.id ? action.payload : c))
},
addConversationToThread: (state, action: PayloadAction<{ threadId: string; conversationId: string }>) => {
state.threads = state.threads.map((c) =>
c.id === action.payload.threadId
? {
...c,
conversations: [...c.conversations, action.payload.conversationId]
}
: c
)
},
removeConversationFromThread: (state, action: PayloadAction<{ threadId: string; conversationId: string }>) => {
state.threads = state.threads.map((c) =>
c.id === action.payload.threadId
? {
...c,
conversations: c.conversations.filter((id) => id !== action.payload.conversationId)
}
: c
)
}
}
})
export const { addThread, removeThread, updateThread, addConversationToThread, removeConversationFromThread } =
threadsSlice.actions
export default threadsSlice.reducer

View File

@ -1,4 +1,4 @@
export type Thread = { export type Agent = {
id: string id: string
name: string name: string
avatar: string avatar: string
@ -10,7 +10,7 @@ export type Thread = {
export type Message = { export type Message = {
id: string id: string
content: string content: string
threadId: string agentId: string
conversationId: string conversationId: string
createdAt: string createdAt: string
} }