Add irrigation and fertilization recommendations, farm AI assistant, and pest detection features with Persian localization
- 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.
This commit is contained in:
@@ -0,0 +1,671 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user