/* global React, Icon, API */ function initials(name) { return String(name || '?').split(' ').map(s => s[0]).slice(0, 2).join('').toUpperCase(); } function conversationDisplayName(c) { const phone = c?.number || c?.customer_phone || '-'; const name = c?.name || c?.customer_name || c?.state?.customer_name || ''; return name ? `${phone} - ${name}` : phone; } window.conversationDisplayName = conversationDisplayName; function conversationChannel(c) { const raw = c?.raw || c || {}; const state = raw.state || c?.state || {}; const phone = String(c?.number || raw.customer_phone || '').toLowerCase(); const source = String(c?.channel || raw.channel || raw.source || state.source || '').toLowerCase(); if (source === 'agent_test' || phone.startsWith('test-')) return 'agent_test'; if (['public_test', 'public', 'shared_chat', 'shared_chat_demo', 'shared_chat_contact'].includes(source) || phone.startsWith('pub-')) return 'public'; if (source === 'site' || phone.startsWith('site-')) return 'site'; return 'whatsapp'; } function conversationChannelLabel(c) { const channel = conversationChannel(c); if (channel === 'site') return 'Site'; if (channel === 'public') return 'Chat Compartilhado'; if (channel === 'agent_test') return 'Teste interno'; return 'WhatsApp'; } function studioToast(message, type = '') { if (window.STUDIO?.toast) window.STUDIO.toast(message, type); } function studioConfirm(config) { return window.STUDIO?.confirm ? window.STUDIO.confirm(config) : Promise.resolve(false); } function conversationAgentId(c) { return c?.agentId || c?.agent_id || c?.raw?.agent_id || c?.state?.agent_id || ''; } function messageDir(role) { if (role === 'assistant') return 'out'; if (role === 'human') return 'out human'; if (role === 'system' || role === 'tool') return 'system'; return 'in'; } function runtimeBadgeClass(value) { const text = String(value || '').toLowerCase(); if (text.includes('ok') || text.includes('ativo') || text.includes('done') || text.includes('resolvido') || text.includes('concluido')) return 'ok'; if (text.includes('frustr') || text.includes('handoff') || text.includes('paus') || text.includes('erro')) return 'bad'; if (text.includes('pend') || text.includes('wait') || text.includes('process') || text.includes('em andamento')) return 'warn'; return ''; } function runtimeText(value, fallback = '--') { if (value == null || value === '') return fallback; if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value); return value.summary || value.text || value.content || value.value || value.name || fallback; } function toolLine(item) { const latency = item.latency_ms ? `${item.latency_ms}ms` : '--ms'; return `${item.tool || item.name || item.status || 'tool'} - ${latency}`; } function latestConversationPreview(preview) { return String(preview || '').split('\n').filter(Boolean).pop()?.replace(/^(user|assistant|human|system):\s*/i, '') || ''; } function conversationFromApi(c) { const ticket = c.ticket ? `T-${c.ticket.id}` : null; const handoff = c.state?.human_handoff || {}; const paused = Boolean(ticket) || Boolean(handoff.paused || handoff.requested || handoff.ticket_status === 'in_progress'); const source = c.channel || c.source || c.state?.source || ''; const phone = String(c.customer_phone || ''); const channel = ['public_test', 'public', 'shared_chat', 'shared_chat_demo', 'shared_chat_contact'].includes(source) || phone.startsWith('pub-') ? 'public' : source || (phone.startsWith('site-') ? 'site' : 'whatsapp'); return { id: c.id, name: c.customer_name || c.customer_phone || 'Contato', number: c.customer_phone || '--', agent: c.agent_name || `Agente #${c.agent_id || '-'}`, agentId: c.agent_id, channel, channelLabel: conversationChannelLabel({ channel }), last: latestConversationPreview(c.preview), time: window.fmtTime ? window.fmtTime(c.updated_at || c.created_at) : '--:--', unread: 0, paused, ticket, raw: c, }; } function objectiveStatusText(status, note) { const text = String(status || '').toLowerCase(); if (text.includes('conclu') || text.includes('done') || text.includes('ok')) return 'concluido'; if (text.includes('process') || text.includes('andamento')) return 'em processo'; return note || status || 'pendente'; } window.ConversationsPage = function ConversationsPage({ agentId = null, embedded = false } = {}) { const data = window.MOCK || {}; const agents = data.agents || []; const fixedAgentId = agentId == null ? null : String(agentId); const [liveConversations, setLiveConversations] = React.useState(null); const conversations = liveConversations || data.conversations || []; const [active, setActive] = React.useState(conversations.find(c => !fixedAgentId || String(conversationAgentId(c)) === fixedAgentId)?.id || null); const [detail, setDetail] = React.useState(null); const [q, setQ] = React.useState(''); const [filter, setFilter] = React.useState('todas'); const [channelFilter, setChannelFilter] = React.useState('all'); const [agentFilter, setAgentFilter] = React.useState(fixedAgentId || 'all'); const [showThread, setShowThread] = React.useState(false); const [draft, setDraft] = React.useState(''); const [syncing, setSyncing] = React.useState(false); const [lastSync, setLastSync] = React.useState(null); const effectiveAgentFilter = fixedAgentId || agentFilter; const agentScoped = conversations.filter(c => effectiveAgentFilter === 'all' || String(conversationAgentId(c)) === String(effectiveAgentFilter)); const channelScoped = agentScoped.filter(c => channelFilter === 'all' || conversationChannel(c) === channelFilter); const selected = channelScoped.find(c => String(c.id) === String(active)) || channelScoped[0] || null; const scopedAgent = fixedAgentId ? agents.find(agent => String(agent.id) === fixedAgentId) : null; const siteCount = agentScoped.filter(c => conversationChannel(c) === 'site').length; const publicCount = agentScoped.filter(c => conversationChannel(c) === 'public').length; const whatsappCount = agentScoped.filter(c => conversationChannel(c) === 'whatsapp').length; const agentCounts = conversations.reduce((acc, c) => { const id = String(conversationAgentId(c) || 'sem-agente'); acc[id] = (acc[id] || 0) + 1; return acc; }, {}); React.useEffect(() => { if (!channelScoped.length) { setActive(null); return; } if (!active || !channelScoped.some(c => String(c.id) === String(active))) setActive(channelScoped[0].id); }, [conversations.length, effectiveAgentFilter, channelFilter]); React.useEffect(() => { if (fixedAgentId && agentFilter !== fixedAgentId) setAgentFilter(fixedAgentId); }, [fixedAgentId]); React.useEffect(() => { if (!active) { setDetail(null); return; } let alive = true; API.getConversationDetail(active).then(item => { if (alive) setDetail(item); }).catch(() => { if (alive) setDetail(null); }); return () => { alive = false; }; }, [active]); React.useEffect(() => { if (!API?.getConversations) return undefined; let alive = true; let busy = false; async function syncLive() { if (busy || document.hidden) return; busy = true; if (alive) setSyncing(true); try { const rows = await API.getConversations(fixedAgentId || null); if (alive) setLiveConversations((rows || []).map(conversationFromApi)); if (active) { const item = await API.getConversationDetail(active); if (alive) setDetail(item); } if (alive) setLastSync(new Date()); } catch (err) { // A tela segue com os dados atuais; o toast global já cobre erros de backend. } finally { busy = false; if (alive) setSyncing(false); } } const timer = setInterval(syncLive, 2500); return () => { alive = false; clearInterval(timer); }; }, [active, effectiveAgentFilter]); const list = channelScoped.filter(c => { const text = `${c.name} ${c.number} ${c.agent} ${c.last}`.toLowerCase(); const okText = text.includes(q.toLowerCase()); const okFilter = filter === 'todas' || (filter === 'pausadas' && c.paused) || (filter === 'ativas' && !c.paused); return okText && okFilter; }); const paused = channelScoped.filter(c => c.paused).length; const messages = detail?.messages?.length ? detail.messages.map(m => ({ id: m.id, dir: messageDir(m.role), text: m.content, time: window.fmtTime ? window.fmtTime(m.created_at) : '--', role: m.role, })) : []; const state = detail?.state || selected?.raw?.state || {}; const handoff = state.human_handoff || {}; const runtime = detail?.runtime || {}; const runtimeStatus = runtime.status || {}; const fallbackMemory = Object.entries(state.memory || state.context || {}).map(([key, value]) => ({ name: key, label: `@${key}`, value: runtimeText(value), filled: true })); const runtimeMemory = (runtime.memory?.entries || []).length ? runtime.memory.entries : fallbackMemory; const runtimeObjectives = runtime.objectives || []; const runtimeTools = (runtime.tools?.used || []).length ? runtime.tools.used : (data.runs || []).filter(r => String(r.conversation_id) === String(active)).slice(0, 4).map(r => ({ id: r.id, tool: r.output?.tool || r.input?.tool || r.status || 'run', latency_ms: r.latency_ms, status: r.status, output: r.output, input: r.input, error: r.error })); const runtimeConditionals = Array.isArray(runtime.conditionals?.matched) ? runtime.conditionals.matched : []; async function refreshDetail() { const rows = await API.getConversations(fixedAgentId || null); setLiveConversations((rows || []).map(conversationFromApi)); if (active) setDetail(await API.getConversationDetail(active)); } async function pauseAgent() { if (!active) return; await API.pauseAgent(active); await refreshDetail(); } async function resumeAgent() { if (!active) return; await API.resumeAgent(active); await refreshDetail(); } async function sendHumanMessage() { if (!active || !draft.trim()) return; await API.sendHumanMessage(active, draft.trim()); setDraft(''); await refreshDetail(); } async function deleteSelectedConversation() { if (!active || !(await studioConfirm({ title: 'Apagar conversa', message: 'Apagar conversa selecionada?', confirmLabel: 'Apagar', danger: true }))) return; await API.deleteConversation(active); await window.STUDIO.refresh(); setActive(null); setDetail(null); } async function deleteAgentConversations() { const agentId = conversationAgentId(selected); if (!agentId || !(await studioConfirm({ title: 'Apagar conversas do agente', message: 'Apagar todas as conversas deste agente?', confirmLabel: 'Apagar', danger: true }))) return; await API.deleteAgentConversations(agentId); await window.STUDIO.refresh(); setActive(null); setDetail(null); } return ( <> {!embedded &&
Threads reais do site, WhatsApp e chat compartilhado separadas por agente, canal, runtime, ticket e pausa humana.
Tickets reais criados por handoff humano, atualizaveis direto no backend.
| ID | Cliente | Origem | Tipo | Time | Atendente | Resumo | Tags | Status | Atualizado | |
|---|---|---|---|---|---|---|---|---|---|---|
| {t.id} | {initials(t.client)}{t.client} |
{sourceLabel(t)} | {t.requestType || '--'} | {t.team} | {t.attendant !== '--' ? t.attendant : não atribuído} | {truncateTicketText(t.summary)} | {(t.tags || []).map(g => {g})} | {t.status} | {t.updated} |
{t.status !== 'Em andamento' && }
{t.status !== 'Resolvido' && }
|
| Nenhum ticket corresponde aos filtros. | ||||||||||