2b6538c650
- Introduced new sections in the dashboard for irrigation recommendations, fertilization recommendations, farm AI assistant, and pest detection. - Added Persian translations for new features to enhance user experience. - Updated the vertical menu to include links to the new sections. - Enhanced global styles with animations for improved UI interactions.
672 lines
22 KiB
TypeScript
672 lines
22 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useRef, useEffect } from 'react'
|
||
import { useTranslations } from 'next-intl'
|
||
import Box from '@mui/material/Box'
|
||
import Typography from '@mui/material/Typography'
|
||
import Card from '@mui/material/Card'
|
||
import IconButton from '@mui/material/IconButton'
|
||
import TextField from '@mui/material/TextField'
|
||
import Collapse from '@mui/material/Collapse'
|
||
import classnames from 'classnames'
|
||
import type { FarmContext, FarmAIMessage, AIResponseSection } from './farmAiAssistantTypes'
|
||
|
||
// ─── Constants ─────────────────────────────────────────────────────────────
|
||
|
||
const DEFAULT_FARM_CONTEXT: FarmContext = {
|
||
soilType: 'Loamy',
|
||
waterEC: '1.2 dS/m',
|
||
selectedCrop: 'Tomato',
|
||
growthStage: 'Flowering',
|
||
lastIrrigationStatus: '2 days ago'
|
||
}
|
||
|
||
const SUGGESTION_CHIPS = [
|
||
{ id: 'yellow-leaves', labelKey: 'suggestions.yellowLeaves' },
|
||
{ id: 'irrigation-plan', labelKey: 'suggestions.irrigationPlan' },
|
||
{ id: 'fertilizer-flowering', labelKey: 'suggestions.fertilizerFlowering' },
|
||
{ id: 'plant-disease', labelKey: 'suggestions.plantDisease' }
|
||
]
|
||
|
||
// Demo structured AI response for display
|
||
const DEMO_AI_RESPONSE_SECTIONS: AIResponseSection[] = [
|
||
{
|
||
type: 'recommendation',
|
||
title: 'Irrigation recommendation',
|
||
icon: 'droplet',
|
||
frequency: '3 times per week',
|
||
amount: '15–20 L per plant',
|
||
timing: 'Early morning (05:00–07:00)',
|
||
expandableExplanation:
|
||
'Your loamy soil holds moisture well, but tomatoes during flowering need consistent moisture. Water EC of 1.2 dS/m is suitable. Last irrigation was 2 days ago—avoid overwatering to prevent blossom-end rot.'
|
||
},
|
||
{
|
||
type: 'list',
|
||
title: 'Key points',
|
||
icon: 'leaf',
|
||
items: [
|
||
'Avoid midday watering to reduce evaporation',
|
||
'Drip irrigation preferred for root zone targeting',
|
||
'Monitor soil moisture before each session'
|
||
]
|
||
},
|
||
{
|
||
type: 'warning',
|
||
title: 'Weather advisory',
|
||
icon: 'warning',
|
||
content: 'High temps forecasted next week. Consider increasing frequency to 4x/week temporarily.'
|
||
}
|
||
]
|
||
|
||
// ─── Main Component ────────────────────────────────────────────────────────
|
||
|
||
export default function FarmAiAssistantChat() {
|
||
const t = useTranslations('farmAiAssistant')
|
||
const [messages, setMessages] = useState<FarmAIMessage[]>([])
|
||
const [inputValue, setInputValue] = useState('')
|
||
const [isContextExpanded, setIsContextExpanded] = useState(true)
|
||
const [isTyping, setIsTyping] = useState(false)
|
||
const [selectedChip, setSelectedChip] = useState<string | null>(null)
|
||
const [expandedExplanations, setExpandedExplanations] = useState<Set<string>>(new Set())
|
||
const scrollRef = useRef<HTMLDivElement>(null)
|
||
|
||
const farmContext = DEFAULT_FARM_CONTEXT
|
||
|
||
// Scroll to bottom on new messages
|
||
useEffect(() => {
|
||
scrollRef.current?.scrollTo({
|
||
top: scrollRef.current.scrollHeight,
|
||
behavior: 'smooth'
|
||
})
|
||
}, [messages, isTyping])
|
||
|
||
const handleSuggestionClick = (chipId: string, labelKey: string) => {
|
||
const label = t(labelKey)
|
||
setSelectedChip(prev => (prev === chipId ? null : chipId))
|
||
handleSend(label)
|
||
}
|
||
|
||
const handleSend = async (text?: string) => {
|
||
const content = (text || inputValue).trim()
|
||
if (!content) return
|
||
|
||
setInputValue('')
|
||
setSelectedChip(null)
|
||
|
||
const userMessage: FarmAIMessage = {
|
||
id: `u-${Date.now()}`,
|
||
role: 'user',
|
||
content,
|
||
timestamp: new Date()
|
||
}
|
||
setMessages(prev => [...prev, userMessage])
|
||
setIsTyping(true)
|
||
|
||
// Simulate AI response (replace with actual API call)
|
||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||
|
||
const aiMessage: FarmAIMessage = {
|
||
id: `a-${Date.now()}`,
|
||
role: 'assistant',
|
||
content: '',
|
||
timestamp: new Date(),
|
||
sections: DEMO_AI_RESPONSE_SECTIONS
|
||
}
|
||
setMessages(prev => [...prev, aiMessage])
|
||
setIsTyping(false)
|
||
}
|
||
|
||
const toggleExplanation = (id: string) => {
|
||
setExpandedExplanations(prev => {
|
||
const next = new Set(prev)
|
||
if (next.has(id)) next.delete(id)
|
||
else next.add(id)
|
||
return next
|
||
})
|
||
}
|
||
|
||
return (
|
||
<Box
|
||
className='flex flex-col min-bs-screen max-w-md mx-auto'
|
||
sx={{
|
||
background: 'linear-gradient(180deg, #f8fcf8 0%, #f0f7f4 30%, #e8f4ef 100%)',
|
||
minHeight: '100dvh'
|
||
}}
|
||
>
|
||
{/* 1) Smart Header */}
|
||
<Box
|
||
className='flex items-center gap-3 px-4 pt-4 pb-3 flex-shrink-0'
|
||
sx={{
|
||
background: 'linear-gradient(135deg, rgba(34, 197, 94, 0.06) 0%, rgba(14, 165, 233, 0.06) 100%)',
|
||
borderBottom: '1px solid rgba(34, 197, 94, 0.12)'
|
||
}}
|
||
>
|
||
<Box
|
||
className='w-12 h-12 rounded-2xl flex items-center justify-center shrink-0'
|
||
sx={{
|
||
background: 'linear-gradient(145deg, #22c55e 0%, #0ea5e9 100%)',
|
||
boxShadow: '0 4px 12px rgba(34, 197, 94, 0.3)'
|
||
}}
|
||
>
|
||
<i className='tabler-leaf text-2xl text-white' />
|
||
</Box>
|
||
<Box className='min-w-0 flex-1'>
|
||
<Typography
|
||
variant='h6'
|
||
className='font-bold truncate'
|
||
sx={{
|
||
background: 'linear-gradient(135deg, #16a34a 0%, #0ea5e9 100%)',
|
||
backgroundClip: 'text',
|
||
WebkitBackgroundClip: 'text',
|
||
color: 'transparent',
|
||
fontSize: '1.1rem'
|
||
}}
|
||
>
|
||
{t('header.title')}
|
||
</Typography>
|
||
<Typography variant='caption' color='text.secondary' className='block truncate'>
|
||
{t('header.subtitle')}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* 2) Expandable Farm Context Bar */}
|
||
<Box
|
||
className='mx-4 mt-3 flex-shrink-0 rounded-2xl overflow-hidden'
|
||
sx={{
|
||
background: 'linear-gradient(145deg, #ffffff 0%, rgba(34, 197, 94, 0.04) 100%)',
|
||
border: '1px solid rgba(34, 197, 94, 0.15)',
|
||
boxShadow: '0 2px 12px rgba(0,0,0,0.04)'
|
||
}}
|
||
>
|
||
<Box
|
||
component='button'
|
||
type='button'
|
||
onClick={() => setIsContextExpanded(!isContextExpanded)}
|
||
className='w-full flex items-center justify-between px-4 py-3 text-start'
|
||
sx={{
|
||
'&:hover': { bgcolor: 'rgba(34, 197, 94, 0.04)' },
|
||
transition: 'background 0.2s'
|
||
}}
|
||
>
|
||
<Typography variant='subtitle2' fontWeight={600} color='text.secondary'>
|
||
{t('context.title')}
|
||
</Typography>
|
||
<i
|
||
className={classnames('tabler-chevron-down text-xl text-[#22c55e] transition-transform duration-300', {
|
||
'rotate-180': isContextExpanded
|
||
})}
|
||
/>
|
||
</Box>
|
||
<Collapse in={isContextExpanded}>
|
||
<Box className='px-4 pb-4 grid grid-cols-2 gap-2'>
|
||
<ContextBadge icon='tabler-seedling' label={t('context.soilType')} value={farmContext.soilType} />
|
||
<ContextBadge icon='tabler-droplet' label={t('context.waterEC')} value={farmContext.waterEC} />
|
||
<ContextBadge icon='tabler-plant-2' label={t('context.crop')} value={farmContext.selectedCrop} />
|
||
<ContextBadge icon='tabler-flower' label={t('context.growthStage')} value={farmContext.growthStage} />
|
||
<ContextBadge
|
||
icon='tabler-calendar'
|
||
label={t('context.lastIrrigation')}
|
||
value={farmContext.lastIrrigationStatus}
|
||
colSpan={2}
|
||
/>
|
||
</Box>
|
||
</Collapse>
|
||
</Box>
|
||
|
||
{/* 3) Chat Area */}
|
||
<Box
|
||
ref={scrollRef}
|
||
className='flex-1 overflow-y-auto overflow-x-hidden px-4 py-4 space-y-4'
|
||
sx={{ scrollBehavior: 'smooth' }}
|
||
>
|
||
{messages.length === 0 && !isTyping && (
|
||
<Box className='flex flex-col items-center justify-center py-16 text-center'>
|
||
<Box
|
||
className='w-20 h-20 rounded-3xl flex items-center justify-center mb-4'
|
||
sx={{
|
||
background: 'linear-gradient(145deg, rgba(34, 197, 94, 0.1) 0%, rgba(14, 165, 233, 0.1) 100%)'
|
||
}}
|
||
>
|
||
<i className='tabler-message-circle text-4xl text-[#22c55e]' />
|
||
</Box>
|
||
<Typography variant='body1' color='text.secondary' className='mb-1'>
|
||
{t('emptyState.title')}
|
||
</Typography>
|
||
<Typography variant='caption' color='text.secondary'>
|
||
{t('emptyState.subtitle')}
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
|
||
{messages.map(msg => (
|
||
<MessageBubble
|
||
key={msg.id}
|
||
message={msg}
|
||
expandedExplanations={expandedExplanations}
|
||
onToggleExplanation={toggleExplanation}
|
||
t={t}
|
||
/>
|
||
))}
|
||
|
||
{/* Typing indicator */}
|
||
{isTyping && (
|
||
<Box className='animate-fade-in flex items-start gap-3'>
|
||
<Box
|
||
className='w-9 h-9 rounded-xl flex items-center justify-center shrink-0'
|
||
sx={{
|
||
background: 'linear-gradient(145deg, #22c55e 0%, #0ea5e9 100%)'
|
||
}}
|
||
>
|
||
<i className='tabler-leaf text-lg text-white' />
|
||
</Box>
|
||
<Box
|
||
className='px-4 py-3 rounded-2xl rounded-tl-md'
|
||
sx={{
|
||
background: 'linear-gradient(145deg, #ffffff 0%, rgba(34, 197, 94, 0.05) 100%)',
|
||
border: '1px solid rgba(34, 197, 94, 0.15)',
|
||
boxShadow: '0 2px 12px rgba(0,0,0,0.04)'
|
||
}}
|
||
>
|
||
<TypingIndicator />
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
|
||
{/* 4) Suggestion Chips */}
|
||
<Box className='px-4 py-2 flex-shrink-0 overflow-x-auto scrollbar-hide'>
|
||
<Box className='flex gap-2 pb-2'>
|
||
{SUGGESTION_CHIPS.map(chip => (
|
||
<Box
|
||
key={chip.id}
|
||
component='button'
|
||
type='button'
|
||
onClick={() => handleSuggestionClick(chip.id, chip.labelKey)}
|
||
className={classnames(
|
||
'px-4 py-2 rounded-2xl text-sm font-medium whitespace-nowrap transition-all duration-200 shrink-0',
|
||
'hover:scale-[1.02] active:scale-[0.98]'
|
||
)}
|
||
sx={{
|
||
background:
|
||
selectedChip === chip.id
|
||
? 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)'
|
||
: 'linear-gradient(145deg, #ffffff 0%, rgba(34, 197, 94, 0.08) 100%)',
|
||
color: selectedChip === chip.id ? '#ffffff' : 'text.primary',
|
||
border: selectedChip === chip.id ? 'none' : '1px solid rgba(34, 197, 94, 0.2)',
|
||
boxShadow:
|
||
selectedChip === chip.id
|
||
? '0 4px 12px rgba(34, 197, 94, 0.35)'
|
||
: '0 2px 8px rgba(0,0,0,0.04)'
|
||
}}
|
||
>
|
||
{t(chip.labelKey)}
|
||
</Box>
|
||
))}
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* 5) Input Area - Sticky Bottom */}
|
||
<Box
|
||
className='px-4 py-3 pb-6 flex-shrink-0'
|
||
sx={{
|
||
background: 'linear-gradient(to top, rgba(248,252,248,0.98) 0%, rgba(248,252,248,0.9) 70%, transparent 100%)',
|
||
backdropFilter: 'blur(12px)'
|
||
}}
|
||
>
|
||
<Box
|
||
className='flex items-end gap-2'
|
||
sx={{
|
||
background: '#ffffff',
|
||
borderRadius: '24px',
|
||
border: '1px solid rgba(34, 197, 94, 0.2)',
|
||
boxShadow: '0 4px 20px rgba(34, 197, 94, 0.08)',
|
||
p: 1
|
||
}}
|
||
>
|
||
<IconButton
|
||
component='label'
|
||
htmlFor='farm-ai-camera'
|
||
size='small'
|
||
className='shrink-0'
|
||
sx={{
|
||
color: 'text.secondary',
|
||
'&:hover': { color: '#22c55e', bgcolor: 'rgba(34, 197, 94, 0.08)' }
|
||
}}
|
||
>
|
||
<i className='tabler-camera text-xl' />
|
||
<input id='farm-ai-camera' type='file' accept='image/*' hidden />
|
||
</IconButton>
|
||
<IconButton
|
||
size='small'
|
||
className='shrink-0'
|
||
sx={{
|
||
color: 'text.secondary',
|
||
'&:hover': { color: '#0ea5e9', bgcolor: 'rgba(14, 165, 233, 0.08)' }
|
||
}}
|
||
>
|
||
<i className='tabler-microphone text-xl' />
|
||
</IconButton>
|
||
<TextField
|
||
fullWidth
|
||
multiline
|
||
maxRows={4}
|
||
placeholder={t('input.placeholder')}
|
||
value={inputValue}
|
||
onChange={e => setInputValue(e.target.value)}
|
||
onKeyDown={e => {
|
||
if (e.key === 'Enter' && !e.shiftKey) {
|
||
e.preventDefault()
|
||
handleSend()
|
||
}
|
||
}}
|
||
variant='standard'
|
||
InputProps={{
|
||
disableUnderline: true,
|
||
sx: {
|
||
px: 1.5,
|
||
py: 1,
|
||
fontSize: '0.95rem'
|
||
}
|
||
}}
|
||
sx={{ flex: 1, minWidth: 0 }}
|
||
/>
|
||
<IconButton
|
||
onClick={() => handleSend()}
|
||
disabled={!inputValue.trim()}
|
||
className='shrink-0'
|
||
sx={{
|
||
background: inputValue.trim()
|
||
? 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)'
|
||
: 'action.disabledBackground',
|
||
color: inputValue.trim() ? '#ffffff' : 'action.disabled',
|
||
'&:hover': inputValue.trim()
|
||
? {
|
||
background: 'linear-gradient(135deg, #16a34a 0%, #15803d 100%)',
|
||
boxShadow: '0 4px 12px rgba(34, 197, 94, 0.4)'
|
||
}
|
||
: {},
|
||
'&.Mui-disabled': { background: 'action.disabledBackground', color: 'action.disabled' }
|
||
}}
|
||
>
|
||
<i className='tabler-send text-xl' />
|
||
</IconButton>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
// ─── Sub-components ────────────────────────────────────────────────────────
|
||
|
||
function ContextBadge({
|
||
icon,
|
||
label,
|
||
value,
|
||
colSpan = 1
|
||
}: {
|
||
icon: string
|
||
label: string
|
||
value: string
|
||
colSpan?: number
|
||
}) {
|
||
return (
|
||
<Box
|
||
className={classnames('flex items-center gap-2 px-3 py-2 rounded-xl', colSpan === 2 ? 'col-span-2' : '')}
|
||
sx={{
|
||
background: 'rgba(34, 197, 94, 0.06)',
|
||
border: '1px solid rgba(34, 197, 94, 0.12)'
|
||
}}
|
||
>
|
||
<i className={`${icon} text-lg text-[#22c55e] shrink-0`} />
|
||
<Box className='min-w-0'>
|
||
<Typography variant='caption' color='text.secondary' display='block' lineHeight={1.2}>
|
||
{label}
|
||
</Typography>
|
||
<Typography variant='body2' fontWeight={600} color='text.primary' noWrap>
|
||
{value}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
function MessageBubble({
|
||
message,
|
||
expandedExplanations,
|
||
onToggleExplanation,
|
||
t
|
||
}: {
|
||
message: FarmAIMessage
|
||
expandedExplanations: Set<string>
|
||
onToggleExplanation: (id: string) => void
|
||
t: (key: string) => string
|
||
}) {
|
||
const isUser = message.role === 'user'
|
||
|
||
if (isUser) {
|
||
return (
|
||
<Box className='flex justify-end animate-fade-in'>
|
||
<Box
|
||
className='max-w-[85%] px-4 py-2.5 rounded-2xl rounded-tr-md'
|
||
sx={{
|
||
background: 'linear-gradient(145deg, rgba(34, 197, 94, 0.15) 0%, rgba(34, 197, 94, 0.08) 100%)',
|
||
border: '1px solid rgba(34, 197, 94, 0.2)'
|
||
}}
|
||
>
|
||
<Typography variant='body2' color='text.primary'>
|
||
{message.content}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
// AI message - structured cards
|
||
return (
|
||
<Box className='flex items-start gap-3 animate-fade-in'>
|
||
<Box
|
||
className='w-9 h-9 rounded-xl flex items-center justify-center shrink-0'
|
||
sx={{
|
||
background: 'linear-gradient(145deg, #22c55e 0%, #0ea5e9 100%)'
|
||
}}
|
||
>
|
||
<i className='tabler-leaf text-lg text-white' />
|
||
</Box>
|
||
<Box className='flex-1 min-w-0 space-y-3'>
|
||
{message.sections?.map((section, idx) => (
|
||
<AISectionCard
|
||
key={`${message.id}-${idx}`}
|
||
section={section}
|
||
expandedExplanations={expandedExplanations}
|
||
onToggleExplanation={onToggleExplanation}
|
||
messageId={message.id}
|
||
idx={idx}
|
||
t={t}
|
||
/>
|
||
))}
|
||
</Box>
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
function AISectionCard({
|
||
section,
|
||
expandedExplanations,
|
||
onToggleExplanation,
|
||
messageId,
|
||
idx,
|
||
t
|
||
}: {
|
||
section: AIResponseSection
|
||
expandedExplanations: Set<string>
|
||
onToggleExplanation: (id: string) => void
|
||
messageId: string
|
||
idx: number
|
||
t: (key: string) => string
|
||
}) {
|
||
const expId = `${messageId}-exp-${idx}`
|
||
|
||
const iconMap = {
|
||
droplet: 'tabler-droplet',
|
||
leaf: 'tabler-leaf',
|
||
warning: 'tabler-alert-triangle',
|
||
fertilizer: 'tabler-atom-2',
|
||
calendar: 'tabler-calendar'
|
||
}
|
||
const iconClass = section.icon ? iconMap[section.icon] : 'tabler-leaf'
|
||
|
||
if (section.type === 'recommendation') {
|
||
return (
|
||
<Card
|
||
elevation={0}
|
||
className='overflow-hidden'
|
||
sx={{
|
||
borderRadius: '20px',
|
||
background: 'linear-gradient(160deg, #ffffff 0%, rgba(34, 197, 94, 0.06) 100%)',
|
||
border: '1px solid rgba(34, 197, 94, 0.2)',
|
||
boxShadow: '0 4px 20px rgba(34, 197, 94, 0.12)'
|
||
}}
|
||
>
|
||
<Box className='p-4'>
|
||
<Box className='flex items-center gap-2 mb-3'>
|
||
<i className={`${iconClass} text-xl text-[#22c55e]`} />
|
||
<Typography variant='subtitle2' fontWeight={700} color='primary'>
|
||
{section.title}
|
||
</Typography>
|
||
</Box>
|
||
<Box className='space-y-2'>
|
||
{section.frequency && (
|
||
<Box className='flex justify-between'>
|
||
<Typography variant='caption' color='text.secondary'>
|
||
{t('recommendation.frequency')}
|
||
</Typography>
|
||
<Typography variant='body2' fontWeight={600}>
|
||
{section.frequency}
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
{section.amount && (
|
||
<Box className='flex justify-between'>
|
||
<Typography variant='caption' color='text.secondary'>
|
||
{t('recommendation.amount')}
|
||
</Typography>
|
||
<Typography variant='body2' fontWeight={600}>
|
||
{section.amount}
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
{section.timing && (
|
||
<Box className='flex justify-between'>
|
||
<Typography variant='caption' color='text.secondary'>
|
||
{t('recommendation.timing')}
|
||
</Typography>
|
||
<Typography variant='body2' fontWeight={600}>
|
||
{section.timing}
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
{section.expandableExplanation && (
|
||
<Box className='mt-3'>
|
||
<Box
|
||
component='button'
|
||
type='button'
|
||
onClick={() => onToggleExplanation(expId)}
|
||
className='flex items-center gap-1 text-sm font-medium'
|
||
sx={{ color: '#22c55e', '&:hover': { color: '#16a34a' } }}
|
||
>
|
||
{t('recommendation.whyThis')}
|
||
<i
|
||
className={classnames('tabler-chevron-down text-base transition-transform', {
|
||
'rotate-180': expandedExplanations.has(expId)
|
||
})}
|
||
/>
|
||
</Box>
|
||
<Collapse in={expandedExplanations.has(expId)}>
|
||
<Typography variant='body2' color='text.secondary' className='mt-2'>
|
||
{section.expandableExplanation}
|
||
</Typography>
|
||
</Collapse>
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
if (section.type === 'warning') {
|
||
return (
|
||
<Box
|
||
className='p-4 rounded-2xl'
|
||
sx={{
|
||
background: 'linear-gradient(135deg, rgba(251, 191, 36, 0.15) 0%, rgba(245, 158, 11, 0.08) 100%)',
|
||
border: '1px solid rgba(251, 191, 36, 0.35)'
|
||
}}
|
||
>
|
||
<Box className='flex gap-2'>
|
||
<i className='tabler-alert-triangle text-xl text-amber-600 shrink-0 mt-0.5' />
|
||
<Box>
|
||
{section.title && (
|
||
<Typography variant='subtitle2' fontWeight={600} color='warning.dark' className='mb-1'>
|
||
{section.title}
|
||
</Typography>
|
||
)}
|
||
<Typography variant='body2' color='text.secondary'>
|
||
{section.content}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
if (section.type === 'list') {
|
||
return (
|
||
<Box
|
||
className='p-4 rounded-2xl'
|
||
sx={{
|
||
background: 'linear-gradient(145deg, #ffffff 0%, rgba(34, 197, 94, 0.04) 100%)',
|
||
border: '1px solid rgba(34, 197, 94, 0.15)'
|
||
}}
|
||
>
|
||
{section.title && (
|
||
<Box className='flex items-center gap-2 mb-2'>
|
||
<i className={`${iconClass} text-lg text-[#22c55e]`} />
|
||
<Typography variant='subtitle2' fontWeight={600} color='text.primary'>
|
||
{section.title}
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
{section.items && (
|
||
<Box component='ul' className='m-0 ps-5 space-y-1'>
|
||
{section.items.map((item, i) => (
|
||
<Typography key={i} component='li' variant='body2' color='text.secondary'>
|
||
{item}
|
||
</Typography>
|
||
))}
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
function TypingIndicator() {
|
||
return (
|
||
<Box className='flex gap-1'>
|
||
{[0, 1, 2].map(i => (
|
||
<Box
|
||
key={i}
|
||
className='w-2 h-2 rounded-full bg-[#22c55e]'
|
||
sx={{
|
||
animation: `typing-bounce 1.4s ease-in-out ${i * 0.16}s infinite both`
|
||
}}
|
||
/>
|
||
))}
|
||
</Box>
|
||
)
|
||
}
|