From 3b472cf48b0313e85ccb618b6c861be758ca9dc2 Mon Sep 17 00:00:00 2001 From: Vaayne Date: Tue, 5 Aug 2025 14:39:45 +0800 Subject: [PATCH] Redesign chat UI with improved styling and message display --- .../pages/cherry-agent/CherryAgentPage.tsx | 573 +++++++++++++++--- 1 file changed, 485 insertions(+), 88 deletions(-) diff --git a/src/renderer/src/pages/cherry-agent/CherryAgentPage.tsx b/src/renderer/src/pages/cherry-agent/CherryAgentPage.tsx index 5b40c56cf..84f94a612 100644 --- a/src/renderer/src/pages/cherry-agent/CherryAgentPage.tsx +++ b/src/renderer/src/pages/cherry-agent/CherryAgentPage.tsx @@ -1,4 +1,15 @@ -import { MenuFoldOutlined, PlusOutlined, SettingOutlined } from '@ant-design/icons' +import { + ClockCircleOutlined, + DownOutlined, + ExclamationCircleOutlined, + InfoCircleOutlined, + MenuFoldOutlined, + PlusOutlined, + RightOutlined, + SettingOutlined as CogIcon, + SettingOutlined, + UserOutlined +} from '@ant-design/icons' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { useNavbarPosition } from '@renderer/hooks/useSettings' import { loggerService } from '@renderer/services/LoggerService' @@ -11,10 +22,67 @@ import { } from '@renderer/types/agent' import { Button, Input, message, Modal, Select, Tooltip } from 'antd' import React, { useCallback, useEffect, useState } from 'react' -import styled from 'styled-components' +import styled, { keyframes } from 'styled-components' const logger = loggerService.withContext('CherryAgentPage') +// Simple markdown-like formatter +const formatMarkdown = (text: string): string => { + return text + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + .replace(/`(.*?)`/g, '$1') + .replace(/```([\s\S]*?)```/g, '
$1
') + .replace(/\n/g, '
') +} + +// Message metadata extractor +const extractSystemMetadata = (log: SessionLogEntity) => { + const content = log.content as any + const metadata: { label: string; value: string; icon?: React.ReactNode }[] = [] + + switch (log.type) { + case 'agent_session_init': + if (content.system_prompt) + metadata.push({ label: 'System Prompt', value: content.system_prompt, icon: }) + if (content.max_turns) + metadata.push({ label: 'Max Turns', value: content.max_turns.toString(), icon: }) + if (content.permission_mode) + metadata.push({ label: 'Permission', value: content.permission_mode, icon: }) + if (content.cwd) metadata.push({ label: 'Working Directory', value: content.cwd, icon: }) + break + case 'agent_session_started': + if (content.session_id) + metadata.push({ label: 'Claude Session ID', value: content.session_id, icon: }) + break + case 'agent_session_result': + metadata.push({ + label: 'Status', + value: content.success ? 'Success' : 'Failed', + icon: content.success ? : + }) + if (content.num_turns) + metadata.push({ label: 'Turns', value: content.num_turns.toString(), icon: }) + if (content.duration_ms) + metadata.push({ + label: 'Duration', + value: `${(content.duration_ms / 1000).toFixed(1)}s`, + icon: + }) + if (content.total_cost_usd) + metadata.push({ label: 'Cost', value: `$${content.total_cost_usd.toFixed(4)}`, icon: }) + break + case 'agent_error': + if (content.error_message) + metadata.push({ label: 'Error', value: content.error_message, icon: }) + if (content.error_type) + metadata.push({ label: 'Type', value: content.error_type, icon: }) + break + } + + return metadata +} + // Helper function to format message content for display const formatMessageContent = (log: SessionLogEntity): string => { if (typeof log.content === 'string') { @@ -27,33 +95,9 @@ const formatMessageContent = (log: SessionLogEntity): string => { case 'user_prompt': return log.content.prompt || 'User message' - case 'agent_session_init': { - const settings: string[] = [] - if (log.content.system_prompt) settings.push(`System: ${log.content.system_prompt}`) - if (log.content.max_turns) settings.push(`Max turns: ${log.content.max_turns}`) - if (log.content.permission_mode) settings.push(`Permission: ${log.content.permission_mode}`) - if (log.content.cwd) settings.push(`Working directory: ${log.content.cwd}`) - return settings.length > 0 ? settings.join('\n') : 'Session initialized' - } - - case 'agent_session_started': - return `Started Claude session: ${log.content.session_id || 'unknown'}` - case 'agent_response': return log.content.content || 'Agent response' - case 'agent_session_result': { - const result: string[] = [] - result.push(`Session completed ${log.content.success ? 'successfully' : 'with errors'}`) - if (log.content.num_turns) result.push(`Turns: ${log.content.num_turns}`) - if (log.content.duration_ms) result.push(`Duration: ${(log.content.duration_ms / 1000).toFixed(1)}s`) - if (log.content.total_cost_usd) result.push(`Cost: $${log.content.total_cost_usd.toFixed(4)}`) - return result.join('\n') - } - - case 'agent_error': - return `Error: ${log.content.error_message || log.content.error_type || 'Unknown error'}` - case 'raw_stdout': case 'raw_stderr': // Skip raw output in UI @@ -91,6 +135,37 @@ const formatMessageContent = (log: SessionLogEntity): string => { return 'No content' } +// Get system message title +const getSystemMessageTitle = (log: SessionLogEntity): string => { + switch (log.type) { + case 'agent_session_init': + return 'Session Initialized' + case 'agent_session_started': + return 'Claude Session Started' + case 'agent_session_result': + return 'Session Completed' + case 'agent_error': + return 'Error Occurred' + default: + return 'System Message' + } +} + +// Get system message status +const getSystemMessageStatus = (log: SessionLogEntity): 'info' | 'success' | 'warning' | 'error' => { + switch (log.type) { + case 'agent_session_init': + case 'agent_session_started': + return 'info' + case 'agent_session_result': + return (log.content as any)?.success ? 'success' : 'error' + case 'agent_error': + return 'error' + default: + return 'info' + } +} + // Helper function to check if a log should be displayed const shouldDisplayLog = (log: SessionLogEntity): boolean => { // Hide raw stdout/stderr logs @@ -119,6 +194,26 @@ const CherryAgentPage: React.FC = () => { const [createForm, setCreateForm] = useState({ name: '', model: 'claude-3-5-sonnet-20241022' }) const [inputMessage, setInputMessage] = useState('') const [isRunning, setIsRunning] = useState(false) + const [collapsedSystemMessages, setCollapsedSystemMessages] = useState>(new Set()) + + // Toggle system message collapse + const toggleSystemMessage = useCallback((logId: number) => { + setCollapsedSystemMessages((prev) => { + const newSet = new Set(prev) + if (newSet.has(logId)) { + newSet.delete(logId) + } else { + newSet.add(logId) + } + return newSet + }) + }, []) + + // Initialize collapsed state for system messages + useEffect(() => { + const systemMessages = sessionLogs.filter((log) => log.role === 'system') + setCollapsedSystemMessages(new Set(systemMessages.map((log) => log.id))) + }, [sessionLogs]) // Define callback functions first const loadAgents = useCallback(async () => { @@ -385,16 +480,78 @@ const CherryAgentPage: React.FC = () => { const content = formatMessageContent(log) if (!content) return null + // Render system messages differently + if (log.role === 'system') { + const isCollapsed = collapsedSystemMessages.has(log.id) + const metadata = extractSystemMetadata(log) + const title = getSystemMessageTitle(log) + const status = getSystemMessageStatus(log) + + return ( + + toggleSystemMessage(log.id)} + $clickable={metadata.length > 0}> + + + {status === 'error' ? : } + + {title} + + + {new Date(log.created_at).toLocaleTimeString()} + {metadata.length > 0 && ( + + {isCollapsed ? : } + + )} + + + {!isCollapsed && metadata.length > 0 && ( + + {metadata.map((item, index) => ( + + + {item.icon && {item.icon}} + {item.label} + + {item.value} + + ))} + + )} + + ) + } + + // Render user and agent messages + const isUser = log.role === 'user' + return ( - - {log.role.toUpperCase()} - {content} - {new Date(log.created_at).toLocaleTimeString()} - + + {isUser ? ( + + {content} + {new Date(log.created_at).toLocaleTimeString()} + + ) : ( + + 🤖 + + + {new Date(log.created_at).toLocaleTimeString()} + + + )} + ) })} {sessionLogs.filter(shouldDisplayLog).length === 0 && ( - No messages yet. Start the conversation below! + + 💬 + No messages yet + Start the conversation below! + )} @@ -707,7 +864,8 @@ const ConversationArea = styled.div` flex: 1; display: flex; flex-direction: column; - overflow: hidden; + overflow: auto; + max-height: 88%; ` const ConversationHeader = styled.div` @@ -757,81 +915,310 @@ const SessionStatusBadge = styled.span<{ $status: string }>` }}; ` +// Animations +const fadeIn = keyframes` + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +` + const MessagesContainer = styled.div` flex: 1; overflow-y: auto; - padding: 20px; + padding: 24px; display: flex; flex-direction: column; - gap: 16px; + gap: 20px; + background: linear-gradient(to bottom, var(--color-background), var(--color-background-soft)); ` -const MessageBubble = styled.div<{ $role: string; $type?: string }>` - align-self: ${(props) => (props.$role === 'user' ? 'flex-end' : 'flex-start')}; - max-width: ${(props) => (props.$role === 'system' ? '90%' : '70%')}; - background-color: ${(props) => { - if (props.$role === 'user') return 'var(--color-primary)' - if (props.$role === 'system') { - // Different colors for different system message types - if (props.$type?.includes('error')) return 'var(--color-error-light)' - if (props.$type?.includes('result')) return 'var(--color-success-light)' - return 'var(--color-warning-light)' - } - return 'var(--color-background-muted)' - }}; - color: ${(props) => { - if (props.$role === 'user') return 'white' - if (props.$role === 'system' && props.$type?.includes('error')) return 'var(--color-error)' - if (props.$role === 'system' && props.$type?.includes('result')) return 'var(--color-success)' - return 'var(--color-text)' - }}; +// System Message Styles +const SystemMessageCard = styled.div<{ $status: 'info' | 'success' | 'warning' | 'error' }>` + background: var(--color-background); + border: 1px solid + ${(props) => { + switch (props.$status) { + case 'success': + return 'var(--color-success-light)' + case 'error': + return 'var(--color-error-light)' + case 'warning': + return 'var(--color-warning-light)' + default: + return 'var(--color-border)' + } + }}; border-radius: 12px; + overflow: hidden; + animation: ${fadeIn} 0.3s ease-out; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + transition: all 0.2s ease; + + &:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + } +` + +const SystemMessageHeader = styled.div<{ $clickable: boolean }>` + display: flex; + align-items: center; + justify-content: space-between; padding: 12px 16px; - position: relative; - word-break: break-word; - overflow-wrap: break-word; - border: ${(props) => { - if (props.$role === 'system' && props.$type?.includes('error')) return '1px solid var(--color-error)' - if (props.$role === 'system' && props.$type?.includes('result')) return '1px solid var(--color-success)' - return 'none' + background: var(--color-background-soft); + cursor: ${(props) => (props.$clickable ? 'pointer' : 'default')}; + transition: background-color 0.2s ease; + + ${(props) => + props.$clickable && + ` + &:hover { + background: var(--color-background-hover); + } + `} +` + +const SystemMessageTitle = styled.div` + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; + font-size: 14px; + color: var(--color-text); +` + +const SystemMessageIcon = styled.div<{ $status: 'info' | 'success' | 'warning' | 'error' }>` + width: 16px; + height: 16px; + color: ${(props) => { + switch (props.$status) { + case 'success': + return 'var(--color-success)' + case 'error': + return 'var(--color-error)' + case 'warning': + return 'var(--color-warning)' + default: + return 'var(--color-primary)' + } }}; ` -const MessageRole = styled.div` - font-size: 10px; - font-weight: 600; +const SystemMessageHeaderRight = styled.div` + display: flex; + align-items: center; + gap: 8px; +` + +const SystemMessageTime = styled.div` + font-size: 11px; + color: var(--color-text-tertiary); +` + +const CollapseIcon = styled.div<{ $collapsed: boolean }>` + width: 16px; + height: 16px; + color: var(--color-text-secondary); + transition: transform 0.2s ease; + transform: ${(props) => (props.$collapsed ? 'rotate(0deg)' : 'rotate(0deg)')}; +` + +const SystemMessageContent = styled.div` + padding: 16px; + border-top: 1px solid var(--color-border-light); + background: var(--color-background); +` + +const MetadataItem = styled.div` + display: flex; + justify-content: space-between; + align-items: flex-start; + padding: 8px 0; + border-bottom: 1px solid var(--color-border-light); + + &:last-child { + border-bottom: none; + padding-bottom: 0; + } + + &:first-child { + padding-top: 0; + } +` + +const MetadataLabel = styled.div` + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + font-weight: 500; + color: var(--color-text-secondary); text-transform: uppercase; - margin-bottom: 4px; - opacity: 0.7; + letter-spacing: 0.5px; + flex-shrink: 0; + min-width: 120px; ` -const MessageContent = styled.div` +const MetadataIcon = styled.div` + width: 12px; + height: 12px; + color: var(--color-text-tertiary); +` + +const MetadataValue = styled.div` + font-size: 13px; + color: var(--color-text); + text-align: right; + word-break: break-all; + max-width: 60%; + background: var(--color-background-muted); + padding: 4px 8px; + border-radius: 6px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; +` + +// Message Wrapper +const MessageWrapper = styled.div<{ $align: 'left' | 'right' }>` + display: flex; + justify-content: ${(props) => (props.$align === 'right' ? 'flex-end' : 'flex-start')}; + animation: ${fadeIn} 0.3s ease-out; +` + +// User Message Styles +const UserMessage = styled.div` + max-width: 70%; + background: linear-gradient(135deg, var(--color-primary), var(--color-primary-dark, #1890ff)); + color: white; + border-radius: 18px 18px 4px 18px; + padding: 12px 16px; + box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2); + position: relative; +` + +const UserMessageContent = styled.div` font-size: 14px; - line-height: 1.4; - white-space: pre-wrap; + line-height: 1.5; word-break: break-word; - overflow-wrap: break-word; - max-width: 100%; + margin-bottom: 4px; ` -const MessageTime = styled.div` - font-size: 10px; - margin-top: 4px; +// Agent Message Styles +const AgentMessage = styled.div` + max-width: 85%; + display: flex; + gap: 12px; + align-items: flex-start; +` + +const AgentAvatar = styled.div` + width: 32px; + height: 32px; + border-radius: 50%; + background: linear-gradient(135deg, var(--color-success), var(--color-success-dark, #52c41a)); + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + flex-shrink: 0; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +` + +const AgentMessageContent = styled.div` + flex: 1; + background: var(--color-background); + border: 1px solid var(--color-border); + border-radius: 4px 18px 18px 18px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +` + +const AgentMessageText = styled.div` + padding: 16px; + font-size: 14px; + line-height: 1.6; + color: var(--color-text); + + strong { + font-weight: 600; + color: var(--color-text); + } + + em { + font-style: italic; + color: var(--color-text-secondary); + } + + code { + background: var(--color-background-muted); + padding: 2px 6px; + border-radius: 4px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 13px; + color: var(--color-primary); + } + + pre { + background: var(--color-background-muted); + padding: 12px; + border-radius: 6px; + overflow-x: auto; + margin: 8px 0; + border-left: 3px solid var(--color-primary); + + code { + background: none; + padding: 0; + color: var(--color-text); + } + } +` + +// Shared Message Timestamp +const MessageTimestamp = styled.div` + font-size: 11px; + opacity: 0.7; + padding: 8px 16px; + background: var(--color-background-soft); + border-top: 1px solid var(--color-border-light); + color: var(--color-text-tertiary); +` + +// Empty State +const EmptyConversation = styled.div` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + text-align: center; + flex: 1; +` + +const EmptyConversationIcon = styled.div` + font-size: 48px; + margin-bottom: 16px; opacity: 0.6; ` -const EmptyConversation = styled.div` - text-align: center; +const EmptyConversationTitle = styled.div` + font-size: 18px; + font-weight: 500; + color: var(--color-text); + margin-bottom: 8px; +` + +const EmptyConversationSubtitle = styled.div` + font-size: 14px; color: var(--color-text-secondary); - font-style: italic; - padding: 40px 20px; ` const InputArea = styled.div` - border-top: 1px solid var(--color-border); - padding: 16px 20px; - background-color: var(--color-background); - flex-shrink: 0; /* Don't allow this to shrink */ + padding: 20px 24px; + flex-shrink: 0; ` const MessageInput = styled.div` @@ -839,16 +1226,26 @@ const MessageInput = styled.div` gap: 12px; align-items: flex-end; width: 100%; - - .ant-input { - flex: 1; - min-width: 0; /* Allow text area to shrink */ - } + max-width: 800px; + margin: 0 auto; ` const SendButton = styled(Button)` height: auto; - padding: 8px 16px; + padding: 10px 20px; + border-radius: 12px; + font-weight: 500; + box-shadow: 0 2px 4px rgba(24, 144, 255, 0.2); + transition: all 0.2s ease; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(24, 144, 255, 0.3); + } + + &:active { + transform: translateY(0); + } ` const SelectionPrompt = styled.div`