Redesign chat UI with improved styling and message display

This commit is contained in:
Vaayne
2025-08-05 14:39:45 +08:00
parent 6087cb687d
commit 3b472cf48b
@@ -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, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code>$1</code>')
.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
.replace(/\n/g, '<br/>')
}
// 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: <CogIcon /> })
if (content.max_turns)
metadata.push({ label: 'Max Turns', value: content.max_turns.toString(), icon: <ClockCircleOutlined /> })
if (content.permission_mode)
metadata.push({ label: 'Permission', value: content.permission_mode, icon: <UserOutlined /> })
if (content.cwd) metadata.push({ label: 'Working Directory', value: content.cwd, icon: <InfoCircleOutlined /> })
break
case 'agent_session_started':
if (content.session_id)
metadata.push({ label: 'Claude Session ID', value: content.session_id, icon: <InfoCircleOutlined /> })
break
case 'agent_session_result':
metadata.push({
label: 'Status',
value: content.success ? 'Success' : 'Failed',
icon: content.success ? <InfoCircleOutlined /> : <ExclamationCircleOutlined />
})
if (content.num_turns)
metadata.push({ label: 'Turns', value: content.num_turns.toString(), icon: <ClockCircleOutlined /> })
if (content.duration_ms)
metadata.push({
label: 'Duration',
value: `${(content.duration_ms / 1000).toFixed(1)}s`,
icon: <ClockCircleOutlined />
})
if (content.total_cost_usd)
metadata.push({ label: 'Cost', value: `$${content.total_cost_usd.toFixed(4)}`, icon: <InfoCircleOutlined /> })
break
case 'agent_error':
if (content.error_message)
metadata.push({ label: 'Error', value: content.error_message, icon: <ExclamationCircleOutlined /> })
if (content.error_type)
metadata.push({ label: 'Type', value: content.error_type, icon: <ExclamationCircleOutlined /> })
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<Set<number>>(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 (
<SystemMessageCard key={log.id} $status={status}>
<SystemMessageHeader
onClick={() => toggleSystemMessage(log.id)}
$clickable={metadata.length > 0}>
<SystemMessageTitle>
<SystemMessageIcon $status={status}>
{status === 'error' ? <ExclamationCircleOutlined /> : <InfoCircleOutlined />}
</SystemMessageIcon>
<span>{title}</span>
</SystemMessageTitle>
<SystemMessageHeaderRight>
<SystemMessageTime>{new Date(log.created_at).toLocaleTimeString()}</SystemMessageTime>
{metadata.length > 0 && (
<CollapseIcon $collapsed={isCollapsed}>
{isCollapsed ? <RightOutlined /> : <DownOutlined />}
</CollapseIcon>
)}
</SystemMessageHeaderRight>
</SystemMessageHeader>
{!isCollapsed && metadata.length > 0 && (
<SystemMessageContent>
{metadata.map((item, index) => (
<MetadataItem key={index}>
<MetadataLabel>
{item.icon && <MetadataIcon>{item.icon}</MetadataIcon>}
{item.label}
</MetadataLabel>
<MetadataValue>{item.value}</MetadataValue>
</MetadataItem>
))}
</SystemMessageContent>
)}
</SystemMessageCard>
)
}
// Render user and agent messages
const isUser = log.role === 'user'
return (
<MessageBubble key={log.id} $role={log.role} $type={log.type}>
<MessageRole>{log.role.toUpperCase()}</MessageRole>
<MessageContent>{content}</MessageContent>
<MessageTime>{new Date(log.created_at).toLocaleTimeString()}</MessageTime>
</MessageBubble>
<MessageWrapper key={log.id} $align={isUser ? 'right' : 'left'}>
{isUser ? (
<UserMessage>
<UserMessageContent>{content}</UserMessageContent>
<MessageTimestamp>{new Date(log.created_at).toLocaleTimeString()}</MessageTimestamp>
</UserMessage>
) : (
<AgentMessage>
<AgentAvatar>🤖</AgentAvatar>
<AgentMessageContent>
<AgentMessageText dangerouslySetInnerHTML={{ __html: formatMarkdown(content) }} />
<MessageTimestamp>{new Date(log.created_at).toLocaleTimeString()}</MessageTimestamp>
</AgentMessageContent>
</AgentMessage>
)}
</MessageWrapper>
)
})}
{sessionLogs.filter(shouldDisplayLog).length === 0 && (
<EmptyConversation>No messages yet. Start the conversation below!</EmptyConversation>
<EmptyConversation>
<EmptyConversationIcon>💬</EmptyConversationIcon>
<EmptyConversationTitle>No messages yet</EmptyConversationTitle>
<EmptyConversationSubtitle>Start the conversation below!</EmptyConversationSubtitle>
</EmptyConversation>
)}
</MessagesContainer>
</ConversationArea>
@@ -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`