/* global React, Icon, API */
const AGENT_PROFILE_DIRTY_FIELDS = new Set();
let AGENT_PROFILE_SYNCING = false;
function registerAgentProfileDirtyTracking() {
AGENT_PROFILE_DIRTY_FIELDS.clear();
}
function setAgentProfileControlValue(id, value, prop = 'value') {
const el = document.getElementById(id);
if (!el || AGENT_PROFILE_DIRTY_FIELDS.has(id)) return;
if (prop === 'checked') el.checked = !!value;
else el.value = value == null ? '' : value;
}
window.AGENT_PROFILE_DIRTY_FIELDS = AGENT_PROFILE_DIRTY_FIELDS;
window.registerAgentProfileDirtyTracking = registerAgentProfileDirtyTracking;
window.setAgentProfileControlValue = setAgentProfileControlValue;
const studioToast = window.studioToast || function(m, t) { if (window.STUDIO?.toast) window.STUDIO.toast(m, t); };
const studioForm = window.studioForm || function(c) { return window.STUDIO?.form ? window.STUDIO.form(c) : Promise.resolve(null); };
const studioConfirm = window.studioConfirm || function(c) { return window.STUDIO?.confirm ? window.STUDIO.confirm(c) : Promise.resolve(false); };
async function copyToClipboard(text) {
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {}
}
const ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', '');
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand('copy');
document.body.removeChild(ta);
return ok;
}
function setupBadgeClass(status) {
if (status === 'production') return 'ok';
if (status === 'paused') return 'warn';
if (status === 'ready') return 'accent';
return 'warn';
}
const SETUP_STAGE_META = {
created: { tab: 'instructions', title: 'Bot criado', body: 'Nome e estrutura inicial existem. Você pode ajustar depois.' },
personality: { tab: 'instructions', title: 'Personalidade', body: 'Defina como o bot fala, quais limites segue e qual IA usa.' },
goals: { tab: 'instructions', title: 'Objetivos', body: 'Liste o que o bot precisa conseguir durante a conversa.' },
kb: { tab: 'knowledge', title: 'Conhecimento', body: 'Adicione documentos, URLs ou textos que o bot deve consultar.' },
channels: { tab: 'connections', title: 'WhatsApp ou API', body: 'Receba mensagens reais por WhatsApp, API ou site. Conecte WhatsApp quando quiser repassar informacoes para alvos selecionados.' },
tested: { tab: 'test', title: 'Teste', body: 'Teste antes de publicar. Use o sandbox para validar tom e respostas.' },
review: { tab: 'setup', title: 'Pendências', body: 'Revise bloqueios obrigatórios e recomendações antes de publicar.' },
publish: { tab: 'setup', title: 'Publicar', body: 'Libera o bot para responder mensagens reais. Pode pausar depois.' },
};
function parseKbSources(text) {
return String(text || '').split(/\n+/).map(line => line.trim()).filter(Boolean).map(line => {
if (/^https?:\/\//i.test(line)) return { source_type: 'url', uri: line };
return { source_type: 'text', content: line };
});
}
function readKbFiles(files) {
const list = Array.from(files || []);
return Promise.all(list.map(file => new Promise(resolve => {
const reader = new FileReader();
reader.onload = () => resolve({ source_type: 'file', uri: file.name, mime_type: file.type || 'application/octet-stream', content: reader.result });
reader.onerror = () => resolve(null);
reader.readAsDataURL(file);
}))).then(items => items.filter(Boolean));
}
function normalizePublicSlugInput(value) {
return String(value || '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.replace(/-{2,}/g, '-')
.slice(0, 80);
}
window.AgentProfilePage = function AgentProfilePage({ agentId, onBack, initialTab, onTabChange }) {
const agent = (window.MOCK.agents || []).find(a => String(a.id) === String(agentId)) || (window.MOCK.agents || [])[0];
const [tab, setTab] = React.useState(initialTab || 'setup');
const [publicLinkInfo, setPublicLinkInfo] = React.useState(null);
const [publicLinkBusy, setPublicLinkBusy] = React.useState('');
React.useEffect(() => {
if (!agent) return;
setTab(initialTab || (agent.isPublished ? 'instructions' : 'setup'));
}, [agent?.id, initialTab]);
if (!agent) {
return (
<>
Teste do agente
{agent.name} - sessão interna
Abrir testador
Excluir testes
{sessionBusy ? 'Aguarde...' : 'Nova sessão'}
O testador automático fica em Assistentes para comparar cenários e enviar melhorias ao Otimizador.
Ir para testador
interno
sem canal externo
{sessionId.slice(0, 18)}...
{messages.map(item => (
{item.text}
{item.role === 'assistant' ? 'agente - ' : item.role === 'system' ? 'sistema - ' : 'teste - '}{item.time}
))}
{sending && (
)}
{!messages.length &&
Envie uma mensagem para testar este agente.
}
);
}
function AgentAutoList({ title, items }) {
const list = Array.isArray(items) ? items : [];
return (
{title}
{list.length ? list.slice(0, 4).map((item, idx) =>
{typeof item === 'string' ? item : window.prettyStudioValue?.(item) || String(item)}
) :
Nenhum item.
}
);
}
function ConnectionsTab({ agent }) {
const [qrData, setQrData] = React.useState(null);
const [qrLoading, setQrLoading] = React.useState(false);
const [qrError, setQrError] = React.useState('');
const token = agent.raw?.instance_token || agent.channel;
const inst = (window.MOCK?.instances || []).find(item => item.id === token || item.name === token || item.raw?.name === token);
const waInfo = whatsappInstanceUiState(inst, token);
React.useEffect(() => {
setQrData(null);
setQrError('');
}, [agent.id]);
async function loadQr() {
setQrLoading(true);
setQrError('');
try {
setQrData(await API.getQr(agent.id));
} catch (err) {
setQrError(err.message || String(err));
setQrData(null);
} finally {
setQrLoading(false);
}
}
return (
Conexões do agente
Site e WhatsApp podem ficar ativos ao mesmo tempo.
Site disponível
WhatsApp {waInfo.label}
Site -> WhatsApp
Quando uma conversa do site indicar negócio fechado, a IA gera um resumo com LLM e envia pelo WhatsApp do agente para os alvos configurados em Ferramentas.
);
}
const SHARED_CHAT_PRESET_LIMITS = {
low: { max_messages_per_conversation: 10, max_chars_per_message: 500, max_total_chars_per_conversation: 3000 },
medium: { max_messages_per_conversation: 30, max_chars_per_message: 1000, max_total_chars_per_conversation: 10000 },
high: { max_messages_per_conversation: 100, max_chars_per_message: 2000, max_total_chars_per_conversation: 30000 },
};
function sharedChatConfigToForm(config) {
const raw = config || {};
const limits = raw.limits || SHARED_CHAT_PRESET_LIMITS.medium;
const messages = raw.messages || {};
const ui = raw.ui || {};
const contact = raw.contact || {};
return {
preset: raw.preset || 'medium',
maxMessages: String(limits.max_messages_per_conversation || 30),
maxChars: String(limits.max_chars_per_message || 1000),
maxTotalChars: String(limits.max_total_chars_per_conversation || 10000),
showCounter: ui.show_character_counter !== false,
showWarning: ui.show_near_limit_warning !== false,
nearLimit: messages.near_limit || 'Você está chegando ao limite de mensagens desta conversa.',
limitReached: messages.limit_reached || 'Este chat atingiu o limite de uso configurado. Para continuar o atendimento, entre em contato com nossa equipe.',
charLimit: messages.char_limit || 'Sua mensagem ultrapassou o limite de caracteres permitido. Reduza o texto para continuar.',
contactEnabled: contact.enabled !== false,
allowErrorReport: contact.allow_error_report !== false,
requireName: !!contact.require_name,
requireEmail: contact.require_email !== false,
requirePhone: !!contact.require_phone,
destinationEmail: contact.destination_email || '',
};
}
function SharedChatSecurityPanel({ agent }) {
const initialConfig = agent.sharedChatConfig || agent.raw?.shared_chat_config || {};
const [form, setForm] = React.useState(() => sharedChatConfigToForm(initialConfig));
const [saving, setSaving] = React.useState(false);
React.useEffect(() => {
setForm(sharedChatConfigToForm(agent.sharedChatConfig || agent.raw?.shared_chat_config || {}));
}, [agent.id, JSON.stringify(agent.sharedChatConfig || agent.raw?.shared_chat_config || {})]);
function setField(name, value) {
setForm(prev => ({ ...prev, [name]: value }));
}
function numericField(label, name, min, max) {
return (
{label}
setField(name, e.target.value)}/>
);
}
function payload() {
return {
preset: form.preset,
limits: {
max_messages_per_conversation: Number(form.maxMessages),
max_chars_per_message: Number(form.maxChars),
max_total_chars_per_conversation: Number(form.maxTotalChars),
},
ui: {
show_character_counter: !!form.showCounter,
show_near_limit_warning: !!form.showWarning,
},
messages: {
near_limit: form.nearLimit,
limit_reached: form.limitReached,
char_limit: form.charLimit,
},
contact: {
enabled: !!form.contactEnabled,
allow_error_report: !!form.allowErrorReport,
require_name: !!form.requireName,
require_email: !!form.requireEmail,
require_phone: !!form.requirePhone,
destination_email: form.destinationEmail,
request_types: ['Dúvida', 'Erro no chat', 'Resposta incorreta', 'Quero falar com uma pessoa', 'Problema técnico', 'Sugestão', 'Sugestão de melhoria', 'Outro'],
},
};
}
async function save() {
setSaving(true);
try {
await API.updateAgent(agent.id, { shared_chat_config: payload() });
await window.STUDIO.refresh();
studioToast('Limites do chat compartilhável salvos.');
} catch (err) {
studioToast(err.message || String(err), 'bad');
} finally {
setSaving(false);
}
}
const presetLimits = SHARED_CHAT_PRESET_LIMITS[form.preset] || {
max_messages_per_conversation: form.maxMessages,
max_chars_per_message: form.maxChars,
max_total_chars_per_conversation: form.maxTotalChars,
};
return (
Limites e segurança
Configuração aplicada somente ao Chat Compartilhado.
{saving ? 'Salvando...' : 'Salvar limites'}
Uso
Preset
setField('preset', e.target.value)}>
Baixo
Médio
Alto
Personalizado
{form.preset === 'custom' ? (
{numericField('Máximo de mensagens por conversa', 'maxMessages', 1, 500)}
{numericField('Caracteres por mensagem', 'maxChars', 50, 10000)}
{numericField('Caracteres totais por conversa', 'maxTotalChars', 100, 200000)}
) : (
{presetLimits.max_messages_per_conversation} mensagens
{presetLimits.max_chars_per_message} caracteres por mensagem
{presetLimits.max_total_chars_per_conversation} caracteres totais
)}
setField('showCounter', e.target.checked)}/> Mostrar contador de caracteres
setField('showWarning', e.target.checked)}/> Avisar próximo do limite
Mensagens
Aviso próximo do limite
Limite atingido
Limite por mensagem
Contato
setField('contactEnabled', e.target.checked)}/> Habilitar Fale Conosco
setField('allowErrorReport', e.target.checked)}/> Habilitar Encontrou erro?
setField('requireName', e.target.checked)}/> Exigir nome
setField('requireEmail', e.target.checked)}/> Exigir e-mail
setField('requirePhone', e.target.checked)}/> Exigir telefone
Destino interno opcional
setField('destinationEmail', e.target.value)} placeholder="equipe@empresa.com"/>
Tipos: Dúvida, Erro no chat, Resposta incorreta, Quero falar com uma pessoa, Problema técnico, Sugestão, Sugestão de melhoria e Outro.
);
}
function WhatsappConnectionPanel({ agent, qrData, loading, error, onRefresh, statusInfo }) {
const status = statusInfo || { label: 'Sem conexão', tag: 'warn' };
return (
{error ? 'Falha ao gerar QR' : qrData ? 'QR pronto' : loading ? 'Gerando QR...' : `WhatsApp ${status.label}`}
{loading ? 'Gerando...' : 'Gerar QR'}
{agent.raw?.instance_token || agent.channel}
{qrData?.base64 || qrData?.qrcode
?
:
{error || (loading ? 'Gerando QR Code...' : 'Clique para gerar QR.')}
}
WhatsApp > Aparelhos conectados > Conectar aparelho. Se expirar, gere outro QR.
);
}
function KnowledgeTab({ agent }) {
const [kbSources, setKbSources] = React.useState([]);
const [sourceText, setSourceText] = React.useState('');
const [sourceFiles, setSourceFiles] = React.useState([]);
const [sourceDetail, setSourceDetail] = React.useState(null);
const [loadingSourceId, setLoadingSourceId] = React.useState(null);
const docCount = kbSources.reduce((sum, item) => sum + Number(item.document_count || 0), 0);
const chunkCount = kbSources.reduce((sum, item) => sum + Number(item.chunk_count || 0), 0);
async function loadKbSources() {
try {
setKbSources(await API.getKbSources(agent.id));
} catch (err) {
setKbSources([]);
}
}
React.useEffect(() => { loadKbSources(); }, [agent.id]);
async function addSources() {
const fileSources = await readKbFiles(sourceFiles);
const sources = [...parseKbSources(sourceText), ...fileSources];
if (!sources.length) {
studioToast('Cole uma URL, texto ou anexe um arquivo.', 'bad');
return;
}
try {
await API.addKbSource(agent.id, { sources });
setSourceText('');
setSourceFiles([]);
await loadKbSources();
await window.STUDIO.refresh();
studioToast('Fonte adicionada a base.');
} catch (err) {
studioToast(err.message || String(err), 'bad');
}
}
async function openSourceData(source) {
if (!source?.id) return;
setLoadingSourceId(source.id);
try {
setSourceDetail(await API.getKbSource(agent.id, source.id));
} finally {
setLoadingSourceId(null);
}
}
return (
<>
Conhecimento do agente
Cole URLs, textos ou anexe arquivos. A base técnica e criada automaticamente.
addSources().catch(err => studioToast(err.message || String(err), 'bad'))}> Adicionar fontes
URLs ou texto direto
Arquivos
setSourceFiles(Array.from(e.target.files || []))}/>
{sourceFiles.length ? `${sourceFiles.length} arquivo(s) selecionado(s)` : 'PDF, DOCX ou texto.'}
Fonte Tipo Status Docs Chunks Criada Ações
{kbSources.map(s => (
{s.uri || '--'}
{s.source_type || 'text'}
{s.status || 'ingested'}
{s.document_count || 0}
{s.chunk_count || 0}
{window.fmtShortDate ? window.fmtShortDate(s.created_at) : '--'}
openSourceData(s)} disabled={loadingSourceId === s.id}>{loadingSourceId === s.id ? 'Abrindo...' : 'Dados'}
))}
{!kbSources.length && Nenhuma fonte indexada ainda. }
{sourceDetail && (
Dados da fonte
Fonte {sourceDetail.uri || `Fonte ${sourceDetail.id}`}
Chunks carregados {sourceDetail.chunks_returned || 0} de {sourceDetail.chunk_count || 0}
{(sourceDetail.documents || []).map(doc => (
{doc.title || `Documento ${doc.id}`}
{(doc.chunks || []).map(chunk => (
{chunk.content}
))}
))}
)}
>
);
}
function normalizeWhatsAppTargetId(value, kind) {
const raw = String(value || '').trim();
if (!raw) return '';
let compact = raw.replace(/\s+/g, '');
const lowered = compact.toLowerCase();
if (lowered.endsWith('@g.us')) return lowered;
if (compact.includes('@') && String(kind || '').toLowerCase() !== 'group') {
compact = compact.split('@')[0];
}
const digits = compact.replace(/\D+/g, '');
if (digits && String(kind || '').toLowerCase() !== 'group') return digits;
return compact.includes('@') ? compact.toLowerCase() : compact;
}
function normalizeWaTargetItem(item, fallbackKind) {
const kind = item?.kind || fallbackKind || (String(item?.id || '').toLowerCase().endsWith('@g.us') ? 'group' : 'contact');
const id = normalizeWhatsAppTargetId(item?.id || item?.target_id || item?.number, kind);
if (!id) return null;
return { ...item, id, kind, name: item?.name || item?.target_name || id };
}
function uniqueWaTargets(items) {
const out = [];
const seen = new Set();
(items || []).forEach(item => {
const normalized = normalizeWaTargetItem(item);
if (!normalized || seen.has(normalized.id)) return;
seen.add(normalized.id);
out.push(normalized);
});
return out;
}
function ToolsTab({ agent }) {
const tools = (window.MOCK.tools || []).filter(item => String(item.agent_id) === String(agent.id));
const notifyTool = tools.find(item => item.tool_type === 'whatsapp_notify');
const [waTargets, setWaTargets] = React.useState({ contacts: [], groups: [] });
const [waSearchTargets, setWaSearchTargets] = React.useState(null);
const [waTargetError, setWaTargetError] = React.useState('');
const [waTargetStatus, setWaTargetStatus] = React.useState('Preparando agenda...');
const [waTargetsLoading, setWaTargetsLoading] = React.useState(true);
const [waSearchLoading, setWaSearchLoading] = React.useState(false);
const [waTargetQuery, setWaTargetQuery] = React.useState('');
const [notifyTargetId, setNotifyTargetId] = React.useState('');
const [notifyTargetIds, setNotifyTargetIds] = React.useState([]);
const [notifyManualId, setNotifyManualId] = React.useState('');
const [notifyTriggerMode, setNotifyTriggerMode] = React.useState(notifyTool?.config?.trigger_mode || 'handoff_or_qualified');
const [savingNotify, setSavingNotify] = React.useState(false);
const currentNotifyTarget = notifyTool?.config?.target_id
? [{
id: notifyTool.config.target_id,
name: notifyTool.config.target_name || notifyTool.config.target_id,
kind: notifyTool.config.target_kind || (String(notifyTool.config.target_id).toLowerCase().endsWith('@g.us') ? 'group' : 'contact'),
source: 'config',
}]
: [];
const sourceWaTargets = waSearchTargets || waTargets;
const allWaTargets = React.useMemo(() => uniqueWaTargets([
...(sourceWaTargets.contacts || []).map(item => ({ ...item, kind: 'contact' })),
...(sourceWaTargets.groups || []).map(item => ({ ...item, kind: 'group' })),
...currentNotifyTarget,
]), [sourceWaTargets.contacts, sourceWaTargets.groups, notifyTool?.config?.target_id, notifyTool?.config?.target_name, notifyTool?.config?.target_kind]);
const targetQuery = waTargetQuery.trim().toLowerCase();
let visibleWaTargets = React.useMemo(() => allWaTargets
.filter(item => !targetQuery || `${item.name || ''} ${item.id || ''}`.toLowerCase().includes(targetQuery))
.slice(0, 250), [allWaTargets, targetQuery]);
const selectedVisible = allWaTargets.find(item => String(item.id) === String(notifyTargetId));
if (selectedVisible && !visibleWaTargets.some(item => item.id === selectedVisible.id)) {
visibleWaTargets = [selectedVisible, ...visibleWaTargets];
}
React.useEffect(() => {
let alive = true;
setWaTargetError('');
setWaTargetsLoading(true);
setWaTargetStatus('Carregando agenda WhatsApp...');
setWaSearchTargets(null);
setNotifyTargetId(normalizeWhatsAppTargetId(notifyTool?.config?.target_id || '', notifyTool?.config?.target_kind));
setNotifyTargetIds(uniqueWaTargets(notifyTool?.config?.targets || []).map(item => item.id));
setNotifyTriggerMode(notifyTool?.config?.trigger_mode || 'handoff_or_qualified');
setNotifyManualId('');
const started = performance.now();
API.getWhatsappTargets(agent.id)
.then(data => {
if (!alive) return;
setWaTargets(data || { contacts: [], groups: [] });
setWaTargetError(Array.isArray(data?.errors) && data.errors.length ? data.errors.join(' | ') : '');
const total = (data?.contacts?.length || 0) + (data?.groups?.length || 0);
const ms = Math.max(1, Math.round(performance.now() - started));
setWaTargetStatus(`${total} alvos carregados em ${ms}ms${data?.cached ? ' (cache)' : ''}.`);
})
.catch(err => {
if (!alive) return;
setWaTargets({ contacts: [], groups: [] });
setWaTargetError(err.message || String(err));
setWaTargetStatus('Falha ao carregar agenda.');
})
.finally(() => {
if (alive) setWaTargetsLoading(false);
});
return () => { alive = false; };
}, [agent.id, notifyTool?.id]);
React.useEffect(() => {
const query = waTargetQuery.trim();
if (query.length < 2) {
setWaSearchTargets(null);
setWaSearchLoading(false);
return undefined;
}
let alive = true;
const timer = setTimeout(() => {
const started = performance.now();
setWaSearchLoading(true);
setWaTargetStatus(`Pesquisando "${query}"...`);
API.getWhatsappTargets(agent.id, query)
.then(data => {
if (!alive) return;
setWaSearchTargets(data || { contacts: [], groups: [] });
setWaTargetError(Array.isArray(data?.errors) && data.errors.length ? data.errors.join(' | ') : '');
const total = (data?.contacts?.length || 0) + (data?.groups?.length || 0);
const ms = Math.max(1, Math.round(performance.now() - started));
setWaTargetStatus(`${total} resultado(s) para "${query}" em ${ms}ms${data?.cached ? ' (cache)' : ''}.`);
})
.catch(err => {
if (!alive) return;
setWaTargetError(err.message || String(err));
setWaTargetStatus(`Pesquisa de "${query}" falhou.`);
})
.finally(() => {
if (alive) setWaSearchLoading(false);
});
}, 280);
return () => {
alive = false;
clearTimeout(timer);
};
}, [agent.id, waTargetQuery]);
async function saveWhatsappNotifyTool() {
const selectedTargets = notifyTargetIds
.map(id => allWaTargets.find(item => String(item.id) === String(id)) || normalizeWaTargetItem({ id }))
.filter(Boolean);
const manualId = normalizeWhatsAppTargetId(notifyManualId);
if (manualId && !selectedTargets.some(item => item.id === manualId)) {
selectedTargets.push({ id: manualId, name: manualId, kind: manualId.endsWith('@g.us') ? 'group' : 'contact' });
}
const targets = uniqueWaTargets(selectedTargets);
if (!targets.length) {
studioToast('Selecione ou informe ao menos um número/grupo para notificar.', 'bad');
return;
}
const primary = targets[0];
const payload = {
name: 'Notificar humano WhatsApp',
agent_id: agent.id,
tool_type: 'whatsapp_notify',
description: 'Envia resumo de lead, negócio fechado ou repasse para humano no WhatsApp selecionado.',
enabled: true,
config: {
target_id: primary.id,
target_name: primary.name || primary.id,
target_kind: primary.kind,
targets,
allow_dynamic_target: false,
trigger_mode: notifyTriggerMode,
},
};
try {
setSavingNotify(true);
if (notifyTool) await API.updateTool(notifyTool.id, payload);
else await API.createTool(payload);
await window.STUDIO.refresh();
studioToast('Notificacao WhatsApp configurada.');
} catch (err) {
studioToast(err.message || String(err), 'bad');
} finally {
setSavingNotify(false);
}
}
function addNotifyTarget() {
const normalized = normalizeWhatsAppTargetId(notifyTargetId);
if (!normalized) return;
setNotifyTargetIds(prev => prev.includes(normalized) ? prev : [...prev, normalized]);
}
function removeNotifyTarget(id) {
setNotifyTargetIds(prev => prev.filter(item => item !== id));
}
async function createTool() {
const values = await studioForm({
title: 'Nova ferramenta do agente',
description: 'Esta ferramenta aparece somente neste perfil.',
submitLabel: 'Criar ferramenta',
fields: [
{ name: 'name', label: 'Nome', required: true, placeholder: 'Consultar base' },
{
name: 'tool_type',
label: 'Tipo',
type: 'select',
value: 'knowledge_base',
options: [
{ value: 'knowledge_base', label: 'Knowledge base' },
{ value: 'web_search', label: 'Web search' },
{ value: 'data_table', label: 'Data table' },
{ value: 'ticket_creation', label: 'Ticket creation' },
{ value: 'whatsapp_notify', label: 'WhatsApp notify' },
{ value: 'contextual_memory', label: 'Contextual memory' },
],
},
{ name: 'description', label: 'Descrição', type: 'textarea' },
],
});
if (!values?.name?.trim()) return;
try {
await API.post('/api/tools', {
name: values.name.trim(),
agent_id: agent.id,
tool_type: values.tool_type || 'knowledge_base',
description: values.description || '',
enabled: true,
});
await window.STUDIO.refresh();
studioToast('Ferramenta vinculada ao agente.');
} catch (err) {
studioToast(err.message || String(err), 'bad');
}
}
function toggle(tool) {
API.put(`/api/tools/${tool.id}`, { enabled: !tool.enabled })
.then(() => window.STUDIO.refresh())
.catch(err => studioToast(err.message || String(err), 'bad'));
}
async function editTool(tool) {
const values = await studioForm({
title: 'Editar ferramenta',
submitLabel: 'Salvar ferramenta',
fields: [
{ name: 'name', label: 'Nome', required: true, value: tool.name || '' },
{
name: 'tool_type',
label: 'Tipo',
type: 'select',
value: tool.tool_type || 'knowledge_base',
options: [
{ value: 'knowledge_base', label: 'Knowledge base' },
{ value: 'web_search', label: 'Web search' },
{ value: 'data_table', label: 'Data table' },
{ value: 'ticket_creation', label: 'Ticket creation' },
{ value: 'whatsapp_notify', label: 'WhatsApp notify' },
{ value: 'contextual_memory', label: 'Contextual memory' },
],
},
{ name: 'description', label: 'Descrição', type: 'textarea', value: tool.description || '' },
{ name: 'enabled', label: 'Ativa', type: 'select', value: tool.enabled ? 'true' : 'false', options: [{ value: 'true', label: 'Ativa' }, { value: 'false', label: 'Inativa' }] },
],
});
if (!values?.name?.trim()) return;
try {
await API.updateTool(tool.id, {
name: values.name.trim(),
tool_type: values.tool_type || tool.tool_type,
description: values.description || '',
enabled: values.enabled !== 'false',
});
await window.STUDIO.refresh();
studioToast('Ferramenta atualizada.');
} catch (err) {
studioToast(err.message || String(err), 'bad');
}
}
async function deleteTool(tool) {
if (!(await studioConfirm({ title: 'Excluir ferramenta', message: `Excluir "${tool.name}" deste agente?`, confirmLabel: 'Excluir', danger: true }))) return;
try {
await API.deleteTool(tool.id);
await window.STUDIO.refresh();
studioToast('Ferramenta excluida.');
} catch (err) {
studioToast(err.message || String(err), 'bad');
}
}
return (
<>
Repasse humano para alvos *
O agente pode receber mensagens por WhatsApp, API ou site. Para repassar informacoes a alvos, conecte uma conta WhatsApp.
{(waTargetsLoading || waSearchLoading) && }
{waTargetsLoading ? 'carregando' : waSearchLoading ? 'pesquisando' : waTargetError ? 'parcial' : 'pronto'}
{savingNotify ? 'Salvando...' : 'Salvar repasse'}
{tools.map(tool =>
toggle(tool)} onEdit={() => editTool(tool)} onDelete={() => deleteTool(tool)}/>)}
{!tools.length && Nenhuma ferramenta ligada a este agente.
}
Ferramentas do backend
Nova ferramenta
>
);
}
function ConditionalsTab({ agent }) {
const [rows, setRows] = React.useState([]);
const [loading, setLoading] = React.useState(true);
function load() {
setLoading(true);
API.getConditionalPrompts(agent.id)
.then(items => setRows(Array.isArray(items) ? items : []))
.catch(() => setRows([]))
.finally(() => setLoading(false));
}
React.useEffect(load, [agent.id]);
async function createConditional() {
const values = await studioForm({
title: 'Nova regra condicional',
description: 'Injeta prompt extra quando a condicao bater.',
submitLabel: 'Criar regra',
fields: [
{ name: 'name', label: 'Nome da regra', required: true, placeholder: 'Urgencia' },
{
name: 'condition_type',
label: 'Tipo',
type: 'select',
value: 'intent',
options: [
{ value: 'intent', label: 'Intent' },
{ value: 'regex', label: 'Regex' },
{ value: 'state_equals', label: 'State equals' },
{ value: 'always', label: 'Always' },
],
},
{ name: 'condition_value', label: 'Condicao', placeholder: 'pedido urgente' },
{ name: 'system_prompt', label: 'Prompt a injetar', type: 'textarea', value: 'Ajuste a resposta para este contexto.' },
],
});
if (!values?.name?.trim()) return;
API.createConditional(agent.id, {
name: values.name.trim(),
condition_type: values.condition_type || 'intent',
condition_value: values.condition_value || values.name.trim(),
system_prompt: values.system_prompt || '',
enabled: true,
}).then(() => {
studioToast('Regra criada.');
load();
}).catch(err => studioToast(err.message || String(err), 'bad'));
}
function toggle(row) {
API.updateConditional(row.id, { enabled: !row.enabled }).then(load).catch(err => studioToast(err.message || String(err), 'bad'));
}
async function remove(row) {
if (!(await studioConfirm({ title: 'Excluir regra', message: 'Excluir prompt condicional?', confirmLabel: 'Excluir', danger: true }))) return;
API.deleteConditional(row.id).then(load).catch(err => studioToast(err.message || String(err), 'bad'));
}
return (
intent, regex, state_equals, always
Prompts condicionais
Nova regra
Regra Tipo Condicao Prompt Status
{rows.map(row => (
{row.name}
{row.condition_type || '--'}
{row.condition_value || row.intent || row.regex || '--'}
{row.system_prompt || row.prompt || '--'}
{row.enabled ? 'ativa' : 'inativa'}
toggle(row)}>{row.enabled ? 'Pausar' : 'Ativar'}
remove(row)}>Excluir
))}
{!rows.length && {loading ? 'Carregando...' : 'Nenhum prompt condicional.'} }
);
}
function AgentConversationsTab({ agent }) {
const ConversationWorkspace = window.ConversationWorkspace || window.ConversationsPage;
if (ConversationWorkspace) {
return
;
}
const list = (window.MOCK.conversations || []).filter(c => String(c.agentId) === String(agent.id));
return (
<>
c.paused).length)} sub="pausadas"/>
Cliente Última mensagem Ticket Status Hora
{list.map(c => (
{c.name[0]} {c.name}
{c.last}
{c.ticket || '--'}
{c.paused ? pausada : ativa }
{c.time}
))}
{!list.length && Sem conversa para este agente. }
>
);
}
function AgentLogsTab({ agent }) {
const rows = (window.MOCK.runs || []).filter(r => String(r.agent_id) === String(agent.id)).slice(0, 80);
return (
<>
r.error).length)}/>
Hora Conversa Status Latência Mensagem
{rows.map(r => {
const cls = r.error ? 'bad' : Number(r.latency_ms || 0) > 1500 ? 'warn' : 'ok';
return (
{window.fmtTime ? window.fmtTime(r.created_at) : '--'}
{r.conversation_id || '--'}
{r.status || 'ok'}
{r.latency_ms ? `${r.latency_ms} ms` : '--'}
{r.error || 'OK'}
);
})}
{!rows.length && Sem run para este agente. }
>
);
}
function formFromAgent(agent) {
const raw = agent.raw || {};
const model = raw.ai_model || agent.model || 'gpt-4o-mini';
const preset = window.WA.modelPresetForModel(model);
return {
name: raw.name || agent.name || '',
public_slug: raw.public_slug || '',
instance_name: raw.instance_token || agent.channel || '',
description: raw.description || agent.role || '',
ai_model: model,
model_preset: preset?.id || 'current-model',
ai_api_key: '',
temperature: raw.temperature ?? 0.7,
system_prompt: raw.system_prompt || `Você é ${agent.name}. Atenda nos canais conectados do agente em pt-BR.`,
negative_rules: linesToEditableText(raw.negative_rules),
conversation_goals: linesToEditableText(raw.conversation_goals),
final_goal: raw.final_goal || '',
handoff_enabled: raw.handoff_config?.enabled !== false,
};
}
function lines(value) {
return String(value || '').split('\n').map(s => s.trim()).filter(Boolean);
}
function linesToEditableText(value) {
if (Array.isArray(value)) return value.map(lineItemToText).filter(Boolean).join('\n');
return lineItemToText(value);
}
function lineItemToText(value) {
if (value == null) return '';
if (typeof value === 'string') return value;
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
if (Array.isArray(value)) return value.map(lineItemToText).filter(Boolean).join(' - ');
const direct = value.text || value.goal || value.rule || value.description || value.content || value.value || value.name || value.title;
if (direct) return String(direct);
const flat = Object.values(value).filter(item => item != null && typeof item !== 'object').map(String).filter(Boolean);
return flat.length ? flat.join(' - ') : window.prettyStudioValue?.(value) || String(value);
}
function Field({ id, label, value, onChange, multiline, helper, required, type = 'text', placeholder, autoComplete }) {
return (
{label}
{multiline
?
);
}
function ModelPresetField({ value, model, onChange }) {
const hasCurrentModel = !window.WA.modelPresetForModel(model);
return (
Qual IA usar?
onChange(e.target.value)}>
{hasCurrentModel && {model} - atual }
{window.WA.MODEL_PRESETS.map(preset => {preset.label} - temp {preset.temperature} )}
OpenAI rápido para atendimento geral; Groq forte para fluxos com mais raciocínio. Atual: {model}
);
}
function SiteInstallPanel({ agent, framed = true }) {
const apiUrl = window.WA.siteAgentApiUrl(agent);
const code = window.WA.siteAgentEmbedCode(agent);
function copy(text, label) {
navigator.clipboard?.writeText(text)
.then(() => studioToast(`${label} copiado.`))
.catch(() => studioToast('Não foi possível copiar automaticamente.', 'bad'));
}
const content = (
<>
{!framed && (
API para site
Endpoint ?nico deste agente para embed no site.
)}
Endpoint
copy(apiUrl, 'Endpoint')}>Copiar API
Snippet
>
);
if (!framed) return content;
return (
API para site
Use este endpoint no site oficial ou cole o snippet no arquivo da página.
copy(code, 'Snippet')}>Copiar snippet
{content}
);
}
function agentQrSrc(data) {
const qr = data?.base64 || data?.qrcode || data?.qrCode || data?.code || '';
return String(qr).startsWith('data:image') ? qr : `data:image/png;base64,${qr}`;
}
function Kpi({ label, value, sub }) {
return (
{label}
{value}
{sub &&
{sub}
}
);
}
function ToolCard({ tool, onToggle, onEdit, onDelete }) {
return (
{tool.name}
{tool.enabled ? ativo : inativo }
{tool.description || tool.tool_type}
{tool.requires_approval ? 'requer aprovação' : 'auto'}
Alterar
Excluir
);
}