671 lines
43 KiB
TypeScript
671 lines
43 KiB
TypeScript
'use client';
|
||
|
||
import React, { useState, useEffect, useCallback } from 'react';
|
||
import {
|
||
BrainCircuit, Plus, Trash2, Pencil, ChevronDown, Check,
|
||
Eye, EyeOff, AlertCircle, Bot, Key, Settings,
|
||
Zap, Loader2, Save, ArrowLeft, Info, Power,
|
||
UserCheck, UserX,
|
||
} from 'lucide-react';
|
||
import { motion, AnimatePresence } from 'framer-motion';
|
||
import { api } from '@/lib/api';
|
||
import Link from 'next/link';
|
||
import { Button, Toast } from '@/components/ui';
|
||
|
||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||
|
||
interface Instance {
|
||
id: string;
|
||
nome: string;
|
||
phone: string | null;
|
||
status?: string;
|
||
}
|
||
|
||
interface AICred {
|
||
id: string;
|
||
name: string;
|
||
provider: 'GEMINI' | 'OPENAI';
|
||
createdAt: string;
|
||
}
|
||
|
||
interface AIBot {
|
||
id: string;
|
||
name: string;
|
||
credentialId: string;
|
||
credential?: { id: string; name: string; provider: string };
|
||
systemPrompt: string;
|
||
model: string;
|
||
enabled: boolean;
|
||
triggerMode: 'ALL' | 'KEYWORD';
|
||
keywords: string[];
|
||
}
|
||
|
||
const GEMINI_MODELS = [
|
||
{ value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash (rápido, econômico)' },
|
||
{ value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro (melhor qualidade)' },
|
||
{ value: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash (mais recente)' },
|
||
];
|
||
|
||
const OPENAI_MODELS = [
|
||
{ value: 'gpt-4o-mini', label: 'GPT-4o Mini (rápido, barato)' },
|
||
{ value: 'gpt-4o', label: 'GPT-4o (melhor qualidade)' },
|
||
];
|
||
|
||
function emptyBot(): Partial<AIBot> {
|
||
return {
|
||
name: 'Assistente IA',
|
||
systemPrompt: '',
|
||
model: 'gemini-1.5-flash',
|
||
enabled: false,
|
||
triggerMode: 'ALL',
|
||
keywords: [],
|
||
};
|
||
}
|
||
|
||
// ─── Subcomponents ────────────────────────────────────────────────────────────
|
||
|
||
function SectionCard({ title, icon, children }: { title: string; icon: React.ReactNode; children: React.ReactNode }) {
|
||
return (
|
||
<div className="bg-white/[0.02] border border-white/5 rounded-2xl overflow-hidden backdrop-blur-xl">
|
||
<div className="flex items-center gap-3 px-6 py-4 border-b border-white/5">
|
||
<div className="w-8 h-8 bg-brand-500/15 rounded-lg flex items-center justify-center text-brand-400">
|
||
{icon}
|
||
</div>
|
||
<h2 className="text-base font-bold text-white">{title}</h2>
|
||
</div>
|
||
<div className="p-6">{children}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Toggle({ value, onChange, disabled }: { value: boolean; onChange: (v: boolean) => void; disabled?: boolean }) {
|
||
return (
|
||
<button
|
||
type="button"
|
||
onClick={() => !disabled && onChange(!value)}
|
||
disabled={disabled}
|
||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none disabled:opacity-50 ${value ? 'bg-brand-500' : 'bg-slate-700'}`}
|
||
>
|
||
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${value ? 'translate-x-6' : 'translate-x-1'}`} />
|
||
</button>
|
||
);
|
||
}
|
||
|
||
// ─── Main Page ────────────────────────────────────────────────────────────────
|
||
|
||
export default function WhatsAppBotsPage() {
|
||
const [instances, setInstances] = useState<Instance[]>([]);
|
||
const [activeInstance, setActiveInstance] = useState<Instance | null>(null);
|
||
const [instanceDropdownOpen, setInstanceDropdownOpen] = useState(false);
|
||
const [loadingInstances, setLoadingInstances] = useState(true);
|
||
|
||
// Credentials
|
||
const [creds, setCreds] = useState<AICred[]>([]);
|
||
const [loadingCreds, setLoadingCreds] = useState(false);
|
||
const [showCredForm, setShowCredForm] = useState(false);
|
||
const [credName, setCredName] = useState('');
|
||
const [credApiKey, setCredApiKey] = useState('');
|
||
const [credProvider, setCredProvider] = useState<'GEMINI' | 'OPENAI'>('GEMINI');
|
||
const [showApiKey, setShowApiKey] = useState(false);
|
||
const [savingCred, setSavingCred] = useState(false);
|
||
const [deletingCredId, setDeletingCredId] = useState<string | null>(null);
|
||
|
||
// Bots
|
||
const [bots, setBots] = useState<AIBot[]>([]);
|
||
const [loadingBots, setLoadingBots] = useState(false);
|
||
const [editingBot, setEditingBot] = useState<Partial<AIBot> | null>(null);
|
||
const [editingBotId, setEditingBotId] = useState<string | null>(null);
|
||
const [savingBot, setSavingBot] = useState(false);
|
||
const [deletingBotId, setDeletingBotId] = useState<string | null>(null);
|
||
const [keywordInput, setKeywordInput] = useState('');
|
||
|
||
// Feedback
|
||
const [toast, setToast] = useState<{ msg: string; type: 'ok' | 'err' } | null>(null);
|
||
const showToast = (msg: string, type: 'ok' | 'err' = 'ok') => {
|
||
setToast({ msg, type });
|
||
setTimeout(() => setToast(null), 3500);
|
||
};
|
||
|
||
// Load instances
|
||
useEffect(() => {
|
||
api.getWhatsAppInstances().then((res: any) => {
|
||
const data: Instance[] = res.instances || [];
|
||
setInstances(data);
|
||
if (data.length) setActiveInstance(data[0]);
|
||
}).catch(() => {}).finally(() => setLoadingInstances(false));
|
||
}, []);
|
||
|
||
const loadCreds = useCallback(async (instanceId: string) => {
|
||
setLoadingCreds(true);
|
||
try {
|
||
const data = await api.getAICredentials(instanceId);
|
||
setCreds(Array.isArray(data) ? data : []);
|
||
} catch { setCreds([]); }
|
||
finally { setLoadingCreds(false); }
|
||
}, []);
|
||
|
||
const loadBots = useCallback(async (instanceId: string) => {
|
||
setLoadingBots(true);
|
||
try {
|
||
const data = await api.getAIBots(instanceId);
|
||
setBots(Array.isArray(data) ? data : []);
|
||
} catch { setBots([]); }
|
||
finally { setLoadingBots(false); }
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!activeInstance) return;
|
||
loadCreds(activeInstance.id);
|
||
loadBots(activeInstance.id);
|
||
setEditingBot(null);
|
||
setEditingBotId(null);
|
||
setShowCredForm(false);
|
||
}, [activeInstance, loadCreds, loadBots]);
|
||
|
||
// ─ Helpers ────────────────────────────────────────────────────────────────
|
||
|
||
const selectedCredProvider = creds.find(c => c.id === editingBot?.credentialId)?.provider ?? 'GEMINI';
|
||
const modelOptions = selectedCredProvider === 'OPENAI' ? OPENAI_MODELS : GEMINI_MODELS;
|
||
|
||
// ─ Credential actions ─────────────────────────────────────────────────────
|
||
|
||
const handleSaveCred = async () => {
|
||
if (!activeInstance || !credName.trim() || !credApiKey.trim()) return;
|
||
setSavingCred(true);
|
||
try {
|
||
await api.createAICredential(activeInstance.id, credName.trim(), credApiKey.trim(), credProvider);
|
||
showToast('Credencial salva!');
|
||
setCredName(''); setCredApiKey(''); setShowCredForm(false);
|
||
loadCreds(activeInstance.id);
|
||
} catch (err: any) { showToast(err.response?.data?.error || err.message || 'Erro ao salvar credencial', 'err'); }
|
||
finally { setSavingCred(false); }
|
||
};
|
||
|
||
const handleDeleteCred = async (credId: string) => {
|
||
if (!activeInstance || !confirm('Deletar esta credencial? Os bots que a usam serão afetados.')) return;
|
||
setDeletingCredId(credId);
|
||
try {
|
||
await api.deleteAICredential(activeInstance.id, credId);
|
||
showToast('Credencial removida');
|
||
loadCreds(activeInstance.id);
|
||
loadBots(activeInstance.id);
|
||
} catch (err: any) { showToast(err.message || 'Erro ao deletar', 'err'); }
|
||
finally { setDeletingCredId(null); }
|
||
};
|
||
|
||
// ─ Bot actions ────────────────────────────────────────────────────────────
|
||
|
||
const openNewBot = () => {
|
||
setEditingBotId(null);
|
||
setEditingBot(emptyBot());
|
||
setKeywordInput('');
|
||
};
|
||
|
||
const openEditBot = (bot: AIBot) => {
|
||
setEditingBotId(bot.id);
|
||
setEditingBot({ ...bot });
|
||
setKeywordInput('');
|
||
};
|
||
|
||
const handleSaveBot = async () => {
|
||
if (!activeInstance || !editingBot) return;
|
||
if (!editingBot.credentialId) { showToast('Selecione uma credencial', 'err'); return; }
|
||
if (!editingBot.systemPrompt?.trim() || editingBot.systemPrompt.length < 10) {
|
||
showToast('O System Prompt deve ter pelo menos 10 caracteres', 'err'); return;
|
||
}
|
||
|
||
setSavingBot(true);
|
||
const payload = {
|
||
credentialId: editingBot.credentialId,
|
||
name: editingBot.name || 'Assistente IA',
|
||
systemPrompt: editingBot.systemPrompt,
|
||
model: editingBot.model || 'gemini-1.5-flash',
|
||
enabled: editingBot.enabled ?? false,
|
||
triggerMode: editingBot.triggerMode || 'ALL',
|
||
keywords: editingBot.keywords || [],
|
||
};
|
||
try {
|
||
if (editingBotId) {
|
||
await api.updateAIBot(activeInstance.id, editingBotId, payload);
|
||
showToast('Bot atualizado!');
|
||
} else {
|
||
await api.createAIBot(activeInstance.id, payload);
|
||
showToast('Bot criado!');
|
||
}
|
||
setEditingBot(null);
|
||
setEditingBotId(null);
|
||
loadBots(activeInstance.id);
|
||
} catch (err: any) {
|
||
showToast(err.response?.data?.error || err.message || 'Erro ao salvar bot', 'err');
|
||
} finally { setSavingBot(false); }
|
||
};
|
||
|
||
const handleToggleBot = async (bot: AIBot) => {
|
||
if (!activeInstance) return;
|
||
try {
|
||
await api.updateAIBot(activeInstance.id, bot.id, { enabled: !bot.enabled });
|
||
showToast(bot.enabled ? 'Bot desativado' : 'Bot ativado!');
|
||
loadBots(activeInstance.id);
|
||
} catch (err: any) { showToast(err.message || 'Erro', 'err'); }
|
||
};
|
||
|
||
const handleDeleteBot = async (botId: string) => {
|
||
if (!activeInstance || !confirm('Deletar este bot?')) return;
|
||
setDeletingBotId(botId);
|
||
try {
|
||
await api.deleteAIBot(activeInstance.id, botId);
|
||
showToast('Bot removido');
|
||
loadBots(activeInstance.id);
|
||
} catch (err: any) { showToast(err.message || 'Erro ao deletar', 'err'); }
|
||
finally { setDeletingBotId(null); }
|
||
};
|
||
|
||
const patchBot = (patch: Partial<AIBot>) => setEditingBot(prev => prev ? { ...prev, ...patch } : prev);
|
||
|
||
const addKeyword = () => {
|
||
const kw = keywordInput.trim();
|
||
if (!kw) return;
|
||
patchBot({ keywords: [...(editingBot?.keywords || []), kw] });
|
||
setKeywordInput('');
|
||
};
|
||
|
||
const removeKeyword = (i: number) => {
|
||
patchBot({ keywords: (editingBot?.keywords || []).filter((_, idx) => idx !== i) });
|
||
};
|
||
|
||
// ─ Render ─────────────────────────────────────────────────────────────────
|
||
|
||
if (loadingInstances) {
|
||
return (
|
||
<div className="flex items-center justify-center h-64">
|
||
<Loader2 className="w-6 h-6 animate-spin text-brand-400" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-surface p-6 lg:p-10 font-sans">
|
||
<div className="max-w-4xl mx-auto space-y-6">
|
||
|
||
<Toast message={toast?.msg || ''} type={toast?.type === 'ok' ? 'success' : 'error'} visible={!!toast} />
|
||
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||
<div className="flex items-center gap-3">
|
||
<Link href="/dashboard" className="p-2 hover:bg-white/5 rounded-lg transition-colors text-slate-400 hover:text-white">
|
||
<ArrowLeft className="w-5 h-5" />
|
||
</Link>
|
||
<div className="w-10 h-10 bg-gradient-to-br from-brand-500 to-brand-700 rounded-xl flex items-center justify-center text-white shadow-glow-brand">
|
||
<BrainCircuit className="w-6 h-6" />
|
||
</div>
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-white">IA Multi-Agente</h1>
|
||
<p className="text-sm text-slate-500">Gemini Flash · Cérebro Stateful · Micro-prompts</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Instance selector */}
|
||
<div className="relative">
|
||
<button
|
||
onClick={() => setInstanceDropdownOpen(v => !v)}
|
||
className="flex items-center gap-2 px-4 py-2 bg-white/5 border border-white/10 rounded-xl text-sm font-medium text-slate-300 hover:border-brand-500/30 transition-colors"
|
||
>
|
||
<div className={`w-2 h-2 rounded-full ${activeInstance?.status === 'connected' ? 'bg-emerald-400' : 'bg-slate-500'}`} />
|
||
{activeInstance?.nome || 'Selecionar instância'}
|
||
<ChevronDown className={`w-4 h-4 text-slate-400 transition-transform ${instanceDropdownOpen ? 'rotate-180' : ''}`} />
|
||
</button>
|
||
<AnimatePresence>
|
||
{instanceDropdownOpen && (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -8 }}
|
||
className="absolute right-0 top-full mt-1 w-56 glass-elevated rounded-xl shadow-premium py-1 z-50"
|
||
>
|
||
{instances.map(inst => (
|
||
<button key={inst.id} onClick={() => { setActiveInstance(inst); setInstanceDropdownOpen(false); }}
|
||
className={`w-full flex items-center gap-3 px-4 py-2.5 text-sm hover:bg-white/5 transition-colors ${activeInstance?.id === inst.id ? 'text-brand-400 font-semibold' : 'text-slate-300'}`}
|
||
>
|
||
<div className={`w-2 h-2 rounded-full ${inst.status === 'connected' ? 'bg-emerald-400' : 'bg-slate-500'}`} />
|
||
<span className="truncate">{inst.nome}</span>
|
||
{activeInstance?.id === inst.id && <Check className="w-3.5 h-3.5 ml-auto shrink-0" />}
|
||
</button>
|
||
))}
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
</div>
|
||
|
||
{!activeInstance ? (
|
||
<div className="bg-amber-500/10 border border-amber-500/20 rounded-2xl p-6 flex items-center gap-4">
|
||
<AlertCircle className="w-6 h-6 text-amber-400 shrink-0" />
|
||
<p className="text-amber-200 text-sm">Nenhuma instância disponível. <Link href="/sessions" className="underline font-semibold text-amber-300">Conectar WhatsApp →</Link></p>
|
||
</div>
|
||
) : (
|
||
<>
|
||
{/* Info box */}
|
||
<div className="bg-brand-500/5 border border-brand-500/15 rounded-2xl p-4 flex items-start gap-3">
|
||
<Info className="w-5 h-5 text-brand-400 shrink-0 mt-0.5" />
|
||
<div className="text-sm text-slate-400 leading-relaxed">
|
||
<strong className="text-brand-300">Padrão Cérebro:</strong> o bot mantém um resumo de 2 frases por conversa para economizar tokens.
|
||
Micro-prompt de intenção decide automaticamente: <em>resolver</em> (responde) ou <em>escalar</em> (pausa o bot e notifica o atendente).
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── Seção 1: Credenciais ──────────────────────────────────────────── */}
|
||
<SectionCard title="Credenciais de IA" icon={<Key className="w-4 h-4" />}>
|
||
<div className="space-y-3">
|
||
{loadingCreds ? (
|
||
<div className="flex items-center gap-2 text-slate-500 text-sm py-2">
|
||
<Loader2 className="w-4 h-4 animate-spin" /> Carregando...
|
||
</div>
|
||
) : (
|
||
creds.map(cred => (
|
||
<div key={cred.id} className="flex items-center justify-between p-3 bg-white/[0.02] border border-white/5 rounded-xl">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-8 h-8 bg-white/5 rounded-lg flex items-center justify-center">
|
||
<Key className="w-4 h-4 text-slate-400" />
|
||
</div>
|
||
<div>
|
||
<p className="text-sm font-semibold text-white">{cred.name}</p>
|
||
<p className="text-xs text-slate-500">
|
||
<span className={`inline-block px-1.5 py-0.5 rounded text-[10px] font-bold mr-1.5 ${cred.provider === 'GEMINI' ? 'bg-blue-500/15 text-blue-400' : 'bg-emerald-500/15 text-emerald-400'}`}>
|
||
{cred.provider}
|
||
</span>
|
||
••••••••••••••••
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
onClick={() => handleDeleteCred(cred.id)}
|
||
disabled={deletingCredId === cred.id}
|
||
className="p-2 text-slate-500 hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-colors disabled:opacity-50"
|
||
>
|
||
{deletingCredId === cred.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
))
|
||
)}
|
||
|
||
<AnimatePresence>
|
||
{showCredForm && (
|
||
<motion.div
|
||
initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} exit={{ opacity: 0, height: 0 }}
|
||
className="border border-brand-500/20 bg-brand-500/5 rounded-xl p-4 space-y-3"
|
||
>
|
||
<input
|
||
type="text"
|
||
placeholder="Nome (ex: Gemini Produção)"
|
||
value={credName}
|
||
onChange={e => setCredName(e.target.value)}
|
||
className="w-full bg-black/30 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-brand-500/40"
|
||
/>
|
||
{/* Provider selector */}
|
||
<div className="grid grid-cols-2 gap-2">
|
||
{(['GEMINI', 'OPENAI'] as const).map(p => (
|
||
<button key={p} type="button" onClick={() => setCredProvider(p)}
|
||
className={`p-2.5 rounded-lg border text-sm font-semibold transition-all ${credProvider === p ? 'border-brand-500/50 bg-brand-500/10 text-brand-300' : 'border-white/10 text-slate-500 hover:border-white/20'}`}
|
||
>
|
||
{p === 'GEMINI' ? '✦ Gemini' : '⬡ OpenAI'}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<input
|
||
type={showApiKey ? 'text' : 'password'}
|
||
placeholder={credProvider === 'GEMINI' ? 'AIza...' : 'sk-...'}
|
||
value={credApiKey}
|
||
onChange={e => setCredApiKey(e.target.value)}
|
||
className="flex-1 bg-black/30 border border-white/10 rounded-lg px-3 py-2 text-sm font-mono text-white placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-brand-500/40"
|
||
/>
|
||
<button onClick={() => setShowApiKey(v => !v)} className="p-2 text-slate-400 hover:text-white bg-white/5 border border-white/10 rounded-lg transition-colors">
|
||
{showApiKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<Button variant="primary" size="sm" onClick={handleSaveCred}
|
||
disabled={savingCred || !credName.trim() || !credApiKey.trim()} loading={savingCred}
|
||
icon={!savingCred ? <Check className="w-3.5 h-3.5" /> : undefined}
|
||
>Salvar</Button>
|
||
<Button variant="ghost" size="sm" onClick={() => { setShowCredForm(false); setCredName(''); setCredApiKey(''); }}>
|
||
Cancelar
|
||
</Button>
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
{!showCredForm && (
|
||
<button onClick={() => setShowCredForm(true)}
|
||
className="flex items-center gap-2 text-sm text-brand-400 hover:text-brand-300 font-semibold mt-1 transition-colors"
|
||
>
|
||
<Plus className="w-4 h-4" /> Adicionar credencial
|
||
</button>
|
||
)}
|
||
</div>
|
||
</SectionCard>
|
||
|
||
{/* ── Seção 2: Bots ─────────────────────────────────────────────────── */}
|
||
<SectionCard title="Bot Configurado" icon={<Bot className="w-4 h-4" />}>
|
||
<div className="space-y-3">
|
||
{loadingBots ? (
|
||
<div className="flex items-center gap-2 text-slate-500 text-sm py-2">
|
||
<Loader2 className="w-4 h-4 animate-spin" /> Carregando...
|
||
</div>
|
||
) : bots.length === 0 && !editingBot ? (
|
||
<div className="text-sm text-slate-500 py-2">Nenhum bot configurado para esta instância.</div>
|
||
) : (
|
||
bots.map(bot => (
|
||
<div key={bot.id} className="flex items-start justify-between p-4 bg-white/[0.02] border border-white/5 rounded-xl gap-4">
|
||
<div className="flex items-start gap-3 min-w-0">
|
||
<div className="w-8 h-8 bg-brand-500/15 rounded-lg flex items-center justify-center shrink-0 mt-0.5">
|
||
<BrainCircuit className="w-4 h-4 text-brand-400" />
|
||
</div>
|
||
<div className="min-w-0">
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
<span className="text-sm font-semibold text-white">{bot.name}</span>
|
||
<span className="text-xs text-slate-500 font-mono">{bot.model}</span>
|
||
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full border ${bot.enabled ? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20' : 'bg-white/5 text-slate-400 border-white/10'}`}>
|
||
{bot.enabled ? '● Ativo' : '○ Pausado'}
|
||
</span>
|
||
<span className="text-[10px] text-slate-400 bg-white/5 px-2 py-0.5 rounded-full border border-white/10">
|
||
{bot.triggerMode === 'ALL' ? 'Todos os chats' : `Keywords: ${bot.keywords.join(', ')}`}
|
||
</span>
|
||
</div>
|
||
<p className="text-xs text-slate-500 mt-1 line-clamp-1 max-w-md">
|
||
{bot.systemPrompt?.slice(0, 100)}{(bot.systemPrompt?.length || 0) > 100 ? '…' : ''}
|
||
</p>
|
||
{bot.credential && (
|
||
<p className="text-[11px] text-brand-400 mt-0.5">🔑 {bot.credential.name} ({bot.credential.provider})</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-1 shrink-0">
|
||
<button onClick={() => handleToggleBot(bot)} title={bot.enabled ? 'Desativar bot' : 'Ativar bot'}
|
||
className={`p-2 rounded-lg transition-colors ${bot.enabled ? 'text-emerald-400 hover:bg-emerald-500/10' : 'text-slate-500 hover:text-emerald-400 hover:bg-emerald-500/10'}`}>
|
||
<Power className="w-4 h-4" />
|
||
</button>
|
||
<button onClick={() => openEditBot(bot)} className="p-2 text-slate-500 hover:text-brand-400 hover:bg-brand-500/10 rounded-lg transition-colors">
|
||
<Pencil className="w-4 h-4" />
|
||
</button>
|
||
<button onClick={() => handleDeleteBot(bot.id)} disabled={deletingBotId === bot.id}
|
||
className="p-2 text-slate-500 hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-colors disabled:opacity-50">
|
||
{deletingBotId === bot.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
|
||
{!editingBot && bots.length === 0 && (
|
||
<button onClick={openNewBot} disabled={creds.length === 0}
|
||
title={creds.length === 0 ? 'Adicione uma credencial primeiro' : undefined}
|
||
className="flex items-center gap-2 text-sm text-brand-400 hover:text-brand-300 font-semibold mt-1 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||
>
|
||
<Plus className="w-4 h-4" /> Criar bot
|
||
</button>
|
||
)}
|
||
</div>
|
||
</SectionCard>
|
||
|
||
{/* ── Seção 3: Formulário de Bot ────────────────────────────────────── */}
|
||
<AnimatePresence>
|
||
{editingBot && (
|
||
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 16 }}>
|
||
<SectionCard title={editingBotId ? 'Editar Bot' : 'Novo Bot'} icon={<Settings className="w-4 h-4" />}>
|
||
<div className="space-y-6">
|
||
|
||
{/* Nome */}
|
||
<div>
|
||
<label className="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-1.5">
|
||
Nome do Bot
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={editingBot.name || ''}
|
||
onChange={e => patchBot({ name: e.target.value })}
|
||
placeholder="Assistente IA"
|
||
className="w-full bg-black/30 border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:ring-2 focus:ring-brand-500/40"
|
||
/>
|
||
</div>
|
||
|
||
{/* Credencial */}
|
||
<div>
|
||
<label className="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-1.5">
|
||
Credencial de IA <span className="text-red-400">*</span>
|
||
</label>
|
||
<select
|
||
value={editingBot.credentialId || ''}
|
||
onChange={e => {
|
||
const cred = creds.find(c => c.id === e.target.value);
|
||
const defaultModel = cred?.provider === 'OPENAI' ? 'gpt-4o-mini' : 'gemini-1.5-flash';
|
||
patchBot({ credentialId: e.target.value, model: defaultModel });
|
||
}}
|
||
className="w-full bg-black/30 border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:ring-2 focus:ring-brand-500/40"
|
||
>
|
||
<option value="">Selecionar credencial...</option>
|
||
{creds.map(c => <option key={c.id} value={c.id}>{c.name} ({c.provider})</option>)}
|
||
</select>
|
||
</div>
|
||
|
||
{/* Modelo */}
|
||
<div>
|
||
<label className="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-1.5">
|
||
Modelo
|
||
</label>
|
||
<select
|
||
value={editingBot.model || 'gemini-1.5-flash'}
|
||
onChange={e => patchBot({ model: e.target.value })}
|
||
className="w-full bg-black/30 border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:ring-2 focus:ring-brand-500/40"
|
||
>
|
||
{modelOptions.map(m => <option key={m.value} value={m.value}>{m.label}</option>)}
|
||
</select>
|
||
</div>
|
||
|
||
{/* System Prompt */}
|
||
<div>
|
||
<div className="flex items-center justify-between mb-1.5">
|
||
<label className="text-xs font-bold text-slate-500 uppercase tracking-wide">
|
||
System Prompt (Cérebro) <span className="text-red-400">*</span>
|
||
</label>
|
||
<span className="text-[11px] text-slate-600">{editingBot.systemPrompt?.length || 0} chars</span>
|
||
</div>
|
||
<div className="bg-brand-500/5 border border-brand-500/20 rounded-xl p-1">
|
||
<textarea
|
||
rows={10}
|
||
placeholder={`Você é o assistente virtual da empresa X. Responda perguntas sobre nossos produtos e serviços de forma amigável e direta. Se o cliente tiver um problema complexo que exige análise, escale para um atendente humano.`}
|
||
value={editingBot.systemPrompt || ''}
|
||
onChange={e => patchBot({ systemPrompt: e.target.value })}
|
||
className="w-full bg-transparent px-3 py-2.5 text-sm text-white placeholder:text-slate-600 focus:outline-none resize-none leading-relaxed"
|
||
/>
|
||
</div>
|
||
<p className="text-[11px] text-slate-600 mt-1.5 flex items-center gap-1">
|
||
<Info className="w-3 h-3" />
|
||
Define personalidade, conhecimento e quando escalar para humano. O bot mantém um resumo de 2 frases (Cérebro) para economizar tokens.
|
||
</p>
|
||
</div>
|
||
|
||
{/* Trigger Mode */}
|
||
<div>
|
||
<label className="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-1.5">
|
||
<Zap className="w-3 h-3 inline mr-1" /> Ativar bot para
|
||
</label>
|
||
<div className="grid grid-cols-2 gap-2">
|
||
{[
|
||
{ value: 'ALL', label: 'Todos os chats', desc: 'Responde qualquer mensagem recebida' },
|
||
{ value: 'KEYWORD', label: 'Keyword', desc: 'Só quando contém palavra-chave' },
|
||
].map(opt => (
|
||
<button key={opt.value} type="button"
|
||
onClick={() => patchBot({ triggerMode: opt.value as AIBot['triggerMode'] })}
|
||
className={`p-3 rounded-xl border text-left transition-all ${editingBot.triggerMode === opt.value ? 'border-brand-500/40 bg-brand-500/10 text-brand-300' : 'border-white/10 bg-white/[0.02] text-slate-400 hover:border-white/20'}`}
|
||
>
|
||
<p className="text-xs font-bold">{opt.label}</p>
|
||
<p className="text-[10px] text-slate-600 mt-0.5 leading-tight">{opt.desc}</p>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{editingBot.triggerMode === 'KEYWORD' && (
|
||
<div className="mt-3 space-y-2">
|
||
<div className="flex items-center gap-2">
|
||
<input
|
||
type="text"
|
||
placeholder="Adicionar palavra-chave (Enter para confirmar)"
|
||
value={keywordInput}
|
||
onChange={e => setKeywordInput(e.target.value)}
|
||
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); addKeyword(); } }}
|
||
className="flex-1 bg-black/30 border border-white/10 rounded-lg px-3 py-2 text-sm text-white placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-brand-500/40"
|
||
/>
|
||
<Button variant="ghost" size="sm" onClick={addKeyword}>Adicionar</Button>
|
||
</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
{(editingBot.keywords || []).map((kw, i) => (
|
||
<span key={i} className="flex items-center gap-1.5 bg-brand-500/10 border border-brand-500/20 text-brand-300 text-xs px-2.5 py-1 rounded-full">
|
||
{kw}
|
||
<button onClick={() => removeKeyword(i)} className="hover:text-red-400 transition-colors">×</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Enabled */}
|
||
<div className="flex items-center justify-between p-3 bg-white/[0.02] border border-white/5 rounded-xl">
|
||
<div className="flex items-center gap-3">
|
||
{editingBot.enabled
|
||
? <UserCheck className="w-5 h-5 text-emerald-400" />
|
||
: <UserX className="w-5 h-5 text-slate-500" />
|
||
}
|
||
<div>
|
||
<p className="text-sm font-medium text-slate-300">Bot ativo</p>
|
||
<p className="text-[11px] text-slate-600">Bot responderá automaticamente ao receber mensagens</p>
|
||
</div>
|
||
</div>
|
||
<Toggle value={!!editingBot.enabled} onChange={v => patchBot({ enabled: v })} />
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="flex items-center justify-between pt-4 border-t border-white/5">
|
||
<Button variant="ghost" onClick={() => { setEditingBot(null); setEditingBotId(null); }}>
|
||
Cancelar
|
||
</Button>
|
||
<Button variant="primary" onClick={handleSaveBot}
|
||
disabled={savingBot || !editingBot.credentialId || !editingBot.systemPrompt?.trim()}
|
||
loading={savingBot}
|
||
icon={!savingBot ? <Save className="w-4 h-4" /> : undefined}
|
||
>
|
||
{editingBotId ? 'Salvar alterações' : 'Criar bot'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</SectionCard>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|