feat: add input toolbar

This commit is contained in:
kangfenmao 2024-07-01 14:19:53 +08:00
parent 9313452490
commit 64a93e5c4b
14 changed files with 146 additions and 69 deletions

View File

@ -28,6 +28,7 @@
"@fontsource/inter": "^5.0.18",
"@reduxjs/toolkit": "^2.2.5",
"antd": "^5.18.3",
"dayjs": "^1.11.11",
"electron-updater": "^6.1.7",
"electron-window-state": "^5.0.3",
"emittery": "^1.0.3",

View File

@ -1,4 +1,4 @@
@import 'https://at.alicdn.com/t/c/font_4563475_yuh5d3ftmm.css';
@import 'https://at.alicdn.com/t/c/font_4563475_hrx8c92awui.css';
@import './markdown.scss';
:root {
--color-white: #ffffff;
@ -27,9 +27,10 @@
--navbar-height: 40px;
--sidebar-width: 70px;
--conversations-width: 240px;
--agents-width: 240px;
--settings-width: 280px;
--status-bar-height: 40px;
--input-bar-height: 120px;
}
*,

View File

@ -1,6 +1,4 @@
.markdown {
width: 100%;
max-width: 800px;
color: #fff;
font-size: 14px;
line-height: 1.6;
@ -79,6 +77,14 @@
background-color: #555;
}
pre {
white-space: pre-wrap;
}
span {
word-break: break-all;
}
code {
font-weight: 600;
padding: 3px 5px;
@ -92,5 +98,6 @@
Liberation Mono,
monospace;
font-size: 80%;
display: inline-block;
}
}

View File

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

View File

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

View File

@ -7,6 +7,7 @@ import {
updateAgent
} from '@renderer/store/agents'
import { Agent } from '@renderer/types'
import localforage from 'localforage'
export default function useAgents() {
const { agents } = useAppSelector((state) => state.agents)
@ -15,7 +16,13 @@ export default function useAgents() {
return {
agents,
addAgent: (agent: Agent) => dispatch(addAgent(agent)),
removeAgent: (id: string) => dispatch(removeAgent({ id })),
removeAgent: (id: string) => {
dispatch(removeAgent({ id }))
const agent = agents.find((a) => a.id === id)
if (agent) {
agent.conversations.forEach((id) => localforage.removeItem(`conversation:${id}`))
}
},
updateAgent: (agent: Agent) => dispatch(updateAgent(agent)),
addConversation: (agentId: string, conversationId: string) => {
dispatch(addConversationToAgent({ agentId, conversationId }))

View File

@ -5,36 +5,24 @@ import styled from 'styled-components'
import Chat from './components/Chat'
import Agents from './components/Agents'
import { uuid } from '@renderer/utils'
import { Agent } from '@renderer/types'
import { last } from 'lodash'
import { getDefaultAgent } from '@renderer/services/agent'
const HomePage: FC = () => {
const { agents, addAgent } = useAgents()
const [activeAgent, setActiveAgent] = useState(agents[0])
const onCreateConversation = () => {
const _agent = {
id: uuid(),
name: 'New conversation',
avatar: 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&f=y',
lastMessage: 'message',
lastMessageAt: 'now',
conversations: []
}
const onCreateAgent = () => {
const _agent = getDefaultAgent()
_agent.id = uuid()
addAgent(_agent)
setActiveAgent(_agent)
}
const onRemoveAgent = (agent: Agent) => {
const _agent = last(agents.filter((a) => a.id !== agent.id))
_agent && setActiveAgent(_agent)
}
return (
<Container>
<Navbar>
<NavbarLeft style={{ justifyContent: 'flex-end', borderRight: 'none' }}>
<NewButton onClick={onCreateConversation}>
<NewButton onClick={onCreateAgent}>
<i className="iconfont icon-a-addchat"></i>
</NewButton>
</NavbarLeft>
@ -46,7 +34,7 @@ const HomePage: FC = () => {
</NavbarRight>
</Navbar>
<ContentContainer>
<Agents activeAgent={activeAgent} onActive={setActiveAgent} onRemove={onRemoveAgent} />
<Agents activeAgent={activeAgent} onActive={setActiveAgent} />
<Chat agent={activeAgent} />
</ContentContainer>
</Container>

View File

@ -4,20 +4,23 @@ import useAgents from '@renderer/hooks/useAgents'
import { Agent } from '@renderer/types'
import { Dropdown, MenuProps } from 'antd'
import { EllipsisOutlined } from '@ant-design/icons'
import { last } from 'lodash'
interface Props {
activeAgent: Agent
onActive: (agent: Agent) => void
onRemove: (agent: Agent) => void
}
const Agents: FC<Props> = ({ activeAgent, onActive, onRemove }) => {
const Agents: FC<Props> = ({ activeAgent, onActive }) => {
const { agents, removeAgent } = useAgents()
const targetAgent = useRef<Agent | null>(null)
const onDelete = (agent: Agent) => {
removeAgent(agent.id)
onRemove(agent)
setTimeout(() => {
const _agent = last(agents.filter((a) => a.id !== agent.id))
_agent && onActive(_agent)
}, 0)
}
const items: MenuProps['items'] = [
@ -36,14 +39,17 @@ const Agents: FC<Props> = ({ activeAgent, onActive, onRemove }) => {
}
]
console.debug('activeAgent', activeAgent)
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">
<Dropdown menu={{ items }} trigger={['click']} placement="bottom" arrow>
<EllipsisOutlined
style={{ position: 'absolute', right: 12, top: 12 }}
onClick={(e) => {
@ -53,8 +59,7 @@ const Agents: FC<Props> = ({ activeAgent, onActive, onRemove }) => {
/>
</Dropdown>
<AgentName>{agent.name}</AgentName>
<AgentLastMessage>{agent.lastMessage}</AgentLastMessage>
<AgentTime>{agent.lastMessageAt}</AgentTime>
<AgentLastMessage>{agent.description}</AgentLastMessage>
</AgentItem>
))}
</Container>
@ -64,8 +69,8 @@ const Agents: FC<Props> = ({ activeAgent, onActive, onRemove }) => {
const Container = styled.div`
display: flex;
flex-direction: column;
min-width: var(--conversations-width);
max-width: var(--conversations-width);
min-width: var(--agents-width);
max-width: var(--agents-width);
border-right: 0.5px solid #ffffff20;
height: calc(100vh - var(--navbar-height));
overflow-y: scroll;
@ -95,11 +100,6 @@ const AgentItem = styled.div`
}
`
const AgentTime = styled.div`
font-size: 12px;
color: var(--color-text-2);
`
const AgentName = styled.div`
font-size: 14px;
color: var(--color-text-1);

View File

@ -43,6 +43,7 @@ const Conversations: FC<Props> = ({ agent, conversationId }) => {
const _message: Message = {
id: uuid(),
role: 'agent',
content: '',
agentId: agent.id,
conversationId,
@ -82,7 +83,6 @@ const Conversations: FC<Props> = ({ agent, conversationId }) => {
useEffect(() => {
runAsyncFunction(async () => {
const conversation = await localforage.getItem<Conversation>(`conversation:${conversationId}`)
console.debug('conversation', conversation)
setMessages(conversation ? conversation.messages : [])
})
}, [conversationId])
@ -102,6 +102,7 @@ const Container = styled.div`
flex-direction: column;
overflow-y: scroll;
flex-direction: column-reverse;
max-height: calc(100vh - var(--input-bar-height) - var(--navbar-height));
&::-webkit-scrollbar {
display: none;
}

View File

@ -3,6 +3,8 @@ import { Agent, Message } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { FC, useState } from 'react'
import styled from 'styled-components'
import { MoreOutlined } from '@ant-design/icons'
import { Tooltip } from 'antd'
interface Props {
agent: Agent
@ -17,6 +19,7 @@ const Inputbar: FC<Props> = ({ agent }) => {
const message: Message = {
id: uuid(),
role: 'user',
content: text,
agentId: agent.id,
conversationId,
@ -31,28 +34,100 @@ const Inputbar: FC<Props> = ({ agent }) => {
}
return (
<Textarea
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type a message..."
autoFocus
/>
<Container>
<Toolbar>
<ToolbarMenu>
<Tooltip placement="top" title=" New Chat " arrow>
<ToolbarItem>
<i className="iconfont icon-a-new-chat"></i>
</ToolbarItem>
</Tooltip>
<Tooltip placement="top" title=" Topics " arrow>
<ToolbarItem>
<i className="iconfont icon-textedit_text_topic"></i>
</ToolbarItem>
</Tooltip>
</ToolbarMenu>
<ToolbarMenu>
<Tooltip placement="top" title=" Settings " arrow>
<ToolbarItem style={{ marginRight: 0 }}>
<MoreOutlined />
</ToolbarItem>
</Tooltip>
</ToolbarMenu>
</Toolbar>
<Textarea
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type a message..."
autoFocus
contextMenu="true"
/>
</Container>
)
}
const Textarea = styled.textarea`
padding: 15px;
const Container = styled.div`
display: flex;
flex-direction: column;
width: 100%;
height: 120px;
min-height: 120px;
height: var(--input-bar-height);
border-top: 0.5px solid #ffffff20;
padding: 5px 15px;
`
const Textarea = styled.textarea`
display: flex;
flex: 1;
border: none;
outline: none;
resize: none;
font-size: 14px;
color: var(--color-text);
background-color: transparent;
border-top: 0.5px solid #ffffff20;
`
const Toolbar = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 5px;
margin: 0 -5px;
`
const ToolbarMenu = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`
const ToolbarItem = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
cursor: pointer;
width: 32px;
height: 32px;
font-size: 18px;
border-radius: 50%;
transition: all 0.2s ease-in-out;
margin-right: 6px;
color: var(--color-icon);
.iconfont {
font-size: 18px;
transition: all 0.2s ease-in-out;
}
.icon-textedit_text_topic {
font-size: 20px;
}
&:hover {
background-color: var(--color-background-soft);
.iconfont {
color: white;
}
}
`
export default Inputbar

View File

@ -65,7 +65,7 @@ const ContentContainer = styled.div`
const SettingMenus = styled.ul`
display: flex;
flex-direction: column;
min-width: var(--conversations-width);
min-width: var(--agents-width);
border-right: 1px solid #ffffff20;
padding: 10px;
`

View File

@ -1,10 +1,8 @@
export function getDefaultAgent() {
return {
id: 'default',
name: 'Chat Assistant',
avatar: '',
lastMessage: 'I am your personal intelligent assistant Cherry, how can I help you now?',
lastMessageAt: 'now',
name: 'Default Agent',
description: "Hello, I'm Default Agent.",
conversations: []
}
}

View File

@ -24,23 +24,23 @@ const agentsSlice = createSlice({
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
state.agents = state.agents.map((agent) =>
agent.id === action.payload.agentId
? {
...c,
conversations: [...c.conversations, action.payload.conversationId]
...agent,
conversations: [...new Set([...agent.conversations, action.payload.conversationId])]
}
: c
: agent
)
},
removeConversationFromAgent: (state, action: PayloadAction<{ agentId: string; conversationId: string }>) => {
state.agents = state.agents.map((c) =>
c.id === action.payload.agentId
state.agents = state.agents.map((agent) =>
agent.id === action.payload.agentId
? {
...c,
conversations: c.conversations.filter((id) => id !== action.payload.conversationId)
...agent,
conversations: agent.conversations.filter((id) => id !== action.payload.conversationId)
}
: c
: agent
)
}
}

View File

@ -1,14 +1,13 @@
export type Agent = {
id: string
name: string
avatar: string
lastMessage: string
lastMessageAt: string
description: string
conversations: string[]
}
export type Message = {
id: string
role: 'user' | 'agent'
content: string
agentId: string
conversationId: string