Skip to content

.archon/dashboard/public/js/workflow.js

Source location: docs/source-files/.archon/dashboard/public/js/workflow.js — this page is a rendered mirror; the file is the source of truth.

workflow.js
js
/**
 * Workflow topology renderer — file cards, phase cards, SVG lines,
 * live agent highlight, floating agent panel.
 */

/* global WF_FILES, WF_MODES, esc, elapsed, PHASE_LABELS, renderLifecycleTrack, S */

var _wfMode = 'demand';
var _agentFloatOpen = false;

// ═══════════════════════════════════════════════════════════════════════════
// RENDER: Main workflow canvas
// ═══════════════════════════════════════════════════════════════════════════

function renderWorkflow(state) {
  var mode = WF_MODES[_wfMode];
  if (!mode) return '<div class="page"><p>Unknown mode.</p></div>';

  var html = '';

  // Tab bar
  html += '<div class="wf-tabs">';
  var modeKeys = ['demand', 'plan', 'review'];
  for (var mi = 0; mi < modeKeys.length; mi++) {
    var mk = modeKeys[mi];
    var m = WF_MODES[mk];
    var activeCls = mk === _wfMode ? ' wf-tab--active' : '';
    html += '<button class="wf-tab' + activeCls + '" data-wfmode="' + mk + '">';
    html += m.icon + ' ' + m.label;
    html += '</button>';
  }
  html += '</div>';

  // Legend
  html += '<div class="wf-legend">';
  html += '<span><span class="wf-legend-line" style="background:#66BB6A"></span>READ</span>';
  html += '<span><span class="wf-legend-line" style="background:#EF5350"></span>WRITE</span>';
  html += '<span><span class="wf-legend-line" style="background:#F59E0B"></span>R + W</span>';
  html += '</div>';

  // Grid container for topology
  html += '<div class="wf-grid" id="wf-grid">';

  // Top file zone — core governance files
  html += '<div class="wf-zone wf-zone--top">';
  html += '<div class="wf-zone-label pixel">Core Governance .archon</div>';
  html += '<div class="wf-zone-cards">';
  for (var ti = 0; ti < mode.topFiles.length; ti++) {
    html += renderFileCard(mode.topFiles[ti], state);
  }
  html += '</div></div>';

  // Flow label
  html += '<div class="wf-flow-label pixel">Execution Pipeline →</div>';

  // Phase row
  html += '<div class="wf-flow-row">';
  var activeSessions = (state.sessions || []).filter(function(s) { return s.phase !== 'idle'; });
  var activePhase = activeSessions.length > 0 ? activeSessions[0].phase : null;

  for (var pi = 0; pi < mode.phases.length; pi++) {
    if (pi > 0) html += '<div class="wf-flow-arrow">▸</div>';
    html += renderPhaseCard(mode.phases[pi], activePhase, mode.phases, pi);
  }
  html += '</div>';

  // Bottom file zone — grouped by category
  html += '<div class="wf-zone wf-zone--bot">';
  for (var gi = 0; gi < mode.botGroups.length; gi++) {
    var grp = mode.botGroups[gi];
    html += '<div class="wf-file-group">';
    html += '<div class="wf-group-label">' + esc(grp.label) + '</div>';
    html += '<div class="wf-group-cards">';
    for (var fi = 0; fi < grp.files.length; fi++) {
      html += renderFileCard(grp.files[fi], state);
    }
    html += '</div></div>';
  }
  html += '</div>';

  // SVG overlay
  html += '<svg class="wf-lines" id="wf-lines"></svg>';

  html += '</div>'; // end wf-grid

  return html;
}

// ═══════════════════════════════════════════════════════════════════════════
// RENDER: File card (Brutalist style)
// ═══════════════════════════════════════════════════════════════════════════

function renderFileCard(fileKey, state) {
  var f = WF_FILES[fileKey];
  if (!f) return '';

  var liveInfo = getFileLiveInfo(fileKey, state);

  var html = '<div class="wf-fcard" style="border-left-color:' + f.border + '">';
  html += '<div class="wf-fcard-hdr">';
  html += '<span class="wf-fcard-icon">' + f.icon + '</span>';
  html += '<span class="wf-fcard-name">' + esc(f.name) + '</span>';
  html += '<span class="wf-fcard-grp">' + esc(f.group) + '</span>';
  html += '</div>';

  var secs = f.sections;
  var secKeys = Object.keys(secs);
  for (var i = 0; i < secKeys.length; i++) {
    var secId = secKeys[i];
    var label = secs[secId];
    var liveVal = liveInfo[secId] || '';
    html += '<div class="wf-sec" data-secid="' + secId + '">';
    html += '<span class="wf-sec-dot" style="background:' + f.border + '"></span>';
    html += '<span class="wf-sec-text">' + esc(label) + '</span>';
    if (liveVal) html += '<span class="wf-sec-live">' + esc(liveVal) + '</span>';
    html += '</div>';
  }
  html += '</div>';
  return html;
}

function getFileLiveInfo(fileKey, state) {
  var info = {};
  if (!state) return info;
  var d = state.drift;
  var debt = state.debt;
  var m = state.manifest;

  if (fileKey === 'drift' && d) {
    info['dr-val'] = d.current + '/' + d.threshold;
    info['dr-log'] = d.logs ? d.logs.length + ' entries' : '';
  }
  if (fileKey === 'debt' && debt) {
    var pending = debt.items ? debt.items.filter(function(i) { return i.status !== 'resolved'; }).length : 0;
    info['db-table'] = pending + ' pending';
  }
  if (fileKey === 'manifest' && m) {
    if (m.milestones && m.milestones.length) {
      var cur = m.milestones.find(function(ms) { return ms.status === '🔧'; }) || m.milestones[m.milestones.length - 1];
      if (cur) info['mf-milestone'] = cur.id + ' ' + Math.round((cur.checks.done / Math.max(1, cur.checks.total)) * 100) + '%';
    }
    if (m.knowledgeAssets) {
      var ka = m.knowledgeAssets;
      info['mf-asset'] = ka.rules.length + 'R ' + ka.skills.length + 'S ' + ka.hooks.length + 'H';
    }
  }
  if (fileKey === 'memos' && m && m.memos) {
    info['mm-table'] = m.memos.length + ' entries';
  }
  return info;
}

// ═══════════════════════════════════════════════════════════════════════════
// RENDER: Phase card (Brutalist style)
// ═══════════════════════════════════════════════════════════════════════════

function renderPhaseCard(phase, activePhase, allPhases, idx) {
  var isActive = activePhase && phase.mapPhase === activePhase;
  var currentPhaseIdx = -1;
  if (activePhase) {
    for (var i = 0; i < allPhases.length; i++) {
      if (allPhases[i].mapPhase === activePhase) { currentPhaseIdx = i; break; }
    }
  }
  var isDone = activePhase && currentPhaseIdx > idx;
  var cardCls = 'wf-pcard';
  if (isActive && !isDone) cardCls += ' wf-pcard--active';
  if (isDone) cardCls += ' wf-pcard--done';

  var html = '<div class="' + cardCls + '" data-phase="' + phase.id + '">';
  html += '<div class="wf-pcard-hdr">';
  html += '<span class="wf-pcard-num" style="background:' + phase.accent + '">' + (idx + 1) + '</span>';
  html += '<span class="wf-pcard-title">' + esc(phase.label) + '</span>';
  html += '</div>';
  for (var si = 0; si < phase.steps.length; si++) {
    var step = phase.steps[si];
    html += '<div class="wf-step" data-sid="' + step.id + '">';
    html += '<span class="wf-step-icon">' + step.icon + '</span>';
    html += esc(step.label);
    html += '</div>';
  }
  html += '</div>';
  return html;
}

// ═══════════════════════════════════════════════════════════════════════════
// SVG LINE DRAWING — ported from workflow.html
// ═══════════════════════════════════════════════════════════════════════════

function drawWorkflowLines() {
  var grid = document.getElementById('wf-grid');
  var svg = document.getElementById('wf-lines');
  if (!grid || !svg) return;

  var mode = WF_MODES[_wfMode];
  if (!mode) return;

  var gr = grid.getBoundingClientRect();
  svg.setAttribute('width', grid.scrollWidth);
  svg.setAttribute('height', grid.scrollHeight);
  svg.innerHTML = '';

  // Arrow markers
  var ns = 'http://www.w3.org/2000/svg';
  var defs = document.createElementNS(ns, 'defs');
  var colors = ['#66BB6A', '#EF5350', '#F59E0B'];
  for (var ci = 0; ci < colors.length; ci++) {
    var c = colors[ci];
    var marker = document.createElementNS(ns, 'marker');
    marker.setAttribute('id', 'wf-ah-' + c.slice(1));
    marker.setAttribute('markerWidth', '6');
    marker.setAttribute('markerHeight', '5');
    marker.setAttribute('refX', '5');
    marker.setAttribute('refY', '2.5');
    marker.setAttribute('orient', 'auto');
    var pg = document.createElementNS(ns, 'polygon');
    pg.setAttribute('points', '0 0, 6 2.5, 0 5');
    pg.setAttribute('fill', c);
    marker.appendChild(pg);
    defs.appendChild(marker);
  }
  svg.appendChild(defs);

  var conns = mode.conns;
  var flowRow = grid.querySelector('.wf-flow-row');
  if (!flowRow) return;
  var flowMidY = flowRow.getBoundingClientRect().top + flowRow.getBoundingClientRect().height / 2 - gr.top;

  var fromCnt = {}, toCnt = {}, fromIdx = {}, toIdx = {};
  for (var i = 0; i < conns.length; i++) {
    fromCnt[conns[i][0]] = (fromCnt[conns[i][0]] || 0) + 1;
    toCnt[conns[i][1]] = (toCnt[conns[i][1]] || 0) + 1;
  }

  for (var j = 0; j < conns.length; j++) {
    var conn = conns[j];
    var fromEl = grid.querySelector('[data-sid="' + conn[0] + '"]');
    var toEl = grid.querySelector('[data-secid="' + conn[1] + '"]');
    if (!fromEl || !toEl) continue;

    var fr = fromEl.getBoundingClientRect();
    var tr = toEl.getBoundingClientRect();
    var isTop = (tr.top + tr.height / 2 - gr.top) < flowMidY;

    if (!fromIdx[conn[0]]) fromIdx[conn[0]] = 0;
    var fi = fromIdx[conn[0]]++;
    var fn = fromCnt[conn[0]];
    var fxOff = (fi - (fn - 1) / 2) * 5;
    var fx = fr.left + fr.width / 2 - gr.left + fxOff;
    var fy = isTop ? (fr.top - gr.top) : (fr.bottom - gr.top);

    if (!toIdx[conn[1]]) toIdx[conn[1]] = 0;
    var ti = toIdx[conn[1]]++;
    var tn = toCnt[conn[1]];
    var txOff = (ti - (tn - 1) / 2) * 5;
    var tx = tr.left + tr.width / 2 - gr.left + txOff;
    var ty = isTop ? (tr.bottom - gr.top) : (tr.top - gr.top);

    var colorMap = { r: '#66BB6A', w: '#EF5350', rw: '#F59E0B' };
    var lineColor = colorMap[conn[2]] || '#66BB6A';
    var dist = Math.abs(fy - ty);
    var cp = dist * 0.38;
    var cy1 = isTop ? fy - cp : fy + cp;
    var cy2 = isTop ? ty + cp : ty - cp;
    var d = 'M' + fx + ',' + fy + ' C' + fx + ',' + cy1 + ' ' + tx + ',' + cy2 + ' ' + tx + ',' + ty;

    var p = document.createElementNS(ns, 'path');
    p.setAttribute('d', d);
    p.setAttribute('stroke', lineColor);
    p.setAttribute('marker-end', 'url(#wf-ah-' + lineColor.slice(1) + ')');
    p.dataset.from = conn[0];
    p.dataset.to = conn[1];
    p.dataset.type = conn[2];
    p.dataset.note = conn[3] || '';
    svg.appendChild(p);
  }

  bindWorkflowLineEvents(svg);
}

function bindWorkflowLineEvents(svg) {
  var paths = svg.querySelectorAll('path');
  var tip = document.getElementById('wf-tip');
  var grid = document.getElementById('wf-grid');

  for (var i = 0; i < paths.length; i++) {
    (function(p) {
      p.addEventListener('mouseenter', function(e) {
        var lbl = { r: 'READ', w: 'WRITE', rw: 'R+W' }[p.dataset.type] || '';
        var clr = { r: '#66BB6A', w: '#EF5350', rw: '#F59E0B' }[p.dataset.type] || '#666';
        if (tip) {
          tip.innerHTML = '<span style="color:' + clr + ';font-weight:700">' + lbl + '</span> → ' + esc(p.dataset.note);
          tip.classList.add('wf-tip--show');
        }
        for (var j = 0; j < paths.length; j++) paths[j].classList.add('wf-line--dim');
        p.classList.remove('wf-line--dim');
        p.classList.add('wf-line--hl');
        hlWorkflowNode(grid, p.dataset.from, true);
        hlWorkflowNode(grid, p.dataset.to, true);
      });
      p.addEventListener('mousemove', function(e) {
        if (tip) { tip.style.left = e.clientX + 14 + 'px'; tip.style.top = e.clientY - 28 + 'px'; }
      });
      p.addEventListener('mouseleave', function() {
        if (tip) tip.classList.remove('wf-tip--show');
        for (var j = 0; j < paths.length; j++) {
          paths[j].classList.remove('wf-line--dim', 'wf-line--hl');
        }
        var hls = grid.querySelectorAll('.wf-hl');
        for (var k = 0; k < hls.length; k++) hls[k].classList.remove('wf-hl');
      });
    })(paths[i]);
  }
}

function hlWorkflowNode(grid, id, on) {
  var el = grid.querySelector('[data-sid="' + id + '"]') || grid.querySelector('[data-secid="' + id + '"]');
  if (el) { if (on) el.classList.add('wf-hl'); else el.classList.remove('wf-hl'); }
}

// Click-to-highlight
function onWorkflowClick(e) {
  var step = e.target.closest('.wf-step');
  var sec = e.target.closest('.wf-sec');
  var id = step ? step.dataset.sid : (sec ? sec.dataset.secid : null);
  if (!id) return;

  var svg = document.getElementById('wf-lines');
  if (!svg) return;
  var paths = svg.querySelectorAll('path');
  var any = false;
  for (var i = 0; i < paths.length; i++) {
    var match = paths[i].dataset.from === id || paths[i].dataset.to === id;
    if (match) any = true;
    paths[i].classList.toggle('wf-line--hl', match);
    paths[i].classList.toggle('wf-line--dim', !match);
  }
  if (!any) {
    for (var j = 0; j < paths.length; j++) {
      paths[j].classList.remove('wf-line--dim', 'wf-line--hl');
    }
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// FLOATING AGENT PANEL
// ═══════════════════════════════════════════════════════════════════════════

function renderAgentFloat(state) {
  var el = document.getElementById('agent-float');
  if (!el) return;

  var sessions = (state.sessions || []).filter(function(s) { return s.phase !== 'idle'; });

  if (sessions.length === 0) {
    el.innerHTML = '<div class="af-collapsed af-collapsed--idle" onclick="toggleAgentFloat()">' +
      '<span class="af-dot af-dot--idle"></span>' +
      '<span class="af-label pixel">IDLE</span></div>';
    el.classList.remove('agent-float--open');
    return;
  }

  if (!_agentFloatOpen) {
    el.innerHTML = '<div class="af-collapsed" onclick="toggleAgentFloat()">' +
      '<span class="af-dot af-dot--active"></span>' +
      '<span class="af-label pixel">' + sessions.length + ' ACTIVE</span></div>';
    el.classList.remove('agent-float--open');
    return;
  }

  el.classList.add('agent-float--open');
  var html = '<div class="af-header">';
  html += '<span class="af-label pixel">' + sessions.length + ' ACTIVE</span>';
  html += '<button class="af-close" onclick="toggleAgentFloat()">✕</button>';
  html += '</div>';

  for (var i = 0; i < sessions.length; i++) {
    var s = sessions[i];
    var phaseLabel = (PHASE_LABELS && PHASE_LABELS[s.phase]) || s.phase.toUpperCase();
    html += '<div class="af-session">';
    html += '<div class="af-session-top">';
    html += '<span class="af-dot af-dot--active"></span>';
    html += '<span class="mono" style="font-size:10px">' + esc((s.sessionId || '').slice(0, 8)) + '</span>';
    html += '<span class="capsule capsule--sm capsule--green">' + esc(phaseLabel) + '</span>';
    html += '</div>';
    if (s.demand) html += '<div class="af-demand">' + esc(s.demand.slice(0, 60)) + '</div>';
    if (s._eventCount) html += '<div class="af-meta">' + s._eventCount + ' events · ' + esc(s._lastTool || '') + '</div>';
    var traceLink = s._transcriptId || s.sessionId || s._file || '';
    if (traceLink) html += '<a href="#/trace/' + esc(traceLink) + '" class="af-trace-link">View Trace →</a>';
    html += '</div>';
  }
  el.innerHTML = html;
}

function toggleAgentFloat() {
  _agentFloatOpen = !_agentFloatOpen;
  if (typeof S !== 'undefined') renderAgentFloat(S);
}

// ═══════════════════════════════════════════════════════════════════════════
// LIVE HIGHLIGHT: auto-highlight active agent's phase connections
// ═══════════════════════════════════════════════════════════════════════════

function highlightActivePhaseLines(state) {
  var svg = document.getElementById('wf-lines');
  if (!svg) return;

  var sessions = (state.sessions || []).filter(function(s) { return s.phase !== 'idle'; });
  if (sessions.length === 0) return;

  var activePhase = sessions[0].phase;
  var mode = WF_MODES[_wfMode];
  if (!mode) return;

  var activeStepIds = {};
  for (var pi = 0; pi < mode.phases.length; pi++) {
    if (mode.phases[pi].mapPhase === activePhase) {
      for (var si = 0; si < mode.phases[pi].steps.length; si++) {
        activeStepIds[mode.phases[pi].steps[si].id] = true;
      }
    }
  }

  var paths = svg.querySelectorAll('path');
  for (var i = 0; i < paths.length; i++) {
    if (activeStepIds[paths[i].dataset.from]) {
      paths[i].classList.add('wf-line--live');
    }
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// POST-RENDER HOOK: wire up events after DOM insertion
// ═══════════════════════════════════════════════════════════════════════════

function initWorkflowEvents() {
  // Tab switching
  var tabs = document.querySelectorAll('.wf-tab');
  for (var i = 0; i < tabs.length; i++) {
    tabs[i].addEventListener('click', function(e) {
      var newMode = this.getAttribute('data-wfmode');
      if (newMode && newMode !== _wfMode) {
        _wfMode = newMode;
        if (typeof render === 'function') render();
      }
    });
  }

  // Click-to-highlight on steps/sections
  var grid = document.getElementById('wf-grid');
  if (grid) grid.addEventListener('click', onWorkflowClick);

  // Draw lines after layout settles
  requestAnimationFrame(function() {
    drawWorkflowLines();
    if (typeof S !== 'undefined') highlightActivePhaseLines(S);
    setTimeout(drawWorkflowLines, 200);
  });
}

Released under the Apache-2.0 License.