AutoMLOps / templates /pipeline.html
mnoorchenar's picture
Update 2026-03-26 18:00:31
fb9037e
Raw
History Blame Contribute Delete
32.7 kB
{% extends "base.html" %}
{% set active_page = "pipeline" %}
{% block title %}AutoMLOps{% endblock %}
{% block page_title %}<i class="fa-solid fa-diagram-project" style="color:var(--cyan)"></i> Pipeline Studio{% endblock %}
{% block head_extra %}
<style>
/* ── Override page padding so studio fills the viewport ─────────────────── */
.page-content { padding: 0 !important; overflow: hidden; height: calc(100vh - var(--navbar-h)); }
.main { overflow: hidden; }
/* ── Studio shell ─────────────────────────────────────────────────────────── */
.ps { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
/* ── Toolbar ─────────────────────────────────────────────────────────────── */
.ps-toolbar {
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
padding: 0 16px; min-height: 52px; flex-shrink: 0;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.ps-tabs { display: flex; gap: 3px; }
.ps-tab {
display: flex; align-items: center; gap: 6px;
padding: 5px 13px; border-radius: 6px;
font-size: .8rem; font-weight: 500;
background: transparent; border: 1px solid transparent;
color: var(--text-secondary); cursor: pointer;
transition: background .13s, color .13s, border-color .13s;
}
.ps-tab:hover { background: var(--bg-tertiary); color: var(--text-primary); }
.ps-tab.active { background: rgba(139,92,246,.12); border-color: rgba(139,92,246,.3); color: var(--accent-light); }
.ps-info { flex: 1; min-width: 0; padding: 0 12px; }
.ps-name { font-size: .88rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ps-desc { font-size: .74rem; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
.ps-badge {
display: inline-flex; align-items: center; gap: 5px; flex-shrink: 0;
padding: 3px 10px; border-radius: 20px; font-size: .72rem; font-weight: 600;
}
.ps-badge.idle { background: var(--bg-tertiary); color: var(--text-muted); }
.ps-badge.running { background: rgba(245,158,11,.12); color: var(--warning); }
.ps-badge.success { background: rgba(34,197,94,.12); color: var(--success); }
.ps-badge.failed { background: rgba(239,68,68,.12); color: var(--danger); }
.ps-run-btn {
display: flex; align-items: center; gap: 7px; flex-shrink: 0;
padding: 6px 16px; border: none; border-radius: 6px;
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-blue) 100%);
color: #fff; font-size: .83rem; font-weight: 600;
cursor: pointer; transition: opacity .15s, transform .1s;
}
.ps-run-btn:hover:not(:disabled) { opacity: .88; }
.ps-run-btn:active:not(:disabled) { transform: scale(.96); }
.ps-run-btn:disabled { opacity: .45; cursor: default; }
/* ── Main area (canvas + config panel) ───────────────────────────────────── */
.ps-main { flex: 1; display: flex; overflow: hidden; }
/* DAG canvas */
.ps-canvas {
flex: 1; overflow: auto; position: relative;
background-color: var(--bg-primary);
background-image: radial-gradient(circle, var(--border-color) 1px, transparent 1px);
background-size: 28px 28px;
}
.ps-canvas-inner { position: relative; }
.dag-arrows { position: absolute; top: 0; left: 0; pointer-events: none; z-index: 5; overflow: visible; }
/* ── DAG nodes ────────────────────────────────────────────────────────────── */
.dag-node {
position: absolute;
background: var(--bg-secondary);
border: 1.5px solid var(--border-color);
border-radius: 10px;
padding: 10px 11px 8px;
cursor: pointer;
transition: border-color .18s, box-shadow .18s, transform .12s;
user-select: none; z-index: 10;
}
.dag-node:hover {
border-color: var(--accent-light);
box-shadow: 0 0 0 3px rgba(139,92,246,.15), 0 4px 14px rgba(0,0,0,.3);
transform: translateY(-1px);
}
.dag-node.selected { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(139,92,246,.25); }
.dag-node.s-running { border-color: var(--warning); animation: node-pulse 1.6s ease-in-out infinite; }
.dag-node.s-success { border-color: var(--success); box-shadow: 0 0 0 2px rgba(34,197,94,.2); }
.dag-node.s-failed { border-color: var(--danger); box-shadow: 0 0 0 2px rgba(239,68,68,.2); }
@keyframes node-pulse {
0%, 100% { box-shadow: 0 0 0 2px rgba(245,158,11,.2); }
50% { box-shadow: 0 0 0 8px rgba(245,158,11,.04); }
}
/* Purple dot = configurable */
.dag-node.configurable::after {
content: ''; position: absolute; top: 7px; right: 7px;
width: 5px; height: 5px; border-radius: 50%; background: var(--accent);
}
.nd-icon { font-size: 1.1rem; display: block; line-height: 1; margin-bottom: 3px; }
.nd-name { font-size: .77rem; font-weight: 600; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.nd-badge {
display: inline-flex; align-items: center; gap: 4px;
margin-top: 5px; padding: 2px 6px; border-radius: 8px;
font-size: .66rem; font-weight: 500;
}
.nd-badge.pending { background: rgba(101,109,118,.15); color: var(--text-muted); }
.nd-badge.running { background: rgba(245,158,11,.14); color: var(--warning); }
.nd-badge.success { background: rgba(34,197,94,.12); color: var(--success); }
.nd-badge.failed { background: rgba(239,68,68,.12); color: var(--danger); }
.nd-result { font-size: .65rem; color: var(--text-muted); margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sdot { width: 5px; height: 5px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
.sdot.pending { background: var(--text-muted); }
.sdot.running { background: var(--warning); animation: blink 1s step-start infinite; }
.sdot.success { background: var(--success); }
.sdot.failed { background: var(--danger); }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
/* ── Config panel ─────────────────────────────────────────────────────────── */
.ps-cfg {
width: 0; overflow: hidden; flex-shrink: 0;
background: var(--bg-secondary);
border-left: 1px solid var(--border-color);
display: flex; flex-direction: column;
transition: width .24s cubic-bezier(.4,0,.2,1);
}
.ps-cfg.open { width: 296px; }
.cfg-hdr {
display: flex; align-items: center; justify-content: space-between;
padding: 11px 13px; flex-shrink: 0;
border-bottom: 1px solid var(--border-color);
font-size: .8rem; font-weight: 600; color: var(--text-secondary);
}
.cfg-body { flex: 1; overflow-y: auto; padding: 14px; }
.cfg-node-hdr {
display: flex; align-items: flex-start; gap: 10px;
padding-bottom: 13px; margin-bottom: 13px;
border-bottom: 1px solid var(--border-color);
}
.cfg-node-icon { font-size: 1.5rem; flex-shrink: 0; line-height: 1; }
.cfg-node-title { font-size: .9rem; font-weight: 600; line-height: 1.3; }
.cfg-node-desc { font-size: .76rem; color: var(--text-secondary); margin-top: 4px; line-height: 1.5; }
.cfg-sec { margin-bottom: 16px; }
.cfg-lbl {
display: block; margin-bottom: 6px;
font-size: .69rem; font-weight: 700; letter-spacing: .08em;
text-transform: uppercase; color: var(--text-muted);
}
.cfg-select {
width: 100%; background: var(--bg-tertiary); border: 1px solid var(--border-color);
color: var(--text-primary); border-radius: 6px; padding: 7px 28px 7px 10px;
font-size: .82rem; outline: none; cursor: pointer; appearance: none;
transition: border-color .15s;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23656d76' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-position: right 9px center;
}
.cfg-select:focus { border-color: var(--accent); }
.cfg-row {
display: flex; justify-content: space-between; align-items: flex-start;
padding: 6px 0; border-bottom: 1px solid var(--border-color); font-size: .79rem;
}
.cfg-row:last-child { border-bottom: none; }
.cfg-row-k { color: var(--text-muted); white-space: nowrap; padding-right: 8px; }
.cfg-row-v { color: var(--text-primary); font-weight: 500; text-align: right; word-break: break-word; max-width: 62%; font-size: .77rem; }
/* ── HPO toggle ──────────────────────────────────────────────────────────── */
.hpo-toggle { position:relative; display:inline-block; width:34px; height:19px; flex-shrink:0; }
.hpo-toggle input { opacity:0; width:0; height:0; }
.hpo-knob {
position:absolute; cursor:pointer; inset:0;
background:var(--bg-tertiary); border:1px solid var(--border-color);
border-radius:20px; transition:.18s;
}
.hpo-knob::before {
content:''; position:absolute;
width:13px; height:13px; left:2px; top:2px;
background:var(--text-muted); border-radius:50%; transition:.18s;
}
.hpo-toggle input:checked + .hpo-knob { background:rgba(139,92,246,.2); border-color:var(--accent); }
.hpo-toggle input:checked + .hpo-knob::before { transform:translateX(15px); background:var(--accent); }
.cfg-num {
width:64px; background:var(--bg-tertiary); border:1px solid var(--border-color);
color:var(--text-primary); border-radius:6px; padding:3px 7px;
font-size:.8rem; text-align:right; outline:none;
}
.cfg-num:focus { border-color:var(--accent); }
/* ── Terminal ─────────────────────────────────────────────────────────────── */
.ps-term {
flex-shrink: 0; height: 34px; overflow: hidden;
background: #07090d;
border-top: 1px solid var(--border-color);
transition: height .22s cubic-bezier(.4,0,.2,1);
}
.ps-term.expanded { height: 220px; }
.term-hdr {
display: flex; align-items: center; gap: 5px;
height: 34px; padding: 0 14px;
cursor: pointer; user-select: none;
border-bottom: 1px solid var(--border-color);
font-family: 'Fira Code', monospace; font-size: .72rem;
color: var(--text-secondary);
}
.term-hdr:hover { background: rgba(255,255,255,.02); }
.term-body {
height: calc(100% - 34px); overflow-y: auto;
padding: 7px 14px;
font-family: 'Fira Code', monospace; font-size: .71rem;
color: #8b949e; line-height: 1.65;
}
.l-ok { color: #22c55e; }
.l-err { color: #ef4444; }
.l-info { color: #f59e0b; }
.l-dim { color: #5a6475; }
</style>
{% endblock %}
{% block content %}
<div class="ps">
<!-- ── Toolbar ─────────────────────────────────────────────────────────── -->
<div class="ps-toolbar">
<div class="ps-tabs">
<button class="ps-tab active" onclick="switchPipeline('training_pipeline',this)">
<i class="fa-solid fa-brain"></i> Training
</button>
<button class="ps-tab" onclick="switchPipeline('retraining_pipeline',this)">
<i class="fa-solid fa-rotate"></i> Retraining
</button>
<button class="ps-tab" onclick="switchPipeline('data_pipeline',this)">
<i class="fa-solid fa-database"></i> Data
</button>
</div>
<div class="ps-info">
<div class="ps-name" id="ps-name">β€”</div>
<div class="ps-desc" id="ps-desc">β€”</div>
</div>
<div class="ps-badge idle" id="ps-badge">
<span class="sdot pending" id="ps-dot"></span>
<span id="ps-badge-txt">Idle</span>
</div>
<button class="ps-run-btn" id="ps-run-btn" onclick="runPipeline()">
<i class="fa-solid fa-play" id="ps-btn-icon"></i>
<span id="ps-btn-txt">Execute DAG</span>
</button>
</div>
<!-- ── Main area ──────────────────────────────────────────────────────── -->
<div class="ps-main">
<!-- DAG Canvas -->
<div class="ps-canvas" id="ps-canvas">
<div class="ps-canvas-inner" id="ps-ci">
<svg class="dag-arrows" id="dag-svg" width="1" height="1"></svg>
<!-- nodes injected by JS -->
</div>
</div>
<!-- Config panel (slides in) -->
<div class="ps-cfg" id="ps-cfg">
<div class="cfg-hdr">
<span><i class="fa-solid fa-sliders" style="color:var(--accent);margin-right:6px"></i>Node Config</span>
<button class="btn btn-ghost btn-sm" onclick="closeConfig()" title="Close">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="cfg-body" id="cfg-body">
<div style="text-align:center;padding:36px 0 28px;color:var(--text-muted);font-size:.82rem">
<i class="fa-solid fa-arrow-pointer" style="font-size:1.5rem;opacity:.3;display:block;margin-bottom:10px"></i>
Click any node to configure it
</div>
</div>
</div>
</div>
<!-- ── Execution terminal ─────────────────────────────────────────────── -->
<div class="ps-term" id="ps-term">
<div class="term-hdr" onclick="toggleTerm()">
<span style="color:#ff5f56;font-size:.5rem">●</span>
<span style="color:#ffbd2e;font-size:.5rem">●</span>
<span style="color:#27c93f;font-size:.5rem">●</span>
<span style="margin-left:8px;color:var(--warning);letter-spacing:.05em">EXECUTION LOG</span>
<span id="term-pct" style="margin-left:8px;color:var(--text-muted)"></span>
<span style="margin-left:auto" id="term-caret"><i class="fa-solid fa-chevron-up"></i></span>
</div>
<div class="term-body" id="term-body">
<div class="l-dim"># Waiting for pipeline execution…</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// ── Data ──────────────────────────────────────────────────────────────────────
const DAGS = {{ dags | safe }};
const DATASETS = {{ datasets | tojson }};
// ── State ─────────────────────────────────────────────────────────────────────
let cur = 'training_pipeline';
let execId = null;
let pollIv = null;
let selNode = null;
let tstates = {};
let _seenLogs = 0;
// Pipeline context β€” updated via config panel; used when running
let pCtx = {
dataset: Object.keys(DATASETS)[0] || 'Iris Flowers',
category: 'Tree-Based',
algorithm: 'Random Forest',
task_type: 'classification',
hpo_enabled: false,
hpo_trials: 20,
};
// Layout
const NW=130, NH=64, HGAP=68, VGAP=24, PX=44, PY=44;
// Which nodes show config controls (purple dot)
const CFG_TYPE = { load_data:'dataset', ingest:'dataset', train:'algorithm', retrain:'algorithm' };
// Node positions (populated in renderDAG)
let npos = {};
// ── Init ──────────────────────────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () =>
switchPipeline('training_pipeline', document.querySelector('.ps-tab.active')));
// ── Pipeline switch ───────────────────────────────────────────────────────────
function switchPipeline(id, btn) {
if (pollIv) { clearInterval(pollIv); pollIv = null; }
cur = id; execId = null; tstates = {}; selNode = null;
closeConfig(false);
document.querySelectorAll('.ps-tab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const d = DAGS[id];
document.getElementById('ps-name').textContent = d.name;
document.getElementById('ps-desc').textContent = d.description;
renderDAG(d, {});
_setBadge('idle');
_resetTerm();
_resetBtn();
}
// ── DAG rendering ─────────────────────────────────────────────────────────────
function renderDAG(dag, states) {
states = states || {};
const tasks = Object.values(dag.tasks);
const layers = {};
tasks.forEach(t => (layers[t.layer] = layers[t.layer] || []).push(t));
const maxL = Math.max(...tasks.map(t => t.layer));
const maxN = Math.max(...Object.values(layers).map(ts => ts.length));
// Compute positions (center-align multi-node layers)
npos = {};
Object.entries(layers).forEach(([li, ts]) => {
const x = PX + +li * (NW + HGAP);
const totalH = ts.length * NH + Math.max(0, ts.length-1) * VGAP;
const baseY = PY + (maxN * (NH+VGAP) - totalH) / 2;
ts.forEach((t, i) => { npos[t.task_id] = { x, y: baseY + i*(NH+VGAP) }; });
});
const cw = PX*2 + (maxL+1)*NW + maxL*HGAP;
const ch = PY*2 + maxN*NH + Math.max(0,maxN-1)*VGAP;
const ci = document.getElementById('ps-ci');
ci.style.width = Math.max(cw,400)+'px';
ci.style.height = Math.max(ch,320)+'px';
// Remove old nodes
ci.querySelectorAll('.dag-node').forEach(e => e.remove());
// Create nodes
tasks.forEach(t => {
const p = npos[t.task_id];
const st = (states[t.task_id]||{}).status || 'pending';
const res = (states[t.task_id]||{}).result || '';
const sel = t.task_id === selNode;
const cfg = !!CFG_TYPE[t.task_id];
const el = document.createElement('div');
el.className = `dag-node s-${st}${sel?' selected':''}${cfg?' configurable':''}`;
el.id = 'node-'+t.task_id;
el.style.cssText = `left:${p.x}px;top:${p.y}px;width:${NW}px`;
el.innerHTML = `
<span class="nd-icon">${t.icon}</span>
<div class="nd-name">${t.name}</div>
<div class="nd-badge ${st}">
<span class="sdot ${st}"></span>${st}
</div>
${res?`<div class="nd-result" title="${res}">${res}</div>`:''}
`;
el.addEventListener('click', () => openConfig(t.task_id));
ci.appendChild(el);
});
// Arrows
_drawArrows(dag, states, Math.max(cw,400), Math.max(ch,320));
}
function _drawArrows(dag, states, w, h) {
const svg = document.getElementById('dag-svg');
svg.setAttribute('width', w);
svg.setAttribute('height', h);
svg.innerHTML = `
<defs>
<marker id="ah" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0,8 3,0 6" fill="var(--border-color)"/>
</marker>
<marker id="ah-ok" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0,8 3,0 6" fill="#22c55e66"/>
</marker>
</defs>`;
Object.values(dag.tasks).forEach(t => {
t.upstream.forEach(uid => {
const a = npos[uid], b = npos[t.task_id];
if (!a||!b) return;
const x1=a.x+NW, y1=a.y+NH/2, x2=b.x, y2=b.y+NH/2, cx=(x1+x2)/2;
const uSt=(states[uid]||{}).status||'pending';
const tSt=(states[t.task_id]||{}).status||'pending';
const ok = uSt==='success'&&tSt==='success';
const p = document.createElementNS('http://www.w3.org/2000/svg','path');
p.setAttribute('d', `M${x1},${y1} C${cx},${y1} ${cx},${y2} ${x2},${y2}`);
p.setAttribute('stroke', ok?'#22c55e55':'var(--border-color)');
p.setAttribute('stroke-width','1.5');
p.setAttribute('fill','none');
p.setAttribute('marker-end',`url(#${ok?'ah-ok':'ah'})`);
svg.appendChild(p);
});
});
}
// ── Config panel ──────────────────────────────────────────────────────────────
async function openConfig(taskId) {
selNode = taskId;
const dag = DAGS[cur];
const task = dag.tasks[taskId];
const st = (tstates[taskId]||{}).status || 'pending';
const res = (tstates[taskId]||{}).result || null;
const err = (tstates[taskId]||{}).error || null;
const cfgT = CFG_TYPE[taskId];
const stClr= {running:'var(--warning)',success:'var(--success)',failed:'var(--danger)'}[st]||'var(--text-muted)';
// Highlight node
document.querySelectorAll('.dag-node').forEach(e => e.classList.remove('selected'));
const ne = document.getElementById('node-'+taskId);
if (ne) ne.classList.add('selected');
let html = `
<div class="cfg-node-hdr">
<span class="cfg-node-icon">${task.icon}</span>
<div>
<div class="cfg-node-title">${task.name}</div>
<div class="cfg-node-desc">${task.description}</div>
</div>
</div>
<!-- Status (always visible, updated in-place) -->
<div class="cfg-sec">
<span class="cfg-lbl">Status</span>
<div class="cfg-row">
<span class="cfg-row-k">State</span>
<span class="cfg-row-v" id="cfg-st" style="color:${stClr}">${st.toUpperCase()}</span>
</div>
<div class="cfg-row" id="cfg-res-row" style="${res?'':'display:none'}">
<span class="cfg-row-k">Result</span>
<span class="cfg-row-v" id="cfg-res">${res||''}</span>
</div>
<div class="cfg-row" id="cfg-err-row" style="${err?'':'display:none'}">
<span class="cfg-row-k">Error</span>
<span class="cfg-row-v" id="cfg-err" style="color:var(--danger)">${err||''}</span>
</div>
</div>
`;
// Inputs for configurable nodes
if (cfgT === 'dataset') {
html += `
<div class="cfg-sec">
<label class="cfg-lbl" for="cfg-ds">Dataset</label>
<select class="cfg-select" id="cfg-ds" onchange="onDatasetChange(this.value)">
${Object.keys(DATASETS).map(n=>`<option${n===pCtx.dataset?' selected':''}>${n}</option>`).join('')}
</select>
</div>`;
} else if (cfgT === 'algorithm') {
html += `
<div class="cfg-sec">
<label class="cfg-lbl" for="cfg-tt">Task Type</label>
<select class="cfg-select" id="cfg-tt" onchange="onTtChange(this.value)">
<option value="classification"${pCtx.task_type==='classification'?' selected':''}>Classification</option>
<option value="regression"${pCtx.task_type==='regression'?' selected':''}>Regression</option>
</select>
</div>
<div class="cfg-sec">
<label class="cfg-lbl" for="cfg-cat">Category</label>
<select class="cfg-select" id="cfg-cat"><option>Loading…</option></select>
</div>
<div class="cfg-sec">
<label class="cfg-lbl" for="cfg-alg">Algorithm</label>
<select class="cfg-select" id="cfg-alg" onchange="pCtx.algorithm=this.value"><option>Loading…</option></select>
</div>
<div class="cfg-sec">
<span class="cfg-lbl">Hyperparameter Search</span>
<div class="cfg-row">
<span class="cfg-row-k">Enable HPO</span>
<label class="hpo-toggle">
<input type="checkbox" id="cfg-hpo" onchange="onHpoToggle(this.checked)"${pCtx.hpo_enabled?' checked':''}>
<span class="hpo-knob"></span>
</label>
</div>
<div id="cfg-hpo-opts"${pCtx.hpo_enabled?'':' style="display:none"'}>
<div class="cfg-row">
<span class="cfg-row-k">Search trials</span>
<input type="number" class="cfg-num" id="cfg-trials" min="5" max="100" value="${pCtx.hpo_trials}"
onchange="pCtx.hpo_trials=Math.max(5,Math.min(100,+this.value))">
</div>
<div class="cfg-row">
<span class="cfg-row-k">CV folds</span>
<span class="cfg-row-v" style="color:var(--text-muted)">3</span>
</div>
</div>
</div>`;
}
// Upstream deps
if (task.upstream && task.upstream.length) {
html += `<div class="cfg-sec"><span class="cfg-lbl">Upstream</span>`;
task.upstream.forEach(uid => {
const up = dag.tasks[uid];
const upSt = (tstates[uid]||{}).status||'pending';
html += `<div class="cfg-row">
<span class="cfg-row-k">${up?up.icon+' '+up.name:uid}</span>
<span class="nd-badge ${upSt}" style="font-size:.65rem;padding:1px 6px">
<span class="sdot ${upSt}"></span>${upSt}
</span>
</div>`;
});
html += `</div>`;
}
document.getElementById('cfg-body').innerHTML = html;
document.getElementById('ps-cfg').classList.add('open');
if (cfgT === 'algorithm') await _loadAlgos(pCtx.task_type);
}
function closeConfig(redraw=true) {
document.getElementById('ps-cfg').classList.remove('open');
if (redraw) document.querySelectorAll('.dag-node').forEach(e => e.classList.remove('selected'));
selNode = null;
}
// Update only the status bits in the open config panel (called during polling)
function _updateCfgStatus() {
if (!selNode) return;
const st = (tstates[selNode]||{}).status || 'pending';
const res = (tstates[selNode]||{}).result || null;
const err = (tstates[selNode]||{}).error || null;
const stClr={running:'var(--warning)',success:'var(--success)',failed:'var(--danger)'}[st]||'var(--text-muted)';
const stEl = document.getElementById('cfg-st');
if (stEl) { stEl.textContent = st.toUpperCase(); stEl.style.color = stClr; }
const rRow = document.getElementById('cfg-res-row');
const rEl = document.getElementById('cfg-res');
if (rRow && rEl && res) { rRow.style.display=''; rEl.textContent=res; }
const eRow = document.getElementById('cfg-err-row');
const eEl = document.getElementById('cfg-err');
if (eRow && eEl && err) { eRow.style.display=''; eEl.textContent=err; }
}
// ── Algorithm dropdowns ───────────────────────────────────────────────────────
let _algoData = null;
async function _loadAlgos(tt) {
try {
const r = await fetch(`/api/algorithms?task=${tt}`);
_algoData = await r.json();
const cats = Object.keys(_algoData);
const catSel = document.getElementById('cfg-cat');
if (!catSel) return;
catSel.innerHTML = cats.map(c=>`<option${c===pCtx.category?' selected':''}>${c}</option>`).join('');
const cat = cats.includes(pCtx.category)?pCtx.category:cats[0];
catSel.value = cat; pCtx.category = cat;
catSel.onchange = e => onCatChange(e.target.value);
_fillAlgos(cat);
} catch(e) {}
}
function _fillAlgos(cat) {
const algs = _algoData&&_algoData[cat]?Object.keys(_algoData[cat]):[];
const sel = document.getElementById('cfg-alg');
if (!sel) return;
sel.innerHTML = algs.map(a=>`<option${a===pCtx.algorithm?' selected':''}>${a}</option>`).join('');
const alg = algs.includes(pCtx.algorithm)?pCtx.algorithm:algs[0]||'';
sel.value = alg; pCtx.algorithm = alg;
sel.onchange = e => { pCtx.algorithm = e.target.value; };
}
function onDatasetChange(name) {
pCtx.dataset = name;
const dsTask = (DATASETS[name] || {}).task || 'classification';
if (dsTask !== pCtx.task_type) {
pCtx.task_type = dsTask;
pCtx.category = '';
pCtx.algorithm = '';
}
}
function onHpoToggle(enabled) {
pCtx.hpo_enabled = enabled;
const el = document.getElementById('cfg-hpo-opts');
if (el) el.style.display = enabled ? '' : 'none';
}
async function onTtChange(tt) {
pCtx.task_type = tt; pCtx.category=''; pCtx.algorithm='';
await _loadAlgos(tt);
}
function onCatChange(cat) { pCtx.category=cat; _fillAlgos(cat); }
// ── Execute pipeline ──────────────────────────────────────────────────────────
async function runPipeline() {
const runBtn = document.getElementById('ps-run-btn');
runBtn.disabled = true;
document.getElementById('ps-btn-icon').className = 'spinner';
document.getElementById('ps-btn-txt').textContent = 'Running…';
_setBadge('running');
_openTerm();
const ctx = {};
if (cur === 'training_pipeline') {
Object.assign(ctx, {
dataset: pCtx.dataset,
category: pCtx.category,
algorithm: pCtx.algorithm,
task_type: pCtx.task_type,
hpo_enabled: pCtx.hpo_enabled || false,
hpo_trials: pCtx.hpo_trials || 20,
});
}
try {
const r = await fetch(`/api/pipeline/${cur}/execute`, {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify(ctx),
});
const d = await r.json();
if (d.error) { _execFailed(d.error); return; }
execId = d.exec_id;
_poll();
} catch(e) { _execFailed(e.message); }
}
function _poll() {
_seenLogs = 0;
if (pollIv) clearInterval(pollIv);
pollIv = setInterval(async () => {
try {
const r = await fetch(`/api/pipeline/status/${execId}`);
const exec = await r.json();
tstates = exec.task_states || {};
// Sync new log lines
const lines = exec.logs||[];
if (lines.length > _seenLogs) {
lines.slice(_seenLogs).forEach(ln => {
if (!ln) return;
let c='dim';
if (ln.includes('βœ”')) c='ok';
else if (ln.includes('βœ–')||ln.toLowerCase().includes('fail')) c='err';
else if (ln.includes('β–Ά')) c='info';
_addLog(ln, c);
});
_seenLogs = lines.length;
}
document.getElementById('term-pct').textContent = exec.progress!=null?exec.progress+'%':'';
renderDAG(DAGS[cur], tstates);
_updateCfgStatus();
if (exec.status==='completed') {
clearInterval(pollIv); pollIv=null;
_setBadge('success');
_addLog('βœ” Pipeline completed successfully', 'ok');
document.getElementById('ps-btn-icon').className = 'fa-solid fa-rotate-right';
document.getElementById('ps-btn-txt').textContent = 'Run Again';
document.getElementById('ps-run-btn').disabled = false;
showToast(`${DAGS[cur].name} completed`,'success');
} else if (exec.status==='failed') {
clearInterval(pollIv); pollIv=null;
_setBadge('failed');
_addLog('βœ– '+(exec.error||'Pipeline failed'),'err');
_resetBtn();
showToast('Pipeline failed','error');
}
} catch(e) {}
}, 900);
}
function _execFailed(msg) { _addLog('βœ– '+msg,'err'); _setBadge('failed'); _resetBtn(); }
// ── Helpers ───────────────────────────────────────────────────────────────────
function _setBadge(s) {
const labels={idle:'Idle',running:'Running',success:'Completed',failed:'Failed'};
document.getElementById('ps-badge').className = 'ps-badge '+s;
document.getElementById('ps-dot').className = 'sdot '+(s==='idle'?'pending':s);
document.getElementById('ps-badge-txt').textContent = labels[s]||s;
}
function _resetBtn() {
btn().disabled = false;
document.getElementById('ps-btn-icon').className = 'fa-solid fa-play';
document.getElementById('ps-btn-txt').textContent = 'Execute DAG';
}
function _addLog(txt, cls) {
const b = document.getElementById('term-body');
const el = document.createElement('div');
el.className = 'l-'+(cls||'dim'); el.textContent = txt;
b.appendChild(el); b.scrollTop = b.scrollHeight;
}
function _openTerm() {
document.getElementById('term-body').innerHTML = '';
document.getElementById('ps-term').classList.add('expanded');
document.getElementById('term-caret').innerHTML = '<i class="fa-solid fa-chevron-down"></i>';
}
function toggleTerm() {
const el = document.getElementById('ps-term');
const ex = el.classList.contains('expanded');
el.classList.toggle('expanded',!ex);
document.getElementById('term-caret').innerHTML =
`<i class="fa-solid fa-chevron-${ex?'up':'down'}"></i>`;
}
function _resetTerm() {
document.getElementById('term-body').innerHTML = '<div class="l-dim"># Waiting for pipeline execution…</div>';
document.getElementById('term-pct').textContent='';
document.getElementById('ps-term').classList.remove('expanded');
document.getElementById('term-caret').innerHTML='<i class="fa-solid fa-chevron-up"></i>';
}
</script>
{% endblock %}