/* @jsxRuntime classic */ /* @jsx React.createElement */ // Orchestra — Frontend v10 (API-wired) // All data comes from the FastAPI backend. No seed data, no localStorage, no in-browser LLM calls. // The simulation engine (buildSimEvents) is kept — it runs client-only as a structural dry-run. // ───────────────────────────────────────────────────────────────────────────────────────────── const { useState, useEffect, useRef, useMemo, useCallback } = React; // ─── Backend URL ──────────────────────────────────────────────────────────────── // Empty string = same origin (Nginx proxies /api/* to FastAPI). // To point at a remote server during local dev, set e.g. 'http://YOUR_IP' const BACKEND = ''; // ─── API helper ───────────────────────────────────────────────────────────────── async function api(method, path, body) { const opts = { method, headers: { 'Content-Type': 'application/json' }, }; if (body !== undefined) opts.body = JSON.stringify(deepSnake(body)); const res = await fetch(BACKEND + path, opts); if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error(`API ${method} ${path} → ${res.status}: ${text}`); } if (res.status === 204) return null; const json = await res.json(); return deepCamel(json); } // ─── Case converters ───────────────────────────────────────────────────────────── // Python returns snake_case; React uses camelCase. Convert at the boundary. function toCamel(s) { return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); } function toSnake(s) { return s.replace(/([A-Z])/g, '_$1').toLowerCase(); } function deepCamel(obj) { if (Array.isArray(obj)) return obj.map(deepCamel); if (obj && typeof obj === 'object') { const out = {}; for (const k of Object.keys(obj)) out[toCamel(k)] = deepCamel(obj[k]); return out; } return obj; } function deepSnake(obj) { if (Array.isArray(obj)) return obj.map(deepSnake); if (obj && typeof obj === 'object') { const out = {}; for (const k of Object.keys(obj)) out[toSnake(k)] = deepSnake(obj[k]); return out; } return obj; } // ─── Map a raw run record from the backend into UI shape ───────────────────────── function mapRunData(raw) { if (!raw) return null; return { id: raw.id, wfId: raw.wfId, wfName: raw.wfName, input: raw.input, status: raw.status, // running | completed | failed | cancelled startedAt: raw.startedAt, completedAt: raw.completedAt, log: raw.log || [], // [{time, event, stage, agent, detail, verdict}] stageStatus: raw.stageStatus || {}, // { stageId: 'running'|'done'|'rework'|'blocked' } docs: raw.docs || {}, // { stageId: { content, deliverableName } } calls: raw.calls || 0, reworks: raw.reworks || 0, stageOrder: raw.stageOrder || [], pendingHuman: raw.pendingHuman || null, // { id, purpose, prompt } | null advisoryBatch: raw.advisoryBatch || [], }; } // ─── Delivery depth constants ──────────────────────────────────────────────────── const DEPTH_LABEL = ['Advisory', 'Standard', 'Rigorous']; const DEPTH_COLOR = ['#4c8ab5', '#c8a84a', '#e08080']; // ─── Deliverable categories ────────────────────────────────────────────────────── const OUTPUT_CATEGORIES = [ 'Requirements', 'Design', 'Development', 'Quality', 'Release', 'Enablement', 'Management', ]; // ─── CSS ───────────────────────────────────────────────────────────────────────── const CSS = ` *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } html, body { height: 100%; } body { font-family: 'Inter', 'Plus Jakarta Sans', system-ui, sans-serif; background: #0a0e1a; background-image: linear-gradient(rgba(255,255,255,0.016) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.016) 1px, transparent 1px); background-size: 52px 52px; color: #e8e6df; min-height: 100vh; -webkit-font-smoothing: antialiased; } /* ── Shell ── */ .shell { display:flex; height:100vh; overflow:hidden; } .sidebar { width:220px; min-width:220px; background:#0d1220; border-right:1px solid #1b2640; display:flex; flex-direction:column; padding:0; overflow-y:auto; } .sidebar-logo { padding:18px 16px 14px; display:flex; align-items:center; gap:9px; border-bottom:1px solid #1b2640; } .logo-mark { width:28px; height:28px; background:#c8a84a; border-radius:6px; display:flex; align-items:center; justify-content:center; flex-shrink:0; } .logo-text { font-size:1.05rem; font-weight:700; color:#e8e6df; } .nav-group { padding:18px 10px 6px; } .nav-label { font-size:0.59rem; font-weight:600; letter-spacing:0.1em; text-transform:uppercase; color:#344054; padding:0 8px; margin-bottom:4px; } .nav-item { display:flex; align-items:center; gap:9px; padding:8px 10px; border-radius:7px; cursor:pointer; font-size:0.85rem; color:#748090; transition:all 0.15s; border:none; background:none; width:100%; text-align:left; } .nav-item:hover { background:#172038; color:#e8e6df; } .nav-item.active { background:#172038; color:#e8e6df; } .nav-item-icon { font-size:0.95rem; width:18px; text-align:center; } .main { flex:1; display:flex; flex-direction:column; overflow:hidden; } .topbar { padding:14px 24px; border-bottom:1px solid #1b2640; display:flex; align-items:center; justify-content:space-between; min-height:56px; background:rgba(13,18,32,0.7); backdrop-filter:blur(12px); flex-shrink:0; } .topbar-title { font-size:0.95rem; font-weight:600; } .page { flex:1; overflow-y:auto; padding:24px; } /* ── Cards & Surfaces ── */ .card { background:#0f1525; border:1px solid #1b2640; border-radius:12px; padding:20px; } .card-sm { background:#0f1525; border:1px solid #1b2640; border-radius:10px; padding:16px; } .surface { background:#172038; border-radius:8px; padding:12px 14px; } /* ── Buttons ── */ .btn { display:inline-flex; align-items:center; gap:6px; padding:8px 14px; border-radius:7px; font-size:0.85rem; font-weight:500; cursor:pointer; border:none; transition:all 0.15s; text-decoration:none; } .btn-gold { background:#c8a84a; color:#08090f; } .btn-gold:hover { background:#d4b860; } .btn-gold:disabled { background:#3a3016; color:#6b5d30; cursor:not-allowed; } .btn-outline { background:transparent; color:#748090; border:1px solid #273558; } .btn-outline:hover { color:#e8e6df; border-color:#4a5568; } .btn-ghost { background:transparent; color:#748090; border:none; } .btn-ghost:hover { color:#e8e6df; } .btn-danger { background:rgba(224,128,128,0.12); color:#e08080; border:1px solid rgba(224,128,128,0.2); } .btn-danger:hover { background:rgba(224,128,128,0.22); } .btn-sm { padding:5px 10px; font-size:0.8rem; } .btn-xs { padding:3px 8px; font-size:0.75rem; } /* ── Forms ── */ input, textarea, select { background:#0a0e1a; border:1px solid #273558; border-radius:6px; color:#e8e6df; font-family:inherit; font-size:0.875rem; padding:8px 10px; outline:none; transition:border-color 0.15s; } input:focus, textarea:focus, select:focus { border-color:#c8a84a; } textarea { resize:vertical; line-height:1.5; } label { font-size:0.8rem; color:#748090; display:block; margin-bottom:4px; } .field { margin-bottom:14px; } /* ── Badges ── */ .badge { display:inline-flex; align-items:center; padding:2px 8px; border-radius:5px; font-size:0.72rem; font-weight:500; letter-spacing:0.02em; } .badge-gold { background:rgba(200,168,74,0.15); color:#c8a84a; } .badge-blue { background:rgba(76,138,181,0.15); color:#6baede; } .badge-red { background:rgba(224,128,128,0.12); color:#e08080; } .badge-gray { background:rgba(116,128,144,0.15); color:#748090; } .badge-green { background:rgba(74,222,128,0.12); color:#4ade80; } /* ── Depth tags ── */ .depth-tag { display:inline-flex; align-items:center; padding:2px 8px; border-radius:20px; font-size:0.73rem; font-weight:500; } .depth-0 { background:rgba(76,138,181,0.15); color:#6baede; } .depth-1 { background:rgba(200,168,74,0.15); color:#c8a84a; } .depth-2 { background:rgba(224,128,128,0.12); color:#e08080; } /* ── Agent chips ── */ .agent-chip { display:inline-flex; align-items:center; gap:5px; padding:3px 8px; background:#172038; border:1px solid #1b2640; border-radius:20px; font-size:0.77rem; cursor:default; } .agent-avatar { width:20px; height:20px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:0.6rem; font-weight:700; flex-shrink:0; } /* ── Workflow canvas ── */ .wf-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(300px,1fr)); gap:14px; } .wf-card { background:#0f1525; border:1px solid #1b2640; border-radius:12px; padding:18px; cursor:pointer; transition:all 0.15s; } .wf-card:hover { border-color:#c8a84a; transform:translateY(-1px); } .wf-card.active-wf { border-color:#c8a84a; } .wf-name { font-weight:600; font-size:0.95rem; margin-bottom:5px; } .wf-desc { font-size:0.8rem; color:#748090; line-height:1.55; } .wf-meta { font-size:0.72rem; color:#344054; margin-top:10px; } /* ── Pipeline ── */ .pipeline-wrap { overflow-x:auto; padding:12px 0 24px; } .pipeline-row { display:flex; align-items:flex-start; gap:0; min-width:max-content; position:relative; } .stage-col { display:flex; flex-direction:column; align-items:center; position:relative; } .stage-connector { width:32px; height:2px; background:linear-gradient(90deg,#c8a84a88,#c8a84a44); align-self:center; margin-top:52px; flex-shrink:0; } .stage-node { width:140px; border:1px solid #273558; border-radius:10px; background:#0f1525; padding:12px; cursor:pointer; transition:all 0.15s; position:relative; } .stage-node:hover { border-color:#4a5568; } .stage-node.active-stage { border-color:#c8a84a; box-shadow:0 0 0 1px #c8a84a22; } .stage-num { font-size:0.6rem; color:#344054; letter-spacing:0.06em; margin-bottom:4px; font-variant-numeric:tabular-nums; } .stage-name { font-size:0.85rem; font-weight:600; margin-bottom:4px; } .stage-owner { font-size:0.72rem; color:#748090; } .stage-output { font-size:0.7rem; color:#4a5568; margin-top:5px; } .stage-reviews { display:flex; flex-wrap:wrap; gap:4px; margin-top:8px; } .review-dot { width:8px; height:8px; border-radius:50%; } /* Arc SVG layer */ .arc-layer { position:absolute; top:0; left:0; pointer-events:none; overflow:visible; } /* ── Inspector panel ── */ .inspector { width:360px; min-width:360px; border-left:1px solid #1b2640; background:#0d1220; overflow-y:auto; display:flex; flex-direction:column; } .inspector-head { padding:16px 18px; border-bottom:1px solid #1b2640; display:flex; align-items:center; justify-content:space-between; flex-shrink:0; } .inspector-body { padding:18px; flex:1; overflow-y:auto; } .inspector-section { margin-bottom:22px; } .inspector-section-title { font-size:0.7rem; font-weight:600; letter-spacing:0.08em; text-transform:uppercase; color:#344054; margin-bottom:10px; } /* ── Reviews list ── */ .review-item { background:#172038; border-radius:8px; padding:10px 12px; margin-bottom:8px; display:flex; align-items:center; justify-content:space-between; gap:8px; } .review-drag { color:#344054; cursor:grab; font-size:0.8rem; } .review-drag:active { cursor:grabbing; } /* ── Run page ── */ .run-layout { display:flex; gap:20px; height:100%; } .run-sidebar { width:280px; min-width:280px; display:flex; flex-direction:column; gap:14px; } .run-main { flex:1; display:flex; flex-direction:column; gap:14px; min-width:0; } .timeline { display:flex; flex-direction:column; gap:0; } .tl-entry { display:flex; gap:12px; padding:8px 0; border-bottom:1px solid #1b2640; font-size:0.82rem; } .tl-entry:last-child { border-bottom:none; } .tl-dot { width:8px; height:8px; border-radius:50%; margin-top:5px; flex-shrink:0; } .tl-time { color:#344054; font-size:0.72rem; white-space:nowrap; padding-top:1px; min-width:44px; } .tl-text { flex:1; line-height:1.5; color:#748090; } .tl-text b { color:#e8e6df; } .tl-verdict { font-size:0.72rem; margin-top:3px; } .status-pill { display:inline-flex; align-items:center; gap:5px; padding:3px 9px; border-radius:20px; font-size:0.78rem; font-weight:500; } .status-running { background:rgba(200,168,74,0.12); color:#c8a84a; } .status-completed { background:rgba(74,222,128,0.1); color:#4ade80; } .status-failed { background:rgba(224,128,128,0.1); color:#e08080; } .status-cancelled { background:rgba(116,128,144,0.12); color:#748090; } .status-idle { background:rgba(116,128,144,0.08); color:#4a5568; } .pulse-dot { width:6px; height:6px; border-radius:50%; background:#c8a84a; animation:pulse 2s ease-in-out infinite; } @keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:0.25;} } /* ── Docs panel ── */ .doc-card { background:#0f1525; border:1px solid #1b2640; border-radius:10px; padding:14px; margin-bottom:10px; } .doc-name { font-weight:600; font-size:0.85rem; margin-bottom:6px; } .doc-content { font-size:0.8rem; color:#748090; line-height:1.65; white-space:pre-wrap; max-height:200px; overflow-y:auto; } /* ── Doc expand modal ── */ .doc-modal { max-width:860px; max-height:90vh; } .doc-modal-content { font-size:0.82rem; color:#c8c4bc; line-height:1.7; white-space:pre-wrap; font-family:'JetBrains Mono','Courier New',monospace; } /* ── Agents page ── */ .agents-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(280px,1fr)); gap:14px; } .agent-card { background:#0f1525; border:1px solid #1b2640; border-radius:12px; padding:18px; cursor:pointer; transition:all 0.15s; } .agent-card:hover { border-color:#4a5568; } .agent-card.active { border-color:#c8a84a; } .agent-card-head { display:flex; align-items:center; gap:10px; margin-bottom:12px; } .agent-role { font-weight:600; font-size:0.95rem; } .agent-tagline { font-size:0.8rem; color:#748090; line-height:1.45; margin-bottom:12px; } .skill-row { display:flex; align-items:flex-start; gap:8px; padding:8px 10px; background:#172038; border-radius:7px; margin-bottom:6px; } .skill-toggle { flex-shrink:0; margin-top:1px; cursor:pointer; accent-color:#c8a84a; } .skill-name { font-size:0.82rem; font-weight:500; } .skill-prompt { font-size:0.76rem; color:#748090; margin-top:3px; line-height:1.45; white-space:pre-wrap; } .skill-modified { color:#c8a84a; font-size:0.68rem; margin-left:5px; } /* ── Economics page ── */ .econ-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(220px,1fr)); gap:14px; margin-bottom:24px; } .stat-card { background:#0f1525; border:1px solid #1b2640; border-radius:10px; padding:18px; } .stat-val { font-size:1.8rem; font-weight:700; color:#c8a84a; line-height:1; } .stat-label { font-size:0.78rem; color:#748090; margin-top:5px; } /* ── Org context page ── */ .org-grid { display:grid; grid-template-columns:1fr 1fr; gap:14px; } /* ── Advisor / FinOps modals ── */ .modal-overlay { position:fixed; inset:0; background:rgba(0,0,0,0.7); display:flex; align-items:center; justify-content:center; z-index:500; padding:24px; } .modal { background:#0d1220; border:1px solid #273558; border-radius:14px; max-width:680px; width:100%; max-height:80vh; display:flex; flex-direction:column; box-shadow:0 24px 60px rgba(0,0,0,0.5); } .modal-head { padding:18px 20px; border-bottom:1px solid #1b2640; display:flex; align-items:center; justify-content:space-between; flex-shrink:0; } .modal-body { padding:20px; overflow-y:auto; flex:1; } .rec-item { background:#172038; border-radius:8px; padding:12px 14px; margin-bottom:10px; } .rec-title { font-weight:600; font-size:0.88rem; margin-bottom:4px; } .rec-detail { font-size:0.8rem; color:#748090; line-height:1.5; margin-bottom:8px; } .rec-actions { display:flex; gap:6px; flex-wrap:wrap; } /* ── Human gate ── */ .human-gate { background:rgba(200,168,74,0.08); border:1px solid rgba(200,168,74,0.3); border-radius:10px; padding:18px; margin-bottom:14px; } .human-gate-title { font-weight:600; font-size:0.9rem; color:#c8a84a; margin-bottom:6px; } .human-gate-prompt { font-size:0.83rem; color:#748090; line-height:1.55; margin-bottom:12px; } .human-gate-actions { display:flex; gap:8px; flex-wrap:wrap; } /* ── Sim mode banner ── */ .sim-banner { background:rgba(76,138,181,0.08); border:1px solid rgba(76,138,181,0.2); border-radius:8px; padding:10px 14px; font-size:0.8rem; color:#6baede; margin-bottom:14px; } /* ── Load error ── */ .load-error { padding:40px; text-align:center; } .load-error h2 { font-size:1.1rem; margin-bottom:8px; color:#e08080; } .load-error p { font-size:0.85rem; color:#748090; line-height:1.6; } /* ── Tabs ── */ .tabs { display:flex; gap:2px; border-bottom:1px solid #1b2640; margin-bottom:20px; } .tab { padding:9px 16px; font-size:0.84rem; cursor:pointer; border:none; background:none; color:#748090; border-bottom:2px solid transparent; transition:all 0.15s; margin-bottom:-1px; } .tab:hover { color:#e8e6df; } .tab.active { color:#c8a84a; border-bottom-color:#c8a84a; } /* ── Misc ── */ .row { display:flex; align-items:center; gap:8px; } .row-between { display:flex; align-items:center; justify-content:space-between; gap:8px; } .col { display:flex; flex-direction:column; gap:8px; } .gap-4 { gap:4px; } .gap-6 { gap:6px; } .gap-12 { gap:12px; } .mt-8 { margin-top:8px; } .mt-14 { margin-top:14px; } .mt-20 { margin-top:20px; } .mb-8 { margin-bottom:8px; } .mb-14 { margin-bottom:14px; } .text-muted { color:#748090; font-size:0.82rem; } .text-xs { font-size:0.75rem; color:#344054; } .text-gold { color:#c8a84a; } .text-red { color:#e08080; } .divider { height:1px; background:#1b2640; margin:18px 0; } .mono { font-family:'JetBrains Mono','Courier New',monospace; } .empty-state { padding:40px; text-align:center; color:#344054; font-size:0.85rem; } /* ── Drag-over highlight ── */ .drag-over { border-color:#c8a84a !important; background:rgba(200,168,74,0.05) !important; } /* ── Scrollbar ── */ ::-webkit-scrollbar { width:6px; height:6px; } ::-webkit-scrollbar-track { background:transparent; } ::-webkit-scrollbar-thumb { background:#1b2640; border-radius:3px; } ::-webkit-scrollbar-thumb:hover { background:#273558; } @media (max-width:900px) { .sidebar { display:none; } .inspector { display:none; } .org-grid { grid-template-columns:1fr; } } `; // ─── Simulation engine (client-only, no API calls) ─────────────────────────────── function buildSimEvents(workflow, agentById) { const events = []; let t = 0; const stamp = (pause = 650) => { t += pause; return t; }; for (let si = 0; si < workflow.stages.length; si++) { const stage = workflow.stages[si]; const owner = agentById(stage.ownerId); events.push({ t: stamp(400), type: 'stage_start', stage: stage.name, agent: owner?.role || '?', detail: `${owner?.role || 'Agent'} beginning work on ${stage.name}` }); events.push({ t: stamp(900), type: 'generating', stage: stage.name, agent: owner?.role || '?', detail: `Producing ${stage.outputId || 'deliverable'}…` }); events.push({ t: stamp(1100), type: 'stage_done', stage: stage.name, agent: owner?.role || '?', detail: `Deliverable complete` }); const reviews = (stage.reviews || []).filter(r => r.kind !== 'human'); for (const rev of reviews) { if (!rev.agentId) continue; const reviewer = agentById(rev.agentId); const depthLabel = DEPTH_LABEL[rev.depth ?? 1]; events.push({ t: stamp(600), type: 'review_start', stage: stage.name, agent: reviewer?.role || '?', detail: `${depthLabel} review starting`, depth: rev.depth }); // Simulate occasional rework at Standard/Rigorous const rework = rev.depth >= 1 && Math.random() < 0.25; if (rework && si > 0) { const prevStage = workflow.stages[si - 1]; events.push({ t: stamp(800), type: 'rework', stage: stage.name, agent: reviewer?.role || '?', detail: `Changes required — routing back to ${prevStage.name}`, verdict: 'rework' }); events.push({ t: stamp(600), type: 'stage_start', stage: prevStage.name, agent: agentById(prevStage.ownerId)?.role || '?', detail: `Rework: revising ${prevStage.name}` }); events.push({ t: stamp(900), type: 'stage_done', stage: prevStage.name, agent: agentById(prevStage.ownerId)?.role || '?', detail: `Revision complete` }); events.push({ t: stamp(500), type: 'review_start', stage: stage.name, agent: reviewer?.role || '?', detail: `Re-review after revision`, depth: rev.depth }); } events.push({ t: stamp(700), type: 'review_done', stage: stage.name, agent: reviewer?.role || '?', detail: `Approved`, verdict: 'approve' }); } } events.push({ t: stamp(400), type: 'workflow_done', stage: '', agent: '', detail: 'Workflow complete — all stages and review gates cleared' }); return events; } // ─── Icon helpers ───────────────────────────────────────────────────────────────── const PAGE_ICONS = { workflows:'⚡', run:'▶', agents:'🧑‍💼', 'context-sources':'🔗', economics:'💰', org:'🏢', documents:'📄', operations:'📊' }; const EVENT_COLORS = { stage_start:'#c8a84a', generating:'#4c8ab5', stage_done:'#4ade80', review_start:'#6baede', review_done:'#4ade80', rework:'#e08080', workflow_done:'#4ade80', human_gate:'#c8a84a', advisory:'#4c8ab5', error:'#e08080', }; // ─── Main App Component ───────────────────────────────────────────────────────── function Orchestra() { // ── Page state ── const [page, setPage] = useState('run'); const [loadError, setLoadError] = useState(null); const [loading, setLoading] = useState(true); // ── Data state (all loaded from backend) ── const [agents, setAgents] = useState([]); const [workflows, setWorkflows] = useState([]); const [outputs, setOutputs] = useState([]); // deliverable catalog const [economics, setEconomics] = useState(null); const [operations, setOperations] = useState(null); const [orgContext, setOrgContext] = useState({ description: '', objects: '', automations: '', security: '', conventions: '', openWork: '' }); const [orgEntries, setOrgEntries] = useState([]); const [allDocs, setAllDocs] = useState([]); // ── Workflow editor state ── const [activeWfId, setActiveWfId] = useState(null); const [activeStageId, setActiveStageId] = useState(null); const [wfTab, setWfTab] = useState('canvas'); // canvas | settings // ── Agent inspector state ── const [activeAgentId, setActiveAgentId] = useState(null); const [expandedSkillId, setExpandedSkillId] = useState(null); // ── Run state ── const [runs, setRuns] = useState([]); const [activeRunId, setActiveRunId] = useState(null); const [running, setRunning] = useState(false); const [runInput, setRunInput] = useState(''); const [selectedWfForRun, setSelectedWfForRun] = useState(null); // ── Simulation state ── const [simMode, setSimMode] = useState(false); const [sim, setSim] = useState(null); // { log, running, wfName } const simTimers = useRef([]); // ── Advisor / FinOps modals ── const [advisorModal, setAdvisorModal] = useState(null); // { wfId, loading, recs } const [finopsModal, setFinopsModal] = useState(null); // { loading, data } const [settingsModal, setSettingsModal] = useState(null); // { loading, data, error, saving } const [settingsPin, setSettingsPin] = useState(''); // ── Arc rendering ── const pipeRef = useRef(null); const [cardRects, setCardRects] = useState({}); // ── Debounce save timers ── const saveTimers = useRef({}); // ─── Load everything on mount ──────────────────────────────────────────────── useEffect(() => { (async () => { try { const [ags, wfs, outs] = await Promise.all([ api('GET', '/api/agents'), api('GET', '/api/workflows'), api('GET', '/api/outputs'), ]); setAgents(ags); setWorkflows(wfs); setOutputs(outs); if (wfs.length) { setActiveWfId(wfs[0].id); setSelectedWfForRun(wfs[0].id); } if (ags.length) setActiveAgentId(ags[0].id); // Load runs, economics, org context (non-critical — don't block on failure) try { const runsData = await api('GET', '/api/runs'); setRuns((runsData || []).map(mapRunData)); if (runsData?.length) setActiveRunId(runsData[0].id); } catch {} try { const econ = await api('GET', '/api/economics'); setEconomics(econ); } catch {} try { const org = await api('GET', '/api/org-context'); if (org) setOrgContext(org); } catch {} try { const entries = await api('GET', '/api/org-context/entries'); if (entries) setOrgEntries(entries); } catch {} try { const docs = await api('GET', '/api/documents'); if (docs) setAllDocs(docs); } catch {} setLoading(false); } catch (err) { setLoadError(err.message); setLoading(false); } })(); }, []); // ─── Derived helpers ───────────────────────────────────────────────────────── const agentById = useCallback((id) => agents.find(a => a.id === id), [agents]); const outputById = useCallback((id) => outputs.find(o => o.id === id), [outputs]); const activeWf = useMemo(() => workflows.find(w => w.id === activeWfId), [workflows, activeWfId]); const activeStage = useMemo(() => activeWf?.stages?.find(s => s.id === activeStageId), [activeWf, activeStageId]); const activeAgent = useMemo(() => agents.find(a => a.id === activeAgentId), [agents, activeAgentId]); const activeRun = useMemo(() => runs.find(r => r.id === activeRunId), [runs, activeRunId]); // ─── Debounced save helpers ────────────────────────────────────────────────── function debounceSave(key, fn, delay = 700) { if (saveTimers.current[key]) clearTimeout(saveTimers.current[key]); saveTimers.current[key] = setTimeout(fn, delay); } // ─── Workflow mutations ────────────────────────────────────────────────────── function patchWorkflows(fn) { setWorkflows(ws => { const next = fn(ws); const updated = next.find(w => w.id === activeWfId); if (updated) { debounceSave(`wf-${updated.id}`, () => { api('PUT', `/api/workflows/${updated.id}`, updated).catch(console.error); }); } return next; }); } function patchStages(fn) { patchWorkflows(ws => ws.map(w => w.id === activeWfId ? { ...w, stages: fn(w.stages) } : w)); } function patchStage(stageId, fn) { patchStages(stages => stages.map(s => s.id === stageId ? fn(s) : s)); } // ─── Stage operations ──────────────────────────────────────────────────────── function addStage() { const id = `s_${Date.now()}`; patchStages(stages => [...stages, { id, name: 'New Stage', ownerId: agents[0]?.id || '', outputId: outputs[0]?.id || '', ownerSkillIds: null, instructions: '', reviews: [], }]); setActiveStageId(id); } function deleteStage(stageId) { patchStages(stages => stages.filter(s => s.id !== stageId)); if (activeStageId === stageId) setActiveStageId(null); } function addReview(stageId) { const id = `rv_${Date.now()}`; patchStage(stageId, s => ({ ...s, reviews: [...(s.reviews || []), { id, agentId: agents[0]?.id || '', depth: 1, skillIds: null }] })); } function addHumanGate(stageId) { const id = `hg_${Date.now()}`; patchStage(stageId, s => ({ ...s, reviews: [...(s.reviews || []), { id, kind: 'human', purpose: 'review_approve', trigger: 'always', flags: [], prompt: '' }] })); } function removeReview(stageId, reviewId) { patchStage(stageId, s => ({ ...s, reviews: (s.reviews || []).filter(r => r.id !== reviewId) })); } // ─── Agent mutations ───────────────────────────────────────────────────────── function patchAgent(agentId, fn) { setAgents(as => { const next = as.map(a => a.id === agentId ? fn(a) : a); const updated = next.find(a => a.id === agentId); if (updated) { debounceSave(`ag-${agentId}`, () => { api('PUT', `/api/agents/${agentId}`, updated).catch(console.error); }); } return next; }); } function toggleSkill(agentId, skillId) { patchAgent(agentId, a => ({ ...a, skills: a.skills.map(sk => sk.id === skillId ? { ...sk, enabled: !sk.enabled } : sk) })); } function editSkillPrompt(agentId, skillId, prompt) { patchAgent(agentId, a => ({ ...a, skills: a.skills.map(sk => sk.id === skillId ? { ...sk, prompt, custom: prompt !== sk.defaultPrompt } : sk), })); } function revertSkill(agentId, skillId) { patchAgent(agentId, a => ({ ...a, skills: a.skills.map(sk => sk.id === skillId ? { ...sk, prompt: sk.defaultPrompt, custom: false } : sk), })); } // ─── Workflow CRUD ─────────────────────────────────────────────────────────── async function createWorkflow() { const id = `wf_${Date.now()}`; const wf = { id, name: 'New Workflow', description: '', stages: [] }; try { const saved = await api('POST', '/api/workflows', wf); setWorkflows(ws => [...ws, saved || wf]); setActiveWfId(id); } catch (err) { console.error(err); } } async function duplicateWorkflow(wfId) { const src = workflows.find(w => w.id === wfId); if (!src) return; const id = `wf_${Date.now()}`; const copy = { ...src, id, name: src.name + ' (copy)' }; try { const saved = await api('POST', '/api/workflows', copy); setWorkflows(ws => [...ws, saved || copy]); setActiveWfId(id); } catch (err) { console.error(err); } } async function deleteWorkflow(wfId) { try { await api('DELETE', `/api/workflows/${wfId}`); setWorkflows(ws => ws.filter(w => w.id !== wfId)); if (activeWfId === wfId) { const remaining = workflows.filter(w => w.id !== wfId); setActiveWfId(remaining[0]?.id || null); } } catch (err) { console.error(err); } } // ─── Run workflow ──────────────────────────────────────────────────────────── async function startRun(wfId, input) { if (!input.trim()) return; setRunning(true); try { const run = await api('POST', '/api/runs', { wfId, input }); const mapped = mapRunData(run); setRuns(rs => [mapped, ...rs]); setActiveRunId(mapped.id); pollRun(mapped.id); } catch (err) { console.error(err); setRunning(false); } } function pollRun(runId) { let live = true; const tick = async () => { if (!live) return; try { const raw = await api('GET', `/api/runs/${runId}`); const mapped = mapRunData(raw); setRuns(rs => rs.map(r => r.id === runId ? mapped : r)); if (mapped.status === 'running') { setTimeout(tick, 2000); } else { setRunning(false); } } catch { setTimeout(tick, 3000); } }; setTimeout(tick, 1500); return () => { live = false; }; } async function stopRun(runId) { try { await api('POST', `/api/runs/${runId}/cancel`); } catch {} setRunning(false); } async function resolveHuman(runId, gateId, approved, notes) { try { await api('POST', `/api/runs/${runId}/resume`, { gateId, approved, notes }); pollRun(runId); } catch (err) { console.error(err); } } // ─── Simulation (client-only) ──────────────────────────────────────────────── function runSimulation(wf) { simTimers.current.forEach(t => clearTimeout(t)); simTimers.current = []; setSim({ log: [], running: true, wfName: wf.name }); setSimMode(true); const events = buildSimEvents(wf, agentById); events.forEach(ev => { const tid = setTimeout(() => { setSim(s => { if (!s) return s; const done = ev.type === 'workflow_done'; return { ...s, log: [...s.log, ev], running: !done }; }); }, ev.t); simTimers.current.push(tid); }); } function stopSim() { simTimers.current.forEach(t => clearTimeout(t)); simTimers.current = []; setSim(null); setSimMode(false); } // ─── Advisor ──────────────────────────────────────────────────────────────── async function openSettings() { setSettingsPin(''); setSettingsModal({ loading: true }); try { const data = await api('GET', '/api/admin/settings'); setSettingsModal({ loading: false, data }); } catch (err) { setSettingsModal({ loading: false, error: err.message }); } } async function setModelProvider(provider) { setSettingsModal(m => ({ ...m, saving: true, error: null })); try { const headers = { 'Content-Type': 'application/json' }; if (settingsPin) headers['X-Admin-Pin'] = settingsPin; const res = await fetch(BACKEND + '/api/admin/settings', { method: 'PATCH', headers, body: JSON.stringify(deepSnake({ modelProvider: provider })), }); if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error(`API PATCH /api/admin/settings → ${res.status}: ${text}`); } const data = deepCamel(await res.json()); setSettingsModal({ loading: false, data }); } catch (err) { const match = (err.message || '').match(/"detail":"([^"]+)"/); const msg = match ? match[1] : err.message; setSettingsModal(m => ({ ...m, saving: false, error: msg })); } } async function runAdvisor(wfId) { setAdvisorModal({ wfId, loading: true, recs: [] }); try { const res = await api('POST', `/api/workflows/${wfId}/advisor`, {}); setAdvisorModal({ wfId, loading: false, recs: res.recommendations || [] }); } catch (err) { setAdvisorModal({ wfId, loading: false, recs: [], error: err.message }); } } async function applyRec(wfId, action) { try { const res = await api('POST', `/api/workflows/${wfId}/apply-action`, { action }); if (res?.workflow) { setWorkflows(ws => ws.map(w => w.id === wfId ? res.workflow : w)); } } catch (err) { console.error(err); } } // ─── FinOps ───────────────────────────────────────────────────────────────── async function runFinOps(wfId) { setFinopsModal({ loading: true, data: null }); try { const res = await api('POST', `/api/workflows/${wfId}/finops`, {}); setFinopsModal({ loading: false, data: res }); } catch (err) { setFinopsModal({ loading: false, data: null, error: err.message }); } } // ─── Org context ──────────────────────────────────────────────────────────── function saveOrgContext(updates) { const next = { ...orgContext, ...updates }; setOrgContext(next); debounceSave('org', () => { api('PUT', '/api/org-context', next).catch(console.error); }); } async function createOrgEntry(data) { const entry = await api('POST', '/api/org-context/entries', data); setOrgEntries(prev => [...prev, entry]); return entry; } async function updateOrgEntry(id, data) { await api('PATCH', `/api/org-context/entries/${id}`, data); setOrgEntries(prev => prev.map(e => e.id === id ? { ...e, ...data } : e)); } async function deleteOrgEntry(id) { await api('DELETE', `/api/org-context/entries/${id}`); setOrgEntries(prev => prev.filter(e => e.id !== id)); } async function refreshDocs() { try { const docs = await api('GET', '/api/documents'); if (docs) setAllDocs(docs); } catch {} } // ─── Arc rendering ─────────────────────────────────────────────────────────── const arcs = useMemo(() => { if (!activeWf?.stages || Object.keys(cardRects).length === 0) return []; const result = []; activeWf.stages.forEach((stage, si) => { (stage.reviews || []).forEach(rev => { if (rev.kind === 'human' || !rev.agentId) return; // find the stage the reviewer "owns" (where they are the owner agent) const reviewerStageIdx = activeWf.stages.findIndex(s => s.ownerId === rev.agentId); if (reviewerStageIdx < 0 || reviewerStageIdx >= si) return; // only backward arcs const fromRect = cardRects[stage.id]; const toRect = cardRects[activeWf.stages[reviewerStageIdx].id]; if (!fromRect || !toRect) return; result.push({ from: stage.id, to: activeWf.stages[reviewerStageIdx].id, fromRect, toRect, depth: rev.depth ?? 1 }); }); }); return result; }, [activeWf, cardRects]); // ─── Measure card positions ────────────────────────────────────────────────── useEffect(() => { if (!pipeRef.current) return; const measure = () => { const pipe = pipeRef.current; if (!pipe) return; const pipeRect = pipe.getBoundingClientRect(); const rects = {}; pipe.querySelectorAll('[data-stage-id]').forEach(el => { const sid = el.dataset.stageId; const r = el.getBoundingClientRect(); rects[sid] = { x: r.left - pipeRect.left + r.width / 2, y: r.top - pipeRect.top, w: r.width, h: r.height, }; }); setCardRects(rects); }; measure(); window.addEventListener('resize', measure); return () => window.removeEventListener('resize', measure); }, [activeWf, page]); // ─── Inject CSS ────────────────────────────────────────────────────────────── useEffect(() => { const el = document.createElement('style'); el.textContent = CSS; document.head.appendChild(el); return () => el.remove(); }, []); // ─── Error / loading screens ───────────────────────────────────────────────── if (loadError) { return (
⚠️
Cannot connect to Orchestra backend
Make sure the backend is running (docker compose up -d) and accessible, then reload this page.

Error: {loadError}
); } if (loading) { return (
Orchestra
); } // ─── Render ────────────────────────────────────────────────────────────────── return (
{/* ── Sidebar ── */}
Orchestra
Workspace
{[ ['run', 'Run & Generate'], ['workflows', 'Workflows'], ['agents', 'Agent Library'], ].map(([id, label]) => ( ))}
Context
{[ ['org', 'Org Context'], ['documents', 'Documents'], ].map(([id, label]) => ( ))}
Analytics
{[ ['economics', 'Economics'], ['operations', 'Operations'], ].map(([id, label]) => ( ))}
Configuration
{/* ── Main content area ── */}
{page === 'workflows' && } {page === 'run' && } {page === 'agents' && } {page === 'context-sources' && window.ContextSourcesPage && React.createElement(window.ContextSourcesPage, { agents })} {page === 'economics' && } {page === 'operations' && } {page === 'org' && } {page === 'documents' && }
{/* ── Advisor modal ── */} {advisorModal && (
e.target === e.currentTarget && setAdvisorModal(null)}>
Delivery Advisor
Workflow analysis & recommendations
{advisorModal.loading &&
Analysing workflow…
} {advisorModal.error &&
{advisorModal.error}
} {!advisorModal.loading && (advisorModal.recs || []).map((rec, i) => (
{rec.title}
{rec.detail || rec.description}
{rec.action && ( )}
))} {!advisorModal.loading && !advisorModal.error && !(advisorModal.recs?.length) && (
No recommendations — workflow looks well-configured.
)}
)} {/* ── FinOps modal ── */} {finopsModal && (
e.target === e.currentTarget && setFinopsModal(null)}>
FinOps Analyst
Cost projection & run economics
{finopsModal.loading &&
Calculating cost projection…
} {finopsModal.error &&
{finopsModal.error}
} {finopsModal.data && (
{[ ['Estimated cost', finopsModal.data.estimatedCost || '—'], ['Estimated tokens', finopsModal.data.estimatedTokens || '—'], ['Estimated duration', finopsModal.data.estimatedDuration || '—'], ].map(([label, val]) => (
{val}
{label}
))}
{finopsModal.data.notes && (
{finopsModal.data.notes}
)}
)}
)} {/* ── Settings modal (AI model toggle) ── */} {settingsModal && (
e.target === e.currentTarget && setSettingsModal(null)}>
Settings
AI model & preferences
{settingsModal.loading && (
Loading settings…
)} {settingsModal.error && (
{settingsModal.error}
)} {settingsModal.data && (
AI Model
Which model new runs use. Vertex AI is production (billed); AI Studio is the free tier for testing.
{[['vertex', settingsModal.data.providers?.vertex], ['aistudio', settingsModal.data.providers?.aistudio]].map(([key, info]) => { const cur = settingsModal.data.modelProvider; const active = cur === key || (!cur && settingsModal.data.activeModel === info?.model); const unavailable = info && !info.available; return ( ); })}
Active model {settingsModal.data.activeModel}
{settingsModal.data.pinRequired && (
Admin PIN
setSettingsPin(e.target.value)} style={{ width:'100%' }} />
)}
)}
)}
); } // ─── Workflows Page ────────────────────────────────────────────────────────────── function WorkflowsPage({ workflows, activeWfId, activeStageId, setActiveWfId, setActiveStageId, activeWf, activeStage, agents, outputs, agentById, outputById, wfTab, setWfTab, addStage, deleteStage, patchStage, patchWorkflows, addReview, addHumanGate, removeReview, createWorkflow, duplicateWorkflow, deleteWorkflow, runSimulation, runAdvisor, pipeRef, arcs }) { return (
{/* Left panel: wf list */}
Workflows
{workflows.map(wf => (
{ setActiveWfId(wf.id); setActiveStageId(null); }} >
{wf.name}
{wf.stages?.length || 0} stages
{workflows.length > 1 && ( )}
))}
{/* Centre: canvas */}
{activeWf ? ( <>
{activeWf.name}
{activeWf.description}
{/* Pipeline canvas */}
{/* Arc SVG layer */} {arcs.length > 0 && ( {arcs.map((arc, i) => { const x1 = arc.fromRect.x, x2 = arc.toRect.x; const y1 = -10, y2 = -10; const cpY = -80 - i * 18; const color = DEPTH_COLOR[arc.depth ?? 1]; const dash = arc.depth === 0 ? '5,4' : arc.depth === 2 ? 'none' : 'none'; const sw = arc.depth === 2 ? 2 : arc.depth === 0 ? 1 : 1.5; return ( ); })} )} {(activeWf.stages || []).map((stage, si) => { const owner = agentById(stage.ownerId); const out = outputById(stage.outputId); const isActive = stage.id === activeStageId; return ( {si > 0 &&
}
setActiveStageId(isActive ? null : stage.id)} >
STAGE {String(si+1).padStart(2,'0')}
{stage.name}
{owner?.role || '—'}
{out &&
{out.name}
}
{(stage.reviews || []).map(rev => (
))}
); })}
) : (
Select a workflow or create a new one.
)}
{/* Right: stage inspector */} {activeStage && ( s.id === activeStage.id)} agents={agents} outputs={outputs} agentById={agentById} patchStage={patchStage} addReview={addReview} addHumanGate={addHumanGate} removeReview={removeReview} deleteStage={deleteStage} /> )}
); } // ─── Stage Inspector ───────────────────────────────────────────────────────────── function StageInspector({ stage, stageIndex, agents, outputs, agentById, patchStage, addReview, addHumanGate, removeReview, deleteStage }) { return (
Stage {stageIndex+1}: {stage.name}
{/* Name */}
Name
patchStage(stage.id, s => ({ ...s, name: e.target.value }))} />
{/* Owner */}
Owner Agent
{/* Deliverable */}
Deliverable
{/* Instructions */}
Stage Instructions