aipackage/cezen-portal/workflows.html
Jino Jose b3eec34158 Sync cezen-portal/ from live working copy (V1.0 GA polish + search-icon ordering fix)
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.
2026-07-09 11:11:38 +05:30

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 &amp; 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 &amp; 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 &mdash; <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}} == &quot;legal&quot;">
<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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function escAttr(s) { return (s||'').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
// ── Init ─────────────────────────────────────────────────────────────────────
loadWorkflows();
</script>
<script src="auth.js"></script>
<script src="branding.js?v=3"></script>
</body>
</html>