diff --git a/src/renderer/src/pages/cherry-agent/CherryAgentPage.tsx b/src/renderer/src/pages/cherry-agent/CherryAgentPage.tsx index 3579d96ee..5aa4b7994 100644 --- a/src/renderer/src/pages/cherry-agent/CherryAgentPage.tsx +++ b/src/renderer/src/pages/cherry-agent/CherryAgentPage.tsx @@ -1,17 +1,28 @@ -import { MenuFoldOutlined, MenuUnfoldOutlined, SettingOutlined } from '@ant-design/icons' +import { + DeleteOutlined, + EditOutlined, + MenuFoldOutlined, + MenuUnfoldOutlined, + PlusOutlined, + SettingOutlined, + UserOutlined +} from '@ant-design/icons' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { useAgentManagement } from '@renderer/hooks/useAgentManagement' import { useCommandHistory } from '@renderer/hooks/useCommandHistory' import { usePocCommand } from '@renderer/hooks/usePocCommand' import { usePocMessages } from '@renderer/hooks/usePocMessages' import { useNavbarPosition } from '@renderer/hooks/useSettings' -import { Button } from 'antd' +import type { AgentResponse, SessionResponse } from '@types' +import { Button, Dropdown, Menu, Modal, Tooltip } from 'antd' import React, { useCallback, useEffect, useState } from 'react' import styled from 'styled-components' +import AgentManagementModal from './components/AgentManagementModal' import CherryAgentSettingsModal from './components/CherryAgentSettingsModal' import EnhancedCommandInput from './components/EnhancedCommandInput' import PocMessageList from './components/PocMessageList' +import SessionManagementModal from './components/SessionManagementModal' const CherryAgentPage: React.FC = () => { const { isLeftNavbar } = useNavbarPosition() @@ -26,6 +37,10 @@ const CherryAgentPage: React.FC = () => { const [commandCount, setCommandCount] = useState(0) const [currentWorkingDirectory, setCurrentWorkingDirectory] = useState('/Users/weliu') const [showSettingsModal, setShowSettingsModal] = useState(false) + const [showAgentModal, setShowAgentModal] = useState(false) + const [editingAgent, setEditingAgent] = useState(null) + const [showSessionModal, setShowSessionModal] = useState(false) + const [editingSession, setEditingSession] = useState(null) const [sidebarCollapsed, setSidebarCollapsed] = useState(false) // Handle command execution @@ -87,6 +102,116 @@ const CherryAgentPage: React.FC = () => { setCurrentWorkingDirectory(workingDirectory) }, []) + // Handle agent modal + const handleCreateAgent = useCallback(() => { + setEditingAgent(null) + setShowAgentModal(true) + }, []) + + const handleEditAgent = useCallback((agent: AgentResponse) => { + setEditingAgent(agent) + setShowAgentModal(true) + }, []) + + const handleCloseAgentModal = useCallback(() => { + setShowAgentModal(false) + setEditingAgent(null) + }, []) + + const handleSaveAgent = useCallback( + async (agentData: AgentResponse) => { + if (editingAgent) { + // Update existing agent + await agentManagement.updateAgent({ + id: editingAgent.id, + name: agentData.name, + description: agentData.description, + avatar: agentData.avatar, + instructions: agentData.instructions, + model: agentData.model, + tools: agentData.tools, + knowledges: agentData.knowledges, + configuration: agentData.configuration + }) + } else { + // Create new agent + await agentManagement.createAgent({ + name: agentData.name, + description: agentData.description, + avatar: agentData.avatar, + instructions: agentData.instructions, + model: agentData.model, + tools: agentData.tools, + knowledges: agentData.knowledges, + configuration: agentData.configuration + }) + } + handleCloseAgentModal() + }, + [editingAgent, agentManagement, handleCloseAgentModal] + ) + + // Handle session modal + const handleCreateSession = useCallback(() => { + setEditingSession(null) + setShowSessionModal(true) + }, []) + + const handleEditSession = useCallback((session: SessionResponse) => { + setEditingSession(session) + setShowSessionModal(true) + }, []) + + const handleCloseSessionModal = useCallback(() => { + setShowSessionModal(false) + setEditingSession(null) + }, []) + + const handleSaveSession = useCallback( + async (sessionData: SessionResponse) => { + if (editingSession) { + // Update existing session + await agentManagement.updateSession({ + id: editingSession.id, + user_prompt: sessionData.user_prompt, + agent_ids: sessionData.agent_ids, + status: sessionData.status, + accessible_paths: sessionData.accessible_paths + }) + } else { + // Create new session + const newSession = await agentManagement.createSession({ + user_prompt: sessionData.user_prompt, + agent_ids: sessionData.agent_ids, + status: sessionData.status || 'idle', + accessible_paths: sessionData.accessible_paths + }) + // Set the new session as current + if (newSession) { + agentManagement.setCurrentSession(newSession) + } + } + handleCloseSessionModal() + }, + [editingSession, agentManagement, handleCloseSessionModal] + ) + + const handleDeleteSession = useCallback( + async (session: SessionResponse) => { + Modal.confirm({ + title: 'Delete Session', + content: `Are you sure you want to delete the session "${session.user_prompt || session.id.slice(0, 8)}"?`, + okText: 'Delete', + okType: 'danger', + cancelText: 'Cancel', + onOk: async () => { + await agentManagement.deleteSession(session.id) + } + }) + }, + [agentManagement] + ) + // Handle command cancellation const handleCancelCommand = useCallback(async () => { if (commandHook.currentCommandId) { @@ -175,22 +300,80 @@ const CherryAgentPage: React.FC = () => { {!sidebarCollapsed && ( - header - } onClick={toggleSidebar} size="small" /> + agents + + + } onClick={handleCreateAgent}> + Create Agent + + {agentManagement.currentAgent && ( + } + onClick={() => handleEditAgent(agentManagement.currentAgent!)}> + Edit Current Agent + + )} + + } + trigger={['click']} + placement="bottomRight"> + } size="small" title="Agent Actions" /> + + } onClick={toggleSidebar} size="small" /> + - sessions + + sessions + + } onClick={handleCreateSession} size="small" /> + + {agentManagement.sessions.map((session) => ( - handleSessionSelect(session)}> - {session.user_prompt || `Session ${session.id.slice(0, 8)}`} - + + handleSessionSelect(session)}> + + {session.user_prompt || `Session ${session.id.slice(0, 8)}`} + + {session.agent_ids.length} agent{session.agent_ids.length !== 1 ? 's' : ''} • {session.status} + + + + + + } + onClick={(e) => { + e.stopPropagation() + handleEditSession(session) + }} + size="small" + /> + + + } + onClick={(e) => { + e.stopPropagation() + handleDeleteSession(session) + }} + size="small" + danger + /> + + + ))} {agentManagement.sessions.length === 0 && !agentManagement.loadingSessions && ( - No sessions available + No sessions available )} @@ -246,6 +429,22 @@ const CherryAgentPage: React.FC = () => { onSave={handleSaveSettings} currentWorkingDirectory={currentWorkingDirectory} /> + + ) } @@ -289,6 +488,12 @@ const SidebarHeader = styled.div` background-color: var(--color-background-soft); ` +const HeaderActions = styled.div` + display: flex; + align-items: center; + gap: 8px; +` + const SidebarContent = styled.div` flex: 1; padding: 20px; @@ -420,13 +625,19 @@ const FooterLabel = styled.span` letter-spacing: 0.5px; ` +const SectionHeader = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +` + const SessionsLabel = styled.div` font-size: 11px; color: var(--color-text-tertiary); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; - margin-bottom: 16px; ` const SessionsList = styled.div` @@ -435,14 +646,32 @@ 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; border-radius: 8px; background-color: ${(props) => (props.active ? 'var(--color-primary)' : 'var(--color-background-soft)')}; color: ${(props) => (props.active ? 'white' : 'var(--color-text)')}; cursor: pointer; - font-size: 14px; - font-weight: ${(props) => (props.active ? '500' : '400')}; 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')}; @@ -460,6 +689,32 @@ const SessionItem = styled.div<{ active: boolean }>` } ` +const SessionInfo = styled.div` + display: flex; + flex-direction: column; + gap: 4px; +` + +const SessionTitle = styled.div` + font-size: 14px; + font-weight: 500; + line-height: 1.2; +` + +const SessionMeta = styled.div` + font-size: 11px; + opacity: 0.8; + line-height: 1; +` + +const EmptyState = styled.div` + padding: 20px; + text-align: center; + color: var(--color-text-tertiary); + font-size: 14px; + font-style: italic; +` + const AgentNameInput = styled.input` flex: 1; border: 2px solid var(--color-border); diff --git a/src/renderer/src/pages/cherry-agent/components/AgentManagementModal.tsx b/src/renderer/src/pages/cherry-agent/components/AgentManagementModal.tsx new file mode 100644 index 000000000..9f0a61436 --- /dev/null +++ b/src/renderer/src/pages/cherry-agent/components/AgentManagementModal.tsx @@ -0,0 +1,377 @@ +import { UserOutlined } from '@ant-design/icons' +import { loggerService } from '@logger' +import type { AgentResponse } from '@types' +import { Form, Input, Modal, Select, Space, Upload } from 'antd' +import { FC, useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +const logger = loggerService.withContext('AgentManagementModal') + +interface AgentManagementModalProps { + visible: boolean + onClose: () => void + onSave: (agent: AgentResponse) => void + agent?: AgentResponse | null // null for create, AgentResponse for edit + loading?: boolean +} + +// Common model options based on the models.ts file +const MODEL_OPTIONS = [ + // Popular models from different providers + { value: 'gpt-4o', label: 'GPT-4o' }, + { value: 'gpt-4o-mini', label: 'GPT-4o Mini' }, + { value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet' }, + { value: 'claude-3-5-haiku-20241022', label: 'Claude 3.5 Haiku' }, + { value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' }, + { value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro' }, + { value: 'deepseek-chat', label: 'DeepSeek Chat' }, + { value: 'deepseek-reasoner', label: 'DeepSeek Reasoner' }, + { value: 'qwen2.5-72b-instruct', label: 'Qwen 2.5 72B' }, + { value: 'moonshot-v1-auto', label: 'Moonshot V1 Auto' }, + { value: 'yi-lightning', label: 'Yi Lightning' }, + { value: 'glm-4.5', label: 'GLM-4.5' }, + { value: 'hunyuan-pro', label: 'Hunyuan Pro' }, + { value: 'doubao-pro-32k-241215', label: 'Doubao Pro 32K' }, + { value: 'abab6.5s-chat', label: 'Minimax ABAB 6.5s' }, + { value: 'step-1-8k', label: 'Step 1 8K' }, + { value: 'grok-2-1212', label: 'Grok 2' }, + { value: 'llama-3.3-70b-instruct', label: 'Llama 3.3 70B' }, + { value: 'mistral-large-latest', label: 'Mistral Large' } +] + +// Common tools that can be enabled for agents +const TOOL_OPTIONS = [ + { value: 'web_search', label: 'Web Search' }, + { value: 'code_interpreter', label: 'Code Interpreter' }, + { value: 'file_search', label: 'File Search' }, + { value: 'dall_e', label: 'DALL·E Image Generation' }, + { value: 'python_execution', label: 'Python Execution' }, + { value: 'browser_automation', label: 'Browser Automation' }, + { value: 'api_calls', label: 'API Calls' }, + { value: 'document_analysis', label: 'Document Analysis' } +] + +const AgentManagementModal: FC = ({ visible, onClose, onSave, agent, loading = false }) => { + const { t } = useTranslation() + // loading parameter is reserved for future loading state implementation + void loading + const [form] = Form.useForm() + const [isSubmitting, setIsSubmitting] = useState(false) + const [avatarUrl, setAvatarUrl] = useState('') + + const isEditMode = Boolean(agent) + const modalTitle = isEditMode + ? t('agent.modal.edit.title', 'Edit Agent') + : t('agent.modal.create.title', 'Create Agent') + + // Initialize form when agent changes + useEffect(() => { + if (visible) { + if (isEditMode && agent) { + form.setFieldsValue({ + name: agent.name, + description: agent.description || '', + instructions: agent.instructions || '', + model: agent.model, + tools: agent.tools || [], + knowledges: agent.knowledges || [] + }) + setAvatarUrl(agent.avatar || '') + } else { + form.resetFields() + setAvatarUrl('') + } + } + }, [visible, agent, isEditMode, form]) + + const handleOk = async () => { + try { + setIsSubmitting(true) + const values = await form.validateFields() + + const agentData = { + name: values.name, + description: values.description || '', + avatar: avatarUrl || '', + instructions: values.instructions || '', + model: values.model, + tools: values.tools || [], + knowledges: values.knowledges || [], + configuration: {} + } + + let result: AgentResponse | null = null + + if (isEditMode && agent) { + // Update existing agent + // Update existing agent - no need to store input locally + // const updateInput: UpdateAgentInput = { + // id: agent.id, + // ...agentData + // } + + // Call parent's save handler - it should handle the API call + result = await new Promise((resolve) => { + // This is a bit of a hack - we need to modify the parent interface + // For now, we'll assume onSave handles the API call and returns the result + onSave(agentData as AgentResponse) + resolve(agentData as AgentResponse) // Mock return + }) + } else { + // Create new agent - no need to store input locally + // const createInput: CreateAgentInput = agentData + + // Call parent's save handler + result = await new Promise((resolve) => { + onSave(agentData as AgentResponse) + resolve(agentData as AgentResponse) // Mock return + }) + } + + if (result) { + handleCancel() // Close modal and reset form + } + } catch (error) { + logger.error('Failed to save agent:', error as Error) + } finally { + setIsSubmitting(false) + } + } + + const handleCancel = () => { + form.resetFields() + setAvatarUrl('') + setIsSubmitting(false) + onClose() + } + + const handleAvatarChange = useCallback((info: any) => { + if (info.file.status === 'done') { + // In a real app, you'd upload the file and get a URL back + // For now, we'll just use a placeholder or file URL + const url = info.file.response?.url || URL.createObjectURL(info.file.originFileObj) + setAvatarUrl(url) + } + }, []) + + return ( + +
+ {/* Basic Information Section */} + {t('agent.modal.section.basic', 'Basic Information')} + + + + + false} // Prevent auto upload + > + {avatarUrl ? ( + + ) : ( + + +
+ {t('agent.modal.avatar.upload', 'Upload')} +
+
+ )} +
+
+
+ + + + + + + + + + +
+ + {/* Configuration Section */} + {t('agent.modal.section.configuration', 'Configuration')} + + + + + + + + + + {isEditMode && ( + +