Versão inicial do Agents Orchestrator
Painel administrativo web para orquestração de agentes Claude Code com suporte a execução de tarefas, agendamento cron, pipelines sequenciais e terminal com streaming em tempo real via WebSocket.
This commit is contained in:
3279
public/css/styles.css
Normal file
3279
public/css/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
873
public/index.html
Normal file
873
public/index.html
Normal file
@@ -0,0 +1,873 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Agents Orchestrator</title>
|
||||
<link rel="stylesheet" href="css/styles.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="layout">
|
||||
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-logo">
|
||||
<div class="sidebar-logo-icon">
|
||||
<i data-lucide="bot"></i>
|
||||
</div>
|
||||
<span class="sidebar-logo-text">Agents Orchestrator</span>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<ul class="sidebar-nav-list">
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="#" class="sidebar-nav-link" data-section="dashboard">
|
||||
<i data-lucide="layout-dashboard"></i>
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="#" class="sidebar-nav-link" data-section="agents">
|
||||
<i data-lucide="cpu"></i>
|
||||
<span>Agentes</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="#" class="sidebar-nav-link" data-section="tasks">
|
||||
<i data-lucide="list-checks"></i>
|
||||
<span>Tarefas</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="#" class="sidebar-nav-link" data-section="schedules">
|
||||
<i data-lucide="clock"></i>
|
||||
<span>Agendamentos</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item" data-section="pipelines">
|
||||
<a class="sidebar-nav-link" href="#" data-section="pipelines">
|
||||
<i data-lucide="git-merge" class="nav-icon"></i>
|
||||
<span>Pipelines</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="#" class="sidebar-nav-link" data-section="terminal">
|
||||
<i data-lucide="terminal"></i>
|
||||
<span>Terminal</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="sidebar-nav-item">
|
||||
<a href="#" class="sidebar-nav-link" data-section="settings">
|
||||
<i data-lucide="settings"></i>
|
||||
<span>Configurações</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="ws-status" id="ws-status">
|
||||
<span class="ws-indicator ws-indicator--disconnected" id="ws-indicator"></span>
|
||||
<span class="ws-label" id="ws-label">Desconectado</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<header class="main-header">
|
||||
<div class="header-breadcrumb">
|
||||
<h1 class="header-title" id="header-title">Dashboard</h1>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="header-badge" id="active-executions-badge" aria-label="Execuções ativas">
|
||||
<i data-lucide="activity"></i>
|
||||
<span id="active-executions-count">0</span>
|
||||
<span class="header-badge-label">ativas</span>
|
||||
</div>
|
||||
<button class="btn btn--ghost btn--icon-text" id="system-status-btn" type="button" aria-label="Status do sistema">
|
||||
<i data-lucide="server"></i>
|
||||
<span>Sistema</span>
|
||||
</button>
|
||||
<button class="btn btn--primary btn--icon-text" id="new-agent-btn" type="button">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Novo Agente</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
|
||||
<section id="dashboard" class="section active" aria-label="Dashboard">
|
||||
<div class="metrics-grid">
|
||||
<article class="metric-card">
|
||||
<div class="metric-card-icon metric-card-icon--indigo">
|
||||
<i data-lucide="cpu"></i>
|
||||
</div>
|
||||
<div class="metric-card-body">
|
||||
<span class="metric-card-label">Total de Agentes</span>
|
||||
<span class="metric-card-value" id="metric-total-agents">0</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<div class="metric-card-icon metric-card-icon--green">
|
||||
<i data-lucide="zap"></i>
|
||||
</div>
|
||||
<div class="metric-card-body">
|
||||
<span class="metric-card-label">Agentes Ativos</span>
|
||||
<span class="metric-card-value" id="metric-active-agents">0</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<div class="metric-card-icon metric-card-icon--purple">
|
||||
<i data-lucide="play-circle"></i>
|
||||
</div>
|
||||
<div class="metric-card-body">
|
||||
<span class="metric-card-label">Execuções Hoje</span>
|
||||
<span class="metric-card-value" id="metric-executions-today">0</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<div class="metric-card-icon metric-card-icon--orange">
|
||||
<i data-lucide="clock"></i>
|
||||
</div>
|
||||
<div class="metric-card-body">
|
||||
<span class="metric-card-label">Agendamentos</span>
|
||||
<span class="metric-card-value" id="metric-schedules">0</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Atividade Recente</h2>
|
||||
<button class="btn btn--ghost btn--sm" type="button" id="refresh-activity-btn">
|
||||
<i data-lucide="refresh-cw"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="activity-list" id="activity-list">
|
||||
<li class="activity-empty">
|
||||
<i data-lucide="inbox"></i>
|
||||
<span>Nenhuma execução recente</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Status do Sistema</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="system-status-list" id="system-status-list">
|
||||
<li class="system-status-item">
|
||||
<div class="system-status-info">
|
||||
<i data-lucide="server"></i>
|
||||
<span>Servidor HTTP</span>
|
||||
</div>
|
||||
<span class="badge badge--green">Online</span>
|
||||
</li>
|
||||
<li class="system-status-item">
|
||||
<div class="system-status-info">
|
||||
<i data-lucide="radio"></i>
|
||||
<span>WebSocket</span>
|
||||
</div>
|
||||
<span class="badge badge--red" id="system-ws-status-badge">Desconectado</span>
|
||||
</li>
|
||||
<li class="system-status-item">
|
||||
<div class="system-status-info">
|
||||
<i data-lucide="database"></i>
|
||||
<span>Armazenamento</span>
|
||||
</div>
|
||||
<span class="badge badge--green">OK</span>
|
||||
</li>
|
||||
<li class="system-status-item">
|
||||
<div class="system-status-info">
|
||||
<i data-lucide="terminal"></i>
|
||||
<span>Claude CLI</span>
|
||||
</div>
|
||||
<span class="badge badge--gray" id="system-claude-status-badge">Verificando...</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="agents" class="section" aria-label="Agentes" hidden>
|
||||
<div class="section-toolbar">
|
||||
<div class="search-field">
|
||||
<i data-lucide="search"></i>
|
||||
<input type="search" placeholder="Buscar agentes..." id="agents-search" aria-label="Buscar agentes" />
|
||||
</div>
|
||||
<div class="section-toolbar-actions">
|
||||
<select class="select" id="agents-filter-status" aria-label="Filtrar por status">
|
||||
<option value="">Todos os status</option>
|
||||
<option value="active">Ativo</option>
|
||||
<option value="inactive">Inativo</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="agents-grid" id="agents-grid">
|
||||
<div class="empty-state" id="agents-empty-state">
|
||||
<div class="empty-state-icon">
|
||||
<i data-lucide="bot"></i>
|
||||
</div>
|
||||
<h3 class="empty-state-title">Nenhum agente cadastrado</h3>
|
||||
<p class="empty-state-desc">Crie seu primeiro agente para começar a orquestrar tarefas com Claude.</p>
|
||||
<button class="btn btn--primary btn--icon-text" type="button" id="agents-empty-new-btn">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Criar Agente</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="tasks" class="section" aria-label="Tarefas" hidden>
|
||||
<div class="section-toolbar">
|
||||
<div class="search-field">
|
||||
<i data-lucide="search"></i>
|
||||
<input type="search" placeholder="Buscar tarefas..." id="tasks-search" aria-label="Buscar tarefas" />
|
||||
</div>
|
||||
<div class="section-toolbar-actions">
|
||||
<select class="select" id="tasks-filter-category" aria-label="Filtrar por categoria">
|
||||
<option value="">Todas as categorias</option>
|
||||
<option value="code-review">Code Review</option>
|
||||
<option value="security">Segurança</option>
|
||||
<option value="refactor">Refatoração</option>
|
||||
<option value="tests">Testes</option>
|
||||
<option value="docs">Documentação</option>
|
||||
<option value="performance">Performance</option>
|
||||
</select>
|
||||
<button class="btn btn--primary btn--icon-text" type="button" id="tasks-new-btn">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Nova Tarefa</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tasks-grid" id="tasks-grid">
|
||||
<div class="empty-state" id="tasks-empty-state">
|
||||
<div class="empty-state-icon">
|
||||
<i data-lucide="list-checks"></i>
|
||||
</div>
|
||||
<h3 class="empty-state-title">Nenhuma tarefa cadastrada</h3>
|
||||
<p class="empty-state-desc">Crie templates de tarefas para reutilizar em diferentes agentes.</p>
|
||||
<button class="btn btn--primary btn--icon-text" type="button" id="tasks-empty-new-btn">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Criar Tarefa</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="schedules" class="section" aria-label="Agendamentos" hidden>
|
||||
<div class="section-toolbar">
|
||||
<div class="search-field">
|
||||
<i data-lucide="search"></i>
|
||||
<input type="search" placeholder="Buscar agendamentos..." id="schedules-search" aria-label="Buscar agendamentos" />
|
||||
</div>
|
||||
<div class="section-toolbar-actions">
|
||||
<select class="select" id="schedules-filter-status" aria-label="Filtrar por status">
|
||||
<option value="">Todos</option>
|
||||
<option value="active">Ativo</option>
|
||||
<option value="paused">Pausado</option>
|
||||
</select>
|
||||
<button class="btn btn--primary btn--icon-text" type="button" id="schedules-new-btn">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Novo Agendamento</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="table-wrapper">
|
||||
<table class="table" id="schedules-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Agente</th>
|
||||
<th scope="col">Tarefa</th>
|
||||
<th scope="col">Expressão Cron</th>
|
||||
<th scope="col">Próxima Execução</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col" aria-label="Ações"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="schedules-tbody">
|
||||
<tr class="table-empty-row">
|
||||
<td colspan="6">
|
||||
<div class="empty-state empty-state--inline">
|
||||
<i data-lucide="clock"></i>
|
||||
<span>Nenhum agendamento configurado</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="pipelines" class="section" aria-label="Pipelines" hidden>
|
||||
<div class="page-title">Pipelines</div>
|
||||
<div class="page-subtitle">Encadeie agentes para fluxos de trabalho automatizados</div>
|
||||
|
||||
<div class="section-toolbar">
|
||||
<div class="search-field">
|
||||
<i data-lucide="search" class="search-icon" style="width:16px;height:16px"></i>
|
||||
<input type="text" placeholder="Buscar pipelines..." id="pipelines-search">
|
||||
</div>
|
||||
<div class="section-toolbar-actions">
|
||||
<button class="btn btn--primary btn--icon-text" type="button" id="pipelines-new-btn">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Novo Pipeline</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pipelines-grid" class="agents-grid">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="terminal" class="section" aria-label="Terminal" hidden>
|
||||
<div class="terminal-wrapper">
|
||||
<div class="terminal-toolbar">
|
||||
<div class="terminal-toolbar-left">
|
||||
<div class="terminal-dot terminal-dot--red"></div>
|
||||
<div class="terminal-dot terminal-dot--yellow"></div>
|
||||
<div class="terminal-dot terminal-dot--green"></div>
|
||||
<span class="terminal-title">Output de Execução</span>
|
||||
</div>
|
||||
<div class="terminal-toolbar-right">
|
||||
<select class="select select--sm" id="terminal-execution-select" aria-label="Selecionar execução">
|
||||
<option value="">Selecionar execução...</option>
|
||||
</select>
|
||||
<div class="terminal-ws-indicator" id="terminal-ws-indicator">
|
||||
<span class="ws-indicator ws-indicator--disconnected" id="terminal-ws-dot"></span>
|
||||
<span id="terminal-ws-label">Desconectado</span>
|
||||
</div>
|
||||
<button class="btn btn--ghost btn--sm btn--icon-text" type="button" id="terminal-clear-btn" aria-label="Limpar terminal">
|
||||
<i data-lucide="trash-2"></i>
|
||||
<span>Limpar</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-output" id="terminal-output" role="log" aria-live="polite" aria-label="Saída do terminal">
|
||||
<div class="terminal-welcome">
|
||||
<span class="terminal-prompt">$</span>
|
||||
<span class="terminal-text">Aguardando execução de agente...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="settings" class="section" aria-label="Configurações" hidden>
|
||||
<div class="settings-grid">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Configurações Gerais</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="settings-form" id="settings-form" novalidate>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="settings-default-model">Modelo Padrão</label>
|
||||
<select class="select" id="settings-default-model" name="defaultModel">
|
||||
<option value="claude-sonnet-4-6">claude-sonnet-4-6</option>
|
||||
<option value="claude-opus-4-6">claude-opus-4-6</option>
|
||||
<option value="claude-haiku-4-5-20251001">claude-haiku-4-5-20251001</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="settings-default-workdir">Diretório de Trabalho Padrão</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
id="settings-default-workdir"
|
||||
name="defaultWorkdir"
|
||||
placeholder="/home/fred/projetos"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="settings-max-concurrent">Execuções Simultâneas Máximas</label>
|
||||
<input
|
||||
type="number"
|
||||
class="input"
|
||||
id="settings-max-concurrent"
|
||||
name="maxConcurrent"
|
||||
min="1"
|
||||
max="20"
|
||||
placeholder="5"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn--primary">Salvar Configurações</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Informações do Sistema</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="system-info-list" id="system-info-list">
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Versão do Servidor</span>
|
||||
<span class="system-info-value" id="info-server-version">1.0.0</span>
|
||||
</li>
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Versão do Node.js</span>
|
||||
<span class="system-info-value font-mono" id="info-node-version">Carregando...</span>
|
||||
</li>
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Versão do Claude CLI</span>
|
||||
<span class="system-info-value font-mono" id="info-claude-version">Carregando...</span>
|
||||
</li>
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Plataforma</span>
|
||||
<span class="system-info-value font-mono" id="info-platform">Carregando...</span>
|
||||
</li>
|
||||
<li class="system-info-item">
|
||||
<span class="system-info-label">Tempo Online</span>
|
||||
<span class="system-info-value font-mono" id="info-uptime">Carregando...</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="agent-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="agent-modal-title" hidden>
|
||||
<div class="modal modal--lg">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="agent-modal-title">Novo Agente</h2>
|
||||
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="agent-modal-overlay">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="modal-form" id="agent-form" novalidate>
|
||||
<input type="hidden" id="agent-form-id" name="id" />
|
||||
<div class="form-row">
|
||||
<div class="form-group form-group--grow">
|
||||
<label class="form-label" for="agent-name">
|
||||
Nome do Agente
|
||||
<span class="form-required" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
id="agent-name"
|
||||
name="name"
|
||||
placeholder="Ex: Revisor de Código"
|
||||
required
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="agent-status-toggle">Status</label>
|
||||
<div class="toggle-wrapper">
|
||||
<input type="checkbox" class="toggle-input" id="agent-status-toggle" name="active" role="switch" />
|
||||
<label class="toggle-label" for="agent-status-toggle">
|
||||
<span class="toggle-thumb"></span>
|
||||
<span class="toggle-text-on">Ativo</span>
|
||||
<span class="toggle-text-off">Inativo</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="agent-description">Descrição</label>
|
||||
<textarea
|
||||
class="textarea"
|
||||
id="agent-description"
|
||||
name="description"
|
||||
rows="2"
|
||||
placeholder="Descreva brevemente o propósito deste agente"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="agent-system-prompt">
|
||||
System Prompt
|
||||
</label>
|
||||
<textarea
|
||||
class="textarea textarea--code"
|
||||
id="agent-system-prompt"
|
||||
name="systemPrompt"
|
||||
rows="6"
|
||||
placeholder="Você é um especialista em... Seu objetivo é... Sempre responda em português..."
|
||||
></textarea>
|
||||
<p class="form-hint">Instruções base que definem o comportamento, personalidade e restrições do agente.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group form-group--grow">
|
||||
<label class="form-label" for="agent-model">Modelo</label>
|
||||
<select class="select" id="agent-model" name="model">
|
||||
<option value="claude-sonnet-4-6">claude-sonnet-4-6</option>
|
||||
<option value="claude-opus-4-6">claude-opus-4-6</option>
|
||||
<option value="claude-haiku-4-5-20251001">claude-haiku-4-5-20251001</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group form-group--grow">
|
||||
<label class="form-label" for="agent-workdir">Diretório de Trabalho</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input"
|
||||
id="agent-workdir"
|
||||
name="workdir"
|
||||
placeholder="/home/fred/projetos"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="agent-tags-input">Tags</label>
|
||||
<div class="tags-input-wrapper" id="agent-tags-wrapper">
|
||||
<div class="tags-chips" id="agent-tags-chips"></div>
|
||||
<input
|
||||
type="text"
|
||||
class="tags-input"
|
||||
id="agent-tags-input"
|
||||
placeholder="Digite e pressione Enter para adicionar"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<input type="hidden" id="agent-tags" name="tags" value="[]" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn--ghost" type="button" data-modal-close="agent-modal-overlay">Cancelar</button>
|
||||
<button class="btn btn--primary" type="submit" form="agent-form" id="agent-form-submit">Salvar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="execute-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="execute-modal-title" hidden>
|
||||
<div class="modal modal--md">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="execute-modal-title">Executar Tarefa</h2>
|
||||
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="execute-modal-overlay">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="modal-form" id="execute-form" novalidate>
|
||||
<input type="hidden" id="execute-agent-id" name="agentId" />
|
||||
|
||||
<div class="form-group" id="execute-agent-select-group">
|
||||
<label class="form-label" for="execute-agent-select">
|
||||
Agente
|
||||
<span class="form-required" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<select class="select" id="execute-agent-select" name="agentSelect" required>
|
||||
<option value="">Selecionar agente...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="execute-task-desc">
|
||||
Descrição da Tarefa
|
||||
<span class="form-required" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="textarea"
|
||||
id="execute-task-desc"
|
||||
name="task"
|
||||
rows="3"
|
||||
placeholder="Descreva o que o agente deve fazer..."
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="execute-instructions">Instruções Adicionais</label>
|
||||
<textarea
|
||||
class="textarea textarea--code"
|
||||
id="execute-instructions"
|
||||
name="instructions"
|
||||
rows="3"
|
||||
placeholder="Contexto adicional, restrições ou preferências..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="quick-templates">
|
||||
<p class="quick-templates-label">Templates rápidos</p>
|
||||
<div class="quick-templates-grid">
|
||||
<button class="template-btn" type="button" data-template="Analisar código e detectar bugs, vulnerabilidades e problemas de qualidade. Forneça um relatório detalhado com sugestões de correção.">
|
||||
<i data-lucide="bug"></i>
|
||||
<span>Detectar Bugs</span>
|
||||
</button>
|
||||
<button class="template-btn" type="button" data-template="Realizar revisão de segurança seguindo as diretrizes OWASP Top 10. Identificar vulnerabilidades, inputs não validados e problemas de autenticação/autorização.">
|
||||
<i data-lucide="shield"></i>
|
||||
<span>Revisão OWASP</span>
|
||||
</button>
|
||||
<button class="template-btn" type="button" data-template="Refatorar o código para melhorar legibilidade, manutenibilidade e aderência às boas práticas. Manter comportamento funcional intacto.">
|
||||
<i data-lucide="wand-2"></i>
|
||||
<span>Refatorar Código</span>
|
||||
</button>
|
||||
<button class="template-btn" type="button" data-template="Escrever testes unitários e de integração abrangentes. Garantir cobertura dos casos de sucesso, erro e edge cases.">
|
||||
<i data-lucide="test-tube-2"></i>
|
||||
<span>Escrever Testes</span>
|
||||
</button>
|
||||
<button class="template-btn" type="button" data-template="Documentar o código com JSDoc/docstrings, README atualizado, exemplos de uso e descrição de APIs públicas.">
|
||||
<i data-lucide="book-open"></i>
|
||||
<span>Documentar Código</span>
|
||||
</button>
|
||||
<button class="template-btn" type="button" data-template="Analisar e otimizar a performance do código. Identificar gargalos, reduzir complexidade algorítmica e melhorar uso de memória.">
|
||||
<i data-lucide="gauge"></i>
|
||||
<span>Otimizar Performance</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn--ghost" type="button" data-modal-close="execute-modal-overlay">Cancelar</button>
|
||||
<button class="btn btn--primary btn--icon-text" type="submit" form="execute-form" id="execute-form-submit">
|
||||
<i data-lucide="play"></i>
|
||||
<span>Executar</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="schedule-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="schedule-modal-title" hidden>
|
||||
<div class="modal modal--md">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="schedule-modal-title">Novo Agendamento</h2>
|
||||
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="schedule-modal-overlay">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="modal-form" id="schedule-form" novalidate>
|
||||
<input type="hidden" id="schedule-form-id" name="id" />
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="schedule-agent">
|
||||
Agente
|
||||
<span class="form-required" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<select class="select" id="schedule-agent" name="agentId" required>
|
||||
<option value="">Selecionar agente...</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="schedule-task">
|
||||
Descrição da Tarefa
|
||||
<span class="form-required" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="textarea"
|
||||
id="schedule-task"
|
||||
name="task"
|
||||
rows="3"
|
||||
placeholder="Descreva a tarefa que será executada automaticamente..."
|
||||
required
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="schedule-cron">
|
||||
Expressão Cron
|
||||
<span class="form-required" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input--mono"
|
||||
id="schedule-cron"
|
||||
name="cron"
|
||||
placeholder="0 * * * *"
|
||||
required
|
||||
autocomplete="off"
|
||||
aria-describedby="schedule-cron-hint"
|
||||
/>
|
||||
<p class="form-hint" id="schedule-cron-hint">Formato: minuto hora dia-mês mês dia-semana</p>
|
||||
</div>
|
||||
|
||||
<div class="cron-presets">
|
||||
<p class="cron-presets-label">Presets</p>
|
||||
<div class="cron-presets-list">
|
||||
<button class="cron-preset-btn" type="button" data-cron="0 * * * *">
|
||||
<span class="cron-preset-name">A cada hora</span>
|
||||
<span class="cron-preset-expr">0 * * * *</span>
|
||||
</button>
|
||||
<button class="cron-preset-btn" type="button" data-cron="0 9 * * *">
|
||||
<span class="cron-preset-name">Diário (09:00)</span>
|
||||
<span class="cron-preset-expr">0 9 * * *</span>
|
||||
</button>
|
||||
<button class="cron-preset-btn" type="button" data-cron="0 9 * * 1">
|
||||
<span class="cron-preset-name">Semanal (seg 09:00)</span>
|
||||
<span class="cron-preset-expr">0 9 * * 1</span>
|
||||
</button>
|
||||
<button class="cron-preset-btn" type="button" data-cron="0 0 1 * *">
|
||||
<span class="cron-preset-name">Mensal (dia 1)</span>
|
||||
<span class="cron-preset-expr">0 0 1 * *</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn--ghost" type="button" data-modal-close="schedule-modal-overlay">Cancelar</button>
|
||||
<button class="btn btn--primary btn--icon-text" type="submit" form="schedule-form" id="schedule-form-submit">
|
||||
<i data-lucide="clock"></i>
|
||||
<span>Agendar</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="pipeline-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="pipeline-modal-title" hidden>
|
||||
<div class="modal modal--lg">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="pipeline-modal-title">Novo Pipeline</h2>
|
||||
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="pipeline-modal-overlay">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="modal-form" id="pipeline-form" novalidate>
|
||||
<input type="hidden" id="pipeline-form-id">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="pipeline-name">
|
||||
Nome
|
||||
<span class="form-required" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<input class="input" type="text" id="pipeline-name" placeholder="Ex: Análise e Correção de Bugs" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="pipeline-description">Descrição</label>
|
||||
<textarea class="textarea" id="pipeline-description" rows="2" placeholder="Descreva o objetivo do pipeline..."></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
Passos do Pipeline
|
||||
<span class="form-required" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<p class="form-hint mb-8">Cada passo usa um agente. O output de um passo vira o input do próximo.</p>
|
||||
<div id="pipeline-steps-container"></div>
|
||||
<button class="btn btn--ghost btn--sm mt-8" type="button" id="pipeline-add-step-btn">
|
||||
<i data-lucide="plus" style="width:14px;height:14px"></i>
|
||||
Adicionar Passo
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn--ghost" type="button" data-modal-close="pipeline-modal-overlay">Cancelar</button>
|
||||
<button class="btn btn--primary" type="button" id="pipeline-form-submit">Salvar Pipeline</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="pipeline-execute-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="pipeline-execute-title" hidden>
|
||||
<div class="modal modal--md">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="pipeline-execute-title">Executar Pipeline</h2>
|
||||
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="pipeline-execute-modal-overlay">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="pipeline-execute-id">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="pipeline-execute-input">
|
||||
Input Inicial
|
||||
<span class="form-required" aria-hidden="true">*</span>
|
||||
</label>
|
||||
<textarea class="textarea" id="pipeline-execute-input" rows="4" placeholder="Descreva a tarefa inicial para o pipeline..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn--ghost" type="button" data-modal-close="pipeline-execute-modal-overlay">Cancelar</button>
|
||||
<button class="btn btn--primary btn--icon-text" type="button" id="pipeline-execute-submit">
|
||||
<i data-lucide="play" style="width:14px;height:14px"></i>
|
||||
<span>Executar</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="confirm-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="confirm-modal-title" hidden>
|
||||
<div class="modal modal--sm">
|
||||
<div class="modal-header">
|
||||
<div class="confirm-modal-icon" id="confirm-modal-icon">
|
||||
<i data-lucide="alert-triangle"></i>
|
||||
</div>
|
||||
<h2 class="modal-title" id="confirm-modal-title">Confirmar Ação</h2>
|
||||
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="confirm-modal-overlay">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="confirm-modal-message" id="confirm-modal-message"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn--ghost" type="button" data-modal-close="confirm-modal-overlay">Cancelar</button>
|
||||
<button class="btn btn--danger" type="button" id="confirm-modal-confirm-btn">Confirmar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="export-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="export-modal-title" hidden>
|
||||
<div class="modal modal--md">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="export-modal-title">Exportar Agente</h2>
|
||||
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="export-modal-overlay">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="export-code-wrapper">
|
||||
<div class="export-code-toolbar">
|
||||
<span class="export-code-label">JSON</span>
|
||||
<button class="btn btn--ghost btn--sm btn--icon-text" type="button" id="export-copy-btn" aria-label="Copiar JSON">
|
||||
<i data-lucide="copy"></i>
|
||||
<span>Copiar</span>
|
||||
</button>
|
||||
</div>
|
||||
<pre class="export-code" id="export-code-content" tabindex="0" aria-label="Dados do agente em formato JSON"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn--ghost" type="button" data-modal-close="export-modal-overlay">Fechar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast-container" id="toast-container" aria-live="assertive" aria-atomic="false" role="region" aria-label="Notificações"></div>
|
||||
|
||||
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/components/toast.js"></script>
|
||||
<script src="js/components/modal.js"></script>
|
||||
<script src="js/components/terminal.js"></script>
|
||||
<script src="js/components/agents.js"></script>
|
||||
<script src="js/components/dashboard.js"></script>
|
||||
<script src="js/components/tasks.js"></script>
|
||||
<script src="js/components/schedules.js"></script>
|
||||
<script src="js/components/pipelines.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
App.init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
67
public/js/api.js
Normal file
67
public/js/api.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const API = {
|
||||
baseUrl: '/api',
|
||||
|
||||
async request(method, path, body = null) {
|
||||
const options = {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
|
||||
if (body !== null) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API.baseUrl}${path}`, options);
|
||||
|
||||
if (response.status === 204) return null;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `Erro HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
agents: {
|
||||
list() { return API.request('GET', '/agents'); },
|
||||
get(id) { return API.request('GET', `/agents/${id}`); },
|
||||
create(data) { return API.request('POST', '/agents', data); },
|
||||
update(id, data) { return API.request('PUT', `/agents/${id}`, data); },
|
||||
delete(id) { return API.request('DELETE', `/agents/${id}`); },
|
||||
execute(id, task, instructions) { return API.request('POST', `/agents/${id}/execute`, { task, instructions }); },
|
||||
cancel(id, executionId) { return API.request('POST', `/agents/${id}/cancel/${executionId}`); },
|
||||
export(id) { return API.request('GET', `/agents/${id}/export`); },
|
||||
},
|
||||
|
||||
tasks: {
|
||||
list() { return API.request('GET', '/tasks'); },
|
||||
create(data) { return API.request('POST', '/tasks', data); },
|
||||
update(id, data) { return API.request('PUT', `/tasks/${id}`, data); },
|
||||
delete(id) { return API.request('DELETE', `/tasks/${id}`); },
|
||||
},
|
||||
|
||||
schedules: {
|
||||
list() { return API.request('GET', '/schedules'); },
|
||||
create(data) { return API.request('POST', '/schedules', data); },
|
||||
delete(taskId) { return API.request('DELETE', `/schedules/${taskId}`); },
|
||||
},
|
||||
|
||||
pipelines: {
|
||||
list() { return API.request('GET', '/pipelines'); },
|
||||
get(id) { return API.request('GET', `/pipelines/${id}`); },
|
||||
create(data) { return API.request('POST', '/pipelines', data); },
|
||||
update(id, data) { return API.request('PUT', `/pipelines/${id}`, data); },
|
||||
delete(id) { return API.request('DELETE', `/pipelines/${id}`); },
|
||||
execute(id, input) { return API.request('POST', `/pipelines/${id}/execute`, { input }); },
|
||||
cancel(id) { return API.request('POST', `/pipelines/${id}/cancel`); },
|
||||
},
|
||||
|
||||
system: {
|
||||
status() { return API.request('GET', '/system/status'); },
|
||||
activeExecutions() { return API.request('GET', '/executions/active'); },
|
||||
},
|
||||
};
|
||||
|
||||
window.API = API;
|
||||
545
public/js/app.js
Normal file
545
public/js/app.js
Normal file
@@ -0,0 +1,545 @@
|
||||
const App = {
|
||||
currentSection: 'dashboard',
|
||||
ws: null,
|
||||
wsReconnectAttempts: 0,
|
||||
wsReconnectTimer: null,
|
||||
_initialized: false,
|
||||
|
||||
sectionTitles: {
|
||||
dashboard: 'Dashboard',
|
||||
agents: 'Agentes',
|
||||
tasks: 'Tarefas',
|
||||
schedules: 'Agendamentos',
|
||||
pipelines: 'Pipelines',
|
||||
terminal: 'Terminal',
|
||||
settings: 'Configurações',
|
||||
},
|
||||
|
||||
init() {
|
||||
if (App._initialized) return;
|
||||
App._initialized = true;
|
||||
|
||||
App.setupNavigation();
|
||||
App.setupWebSocket();
|
||||
App.setupEventListeners();
|
||||
App.setupKeyboardShortcuts();
|
||||
App.navigateTo('dashboard');
|
||||
App.startPeriodicRefresh();
|
||||
|
||||
if (window.lucide) lucide.createIcons();
|
||||
},
|
||||
|
||||
setupNavigation() {
|
||||
document.querySelectorAll('.sidebar-nav-link[data-section]').forEach((link) => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
App.navigateTo(link.dataset.section);
|
||||
});
|
||||
});
|
||||
|
||||
const refreshBtn = document.getElementById('refresh-activity-btn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', () => DashboardUI.load());
|
||||
}
|
||||
},
|
||||
|
||||
navigateTo(section) {
|
||||
document.querySelectorAll('.section').forEach((el) => {
|
||||
const isActive = el.id === section;
|
||||
el.classList.toggle('active', isActive);
|
||||
el.hidden = !isActive;
|
||||
});
|
||||
|
||||
document.querySelectorAll('.sidebar-nav-item').forEach((item) => {
|
||||
const link = item.querySelector('.sidebar-nav-link');
|
||||
item.classList.toggle('active', link && link.dataset.section === section);
|
||||
});
|
||||
|
||||
const titleEl = document.getElementById('header-title');
|
||||
if (titleEl) titleEl.textContent = App.sectionTitles[section] || section;
|
||||
|
||||
App.currentSection = section;
|
||||
App._loadSection(section);
|
||||
},
|
||||
|
||||
async _loadSection(section) {
|
||||
try {
|
||||
switch (section) {
|
||||
case 'dashboard': await DashboardUI.load(); break;
|
||||
case 'agents': await AgentsUI.load(); break;
|
||||
case 'tasks': await TasksUI.load(); break;
|
||||
case 'schedules': await SchedulesUI.load(); break;
|
||||
case 'pipelines': await PipelinesUI.load(); break;
|
||||
}
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao carregar seção: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
setupWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const url = `${protocol}//${window.location.host}`;
|
||||
|
||||
try {
|
||||
App.ws = new WebSocket(url);
|
||||
|
||||
App.ws.onopen = () => {
|
||||
App.updateWsStatus('connected');
|
||||
App.wsReconnectAttempts = 0;
|
||||
if (App.wsReconnectTimer) {
|
||||
clearTimeout(App.wsReconnectTimer);
|
||||
App.wsReconnectTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
App.ws.onclose = () => {
|
||||
App.updateWsStatus('disconnected');
|
||||
App._scheduleWsReconnect();
|
||||
};
|
||||
|
||||
App.ws.onerror = () => {
|
||||
App.updateWsStatus('error');
|
||||
};
|
||||
|
||||
App.ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
App.handleWsMessage(data);
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
};
|
||||
} catch {
|
||||
App.updateWsStatus('error');
|
||||
App._scheduleWsReconnect();
|
||||
}
|
||||
},
|
||||
|
||||
_scheduleWsReconnect() {
|
||||
const delay = Math.min(1000 * Math.pow(2, App.wsReconnectAttempts), 30000);
|
||||
App.wsReconnectAttempts++;
|
||||
|
||||
App.wsReconnectTimer = setTimeout(() => {
|
||||
App.setupWebSocket();
|
||||
}, delay);
|
||||
},
|
||||
|
||||
handleWsMessage(data) {
|
||||
switch (data.type) {
|
||||
case 'execution_output': {
|
||||
Terminal.stopProcessing();
|
||||
const content = data.data?.content || '';
|
||||
if (content) {
|
||||
Terminal.addLine(content, 'default');
|
||||
}
|
||||
App._updateActiveBadge();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'execution_complete': {
|
||||
Terminal.stopProcessing();
|
||||
const result = data.data?.result || '';
|
||||
if (result) {
|
||||
Terminal.addLine(result, 'success');
|
||||
} else {
|
||||
Terminal.addLine('Execução concluída (sem resultado textual).', 'info');
|
||||
}
|
||||
if (data.data?.stderr) {
|
||||
Terminal.addLine(data.data.stderr, 'error');
|
||||
}
|
||||
Toast.success('Execução concluída');
|
||||
App.refreshCurrentSection();
|
||||
App._updateActiveBadge();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'execution_error':
|
||||
Terminal.stopProcessing();
|
||||
Terminal.addLine(data.data?.error || 'Erro na execução', 'error');
|
||||
Toast.error(`Erro na execução: ${data.data?.error || 'desconhecido'}`);
|
||||
App._updateActiveBadge();
|
||||
break;
|
||||
|
||||
case 'pipeline_step_start':
|
||||
Terminal.stopProcessing();
|
||||
Terminal.addLine(`Pipeline passo ${data.stepIndex + 1}/${data.totalSteps}: Executando agente "${data.agentName}"...`, 'system');
|
||||
Terminal.startProcessing(data.agentName);
|
||||
break;
|
||||
|
||||
case 'pipeline_step_complete':
|
||||
Terminal.stopProcessing();
|
||||
Terminal.addLine(`Passo ${data.stepIndex + 1} concluído.`, 'info');
|
||||
Terminal.addLine(data.result || '(sem output)', 'default');
|
||||
break;
|
||||
|
||||
case 'pipeline_complete':
|
||||
Terminal.stopProcessing();
|
||||
Terminal.addLine('Pipeline concluído com sucesso.', 'success');
|
||||
Toast.success('Pipeline concluído');
|
||||
App.refreshCurrentSection();
|
||||
break;
|
||||
|
||||
case 'pipeline_error':
|
||||
Terminal.stopProcessing();
|
||||
Terminal.addLine(`Erro no passo ${data.stepIndex + 1}: ${data.error}`, 'error');
|
||||
Toast.error('Erro no pipeline');
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
updateWsStatus(status) {
|
||||
const indicator = document.getElementById('ws-indicator');
|
||||
const label = document.getElementById('ws-label');
|
||||
const terminalDot = document.getElementById('terminal-ws-dot');
|
||||
const terminalLabel = document.getElementById('terminal-ws-label');
|
||||
const wsBadge = document.getElementById('system-ws-status-badge');
|
||||
|
||||
const labels = {
|
||||
connected: 'Conectado',
|
||||
disconnected: 'Desconectado',
|
||||
error: 'Erro de conexão',
|
||||
};
|
||||
|
||||
const cssClass = {
|
||||
connected: 'ws-indicator--connected',
|
||||
disconnected: 'ws-indicator--disconnected',
|
||||
error: 'ws-indicator--error',
|
||||
};
|
||||
|
||||
const badgeClass = {
|
||||
connected: 'badge--green',
|
||||
disconnected: 'badge--red',
|
||||
error: 'badge--red',
|
||||
};
|
||||
|
||||
const displayLabel = labels[status] || status;
|
||||
const dotClass = cssClass[status] || 'ws-indicator--disconnected';
|
||||
|
||||
[indicator, terminalDot].forEach((el) => {
|
||||
if (!el) return;
|
||||
el.className = `ws-indicator ${dotClass}`;
|
||||
});
|
||||
|
||||
[label, terminalLabel].forEach((el) => {
|
||||
if (el) el.textContent = displayLabel;
|
||||
});
|
||||
|
||||
if (wsBadge) {
|
||||
wsBadge.textContent = displayLabel;
|
||||
wsBadge.className = `badge ${badgeClass[status] || 'badge--red'}`;
|
||||
}
|
||||
},
|
||||
|
||||
setupEventListeners() {
|
||||
const on = (id, event, handler) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.addEventListener(event, handler);
|
||||
};
|
||||
|
||||
on('new-agent-btn', 'click', () => AgentsUI.openCreateModal());
|
||||
on('agents-empty-new-btn', 'click', () => AgentsUI.openCreateModal());
|
||||
|
||||
on('agent-form-submit', 'click', (e) => {
|
||||
e.preventDefault();
|
||||
AgentsUI.save();
|
||||
});
|
||||
|
||||
on('agent-form', 'submit', (e) => {
|
||||
e.preventDefault();
|
||||
AgentsUI.save();
|
||||
});
|
||||
|
||||
on('execute-form-submit', 'click', (e) => {
|
||||
e.preventDefault();
|
||||
App._handleExecute();
|
||||
});
|
||||
|
||||
on('execute-form', 'submit', (e) => {
|
||||
e.preventDefault();
|
||||
App._handleExecute();
|
||||
});
|
||||
|
||||
on('tasks-new-btn', 'click', () => TasksUI.openCreateModal());
|
||||
on('tasks-empty-new-btn', 'click', () => TasksUI.openCreateModal());
|
||||
|
||||
on('schedules-new-btn', 'click', () => SchedulesUI.openCreateModal());
|
||||
|
||||
on('schedule-form-submit', 'click', (e) => {
|
||||
e.preventDefault();
|
||||
SchedulesUI.save();
|
||||
});
|
||||
|
||||
on('schedule-form', 'submit', (e) => {
|
||||
e.preventDefault();
|
||||
SchedulesUI.save();
|
||||
});
|
||||
|
||||
on('pipelines-new-btn', 'click', () => PipelinesUI.openCreateModal());
|
||||
|
||||
on('pipeline-form-submit', 'click', (e) => {
|
||||
e.preventDefault();
|
||||
PipelinesUI.save();
|
||||
});
|
||||
|
||||
on('pipeline-add-step-btn', 'click', () => PipelinesUI.addStep());
|
||||
|
||||
on('pipeline-execute-submit', 'click', () => PipelinesUI._executeFromModal());
|
||||
|
||||
on('terminal-clear-btn', 'click', () => Terminal.clear());
|
||||
|
||||
on('export-copy-btn', 'click', () => App._copyExportJson());
|
||||
|
||||
on('system-status-btn', 'click', () => App.navigateTo('dashboard'));
|
||||
|
||||
on('terminal-execution-select', 'change', (e) => {
|
||||
Terminal.setExecutionFilter(e.target.value || null);
|
||||
});
|
||||
|
||||
on('settings-form', 'submit', (e) => {
|
||||
e.preventDefault();
|
||||
Toast.info('Configurações salvas');
|
||||
});
|
||||
|
||||
document.getElementById('agents-grid')?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
|
||||
const { action, id } = btn.dataset;
|
||||
|
||||
switch (action) {
|
||||
case 'execute': AgentsUI.execute(id); break;
|
||||
case 'edit': AgentsUI.openEditModal(id); break;
|
||||
case 'export': AgentsUI.export(id); break;
|
||||
case 'delete': AgentsUI.delete(id); break;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('tasks-grid')?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
|
||||
const { action, id } = btn.dataset;
|
||||
|
||||
if (action === 'delete-task') TasksUI.delete(id);
|
||||
});
|
||||
|
||||
document.getElementById('schedules-tbody')?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
|
||||
const { action, id } = btn.dataset;
|
||||
|
||||
if (action === 'delete-schedule') SchedulesUI.delete(id);
|
||||
});
|
||||
|
||||
document.getElementById('pipelines-grid')?.addEventListener('click', (e) => {
|
||||
if (e.target.closest('#pipelines-empty-new-btn')) {
|
||||
PipelinesUI.openCreateModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
|
||||
const { action, id } = btn.dataset;
|
||||
|
||||
switch (action) {
|
||||
case 'execute-pipeline': PipelinesUI.execute(id); break;
|
||||
case 'edit-pipeline': PipelinesUI.openEditModal(id); break;
|
||||
case 'delete-pipeline': PipelinesUI.delete(id); break;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('pipeline-steps-container')?.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-step-action]');
|
||||
if (!btn) return;
|
||||
|
||||
const stepAction = btn.dataset.stepAction;
|
||||
const stepIndex = parseInt(btn.dataset.stepIndex, 10);
|
||||
|
||||
switch (stepAction) {
|
||||
case 'move-up': PipelinesUI.moveStep(stepIndex, -1); break;
|
||||
case 'move-down': PipelinesUI.moveStep(stepIndex, 1); break;
|
||||
case 'remove': PipelinesUI.removeStep(stepIndex); break;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const template = e.target.closest('[data-template]');
|
||||
if (template) {
|
||||
const taskEl = document.getElementById('execute-task-desc');
|
||||
if (taskEl) taskEl.value = template.dataset.template;
|
||||
return;
|
||||
}
|
||||
|
||||
const cronPreset = e.target.closest('[data-cron]');
|
||||
if (cronPreset) {
|
||||
const cronEl = document.getElementById('schedule-cron');
|
||||
if (cronEl) cronEl.value = cronPreset.dataset.cron;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
App._setupTagsInput();
|
||||
},
|
||||
|
||||
_setupTagsInput() {
|
||||
const input = document.getElementById('agent-tags-input');
|
||||
const chips = document.getElementById('agent-tags-chips');
|
||||
const hidden = document.getElementById('agent-tags');
|
||||
|
||||
if (!input || !chips || !hidden) return;
|
||||
|
||||
const getTags = () => {
|
||||
try { return JSON.parse(hidden.value || '[]'); } catch { return []; }
|
||||
};
|
||||
|
||||
const setTags = (tags) => {
|
||||
hidden.value = JSON.stringify(tags);
|
||||
chips.innerHTML = tags.map((t) => `
|
||||
<span class="tag-chip">
|
||||
${t}
|
||||
<button type="button" class="tag-remove" data-tag="${t}" aria-label="Remover tag ${t}">×</button>
|
||||
</span>
|
||||
`).join('');
|
||||
};
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Enter' && e.key !== ',') return;
|
||||
e.preventDefault();
|
||||
|
||||
const value = input.value.trim().replace(/,$/, '');
|
||||
if (!value) return;
|
||||
|
||||
const tags = getTags();
|
||||
if (!tags.includes(value)) {
|
||||
tags.push(value);
|
||||
setTags(tags);
|
||||
}
|
||||
|
||||
input.value = '';
|
||||
});
|
||||
|
||||
chips.addEventListener('click', (e) => {
|
||||
const removeBtn = e.target.closest('.tag-remove');
|
||||
if (!removeBtn) return;
|
||||
|
||||
const tag = removeBtn.dataset.tag;
|
||||
const tags = getTags().filter((t) => t !== tag);
|
||||
setTags(tags);
|
||||
});
|
||||
},
|
||||
|
||||
async _handleExecute() {
|
||||
const agentId = document.getElementById('execute-agent-select')?.value
|
||||
|| document.getElementById('execute-agent-id')?.value;
|
||||
|
||||
if (!agentId) {
|
||||
Toast.warning('Selecione um agente para executar');
|
||||
return;
|
||||
}
|
||||
|
||||
const task = document.getElementById('execute-task-desc')?.value.trim();
|
||||
if (!task) {
|
||||
Toast.warning('Descreva a tarefa a ser executada');
|
||||
return;
|
||||
}
|
||||
|
||||
const instructions = document.getElementById('execute-instructions')?.value.trim() || '';
|
||||
|
||||
try {
|
||||
const selectEl = document.getElementById('execute-agent-select');
|
||||
const agentName = selectEl?.selectedOptions[0]?.text || 'Agente';
|
||||
|
||||
await API.agents.execute(agentId, task, instructions);
|
||||
|
||||
Modal.close('execute-modal-overlay');
|
||||
App.navigateTo('terminal');
|
||||
Toast.info('Execução iniciada');
|
||||
Terminal.startProcessing(agentName);
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao iniciar execução: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async _copyExportJson() {
|
||||
const jsonEl = document.getElementById('export-code-content');
|
||||
if (!jsonEl) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(jsonEl.textContent);
|
||||
Toast.success('JSON copiado para a área de transferência');
|
||||
} catch {
|
||||
Toast.error('Não foi possível copiar o JSON');
|
||||
}
|
||||
},
|
||||
|
||||
setupKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
Modal.closeAll();
|
||||
return;
|
||||
}
|
||||
|
||||
const isTyping = ['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement?.tagName);
|
||||
if (isTyping) return;
|
||||
|
||||
if (e.key === 'n' || e.key === 'N') {
|
||||
if (App.currentSection === 'agents') {
|
||||
AgentsUI.openCreateModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async refreshCurrentSection() {
|
||||
await App._loadSection(App.currentSection);
|
||||
},
|
||||
|
||||
async _updateActiveBadge() {
|
||||
try {
|
||||
const active = await API.system.activeExecutions();
|
||||
const count = Array.isArray(active) ? active.length : 0;
|
||||
|
||||
const badge = document.getElementById('active-executions-badge');
|
||||
const countEl = document.getElementById('active-executions-count');
|
||||
|
||||
if (countEl) countEl.textContent = count;
|
||||
if (badge) badge.style.display = count > 0 ? 'flex' : 'none';
|
||||
|
||||
const terminalSelect = document.getElementById('terminal-execution-select');
|
||||
if (terminalSelect && Array.isArray(active)) {
|
||||
const existing = new Set(
|
||||
Array.from(terminalSelect.options).map((o) => o.value).filter(Boolean)
|
||||
);
|
||||
|
||||
active.forEach((exec) => {
|
||||
const execId = exec.executionId || exec.id;
|
||||
if (!existing.has(execId)) {
|
||||
const option = document.createElement('option');
|
||||
option.value = execId;
|
||||
const agentName = (exec.agentConfig && exec.agentConfig.agent_name) || exec.agentId || 'Agente';
|
||||
option.textContent = `${agentName} — ${new Date(exec.startedAt).toLocaleTimeString('pt-BR')}`;
|
||||
terminalSelect.appendChild(option);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
},
|
||||
|
||||
startPeriodicRefresh() {
|
||||
setInterval(async () => {
|
||||
await App._updateActiveBadge();
|
||||
|
||||
if (App.currentSection === 'dashboard') {
|
||||
await DashboardUI.load();
|
||||
}
|
||||
}, 30000);
|
||||
},
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => App.init());
|
||||
|
||||
window.App = App;
|
||||
309
public/js/components/agents.js
Normal file
309
public/js/components/agents.js
Normal file
@@ -0,0 +1,309 @@
|
||||
const AgentsUI = {
|
||||
agents: [],
|
||||
|
||||
avatarColors: [
|
||||
'#6366f1',
|
||||
'#8b5cf6',
|
||||
'#ec4899',
|
||||
'#f59e0b',
|
||||
'#10b981',
|
||||
'#3b82f6',
|
||||
'#ef4444',
|
||||
'#14b8a6',
|
||||
],
|
||||
|
||||
async load() {
|
||||
try {
|
||||
AgentsUI.agents = await API.agents.list();
|
||||
AgentsUI.render();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao carregar agentes: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const grid = document.getElementById('agents-grid');
|
||||
const empty = document.getElementById('agents-empty-state');
|
||||
|
||||
if (!grid) return;
|
||||
|
||||
if (AgentsUI.agents.length === 0) {
|
||||
if (empty) empty.style.display = 'flex';
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty) empty.style.display = 'none';
|
||||
|
||||
const existingCards = grid.querySelectorAll('.agent-card');
|
||||
existingCards.forEach((c) => c.remove());
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
AgentsUI.agents.forEach((agent) => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = AgentsUI.renderCard(agent);
|
||||
fragment.appendChild(wrapper.firstElementChild);
|
||||
});
|
||||
|
||||
grid.appendChild(fragment);
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [grid] });
|
||||
},
|
||||
|
||||
renderCard(agent) {
|
||||
const name = agent.agent_name || agent.name || 'Sem nome';
|
||||
const color = AgentsUI.getAvatarColor(name);
|
||||
const initials = AgentsUI.getInitials(name);
|
||||
const statusLabel = agent.status === 'active' ? 'Ativo' : 'Inativo';
|
||||
const statusClass = agent.status === 'active' ? 'badge-active' : 'badge-inactive';
|
||||
const model = (agent.config && agent.config.model) || agent.model || 'claude-sonnet-4-6';
|
||||
const updatedAt = AgentsUI.formatDate(agent.updated_at || agent.updatedAt || agent.created_at || agent.createdAt);
|
||||
|
||||
return `
|
||||
<div class="agent-card" data-agent-id="${agent.id}">
|
||||
<div class="agent-card-body">
|
||||
<div class="agent-card-top">
|
||||
<div class="agent-avatar" style="background-color: ${color}" aria-hidden="true">
|
||||
<span>${initials}</span>
|
||||
</div>
|
||||
<div class="agent-info">
|
||||
<h3 class="agent-name">${name}</h3>
|
||||
<span class="badge ${statusClass}">${statusLabel}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${agent.description ? `<p class="agent-description">${agent.description}</p>` : ''}
|
||||
|
||||
<div class="agent-meta">
|
||||
<span class="agent-meta-item">
|
||||
<i data-lucide="cpu"></i>
|
||||
${model}
|
||||
</span>
|
||||
<span class="agent-meta-item">
|
||||
<i data-lucide="clock"></i>
|
||||
${updatedAt}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="agent-actions">
|
||||
<button class="btn btn-primary btn-sm" data-action="execute" data-id="${agent.id}">
|
||||
<i data-lucide="play"></i>
|
||||
Executar
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" data-action="edit" data-id="${agent.id}">
|
||||
<i data-lucide="pencil"></i>
|
||||
Editar
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-icon btn-sm" data-action="export" data-id="${agent.id}" title="Exportar agente">
|
||||
<i data-lucide="download"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-icon btn-sm btn-danger" data-action="delete" data-id="${agent.id}" title="Excluir agente">
|
||||
<i data-lucide="trash-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
openCreateModal() {
|
||||
const form = document.getElementById('agent-form');
|
||||
if (form) form.reset();
|
||||
|
||||
const idField = document.getElementById('agent-form-id');
|
||||
if (idField) idField.value = '';
|
||||
|
||||
const titleEl = document.getElementById('agent-modal-title');
|
||||
if (titleEl) titleEl.textContent = 'Novo Agente';
|
||||
|
||||
const toggle = document.getElementById('agent-status-toggle');
|
||||
if (toggle) toggle.checked = true;
|
||||
|
||||
const tagsHidden = document.getElementById('agent-tags');
|
||||
if (tagsHidden) tagsHidden.value = '[]';
|
||||
|
||||
const tagsChips = document.getElementById('agent-tags-chips');
|
||||
if (tagsChips) tagsChips.innerHTML = '';
|
||||
|
||||
Modal.open('agent-modal-overlay');
|
||||
},
|
||||
|
||||
async openEditModal(agentId) {
|
||||
try {
|
||||
const agent = await API.agents.get(agentId);
|
||||
|
||||
const titleEl = document.getElementById('agent-modal-title');
|
||||
if (titleEl) titleEl.textContent = 'Editar Agente';
|
||||
|
||||
const fields = {
|
||||
'agent-form-id': agent.id,
|
||||
'agent-name': agent.agent_name || agent.name || '',
|
||||
'agent-description': agent.description || '',
|
||||
'agent-system-prompt': (agent.config && agent.config.systemPrompt) || '',
|
||||
'agent-model': (agent.config && agent.config.model) || 'claude-sonnet-4-6',
|
||||
'agent-workdir': (agent.config && agent.config.workingDirectory) || '',
|
||||
};
|
||||
|
||||
for (const [fieldId, value] of Object.entries(fields)) {
|
||||
const el = document.getElementById(fieldId);
|
||||
if (el) el.value = value;
|
||||
}
|
||||
|
||||
const toggle = document.getElementById('agent-status-toggle');
|
||||
if (toggle) toggle.checked = agent.status === 'active';
|
||||
|
||||
const tags = Array.isArray(agent.tags) ? agent.tags : [];
|
||||
const tagsHidden = document.getElementById('agent-tags');
|
||||
if (tagsHidden) tagsHidden.value = JSON.stringify(tags);
|
||||
|
||||
const tagsChips = document.getElementById('agent-tags-chips');
|
||||
if (tagsChips) {
|
||||
tagsChips.innerHTML = tags.map((t) =>
|
||||
`<span class="tag-chip">${t}<button type="button" data-tag="${t}" class="tag-remove" aria-label="Remover tag ${t}">×</button></span>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
Modal.open('agent-modal-overlay');
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao carregar agente: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async save() {
|
||||
const idEl = document.getElementById('agent-form-id');
|
||||
const id = idEl ? idEl.value.trim() : '';
|
||||
|
||||
const nameEl = document.getElementById('agent-name');
|
||||
if (!nameEl || !nameEl.value.trim()) {
|
||||
Toast.warning('Nome do agente é obrigatório');
|
||||
return;
|
||||
}
|
||||
|
||||
const tagsHidden = document.getElementById('agent-tags');
|
||||
let tags = [];
|
||||
try {
|
||||
tags = JSON.parse(tagsHidden?.value || '[]');
|
||||
} catch {
|
||||
tags = [];
|
||||
}
|
||||
|
||||
const toggle = document.getElementById('agent-status-toggle');
|
||||
|
||||
const data = {
|
||||
agent_name: nameEl.value.trim(),
|
||||
description: document.getElementById('agent-description')?.value.trim() || '',
|
||||
status: toggle && toggle.checked ? 'active' : 'inactive',
|
||||
config: {
|
||||
systemPrompt: document.getElementById('agent-system-prompt')?.value.trim() || '',
|
||||
model: document.getElementById('agent-model')?.value || 'claude-sonnet-4-6',
|
||||
workingDirectory: document.getElementById('agent-workdir')?.value.trim() || '',
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
if (id) {
|
||||
await API.agents.update(id, data);
|
||||
Toast.success('Agente atualizado com sucesso');
|
||||
} else {
|
||||
await API.agents.create(data);
|
||||
Toast.success('Agente criado com sucesso');
|
||||
}
|
||||
|
||||
Modal.close('agent-modal-overlay');
|
||||
await AgentsUI.load();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao salvar agente: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async delete(agentId) {
|
||||
const confirmed = await Modal.confirm(
|
||||
'Excluir agente',
|
||||
'Tem certeza que deseja excluir este agente? Esta ação não pode ser desfeita.'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await API.agents.delete(agentId);
|
||||
Toast.success('Agente excluído com sucesso');
|
||||
await AgentsUI.load();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao excluir agente: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async execute(agentId) {
|
||||
const agent = AgentsUI.agents.find((a) => a.id === agentId);
|
||||
|
||||
try {
|
||||
const allAgents = AgentsUI.agents.length > 0 ? AgentsUI.agents : await API.agents.list();
|
||||
const selectEl = document.getElementById('execute-agent-select');
|
||||
|
||||
if (selectEl) {
|
||||
selectEl.innerHTML = '<option value="">Selecionar agente...</option>' +
|
||||
allAgents
|
||||
.filter((a) => a.status === 'active')
|
||||
.map((a) => `<option value="${a.id}">${a.agent_name || a.name}</option>`)
|
||||
.join('');
|
||||
|
||||
selectEl.value = agentId;
|
||||
}
|
||||
|
||||
const hiddenId = document.getElementById('execute-agent-id');
|
||||
if (hiddenId) hiddenId.value = agentId;
|
||||
|
||||
const taskEl = document.getElementById('execute-task-desc');
|
||||
if (taskEl) taskEl.value = '';
|
||||
|
||||
const instructionsEl = document.getElementById('execute-instructions');
|
||||
if (instructionsEl) instructionsEl.value = '';
|
||||
|
||||
Modal.open('execute-modal-overlay');
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao abrir modal de execução: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async export(agentId) {
|
||||
try {
|
||||
const data = await API.agents.export(agentId);
|
||||
const jsonEl = document.getElementById('export-code-content');
|
||||
if (jsonEl) jsonEl.textContent = JSON.stringify(data, null, 2);
|
||||
Modal.open('export-modal-overlay');
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao exportar agente: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
getAvatarColor(name) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
const index = Math.abs(hash) % AgentsUI.avatarColors.length;
|
||||
return AgentsUI.avatarColors[index];
|
||||
},
|
||||
|
||||
getInitials(name) {
|
||||
return name
|
||||
.split(' ')
|
||||
.slice(0, 2)
|
||||
.map((w) => w[0])
|
||||
.join('')
|
||||
.toUpperCase();
|
||||
},
|
||||
|
||||
formatDate(isoString) {
|
||||
if (!isoString) return '—';
|
||||
const date = new Date(isoString);
|
||||
return date.toLocaleDateString('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
window.AgentsUI = AgentsUI;
|
||||
118
public/js/components/dashboard.js
Normal file
118
public/js/components/dashboard.js
Normal file
@@ -0,0 +1,118 @@
|
||||
const DashboardUI = {
|
||||
async load() {
|
||||
try {
|
||||
const [status, agents] = await Promise.all([
|
||||
API.system.status(),
|
||||
API.agents.list(),
|
||||
]);
|
||||
|
||||
DashboardUI.updateMetrics(status, agents);
|
||||
DashboardUI.updateRecentActivity(status.executions?.list || []);
|
||||
DashboardUI.updateSystemStatus(status);
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao carregar dashboard: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
updateMetrics(status, agents) {
|
||||
const metrics = {
|
||||
'metric-total-agents': status.agents?.total ?? (agents?.length ?? 0),
|
||||
'metric-active-agents': status.agents?.active ?? 0,
|
||||
'metric-executions-today': status.executions?.active ?? 0,
|
||||
'metric-schedules': status.schedules?.total ?? 0,
|
||||
};
|
||||
|
||||
for (const [id, target] of Object.entries(metrics)) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) continue;
|
||||
|
||||
const current = parseInt(el.textContent, 10) || 0;
|
||||
DashboardUI._animateCount(el, current, target);
|
||||
}
|
||||
},
|
||||
|
||||
_animateCount(el, from, to) {
|
||||
const duration = 600;
|
||||
const start = performance.now();
|
||||
|
||||
const step = (now) => {
|
||||
const elapsed = now - start;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
const value = Math.round(from + (to - from) * eased);
|
||||
el.textContent = value;
|
||||
|
||||
if (progress < 1) requestAnimationFrame(step);
|
||||
};
|
||||
|
||||
requestAnimationFrame(step);
|
||||
},
|
||||
|
||||
updateRecentActivity(executions) {
|
||||
const list = document.getElementById('activity-list');
|
||||
if (!list) return;
|
||||
|
||||
if (!executions || executions.length === 0) {
|
||||
list.innerHTML = `
|
||||
<li class="activity-empty">
|
||||
<i data-lucide="inbox"></i>
|
||||
<span>Nenhuma execução recente</span>
|
||||
</li>
|
||||
`;
|
||||
if (window.lucide) lucide.createIcons({ nodes: [list] });
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = executions.map((exec) => {
|
||||
const statusClass = DashboardUI._statusBadgeClass(exec.status);
|
||||
const statusLabel = DashboardUI._statusLabel(exec.status);
|
||||
const time = exec.startedAt
|
||||
? new Date(exec.startedAt).toLocaleTimeString('pt-BR')
|
||||
: '—';
|
||||
|
||||
return `
|
||||
<li class="activity-item">
|
||||
<div class="activity-item-info">
|
||||
<span class="activity-item-agent">${exec.agentName || exec.agentId || 'Agente'}</span>
|
||||
<span class="activity-item-task">${exec.task || ''}</span>
|
||||
</div>
|
||||
<div class="activity-item-meta">
|
||||
<span class="badge ${statusClass}">${statusLabel}</span>
|
||||
<span class="activity-item-time">${time}</span>
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
}).join('');
|
||||
},
|
||||
|
||||
updateSystemStatus(status) {
|
||||
const wsBadge = document.getElementById('system-ws-status-badge');
|
||||
if (wsBadge) {
|
||||
const wsConnected = document.getElementById('ws-indicator')?.classList.contains('ws-indicator--connected');
|
||||
wsBadge.textContent = wsConnected ? 'Conectado' : 'Desconectado';
|
||||
wsBadge.className = `badge ${wsConnected ? 'badge--green' : 'badge--red'}`;
|
||||
}
|
||||
},
|
||||
|
||||
_statusBadgeClass(status) {
|
||||
const map = {
|
||||
running: 'badge--blue',
|
||||
completed: 'badge--green',
|
||||
error: 'badge--red',
|
||||
cancelled: 'badge--gray',
|
||||
};
|
||||
return map[status] || 'badge--gray';
|
||||
},
|
||||
|
||||
_statusLabel(status) {
|
||||
const map = {
|
||||
running: 'Em execução',
|
||||
completed: 'Concluído',
|
||||
error: 'Erro',
|
||||
cancelled: 'Cancelado',
|
||||
};
|
||||
return map[status] || status || 'Desconhecido';
|
||||
},
|
||||
};
|
||||
|
||||
window.DashboardUI = DashboardUI;
|
||||
106
public/js/components/modal.js
Normal file
106
public/js/components/modal.js
Normal file
@@ -0,0 +1,106 @@
|
||||
const Modal = {
|
||||
_confirmResolve: null,
|
||||
|
||||
open(modalId) {
|
||||
const overlay = document.getElementById(modalId);
|
||||
if (!overlay) return;
|
||||
|
||||
overlay.hidden = false;
|
||||
requestAnimationFrame(() => overlay.classList.add('active'));
|
||||
|
||||
const firstInput = overlay.querySelector('input:not([type="hidden"]), textarea, select');
|
||||
if (firstInput) {
|
||||
setTimeout(() => firstInput.focus(), 50);
|
||||
}
|
||||
},
|
||||
|
||||
close(modalId) {
|
||||
const overlay = document.getElementById(modalId);
|
||||
if (!overlay) return;
|
||||
|
||||
overlay.classList.remove('active');
|
||||
setTimeout(() => { overlay.hidden = true; }, 200);
|
||||
|
||||
const form = overlay.querySelector('form');
|
||||
if (form) form.reset();
|
||||
},
|
||||
|
||||
closeAll() {
|
||||
document.querySelectorAll('.modal-overlay').forEach((overlay) => {
|
||||
if (!overlay.hidden) {
|
||||
overlay.classList.remove('active');
|
||||
setTimeout(() => { overlay.hidden = true; }, 200);
|
||||
const form = overlay.querySelector('form');
|
||||
if (form) form.reset();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
confirm(title, message) {
|
||||
return new Promise((resolve) => {
|
||||
Modal._confirmResolve = resolve;
|
||||
|
||||
const titleEl = document.getElementById('confirm-modal-title');
|
||||
const messageEl = document.getElementById('confirm-modal-message');
|
||||
|
||||
if (titleEl) titleEl.textContent = title;
|
||||
if (messageEl) messageEl.textContent = message;
|
||||
|
||||
Modal.open('confirm-modal-overlay');
|
||||
});
|
||||
},
|
||||
|
||||
_resolveConfirm(result) {
|
||||
Modal.close('confirm-modal-overlay');
|
||||
if (Modal._confirmResolve) {
|
||||
Modal._confirmResolve(result);
|
||||
Modal._confirmResolve = null;
|
||||
}
|
||||
},
|
||||
|
||||
_setupListeners() {
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('modal-overlay')) {
|
||||
const modalId = e.target.id;
|
||||
|
||||
if (modalId === 'confirm-modal-overlay') {
|
||||
Modal._resolveConfirm(false);
|
||||
} else {
|
||||
Modal.close(modalId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const closeBtn = e.target.closest('[data-modal-close]');
|
||||
if (closeBtn) {
|
||||
const targetId = closeBtn.dataset.modalClose;
|
||||
|
||||
if (targetId === 'confirm-modal-overlay') {
|
||||
Modal._resolveConfirm(false);
|
||||
} else {
|
||||
Modal.close(targetId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
|
||||
const activeModal = document.querySelector('.modal-overlay.active');
|
||||
if (!activeModal) return;
|
||||
|
||||
if (activeModal.id === 'confirm-modal-overlay') {
|
||||
Modal._resolveConfirm(false);
|
||||
} else {
|
||||
Modal.close(activeModal.id);
|
||||
}
|
||||
});
|
||||
|
||||
const confirmBtn = document.getElementById('confirm-modal-confirm-btn');
|
||||
if (confirmBtn) confirmBtn.addEventListener('click', () => Modal._resolveConfirm(true));
|
||||
},
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => Modal._setupListeners());
|
||||
|
||||
window.Modal = Modal;
|
||||
361
public/js/components/pipelines.js
Normal file
361
public/js/components/pipelines.js
Normal file
@@ -0,0 +1,361 @@
|
||||
const PipelinesUI = {
|
||||
pipelines: [],
|
||||
agents: [],
|
||||
_editingId: null,
|
||||
_steps: [],
|
||||
|
||||
async load() {
|
||||
try {
|
||||
const [pipelines, agents] = await Promise.all([
|
||||
API.pipelines.list(),
|
||||
API.agents.list(),
|
||||
]);
|
||||
PipelinesUI.pipelines = Array.isArray(pipelines) ? pipelines : [];
|
||||
PipelinesUI.agents = Array.isArray(agents) ? agents : [];
|
||||
PipelinesUI.render();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao carregar pipelines: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const grid = document.getElementById('pipelines-grid');
|
||||
if (!grid) return;
|
||||
|
||||
const existingCards = grid.querySelectorAll('.pipeline-card');
|
||||
existingCards.forEach((c) => c.remove());
|
||||
|
||||
const emptyState = grid.querySelector('.empty-state');
|
||||
|
||||
if (PipelinesUI.pipelines.length === 0) {
|
||||
if (!emptyState) {
|
||||
grid.insertAdjacentHTML('beforeend', PipelinesUI.renderEmpty());
|
||||
}
|
||||
if (window.lucide) lucide.createIcons({ nodes: [grid] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (emptyState) emptyState.remove();
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
PipelinesUI.pipelines.forEach((pipeline) => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = PipelinesUI.renderCard(pipeline);
|
||||
fragment.appendChild(wrapper.firstElementChild);
|
||||
});
|
||||
|
||||
grid.appendChild(fragment);
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [grid] });
|
||||
},
|
||||
|
||||
renderEmpty() {
|
||||
return `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">
|
||||
<i data-lucide="git-merge"></i>
|
||||
</div>
|
||||
<h3 class="empty-state-title">Nenhum pipeline cadastrado</h3>
|
||||
<p class="empty-state-desc">Crie seu primeiro pipeline para encadear agentes em fluxos automatizados.</p>
|
||||
<button class="btn btn--primary btn--icon-text" type="button" id="pipelines-empty-new-btn">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Criar Pipeline</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
renderCard(pipeline) {
|
||||
const steps = Array.isArray(pipeline.steps) ? pipeline.steps : [];
|
||||
const stepCount = steps.length;
|
||||
|
||||
const flowHtml = steps.map((step, index) => {
|
||||
const agentName = step.agentName || step.agentId || 'Agente';
|
||||
const isLast = index === steps.length - 1;
|
||||
return `
|
||||
<span class="pipeline-step-badge">
|
||||
<span class="pipeline-step-number">${index + 1}</span>
|
||||
${agentName}
|
||||
</span>
|
||||
${!isLast ? '<span class="pipeline-flow-arrow">→</span>' : ''}
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="agent-card pipeline-card" data-pipeline-id="${pipeline.id}">
|
||||
<div class="agent-card-body">
|
||||
<div class="agent-card-top">
|
||||
<div class="agent-info">
|
||||
<h3 class="agent-name">${pipeline.name || 'Sem nome'}</h3>
|
||||
<span class="badge badge-active">${stepCount} ${stepCount === 1 ? 'passo' : 'passos'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${pipeline.description ? `<p class="agent-description">${pipeline.description}</p>` : ''}
|
||||
|
||||
<div class="pipeline-flow">
|
||||
${flowHtml || '<span class="agent-description">Nenhum passo configurado</span>'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="agent-actions">
|
||||
<button class="btn btn-primary btn-sm" data-action="execute-pipeline" data-id="${pipeline.id}">
|
||||
<i data-lucide="play"></i>
|
||||
Executar
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" data-action="edit-pipeline" data-id="${pipeline.id}">
|
||||
<i data-lucide="pencil"></i>
|
||||
Editar
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-icon btn-sm btn-danger" data-action="delete-pipeline" data-id="${pipeline.id}" title="Excluir pipeline">
|
||||
<i data-lucide="trash-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
openCreateModal() {
|
||||
PipelinesUI._editingId = null;
|
||||
PipelinesUI._steps = [
|
||||
{ agentId: '', inputTemplate: '' },
|
||||
{ agentId: '', inputTemplate: '' },
|
||||
];
|
||||
|
||||
const titleEl = document.getElementById('pipeline-modal-title');
|
||||
if (titleEl) titleEl.textContent = 'Novo Pipeline';
|
||||
|
||||
const idEl = document.getElementById('pipeline-form-id');
|
||||
if (idEl) idEl.value = '';
|
||||
|
||||
const nameEl = document.getElementById('pipeline-name');
|
||||
if (nameEl) nameEl.value = '';
|
||||
|
||||
const descEl = document.getElementById('pipeline-description');
|
||||
if (descEl) descEl.value = '';
|
||||
|
||||
PipelinesUI.renderSteps();
|
||||
Modal.open('pipeline-modal-overlay');
|
||||
},
|
||||
|
||||
async openEditModal(pipelineId) {
|
||||
try {
|
||||
const pipeline = await API.pipelines.get(pipelineId);
|
||||
|
||||
PipelinesUI._editingId = pipelineId;
|
||||
PipelinesUI._steps = Array.isArray(pipeline.steps)
|
||||
? pipeline.steps.map((s) => ({ agentId: s.agentId || '', inputTemplate: s.inputTemplate || '' }))
|
||||
: [];
|
||||
|
||||
const titleEl = document.getElementById('pipeline-modal-title');
|
||||
if (titleEl) titleEl.textContent = 'Editar Pipeline';
|
||||
|
||||
const idEl = document.getElementById('pipeline-form-id');
|
||||
if (idEl) idEl.value = pipeline.id;
|
||||
|
||||
const nameEl = document.getElementById('pipeline-name');
|
||||
if (nameEl) nameEl.value = pipeline.name || '';
|
||||
|
||||
const descEl = document.getElementById('pipeline-description');
|
||||
if (descEl) descEl.value = pipeline.description || '';
|
||||
|
||||
PipelinesUI.renderSteps();
|
||||
Modal.open('pipeline-modal-overlay');
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao carregar pipeline: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
renderSteps() {
|
||||
const container = document.getElementById('pipeline-steps-container');
|
||||
if (!container) return;
|
||||
|
||||
if (PipelinesUI._steps.length === 0) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const agentOptions = PipelinesUI.agents
|
||||
.map((a) => `<option value="${a.id}">${a.agent_name || a.name}</option>`)
|
||||
.join('');
|
||||
|
||||
container.innerHTML = PipelinesUI._steps.map((step, index) => {
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === PipelinesUI._steps.length - 1;
|
||||
const connectorHtml = !isLast
|
||||
? '<div class="pipeline-step-connector"><i data-lucide="arrow-down" style="width:14px;height:14px"></i></div>'
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="pipeline-step-row" data-step-index="${index}">
|
||||
<span class="pipeline-step-number-lg">${index + 1}</span>
|
||||
<div class="pipeline-step-content">
|
||||
<select class="select" data-step-field="agentId" data-step-index="${index}">
|
||||
<option value="">Selecionar agente...</option>
|
||||
${agentOptions}
|
||||
</select>
|
||||
<textarea
|
||||
class="textarea"
|
||||
rows="2"
|
||||
placeholder="{{input}} será substituído pelo output anterior"
|
||||
data-step-field="inputTemplate"
|
||||
data-step-index="${index}"
|
||||
>${step.inputTemplate || ''}</textarea>
|
||||
</div>
|
||||
<div class="pipeline-step-actions">
|
||||
<button class="btn btn-ghost btn-icon btn-sm" type="button" data-step-action="move-up" data-step-index="${index}" title="Mover para cima" ${isFirst ? 'disabled' : ''}>
|
||||
<i data-lucide="chevron-up" style="width:14px;height:14px"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-icon btn-sm" type="button" data-step-action="move-down" data-step-index="${index}" title="Mover para baixo" ${isLast ? 'disabled' : ''}>
|
||||
<i data-lucide="chevron-down" style="width:14px;height:14px"></i>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-icon btn-sm btn-danger" type="button" data-step-action="remove" data-step-index="${index}" title="Remover passo">
|
||||
<i data-lucide="x" style="width:14px;height:14px"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${connectorHtml}
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.querySelectorAll('select[data-step-field="agentId"]').forEach((select) => {
|
||||
const index = parseInt(select.dataset.stepIndex, 10);
|
||||
select.value = PipelinesUI._steps[index].agentId || '';
|
||||
});
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
},
|
||||
|
||||
_syncStepsFromDOM() {
|
||||
const container = document.getElementById('pipeline-steps-container');
|
||||
if (!container) return;
|
||||
|
||||
container.querySelectorAll('[data-step-field]').forEach((el) => {
|
||||
const index = parseInt(el.dataset.stepIndex, 10);
|
||||
const field = el.dataset.stepField;
|
||||
if (PipelinesUI._steps[index] !== undefined) {
|
||||
PipelinesUI._steps[index][field] = el.value;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addStep() {
|
||||
PipelinesUI._syncStepsFromDOM();
|
||||
PipelinesUI._steps.push({ agentId: '', inputTemplate: '' });
|
||||
PipelinesUI.renderSteps();
|
||||
},
|
||||
|
||||
removeStep(index) {
|
||||
PipelinesUI._syncStepsFromDOM();
|
||||
PipelinesUI._steps.splice(index, 1);
|
||||
PipelinesUI.renderSteps();
|
||||
},
|
||||
|
||||
moveStep(index, direction) {
|
||||
PipelinesUI._syncStepsFromDOM();
|
||||
const targetIndex = index + direction;
|
||||
if (targetIndex < 0 || targetIndex >= PipelinesUI._steps.length) return;
|
||||
const temp = PipelinesUI._steps[index];
|
||||
PipelinesUI._steps[index] = PipelinesUI._steps[targetIndex];
|
||||
PipelinesUI._steps[targetIndex] = temp;
|
||||
PipelinesUI.renderSteps();
|
||||
},
|
||||
|
||||
async save() {
|
||||
PipelinesUI._syncStepsFromDOM();
|
||||
|
||||
const name = document.getElementById('pipeline-name')?.value.trim();
|
||||
if (!name) {
|
||||
Toast.warning('Nome do pipeline é obrigatório');
|
||||
return;
|
||||
}
|
||||
|
||||
if (PipelinesUI._steps.length < 2) {
|
||||
Toast.warning('O pipeline precisa de pelo menos 2 passos');
|
||||
return;
|
||||
}
|
||||
|
||||
const invalidStep = PipelinesUI._steps.find((s) => !s.agentId);
|
||||
if (invalidStep) {
|
||||
Toast.warning('Todos os passos devem ter um agente selecionado');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
name,
|
||||
description: document.getElementById('pipeline-description')?.value.trim() || '',
|
||||
steps: PipelinesUI._steps.map((s) => ({
|
||||
agentId: s.agentId,
|
||||
inputTemplate: s.inputTemplate || '',
|
||||
})),
|
||||
};
|
||||
|
||||
try {
|
||||
if (PipelinesUI._editingId) {
|
||||
await API.pipelines.update(PipelinesUI._editingId, data);
|
||||
Toast.success('Pipeline atualizado com sucesso');
|
||||
} else {
|
||||
await API.pipelines.create(data);
|
||||
Toast.success('Pipeline criado com sucesso');
|
||||
}
|
||||
|
||||
Modal.close('pipeline-modal-overlay');
|
||||
await PipelinesUI.load();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao salvar pipeline: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async delete(pipelineId) {
|
||||
const confirmed = await Modal.confirm(
|
||||
'Excluir pipeline',
|
||||
'Tem certeza que deseja excluir este pipeline? Esta ação não pode ser desfeita.'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await API.pipelines.delete(pipelineId);
|
||||
Toast.success('Pipeline excluído com sucesso');
|
||||
await PipelinesUI.load();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao excluir pipeline: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
execute(pipelineId) {
|
||||
const pipeline = PipelinesUI.pipelines.find((p) => p.id === pipelineId);
|
||||
|
||||
const titleEl = document.getElementById('pipeline-execute-title');
|
||||
if (titleEl) titleEl.textContent = `Executar: ${pipeline ? pipeline.name : 'Pipeline'}`;
|
||||
|
||||
const idEl = document.getElementById('pipeline-execute-id');
|
||||
if (idEl) idEl.value = pipelineId;
|
||||
|
||||
const inputEl = document.getElementById('pipeline-execute-input');
|
||||
if (inputEl) inputEl.value = '';
|
||||
|
||||
Modal.open('pipeline-execute-modal-overlay');
|
||||
},
|
||||
|
||||
async _executeFromModal() {
|
||||
const pipelineId = document.getElementById('pipeline-execute-id')?.value;
|
||||
const input = document.getElementById('pipeline-execute-input')?.value.trim();
|
||||
|
||||
if (!input) {
|
||||
Toast.warning('O input inicial é obrigatório');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await API.pipelines.execute(pipelineId, input);
|
||||
Modal.close('pipeline-execute-modal-overlay');
|
||||
App.navigateTo('terminal');
|
||||
Toast.info('Pipeline iniciado');
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao executar pipeline: ${err.message}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
window.PipelinesUI = PipelinesUI;
|
||||
182
public/js/components/schedules.js
Normal file
182
public/js/components/schedules.js
Normal file
@@ -0,0 +1,182 @@
|
||||
const SchedulesUI = {
|
||||
schedules: [],
|
||||
|
||||
async load() {
|
||||
try {
|
||||
SchedulesUI.schedules = await API.schedules.list();
|
||||
SchedulesUI.render();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao carregar agendamentos: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const tbody = document.getElementById('schedules-tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
if (SchedulesUI.schedules.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr class="table-empty-row">
|
||||
<td colspan="6">
|
||||
<div class="empty-state empty-state--inline">
|
||||
<i data-lucide="clock"></i>
|
||||
<span>Nenhum agendamento configurado</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
if (window.lucide) lucide.createIcons({ nodes: [tbody] });
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = SchedulesUI.schedules.map((schedule) => {
|
||||
const cronExpr = schedule.cronExpression || schedule.cronExpr || '';
|
||||
const statusClass = schedule.active ? 'badge-active' : 'badge-inactive';
|
||||
const statusLabel = schedule.active ? 'Ativo' : 'Inativo';
|
||||
const humanCron = SchedulesUI.cronToHuman(cronExpr);
|
||||
const nextRun = schedule.nextRun
|
||||
? new Date(schedule.nextRun).toLocaleString('pt-BR')
|
||||
: '—';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${schedule.agentName || schedule.agentId || '—'}</td>
|
||||
<td class="schedule-task-cell" title="${schedule.taskDescription || ''}">${schedule.taskDescription || '—'}</td>
|
||||
<td>
|
||||
<span title="${cronExpr}">${humanCron}</span>
|
||||
<small class="font-mono">${cronExpr}</small>
|
||||
</td>
|
||||
<td>${nextRun}</td>
|
||||
<td><span class="badge ${statusClass}">${statusLabel}</span></td>
|
||||
<td>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm btn-danger"
|
||||
data-action="delete-schedule"
|
||||
data-id="${schedule.taskId}"
|
||||
title="Remover agendamento"
|
||||
aria-label="Remover agendamento"
|
||||
>
|
||||
<i data-lucide="trash-2"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [tbody] });
|
||||
},
|
||||
|
||||
async openCreateModal() {
|
||||
try {
|
||||
const agents = await API.agents.list();
|
||||
const select = document.getElementById('schedule-agent');
|
||||
|
||||
if (select) {
|
||||
select.innerHTML = '<option value="">Selecionar agente...</option>' +
|
||||
agents
|
||||
.filter((a) => a.status === 'active')
|
||||
.map((a) => `<option value="${a.id}">${a.agent_name || a.name}</option>`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
const taskEl = document.getElementById('schedule-task');
|
||||
if (taskEl) taskEl.value = '';
|
||||
|
||||
const cronEl = document.getElementById('schedule-cron');
|
||||
if (cronEl) cronEl.value = '';
|
||||
|
||||
Modal.open('schedule-modal-overlay');
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao abrir modal de agendamento: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async save() {
|
||||
const agentId = document.getElementById('schedule-agent')?.value;
|
||||
const taskDescription = document.getElementById('schedule-task')?.value.trim();
|
||||
const cronExpression = document.getElementById('schedule-cron')?.value.trim();
|
||||
|
||||
if (!agentId) {
|
||||
Toast.warning('Selecione um agente');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!taskDescription) {
|
||||
Toast.warning('Descrição da tarefa é obrigatória');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cronExpression) {
|
||||
Toast.warning('Expressão cron é obrigatória');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await API.schedules.create({ agentId, taskDescription, cronExpression });
|
||||
Toast.success('Agendamento criado com sucesso');
|
||||
Modal.close('schedule-modal-overlay');
|
||||
await SchedulesUI.load();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao criar agendamento: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async delete(taskId) {
|
||||
const confirmed = await Modal.confirm(
|
||||
'Remover agendamento',
|
||||
'Tem certeza que deseja remover este agendamento?'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await API.schedules.delete(taskId);
|
||||
Toast.success('Agendamento removido com sucesso');
|
||||
await SchedulesUI.load();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao remover agendamento: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
cronToHuman(expression) {
|
||||
if (!expression) return '—';
|
||||
|
||||
const presets = {
|
||||
'* * * * *': 'A cada minuto',
|
||||
'*/5 * * * *': 'A cada 5 minutos',
|
||||
'*/10 * * * *': 'A cada 10 minutos',
|
||||
'*/15 * * * *': 'A cada 15 minutos',
|
||||
'*/30 * * * *': 'A cada 30 minutos',
|
||||
'0 * * * *': 'A cada hora',
|
||||
'0 */2 * * *': 'A cada 2 horas',
|
||||
'0 */6 * * *': 'A cada 6 horas',
|
||||
'0 */12 * * *': 'A cada 12 horas',
|
||||
'0 0 * * *': 'Todo dia à meia-noite',
|
||||
'0 9 * * *': 'Todo dia às 9h',
|
||||
'0 18 * * *': 'Todo dia às 18h',
|
||||
'0 0 * * 1': 'Toda segunda-feira',
|
||||
'0 0 * * 1-5': 'Dias úteis à meia-noite',
|
||||
'0 9 * * 1-5': 'Dias úteis às 9h',
|
||||
'0 9 * * 1': 'Semanal (seg 09:00)',
|
||||
'0 0 1 * *': 'Todo primeiro do mês',
|
||||
'0 0 1 1 *': 'Todo 1º de janeiro',
|
||||
};
|
||||
|
||||
if (presets[expression]) return presets[expression];
|
||||
|
||||
const parts = expression.split(' ');
|
||||
if (parts.length !== 5) return expression;
|
||||
|
||||
const [minute, hour, day, month, weekday] = parts;
|
||||
|
||||
if (minute.startsWith('*/')) return `A cada ${minute.slice(2)} minutos`;
|
||||
if (hour.startsWith('*/') && minute === '0') return `A cada ${hour.slice(2)} horas`;
|
||||
if (minute === '0' && hour !== '*' && day === '*' && month === '*' && weekday === '*') {
|
||||
return `Todo dia às ${hour.padStart(2, '0')}h`;
|
||||
}
|
||||
|
||||
return expression;
|
||||
},
|
||||
};
|
||||
|
||||
window.SchedulesUI = SchedulesUI;
|
||||
191
public/js/components/tasks.js
Normal file
191
public/js/components/tasks.js
Normal file
@@ -0,0 +1,191 @@
|
||||
const TasksUI = {
|
||||
tasks: [],
|
||||
|
||||
async load() {
|
||||
try {
|
||||
TasksUI.tasks = await API.tasks.list();
|
||||
TasksUI.render();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao carregar tarefas: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const container = document.getElementById('tasks-grid');
|
||||
const empty = document.getElementById('tasks-empty-state');
|
||||
|
||||
if (!container) return;
|
||||
|
||||
const existingCards = container.querySelectorAll('.task-card');
|
||||
existingCards.forEach((c) => c.remove());
|
||||
|
||||
if (TasksUI.tasks.length === 0) {
|
||||
if (empty) empty.style.display = 'flex';
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty) empty.style.display = 'none';
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
TasksUI.tasks.forEach((task) => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = TasksUI._renderCard(task);
|
||||
fragment.appendChild(wrapper.firstElementChild);
|
||||
});
|
||||
|
||||
container.appendChild(fragment);
|
||||
|
||||
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||
},
|
||||
|
||||
_renderCard(task) {
|
||||
const categoryClass = TasksUI._categoryClass(task.category);
|
||||
const categoryLabel = task.category || 'Geral';
|
||||
const createdAt = TasksUI._formatDate(task.createdAt);
|
||||
|
||||
return `
|
||||
<div class="task-card" data-task-id="${task.id}">
|
||||
<div class="task-card-header">
|
||||
<h4 class="task-card-name">${task.name}</h4>
|
||||
<span class="badge ${categoryClass}">${categoryLabel}</span>
|
||||
</div>
|
||||
${task.description ? `<p class="task-card-description">${task.description}</p>` : ''}
|
||||
<div class="task-card-footer">
|
||||
<span class="task-card-date">
|
||||
<i data-lucide="calendar"></i>
|
||||
${createdAt}
|
||||
</span>
|
||||
<div class="task-card-actions">
|
||||
<button class="btn btn--ghost btn--sm" data-action="edit-task" data-id="${task.id}" title="Editar tarefa">
|
||||
<i data-lucide="pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn--ghost btn--sm btn--danger" data-action="delete-task" data-id="${task.id}" title="Excluir tarefa">
|
||||
<i data-lucide="trash-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
openCreateModal() {
|
||||
const container = document.getElementById('tasks-grid');
|
||||
if (!container) return;
|
||||
|
||||
const existing = document.getElementById('task-inline-form');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const formHtml = `
|
||||
<div class="task-card task-card--form" id="task-inline-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="task-inline-name">Nome da tarefa *</label>
|
||||
<input type="text" id="task-inline-name" class="input" placeholder="Ex: Code Review de PR" required autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="task-inline-category">Categoria</label>
|
||||
<select id="task-inline-category" class="select">
|
||||
<option value="">Selecionar...</option>
|
||||
<option value="code-review">Code Review</option>
|
||||
<option value="security">Segurança</option>
|
||||
<option value="refactor">Refatoração</option>
|
||||
<option value="tests">Testes</option>
|
||||
<option value="docs">Documentação</option>
|
||||
<option value="performance">Performance</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="task-inline-description">Descrição</label>
|
||||
<textarea id="task-inline-description" class="textarea" rows="2" placeholder="Descreva o objetivo desta tarefa..."></textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn btn--primary" id="btn-save-inline-task" type="button">Salvar</button>
|
||||
<button class="btn btn--ghost" id="btn-cancel-inline-task" type="button">Cancelar</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const empty = document.getElementById('tasks-empty-state');
|
||||
if (empty) empty.style.display = 'none';
|
||||
|
||||
container.insertAdjacentHTML('afterbegin', formHtml);
|
||||
|
||||
document.getElementById('btn-save-inline-task')?.addEventListener('click', () => {
|
||||
const name = document.getElementById('task-inline-name')?.value.trim();
|
||||
const category = document.getElementById('task-inline-category')?.value;
|
||||
const description = document.getElementById('task-inline-description')?.value.trim();
|
||||
|
||||
if (!name) {
|
||||
Toast.warning('Nome da tarefa é obrigatório');
|
||||
return;
|
||||
}
|
||||
|
||||
TasksUI.save({ name, category, description });
|
||||
});
|
||||
|
||||
document.getElementById('btn-cancel-inline-task')?.addEventListener('click', () => {
|
||||
document.getElementById('task-inline-form')?.remove();
|
||||
if (TasksUI.tasks.length === 0) {
|
||||
const emptyEl = document.getElementById('tasks-empty-state');
|
||||
if (emptyEl) emptyEl.style.display = 'flex';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('task-inline-name')?.focus();
|
||||
},
|
||||
|
||||
async save(data) {
|
||||
if (!data || !data.name) {
|
||||
Toast.warning('Nome da tarefa é obrigatório');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await API.tasks.create(data);
|
||||
Toast.success('Tarefa criada com sucesso');
|
||||
document.getElementById('task-inline-form')?.remove();
|
||||
await TasksUI.load();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao salvar tarefa: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async delete(taskId) {
|
||||
const confirmed = await Modal.confirm(
|
||||
'Excluir tarefa',
|
||||
'Tem certeza que deseja excluir esta tarefa?'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await API.tasks.delete(taskId);
|
||||
Toast.success('Tarefa excluída com sucesso');
|
||||
await TasksUI.load();
|
||||
} catch (err) {
|
||||
Toast.error(`Erro ao excluir tarefa: ${err.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
_categoryClass(category) {
|
||||
const map = {
|
||||
'code-review': 'badge--blue',
|
||||
security: 'badge--red',
|
||||
refactor: 'badge--purple',
|
||||
tests: 'badge--green',
|
||||
docs: 'badge--gray',
|
||||
performance: 'badge--orange',
|
||||
};
|
||||
return map[(category || '').toLowerCase()] || 'badge--gray';
|
||||
},
|
||||
|
||||
_formatDate(isoString) {
|
||||
if (!isoString) return '—';
|
||||
return new Date(isoString).toLocaleDateString('pt-BR');
|
||||
},
|
||||
};
|
||||
|
||||
window.TasksUI = TasksUI;
|
||||
108
public/js/components/terminal.js
Normal file
108
public/js/components/terminal.js
Normal file
@@ -0,0 +1,108 @@
|
||||
const Terminal = {
|
||||
lines: [],
|
||||
maxLines: 1000,
|
||||
autoScroll: true,
|
||||
executionFilter: null,
|
||||
_processingInterval: null,
|
||||
|
||||
addLine(content, type = 'default') {
|
||||
const time = new Date();
|
||||
const formatted = time.toTimeString().slice(0, 8);
|
||||
|
||||
Terminal.lines.push({ content, type, timestamp: formatted });
|
||||
|
||||
if (Terminal.lines.length > Terminal.maxLines) {
|
||||
Terminal.lines.shift();
|
||||
}
|
||||
|
||||
Terminal.render();
|
||||
},
|
||||
|
||||
startProcessing(agentName) {
|
||||
Terminal.stopProcessing();
|
||||
Terminal.addLine(`Agente "${agentName}" processando tarefa...`, 'system');
|
||||
|
||||
let dots = 0;
|
||||
Terminal._processingInterval = setInterval(() => {
|
||||
dots = (dots + 1) % 4;
|
||||
const indicator = document.getElementById('terminal-processing');
|
||||
if (indicator) {
|
||||
indicator.textContent = 'Processando' + '.'.repeat(dots + 1);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
Terminal.render();
|
||||
},
|
||||
|
||||
stopProcessing() {
|
||||
if (Terminal._processingInterval) {
|
||||
clearInterval(Terminal._processingInterval);
|
||||
Terminal._processingInterval = null;
|
||||
}
|
||||
},
|
||||
|
||||
clear() {
|
||||
Terminal.stopProcessing();
|
||||
Terminal.lines = [];
|
||||
Terminal.executionFilter = null;
|
||||
Terminal.render();
|
||||
},
|
||||
|
||||
setExecutionFilter(executionId) {
|
||||
Terminal.executionFilter = executionId;
|
||||
Terminal.render();
|
||||
},
|
||||
|
||||
scrollToBottom() {
|
||||
const output = document.getElementById('terminal-output');
|
||||
if (output) output.scrollTop = output.scrollHeight;
|
||||
},
|
||||
|
||||
render() {
|
||||
const output = document.getElementById('terminal-output');
|
||||
if (!output) return;
|
||||
|
||||
const lines = Terminal.executionFilter
|
||||
? Terminal.lines.filter((l) => l.executionId === Terminal.executionFilter)
|
||||
: Terminal.lines;
|
||||
|
||||
if (lines.length === 0 && !Terminal._processingInterval) {
|
||||
output.innerHTML = `
|
||||
<div class="terminal-welcome">
|
||||
<span class="terminal-prompt">$</span>
|
||||
<span class="terminal-text">Aguardando execução de agente...</span>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const html = lines.map((line) => {
|
||||
const typeClass = line.type && line.type !== 'default' ? ' ' + line.type : '';
|
||||
const escaped = Terminal._escapeHtml(line.content);
|
||||
const formatted = escaped.replace(/\n/g, '<br>');
|
||||
|
||||
return `<div class="terminal-line${typeClass}">
|
||||
<span class="timestamp">${line.timestamp}</span>
|
||||
<span class="content">${formatted}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
const processing = Terminal._processingInterval
|
||||
? '<div class="terminal-line system"><span class="terminal-processing-indicator"><span id="terminal-processing" class="processing-dots">Processando...</span><span class="terminal-spinner"></span></span></div>'
|
||||
: '';
|
||||
|
||||
output.innerHTML = html + processing + '<span class="terminal-cursor blink">_</span>';
|
||||
|
||||
if (Terminal.autoScroll) Terminal.scrollToBottom();
|
||||
},
|
||||
|
||||
_escapeHtml(text) {
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
};
|
||||
|
||||
window.Terminal = Terminal;
|
||||
66
public/js/components/toast.js
Normal file
66
public/js/components/toast.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const Toast = {
|
||||
iconMap: {
|
||||
success: 'check-circle',
|
||||
error: 'x-circle',
|
||||
info: 'info',
|
||||
warning: 'alert-triangle',
|
||||
},
|
||||
|
||||
colorMap: {
|
||||
success: 'toast-success',
|
||||
error: 'toast-error',
|
||||
info: 'toast-info',
|
||||
warning: 'toast-warning',
|
||||
},
|
||||
|
||||
show(message, type = 'info', duration = 4000) {
|
||||
const container = document.getElementById('toast-container');
|
||||
if (!container) return;
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
|
||||
const iconName = Toast.iconMap[type] || 'info';
|
||||
|
||||
toast.innerHTML = `
|
||||
<span class="toast-icon" data-lucide="${iconName}"></span>
|
||||
<span class="toast-message">${message}</span>
|
||||
<button class="toast-close" aria-label="Fechar notificação">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
const closeBtn = toast.querySelector('.toast-close');
|
||||
closeBtn.addEventListener('click', () => Toast.dismiss(toast));
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
if (window.lucide) {
|
||||
lucide.createIcons({ nodes: [toast] });
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.add('toast-show');
|
||||
});
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => Toast.dismiss(toast), duration);
|
||||
}
|
||||
|
||||
return toast;
|
||||
},
|
||||
|
||||
dismiss(toast) {
|
||||
toast.classList.remove('toast-show');
|
||||
toast.classList.add('removing');
|
||||
toast.addEventListener('animationend', () => toast.remove(), { once: true });
|
||||
setTimeout(() => toast.remove(), 400);
|
||||
},
|
||||
|
||||
success(message, duration) { return Toast.show(message, 'success', duration); },
|
||||
error(message, duration) { return Toast.show(message, 'error', duration); },
|
||||
info(message, duration) { return Toast.show(message, 'info', duration); },
|
||||
warning(message, duration) { return Toast.show(message, 'warning', duration); },
|
||||
};
|
||||
|
||||
window.Toast = Toast;
|
||||
Reference in New Issue
Block a user