Spaces:
Sleeping
Sleeping
| {% 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 ; 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 %} | |