Files
clube67_newwhats.local/clube67/newwhats.local/frontend/pages/bots/index.tsx
T
VPS 4 Deploy Agent 2f8c04a0a7
continuous-integration/webhook Falha no deploy de clube67_newwhats.local (VPS 4)
chore(ops): restore all source files in newwhats.clube67.com
2026-05-18 03:28:29 +02:00

671 lines
43 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}