Redesign agent sidebar with avatar gradients

This commit is contained in:
Vaayne
2025-07-31 23:41:03 +08:00
parent 3d94740482
commit bbc0e9378a
2 changed files with 260 additions and 102 deletions
@@ -24,6 +24,30 @@ import EnhancedCommandInput from './components/EnhancedCommandInput'
import PocMessageList from './components/PocMessageList'
import SessionManagementModal from './components/SessionManagementModal'
// Utility function to generate consistent colors for agent avatars
const getAvatarGradient = (name: string) => {
const gradients = [
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)',
'linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%)',
'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)',
'linear-gradient(135deg, #ff8a80 0%, #ea6e6e 100%)',
'linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%)'
]
// Simple hash function to get consistent color based on name
let hash = 0
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash)
}
return gradients[Math.abs(hash) % gradients.length]
}
const CherryAgentPage: React.FC = () => {
const { isLeftNavbar } = useNavbarPosition()
@@ -196,6 +220,29 @@ const CherryAgentPage: React.FC = () => {
[editingSession, agentManagement, handleCloseSessionModal]
)
const handleDeleteAgent = useCallback(
async (agent: AgentResponse) => {
Modal.confirm({
title: 'Delete Agent',
content: `Are you sure you want to delete the agent "${agent.name}"?`,
okText: 'Delete',
okType: 'danger',
cancelText: 'Cancel',
onOk: async () => {
await agentManagement.deleteAgent(agent.id)
// If we deleted the current agent, switch to another one
if (agentManagement.currentAgent?.id === agent.id && agentManagement.agents.length > 1) {
const remainingAgents = agentManagement.agents.filter((a) => a.id !== agent.id)
if (remainingAgents.length > 0) {
agentManagement.setCurrentAgent(remainingAgents[0])
}
}
}
})
},
[agentManagement]
)
const handleDeleteSession = useCallback(
async (session: SessionResponse) => {
Modal.confirm({
@@ -247,24 +294,29 @@ const CherryAgentPage: React.FC = () => {
[agentManagement]
)
// Filter sessions for the current agent
const filteredSessions = agentManagement.sessions.filter(
(session) => agentManagement.currentAgent && session.agent_ids.includes(agentManagement.currentAgent.id)
)
// Create a default agent if none exists
useEffect(() => {
if (!agentManagement.loadingAgents && agentManagement.agents.length === 0) {
agentManagement.createAgent({
name: 'My Agent',
model: 'gpt-4',
description: 'Default agent for command execution',
name: 'Assistant',
model: '',
description: 'General-purpose AI assistant',
instructions: 'You are a helpful assistant that can execute commands and help with tasks.'
})
}
}, [agentManagement.loadingAgents, agentManagement.agents.length, agentManagement])
}, [agentManagement])
// Set the first agent as current if none is selected
useEffect(() => {
if (!agentManagement.currentAgent && agentManagement.agents.length > 0) {
agentManagement.setCurrentAgent(agentManagement.agents[0])
}
}, [agentManagement.currentAgent, agentManagement.agents, agentManagement])
}, [agentManagement])
// Create a default session for the current agent if none exists
useEffect(() => {
@@ -281,14 +333,7 @@ const CherryAgentPage: React.FC = () => {
agentManagement.setCurrentSession(agentManagement.sessions[0])
}
}
}, [
agentManagement.currentAgent,
agentManagement.currentSession,
agentManagement.sessions,
agentManagement.loadingSessions,
currentWorkingDirectory,
agentManagement
])
}, [agentManagement, currentWorkingDirectory])
return (
<Container id="cherry-agent-page">
@@ -302,39 +347,81 @@ const CherryAgentPage: React.FC = () => {
<SidebarHeader>
<HeaderLabel>agents</HeaderLabel>
<HeaderActions>
<Dropdown
overlay={
<Menu>
<Menu.Item key="create" icon={<PlusOutlined />} onClick={handleCreateAgent}>
Create Agent
</Menu.Item>
{agentManagement.currentAgent && (
<Menu.Item
key="edit"
icon={<UserOutlined />}
onClick={() => handleEditAgent(agentManagement.currentAgent!)}>
Edit Current Agent
</Menu.Item>
)}
</Menu>
}
trigger={['click']}
placement="bottomRight">
<ActionButton type="text" icon={<PlusOutlined />} size="small" title="Agent Actions" />
</Dropdown>
<ActionButton
type="text"
icon={<PlusOutlined />}
size="small"
title="Agent Actions"
onClick={handleCreateAgent}
/>
<CollapseButton type="text" icon={<MenuFoldOutlined />} onClick={toggleSidebar} size="small" />
</HeaderActions>
</SidebarHeader>
<SidebarContent>
<SectionHeader>
{/* Agents Section */}
<AgentsList>
{agentManagement.agents.map((agent) => (
<Dropdown
key={agent.id}
overlay={
<Menu>
<Menu.Item key="edit" icon={<EditOutlined />} onClick={() => handleEditAgent(agent)}>
Edit Agent
</Menu.Item>
<Menu.Item
key="delete"
icon={<DeleteOutlined />}
onClick={() => handleDeleteAgent(agent)}
danger>
Delete Agent
</Menu.Item>
</Menu>
}
trigger={['contextMenu']}>
<AgentItem
active={agentManagement.currentAgent?.id === agent.id}
onClick={() => agentManagement.setCurrentAgent(agent)}>
<AgentAvatar style={{ background: getAvatarGradient(agent.name) }}>
{agent.name.charAt(0).toUpperCase()}
</AgentAvatar>
<AgentInfo>
<AgentName>{agent.name}</AgentName>
<AgentDescription>{agent.description || 'No description'}</AgentDescription>
</AgentInfo>
</AgentItem>
</Dropdown>
))}
{agentManagement.agents.length === 0 && !agentManagement.loadingAgents && (
<EmptyState>No agents available</EmptyState>
)}
</AgentsList>
{/* Sessions Section */}
<SectionHeader style={{ marginTop: '32px' }}>
<SessionsLabel>sessions</SessionsLabel>
<Tooltip title="Create new session">
<ActionButton type="text" icon={<PlusOutlined />} onClick={handleCreateSession} size="small" />
</Tooltip>
</SectionHeader>
<SessionsList>
{agentManagement.sessions.map((session) => (
<SessionItemWrapper key={session.id}>
{filteredSessions.map((session) => (
<Dropdown
key={session.id}
overlay={
<Menu>
<Menu.Item key="edit" icon={<EditOutlined />} onClick={() => handleEditSession(session)}>
Edit Session
</Menu.Item>
<Menu.Item
key="delete"
icon={<DeleteOutlined />}
onClick={() => handleDeleteSession(session)}
danger>
Delete Session
</Menu.Item>
</Menu>
}
trigger={['contextMenu']}>
<SessionItem
active={agentManagement.currentSession?.id === session.id}
onClick={() => handleSessionSelect(session)}>
@@ -345,35 +432,14 @@ const CherryAgentPage: React.FC = () => {
</SessionMeta>
</SessionInfo>
</SessionItem>
<SessionActions>
<Tooltip title="Edit session">
<ActionButton
type="text"
icon={<EditOutlined />}
onClick={(e) => {
e.stopPropagation()
handleEditSession(session)
}}
size="small"
/>
</Tooltip>
<Tooltip title="Delete session">
<ActionButton
type="text"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation()
handleDeleteSession(session)
}}
size="small"
danger
/>
</Tooltip>
</SessionActions>
</SessionItemWrapper>
</Dropdown>
))}
{agentManagement.sessions.length === 0 && !agentManagement.loadingSessions && (
<EmptyState>No sessions available</EmptyState>
{filteredSessions.length === 0 && !agentManagement.loadingSessions && (
<EmptyState>
{agentManagement.currentAgent
? `No sessions for ${agentManagement.currentAgent.name}`
: 'No sessions available'}
</EmptyState>
)}
</SessionsList>
</SidebarContent>
@@ -397,12 +463,20 @@ const CherryAgentPage: React.FC = () => {
{sidebarCollapsed && (
<ToggleButton type="text" icon={<MenuUnfoldOutlined />} onClick={toggleSidebar} size="small" />
)}
<AgentNameInput
value={agentManagement.currentAgent?.name || ''}
onChange={(e) => handleAgentNameChange(e.target.value)}
onBlur={(e) => handleAgentNameChange(e.target.value)}
placeholder="Agent Name & some Info"
/>
{agentManagement.currentAgent && (
<CurrentAgentAvatar style={{ background: getAvatarGradient(agentManagement.currentAgent.name) }}>
{agentManagement.currentAgent.name.charAt(0).toUpperCase()}
</CurrentAgentAvatar>
)}
<AgentTitleSection>
<AgentNameInput
value={agentManagement.currentAgent?.name || ''}
onChange={(e) => handleAgentNameChange(e.target.value)}
onBlur={(e) => handleAgentNameChange(e.target.value)}
placeholder="Agent Name"
/>
<AgentSubtitle>{agentManagement.currentAgent?.description || 'No description provided'}</AgentSubtitle>
</AgentTitleSection>
</AgentHeader>
<MessageArea>
@@ -542,6 +616,8 @@ const ActionButton = styled(Button)`
height: 32px;
border-radius: 8px;
transition: all 0.2s ease;
border: none;
box-shadow: none;
&:hover {
color: var(--color-text);
@@ -552,6 +628,11 @@ const ActionButton = styled(Button)`
&:active {
transform: scale(0.95);
}
&:focus {
box-shadow: none;
border: none;
}
`
const MainContent = styled.div`
@@ -646,25 +727,6 @@ const SessionsList = styled.div`
gap: 8px;
`
const SessionActions = styled.div`
display: flex;
opacity: 0;
transition: opacity 0.2s ease;
gap: 4px;
`
const SessionItemWrapper = styled.div`
display: flex;
align-items: center;
gap: 8px;
&:hover {
${SessionActions} {
opacity: 1;
}
}
`
const SessionItem = styled.div<{ active: boolean }>`
flex: 1;
padding: 12px 16px;
@@ -715,30 +777,32 @@ const EmptyState = styled.div`
font-style: italic;
`
const AgentNameInput = styled.input`
const AgentTitleSection = styled.div`
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
`
const AgentNameInput = styled.input`
border: 2px solid var(--color-border);
border-radius: 12px;
padding: 12px 16px;
font-size: 16px;
font-weight: 500;
background-color: var(--color-background);
border-radius: 8px;
padding: 8px 12px;
font-size: 18px;
font-weight: 600;
background-color: transparent;
color: var(--color-text);
outline: none;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
width: 100%;
&:focus {
border-color: var(--color-primary);
box-shadow:
0 0 0 3px rgba(24, 144, 255, 0.1),
0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.1);
}
&:hover {
border-color: var(--color-primary-hover);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);
}
&::placeholder {
@@ -747,4 +811,98 @@ const AgentNameInput = styled.input`
}
`
const AgentSubtitle = styled.div`
color: var(--color-text-secondary);
font-size: 14px;
font-weight: 400;
`
const CurrentAgentAvatar = styled.div`
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 18px;
flex-shrink: 0;
`
const AgentsList = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 24px;
overflow: auto;
`
const AgentItem = styled.div<{ active: boolean }>`
flex: 1;
width: 100%;
padding: 6px;
border-radius: 4px;
background-color: ${(props) => (props.active ? 'var(--color-primary)' : 'var(--color-background-soft)')};
color: ${(props) => (props.active ? 'white' : 'var(--color-text)')};
cursor: pointer;
transition: all 0.2s ease;
border: ${(props) => (props.active ? 'none' : '1px solid transparent')};
box-shadow: ${(props) => (props.active ? '0 2px 8px rgba(24, 144, 255, 0.2)' : 'none')};
user-select: none;
display: flex;
align-items: center;
gap: 6px;
&:hover {
background-color: ${(props) => (props.active ? 'var(--color-primary)' : 'var(--color-background-hover)')};
border-color: ${(props) => (props.active ? 'transparent' : 'var(--color-border)')};
transform: translateY(-1px);
box-shadow: ${(props) => (props.active ? '0 4px 12px rgba(24, 144, 255, 0.3)' : '0 2px 8px rgba(0, 0, 0, 0.1)')};
}
&:active {
transform: translateY(0);
}
`
const AgentAvatar = styled.div`
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 14px;
flex-shrink: 0;
`
const AgentInfo = styled.div`
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
`
const AgentName = styled.div`
font-size: 14px;
font-weight: 500;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`
const AgentDescription = styled.div`
font-size: 12px;
opacity: 0.8;
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`
export default CherryAgentPage
@@ -212,7 +212,7 @@ const AgentManagementModal: FC<AgentManagementModalProps> = ({ visible, onClose,
name: '',
description: '',
instructions: '',
model: 'gpt-4o',
model: '',
tools: [],
knowledges: []
}}>