import React, { useState, useRef, useEffect } from 'react'; import { Ticket, KBArticle, Agent, TicketStatus, TicketPriority, SurveyResult, AppSettings, ClientUser, TicketQueue, EmailTemplate, EmailTrigger, EmailAudience, AgentAvatarConfig, AgentRole, AiProvider } from '../types'; import { generateNewKBArticle } from '../services/geminiService'; import { ToastType } from './Toast'; import { LayoutDashboard, BookOpen, Users, Sparkles, CheckCircle, Clock, Edit3, Plus, ExternalLink, FileText, BarChart3, TrendingUp, MessageCircle, Star, Settings, Trash2, Mail, Palette, Shield, Save, LogOut, Paperclip, Layers, X, Camera, Move, Check, Zap, Copy, Activity, UserPlus, Loader2, Lock, Cpu, AlertTriangle, RotateCcw, Bot, Key, Archive, Inbox, Send } from 'lucide-react'; interface AgentDashboardProps { currentUser: Agent; tickets: Ticket[]; articles: KBArticle[]; agents: Agent[]; queues: TicketQueue[]; surveys?: SurveyResult[]; clientUsers: ClientUser[]; settings: AppSettings; updateTicketStatus: (id: string, status: TicketStatus) => void; updateTicketAgent: (id: string, agentId: string) => void; onReplyTicket: (ticketId: string, message: string) => void; addArticle: (article: KBArticle) => void; updateArticle: (article: KBArticle) => void; addAgent: (agent: Agent) => void; updateAgent: (agent: Agent) => void; removeAgent: (id: string) => void; addClientUser: (user: ClientUser) => void; updateClientUser: (user: ClientUser) => void; removeClientUser: (id: string) => void; updateSettings: (settings: AppSettings) => void; addQueue: (queue: TicketQueue) => void; removeQueue: (id: string) => void; onLogout: () => void; showToast: (message: string, type: ToastType) => void; } // --- SUB-COMPONENT: Avatar Editor --- interface AvatarEditorProps { initialImage: string; initialConfig?: AgentAvatarConfig; onSave: (image: string, config: AgentAvatarConfig) => void; } const AvatarEditor: React.FC = ({ initialImage, initialConfig, onSave }) => { const [image, setImage] = useState(initialImage); const [config, setConfig] = useState(initialConfig || { x: 50, y: 50, scale: 1 }); const [isDragging, setIsDragging] = useState(false); const dragStart = useRef<{x: number, y: number}>({ x: 0, y: 0 }); // Update local state when props change (for editing) useEffect(() => { setImage(initialImage); if(initialConfig) setConfig(initialConfig); }, [initialImage, initialConfig]); const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { const url = URL.createObjectURL(e.target.files[0]); setImage(url); setConfig({ x: 50, y: 50, scale: 1 }); // Reset config for new image } }; const handleMouseDown = (e: React.MouseEvent) => { setIsDragging(true); dragStart.current = { x: e.clientX, y: e.clientY }; }; const handleMouseMove = (e: React.MouseEvent) => { if (!isDragging) return; const deltaX = (e.clientX - dragStart.current.x) * 0.5; // Sensitivity const deltaY = (e.clientY - dragStart.current.y) * 0.5; setConfig(prev => ({ ...prev, x: Math.min(100, Math.max(0, prev.x - deltaX * 0.2)), // Invert direction for natural feel or keep normal y: Math.min(100, Math.max(0, prev.y - deltaY * 0.2)) })); dragStart.current = { x: e.clientX, y: e.clientY }; }; const handleMouseUp = () => setIsDragging(false); return (
{image ? ( Avatar ) : (
)}
{image && (
Zoom setConfig({...config, scale: parseFloat(e.target.value)})} className="flex-1 h-1 bg-gray-300 rounded-lg appearance-none cursor-pointer" />
)}

Trascina l'immagine per centrarla

); }; // --- MAIN COMPONENT --- export const AgentDashboard: React.FC = ({ currentUser, tickets, articles, agents, queues, surveys = [], clientUsers, settings, updateTicketStatus, updateTicketAgent, onReplyTicket, addArticle, updateArticle, addAgent, updateAgent, removeAgent, addClientUser, updateClientUser, removeClientUser, updateSettings, addQueue, removeQueue, onLogout, showToast }) => { const [view, setView] = useState<'tickets' | 'kb' | 'ai' | 'analytics' | 'settings'>('tickets'); const [selectedTicketId, setSelectedTicketId] = useState(null); const [selectedQueue, setSelectedQueue] = useState(null); // Name of the queue const [isViewingArchive, setIsViewingArchive] = useState(false); const [replyText, setReplyText] = useState(''); // ROLE BASED PERMISSIONS const canManageGlobalSettings = currentUser.role === 'superadmin'; const canManageTeam = currentUser.role === 'superadmin' || currentUser.role === 'supervisor'; const canAccessSettings = canManageTeam || canManageGlobalSettings; // Initialize correct tab based on permissions const [settingsTab, setSettingsTab] = useState<'general' | 'system' | 'ai' | 'users' | 'agents' | 'queues' | 'email'>('users'); const [isSaving, setIsSaving] = useState(false); useEffect(() => { if (view === 'settings') { if (canManageGlobalSettings) setSettingsTab('system'); // Default to system for superadmin else if (canManageTeam) setSettingsTab('users'); } }, [view, canManageGlobalSettings, canManageTeam]); // Set default queue when entering tickets view useEffect(() => { if (view === 'tickets' && !selectedQueue && !isViewingArchive && queues.length > 0) { // Find the first queue assigned to the agent, or just the first queue const firstAssignedQueue = queues.find(q => currentUser.queues.includes(q.name)); if (firstAssignedQueue) { setSelectedQueue(firstAssignedQueue.name); } else if (queues.length > 0) { setSelectedQueue(queues[0].name); } } }, [view, queues, currentUser]); // KB Editor State const [isEditingKB, setIsEditingKB] = useState(false); const [newArticle, setNewArticle] = useState>({ type: 'article', category: 'General' }); const [isFetchingUrl, setIsFetchingUrl] = useState(false); // AI State const [isAiAnalyzing, setIsAiAnalyzing] = useState(false); const [aiSuggestions, setAiSuggestions] = useState>([]); // Forms State for Settings const [newAgentForm, setNewAgentForm] = useState>({ name: '', email: '', password: '', skills: [], queues: [], role: 'agent', avatar: '', avatarConfig: {x: 50, y: 50, scale: 1} }); const [editingAgent, setEditingAgent] = useState(null); const [newUserForm, setNewUserForm] = useState>({ name: '', email: '', status: 'active', company: '' }); const [editingUser, setEditingUser] = useState(null); const [newQueueForm, setNewQueueForm] = useState>({ name: '', description: '' }); const [tempSettings, setTempSettings] = useState(settings); // Email Template Editor State const [editingTemplate, setEditingTemplate] = useState(null); const [isTestingSmtp, setIsTestingSmtp] = useState(false); const selectedTicket = tickets.find(t => t.id === selectedTicketId); // Stats for Quotas const currentAgents = agents.filter(a => a.role === 'agent').length; const currentSupervisors = agents.filter(a => a.role === 'supervisor').length; const currentArticles = articles.length; const currentAiArticles = articles.filter(a => a.source === 'ai').length; const isKbFull = currentArticles >= settings.features.maxKbArticles; const isAgentQuotaFull = currentAgents >= settings.features.maxAgents; const isSupervisorQuotaFull = currentSupervisors >= settings.features.maxSupervisors; // Filter Agents for Assignment based on Role const getAssignableAgents = (ticketQueue: string) => { // Superadmin and Supervisor can assign to anyone if (canManageTeam) return agents; // Agents can only assign to agents in the same queue return agents.filter(a => a.queues.includes(ticketQueue)); }; // Helper function to fetch URL content via proxy const fetchUrlContent = async (url: string): Promise => { const parseContent = (html: string) => { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const scripts = doc.querySelectorAll('script, style, nav, footer, header, svg, noscript, iframe'); scripts.forEach(node => node.remove()); let text = doc.body?.textContent || ""; text = text.replace(/\s+/g, ' ').trim(); return text.substring(0, 5000); }; try { const res1 = await fetch(`https://api.allorigins.win/get?url=${encodeURIComponent(url)}`); if (res1.ok) { const data = await res1.json(); if (data.contents) return parseContent(data.contents); } } catch (e) { console.warn("Primary scraping failed, trying fallback..."); } try { const res2 = await fetch(`https://corsproxy.io/?${encodeURIComponent(url)}`); if (res2.ok) { const html = await res2.text(); return parseContent(html); } } catch (e) { console.error("Scraping error:", e); } return ""; }; // Handlers const handleReplySubmit = () => { if (selectedTicketId && replyText.trim()) { onReplyTicket(selectedTicketId, replyText); setReplyText(''); showToast('Risposta inviata', 'success'); } }; const handleAiAnalysis = async () => { if (!settings.features.aiKnowledgeAgentEnabled) { showToast("L'Agente Knowledge AI è disabilitato dall'amministratore.", 'error'); return; } if (currentAiArticles >= settings.features.maxAiGeneratedArticles) { showToast("Quota creazione articoli AI raggiunta. Impossibile generare nuovi suggerimenti.", 'error'); return; } if (!settings.aiConfig.apiKey) { showToast("Chiave API mancante. Configura l'AI nelle impostazioni.", 'error'); return; } setIsAiAnalyzing(true); setAiSuggestions([]); const suggestions = await generateNewKBArticle( settings.aiConfig.apiKey, tickets, articles, settings.aiConfig.provider, settings.aiConfig.model ); if (suggestions) { setAiSuggestions(suggestions); } else { showToast("Nessuna lacuna identificata dall'AI.", 'info'); } setIsAiAnalyzing(false); }; const saveAiArticle = (suggestion: { title: string; content: string; category: string }, index: number) => { addArticle({ id: `kb-${Date.now()}-${index}`, title: suggestion.title, content: suggestion.content, category: suggestion.category, type: 'article', source: 'ai', lastUpdated: new Date().toISOString().split('T')[0] }); setAiSuggestions(prev => prev.filter((_, i) => i !== index)); showToast("Articolo aggiunto alla KB", 'success'); }; const discardAiArticle = (index: number) => { setAiSuggestions(prev => prev.filter((_, i) => i !== index)); }; const handleSaveArticle = async () => { if (newArticle.title && (newArticle.content || newArticle.url)) { let finalContent = newArticle.content || ''; if (newArticle.type === 'url' && newArticle.url) { setIsFetchingUrl(true); const scrapedText = await fetchUrlContent(newArticle.url); setIsFetchingUrl(false); if (scrapedText) { finalContent = `[CONTENUTO PAGINA WEB SCARICATO]\n\n${scrapedText}\n\n[NOTE MANUALI]\n${newArticle.content || ''}`; } else { finalContent = newArticle.content || 'Contenuto non recuperabile automaticamente. Fare riferimento al link.'; } } const articleToSave: KBArticle = { id: newArticle.id || `kb-${Date.now()}`, title: newArticle.title, content: finalContent, category: newArticle.category || 'General', type: newArticle.type || 'article', url: newArticle.url, source: newArticle.source || 'manual', lastUpdated: new Date().toISOString().split('T')[0] }; if (newArticle.id) { updateArticle(articleToSave); } else { addArticle(articleToSave); } setIsEditingKB(false); setNewArticle({ type: 'article', category: 'General' }); } }; const handleAvatarSaved = (image: string, config: AgentAvatarConfig, isEditing: boolean) => { if (isEditing && editingAgent) { setEditingAgent({ ...editingAgent, avatar: image, avatarConfig: config }); } else { setNewAgentForm({ ...newAgentForm, avatar: image, avatarConfig: config }); } }; const handleAddAgent = () => { if(newAgentForm.name && newAgentForm.email && newAgentForm.queues && newAgentForm.queues.length > 0) { addAgent({ id: `a${Date.now()}`, name: newAgentForm.name, email: newAgentForm.email, password: newAgentForm.password || 'password', role: newAgentForm.role || 'agent', avatar: newAgentForm.avatar || 'https://via.placeholder.com/200', avatarConfig: newAgentForm.avatarConfig, skills: newAgentForm.skills || ['General'], queues: newAgentForm.queues } as Agent); setNewAgentForm({ name: '', email: '', password: '', role: 'agent', skills: [], queues: [], avatar: '', avatarConfig: {x: 50, y: 50, scale: 1} }); } else { showToast("Compila nome, email e seleziona almeno una coda.", 'error'); } }; const handleUpdateAgent = () => { if (editingAgent && newAgentForm.name && newAgentForm.email) { const updatedAgent: Agent = { ...editingAgent, name: newAgentForm.name!, email: newAgentForm.email!, password: newAgentForm.password || editingAgent.password, role: newAgentForm.role as AgentRole, avatar: newAgentForm.avatar || editingAgent.avatar, avatarConfig: newAgentForm.avatarConfig, queues: newAgentForm.queues || [], skills: newAgentForm.skills || [] }; updateAgent(updatedAgent); setEditingAgent(null); setNewAgentForm({ name: '', email: '', password: '', role: 'agent', skills: [], queues: [], avatar: '', avatarConfig: {x: 50, y: 50, scale: 1} }); } }; const handleEditAgentClick = (agent: Agent) => { setEditingAgent(agent); setNewAgentForm({ name: agent.name, email: agent.email, password: agent.password, role: agent.role, avatar: agent.avatar, avatarConfig: agent.avatarConfig, skills: agent.skills, queues: agent.queues }); }; const cancelEditAgent = () => { setEditingAgent(null); setNewAgentForm({ name: '', email: '', password: '', role: 'agent', skills: [], queues: [], avatar: '', avatarConfig: {x: 50, y: 50, scale: 1} }); }; const toggleQueueInForm = (queueName: string, isEditing: boolean) => { const currentQueues = newAgentForm.queues || []; const newQueues = currentQueues.includes(queueName) ? currentQueues.filter(q => q !== queueName) : [...currentQueues, queueName]; setNewAgentForm({ ...newAgentForm, queues: newQueues }); }; // --- USER MANAGEMENT HANDLERS --- const handleAddUser = () => { if(newUserForm.name && newUserForm.email) { addClientUser({ id: `u${Date.now()}`, name: newUserForm.name, email: newUserForm.email, company: newUserForm.company, status: newUserForm.status || 'active', password: 'user' // Default password } as ClientUser); setNewUserForm({ name: '', email: '', status: 'active', company: '' }); } }; const handleUpdateUser = () => { if (editingUser && newUserForm.name && newUserForm.email) { updateClientUser({ ...editingUser, name: newUserForm.name, email: newUserForm.email, company: newUserForm.company, status: newUserForm.status as 'active' | 'inactive' }); setEditingUser(null); setNewUserForm({ name: '', email: '', status: 'active', company: '' }); } }; const handleEditUserClick = (user: ClientUser) => { setEditingUser(user); setNewUserForm({ name: user.name, email: user.email, company: user.company, status: user.status }); }; const cancelEditUser = () => { setEditingUser(null); setNewUserForm({ name: '', email: '', status: 'active', company: '' }); }; const handleSendPasswordReset = (email: string) => { // Simulate sending email showToast(`Link di reset password inviato a ${email}`, 'success'); }; const handleAddQueue = () => { if (newQueueForm.name) { addQueue({ id: `q-${Date.now()}`, name: newQueueForm.name, description: newQueueForm.description } as TicketQueue); setNewQueueForm({ name: '', description: '' }); } }; // Email & SMTP Handlers const handleTestSmtp = () => { setIsTestingSmtp(true); setTimeout(() => { setIsTestingSmtp(false); showToast(`Test Connessione SMTP Riuscito! Host: ${tempSettings.smtp.host}`, 'success'); }, 1500); }; const handleSaveTemplate = () => { if (editingTemplate) { let updatedTemplates = [...tempSettings.emailTemplates]; if (editingTemplate.id === 'new') { updatedTemplates.push({ ...editingTemplate, id: `t${Date.now()}` }); } else { updatedTemplates = updatedTemplates.map(t => t.id === editingTemplate.id ? editingTemplate : t); } setTempSettings({ ...tempSettings, emailTemplates: updatedTemplates }); setEditingTemplate(null); } }; const handleDeleteTemplate = (id: string) => { setTempSettings({ ...tempSettings, emailTemplates: tempSettings.emailTemplates.filter(t => t.id !== id) }); }; const handleSaveSettings = () => { setIsSaving(true); // Simulate API call using prop updateSettings(tempSettings); setTimeout(() => { setIsSaving(false); }, 800); }; // Filter Logic for Ticket View const getFilteredTickets = () => { if (isViewingArchive) { return tickets.filter(t => t.status === TicketStatus.RESOLVED || t.status === TicketStatus.CLOSED); } // Viewing a specific queue (Open tickets only) if (selectedQueue) { return tickets.filter(t => t.queue === selectedQueue && (t.status === TicketStatus.OPEN || t.status === TicketStatus.IN_PROGRESS) ); } return []; }; const filteredTickets = getFilteredTickets(); const queueCounts: Record = {}; // Calculate open tickets per queue for the sidebar queues.forEach(q => { queueCounts[q.name] = tickets.filter(t => t.queue === q.name && (t.status === TicketStatus.OPEN || t.status === TicketStatus.IN_PROGRESS) ).length; }); // ANALYTICS & STATS CALCULATION const totalTickets = tickets.length; const resolvedCount = tickets.filter(t => t.status === TicketStatus.RESOLVED || t.status === TicketStatus.CLOSED).length; const resolutionRate = totalTickets > 0 ? Math.round((resolvedCount / totalTickets) * 100) : 0; const validSurveys = surveys || []; const ratedSurveys = validSurveys.filter(s => s.rating > 0); const avgRating = ratedSurveys.length > 0 ? (ratedSurveys.reduce((acc, curr) => acc + curr.rating, 0) / ratedSurveys.length).toFixed(1) : '0.0'; let maxQueue = 0; queues.forEach(q => { const count = tickets.filter(t => t.queue === q.name).length; if (count > maxQueue) maxQueue = count; }); return (
{/* Sidebar */}

{settings.branding.appName}

{currentUser.role === 'superadmin' ? 'Super Admin' : currentUser.role === 'supervisor' ? 'Supervisor Workspace' : 'Agent Workspace'}

{currentUser.name}

{currentUser.name}

{currentUser.queues.join(', ')}

{/* Main Content */}
{/* SETTINGS VIEW - PERMISSION GATED */} {view === 'settings' && canAccessSettings && (
{/* Sidebar Settings Tabs */}

Impostazioni

{/* SYSTEM SETTINGS TAB */} {settingsTab === 'system' && canManageGlobalSettings && (

Limiti e Quote di Sistema

setTempSettings({...tempSettings, features: {...tempSettings.features, maxKbArticles: parseInt(e.target.value)}})} />
setTempSettings({...tempSettings, features: {...tempSettings.features, maxAgents: parseInt(e.target.value)}})} />
setTempSettings({...tempSettings, features: {...tempSettings.features, maxSupervisors: parseInt(e.target.value)}})} />
setTempSettings({...tempSettings, features: {...tempSettings.features, maxAiGeneratedArticles: parseInt(e.target.value)}})} />
setTempSettings({...tempSettings, features: {...tempSettings.features, kbEnabled: e.target.checked}})} />
setTempSettings({...tempSettings, features: {...tempSettings.features, aiKnowledgeAgentEnabled: e.target.checked}})} />
)} {/* GENERAL (BRANDING) TAB */} {settingsTab === 'general' && canManageGlobalSettings && (

Personalizzazione Branding

setTempSettings({...tempSettings, branding: {...tempSettings.branding, appName: e.target.value}})} />
setTempSettings({...tempSettings, branding: {...tempSettings.branding, primaryColor: e.target.value}})} /> setTempSettings({...tempSettings, branding: {...tempSettings.branding, primaryColor: e.target.value}})} />
setTempSettings({...tempSettings, branding: {...tempSettings.branding, logoUrl: e.target.value}})} />
)} {/* AI CONFIG TAB */} {settingsTab === 'ai' && canManageTeam && (

Integrazione AI

Configurazione Provider AI

Scegli il motore di intelligenza artificiale per l'assistente chat e l'analisi della KB.

setTempSettings({...tempSettings, aiConfig: {...tempSettings.aiConfig, model: e.target.value}})} />
setTempSettings({...tempSettings, aiConfig: {...tempSettings.aiConfig, baseUrl: e.target.value}})} />
{/* API KEY FIELD */}
setTempSettings({...tempSettings, aiConfig: {...tempSettings.aiConfig, apiKey: e.target.value}})} />

La chiave non verrà mostrata in chiaro dopo il salvataggio.

)} {/* USERS TAB */} {settingsTab === 'users' && canManageTeam && (

Gestione Utenti Frontend

{editingUser ? 'Modifica Utente' : 'Aggiungi Nuovo Utente'}

setNewUserForm({...newUserForm, name: e.target.value})} /> setNewUserForm({...newUserForm, email: e.target.value})} /> setNewUserForm({...newUserForm, company: e.target.value})} />
{editingUser && }
{clientUsers.map(user => ( ))}
NomeEmailAziendaStatoAzioni
{user.name} {user.email} {user.company || '-'} {user.status}
)} {/* AGENTS TAB */} {settingsTab === 'agents' && canManageTeam && (

Team di Supporto

{isAgentQuotaFull ? Quota Agenti Raggiunta : ( )}

{editingAgent ? 'Modifica Profilo Agente' : 'Nuovo Membro del Team'}

handleAvatarSaved(img, cfg, !!editingAgent)} />
setNewAgentForm({...newAgentForm, name: e.target.value})} /> setNewAgentForm({...newAgentForm, email: e.target.value})} />
setNewAgentForm({...newAgentForm, password: e.target.value})} />
{queues.map(q => ( ))}
{editingAgent && }
{agents.map(agent => (
{agent.name}

{agent.name}

{agent.role}

{agent.email}

{agent.queues.map(q => {q})}
{agent.role !== 'superadmin' && }
))}
)} {/* QUEUES TAB */} {settingsTab === 'queues' && canManageTeam && (

Code di Smistamento

setNewQueueForm({...newQueueForm, name: e.target.value})} />
setNewQueueForm({...newQueueForm, description: e.target.value})} />
{queues.map(q => (

{q.name}

{q.description}

))}
)} {settingsTab === 'email' && canManageGlobalSettings && (

Configurazione SMTP

setTempSettings({...tempSettings, smtp: {...tempSettings.smtp, host: e.target.value}})} />
setTempSettings({...tempSettings, smtp: {...tempSettings.smtp, port: parseInt(e.target.value)}})} />
setTempSettings({...tempSettings, smtp: {...tempSettings.smtp, user: e.target.value}})} />
setTempSettings({...tempSettings, smtp: {...tempSettings.smtp, pass: e.target.value}})} />
)} {settingsTab !== 'users' && settingsTab !== 'agents' && settingsTab !== 'queues' && (
)}
)} {view === 'tickets' && (
{/* COLUMN 1: QUEUES & ARCHIVE */}

Code Attive

    {queues.map(q => { const count = queueCounts[q.name] || 0; const isActive = selectedQueue === q.name && !isViewingArchive; return (
  • ); })}
Storico
{/* COLUMN 2: TICKET LIST */}

{isViewingArchive ? 'Archivio Ticket' : selectedQueue ? selectedQueue : 'Seleziona Coda'}

{filteredTickets.length} ticket
{filteredTickets.length === 0 ? (
{isViewingArchive ? : }

Nessun ticket {isViewingArchive ? 'in archivio' : 'in questa coda'}.

) : ( filteredTickets.map(ticket => (
setSelectedTicketId(ticket.id)} className={`p-4 border-b border-gray-100 cursor-pointer hover:bg-blue-50 transition relative group ${selectedTicketId === ticket.id ? 'bg-blue-50 border-l-4 border-brand-500' : ''}`} >
{/* Agent Quick Assign - Only for Active Tickets */} {!isViewingArchive && (
e.stopPropagation()}>
)} {/* Status Quick Change */}
e.stopPropagation()}>
{ticket.id} {ticket.priority}

{ticket.subject}

{ticket.customerName} • {ticket.status}

{ticket.attachments && ticket.attachments.length > 0 && (
{ticket.attachments.length}
)} {ticket.createdAt.split('T')[0]}
)) )}
{/* COLUMN 3: DETAIL */}
{selectedTicket ? (

{selectedTicket.subject}

{selectedTicket.customerName} {selectedTicket.createdAt.split('T')[0]} {selectedTicket.queue}

{selectedTicket.description}

{/* Attachments Section */} {selectedTicket.attachments && selectedTicket.attachments.length > 0 && (

Allegati

{selectedTicket.attachments.map(att => (
{att.name}
))}
)}

Cronologia Messaggi

{selectedTicket.messages.length === 0 ? (

Nessun messaggio.

) : ( selectedTicket.messages.map(m => (

{m.role === 'assistant' ? 'Agente' : 'Cliente'} {m.timestamp.split('T')[1].substring(0,5)}

{m.content}

)) )}