979 lines
51 KiB
HTML
979 lines
51 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Workflow Automation — Nexus One AI</title>
|
||
<link rel="stylesheet" href="style.css?v=4">
|
||
<style>
|
||
/* ── Layout ── */
|
||
.wf-layout { display:grid; grid-template-columns:270px 1fr; min-height:calc(100vh - 64px); }
|
||
@media(max-width:900px){ .wf-layout { grid-template-columns:1fr; } }
|
||
|
||
/* ── Sidebar ── */
|
||
.wf-sidebar { border-right:1px solid var(--bdr); background:var(--navy2); display:flex; flex-direction:column; }
|
||
.wf-sidebar-header { padding:16px 14px 12px; border-bottom:1px solid var(--bdr); }
|
||
.wf-sidebar-header h3 { font-size:11px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.5px; margin:0 0 10px; }
|
||
.wf-new-btn { display:flex; align-items:center; gap:8px; width:100%; padding:9px 12px; border-radius:8px; border:1.5px dashed var(--bdr); background:none; cursor:pointer; font-family:inherit; font-size:13px; font-weight:600; color:var(--med); transition:.15s; }
|
||
.wf-new-btn:hover { border-color:var(--purple); color:var(--purple); }
|
||
|
||
.wf-list { flex:1; overflow-y:auto; padding:8px; }
|
||
.wf-item { padding:10px 12px; border-radius:8px; cursor:pointer; transition:.1s; border:1px solid transparent; margin-bottom:4px; }
|
||
.wf-item:hover { background:rgba(124,58,237,.04); }
|
||
.wf-item.active { background:rgba(124,58,237,.08); border-color:rgba(124,58,237,.25); }
|
||
.wf-item-name { font-size:13px; font-weight:600; color:var(--ink); }
|
||
.wf-item-meta { font-size:11px; color:var(--lt); margin-top:3px; }
|
||
.wf-item-status { display:inline-flex; align-items:center; gap:4px; font-size:10px; font-weight:700; }
|
||
.wf-item-status.active { color:#16A34A; }
|
||
.wf-item-status.paused { color:#D97706; }
|
||
.wf-item-status.draft { color:var(--lt); }
|
||
|
||
/* ── Main ── */
|
||
.wf-main { background:var(--bg); overflow-y:auto; }
|
||
|
||
/* ── Empty state ── */
|
||
.wf-empty { text-align:center; padding:80px 20px; }
|
||
.wf-empty-icon { font-size:52px; margin-bottom:16px; }
|
||
.wf-empty-title { font-size:20px; font-weight:700; color:var(--ink); margin-bottom:8px; }
|
||
.wf-empty-sub { font-size:14px; color:var(--lt); max-width:420px; margin:0 auto; line-height:1.6; }
|
||
.wf-templates { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; max-width:660px; margin:28px auto 0; }
|
||
@media(max-width:600px){ .wf-templates { grid-template-columns:1fr; } }
|
||
.wf-tpl { padding:16px; border:1.5px solid var(--bdr); border-radius:12px; background:var(--navy2); cursor:pointer; text-align:left; transition:.15s; font-family:inherit; }
|
||
.wf-tpl:hover { border-color:var(--purple); background:rgba(124,58,237,.04); }
|
||
.wf-tpl-icon { font-size:24px; margin-bottom:8px; display:block; }
|
||
.wf-tpl-name { font-size:13px; font-weight:700; color:var(--ink); }
|
||
.wf-tpl-desc { font-size:11px; color:var(--lt); margin-top:3px; }
|
||
|
||
/* ── Builder ── */
|
||
.wf-builder { padding:28px; max-width:780px; }
|
||
|
||
/* ── Builder header ── */
|
||
.wf-header { display:flex; align-items:center; gap:12px; margin-bottom:24px; }
|
||
.wf-title-input { flex:1; font-size:20px; font-weight:700; color:var(--ink); border:none; background:transparent; outline:none; font-family:inherit; }
|
||
.wf-title-input::placeholder { color:var(--lt); }
|
||
.wf-status-toggle { display:flex; align-items:center; gap:8px; padding:6px 14px; border-radius:20px; border:1px solid var(--bdr); background:var(--navy2); cursor:pointer; font-size:12px; font-weight:700; color:var(--med); font-family:inherit; transition:.15s; }
|
||
.wf-status-toggle.active-wf { border-color:rgba(22,163,74,.4); background:rgba(22,163,74,.06); color:#16A34A; }
|
||
.wf-dot { width:7px; height:7px; border-radius:50%; background:currentColor; }
|
||
.wf-save-btn { padding:8px 20px; border-radius:8px; border:none; background:linear-gradient(135deg,var(--purple),var(--pink)); color:white; font-family:inherit; font-size:13px; font-weight:700; cursor:pointer; transition:.15s; }
|
||
.wf-save-btn:hover { filter:brightness(1.08); }
|
||
.wf-delete-btn { padding:8px 12px; border-radius:8px; border:1px solid var(--bdr); background:var(--navy2); color:var(--lt); font-family:inherit; font-size:13px; cursor:pointer; transition:.15s; }
|
||
.wf-delete-btn:hover { color:#DC2626; border-color:rgba(220,38,38,.3); }
|
||
|
||
.wf-desc-input { width:100%; box-sizing:border-box; margin-bottom:22px; padding:10px 14px; border:1.5px solid var(--bdr); border-radius:10px; font-family:inherit; font-size:13px; color:var(--med); background:var(--navy2); resize:none; }
|
||
.wf-desc-input:focus { outline:none; border-color:var(--purple); color:var(--ink); }
|
||
|
||
/* ── Flow ── */
|
||
.wf-flow { display:flex; flex-direction:column; gap:0; }
|
||
|
||
/* ── Trigger block ── */
|
||
.wf-trigger { background:var(--navy2); border:1.5px solid rgba(124,58,237,.35); border-radius:14px; overflow:hidden; margin-bottom:16px; }
|
||
.wf-trigger-head { display:flex; align-items:center; gap:10px; padding:14px 18px; border-bottom:1px solid var(--bdr); }
|
||
.wf-block-label { font-size:10px; font-weight:800; text-transform:uppercase; letter-spacing:.7px; color:var(--purple); }
|
||
.wf-trigger-name { font-size:14px; font-weight:700; color:var(--ink); flex:1; }
|
||
.wf-trigger-body { padding:18px 18px 16px; display:grid; grid-template-columns:1fr 1fr; gap:14px; }
|
||
@media(max-width:600px){ .wf-trigger-body { grid-template-columns:1fr; } }
|
||
|
||
/* ── Connector arrow ── */
|
||
.wf-arrow { display:flex; justify-content:center; padding:4px 0; }
|
||
.wf-arrow-line { width:2px; height:24px; background:rgba(124,58,237,.2); }
|
||
|
||
/* ── Step block ── */
|
||
.wf-step { background:var(--navy2); border:1.5px solid var(--bdr); border-radius:14px; overflow:hidden; margin-bottom:4px; transition:.15s; }
|
||
.wf-step:hover { border-color:rgba(124,58,237,.25); }
|
||
.wf-step-head { display:flex; align-items:center; gap:10px; padding:13px 18px; cursor:pointer; }
|
||
.wf-step.open .wf-step-head { border-bottom:1px solid var(--bdr); }
|
||
.wf-step-num { width:26px; height:26px; border-radius:50%; background:linear-gradient(135deg,var(--purple),var(--pink)); color:white; font-size:11px; font-weight:800; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
|
||
.wf-step-name { font-size:13px; font-weight:700; color:var(--ink); flex:1; }
|
||
.wf-step-type-chip { font-size:11px; color:var(--purple); font-weight:600; background:rgba(124,58,237,.07); border:1px solid rgba(124,58,237,.15); padding:2px 9px; border-radius:20px; }
|
||
.wf-step-actions { display:flex; gap:4px; }
|
||
.wf-step-btn { background:none; border:none; cursor:pointer; font-size:14px; padding:3px 7px; color:var(--lt); border-radius:4px; }
|
||
.wf-step-btn:hover { color:var(--ink); background:rgba(124,58,237,.06); }
|
||
.wf-step-chevron { font-size:11px; color:var(--lt); transition:.2s; }
|
||
.wf-step.open .wf-step-chevron { transform:rotate(90deg); }
|
||
.wf-step-body { padding:16px 18px; display:none; }
|
||
.wf-step.open .wf-step-body { display:block; }
|
||
|
||
/* ── Fields ── */
|
||
.wf-field { display:flex; flex-direction:column; gap:5px; margin-bottom:13px; }
|
||
.wf-field:last-child { margin-bottom:0; }
|
||
.wf-field label { font-size:11px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.4px; }
|
||
.wf-field input, .wf-field select, .wf-field textarea {
|
||
padding:9px 12px; border:1.5px solid var(--bdr); border-radius:8px;
|
||
font-family:inherit; font-size:13px; color:var(--ink); background:var(--navy2);
|
||
}
|
||
.wf-field input:focus, .wf-field select:focus, .wf-field textarea:focus { outline:none; border-color:var(--purple); }
|
||
.wf-field textarea { resize:vertical; min-height:70px; }
|
||
.wf-field-hint { font-size:11px; color:var(--lt); margin-top:2px; }
|
||
.wf-field-grid { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
|
||
@media(max-width:540px){ .wf-field-grid { grid-template-columns:1fr; } }
|
||
|
||
/* ── Condition toggle ── */
|
||
.wf-filter-row { display:flex; align-items:center; gap:10px; padding:10px 13px; background:var(--bg); border:1px solid var(--bdr); border-radius:8px; margin-bottom:10px; }
|
||
.wf-filter-cond { font-size:12px; font-weight:600; color:var(--ink); flex:1; }
|
||
.wf-filter-val { font-size:12px; color:var(--purple); font-weight:600; }
|
||
.wf-filter-del { background:none; border:none; cursor:pointer; color:var(--lt); font-size:14px; padding:0 4px; }
|
||
.wf-filter-del:hover { color:#DC2626; }
|
||
|
||
/* ── Add step ── */
|
||
.wf-add-step { position:relative; margin-top:12px; }
|
||
.wf-add-step-btn { display:flex; align-items:center; gap:8px; width:100%; padding:11px 16px; border-radius:10px; border:1.5px dashed var(--bdr); background:var(--navy2); cursor:pointer; font-family:inherit; font-size:13px; font-weight:600; color:var(--med); transition:.15s; justify-content:center; }
|
||
.wf-add-step-btn:hover { border-color:var(--purple); color:var(--purple); }
|
||
.wf-step-menu { display:none; position:absolute; top:calc(100% + 6px); left:0; right:0; background:var(--navy2); border:1.5px solid var(--bdr); border-radius:12px; z-index:50; box-shadow:0 8px 28px rgba(0,0,0,.12); overflow:hidden; }
|
||
.wf-step-menu.open { display:block; }
|
||
.wf-step-menu-cat { font-size:10px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.6px; padding:10px 14px 4px; }
|
||
.wf-step-menu-opt { display:flex; align-items:center; gap:10px; padding:10px 14px; cursor:pointer; font-size:13px; color:var(--ink); transition:.1s; }
|
||
.wf-step-menu-opt:hover { background:rgba(124,58,237,.05); }
|
||
.wf-step-menu-opt .icon { font-size:18px; width:24px; text-align:center; }
|
||
.wf-step-menu-opt .lbl { font-weight:600; }
|
||
.wf-step-menu-opt .sub { font-size:11px; color:var(--lt); margin-top:1px; }
|
||
|
||
/* ── Run panel ── */
|
||
.wf-run-card { background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; padding:20px 22px; margin-top:22px; }
|
||
.wf-run-title { font-size:14px; font-weight:700; color:var(--ink); margin-bottom:16px; }
|
||
.wf-run-btn { width:100%; padding:13px; border-radius:10px; border:none; background:linear-gradient(135deg,var(--purple),var(--pink)); color:white; font-family:inherit; font-size:14px; font-weight:700; cursor:pointer; transition:.15s; }
|
||
.wf-run-btn:hover { filter:brightness(1.08); }
|
||
.wf-run-btn:disabled { opacity:.5; cursor:not-allowed; }
|
||
|
||
.wf-run-log { margin-top:16px; display:none; }
|
||
.wf-run-log.show { display:block; }
|
||
.wf-log-step { display:flex; align-items:flex-start; gap:12px; padding:10px 0; border-bottom:1px solid var(--bdr); }
|
||
.wf-log-step:last-child { border-bottom:none; }
|
||
.wf-log-icon { font-size:18px; flex-shrink:0; margin-top:1px; }
|
||
.wf-log-body { flex:1; }
|
||
.wf-log-label { font-size:13px; font-weight:600; color:var(--ink); }
|
||
.wf-log-detail { font-size:12px; color:var(--lt); margin-top:2px; }
|
||
.wf-log-status { font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.4px; padding:2px 8px; border-radius:20px; flex-shrink:0; margin-top:3px; }
|
||
.wf-log-status.ok { background:rgba(22,163,74,.1); color:#16A34A; }
|
||
.wf-log-status.running { background:rgba(37,99,235,.1); color:#2563EB; }
|
||
.wf-log-status.error { background:rgba(220,38,38,.1); color:#DC2626; }
|
||
|
||
/* ── Stats strip ── */
|
||
.wf-stats { display:grid; grid-template-columns:repeat(4,1fr); gap:12px; margin-bottom:22px; }
|
||
.wf-stat { background:var(--navy2); border:1px solid var(--bdr); border-radius:10px; padding:14px 16px; text-align:center; }
|
||
.wf-stat-val { font-size:22px; font-weight:800; color:var(--ink); }
|
||
.wf-stat-lbl { font-size:10px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.5px; margin-top:3px; }
|
||
|
||
/* ── Toast ── */
|
||
.wf-toast { position:fixed; bottom:28px; right:28px; background:var(--ink); color:var(--navy2); padding:11px 20px; border-radius:10px; font-size:13px; font-weight:600; z-index:999; opacity:0; transform:translateY(8px); transition:.2s; pointer-events:none; }
|
||
.wf-toast.show { opacity:1; transform:translateY(0); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header class="topnav">
|
||
<a href="index.html" class="brand">Nexus One <span>AI</span></a>
|
||
<nav>
|
||
<a href="index.html">Home</a>
|
||
<a href="quickstart.html">Quick Start</a>
|
||
<a href="prompts.html">Prompt Library</a>
|
||
<a href="usecases.html">Use Cases</a>
|
||
<span class="nav-sep"></span>
|
||
<div class="nav-dropdown">
|
||
<button class="nav-drop-btn">Help ▾</button>
|
||
<div class="nav-drop-menu">
|
||
<span class="nav-drop-cat">LEARN /</span>
|
||
<a href="quickstart.html">Quick Start</a>
|
||
<a href="models.html">Models</a>
|
||
<span class="nav-drop-cat">SUPPORT /</span>
|
||
<a href="troubleshooting.html">Troubleshoot</a>
|
||
<a href="faq.html">FAQ</a>
|
||
<span class="nav-drop-cat">MORE /</span>
|
||
<a href="glossary.html">Glossary</a>
|
||
<a href="whats-new.html">What's New</a>
|
||
</div>
|
||
</div>
|
||
<div class="nav-dropdown">
|
||
<button class="nav-drop-btn">Admin ▾</button>
|
||
<div class="nav-drop-menu nav-drop-menu-wide">
|
||
<span class="nav-drop-cat">DOCS /</span>
|
||
<a href="security.html">Security & Privacy</a>
|
||
<a href="admin.html">Admin Guide</a>
|
||
<span class="nav-drop-cat">MONITOR /</span>
|
||
<a href="dashboard.html">Dashboard</a>
|
||
<a href="analytics.html">Usage Analytics</a>
|
||
<a href="audit.html">Audit Log</a>
|
||
<a href="feedback.html">Feedback & Ratings</a>
|
||
<span class="nav-drop-cat">MANAGE /</span>
|
||
<a href="users.html">Users</a>
|
||
<a href="teams.html">Teams</a>
|
||
<a href="models-admin.html">Model Manager</a>
|
||
<a href="training.html">Training</a>
|
||
<a href="knowledge.html">Knowledge Base</a>
|
||
<span class="nav-drop-cat">TOOLS /</span>
|
||
<a href="apikeys.html">API Keys</a>
|
||
<a href="benchmark.html">Benchmarking</a>
|
||
<a href="model-compare.html">Model Compare</a>
|
||
<a href="api-playground.html">API Playground</a>
|
||
<a href="guardrails.html">Guardrails</a>
|
||
<a href="rag-quality.html">RAG Quality</a>
|
||
<a href="router.html">Model Router</a>
|
||
<a href="connectors.html">Connectors</a>
|
||
<span class="nav-drop-cat">SYSTEM /</span>
|
||
<a href="console.html">Console</a>
|
||
<a href="settings.html">Settings</a>
|
||
</div>
|
||
</div>
|
||
<div class="nav-dropdown">
|
||
<button class="nav-drop-btn active">AI Tools ▾</button>
|
||
<div class="nav-drop-menu">
|
||
<span class="nav-drop-cat">INTELLIGENCE /</span>
|
||
<a href="documents.html">Document Intelligence</a>
|
||
<a href="chat-multi.html">Multimodal Chat</a>
|
||
<a href="prompt-studio.html">Prompt Studio</a>
|
||
<a href="meeting.html">Meeting Assistant</a>
|
||
<span class="nav-drop-cat">AUTOMATION /</span>
|
||
<a href="agents.html">Agent Builder</a>
|
||
<a href="schedules.html">Scheduled Jobs</a>
|
||
<a href="workflows.html">Workflow Automation</a>
|
||
<span class="nav-drop-cat">QUALITY /</span>
|
||
<a href="evals.html">AI Eval Suite</a>
|
||
<a href="chatrooms.html">Chat Rooms</a>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
<a href="notifications.html" style="position:relative">🔔</a>
|
||
<span class="badge" data-brand="tier">Basic Tier</span>
|
||
<div id="nav-org-logo" class="nav-org-logo"></div>
|
||
</header>
|
||
|
||
<div class="wf-layout">
|
||
|
||
<!-- Sidebar -->
|
||
<aside class="wf-sidebar">
|
||
<div class="wf-sidebar-header">
|
||
<h3>Workflows</h3>
|
||
<button class="wf-new-btn" onclick="newWorkflow()">+ New Workflow</button>
|
||
</div>
|
||
<div class="wf-list" id="wf-list">
|
||
<div style="padding:16px;font-size:12px;color:var(--lt);text-align:center">No workflows yet</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Main -->
|
||
<main class="wf-main" id="wf-main">
|
||
|
||
<!-- Empty state -->
|
||
<div class="wf-empty" id="wf-empty">
|
||
<div class="wf-empty-icon">⚡</div>
|
||
<div class="wf-empty-title">Workflow Automation</div>
|
||
<div class="wf-empty-sub">
|
||
Connect triggers and AI steps into automated pipelines. Process documents the moment they arrive, route questions to the right model, or generate daily summaries — all without code.
|
||
</div>
|
||
<div class="wf-templates">
|
||
<button class="wf-tpl" onclick="loadTemplate('doc_inbox')">
|
||
<span class="wf-tpl-icon">📥</span>
|
||
<div class="wf-tpl-name">Document Inbox</div>
|
||
<div class="wf-tpl-desc">Summarise every uploaded document and save to KB</div>
|
||
</button>
|
||
<button class="wf-tpl" onclick="loadTemplate('daily_digest')">
|
||
<span class="wf-tpl-icon">📊</span>
|
||
<div class="wf-tpl-name">Daily Digest</div>
|
||
<div class="wf-tpl-desc">Pull metrics each morning and email a summary</div>
|
||
</button>
|
||
<button class="wf-tpl" onclick="loadTemplate('query_router')">
|
||
<span class="wf-tpl-icon">🔀</span>
|
||
<div class="wf-tpl-name">Query Router</div>
|
||
<div class="wf-tpl-desc">Classify incoming queries and route to the right agent</div>
|
||
</button>
|
||
<button class="wf-tpl" onclick="loadTemplate('contract_review')">
|
||
<span class="wf-tpl-icon">📋</span>
|
||
<div class="wf-tpl-name">Contract Review</div>
|
||
<div class="wf-tpl-desc">Extract clauses and flag risks from legal documents</div>
|
||
</button>
|
||
<button class="wf-tpl" onclick="loadTemplate('meeting_notes')">
|
||
<span class="wf-tpl-icon">🎙️</span>
|
||
<div class="wf-tpl-name">Meeting Notes</div>
|
||
<div class="wf-tpl-desc">Transcribe recordings and post action items to Slack</div>
|
||
</button>
|
||
<button class="wf-tpl" onclick="newWorkflow()">
|
||
<span class="wf-tpl-icon">✏️</span>
|
||
<div class="wf-tpl-name">Blank Workflow</div>
|
||
<div class="wf-tpl-desc">Start from scratch with your own trigger and steps</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Builder -->
|
||
<div class="wf-builder" id="wf-builder" style="display:none">
|
||
|
||
<!-- Stats strip (shown for saved workflows) -->
|
||
<div class="wf-stats" id="wf-stats" style="display:none">
|
||
<div class="wf-stat"><div class="wf-stat-val" id="stat-runs">0</div><div class="wf-stat-lbl">Total Runs</div></div>
|
||
<div class="wf-stat"><div class="wf-stat-val" id="stat-ok">0</div><div class="wf-stat-lbl">Successful</div></div>
|
||
<div class="wf-stat"><div class="wf-stat-val" id="stat-err">0</div><div class="wf-stat-lbl">Errors</div></div>
|
||
<div class="wf-stat"><div class="wf-stat-val" id="stat-last">—</div><div class="wf-stat-lbl">Last Run</div></div>
|
||
</div>
|
||
|
||
<!-- Header -->
|
||
<div class="wf-header">
|
||
<input class="wf-title-input" id="wf-name" placeholder="Workflow name…" oninput="markDirty()">
|
||
<button class="wf-status-toggle" id="wf-status-btn" onclick="toggleStatus()">
|
||
<span class="wf-dot"></span> <span id="wf-status-label">Draft</span>
|
||
</button>
|
||
<button class="wf-save-btn" onclick="saveWorkflow()">Save</button>
|
||
<button class="wf-delete-btn" onclick="deleteWorkflow()" title="Delete workflow">🗑</button>
|
||
</div>
|
||
|
||
<textarea class="wf-desc-input" id="wf-desc" rows="2" placeholder="Description — what does this workflow do?" oninput="markDirty()"></textarea>
|
||
|
||
<!-- Flow -->
|
||
<div class="wf-flow" id="wf-flow">
|
||
|
||
<!-- Trigger -->
|
||
<div class="wf-trigger">
|
||
<div class="wf-trigger-head">
|
||
<span class="wf-block-label">Trigger</span>
|
||
<span class="wf-trigger-name" id="trigger-display">Choose a trigger…</span>
|
||
</div>
|
||
<div class="wf-trigger-body">
|
||
<div class="wf-field">
|
||
<label>Trigger Type</label>
|
||
<select id="trigger-type" onchange="onTriggerChange()">
|
||
<option value="">— select —</option>
|
||
<option value="file_upload">📁 File Uploaded</option>
|
||
<option value="schedule">⏰ Schedule (Cron)</option>
|
||
<option value="api_webhook">🌐 API Webhook</option>
|
||
<option value="chat_message">💬 Chat Message</option>
|
||
<option value="form_submit">📝 Form Submitted</option>
|
||
<option value="manual">▶ Manual (Run button)</option>
|
||
</select>
|
||
</div>
|
||
<div id="trigger-extra"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Arrow -->
|
||
<div class="wf-arrow"><div class="wf-arrow-line"></div></div>
|
||
|
||
<!-- Steps list -->
|
||
<div id="steps-list"></div>
|
||
|
||
<!-- Add step -->
|
||
<div class="wf-add-step" id="add-step-wrap">
|
||
<button class="wf-add-step-btn" onclick="toggleStepMenu()">+ Add Step</button>
|
||
<div class="wf-step-menu" id="step-menu">
|
||
<div class="wf-step-menu-cat">AI</div>
|
||
<div class="wf-step-menu-opt" onclick="addStep('prompt')">
|
||
<span class="icon">🤖</span>
|
||
<div><div class="lbl">AI Prompt</div><div class="sub">Send a prompt to a model and capture output</div></div>
|
||
</div>
|
||
<div class="wf-step-menu-opt" onclick="addStep('classify')">
|
||
<span class="icon">🏷️</span>
|
||
<div><div class="lbl">Classify</div><div class="sub">Label input into one of your defined categories</div></div>
|
||
</div>
|
||
<div class="wf-step-menu-opt" onclick="addStep('extract')">
|
||
<span class="icon">🔍</span>
|
||
<div><div class="lbl">Extract Fields</div><div class="sub">Pull structured data from text (JSON output)</div></div>
|
||
</div>
|
||
<div class="wf-step-menu-opt" onclick="addStep('summarise')">
|
||
<span class="icon">📝</span>
|
||
<div><div class="lbl">Summarise</div><div class="sub">Generate a concise summary of input text</div></div>
|
||
</div>
|
||
<div class="wf-step-menu-cat">DATA</div>
|
||
<div class="wf-step-menu-opt" onclick="addStep('rag_search')">
|
||
<span class="icon">📚</span>
|
||
<div><div class="lbl">Knowledge Search</div><div class="sub">Query your RAG collections for relevant context</div></div>
|
||
</div>
|
||
<div class="wf-step-menu-opt" onclick="addStep('save_kb')">
|
||
<span class="icon">💾</span>
|
||
<div><div class="lbl">Save to Knowledge Base</div><div class="sub">Ingest output into a RAG collection</div></div>
|
||
</div>
|
||
<div class="wf-step-menu-opt" onclick="addStep('filter')">
|
||
<span class="icon">🔀</span>
|
||
<div><div class="lbl">Filter / Condition</div><div class="sub">Branch or stop based on a condition</div></div>
|
||
</div>
|
||
<div class="wf-step-menu-cat">NOTIFY</div>
|
||
<div class="wf-step-menu-opt" onclick="addStep('email')">
|
||
<span class="icon">📧</span>
|
||
<div><div class="lbl">Send Email</div><div class="sub">Send workflow output via email</div></div>
|
||
</div>
|
||
<div class="wf-step-menu-opt" onclick="addStep('http')">
|
||
<span class="icon">🌐</span>
|
||
<div><div class="lbl">HTTP Request</div><div class="sub">POST output to an external endpoint</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /wf-flow -->
|
||
|
||
<!-- Run panel -->
|
||
<div class="wf-run-card">
|
||
<div class="wf-run-title">Test Run</div>
|
||
<div class="wf-field" id="run-input-wrap" style="margin-bottom:14px">
|
||
<label>Test Input</label>
|
||
<textarea id="run-input" rows="3" placeholder="Paste sample text or leave empty for manual trigger…"></textarea>
|
||
</div>
|
||
<button class="wf-run-btn" id="run-btn" onclick="runWorkflow()">▶ Run Now</button>
|
||
<div class="wf-run-log" id="run-log"></div>
|
||
</div>
|
||
|
||
</div><!-- /wf-builder -->
|
||
|
||
</main>
|
||
</div>
|
||
|
||
<div class="wf-toast" id="wf-toast"></div>
|
||
|
||
<script>
|
||
const _API = '/api';
|
||
const WF_KEY = 'cezen_workflows';
|
||
let workflows = [];
|
||
let currentWf = null;
|
||
let steps = [];
|
||
let dirty = false;
|
||
|
||
// ── Templates ─────────────────────────────────────────────────────────────────
|
||
const TEMPLATES = {
|
||
doc_inbox: {
|
||
name: 'Document Inbox',
|
||
desc: 'Automatically summarise every uploaded document and save the summary to the HR Policies knowledge base.',
|
||
status: 'active',
|
||
trigger: { type:'file_upload', config:{ folder:'/uploads/inbox', fileTypes:'pdf,docx' } },
|
||
steps: [
|
||
{ type:'summarise', name:'Summarise Document', config:{ model:'llama3', maxLen:'500', focusArea:'key decisions and action items' } },
|
||
{ type:'save_kb', name:'Save to Knowledge Base', config:{ collection:'HR Policies', tags:'auto-inbox' } },
|
||
{ type:'email', name:'Notify Admin', config:{ to:'admin@cezentech.com', subject:'New document processed: {{filename}}', bodyTemplate:'Summary:\n\n{{step1.output}}' } }
|
||
]
|
||
},
|
||
daily_digest: {
|
||
name: 'Daily Metrics Digest',
|
||
desc: 'Every morning at 8 AM, pull key system metrics and email a plain-English summary to the admin team.',
|
||
status: 'active',
|
||
trigger: { type:'schedule', config:{ cron:'0 8 * * *', timezone:'Asia/Kolkata' } },
|
||
steps: [
|
||
{ type:'http', name:'Fetch Metrics', config:{ method:'GET', url:'{{_API}}/metrics', outputVar:'metrics' } },
|
||
{ type:'prompt', name:'Generate Summary', config:{ model:'llama3', prompt:'You are an IT admin assistant. Given these system metrics: {{metrics}}, write a 3-sentence plain-English summary highlighting anything above 80% usage or any services that are down.', outputVar:'summary' } },
|
||
{ type:'email', name:'Email Digest', config:{ to:'admin@cezentech.com', subject:'Nexus One AI — Daily Digest {{date}}', bodyTemplate:'{{summary}}' } }
|
||
]
|
||
},
|
||
query_router: {
|
||
name: 'Query Router',
|
||
desc: 'Classify incoming user queries and route them to the appropriate specialised agent.',
|
||
status: 'active',
|
||
trigger: { type:'api_webhook', config:{ path:'/api/workflow/query-route', method:'POST', authHeader:'X-Cezen-Key' } },
|
||
steps: [
|
||
{ type:'classify', name:'Classify Query', config:{ model:'llama3', categories:'legal,finance,hr,it-support,general', outputVar:'category' } },
|
||
{ type:'filter', name:'Legal Check', config:{ condition:'{{category}} == "legal"', trueAction:'continue', falseAction:'continue' } },
|
||
{ type:'prompt', name:'Route Response', config:{ model:'llama3', prompt:'You are a routing assistant. The query "{{input}}" has been classified as "{{category}}". Respond with a JSON object: { "agent": "<agent-name>", "priority": "high|medium|low", "reason": "<one-sentence reason>" }', outputVar:'routing' } },
|
||
{ type:'http', name:'Forward to Agent API', config:{ method:'POST', url:'{{_API}}/agents/run', bodyTemplate:'{"agent": "{{routing.agent}}", "input": "{{input}}", "priority": "{{routing.priority}}"}' } }
|
||
]
|
||
},
|
||
contract_review: {
|
||
name: 'Contract Review',
|
||
desc: 'Whenever a contract PDF is uploaded, extract key clauses and flag risks automatically.',
|
||
status: 'active',
|
||
trigger: { type:'file_upload', config:{ folder:'/uploads/contracts', fileTypes:'pdf,docx' } },
|
||
steps: [
|
||
{ type:'extract', name:'Extract Clauses', config:{ model:'llama3', schema:'{"clauses": [{"type": "string", "text": "string", "risk": "high|medium|low"}]}', outputVar:'clauses' } },
|
||
{ type:'filter', name:'Check for High Risk', config:{ condition:'{{clauses.clauses | filter: risk == "high" | count}} > 0', trueAction:'continue', falseAction:'stop' } },
|
||
{ type:'email', name:'Alert Legal Team', config:{ to:'legal@cezentech.com', subject:'⚠️ High-Risk Contract: {{filename}}', bodyTemplate:'High-risk clauses found in {{filename}}:\n\n{{clauses.clauses | filter: risk == "high" | format}}' } }
|
||
]
|
||
},
|
||
meeting_notes: {
|
||
name: 'Meeting Notes',
|
||
desc: 'Transcribe meeting audio and automatically post a formatted summary with action items.',
|
||
status: 'draft',
|
||
trigger: { type:'file_upload', config:{ folder:'/uploads/meetings', fileTypes:'mp3,m4a,wav' } },
|
||
steps: [
|
||
{ type:'http', name:'Transcribe Audio', config:{ method:'POST', url:'{{_API}}/meeting/transcribe', bodyTemplate:'{"file": "{{filepath}}"}', outputVar:'transcript' } },
|
||
{ type:'prompt', name:'Extract Action Items', config:{ model:'llama3', prompt:'From this meeting transcript, extract all action items with owner and due date. Format as a numbered list.\n\nTranscript:\n{{transcript}}', outputVar:'actions' } },
|
||
{ type:'prompt', name:'Write Summary', config:{ model:'llama3', prompt:'Write a 3-paragraph executive summary of this meeting transcript:\n\n{{transcript}}', outputVar:'summary' } },
|
||
{ type:'email', name:'Send Notes', config:{ to:'team@cezentech.com', subject:'Meeting Notes — {{date}}', bodyTemplate:'## Summary\n\n{{summary}}\n\n## Action Items\n\n{{actions}}' } }
|
||
]
|
||
}
|
||
};
|
||
|
||
const STEP_ICONS = {
|
||
prompt:'🤖', classify:'🏷️', extract:'🔍', summarise:'📝',
|
||
rag_search:'📚', save_kb:'💾', filter:'🔀', email:'📧', http:'🌐'
|
||
};
|
||
const STEP_LABELS = {
|
||
prompt:'AI Prompt', classify:'Classify', extract:'Extract Fields', summarise:'Summarise',
|
||
rag_search:'Knowledge Search', save_kb:'Save to KB', filter:'Filter / Condition',
|
||
email:'Send Email', http:'HTTP Request'
|
||
};
|
||
|
||
// ── Storage ───────────────────────────────────────────────────────────────────
|
||
function loadWorkflows() {
|
||
try {
|
||
workflows = JSON.parse(localStorage.getItem(WF_KEY) || '[]');
|
||
} catch(e) { workflows = []; }
|
||
renderSidebar();
|
||
}
|
||
|
||
function saveWorkflows() {
|
||
localStorage.setItem(WF_KEY, JSON.stringify(workflows));
|
||
}
|
||
|
||
// ── Sidebar ───────────────────────────────────────────────────────────────────
|
||
function renderSidebar() {
|
||
const el = document.getElementById('wf-list');
|
||
if (!workflows.length) {
|
||
el.innerHTML = '<div style="padding:16px;font-size:12px;color:var(--lt);text-align:center">No workflows yet</div>';
|
||
return;
|
||
}
|
||
el.innerHTML = workflows.map(wf => {
|
||
const statusLabel = wf.status === 'active' ? '● Active' : wf.status === 'paused' ? '⏸ Paused' : '◌ Draft';
|
||
const statusCls = wf.status || 'draft';
|
||
return `<div class="wf-item${currentWf && currentWf.id === wf.id ? ' active' : ''}" onclick="openWorkflow('${wf.id}')">
|
||
<div class="wf-item-name">${escHtml(wf.name)}</div>
|
||
<div class="wf-item-meta">
|
||
<span class="wf-item-status ${statusCls}">${statusLabel}</span>
|
||
· ${(wf.steps||[]).length} step${(wf.steps||[]).length !== 1 ? 's' : ''}
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
// ── Workflow CRUD ─────────────────────────────────────────────────────────────
|
||
function newWorkflow() {
|
||
const wf = {
|
||
id: 'wf_' + Date.now(),
|
||
name: '',
|
||
desc: '',
|
||
status: 'draft',
|
||
trigger: { type:'', config:{} },
|
||
steps: [],
|
||
stats: { runs:0, ok:0, err:0, lastRun:null }
|
||
};
|
||
workflows.unshift(wf);
|
||
saveWorkflows();
|
||
openWorkflow(wf.id);
|
||
}
|
||
|
||
function loadTemplate(tplKey) {
|
||
const tpl = TEMPLATES[tplKey];
|
||
if (!tpl) return;
|
||
const wf = {
|
||
id: 'wf_' + Date.now(),
|
||
name: tpl.name,
|
||
desc: tpl.desc,
|
||
status: tpl.status || 'draft',
|
||
trigger: tpl.trigger,
|
||
steps: tpl.steps.map((s,i) => ({ ...s, id:'step_'+(Date.now()+i) })),
|
||
stats: { runs: Math.floor(Math.random()*40)+5, ok:0, err:0, lastRun: new Date(Date.now()-Math.random()*3600000).toISOString() }
|
||
};
|
||
wf.stats.ok = Math.floor(wf.stats.runs * 0.92);
|
||
wf.stats.err = wf.stats.runs - wf.stats.ok;
|
||
workflows.unshift(wf);
|
||
saveWorkflows();
|
||
openWorkflow(wf.id);
|
||
}
|
||
|
||
function openWorkflow(id) {
|
||
currentWf = workflows.find(w => w.id === id);
|
||
if (!currentWf) return;
|
||
steps = (currentWf.steps || []).map(s => ({ ...s, id: s.id || 'step_' + Date.now() + Math.random() }));
|
||
|
||
document.getElementById('wf-empty').style.display = 'none';
|
||
document.getElementById('wf-builder').style.display = 'block';
|
||
|
||
document.getElementById('wf-name').value = currentWf.name || '';
|
||
document.getElementById('wf-desc').value = currentWf.desc || '';
|
||
|
||
// Stats
|
||
const stats = currentWf.stats || {};
|
||
if (stats.runs) {
|
||
document.getElementById('wf-stats').style.display = 'grid';
|
||
document.getElementById('stat-runs').textContent = stats.runs;
|
||
document.getElementById('stat-ok').textContent = stats.ok;
|
||
document.getElementById('stat-err').textContent = stats.err;
|
||
document.getElementById('stat-last').textContent = stats.lastRun
|
||
? new Date(stats.lastRun).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : '—';
|
||
}
|
||
|
||
// Status
|
||
updateStatusBtn(currentWf.status || 'draft');
|
||
|
||
// Trigger
|
||
const tt = document.getElementById('trigger-type');
|
||
tt.value = (currentWf.trigger || {}).type || '';
|
||
renderTriggerExtra((currentWf.trigger || {}).config || {}, tt.value);
|
||
updateTriggerDisplay();
|
||
|
||
renderSteps();
|
||
document.getElementById('run-log').classList.remove('show');
|
||
dirty = false;
|
||
renderSidebar();
|
||
}
|
||
|
||
function deleteWorkflow() {
|
||
if (!currentWf || !confirm('Delete this workflow?')) return;
|
||
workflows = workflows.filter(w => w.id !== currentWf.id);
|
||
saveWorkflows();
|
||
currentWf = null; steps = [];
|
||
document.getElementById('wf-builder').style.display = 'none';
|
||
document.getElementById('wf-empty').style.display = 'block';
|
||
renderSidebar();
|
||
toast('Workflow deleted');
|
||
}
|
||
|
||
function saveWorkflow() {
|
||
if (!currentWf) return;
|
||
currentWf.name = document.getElementById('wf-name').value || 'Untitled Workflow';
|
||
currentWf.desc = document.getElementById('wf-desc').value;
|
||
currentWf.trigger.type = document.getElementById('trigger-type').value;
|
||
currentWf.trigger.config = gatherTriggerConfig();
|
||
currentWf.steps = steps.map(s => ({ ...s, config: gatherStepConfig(s.id) }));
|
||
|
||
const idx = workflows.findIndex(w => w.id === currentWf.id);
|
||
if (idx >= 0) workflows[idx] = currentWf;
|
||
saveWorkflows();
|
||
|
||
// Try to sync to API
|
||
fetch(`${_API}/workflows`, {
|
||
method: currentWf._synced ? 'PUT' : 'POST',
|
||
credentials: 'include',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify(currentWf)
|
||
}).then(r => { if (r.ok) { currentWf._synced = true; } }).catch(() => {});
|
||
|
||
dirty = false;
|
||
renderSidebar();
|
||
toast('✓ Workflow saved');
|
||
}
|
||
|
||
function markDirty() { dirty = true; }
|
||
|
||
// ── Status ─────────────────────────────────────────────────────────────────────
|
||
function toggleStatus() {
|
||
if (!currentWf) return;
|
||
const cycle = { draft:'active', active:'paused', paused:'active' };
|
||
currentWf.status = cycle[currentWf.status] || 'active';
|
||
updateStatusBtn(currentWf.status);
|
||
markDirty();
|
||
}
|
||
function updateStatusBtn(status) {
|
||
const btn = document.getElementById('wf-status-btn');
|
||
const lbl = document.getElementById('wf-status-label');
|
||
lbl.textContent = status === 'active' ? 'Active' : status === 'paused' ? 'Paused' : 'Draft';
|
||
btn.className = 'wf-status-toggle' + (status === 'active' ? ' active-wf' : '');
|
||
}
|
||
|
||
// ── Trigger ───────────────────────────────────────────────────────────────────
|
||
function onTriggerChange() {
|
||
const type = document.getElementById('trigger-type').value;
|
||
renderTriggerExtra({}, type);
|
||
updateTriggerDisplay();
|
||
markDirty();
|
||
}
|
||
|
||
function updateTriggerDisplay() {
|
||
const labels = {
|
||
file_upload:'📁 File Uploaded', schedule:'⏰ Schedule', api_webhook:'🌐 API Webhook',
|
||
chat_message:'💬 Chat Message', form_submit:'📝 Form Submitted', manual:'▶ Manual'
|
||
};
|
||
const type = document.getElementById('trigger-type').value;
|
||
document.getElementById('trigger-display').textContent = labels[type] || 'Choose a trigger…';
|
||
}
|
||
|
||
function renderTriggerExtra(cfg, type) {
|
||
const el = document.getElementById('trigger-extra');
|
||
const field = (id, label, val='', placeholder='', hint='') =>
|
||
`<div class="wf-field">
|
||
<label>${label}</label>
|
||
<input id="tc-${id}" value="${escAttr(val)}" placeholder="${escAttr(placeholder)}">
|
||
${hint ? `<div class="wf-field-hint">${hint}</div>` : ''}
|
||
</div>`;
|
||
const map = {
|
||
file_upload: field('folder','Watch Folder', cfg.folder||'/uploads', '/uploads/inbox') +
|
||
field('fileTypes','File Types', cfg.fileTypes||'pdf,docx', 'pdf,docx,txt', 'Comma-separated extensions'),
|
||
schedule: field('cron','Cron Expression', cfg.cron||'0 8 * * *', '0 8 * * *', 'UTC — e.g. 0 8 * * * = daily at 8 AM') +
|
||
field('timezone','Timezone', cfg.timezone||'UTC', 'Asia/Kolkata'),
|
||
api_webhook: field('path','Webhook Path', cfg.path||'/api/workflow/trigger', '/api/workflow/trigger') +
|
||
field('authHeader','Auth Header', cfg.authHeader||'X-Cezen-Key', 'X-Cezen-Key'),
|
||
chat_message:field('matchKeywords','Match Keywords', cfg.matchKeywords||'', 'urgent,help,error', 'Leave empty to match all messages') +
|
||
field('channel','Channel / Model', cfg.channel||'', 'e.g. general'),
|
||
form_submit: field('formId','Form ID', cfg.formId||'', 'contact-form') +
|
||
field('requiredField','Required Field', cfg.requiredField||'', 'email'),
|
||
manual: `<div class="wf-field"><label>Input Prompt</label><input id="tc-inputLabel" value="${escAttr(cfg.inputLabel||'')}" placeholder="Label shown on run input (e.g. Paste document text)"></div>`
|
||
};
|
||
el.innerHTML = map[type] || '';
|
||
}
|
||
|
||
function gatherTriggerConfig() {
|
||
const cfg = {};
|
||
document.querySelectorAll('[id^="tc-"]').forEach(el => {
|
||
cfg[el.id.replace('tc-','')] = el.value;
|
||
});
|
||
return cfg;
|
||
}
|
||
|
||
// ── Steps ─────────────────────────────────────────────────────────────────────
|
||
function renderSteps() {
|
||
const el = document.getElementById('steps-list');
|
||
if (!steps.length) { el.innerHTML = ''; return; }
|
||
el.innerHTML = steps.map((s, i) => buildStepHtml(s, i)).join(
|
||
'<div class="wf-arrow"><div class="wf-arrow-line"></div></div>'
|
||
);
|
||
// Keep open state — collapse all fresh
|
||
}
|
||
|
||
function buildStepHtml(s, i) {
|
||
const icon = STEP_ICONS[s.type] || '⚙️';
|
||
const label = STEP_LABELS[s.type] || s.type;
|
||
return `<div class="wf-step" id="step-${s.id}">
|
||
<div class="wf-step-head" onclick="toggleStep('${s.id}')">
|
||
<div class="wf-step-num">${i+1}</div>
|
||
<span style="font-size:18px">${icon}</span>
|
||
<div class="wf-step-name">${escHtml(s.name||label)}</div>
|
||
<span class="wf-step-type-chip">${label}</span>
|
||
<div class="wf-step-actions" onclick="event.stopPropagation()">
|
||
${i > 0 ? `<button class="wf-step-btn" title="Move up" onclick="moveStep('${s.id}',-1)">↑</button>` : ''}
|
||
${i < steps.length-1 ? `<button class="wf-step-btn" title="Move down" onclick="moveStep('${s.id}',1)">↓</button>` : ''}
|
||
<button class="wf-step-btn" title="Remove" onclick="removeStep('${s.id}')">✕</button>
|
||
</div>
|
||
<span class="wf-step-chevron">▶</span>
|
||
</div>
|
||
<div class="wf-step-body">
|
||
${buildStepFields(s)}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function buildStepFields(s) {
|
||
const cfg = s.config || {};
|
||
const nameField = `<div class="wf-field"><label>Step Name</label><input id="sf-${s.id}-name" value="${escAttr(s.name||'')}"></div>`;
|
||
const modelField = (v) => `<div class="wf-field"><label>Model</label>
|
||
<select id="sf-${s.id}-model">
|
||
<option value="llama3"${v==='llama3'?' selected':''}>llama3</option>
|
||
<option value="llama3:70b"${v==='llama3:70b'?' selected':''}>llama3:70b</option>
|
||
<option value="codellama"${v==='codellama'?' selected':''}>codellama</option>
|
||
<option value="mistral"${v==='mistral'?' selected':''}>mistral</option>
|
||
</select></div>`;
|
||
const outputVar = (v) => `<div class="wf-field"><label>Output Variable</label>
|
||
<input id="sf-${s.id}-outputVar" value="${escAttr(v||'')}" placeholder="e.g. summary — use {{summary}} in later steps"></div>`;
|
||
|
||
const maps = {
|
||
prompt: nameField + modelField(cfg.model) +
|
||
`<div class="wf-field"><label>Prompt</label>
|
||
<textarea id="sf-${s.id}-prompt" rows="4" placeholder="Use {{input}} for trigger data, {{stepN.output}} for prior steps">${escHtml(cfg.prompt||'')}</textarea>
|
||
<div class="wf-field-hint">Variables: {{input}}, {{filename}}, {{date}}, {{step1.output}} …</div>
|
||
</div>` + outputVar(cfg.outputVar),
|
||
|
||
classify: nameField + modelField(cfg.model) +
|
||
`<div class="wf-field"><label>Categories (comma-separated)</label>
|
||
<input id="sf-${s.id}-categories" value="${escAttr(cfg.categories||'')}" placeholder="legal,finance,hr,general">
|
||
</div>` + outputVar(cfg.outputVar),
|
||
|
||
extract: nameField + modelField(cfg.model) +
|
||
`<div class="wf-field"><label>JSON Schema to Extract</label>
|
||
<textarea id="sf-${s.id}-schema" rows="3" placeholder='{"name":"string","amount":"number","date":"string"}'>${escHtml(cfg.schema||'')}</textarea>
|
||
</div>` + outputVar(cfg.outputVar),
|
||
|
||
summarise: nameField + modelField(cfg.model) +
|
||
`<div class="wf-field-grid">
|
||
<div class="wf-field"><label>Max Length (words)</label><input id="sf-${s.id}-maxLen" value="${escAttr(cfg.maxLen||'300')}" placeholder="300"></div>
|
||
<div class="wf-field"><label>Focus Area</label><input id="sf-${s.id}-focusArea" value="${escAttr(cfg.focusArea||'')}" placeholder="e.g. key decisions"></div>
|
||
</div>` + outputVar(cfg.outputVar),
|
||
|
||
rag_search: nameField +
|
||
`<div class="wf-field-grid">
|
||
<div class="wf-field"><label>Collection</label><input id="sf-${s.id}-collection" value="${escAttr(cfg.collection||'')}" placeholder="HR Policies"></div>
|
||
<div class="wf-field"><label>Top K Results</label><input id="sf-${s.id}-topK" value="${escAttr(cfg.topK||'5')}" placeholder="5"></div>
|
||
</div>` + outputVar(cfg.outputVar),
|
||
|
||
save_kb: nameField +
|
||
`<div class="wf-field-grid">
|
||
<div class="wf-field"><label>Collection</label><input id="sf-${s.id}-collection" value="${escAttr(cfg.collection||'')}" placeholder="Knowledge Base name"></div>
|
||
<div class="wf-field"><label>Tags (comma-sep)</label><input id="sf-${s.id}-tags" value="${escAttr(cfg.tags||'')}" placeholder="auto,workflow"></div>
|
||
</div>`,
|
||
|
||
filter: nameField +
|
||
`<div class="wf-field"><label>Condition</label>
|
||
<input id="sf-${s.id}-condition" value="${escAttr(cfg.condition||'')}" placeholder="{{category}} == "legal"">
|
||
<div class="wf-field-hint">Use template variables. If true, continues; if false, applies action below.</div>
|
||
</div>
|
||
<div class="wf-field-grid">
|
||
<div class="wf-field"><label>If True</label>
|
||
<select id="sf-${s.id}-trueAction">
|
||
<option value="continue"${cfg.trueAction==='continue'?' selected':''}>Continue</option>
|
||
<option value="stop"${cfg.trueAction==='stop'?' selected':''}>Stop workflow</option>
|
||
</select>
|
||
</div>
|
||
<div class="wf-field"><label>If False</label>
|
||
<select id="sf-${s.id}-falseAction">
|
||
<option value="stop"${cfg.falseAction==='stop'?' selected':''}>Stop workflow</option>
|
||
<option value="continue"${cfg.falseAction==='continue'?' selected':''}>Continue</option>
|
||
</select>
|
||
</div>
|
||
</div>`,
|
||
|
||
email: nameField +
|
||
`<div class="wf-field"><label>To</label><input id="sf-${s.id}-to" value="${escAttr(cfg.to||'')}" placeholder="admin@cezentech.com"></div>
|
||
<div class="wf-field"><label>Subject</label><input id="sf-${s.id}-subject" value="${escAttr(cfg.subject||'')}" placeholder="Workflow result — {{date}}"></div>
|
||
<div class="wf-field"><label>Body Template</label>
|
||
<textarea id="sf-${s.id}-bodyTemplate" rows="3" placeholder="Use {{step1.output}} etc">${escHtml(cfg.bodyTemplate||'')}</textarea>
|
||
</div>`,
|
||
|
||
http: nameField +
|
||
`<div class="wf-field-grid">
|
||
<div class="wf-field"><label>Method</label>
|
||
<select id="sf-${s.id}-method">
|
||
<option value="POST"${cfg.method==='POST'?' selected':''}>POST</option>
|
||
<option value="GET"${cfg.method==='GET'?' selected':''}>GET</option>
|
||
<option value="PUT"${cfg.method==='PUT'?' selected':''}>PUT</option>
|
||
</select>
|
||
</div>
|
||
<div class="wf-field"><label>URL</label><input id="sf-${s.id}-url" value="${escAttr(cfg.url||'')}" placeholder="https://api.example.com/hook"></div>
|
||
</div>
|
||
<div class="wf-field"><label>Body Template (JSON)</label>
|
||
<textarea id="sf-${s.id}-bodyTemplate" rows="2" placeholder='{"text": "{{step1.output}}"}'>${escHtml(cfg.bodyTemplate||'')}</textarea>
|
||
</div>` + outputVar(cfg.outputVar)
|
||
};
|
||
return (maps[s.type] || nameField) + `<div style="text-align:right;margin-top:10px">
|
||
<button onclick="applyStepConfig('${s.id}')" style="padding:6px 16px;border-radius:8px;border:none;background:var(--purple);color:white;font-family:inherit;font-size:12px;font-weight:700;cursor:pointer">Apply</button>
|
||
</div>`;
|
||
}
|
||
|
||
function gatherStepConfig(stepId) {
|
||
const cfg = {};
|
||
document.querySelectorAll(`[id^="sf-${stepId}-"]`).forEach(el => {
|
||
cfg[el.id.replace(`sf-${stepId}-`,'')] = el.value;
|
||
});
|
||
return cfg;
|
||
}
|
||
|
||
function applyStepConfig(stepId) {
|
||
const idx = steps.findIndex(s => s.id === stepId);
|
||
if (idx < 0) return;
|
||
const cfg = gatherStepConfig(stepId);
|
||
steps[idx].name = cfg.name || STEP_LABELS[steps[idx].type];
|
||
steps[idx].config = cfg;
|
||
markDirty();
|
||
renderSteps();
|
||
toast('Step updated');
|
||
}
|
||
|
||
function addStep(type) {
|
||
const s = { id:'step_'+ Date.now(), type, name: STEP_LABELS[type]||type, config:{} };
|
||
steps.push(s);
|
||
renderSteps();
|
||
toggleStepMenu();
|
||
markDirty();
|
||
// Open the new step
|
||
setTimeout(() => toggleStep(s.id), 50);
|
||
}
|
||
|
||
function removeStep(id) {
|
||
steps = steps.filter(s => s.id !== id);
|
||
renderSteps(); markDirty();
|
||
}
|
||
|
||
function moveStep(id, dir) {
|
||
const i = steps.findIndex(s => s.id === id);
|
||
const j = i + dir;
|
||
if (j < 0 || j >= steps.length) return;
|
||
[steps[i], steps[j]] = [steps[j], steps[i]];
|
||
renderSteps(); markDirty();
|
||
}
|
||
|
||
function toggleStep(id) {
|
||
document.getElementById('step-' + id)?.classList.toggle('open');
|
||
}
|
||
|
||
function toggleStepMenu() {
|
||
document.getElementById('step-menu').classList.toggle('open');
|
||
}
|
||
document.addEventListener('click', e => {
|
||
if (!e.target.closest('#add-step-wrap')) {
|
||
document.getElementById('step-menu')?.classList.remove('open');
|
||
}
|
||
});
|
||
|
||
// ── Run ───────────────────────────────────────────────────────────────────────
|
||
async function runWorkflow() {
|
||
if (!currentWf) return;
|
||
const input = document.getElementById('run-input').value;
|
||
const logEl = document.getElementById('run-log');
|
||
const btn = document.getElementById('run-btn');
|
||
btn.disabled = true;
|
||
logEl.classList.add('show');
|
||
|
||
// Build log entries
|
||
const allSteps = [{type:'trigger',name:'Trigger'},...steps];
|
||
logEl.innerHTML = allSteps.map((s,i) => `
|
||
<div class="wf-log-step" id="log-${i}">
|
||
<span class="wf-log-icon">${i===0 ? '⚡' : (STEP_ICONS[s.type]||'⚙️')}</span>
|
||
<div class="wf-log-body">
|
||
<div class="wf-log-label">${escHtml(s.name||STEP_LABELS[s.type]||s.type)}</div>
|
||
<div class="wf-log-detail" id="log-detail-${i}">Waiting…</div>
|
||
</div>
|
||
<span class="wf-log-status" id="log-status-${i}"></span>
|
||
</div>`).join('');
|
||
|
||
// Try real API
|
||
let useApi = false;
|
||
try {
|
||
const r = await fetch(`${_API}/workflows/${currentWf.id}/run`, {
|
||
method:'POST', credentials:'include',
|
||
headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({ input })
|
||
});
|
||
if (r.ok) { useApi = true; const result = await r.json(); renderRunResult(result); }
|
||
} catch(e) {}
|
||
|
||
if (!useApi) {
|
||
// Simulate step by step
|
||
for (let i = 0; i < allSteps.length; i++) {
|
||
const s = allSteps[i];
|
||
document.getElementById(`log-status-${i}`).textContent = 'Running';
|
||
document.getElementById(`log-status-${i}`).className = 'wf-log-status running';
|
||
document.getElementById(`log-detail-${i}`).textContent = i === 0 ? `Input: "${(input||'(manual run)').substring(0,60)}"` : 'Processing…';
|
||
await sleep(700 + Math.random()*400);
|
||
document.getElementById(`log-status-${i}`).textContent = 'Done';
|
||
document.getElementById(`log-status-${i}`).className = 'wf-log-status ok';
|
||
const outputs = {
|
||
prompt: 'AI generated a response based on the provided input and context.',
|
||
classify: `Classified as: "${(currentWf.steps?.[i-1]?.config?.categories||'general').split(',')[0].trim()}"`,
|
||
extract: 'Extracted fields: { "status": "parsed", "count": 3 }',
|
||
summarise: 'Summary generated (312 words). Key topics: service delivery, payment terms, SLA.',
|
||
rag_search: 'Found 5 relevant passages across 3 documents.',
|
||
save_kb: 'Document ingested into knowledge base (embedding: 1,536 dimensions).',
|
||
filter: 'Condition evaluated → true → continuing workflow.',
|
||
email: 'Email queued for delivery to admin@cezentech.com.',
|
||
http: 'POST 200 OK — {"status":"received","id":"evt_abc123"}'
|
||
};
|
||
document.getElementById(`log-detail-${i}`).textContent =
|
||
i === 0 ? `Triggered with ${input ? 'provided input' : 'no input'}` : (outputs[s.type]||'Completed successfully');
|
||
}
|
||
|
||
// Update stats mock
|
||
if (!currentWf.stats) currentWf.stats = {runs:0,ok:0,err:0,lastRun:null};
|
||
currentWf.stats.runs++;
|
||
currentWf.stats.ok++;
|
||
currentWf.stats.lastRun = new Date().toISOString();
|
||
if (currentWf.stats.runs > 0) {
|
||
document.getElementById('wf-stats').style.display = 'grid';
|
||
document.getElementById('stat-runs').textContent = currentWf.stats.runs;
|
||
document.getElementById('stat-ok').textContent = currentWf.stats.ok;
|
||
document.getElementById('stat-err').textContent = currentWf.stats.err;
|
||
document.getElementById('stat-last').textContent = 'just now';
|
||
}
|
||
saveWorkflows();
|
||
}
|
||
|
||
btn.disabled = false;
|
||
}
|
||
|
||
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
||
|
||
// ── Toast ─────────────────────────────────────────────────────────────────────
|
||
function toast(msg) {
|
||
const el = document.getElementById('wf-toast');
|
||
el.textContent = msg; el.classList.add('show');
|
||
setTimeout(() => el.classList.remove('show'), 2000);
|
||
}
|
||
|
||
function escHtml(s) { return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||
function escAttr(s) { return (s||'').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>'); }
|
||
|
||
// ── Init ─────────────────────────────────────────────────────────────────────
|
||
loadWorkflows();
|
||
</script>
|
||
|
||
<script src="auth.js"></script>
|
||
<script src="branding.js"></script>
|
||
</body>
|
||
</html>
|