Brings the ISO-packaged portal copy current with the live cezen-portal/ working directory, which had drifted across the entire V1.0 GA polish initiative (Phases 1-10) plus this session's topnav icon-order bugfix. - 52 existing files updated, 2 new pages added (about.html, health.html) - cgit/cezen-portal/ is now byte-identical to cezen-portal/ - Backend files (rag_ingest.py, cezen_license.py) already in sync; main.py intentionally left as-is (cgit is ahead there with workstation-category commercial-fit logic not yet backported to the live working copy) Next: rebuild the ISO (bash autoinstall/build-iso.sh) to bake this in.
1015 lines
58 KiB
HTML
1015 lines
58 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 Designer — Nexus One AI</title>
|
|
<link rel="stylesheet" href="style.css?v=12">
|
|
<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 { width:52px; height:52px; margin:0 auto 16px; color:var(--purple); }
|
|
.wf-empty-icon svg { width:100%; height:100%; }
|
|
.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 { width:24px; height:24px; margin-bottom:8px; display:block; color:var(--text-secondary); }
|
|
.wf-tpl-icon svg { width:100%; height:100%; }
|
|
.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 { width:14px; height:14px; color:var(--lt); transition:.2s; }
|
|
.wf-step-chevron svg { width:100%; height:100%; }
|
|
.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 { width:24px; height:24px; color:var(--text-secondary); flex-shrink:0; }
|
|
.wf-step-menu-opt .icon svg { width:100%; height:100%; }
|
|
.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 { width:18px; height:18px; flex-shrink:0; margin-top:1px; color:var(--text-secondary); }
|
|
.wf-log-icon svg { width:100%; height:100%; }
|
|
.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">Governance & Audit</a>
|
|
<a href="health.html">Health Center</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 Catalog</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">MCP & Enterprise 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">AI Workspace</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 Studio</a>
|
|
<a href="schedules.html">Scheduled Jobs</a>
|
|
<a href="workflows.html">Workflow Designer</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" class="nav-icon-btn" style="position:relative" aria-label="Notifications">
|
|
<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg></span>
|
|
</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()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></span><span>New Workflow</span></span></button>
|
|
</div>
|
|
<input type="text" id="wf-filter" placeholder="Filter workflows…" oninput="renderSidebar()" style="width:100%;box-sizing:border-box;margin:0 0 10px;padding:8px 10px;border:1px solid var(--bdr);border-radius:7px;font-size:12.5px;font-family:inherit">
|
|
<div class="wf-list" id="wf-list">
|
|
<div class="cezen-list-empty">No workflows yet — <a onclick="newWorkflow()">create your first one</a>.</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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M13 2 3 14h7l-1 8 10-12h-7z"/></svg></div>
|
|
<div class="wf-empty-title">Workflow Designer</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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 3v12"/><path d="m7 10 5 5 5-5"/><path d="M5 21h14"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 19h16"/><path d="M7 16V8"/><path d="M12 16V5"/><path d="M17 16v-3"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M16 3h5v5"/><path d="M4 20 21 3"/><path d="M21 16v5h-5"/><path d="M15 15 21 21"/><path d="M4 4h5l6 6"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M9 3h6"/><path d="M10 7h4"/><rect x="5" y="5" width="14" height="16" rx="2"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 10a7 7 0 0 0 14 0"/><path d="M12 17v4"/><path d="M8 21h8"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg></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"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/></svg></span></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 <span class="cezen-help" tabindex="0" data-tip="What starts this workflow: a new file landing in a watched folder, a cron schedule, an incoming webhook call, a chat message, a form submission, or a manual click of Run. Pick one, then fill in its settings below.">?</span></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()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></span><span>Add Step</span></span></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="7" y="8" width="10" height="8" rx="2"/><path d="M12 4v4M9 16v2M15 16v2M6 10H4M20 10h-2"/><circle cx="10" cy="12" r="1"/><circle cx="14" cy="12" r="1"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m20 7-8.5 8.5a2.12 2.12 0 0 1-3 0L4 11V4h7l9 9a2.12 2.12 0 0 1 0 3z"/><circle cx="7.5" cy="7.5" r=".5"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="6"/><path d="m20 20-4.35-4.35"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/><path d="M8 7h8M8 11h8M8 15h5"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 21h14"/><path d="M19 21V7l-3-3H5v17"/><path d="M9 10h6"/><path d="M9 14h6"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M16 3h5v5"/><path d="M4 20 21 3"/><path d="M21 16v5h-5"/><path d="M15 15 21 21"/><path d="M4 4h5l6 6"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 6h16v12H4z"/><path d="m4 7 8 6 8-6"/></svg></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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 0 1 0 18M12 3a15 15 0 0 0 0 18"/></svg></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()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m8 5 11 7-11 7z"/></svg></span><span>Run Now</span></span></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;
|
|
function wfIconSvg(name, sizeClass = 'icon-md') {
|
|
const icons = {
|
|
robot:'<svg viewBox="0 0 24 24"><rect x="7" y="8" width="10" height="8" rx="2"/><path d="M12 4v4M9 16v2M15 16v2M6 10H4M20 10h-2"/><circle cx="10" cy="12" r="1"/><circle cx="14" cy="12" r="1"/></svg>',
|
|
tag:'<svg viewBox="0 0 24 24"><path d="m20 7-8.5 8.5a2.12 2.12 0 0 1-3 0L4 11V4h7l9 9a2.12 2.12 0 0 1 0 3z"/><circle cx="7.5" cy="7.5" r=".5"/></svg>',
|
|
search:'<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="6"/><path d="m20 20-4.35-4.35"/></svg>',
|
|
book:'<svg viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/><path d="M8 7h8M8 11h8M8 15h5"/></svg>',
|
|
kb:'<svg viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
|
|
save:'<svg viewBox="0 0 24 24"><path d="M5 21h14"/><path d="M19 21V7l-3-3H5v17"/><path d="M9 10h6"/><path d="M9 14h6"/></svg>',
|
|
route:'<svg viewBox="0 0 24 24"><path d="M16 3h5v5"/><path d="M4 20 21 3"/><path d="M21 16v5h-5"/><path d="M15 15 21 21"/><path d="M4 4h5l6 6"/></svg>',
|
|
email:'<svg viewBox="0 0 24 24"><path d="M4 6h16v12H4z"/><path d="m4 7 8 6 8-6"/></svg>',
|
|
globe:'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 0 1 0 18M12 3a15 15 0 0 0 0 18"/></svg>',
|
|
plus:'<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>',
|
|
play:'<svg viewBox="0 0 24 24"><path d="m8 5 11 7-11 7z"/></svg>',
|
|
trash:'<svg viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/></svg>',
|
|
up:'<svg viewBox="0 0 24 24"><path d="m18 15-6-6-6 6"/></svg>',
|
|
down:'<svg viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>',
|
|
chevronRight:'<svg viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></svg>',
|
|
bolt:'<svg viewBox="0 0 24 24"><path d="M13 2 3 14h7l-1 8 10-12h-7z"/></svg>',
|
|
settings:'<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01A1.65 1.65 0 0 0 10.5 3.1V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.01a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>'
|
|
};
|
|
return `<span class="icon-svg ${sizeClass}" aria-hidden="true">${icons[name] || icons.settings}</span>`;
|
|
}
|
|
|
|
// ── 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 return the recommended specialised agent.',
|
|
status: 'active',
|
|
trigger: { type:'api_webhook', config:{ path:'/api/workflows/{workflow_id}/run', method:'POST', authHeader:'Session or API auth' } },
|
|
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' } }
|
|
]
|
|
},
|
|
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:wfIconSvg('robot'), classify:wfIconSvg('tag'), extract:wfIconSvg('search'), summarise:wfIconSvg('book'),
|
|
rag_search:wfIconSvg('kb'), save_kb:wfIconSvg('save'), filter:wfIconSvg('route'), email:wfIconSvg('email'), http:wfIconSvg('globe')
|
|
};
|
|
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 class="cezen-list-empty">No workflows yet — <a onclick="newWorkflow()">create your first one</a>.</div>';
|
|
return;
|
|
}
|
|
const q = (document.getElementById('wf-filter')?.value || '').trim().toLowerCase();
|
|
const filtered = q ? workflows.filter(wf => (wf.name || '').toLowerCase().includes(q)) : workflows;
|
|
if (!filtered.length) {
|
|
el.innerHTML = '<div class="cezen-list-empty">No workflows match your filter.</div>';
|
|
return;
|
|
}
|
|
el.innerHTML = filtered.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/workflows/{workflow_id}/run', '/api/workflows/{workflow_id}/run', 'Replace {workflow_id} with the saved workflow ID') +
|
|
field('authHeader','Auth Header', cfg.authHeader||'Session or API auth', 'Session or API auth'),
|
|
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] || wfIconSvg('settings');
|
|
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>${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)">${wfIconSvg('up', 'icon-sm')}</button>` : ''}
|
|
${i < steps.length-1 ? `<button class="wf-step-btn" title="Move down" onclick="moveStep('${s.id}',1)">${wfIconSvg('down', 'icon-sm')}</button>` : ''}
|
|
<button class="wf-step-btn" title="Remove" onclick="removeStep('${s.id}')">${wfIconSvg('trash', 'icon-sm')}</button>
|
|
</div>
|
|
<span class="wf-step-chevron">${wfIconSvg('chevronRight', 'icon-sm')}</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 ? wfIconSvg('bolt', 'icon-sm') : (STEP_ICONS[s.type]||wfIconSvg('settings', 'icon-sm'))}</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 as 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?v=3"></script>
|
|
</body>
|
|
</html>
|