1584 lines
72 KiB
TypeScript
1584 lines
72 KiB
TypeScript
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||
import { motion, AnimatePresence } from 'framer-motion'
|
||
import { format, parseISO } from 'date-fns'
|
||
import { ptBR } from 'date-fns/locale'
|
||
import {
|
||
MessageSquare, Brain, CalendarDays, Bot, Plus, Trash2, Pencil,
|
||
ChevronDown, Check, Send, X, Loader2, ToggleLeft, ToggleRight,
|
||
Sparkles, BookOpen, ShieldCheck, Calendar, AlertTriangle,
|
||
CheckCircle, Clock, User, RefreshCw, MoreVertical, Power, Cpu,
|
||
Phone, Shield, Star, Repeat, UserCheck, Building2, Stethoscope,
|
||
Maximize2, Save,
|
||
} from 'lucide-react'
|
||
import { useSecretariaStore } from '../../store/secretariaStore'
|
||
import type { SecAgent, BrainNode, SecConversation, BrainNodeType, CalendarSlot, SecNumber, NumberRole } from '../../services/secretariaApi'
|
||
|
||
// ─── Thin Nav ─────────────────────────────────────────────────────────────────
|
||
|
||
type NavTab = 'conversations' | 'brain' | 'calendar' | 'agents' | 'numbers'
|
||
|
||
const NAV_ITEMS: { id: NavTab; icon: React.ElementType; label: string }[] = [
|
||
{ id: 'conversations', icon: MessageSquare, label: 'Conversas' },
|
||
{ id: 'brain', icon: Brain, label: 'Cérebro' },
|
||
{ id: 'calendar', icon: CalendarDays, label: 'Agenda' },
|
||
{ id: 'agents', icon: Bot, label: 'Agentes' },
|
||
{ id: 'numbers', icon: Phone, label: 'Números' },
|
||
]
|
||
|
||
function ThinNav({ active, onChange }: { active: NavTab; onChange: (t: NavTab) => void }) {
|
||
return (
|
||
<div className="w-[60px] h-full bg-[#0c0e14] border-r border-white/5 flex flex-col items-center py-4 shrink-0 z-30 gap-1">
|
||
{NAV_ITEMS.map(({ id, icon: Icon, label }) => (
|
||
<button
|
||
key={id}
|
||
title={label}
|
||
onClick={() => onChange(id)}
|
||
className={`w-10 h-10 rounded-xl flex items-center justify-center transition-all ${
|
||
active === id
|
||
? 'bg-brand-500/20 text-brand-400'
|
||
: 'text-slate-600 hover:text-slate-400 hover:bg-white/5'
|
||
}`}
|
||
>
|
||
<Icon size={20} />
|
||
</button>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Agent Dropdown ───────────────────────────────────────────────────────────
|
||
|
||
function AgentDropdown({
|
||
agents, selected, onChange,
|
||
}: { agents: SecAgent[]; selected: SecAgent | null; onChange: (a: SecAgent) => void }) {
|
||
const [open, setOpen] = useState(false)
|
||
const ref = useRef<HTMLDivElement>(null)
|
||
|
||
useEffect(() => {
|
||
const h = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false) }
|
||
document.addEventListener('mousedown', h)
|
||
return () => document.removeEventListener('mousedown', h)
|
||
}, [])
|
||
|
||
return (
|
||
<div className="relative" ref={ref}>
|
||
<button
|
||
onClick={() => setOpen((o) => !o)}
|
||
className="w-full flex items-center gap-2 px-3 py-2.5 text-left hover:bg-white/[0.04] transition-colors"
|
||
>
|
||
<div className="w-7 h-7 rounded-lg bg-brand-500/20 flex items-center justify-center shrink-0">
|
||
<Bot size={14} className="text-brand-400" />
|
||
</div>
|
||
<span className="flex-1 text-sm font-semibold text-white truncate">
|
||
{selected?.name ?? 'Selecionar agente'}
|
||
</span>
|
||
<ChevronDown size={14} className={`text-slate-500 shrink-0 transition-transform ${open ? 'rotate-180' : ''}`} />
|
||
</button>
|
||
|
||
<AnimatePresence>
|
||
{open && (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -6 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -6 }}
|
||
transition={{ duration: 0.12 }}
|
||
className="absolute left-2 right-2 top-full mt-1 bg-[#12151e] border border-white/10 rounded-xl shadow-2xl z-50 overflow-hidden"
|
||
>
|
||
{agents.map((a) => (
|
||
<button key={a.id} onClick={() => { onChange(a); setOpen(false) }}
|
||
className={`w-full flex items-center gap-2 px-3 py-2.5 text-sm transition-colors text-left ${
|
||
selected?.id === a.id ? 'bg-brand-500/10 text-brand-300' : 'text-slate-300 hover:bg-white/5'
|
||
}`}
|
||
>
|
||
<Bot size={14} className={selected?.id === a.id ? 'text-brand-400' : 'text-slate-500'} />
|
||
<span className="flex-1 truncate">{a.name}</span>
|
||
{selected?.id === a.id && <Check size={13} className="text-brand-400" />}
|
||
</button>
|
||
))}
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Conversation List ────────────────────────────────────────────────────────
|
||
|
||
const STATUS_META = {
|
||
active: { label: 'Ativa', color: 'text-green-400', dot: 'bg-green-400' },
|
||
closed: { label: 'Encerrada', color: 'text-slate-500', dot: 'bg-slate-600' },
|
||
escalated: { label: 'Escalada', color: 'text-yellow-400', dot: 'bg-yellow-400' },
|
||
}
|
||
|
||
function ConversationList({
|
||
conversations, selected, onSelect, onCreate, onDelete, loadingConversations, selectedAgent,
|
||
}: {
|
||
conversations: SecConversation[]
|
||
selected: SecConversation | null
|
||
onSelect: (c: SecConversation) => void
|
||
onCreate: () => void
|
||
onDelete: (id: string) => void
|
||
loadingConversations: boolean
|
||
selectedAgent: SecAgent | null
|
||
}) {
|
||
return (
|
||
<div className="flex-1 overflow-y-auto">
|
||
<div className="px-3 py-2 flex items-center justify-between">
|
||
<span className="text-[10px] font-black text-slate-600 uppercase tracking-widest">
|
||
Sessões de Teste
|
||
</span>
|
||
<button
|
||
onClick={onCreate}
|
||
disabled={!selectedAgent}
|
||
title="Nova sessão de teste"
|
||
className="w-6 h-6 rounded-lg bg-white/5 hover:bg-brand-500/20 text-slate-500 hover:text-brand-400 flex items-center justify-center transition-all disabled:opacity-30"
|
||
>
|
||
<Plus size={13} />
|
||
</button>
|
||
</div>
|
||
|
||
{loadingConversations ? (
|
||
<div className="flex items-center justify-center py-10">
|
||
<Loader2 size={20} className="animate-spin text-slate-600" />
|
||
</div>
|
||
) : conversations.length === 0 ? (
|
||
<p className="text-xs text-slate-700 text-center py-8 px-4">
|
||
{selectedAgent ? 'Nenhuma sessão. Clique em + para iniciar.' : 'Selecione um agente.'}
|
||
</p>
|
||
) : (
|
||
<div className="space-y-px px-2">
|
||
{conversations.map((c) => {
|
||
const m = STATUS_META[c.status]
|
||
const isActive = c.id === selected?.id
|
||
return (
|
||
<div key={c.id}
|
||
className={`group flex items-center gap-2 px-2 py-3 rounded-xl cursor-pointer transition-all relative ${
|
||
isActive ? 'bg-brand-500/10 border border-brand-500/15' : 'hover:bg-white/[0.03]'
|
||
}`}
|
||
onClick={() => onSelect(c)}
|
||
>
|
||
<div className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center shrink-0">
|
||
<User size={14} className="text-slate-500" />
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
<p className={`text-sm font-semibold truncate ${isActive ? 'text-brand-300' : 'text-slate-300'}`}>
|
||
{c.contact_name}
|
||
</p>
|
||
<div className="flex items-center gap-1.5 mt-0.5">
|
||
<span className={`w-1.5 h-1.5 rounded-full ${m.dot}`} />
|
||
<span className={`text-[10px] font-medium ${m.color}`}>{m.label}</span>
|
||
<span className="text-[10px] text-slate-700 ml-1">
|
||
{format(parseISO(c.updated_at), "dd/MM HH:mm", { locale: ptBR })}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); onDelete(c.id) }}
|
||
className="opacity-0 group-hover:opacity-100 w-5 h-5 rounded flex items-center justify-center text-slate-600 hover:text-red-400 transition-all"
|
||
>
|
||
<Trash2 size={11} />
|
||
</button>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Modelos por provider ─────────────────────────────────────────────────────
|
||
|
||
const MODELS_BY_PROVIDER: Record<string, { label: string; value: string }[]> = {
|
||
openai: [
|
||
{ label: 'GPT-4o', value: 'gpt-4o' },
|
||
{ label: 'GPT-4o Mini', value: 'gpt-4o-mini' },
|
||
{ label: 'GPT-3.5 Turbo', value: 'gpt-3.5-turbo' },
|
||
],
|
||
anthropic: [
|
||
{ label: 'Claude Sonnet 4.5', value: 'claude-sonnet-4-5' },
|
||
{ label: 'Claude 3.5 Haiku', value: 'claude-3-5-haiku-20241022' },
|
||
{ label: 'Claude 3.5 Sonnet', value: 'claude-3-5-sonnet-20241022' },
|
||
],
|
||
gemini: [
|
||
{ label: 'Gemini 2.0 Flash', value: 'gemini-2.0-flash' },
|
||
{ label: 'Gemini 2.0 Flash Lite', value: 'gemini-2.0-flash-lite' },
|
||
{ label: 'Gemini 2.5 Pro', value: 'gemini-2.5-pro-preview-03-25' },
|
||
{ label: 'Gemini 1.5 Pro', value: 'gemini-1.5-pro-latest' },
|
||
],
|
||
ollama: [
|
||
{ label: 'Llama 3.1', value: 'llama3.1' },
|
||
{ label: 'Mistral', value: 'mistral' },
|
||
{ label: 'Gemma 2', value: 'gemma2' },
|
||
{ label: 'Qwen 2.5', value: 'qwen2.5' },
|
||
],
|
||
}
|
||
|
||
const ALL_MODELS = Object.values(MODELS_BY_PROVIDER).flat()
|
||
|
||
function shortModelLabel(model: string | null): string {
|
||
if (!model) return ''
|
||
const found = ALL_MODELS.find(m => m.value === model)
|
||
if (found) return found.label.split(' ').slice(0, 3).join(' ')
|
||
// fallback: última parte do nome
|
||
return model.split('-').slice(-2).join('-')
|
||
}
|
||
|
||
// ─── Brain Nodes Panel ────────────────────────────────────────────────────────
|
||
|
||
const NODE_META: Record<BrainNodeType, { label: string; icon: React.ElementType; color: string; bg: string }> = {
|
||
persona: { label: 'Persona', icon: Sparkles, color: 'text-purple-400', bg: 'bg-purple-500/10' },
|
||
knowledge: { label: 'Conhecimento', icon: BookOpen, color: 'text-blue-400', bg: 'bg-blue-500/10' },
|
||
rules: { label: 'Regras', icon: ShieldCheck, color: 'text-orange-400', bg: 'bg-orange-500/10' },
|
||
calendar: { label: 'Agenda', icon: Calendar, color: 'text-green-400', bg: 'bg-green-500/10' },
|
||
escalation: { label: 'Escalada', icon: AlertTriangle, color: 'text-red-400', bg: 'bg-red-500/10' },
|
||
}
|
||
|
||
function BrainPanel({
|
||
nodes, loadingNodes, selectedAgent,
|
||
onAdd, onUpdate, onDelete, onOpen, openNodeId,
|
||
}: {
|
||
nodes: BrainNode[]
|
||
loadingNodes: boolean
|
||
selectedAgent: SecAgent | null
|
||
onAdd: (type: BrainNodeType) => void
|
||
onUpdate: (id: string, d: Partial<BrainNode>) => void
|
||
onDelete: (id: string) => void
|
||
onOpen: (node: BrainNode) => void
|
||
openNodeId: string | null
|
||
}) {
|
||
const [editingId, setEditingId] = useState<string | null>(null)
|
||
const [editContent, setEditContent] = useState('')
|
||
const [editTitle, setEditTitle] = useState('')
|
||
const [showAddMenu, setShowAddMenu] = useState(false)
|
||
const [modelPickerFor, setModelPickerFor] = useState<string | null>(null)
|
||
|
||
const startEdit = (node: BrainNode) => {
|
||
setEditingId(node.id)
|
||
setEditTitle(node.title)
|
||
setEditContent(node.content)
|
||
}
|
||
|
||
const saveEdit = (node: BrainNode) => {
|
||
onUpdate(node.id, { title: editTitle, content: editContent })
|
||
setEditingId(null)
|
||
}
|
||
|
||
// Modelos disponíveis baseados no provider do agente selecionado
|
||
const providerModels = selectedAgent
|
||
? (MODELS_BY_PROVIDER[selectedAgent.provider] ?? ALL_MODELS)
|
||
: ALL_MODELS
|
||
|
||
return (
|
||
<div className="flex-1 overflow-y-auto">
|
||
<div className="px-3 py-2 flex items-center justify-between">
|
||
<span className="text-[10px] font-black text-slate-600 uppercase tracking-widest">
|
||
Nós do Cérebro
|
||
</span>
|
||
<div className="relative">
|
||
<button
|
||
onClick={() => setShowAddMenu((o) => !o)}
|
||
disabled={!selectedAgent}
|
||
title="Adicionar nó"
|
||
className="w-6 h-6 rounded-lg bg-white/5 hover:bg-brand-500/20 text-slate-500 hover:text-brand-400 flex items-center justify-center transition-all disabled:opacity-30"
|
||
>
|
||
<Plus size={13} />
|
||
</button>
|
||
<AnimatePresence>
|
||
{showAddMenu && (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -4 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -4 }}
|
||
className="absolute right-0 top-full mt-1 bg-[#12151e] border border-white/10 rounded-xl shadow-2xl z-50 overflow-hidden w-40"
|
||
>
|
||
{(Object.keys(NODE_META) as BrainNodeType[]).map((type) => {
|
||
const m = NODE_META[type]
|
||
const Icon = m.icon
|
||
return (
|
||
<button key={type} onClick={() => { onAdd(type); setShowAddMenu(false) }}
|
||
className="w-full flex items-center gap-2 px-3 py-2 text-xs text-slate-300 hover:bg-white/5 transition-colors"
|
||
>
|
||
<Icon size={13} className={m.color} />
|
||
{m.label}
|
||
</button>
|
||
)
|
||
})}
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
</div>
|
||
|
||
{loadingNodes ? (
|
||
<div className="flex items-center justify-center py-10">
|
||
<Loader2 size={20} className="animate-spin text-slate-600" />
|
||
</div>
|
||
) : nodes.length === 0 ? (
|
||
<p className="text-xs text-slate-700 text-center py-8 px-4">
|
||
{selectedAgent ? 'Nenhum nó. Clique em + para adicionar.' : 'Selecione um agente.'}
|
||
</p>
|
||
) : (
|
||
<div className="space-y-2 px-2 pb-4">
|
||
{nodes.map((node) => {
|
||
const m = NODE_META[node.type as BrainNodeType] ?? NODE_META.knowledge
|
||
const Icon = m.icon
|
||
const isEditing = editingId === node.id
|
||
const isPickingModel = modelPickerFor === node.id
|
||
const hasModel = !!node.node_model
|
||
const isOpen = openNodeId === node.id
|
||
|
||
return (
|
||
<div key={node.id}
|
||
className={`rounded-xl border transition-all ${
|
||
isOpen
|
||
? 'border-brand-500/40 bg-brand-500/5'
|
||
: node.active
|
||
? 'border-white/8 bg-white/[0.02]'
|
||
: 'border-white/4 bg-white/[0.01] opacity-50'
|
||
}`}
|
||
>
|
||
{/* Node Header */}
|
||
<div className="flex items-center gap-2 px-3 py-2.5">
|
||
<div className={`w-6 h-6 rounded-lg flex items-center justify-center shrink-0 ${m.bg}`}>
|
||
<Icon size={12} className={m.color} />
|
||
</div>
|
||
<div className="flex-1 min-w-0">
|
||
{isEditing ? (
|
||
<input
|
||
value={editTitle}
|
||
onChange={(e) => setEditTitle(e.target.value)}
|
||
className="w-full text-xs font-bold bg-transparent text-white border-b border-white/20 focus:outline-none"
|
||
/>
|
||
) : (
|
||
<p className="text-xs font-bold text-slate-300 truncate">{node.title}</p>
|
||
)}
|
||
<p className={`text-[10px] font-medium ${m.color}`}>{m.label}</p>
|
||
</div>
|
||
<div className="flex items-center gap-1">
|
||
{/* Abrir no editor principal */}
|
||
<button
|
||
onClick={() => onOpen(node)}
|
||
title="Abrir editor"
|
||
className={`w-5 h-5 flex items-center justify-center transition-colors ${
|
||
isOpen ? 'text-brand-400' : 'text-slate-600 hover:text-brand-400'
|
||
}`}
|
||
>
|
||
<Maximize2 size={11} />
|
||
</button>
|
||
{/* Toggle active */}
|
||
<button onClick={() => onUpdate(node.id, { active: !node.active })}
|
||
className="w-5 h-5 flex items-center justify-center text-slate-600 hover:text-slate-400 transition-colors"
|
||
>
|
||
{node.active
|
||
? <ToggleRight size={16} className="text-brand-400" />
|
||
: <ToggleLeft size={16} />}
|
||
</button>
|
||
{isEditing ? (
|
||
<>
|
||
<button onClick={() => saveEdit(node)} className="w-5 h-5 flex items-center justify-center text-green-400 hover:text-green-300">
|
||
<Check size={12} />
|
||
</button>
|
||
<button onClick={() => setEditingId(null)} className="w-5 h-5 flex items-center justify-center text-slate-600 hover:text-slate-400">
|
||
<X size={12} />
|
||
</button>
|
||
</>
|
||
) : (
|
||
<>
|
||
<button onClick={() => startEdit(node)} className="w-5 h-5 flex items-center justify-center text-slate-600 hover:text-slate-400 transition-colors">
|
||
<Pencil size={11} />
|
||
</button>
|
||
<button onClick={() => onDelete(node.id)} className="w-5 h-5 flex items-center justify-center text-slate-700 hover:text-red-400 transition-colors">
|
||
<Trash2 size={11} />
|
||
</button>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Model picker row — apenas para nó persona (sobrepõe o modelo do agente) */}
|
||
{node.type === 'persona' && (
|
||
<div className="px-3 pb-2 relative">
|
||
<button
|
||
onClick={() => setModelPickerFor(isPickingModel ? null : node.id)}
|
||
className={`flex items-center gap-1.5 px-2 py-1 rounded-lg text-[10px] font-semibold transition-all ${
|
||
hasModel
|
||
? 'bg-brand-500/15 text-brand-300 border border-brand-500/20'
|
||
: 'bg-white/5 text-slate-600 border border-white/5 hover:text-slate-400'
|
||
}`}
|
||
>
|
||
<Cpu size={10} />
|
||
{hasModel ? shortModelLabel(node.node_model) : 'modelo padrão do agente'}
|
||
<ChevronDown size={9} className={`transition-transform ${isPickingModel ? 'rotate-180' : ''}`} />
|
||
</button>
|
||
|
||
<AnimatePresence>
|
||
{isPickingModel && (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -4 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -4 }}
|
||
className="absolute left-3 top-full mt-1 bg-[#12151e] border border-white/10 rounded-xl shadow-2xl z-50 overflow-hidden w-56"
|
||
>
|
||
{/* Opção: usar padrão do agente */}
|
||
<button
|
||
onClick={() => { onUpdate(node.id, { node_model: null }); setModelPickerFor(null) }}
|
||
className={`w-full flex items-center gap-2 px-3 py-2 text-xs transition-colors text-left ${
|
||
!node.node_model ? 'text-brand-300 bg-brand-500/10' : 'text-slate-400 hover:bg-white/5'
|
||
}`}
|
||
>
|
||
<span className="w-4 h-4 flex items-center justify-center shrink-0">
|
||
{!node.node_model && <Check size={11} className="text-brand-400" />}
|
||
</span>
|
||
<span>Padrão do agente</span>
|
||
</button>
|
||
<div className="border-t border-white/5 my-0.5" />
|
||
{providerModels.map((mdl) => (
|
||
<button
|
||
key={mdl.value}
|
||
onClick={() => { onUpdate(node.id, { node_model: mdl.value }); setModelPickerFor(null) }}
|
||
className={`w-full flex items-center gap-2 px-3 py-2 text-xs transition-colors text-left ${
|
||
node.node_model === mdl.value
|
||
? 'text-brand-300 bg-brand-500/10'
|
||
: 'text-slate-300 hover:bg-white/5'
|
||
}`}
|
||
>
|
||
<span className="w-4 h-4 flex items-center justify-center shrink-0">
|
||
{node.node_model === mdl.value && <Check size={11} className="text-brand-400" />}
|
||
</span>
|
||
{mdl.label}
|
||
</button>
|
||
))}
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
)}
|
||
|
||
{/* Node Content */}
|
||
{isEditing ? (
|
||
<textarea
|
||
value={editContent}
|
||
onChange={(e) => setEditContent(e.target.value)}
|
||
rows={5}
|
||
className="w-full text-xs text-slate-400 bg-black/20 border-t border-white/5 px-3 py-2 focus:outline-none focus:bg-black/30 resize-none font-mono leading-relaxed"
|
||
/>
|
||
) : (
|
||
<p className="px-3 pb-2.5 text-[11px] text-slate-600 font-mono leading-relaxed line-clamp-3">
|
||
{node.content}
|
||
</p>
|
||
)}
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Calendar Panel ───────────────────────────────────────────────────────────
|
||
|
||
const SLOT_STATUS: Record<CalendarSlot['status'], { label: string; color: string; dot: string }> = {
|
||
available: { label: 'Disponível', color: 'text-green-400', dot: 'bg-green-400' },
|
||
booked: { label: 'Agendado', color: 'text-blue-400', dot: 'bg-blue-400' },
|
||
cancelled: { label: 'Cancelado', color: 'text-slate-500', dot: 'bg-slate-600' },
|
||
}
|
||
|
||
function CalendarPanel({
|
||
slots, loading, onUpdate, onDelete, onCreate,
|
||
}: {
|
||
slots: CalendarSlot[]
|
||
loading: boolean
|
||
onUpdate: (id: string, d: Partial<CalendarSlot>) => void
|
||
onDelete: (id: string) => void
|
||
onCreate: () => void
|
||
}) {
|
||
// Group by date
|
||
const grouped = slots.reduce<Record<string, CalendarSlot[]>>((acc, s) => {
|
||
if (!acc[s.date]) acc[s.date] = []
|
||
acc[s.date].push(s)
|
||
return acc
|
||
}, {})
|
||
|
||
return (
|
||
<div className="flex-1 overflow-y-auto">
|
||
<div className="px-3 py-2 flex items-center justify-between">
|
||
<span className="text-[10px] font-black text-slate-600 uppercase tracking-widest">Agenda</span>
|
||
<button onClick={onCreate}
|
||
className="w-6 h-6 rounded-lg bg-white/5 hover:bg-brand-500/20 text-slate-500 hover:text-brand-400 flex items-center justify-center transition-all"
|
||
>
|
||
<Plus size={13} />
|
||
</button>
|
||
</div>
|
||
|
||
{loading ? (
|
||
<div className="flex items-center justify-center py-10">
|
||
<Loader2 size={20} className="animate-spin text-slate-600" />
|
||
</div>
|
||
) : (
|
||
<div className="space-y-4 px-2 pb-4">
|
||
{Object.entries(grouped).map(([date, daySlots]) => (
|
||
<div key={date}>
|
||
<p className="text-[10px] font-black text-slate-600 uppercase tracking-widest px-1 mb-1.5">
|
||
{format(parseISO(date), "EEE, dd/MM", { locale: ptBR })}
|
||
</p>
|
||
<div className="space-y-1">
|
||
{daySlots.map((slot) => {
|
||
const m = SLOT_STATUS[slot.status]
|
||
return (
|
||
<div key={slot.id}
|
||
className="flex items-center gap-2 px-2.5 py-2 rounded-lg bg-white/[0.02] border border-white/5 group"
|
||
>
|
||
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${m.dot}`} />
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-xs font-semibold text-slate-300 truncate">{slot.title}</p>
|
||
<div className="flex items-center gap-1.5 mt-0.5">
|
||
<Clock size={9} className="text-slate-600" />
|
||
<span className="text-[10px] text-slate-600">
|
||
{slot.time_start.slice(0, 5)}–{slot.time_end.slice(0, 5)}
|
||
</span>
|
||
{slot.attendee_name && (
|
||
<span className="text-[10px] text-blue-400 font-medium truncate">· {slot.attendee_name}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||
{slot.status === 'available' && (
|
||
<button onClick={() => onUpdate(slot.id, { status: 'booked' })}
|
||
title="Marcar como agendado"
|
||
className="w-5 h-5 flex items-center justify-center text-slate-600 hover:text-blue-400 transition-colors"
|
||
>
|
||
<CheckCircle size={12} />
|
||
</button>
|
||
)}
|
||
{slot.status === 'booked' && (
|
||
<button onClick={() => onUpdate(slot.id, { status: 'available' })}
|
||
title="Liberar horário"
|
||
className="w-5 h-5 flex items-center justify-center text-slate-600 hover:text-green-400 transition-colors"
|
||
>
|
||
<Power size={12} />
|
||
</button>
|
||
)}
|
||
<button onClick={() => onDelete(slot.id)}
|
||
className="w-5 h-5 flex items-center justify-center text-slate-700 hover:text-red-400 transition-colors"
|
||
>
|
||
<Trash2 size={11} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Agents Config Panel ──────────────────────────────────────────────────────
|
||
|
||
function AgentsPanel({
|
||
agents, selected, loadingAgents, onSelect, onCreate, onUpdate, onDelete,
|
||
}: {
|
||
agents: SecAgent[]
|
||
selected: SecAgent | null
|
||
loadingAgents: boolean
|
||
onSelect: (a: SecAgent) => void
|
||
onCreate: () => void
|
||
onUpdate: (id: string, d: Partial<SecAgent>) => void
|
||
onDelete: (id: string) => void
|
||
}) {
|
||
const [editingId, setEditingId] = useState<string | null>(null)
|
||
const [form, setForm] = useState<Partial<SecAgent>>({})
|
||
|
||
const startEdit = (a: SecAgent) => { setEditingId(a.id); setForm({ ...a }) }
|
||
|
||
return (
|
||
<div className="flex-1 overflow-y-auto">
|
||
<div className="px-3 py-2 flex items-center justify-between">
|
||
<span className="text-[10px] font-black text-slate-600 uppercase tracking-widest">Agentes</span>
|
||
<button onClick={onCreate}
|
||
className="w-6 h-6 rounded-lg bg-white/5 hover:bg-brand-500/20 text-slate-500 hover:text-brand-400 flex items-center justify-center transition-all"
|
||
>
|
||
<Plus size={13} />
|
||
</button>
|
||
</div>
|
||
|
||
{loadingAgents ? (
|
||
<div className="flex items-center justify-center py-10"><Loader2 size={20} className="animate-spin text-slate-600" /></div>
|
||
) : (
|
||
<div className="space-y-2 px-2 pb-4">
|
||
{agents.map((a) => {
|
||
const isEditing = editingId === a.id
|
||
const isSelected = selected?.id === a.id
|
||
return (
|
||
<div key={a.id} className={`rounded-xl border transition-all ${isSelected ? 'border-brand-500/20 bg-brand-500/5' : 'border-white/8 bg-white/[0.02]'}`}>
|
||
<div className="flex items-center gap-2 px-3 py-2.5">
|
||
<div className="w-7 h-7 rounded-lg bg-brand-500/15 flex items-center justify-center shrink-0">
|
||
<Bot size={14} className="text-brand-400" />
|
||
</div>
|
||
<div className="flex-1 min-w-0 cursor-pointer" onClick={() => onSelect(a)}>
|
||
<p className={`text-xs font-bold truncate ${isSelected ? 'text-brand-300' : 'text-slate-300'}`}>{a.name}</p>
|
||
<p className="text-[10px] text-slate-600">{a.provider} / {a.model}</p>
|
||
</div>
|
||
<div className="flex items-center gap-1">
|
||
<button onClick={() => startEdit(a)} className="w-5 h-5 flex items-center justify-center text-slate-600 hover:text-slate-400">
|
||
<Pencil size={11} />
|
||
</button>
|
||
<button onClick={() => onDelete(a.id)} className="w-5 h-5 flex items-center justify-center text-slate-700 hover:text-red-400">
|
||
<Trash2 size={11} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{isEditing && (
|
||
<div className="px-3 pb-3 space-y-2 border-t border-white/5 pt-2">
|
||
{[
|
||
{ key: 'name', label: 'Nome', type: 'text' },
|
||
{ key: 'description', label: 'Descrição', type: 'text' },
|
||
{ key: 'provider', label: 'Provider', type: 'text' },
|
||
{ key: 'model', label: 'Modelo', type: 'text' },
|
||
{ key: 'temperature', label: 'Temperatura', type: 'number' },
|
||
{ key: 'context_window', label: 'Context Window', type: 'number' },
|
||
].map(({ key, label, type }) => (
|
||
<div key={key}>
|
||
<p className="text-[10px] text-slate-600 mb-0.5">{label}</p>
|
||
<input
|
||
type={type}
|
||
value={String((form as any)[key] ?? '')}
|
||
onChange={(e) => setForm((f) => ({ ...f, [key]: type === 'number' ? parseFloat(e.target.value) : e.target.value }))}
|
||
className="w-full text-xs bg-black/30 text-white border border-white/10 rounded-lg px-2 py-1.5 focus:outline-none focus:border-brand-500/40"
|
||
/>
|
||
</div>
|
||
))}
|
||
<div className="flex gap-2 pt-1">
|
||
<button onClick={() => { onUpdate(a.id, form); setEditingId(null) }}
|
||
className="flex-1 py-1.5 text-xs bg-brand-500/20 text-brand-400 rounded-lg hover:bg-brand-500/30 transition-colors font-semibold"
|
||
>
|
||
Salvar
|
||
</button>
|
||
<button onClick={() => setEditingId(null)}
|
||
className="py-1.5 px-3 text-xs bg-white/5 text-slate-400 rounded-lg hover:bg-white/10 transition-colors"
|
||
>
|
||
Cancelar
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Numbers Panel ────────────────────────────────────────────────────────────
|
||
|
||
const ROLE_META: Record<NumberRole, { label: string; icon: React.ElementType; color: string; bg: string }> = {
|
||
secretary_virtual: { label: 'Secretária Virtual', icon: Sparkles, color: 'text-brand-400', bg: 'bg-brand-500/15' },
|
||
clinic: { label: 'Clínica / Empresa', icon: Building2, color: 'text-blue-400', bg: 'bg-blue-500/10' },
|
||
doctor: { label: 'Médico / Dr.', icon: Stethoscope, color: 'text-green-400', bg: 'bg-green-500/10' },
|
||
specialist: { label: 'Especialista', icon: Star, color: 'text-purple-400', bg: 'bg-purple-500/10' },
|
||
manager: { label: 'Gerente / Fallback', icon: Shield, color: 'text-orange-400', bg: 'bg-orange-500/10' },
|
||
reserve: { label: 'Reserva', icon: Repeat, color: 'text-yellow-400', bg: 'bg-yellow-500/10' },
|
||
human_secretary: { label: 'Secretária Humana', icon: UserCheck, color: 'text-rose-400', bg: 'bg-rose-500/10' },
|
||
}
|
||
|
||
const ROLE_OPTIONS: { value: NumberRole; label: string }[] = [
|
||
{ value: 'secretary_virtual', label: 'Secretária Virtual' },
|
||
{ value: 'clinic', label: 'Clínica / Empresa' },
|
||
{ value: 'doctor', label: 'Médico / Dr.' },
|
||
{ value: 'specialist', label: 'Especialista' },
|
||
{ value: 'manager', label: 'Gerente / Fallback' },
|
||
{ value: 'reserve', label: 'Reserva' },
|
||
{ value: 'human_secretary', label: 'Secretária Humana' },
|
||
]
|
||
|
||
interface BaileysInstance { id: string; name: string; status: string }
|
||
|
||
function NumbersPanel({
|
||
numbers, loading, onCreate, onUpdate, onDelete,
|
||
}: {
|
||
numbers: SecNumber[]
|
||
loading: boolean
|
||
onCreate: (d: Partial<SecNumber>) => void
|
||
onUpdate: (id: string, d: Partial<SecNumber>) => void
|
||
onDelete: (id: string) => void
|
||
}) {
|
||
const [instances, setInstances] = useState<BaileysInstance[]>([])
|
||
const [loadingInst, setLoadingInst] = useState(false)
|
||
const [rolePickerFor, setRolePickerFor] = useState<string | null>(null) // instance_id
|
||
const [editingId, setEditingId] = useState<string | null>(null) // sec_number.id
|
||
const [editForm, setEditForm] = useState<{ area: string; priority: number; notes: string }>({ area: '', priority: 10, notes: '' })
|
||
|
||
useEffect(() => {
|
||
setLoadingInst(true)
|
||
import('../../services/chatApiService')
|
||
.then(({ instanceApi }) => instanceApi.list())
|
||
.then((data: any) => setInstances(Array.isArray(data) ? data : data.instances ?? []))
|
||
.catch(() => {})
|
||
.finally(() => setLoadingInst(false))
|
||
}, [])
|
||
|
||
const roleOf = (instanceId: string) => numbers.find((n) => n.instance_id === instanceId)
|
||
const numberOf = (instanceId: string) => numbers.find((n) => n.instance_id === instanceId)
|
||
|
||
const assignRole = (inst: BaileysInstance, role: NumberRole) => {
|
||
const existing = numberOf(inst.id)
|
||
if (existing) {
|
||
onUpdate(existing.id, { role })
|
||
} else {
|
||
onCreate({ instance_id: inst.id, label: inst.name, role, priority: 10 })
|
||
}
|
||
setRolePickerFor(null)
|
||
}
|
||
|
||
const statusDot = (status: string) => {
|
||
if (status === 'CONNECTED') return 'bg-green-400'
|
||
if (status === 'CONNECTING') return 'bg-yellow-400 animate-pulse'
|
||
return 'bg-slate-600'
|
||
}
|
||
|
||
return (
|
||
<div className="flex-1 overflow-y-auto">
|
||
<div className="px-3 py-2 flex items-center justify-between">
|
||
<span className="text-[10px] font-black text-slate-600 uppercase tracking-widest">Instâncias / Papéis</span>
|
||
<button onClick={() => { setLoadingInst(true); import('../../services/chatApiService').then(({ instanceApi }) => instanceApi.list()).then((d: any) => setInstances(Array.isArray(d) ? d : d.instances ?? [])).finally(() => setLoadingInst(false)) }}
|
||
title="Recarregar instâncias"
|
||
className="w-6 h-6 rounded-lg bg-white/5 hover:bg-brand-500/20 text-slate-500 hover:text-brand-400 flex items-center justify-center transition-all"
|
||
>
|
||
<RefreshCw size={12} className={loadingInst ? 'animate-spin' : ''} />
|
||
</button>
|
||
</div>
|
||
|
||
<p className="text-[10px] text-slate-700 px-3 pb-2 leading-relaxed">
|
||
Atribua um papel a cada instância WhatsApp. A secretária usa isso para saber para qual número enviar consultas internas.
|
||
</p>
|
||
|
||
{(loading || loadingInst) ? (
|
||
<div className="flex items-center justify-center py-10">
|
||
<Loader2 size={20} className="animate-spin text-slate-600" />
|
||
</div>
|
||
) : instances.length === 0 ? (
|
||
<p className="text-xs text-slate-700 text-center py-8 px-4">
|
||
Nenhuma instância WhatsApp encontrada. Crie instâncias na tela principal.
|
||
</p>
|
||
) : (
|
||
<div className="space-y-2 px-2 pb-4">
|
||
{instances.map((inst) => {
|
||
const num = numberOf(inst.id)
|
||
const role = num?.role
|
||
const m = role ? ROLE_META[role] : null
|
||
const Icon = m?.icon ?? Phone
|
||
const isEditing = editingId === num?.id
|
||
|
||
return (
|
||
<div key={inst.id}
|
||
className={`rounded-xl border transition-all ${
|
||
num?.active === false
|
||
? 'border-white/4 bg-white/[0.01] opacity-50'
|
||
: 'border-white/8 bg-white/[0.02]'
|
||
}`}
|
||
>
|
||
<div className="flex items-center gap-2 px-3 py-2.5">
|
||
{/* Status dot + ícone */}
|
||
<div className="relative shrink-0">
|
||
<div className={`w-7 h-7 rounded-lg flex items-center justify-center ${m?.bg ?? 'bg-white/5'}`}>
|
||
<Icon size={13} className={m?.color ?? 'text-slate-600'} />
|
||
</div>
|
||
<span className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border border-[#0a0d15] ${statusDot(inst.status)}`} />
|
||
</div>
|
||
|
||
{/* Info */}
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-xs font-bold text-slate-300 truncate">{inst.name}</p>
|
||
<div className="flex items-center gap-1.5 mt-0.5">
|
||
{m ? (
|
||
<span className={`text-[10px] font-semibold ${m.color}`}>{m.label}</span>
|
||
) : (
|
||
<span className="text-[10px] text-slate-700 italic">sem papel atribuído</span>
|
||
)}
|
||
{num?.area && <span className="text-[10px] text-slate-700">· {num.area}</span>}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="flex items-center gap-1 relative">
|
||
{/* Botão atribuir papel */}
|
||
<button
|
||
onClick={() => setRolePickerFor(rolePickerFor === inst.id ? null : inst.id)}
|
||
className={`flex items-center gap-1 px-2 py-1 rounded-lg text-[10px] font-semibold transition-all ${
|
||
m ? `${m.bg} ${m.color}` : 'bg-white/5 text-slate-600 hover:text-slate-400'
|
||
}`}
|
||
>
|
||
{m ? m.label.split(' ')[0] : 'Papel'}
|
||
<ChevronDown size={9} className={`transition-transform ${rolePickerFor === inst.id ? 'rotate-180' : ''}`} />
|
||
</button>
|
||
|
||
{/* Role picker dropdown */}
|
||
<AnimatePresence>
|
||
{rolePickerFor === inst.id && (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -4 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -4 }}
|
||
className="absolute right-0 top-full mt-1 bg-[#12151e] border border-white/10 rounded-xl shadow-2xl z-50 overflow-hidden w-44"
|
||
>
|
||
{num && (
|
||
<>
|
||
<button
|
||
onClick={() => { onDelete(num.id); setRolePickerFor(null) }}
|
||
className="w-full flex items-center gap-2 px-3 py-2 text-xs text-red-400 hover:bg-red-500/10 transition-colors"
|
||
>
|
||
<Trash2 size={11} /> Remover papel
|
||
</button>
|
||
<div className="border-t border-white/5" />
|
||
</>
|
||
)}
|
||
{ROLE_OPTIONS.map((o) => {
|
||
const rm = ROLE_META[o.value]
|
||
const RIcon = rm.icon
|
||
return (
|
||
<button key={o.value}
|
||
onClick={() => assignRole(inst, o.value)}
|
||
className={`w-full flex items-center gap-2 px-3 py-2 text-xs transition-colors text-left ${
|
||
role === o.value ? `${rm.bg} ${rm.color}` : 'text-slate-300 hover:bg-white/5'
|
||
}`}
|
||
>
|
||
<RIcon size={12} className={rm.color} />
|
||
<span className="flex-1">{o.label}</span>
|
||
{role === o.value && <Check size={11} />}
|
||
</button>
|
||
)
|
||
})}
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
{/* Toggle ativo */}
|
||
{num && (
|
||
<button onClick={() => onUpdate(num.id, { active: !num.active })}
|
||
className="w-5 h-5 flex items-center justify-center text-slate-600 hover:text-slate-400"
|
||
>
|
||
{num.active ? <ToggleRight size={16} className="text-brand-400" /> : <ToggleLeft size={16} />}
|
||
</button>
|
||
)}
|
||
|
||
{/* Editar detalhes (área, prioridade) */}
|
||
{num && !isEditing && (
|
||
<button onClick={() => { setEditingId(num.id); setEditForm({ area: num.area ?? '', priority: num.priority, notes: num.notes ?? '' }) }}
|
||
className="w-5 h-5 flex items-center justify-center text-slate-700 hover:text-slate-400"
|
||
>
|
||
<Pencil size={11} />
|
||
</button>
|
||
)}
|
||
{isEditing && (
|
||
<>
|
||
<button onClick={() => { onUpdate(num!.id, editForm); setEditingId(null) }}
|
||
className="w-5 h-5 flex items-center justify-center text-green-400"
|
||
>
|
||
<Check size={12} />
|
||
</button>
|
||
<button onClick={() => setEditingId(null)}
|
||
className="w-5 h-5 flex items-center justify-center text-slate-600"
|
||
>
|
||
<X size={12} />
|
||
</button>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Inline edit: área + prioridade */}
|
||
{isEditing && (
|
||
<div className="px-3 pb-3 pt-1 border-t border-white/5 grid grid-cols-2 gap-2">
|
||
<div>
|
||
<p className="text-[10px] text-slate-600 mb-0.5">Área / Especialidade</p>
|
||
<input
|
||
value={editForm.area}
|
||
onChange={(e) => setEditForm((f) => ({ ...f, area: e.target.value }))}
|
||
placeholder="Ex: Cardiologia"
|
||
className="w-full text-xs bg-black/30 text-white border border-white/10 rounded-lg px-2 py-1.5 focus:outline-none focus:border-brand-500/40"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<p className="text-[10px] text-slate-600 mb-0.5">Prioridade fallback</p>
|
||
<input
|
||
type="number"
|
||
value={editForm.priority}
|
||
onChange={(e) => setEditForm((f) => ({ ...f, priority: parseInt(e.target.value) || 1 }))}
|
||
className="w-full text-xs bg-black/30 text-white border border-white/10 rounded-lg px-2 py-1.5 focus:outline-none focus:border-brand-500/40"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Node Editor Panel ────────────────────────────────────────────────────────
|
||
|
||
function NodeEditorPanel({
|
||
node, selectedAgent, onUpdate, onDelete, onClose,
|
||
}: {
|
||
node: BrainNode
|
||
selectedAgent: SecAgent | null
|
||
onUpdate: (id: string, d: Partial<BrainNode>) => void
|
||
onDelete: (id: string) => void
|
||
onClose: () => void
|
||
}) {
|
||
const [title, setTitle] = useState(node.title)
|
||
const [content, setContent] = useState(node.content)
|
||
const [dirty, setDirty] = useState(false)
|
||
const [modelPickerOpen, setModelPickerOpen] = useState(false)
|
||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||
const modelPickerRef = useRef<HTMLDivElement>(null)
|
||
|
||
// Reseta quando muda de nó
|
||
useEffect(() => {
|
||
setTitle(node.title)
|
||
setContent(node.content)
|
||
setDirty(false)
|
||
}, [node.id])
|
||
|
||
// Fecha model picker ao clicar fora
|
||
useEffect(() => {
|
||
const h = (e: MouseEvent) => {
|
||
if (modelPickerRef.current && !modelPickerRef.current.contains(e.target as Node)) setModelPickerOpen(false)
|
||
}
|
||
document.addEventListener('mousedown', h)
|
||
return () => document.removeEventListener('mousedown', h)
|
||
}, [])
|
||
|
||
// Ctrl+S salva
|
||
useEffect(() => {
|
||
const h = (e: KeyboardEvent) => {
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||
e.preventDefault()
|
||
if (dirty) save()
|
||
}
|
||
}
|
||
window.addEventListener('keydown', h)
|
||
return () => window.removeEventListener('keydown', h)
|
||
}, [dirty, title, content])
|
||
|
||
const save = () => {
|
||
onUpdate(node.id, { title, content })
|
||
setDirty(false)
|
||
}
|
||
|
||
const m = NODE_META[node.type as BrainNodeType] ?? NODE_META.knowledge
|
||
const Icon = m.icon
|
||
|
||
const providerModels = selectedAgent
|
||
? (MODELS_BY_PROVIDER[selectedAgent.provider] ?? ALL_MODELS)
|
||
: ALL_MODELS
|
||
|
||
const lineCount = content.split('\n').length
|
||
const charCount = content.length
|
||
|
||
return (
|
||
<div className="flex flex-col h-full">
|
||
{/* Header */}
|
||
<div className="h-14 px-4 flex items-center gap-3 border-b border-white/5 bg-[#080b10] shrink-0">
|
||
<div className={`w-8 h-8 rounded-xl flex items-center justify-center shrink-0 ${m.bg}`}>
|
||
<Icon size={16} className={m.color} />
|
||
</div>
|
||
<div className="flex-1 min-w-0 flex items-center gap-2">
|
||
<input
|
||
value={title}
|
||
onChange={(e) => { setTitle(e.target.value); setDirty(true) }}
|
||
className="flex-1 text-sm font-bold bg-transparent text-white focus:outline-none border-b border-transparent focus:border-white/20 transition-colors truncate"
|
||
/>
|
||
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full ${m.bg} ${m.color} shrink-0`}>
|
||
{m.label}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center gap-1.5 shrink-0">
|
||
{/* Toggle active */}
|
||
<button
|
||
onClick={() => onUpdate(node.id, { active: !node.active })}
|
||
title={node.active ? 'Desativar nó' : 'Ativar nó'}
|
||
className="flex items-center gap-1 px-2 py-1 rounded-lg text-[10px] font-semibold border transition-all"
|
||
style={node.active
|
||
? { borderColor: 'rgba(99,179,237,0.2)', color: '#63b3ed', background: 'rgba(99,179,237,0.08)' }
|
||
: { borderColor: 'rgba(255,255,255,0.05)', color: '#4a5568', background: 'transparent' }
|
||
}
|
||
>
|
||
{node.active ? <ToggleRight size={13} /> : <ToggleLeft size={13} />}
|
||
{node.active ? 'Ativo' : 'Inativo'}
|
||
</button>
|
||
{/* Save */}
|
||
<button
|
||
onClick={save}
|
||
disabled={!dirty}
|
||
title="Salvar (Ctrl+S)"
|
||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
|
||
dirty
|
||
? 'bg-brand-500 hover:bg-brand-600 text-white'
|
||
: 'bg-white/5 text-slate-600 cursor-not-allowed'
|
||
}`}
|
||
>
|
||
<Save size={12} />
|
||
{dirty ? 'Salvar' : 'Salvo'}
|
||
</button>
|
||
{/* Delete */}
|
||
<button
|
||
onClick={() => { onDelete(node.id); onClose() }}
|
||
title="Excluir nó"
|
||
className="w-8 h-8 flex items-center justify-center rounded-lg text-slate-700 hover:text-red-400 hover:bg-red-500/10 transition-all"
|
||
>
|
||
<Trash2 size={14} />
|
||
</button>
|
||
{/* Close */}
|
||
<button
|
||
onClick={onClose}
|
||
title="Fechar editor"
|
||
className="w-8 h-8 flex items-center justify-center rounded-lg text-slate-600 hover:text-slate-300 hover:bg-white/5 transition-all"
|
||
>
|
||
<X size={14} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Model picker — apenas para nó persona */}
|
||
{node.type === 'persona' && (
|
||
<div className="px-4 py-2 border-b border-white/5 bg-[#080b10]/50 flex items-center gap-2" ref={modelPickerRef}>
|
||
<Cpu size={11} className="text-slate-600" />
|
||
<span className="text-[10px] text-slate-600 font-semibold">Modelo deste nó:</span>
|
||
<div className="relative">
|
||
<button
|
||
onClick={() => setModelPickerOpen((o) => !o)}
|
||
className={`flex items-center gap-1.5 px-2 py-1 rounded-lg text-[10px] font-semibold transition-all ${
|
||
node.node_model
|
||
? 'bg-brand-500/15 text-brand-300 border border-brand-500/20'
|
||
: 'bg-white/5 text-slate-500 border border-white/5 hover:text-slate-400'
|
||
}`}
|
||
>
|
||
{node.node_model ? shortModelLabel(node.node_model) : 'padrão do agente'}
|
||
<ChevronDown size={9} className={`transition-transform ${modelPickerOpen ? 'rotate-180' : ''}`} />
|
||
</button>
|
||
<AnimatePresence>
|
||
{modelPickerOpen && (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -4 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -4 }}
|
||
className="absolute left-0 top-full mt-1 bg-[#12151e] border border-white/10 rounded-xl shadow-2xl z-50 overflow-hidden w-56"
|
||
>
|
||
<button
|
||
onClick={() => { onUpdate(node.id, { node_model: null }); setModelPickerOpen(false) }}
|
||
className={`w-full flex items-center gap-2 px-3 py-2 text-xs transition-colors text-left ${
|
||
!node.node_model ? 'text-brand-300 bg-brand-500/10' : 'text-slate-400 hover:bg-white/5'
|
||
}`}
|
||
>
|
||
<span className="w-4 h-4 flex items-center justify-center shrink-0">
|
||
{!node.node_model && <Check size={11} className="text-brand-400" />}
|
||
</span>
|
||
Padrão do agente
|
||
</button>
|
||
<div className="border-t border-white/5 my-0.5" />
|
||
{providerModels.map((mdl) => (
|
||
<button
|
||
key={mdl.value}
|
||
onClick={() => { onUpdate(node.id, { node_model: mdl.value }); setModelPickerOpen(false) }}
|
||
className={`w-full flex items-center gap-2 px-3 py-2 text-xs transition-colors text-left ${
|
||
node.node_model === mdl.value ? 'text-brand-300 bg-brand-500/10' : 'text-slate-300 hover:bg-white/5'
|
||
}`}
|
||
>
|
||
<span className="w-4 h-4 flex items-center justify-center shrink-0">
|
||
{node.node_model === mdl.value && <Check size={11} className="text-brand-400" />}
|
||
</span>
|
||
{mdl.label}
|
||
</button>
|
||
))}
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Editor */}
|
||
<div className="flex-1 flex flex-col overflow-hidden" style={{ background: 'linear-gradient(180deg, #080b10 0%, #0a0d15 100%)' }}>
|
||
<textarea
|
||
ref={textareaRef}
|
||
value={content}
|
||
onChange={(e) => { setContent(e.target.value); setDirty(true) }}
|
||
placeholder="Conteúdo do nó..."
|
||
className="flex-1 w-full bg-transparent text-sm text-slate-300 font-mono leading-relaxed px-6 py-5 focus:outline-none resize-none"
|
||
style={{ minHeight: 0 }}
|
||
spellCheck={false}
|
||
/>
|
||
</div>
|
||
|
||
{/* Footer — stats */}
|
||
<div className="h-8 px-4 flex items-center gap-4 border-t border-white/5 bg-[#080b10] shrink-0">
|
||
<span className="text-[10px] text-slate-700">{lineCount} linha{lineCount !== 1 ? 's' : ''}</span>
|
||
<span className="text-[10px] text-slate-700">{charCount} caractere{charCount !== 1 ? 's' : ''}</span>
|
||
{dirty && <span className="text-[10px] text-brand-500 ml-auto">● não salvo — Ctrl+S</span>}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Chat Area ────────────────────────────────────────────────────────────────
|
||
|
||
function ChatArea({
|
||
conversation, messages, loadingMessages, sendingMessage,
|
||
onSend, onClose, onStatusChange,
|
||
}: {
|
||
conversation: SecConversation
|
||
messages: any[]
|
||
loadingMessages: boolean
|
||
sendingMessage: boolean
|
||
onSend: (text: string) => void
|
||
onClose: () => void
|
||
onStatusChange: (status: SecConversation['status']) => void
|
||
}) {
|
||
const [input, setInput] = useState('')
|
||
const scrollRef = useRef<HTMLDivElement>(null)
|
||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||
|
||
useEffect(() => {
|
||
if (scrollRef.current) {
|
||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
|
||
}
|
||
}, [messages, loadingMessages])
|
||
|
||
const send = () => {
|
||
const text = input.trim()
|
||
if (!text || sendingMessage) return
|
||
setInput('')
|
||
onSend(text)
|
||
}
|
||
|
||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send() }
|
||
}
|
||
|
||
return (
|
||
<div className="flex flex-col h-full">
|
||
{/* Header */}
|
||
<div className="h-14 px-4 flex items-center gap-3 border-b border-white/5 bg-[#080b10] shrink-0">
|
||
<div className="w-8 h-8 rounded-full bg-white/5 flex items-center justify-center">
|
||
<User size={15} className="text-slate-400" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<p className="text-sm font-bold text-white">{conversation.contact_name}</p>
|
||
<p className="text-[10px] text-slate-600">Sessão de Teste · IA respondendo</p>
|
||
</div>
|
||
<div className="flex items-center gap-1">
|
||
{/* Status quick actions */}
|
||
{conversation.status === 'active' && (
|
||
<>
|
||
<button onClick={() => onStatusChange('closed')} title="Encerrar"
|
||
className="px-2 py-1 text-[10px] font-bold text-slate-500 hover:text-slate-300 border border-white/5 hover:border-white/15 rounded-lg transition-all"
|
||
>
|
||
Encerrar
|
||
</button>
|
||
<button onClick={() => onStatusChange('escalated')} title="Escalar para humano"
|
||
className="px-2 py-1 text-[10px] font-bold text-yellow-600 hover:text-yellow-400 border border-yellow-900/30 hover:border-yellow-500/30 rounded-lg transition-all"
|
||
>
|
||
Escalar
|
||
</button>
|
||
</>
|
||
)}
|
||
{conversation.status !== 'active' && (
|
||
<button onClick={() => onStatusChange('active')}
|
||
className="px-2 py-1 text-[10px] font-bold text-green-600 hover:text-green-400 border border-green-900/30 hover:border-green-500/30 rounded-lg transition-all"
|
||
>
|
||
Reabrir
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Messages */}
|
||
<div
|
||
ref={scrollRef}
|
||
className="flex-1 overflow-y-auto px-4 py-4 space-y-3"
|
||
style={{ background: 'linear-gradient(180deg, #080b10 0%, #0a0d15 100%)' }}
|
||
>
|
||
{loadingMessages ? (
|
||
<div className="flex items-center justify-center h-full">
|
||
<Loader2 size={24} className="animate-spin text-slate-600" />
|
||
</div>
|
||
) : messages.length === 0 ? (
|
||
<div className="flex flex-col items-center justify-center h-full gap-3 opacity-40">
|
||
<Brain size={32} className="text-slate-600" />
|
||
<p className="text-xs text-slate-600 text-center">
|
||
Comece uma conversa para testar a secretária.
|
||
</p>
|
||
</div>
|
||
) : (
|
||
messages.map((msg) => {
|
||
if (msg.role === 'system') return null
|
||
const isUser = msg.role === 'user'
|
||
return (
|
||
<motion.div
|
||
key={msg.id}
|
||
initial={{ opacity: 0, y: 8 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
className={`flex ${isUser ? 'justify-end' : 'justify-start'} gap-2`}
|
||
>
|
||
{!isUser && (
|
||
<div className="w-6 h-6 rounded-full bg-brand-500/20 flex items-center justify-center shrink-0 mt-0.5">
|
||
<Sparkles size={11} className="text-brand-400" />
|
||
</div>
|
||
)}
|
||
<div className={`max-w-[75%] rounded-2xl px-4 py-2.5 ${
|
||
isUser
|
||
? 'bg-brand-600 text-white rounded-br-sm'
|
||
: 'bg-[#12151e] border border-white/5 text-slate-200 rounded-bl-sm'
|
||
}`}>
|
||
<p className="text-sm leading-relaxed whitespace-pre-wrap">{msg.content}</p>
|
||
<p className={`text-[10px] mt-1 ${isUser ? 'text-white/50 text-right' : 'text-slate-600'}`}>
|
||
{format(parseISO(msg.created_at), 'HH:mm')}
|
||
</p>
|
||
</div>
|
||
</motion.div>
|
||
)
|
||
})
|
||
)}
|
||
|
||
{/* Typing indicator */}
|
||
{sendingMessage && (
|
||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="flex justify-start gap-2">
|
||
<div className="w-6 h-6 rounded-full bg-brand-500/20 flex items-center justify-center shrink-0 mt-0.5">
|
||
<Sparkles size={11} className="text-brand-400" />
|
||
</div>
|
||
<div className="bg-[#12151e] border border-white/5 rounded-2xl rounded-bl-sm px-4 py-3">
|
||
<div className="flex gap-1 items-center h-4">
|
||
{[0, 1, 2].map((i) => (
|
||
<motion.span key={i} className="w-1.5 h-1.5 rounded-full bg-brand-400"
|
||
animate={{ opacity: [0.3, 1, 0.3], y: [0, -3, 0] }}
|
||
transition={{ duration: 0.9, repeat: Infinity, delay: i * 0.2 }}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Input */}
|
||
{conversation.status === 'active' ? (
|
||
<div className="px-4 py-3 border-t border-white/5 bg-[#080b10] flex items-end gap-2 shrink-0">
|
||
<textarea
|
||
ref={inputRef}
|
||
value={input}
|
||
onChange={(e) => setInput(e.target.value)}
|
||
onKeyDown={onKeyDown}
|
||
placeholder="Digite uma mensagem... (Enter para enviar)"
|
||
rows={1}
|
||
className="flex-1 bg-white/5 border border-white/10 rounded-2xl px-4 py-2.5 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-brand-500/40 resize-none max-h-32 overflow-y-auto"
|
||
style={{ height: 'auto' }}
|
||
onInput={(e) => {
|
||
const t = e.currentTarget
|
||
t.style.height = 'auto'
|
||
t.style.height = `${Math.min(t.scrollHeight, 128)}px`
|
||
}}
|
||
/>
|
||
<button onClick={send} disabled={!input.trim() || sendingMessage}
|
||
className="w-10 h-10 rounded-xl bg-brand-500 hover:bg-brand-600 text-white flex items-center justify-center transition-all disabled:opacity-40 disabled:cursor-not-allowed shrink-0"
|
||
>
|
||
<Send size={16} />
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="px-4 py-3 border-t border-white/5 bg-[#080b10] shrink-0">
|
||
<p className="text-xs text-center text-slate-600">
|
||
Conversa {STATUS_META[conversation.status].label.toLowerCase()}.
|
||
<button onClick={() => onStatusChange('active')} className="text-brand-400 hover:underline">Reabrir</button>
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── New Conversation Modal ───────────────────────────────────────────────────
|
||
|
||
function NewConvModal({
|
||
open, onClose, onConfirm,
|
||
}: { open: boolean; onClose: () => void; onConfirm: (name: string) => void }) {
|
||
const [name, setName] = useState('')
|
||
if (!open) return null
|
||
return (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
|
||
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }}
|
||
className="bg-[#0f1117] border border-white/10 rounded-2xl p-6 w-80 shadow-2xl"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<h3 className="text-base font-black text-white mb-4">Nova sessão de teste</h3>
|
||
<label className="text-xs text-slate-500 font-semibold block mb-1">Nome do contato (simulado)</label>
|
||
<input
|
||
autoFocus
|
||
value={name}
|
||
onChange={(e) => setName(e.target.value)}
|
||
onKeyDown={(e) => { if (e.key === 'Enter' && name.trim()) { onConfirm(name.trim()); onClose() } }}
|
||
placeholder="Ex: João Teste"
|
||
className="w-full bg-white/5 border border-white/10 rounded-xl px-3 py-2.5 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-brand-500/40 mb-4"
|
||
/>
|
||
<div className="flex gap-2">
|
||
<button onClick={() => { if (name.trim()) { onConfirm(name.trim()); onClose() } }}
|
||
disabled={!name.trim()}
|
||
className="flex-1 py-2.5 bg-brand-500 hover:bg-brand-600 text-white text-sm font-bold rounded-xl transition-all disabled:opacity-40"
|
||
>
|
||
Iniciar
|
||
</button>
|
||
<button onClick={onClose} className="py-2.5 px-4 bg-white/5 text-slate-400 text-sm rounded-xl hover:bg-white/10 transition-all">
|
||
Cancelar
|
||
</button>
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── New Calendar Slot Modal ──────────────────────────────────────────────────
|
||
|
||
function NewSlotModal({
|
||
open, onClose, onConfirm,
|
||
}: { open: boolean; onClose: () => void; onConfirm: (d: any) => void }) {
|
||
const [form, setForm] = useState({ title: '', date: '', time_start: '09:00', time_end: '10:00', notes: '' })
|
||
if (!open) return null
|
||
|
||
const update = (key: string, val: string) => setForm((f) => ({ ...f, [key]: val }))
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={onClose}>
|
||
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }}
|
||
className="bg-[#0f1117] border border-white/10 rounded-2xl p-6 w-96 shadow-2xl"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<h3 className="text-base font-black text-white mb-4">Novo horário na agenda</h3>
|
||
<div className="space-y-3">
|
||
{[
|
||
{ key: 'title', label: 'Título', type: 'text', placeholder: 'Ex: Consulta Técnica' },
|
||
{ key: 'date', label: 'Data', type: 'date', placeholder: '' },
|
||
{ key: 'time_start', label: 'Início', type: 'time', placeholder: '' },
|
||
{ key: 'time_end', label: 'Fim', type: 'time', placeholder: '' },
|
||
{ key: 'notes', label: 'Observações', type: 'text', placeholder: 'Opcional' },
|
||
].map(({ key, label, type, placeholder }) => (
|
||
<div key={key}>
|
||
<p className="text-xs text-slate-500 font-semibold mb-1">{label}</p>
|
||
<input type={type} value={(form as any)[key]} placeholder={placeholder}
|
||
onChange={(e) => update(key, e.target.value)}
|
||
className="w-full bg-white/5 border border-white/10 rounded-xl px-3 py-2 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-brand-500/40"
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="flex gap-2 mt-4">
|
||
<button onClick={() => { if (form.title && form.date) { onConfirm(form); onClose() } }}
|
||
disabled={!form.title || !form.date}
|
||
className="flex-1 py-2.5 bg-brand-500 hover:bg-brand-600 text-white text-sm font-bold rounded-xl transition-all disabled:opacity-40"
|
||
>
|
||
Adicionar
|
||
</button>
|
||
<button onClick={onClose} className="py-2.5 px-4 bg-white/5 text-slate-400 text-sm rounded-xl hover:bg-white/10 transition-all">
|
||
Cancelar
|
||
</button>
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Main Page ────────────────────────────────────────────────────────────────
|
||
|
||
export default function SecretariaPage() {
|
||
const store = useSecretariaStore()
|
||
const [activeTab, setActiveTab] = useState<NavTab>('conversations')
|
||
const [showNewConv, setShowNewConv] = useState(false)
|
||
const [showNewSlot, setShowNewSlot] = useState(false)
|
||
const [openNode, setOpenNode] = useState<BrainNode | null>(null)
|
||
|
||
// Boot
|
||
useEffect(() => {
|
||
store.loadAgents()
|
||
store.loadCalendar()
|
||
store.loadNumbers()
|
||
}, [])
|
||
|
||
// Sincroniza openNode quando o store atualiza o nó (ex: toggle active, node_model)
|
||
useEffect(() => {
|
||
if (!openNode) return
|
||
const fresh = store.nodes.find((n) => n.id === openNode.id)
|
||
if (fresh) setOpenNode(fresh)
|
||
}, [store.nodes])
|
||
|
||
// Handle tab change — load calendar on first visit
|
||
const handleTabChange = useCallback((tab: NavTab) => {
|
||
setActiveTab(tab)
|
||
if (tab !== 'brain') setOpenNode(null)
|
||
if (tab === 'calendar' && store.calendarSlots.length === 0) store.loadCalendar()
|
||
}, [store])
|
||
|
||
// ── Agent actions ────────────────────────────────────────────────────────
|
||
|
||
const handleCreateAgent = async () => {
|
||
const a = await store.createAgent({
|
||
name: `Agente ${store.agents.length + 1}`,
|
||
model: 'gemini-2.0-flash', provider: 'gemini', temperature: 0.7, context_window: 4,
|
||
})
|
||
store.selectAgent(a)
|
||
setActiveTab('brain')
|
||
}
|
||
|
||
const handleAddNode = async (type: BrainNodeType) => {
|
||
if (!store.selectedAgent) return
|
||
const meta: Record<BrainNodeType, { title: string; content: string }> = {
|
||
persona: { title: 'Nova Persona', content: 'Você é um assistente...' },
|
||
knowledge: { title: 'Base de Conhecimento', content: 'Informações sobre...' },
|
||
rules: { title: 'Regras', content: '1. ...\n2. ...' },
|
||
calendar: { title: 'Acesso à Agenda', content: 'Consulte e informe horários disponíveis...' },
|
||
escalation: { title: 'Escalada', content: 'Encaminhe para humano quando...' },
|
||
}
|
||
await store.createNode(store.selectedAgent.id, {
|
||
type, ...meta[type], sort_order: store.nodes.length,
|
||
})
|
||
}
|
||
|
||
// ── Conversation actions ─────────────────────────────────────────────────
|
||
|
||
const handleNewConv = async (name: string) => {
|
||
if (!store.selectedAgent) return
|
||
const conv = await store.createConversation(store.selectedAgent.id, name)
|
||
store.selectConversation(conv)
|
||
setActiveTab('conversations')
|
||
}
|
||
|
||
return (
|
||
<div className="flex h-screen w-full bg-[#080b10] overflow-hidden font-sans">
|
||
|
||
{/* Thin Nav */}
|
||
<ThinNav active={activeTab} onChange={handleTabChange} />
|
||
|
||
{/* Left Panel */}
|
||
<div className="w-[280px] h-full bg-[#0a0d15] border-r border-white/5 flex flex-col shrink-0">
|
||
|
||
{/* Agent selector */}
|
||
<div className="border-b border-white/5 shrink-0">
|
||
<AgentDropdown
|
||
agents={store.agents}
|
||
selected={store.selectedAgent}
|
||
onChange={(a) => store.selectAgent(a)}
|
||
/>
|
||
</div>
|
||
|
||
{/* Panel content */}
|
||
{activeTab === 'conversations' && (
|
||
<ConversationList
|
||
conversations={store.conversations}
|
||
selected={store.selectedConversation}
|
||
onSelect={(c) => store.selectConversation(c)}
|
||
onCreate={() => setShowNewConv(true)}
|
||
onDelete={(id) => store.deleteConversation(id)}
|
||
loadingConversations={store.loadingConversations}
|
||
selectedAgent={store.selectedAgent}
|
||
/>
|
||
)}
|
||
|
||
{activeTab === 'brain' && (
|
||
<BrainPanel
|
||
nodes={store.nodes}
|
||
loadingNodes={store.loadingNodes}
|
||
selectedAgent={store.selectedAgent}
|
||
onAdd={handleAddNode}
|
||
onUpdate={(id, d) => store.updateNode(id, d)}
|
||
onDelete={(id) => store.deleteNode(id)}
|
||
onOpen={(node) => setOpenNode(node)}
|
||
openNodeId={openNode?.id ?? null}
|
||
/>
|
||
)}
|
||
|
||
{activeTab === 'calendar' && (
|
||
<CalendarPanel
|
||
slots={store.calendarSlots}
|
||
loading={store.loadingCalendar}
|
||
onUpdate={(id, d) => store.updateCalendarSlot(id, d)}
|
||
onDelete={(id) => store.deleteCalendarSlot(id)}
|
||
onCreate={() => setShowNewSlot(true)}
|
||
/>
|
||
)}
|
||
|
||
{activeTab === 'agents' && (
|
||
<AgentsPanel
|
||
agents={store.agents}
|
||
selected={store.selectedAgent}
|
||
loadingAgents={store.loadingAgents}
|
||
onSelect={(a) => store.selectAgent(a)}
|
||
onCreate={handleCreateAgent}
|
||
onUpdate={(id, d) => store.updateAgent(id, d)}
|
||
onDelete={(id) => store.deleteAgent(id)}
|
||
/>
|
||
)}
|
||
|
||
{activeTab === 'numbers' && (
|
||
<NumbersPanel
|
||
numbers={store.numbers}
|
||
loading={store.loadingNumbers}
|
||
onCreate={(d) => store.createNumber(d)}
|
||
onUpdate={(id, d) => store.updateNumber(id, d)}
|
||
onDelete={(id) => store.deleteNumber(id)}
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
{/* Main Panel */}
|
||
<div className="flex-1 flex flex-col h-full overflow-hidden">
|
||
{store.selectedConversation && activeTab === 'conversations' ? (
|
||
<ChatArea
|
||
conversation={store.selectedConversation}
|
||
messages={store.messages}
|
||
loadingMessages={store.loadingMessages}
|
||
sendingMessage={store.sendingMessage}
|
||
onSend={(text) => store.sendMessage(store.selectedConversation!.id, text)}
|
||
onClose={() => store.selectConversation(null)}
|
||
onStatusChange={(status) => store.updateConversationStatus(store.selectedConversation!.id, status)}
|
||
/>
|
||
) : activeTab === 'brain' && openNode ? (
|
||
<NodeEditorPanel
|
||
node={openNode}
|
||
selectedAgent={store.selectedAgent}
|
||
onUpdate={(id, d) => store.updateNode(id, d)}
|
||
onDelete={(id) => store.deleteNode(id)}
|
||
onClose={() => setOpenNode(null)}
|
||
/>
|
||
) : (
|
||
<div className="flex-1 flex flex-col items-center justify-center gap-4 opacity-30">
|
||
<Brain size={48} className="text-slate-600" />
|
||
<div className="text-center">
|
||
<p className="text-sm font-bold text-slate-500">
|
||
{activeTab === 'conversations' && 'Selecione ou crie uma sessão de teste'}
|
||
{activeTab === 'brain' && 'Clique em ⤢ em um nó para abrir o editor'}
|
||
{activeTab === 'calendar' && 'Gerencie a agenda no painel lateral'}
|
||
{activeTab === 'agents' && 'Configure e selecione um agente no painel lateral'}
|
||
{activeTab === 'numbers' && 'Gerencie os números e papéis no painel lateral'}
|
||
</p>
|
||
<p className="text-xs text-slate-700 mt-1">Secretária IA — NewWhats</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Modals */}
|
||
<NewConvModal open={showNewConv} onClose={() => setShowNewConv(false)} onConfirm={handleNewConv} />
|
||
<NewSlotModal open={showNewSlot} onClose={() => setShowNewSlot(false)} onConfirm={(d) => store.createCalendarSlot(d)} />
|
||
</div>
|
||
)
|
||
}
|