| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767 |
- let requirements = [];
- let currentSelectedIndex = null;
- let activeRuns = {};
- let statusInterval = null;
- let currentPromptName = null;
- const modalPrompts = document.getElementById('prompts-modal');
- const elPromptList = document.getElementById('prompt-list');
- const elPromptTextarea = document.getElementById('prompt-textarea');
- const elPromptStatus = document.getElementById('prompt-save-status');
- // DOM Elements
- const elTaskList = document.getElementById('task-list');
- const elSearchInput = document.getElementById('search-input');
- const elStatsContainer = document.getElementById('stats-container');
- const elMainContent = document.getElementById('main-content');
- const elEmptyState = document.getElementById('empty-state');
- const elDetailView = document.getElementById('detail-view');
- const elDetailId = document.getElementById('detail-id');
- const elDetailTitle = document.getElementById('detail-title');
- const elStatusBanner = document.getElementById('status-banner');
- const elStatusText = document.getElementById('status-text');
- // Form logic
- const selectForcePhase = document.getElementById('select-force-phase');
- const groupPlatforms = document.getElementById('group-platforms');
- if (selectForcePhase && groupPlatforms) {
- selectForcePhase.addEventListener('change', (e) => {
- const val = e.target.value;
- if (val.startsWith('phase2') || val === 'phase3') {
- groupPlatforms.style.display = 'none';
- } else {
- groupPlatforms.style.display = 'block';
- }
- });
- }
- const jsonStrategy = document.getElementById('json-strategy');
- const jsonBlueprint = document.getElementById('json-blueprint');
- const jsonCaps = document.getElementById('json-caps');
- const jsonRaw = document.getElementById('json-raw');
- // Modals
- const modalRun = document.getElementById('run-modal');
- const modalLogs = document.getElementById('logs-modal');
- const terminalLogs = document.getElementById('terminal-logs');
- // Initialize
- async function init() {
- await fetchRequirements();
- setupEventListeners();
- startStatusPolling();
- }
- // Fetch Data
- async function fetchRequirements() {
- try {
- const res = await fetch('/api/requirements');
- requirements = await res.json();
- renderTaskList(requirements);
- updateStats();
- } catch (e) {
- console.error("Failed to fetch requirements", e);
- elTaskList.innerHTML = '<div style="padding:1rem;color:var(--danger)">Error loading data. Is the backend running?</div>';
- }
- }
- function renderJSON(obj) {
- if (obj === null) return `<span class="json-null">null</span>`;
- if (typeof obj === 'number') return `<span class="json-number">${obj}</span>`;
- if (typeof obj === 'boolean') return `<span class="json-boolean">${obj}</span>`;
- if (typeof obj === 'string') {
- const escaped = obj.replace(/</g, '<').replace(/>/g, '>');
- return `<span class="json-string">"${escaped}"</span>`;
- }
-
- if (Array.isArray(obj)) {
- if (obj.length === 0) return '[]';
- let html = '<div class="json-array">[<div class="json-children">';
- obj.forEach((val, i) => {
- html += `<div class="json-item">${renderJSON(val)}${i < obj.length - 1 ? ',' : ''}</div>`;
- });
- html += '</div>]</div>';
- return html;
- }
-
- if (typeof obj === 'object') {
- const keys = Object.keys(obj);
- if (keys.length === 0) return '{}';
- let html = '<div class="json-object">{<div class="json-children">';
- keys.forEach((k, i) => {
- html += `<div class="json-prop"><span class="json-key">"${k}"</span>: ${renderJSON(obj[k])}${i < keys.length - 1 ? ',' : ''}</div>`;
- });
- html += '</div>}</div>';
- return html;
- }
- return String(obj);
- }
- function renderRawCases(rawCasesObj) {
- if (!rawCasesObj || Object.keys(rawCasesObj).length === 0) return 'No raw cases data';
- const platforms = Object.keys(rawCasesObj);
- let html = `<div class="sub-tabs">`;
- platforms.forEach((p, i) => {
- const name = p.replace('case_', '').toUpperCase();
- html += `<button class="sub-tab-btn ${i === 0 ? 'active' : ''}" onclick="selectSubTab('${p}')">${name}</button>`;
- });
- html += `</div><div class="sub-tab-contents">`;
- platforms.forEach((p, i) => {
- html += `<div id="sub-tab-${p}" class="sub-tab-pane ${i === 0 ? '' : 'hidden'}">`;
- const cases = rawCasesObj[p].cases || [];
- if (cases.length === 0) {
- html += `<p style="color:var(--text-muted)">No cases found for this platform.</p>`;
- } else {
- const platCode = p.replace('case_', '');
- cases.forEach(c => {
- html += `<div class="data-card" id="case-card-${platCode}-${c.id}">
- <div class="card-header">
- <div class="card-title">📝 ${c.title || 'Untitled'}</div>
- <a href="${c.source_url}" target="_blank" class="badge-emoji primary" style="text-decoration:none">🔗 View Source</a>
- </div>
- <div class="card-body">
- <div class="tags-container">
- <span class="badge-emoji">📱 Platform: ${c.platform || p}</span>
- <span class="badge-emoji">❤️ Likes: ${c.metrics?.likes || 0}</span>
- <span class="badge-emoji">💬 Comments: ${c.metrics?.comments || 0}</span>
- <span class="badge-emoji">🔄 Shares: ${c.metrics?.shares || 0}</span>
- </div>`;
- if (c.user_feedback) {
- html += `<div class="card-section"><div class="section-title">🗣️ User Feedback</div>`;
- if (Array.isArray(c.user_feedback)) {
- html += `<ul>`;
- c.user_feedback.forEach(fb => html += `<li>${fb}</li>`);
- html += `</ul>`;
- } else {
- html += `<p>${c.user_feedback}</p>`;
- }
- html += `</div>`;
- }
- if (c.workflow_process) {
- html += `<div class="card-section"><div class="section-title">🧱 Workflow Process</div>`;
- if (Array.isArray(c.workflow_process)) {
- html += `<div class="phase-list">`;
- c.workflow_process.forEach(wp => html += `<div class="phase-item">${wp}</div>`);
- html += `</div>`;
- } else {
- html += `<p>${c.workflow_process}</p>`;
- }
- html += `</div>`;
- }
- if (c.images && c.images.length > 0) {
- html += `<div class="card-section"><div class="section-title">🖼️ Images</div><div class="image-gallery">`;
- c.images.forEach(img => {
- const url = typeof img === 'string' ? img : img.url;
- const desc = (typeof img === 'object' ? img.description : '') || '';
- html += `<div class="image-item"><img src="${url}" alt="${desc}" title="${desc}"><div class="image-caption">${desc}</div></div>`;
- });
- html += `</div></div>`;
- }
- html += `</div></div>`;
- });
- }
- html += `</div>`;
- });
- html += `</div>`;
- return html;
- }
- function renderCapabilities(capsObj) {
- if (!capsObj || !capsObj.extracted_capabilities) return 'No capabilities data';
- const caps = capsObj.extracted_capabilities;
- if (caps.length === 0) return '<p>No capabilities extracted.</p>';
-
- let html = ``;
- caps.forEach(cap => {
- const isNew = cap.is_new ? '<span class="badge-emoji success">✨ New Capability</span>' : '';
- html += `<div class="data-card">
- <div class="card-header">
- <div class="card-title">⚡ [${cap.id || 'N/A'}] ${cap.name || 'Unnamed'}</div>
- ${isNew}
- </div>
- <div class="card-body">
- <p>${cap.description || ''}</p>
- <div class="card-section"><div class="section-title">✨ Effects</div><ul>`;
- if (cap.effects) cap.effects.forEach(eff => html += `<li>${eff}</li>`);
- html += `</ul></div>`;
- if (cap.implements && Object.keys(cap.implements).length > 0) {
- html += `<div class="card-section"><div class="section-title">🛠️ Implements Tools</div><div class="tags-container">`;
- for (const [tool, args] of Object.entries(cap.implements)) {
- html += `<span class="badge-emoji primary" title="${args}">🔧 ${tool}</span>`;
- }
- html += `</div></div>`;
- }
- if (cap.case_references && cap.case_references.length > 0) {
- html += `<div class="card-section"><div class="section-title">📌 Source Cases</div><div class="tags-container" style="gap:0.8rem">`;
- cap.case_references.forEach(ref => {
- let caseId = null;
- let title = ref;
- const matchA = ref.match(/^case_([a-z]+)_(\d+)(?:[::\s]+(.*))?/);
- if (matchA) {
- caseId = `${matchA[1]}-case_${matchA[2]}`;
- title = matchA[3] || ref;
- } else {
- const matchB = ref.match(/^([a-z]+)[\/\s](case_\d+)(?:[::\s]+(.*))?/);
- if (matchB) {
- caseId = `${matchB[1]}-${matchB[2]}`;
- title = matchB[3] || ref;
- } else {
- const matchC = ref.match(/^case_(\d+)_([a-z]+)(?:[::\s]+(.*))?/);
- if (matchC) {
- caseId = `${matchC[2]}-case_${matchC[1]}`;
- title = matchC[3] || ref;
- }
- }
- }
-
- if (caseId) {
- html += `<a href="#" onclick="jumpToCase('${caseId}'); return false;" class="badge-emoji primary" style="text-decoration:none; white-space:normal; text-align:left; line-height:1.4">
- <strong>🔍 ${caseId.replace('-', ' ')}</strong><br>
- <span style="font-size:0.75rem">${title.substring(0, 40) + (title.length>40?'...':'')}</span>
- </a>`;
- } else {
- html += `<span class="badge-emoji" style="white-space:normal; text-align:left; line-height:1.4; font-size:0.75rem">${ref}</span>`;
- }
- });
- html += `</div></div>`;
- }
- html += `</div></div>`;
- });
- return html;
- }
- function renderBlueprint(bpObj) {
- if (!bpObj || !bpObj.blueprints) return 'No blueprint data';
- let html = ``;
-
- if (bpObj.distilled_cases && bpObj.distilled_cases.length > 0) {
- html += `<div style="margin-bottom: 1.5rem; padding: 1rem; background: rgba(0,0,0,0.2); border-radius: 8px;">
- <h3 style="color:var(--text-main); margin-bottom:0.8rem">📚 Source Cases for Blueprint</h3>
- <div class="tags-container" style="gap:0.8rem">`;
- bpObj.distilled_cases.forEach(c => {
- let targetId = c.id;
- const matchA = targetId.match(/^case_([a-z]+)_(\d+)/);
- if (matchA) {
- targetId = `${matchA[1]}-case_${matchA[2]}`;
- } else {
- const matchB = targetId.match(/^([a-z]+)[\/\s](case_\d+)/);
- if (matchB) {
- targetId = `${matchB[1]}-${matchB[2]}`;
- } else {
- const matchC = targetId.match(/^case_(\d+)_([a-z]+)/);
- if (matchC) targetId = `${matchC[2]}-case_${matchC[1]}`;
- }
- }
-
- html += `<a href="#" onclick="jumpToCase('${targetId}'); return false;" class="badge-emoji primary" style="text-decoration:none; white-space:normal; text-align:left; line-height:1.4">
- <strong>🔍 ${c.id}</strong><br>
- <span style="font-size:0.75rem">${c.title ? c.title.substring(0, 40) + (c.title.length>40?'...':'') : 'View Source'}</span>
- </a>`;
- });
- html += `</div></div>`;
- }
- bpObj.blueprints.forEach(bp => {
- html += `<div class="data-card">
- <div class="card-header">
- <div class="card-title">🗺️ ${bp.name || 'Unnamed'}</div>
- </div>
- <div class="card-body">
- <div class="card-section"><div class="section-title">🧠 Reasoning</div>
- <p>${bp.reasoning || ''}</p></div>
- <div class="card-section"><div class="section-title">📍 Phases</div><div class="phase-list">`;
- if (bp.phases) bp.phases.forEach(ph => {
- html += `<div class="phase-item">
- <div class="phase-title">${ph.phase || ''}</div>
- <div>${ph.description || ''}</div>
- </div>`;
- });
- html += `</div></div></div>`;
- });
- return html;
- }
- function renderStrategy(stratObj) {
- if (!stratObj || !stratObj.strategies) return 'No strategy data';
- let html = `<div style="margin-bottom: 1.5rem; padding: 1rem; background: rgba(0,0,0,0.2); border-radius: 8px;">
- <h3 style="color:var(--text-main); margin-bottom:0.5rem">🎯 Requirement</h3>
- <p style="color:var(--text-muted)">${stratObj.requirement || ''}</p>
- </div>`;
-
- stratObj.strategies.sort((a,b) => (b.is_selected === true) - (a.is_selected === true));
-
- stratObj.strategies.forEach(strat => {
- const isSelected = strat.is_selected;
- const icon = isSelected ? '🎯' : '🥈';
- const badge = isSelected ? '<span class="badge-emoji success">⭐ Selected Strategy</span>' : '<span class="badge-emoji warning">Alternative</span>';
-
- let scoreHtml = '';
- if (strat.coverage_score !== undefined) {
- const score = strat.coverage_score;
- const deg = score * 360;
- const scoreClass = score >= 0.8 ? '' : (score >= 0.5 ? 'medium' : 'low');
- scoreHtml = `<div class="score-container">
- <div class="score-circle ${scoreClass}" style="--score-deg: ${deg}deg"><span>${Math.round(score * 100)}%</span></div>
- <div class="score-text"><strong>Coverage Score</strong><br>${strat.coverage_explanation || ''}</div>
- </div>`;
- }
-
- html += `<div class="data-card" style="${isSelected ? 'border-color: var(--accent-primary); box-shadow: 0 0 15px rgba(59,130,246,0.1);' : ''}">
- <div class="card-header">
- <div class="card-title">${icon} ${strat.name || 'Unnamed'}</div>
- ${badge}
- </div>
- <div class="card-body">
- ${scoreHtml}
- <div class="tags-container" style="margin-bottom:1rem">
- <span class="badge-emoji">📥 Source: ${strat.source || 'N/A'}</span>
- </div>`;
-
- if (strat.reasoning) html += `<div class="card-section"><div class="section-title">🧠 Reasoning</div><p>${strat.reasoning}</p></div>`;
- if (strat.why_not) html += `<div class="card-section"><div class="section-title">❌ Why Not Selected</div><p>${strat.why_not}</p></div>`;
-
- if (strat.workflow_outline && strat.workflow_outline.length > 0) {
- html += `<div class="card-section"><div class="section-title">🧱 Workflow Outline</div><div class="phase-list">`;
- strat.workflow_outline.forEach(wo => {
- html += `<div class="phase-item">
- <div class="phase-title">${wo.phase}</div>
- <div style="margin-bottom:0.5rem">${wo.description}</div>
- <div class="tags-container">`;
- if (wo.capabilities) {
- wo.capabilities.forEach(cap => html += `<span class="badge-emoji primary">⚡ ${cap.name}</span>`);
- }
- html += `</div></div>`;
- });
- html += `</div></div>`;
- }
- html += `</div></div>`;
- });
-
- if (stratObj.uncovered_requirements && stratObj.uncovered_requirements.length > 0) {
- html += `<div class="data-card" style="border-color: var(--danger)">
- <div class="card-header"><div class="card-title">⚠️ Uncovered Requirements</div></div>
- <div class="card-body"><ul>`;
- stratObj.uncovered_requirements.forEach(req => html += `<li>${req}</li>`);
- html += `</ul></div></div>`;
- }
-
- return html;
- }
- window.selectSubTab = function(p) {
- document.querySelectorAll('.sub-tab-btn').forEach(b => {
- b.classList.remove('active');
- if(b.textContent === p.replace('case_', '').toUpperCase()) b.classList.add('active');
- });
- document.querySelectorAll('.sub-tab-pane').forEach(pane => pane.classList.add('hidden'));
- const target = document.getElementById('sub-tab-' + p);
- if(target) target.classList.remove('hidden');
- };
- window.jumpToCase = function(caseId) {
- // Switch to raw tab
- document.querySelector('.tab-btn[data-target="tab-raw"]').click();
-
- // Find the case card
- const targetCard = document.getElementById('case-card-' + caseId);
- if (targetCard) {
- // Find which sub-tab pane it's inside
- const pane = targetCard.closest('.sub-tab-pane');
- if (pane) {
- const platformId = pane.id.replace('sub-tab-', '');
- selectSubTab(platformId);
- }
-
- // Scroll into view
- targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
-
- // Add highlight
- targetCard.classList.remove('highlight-pulse');
- void targetCard.offsetWidth; // Trigger reflow
- targetCard.classList.add('highlight-pulse');
- } else {
- alert("Case not found in raw cases data.");
- }
- };
- async function fetchMemo(index) {
- const elTextarea = document.getElementById('memo-textarea');
- const elStatus = document.getElementById('memo-status');
- if(!elTextarea) return;
- elTextarea.value = 'Loading...';
- elTextarea.disabled = true;
- try {
- const res = await fetch(`/api/requirements/${index}/memo`);
- const data = await res.json();
- elTextarea.value = data.memo || '';
- elStatus.textContent = '';
- } catch (e) {
- elTextarea.value = '';
- console.error("Failed to fetch memo", e);
- }
- elTextarea.disabled = false;
- }
- async function fetchPromptsList() {
- try {
- const res = await fetch('/api/prompts');
- const list = await res.json();
- if(!elPromptList) return;
- elPromptList.innerHTML = '';
- list.forEach((p, idx) => {
- const div = document.createElement('div');
- div.className = 'prompt-tab';
- div.textContent = p;
- div.onclick = () => selectPrompt(p, div);
- elPromptList.appendChild(div);
- if (idx === 0) selectPrompt(p, div);
- });
- } catch (e) {
- console.error("Failed to load prompts", e);
- }
- }
- async function selectPrompt(name, tabEl) {
- currentPromptName = name;
- document.querySelectorAll('.prompt-tab').forEach(el => el.classList.remove('active'));
- tabEl.classList.add('active');
-
- elPromptTextarea.value = 'Loading...';
- elPromptTextarea.disabled = true;
- try {
- const res = await fetch(`/api/prompts/${name}`);
- const data = await res.json();
- elPromptTextarea.value = data.content || '';
- } catch (e) {
- elPromptTextarea.value = 'Error loading prompt.';
- }
- elPromptTextarea.disabled = false;
- elPromptStatus.textContent = '';
- }
- async function fetchRequirementData(index) {
- try {
- const res = await fetch(`/api/requirements/${index}/data`);
- const data = await res.json();
-
- jsonStrategy.innerHTML = data.strategy ? renderStrategy(data.strategy) : '<p style="color:var(--text-muted)">No strategy data</p>';
- jsonBlueprint.innerHTML = data.blueprint ? renderBlueprint(data.blueprint) : '<p style="color:var(--text-muted)">No blueprint data</p>';
- jsonCaps.innerHTML = data.capabilities ? renderCapabilities(data.capabilities) : '<p style="color:var(--text-muted)">No capabilities data</p>';
- jsonRaw.innerHTML = data.raw_cases ? renderRawCases(data.raw_cases) : '<p style="color:var(--text-muted)">No raw cases data</p>';
- } catch (e) {
- console.error("Failed to fetch data", e);
- }
- }
- async function pollStatus() {
- try {
- const res = await fetch('/api/pipeline/status');
- const statusData = await res.json();
-
- let needsListUpdate = false;
- // Check if any status changed
- for(const [idxStr, runInfo] of Object.entries(statusData)) {
- const idx = parseInt(idxStr);
- if (!activeRuns[idx] || activeRuns[idx].status !== runInfo.status) {
- needsListUpdate = true;
- }
- activeRuns[idx] = runInfo;
- // Update logs if modal is open for this index
- if (currentSelectedIndex === idx && !modalLogs.classList.contains('hidden')) {
- terminalLogs.textContent = runInfo.logs.join('');
- terminalLogs.scrollTop = terminalLogs.scrollHeight;
- }
-
- // Update detail view banner if this is the selected one
- if (currentSelectedIndex === idx) {
- updateDetailBannerStatus(runInfo.status);
- }
- }
-
- if (needsListUpdate) {
- // update in requirements array
- requirements.forEach(req => {
- if (activeRuns[req.index]) {
- req.status = activeRuns[req.index].status;
- }
- });
- renderTaskList(requirements);
- }
- } catch (e) {
- console.error("Failed to poll status", e);
- }
- }
- function startStatusPolling() {
- if (statusInterval) clearInterval(statusInterval);
- statusInterval = setInterval(pollStatus, 2000);
- }
- // Render
- function renderTaskList(list) {
- elTaskList.innerHTML = '';
- list.forEach(req => {
- const div = document.createElement('div');
- div.className = `task-item ${currentSelectedIndex === req.index ? 'active' : ''}`;
- div.onclick = () => selectRequirement(req.index);
- let statusTag = '';
- if (req.status === 'running') statusTag = '<span class="tag running">Running</span>';
- else if (req.status === 'completed') statusTag = '<span class="tag success">Complete</span>';
- else if (req.status === 'partial') statusTag = '<span class="tag warning">Partial</span>';
- else statusTag = '<span class="tag">Pending</span>';
- let memoHtml = '';
- if (req.memo && req.memo.trim() !== '') {
- memoHtml = `<div class="task-memo" title="${req.memo}">${req.memo}</div>`;
- }
- div.innerHTML = `
- <div class="task-id">#${req.id}</div>
- <div class="task-req" title="${req.requirement}">${req.requirement}</div>
- ${memoHtml}
- <div class="task-tags">
- ${statusTag}
- <span class="tag">Cases: ${req.raw_cases_count}</span>
- </div>
- `;
- elTaskList.appendChild(div);
- });
- }
- function updateStats() {
- const total = requirements.length;
- const completed = requirements.filter(r => r.status === 'completed').length;
- const running = requirements.filter(r => r.status === 'running').length;
-
- elStatsContainer.innerHTML = `
- <span>Total: ${total}</span>
- <span style="color:var(--success)">Done: ${completed}</span>
- ${running > 0 ? `<span style="color:var(--accent-primary)">Running: ${running}</span>` : ''}
- `;
- }
- function selectRequirement(index) {
- currentSelectedIndex = index;
- const req = requirements.find(r => r.index === index);
- if (!req) return;
- // Update List UI
- document.querySelectorAll('.task-item').forEach(el => el.classList.remove('active'));
- // We re-render to be safe, but simple class toggle is better.
- renderTaskList(requirements);
- // Update Detail UI
- elEmptyState.classList.add('hidden');
- elDetailView.classList.remove('hidden');
-
- elDetailId.textContent = req.id;
- elDetailTitle.textContent = req.requirement;
- updateDetailBannerStatus(activeRuns[index] ? activeRuns[index].status : req.status);
- // Fetch data
- jsonStrategy.textContent = 'Loading...';
- jsonBlueprint.textContent = 'Loading...';
- jsonCaps.textContent = 'Loading...';
- jsonRaw.textContent = 'Loading...';
- fetchRequirementData(index);
- fetchMemo(index);
- }
- function updateDetailBannerStatus(status) {
- if (status === 'running') {
- elStatusBanner.classList.remove('hidden');
- elStatusBanner.style.background = 'rgba(59, 130, 246, 0.1)';
- elStatusText.textContent = 'Pipeline is currently running...';
- elStatusBanner.querySelector('.status-indicator').style.display = 'block';
- elStatusBanner.querySelector('.status-indicator').style.background = 'var(--accent-primary)';
- } else if (status === 'failed') {
- elStatusBanner.classList.remove('hidden');
- elStatusBanner.style.background = 'rgba(239, 68, 68, 0.1)';
- elStatusText.textContent = 'Pipeline run failed.';
- elStatusBanner.querySelector('.status-indicator').style.display = 'block';
- elStatusBanner.querySelector('.status-indicator').style.background = 'var(--danger)';
- } else {
- elStatusBanner.classList.add('hidden');
- }
- }
- // Actions
- async function triggerRun() {
- if (currentSelectedIndex === null) return;
-
- const requestData = {
- platforms: document.getElementById('input-platforms').value,
- skip_research: false,
- research_only: false,
- use_claude_sdk: document.getElementById('check-claude-sdk').checked,
- restart_mode: document.getElementById('select-force-phase').value
- };
- modalRun.classList.add('hidden');
-
- // Optimistic UI update
- const req = requirements.find(r => r.index === currentSelectedIndex);
- if (req) req.status = 'running';
- activeRuns[currentSelectedIndex] = { status: 'running', logs: ['Initializing request...'] };
- renderTaskList(requirements);
- updateDetailBannerStatus('running');
- try {
- const res = await fetch(`/api/pipeline/run/${currentSelectedIndex}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(requestData)
- });
-
- if (!res.ok) {
- const err = await res.json();
- alert("Error: " + err.detail);
- }
- } catch (e) {
- console.error("Run failed", e);
- alert("Failed to trigger run");
- }
- }
- // Event Listeners
- function setupEventListeners() {
- // Search
- elSearchInput.addEventListener('input', (e) => {
- const query = e.target.value.toLowerCase();
- const filtered = requirements.filter(r =>
- r.requirement.toLowerCase().includes(query) ||
- r.id.includes(query)
- );
- renderTaskList(filtered);
- });
- // Tabs
- document.querySelectorAll('.tab-btn').forEach(btn => {
- btn.addEventListener('click', () => {
- document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
- document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
-
- btn.classList.add('active');
- document.getElementById(btn.dataset.target).classList.add('active');
- });
- });
- // Modals
- document.getElementById('btn-open-run-modal').addEventListener('click', () => {
- if (currentSelectedIndex !== null) {
- modalRun.classList.remove('hidden');
- }
- });
- document.getElementById('btn-close-modal').addEventListener('click', () => {
- modalRun.classList.add('hidden');
- });
- document.getElementById('btn-cancel-run').addEventListener('click', () => {
- modalRun.classList.add('hidden');
- });
- document.getElementById('btn-confirm-run').addEventListener('click', triggerRun);
- document.getElementById('btn-view-logs').addEventListener('click', () => {
- modalLogs.classList.remove('hidden');
- if (activeRuns[currentSelectedIndex]) {
- terminalLogs.textContent = activeRuns[currentSelectedIndex].logs.join('');
- terminalLogs.scrollTop = terminalLogs.scrollHeight;
- } else {
- terminalLogs.textContent = 'No logs available.';
- }
- });
- document.getElementById('btn-close-logs').addEventListener('click', () => {
- modalLogs.classList.add('hidden');
- });
- const btnSaveMemo = document.getElementById('btn-save-memo');
- if (btnSaveMemo) {
- btnSaveMemo.addEventListener('click', async () => {
- if (currentSelectedIndex === null) return;
- const elTextarea = document.getElementById('memo-textarea');
- const elStatus = document.getElementById('memo-status');
- elStatus.textContent = 'Saving...';
- elStatus.style.color = 'var(--text-muted)';
-
- try {
- const res = await fetch(`/api/requirements/${currentSelectedIndex}/memo`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ memo: elTextarea.value })
- });
- if (res.ok) {
- elStatus.textContent = 'Saved!';
- elStatus.style.color = 'var(--success)';
- setTimeout(() => elStatus.textContent = '', 2000);
-
- const req = requirements.find(r => r.index === currentSelectedIndex);
- if (req) {
- req.memo = elTextarea.value;
- renderTaskList(requirements);
- }
- } else {
- throw new Error("Bad response");
- }
- } catch (e) {
- console.error("Failed to save memo", e);
- elStatus.textContent = 'Save failed';
- elStatus.style.color = 'var(--danger)';
- }
- });
- }
- const btnOpenPrompts = document.getElementById('btn-open-prompts');
- if (btnOpenPrompts) {
- btnOpenPrompts.addEventListener('click', () => {
- modalPrompts.classList.remove('hidden');
- fetchPromptsList();
- });
- }
- const btnClosePrompts = document.getElementById('btn-close-prompts');
- if (btnClosePrompts) {
- btnClosePrompts.addEventListener('click', () => {
- modalPrompts.classList.add('hidden');
- });
- }
- const btnSavePrompt = document.getElementById('btn-save-prompt');
- if (btnSavePrompt) {
- btnSavePrompt.addEventListener('click', async () => {
- if (!currentPromptName) return;
- elPromptStatus.textContent = 'Saving...';
- elPromptStatus.style.color = 'var(--text-muted)';
-
- try {
- const res = await fetch(`/api/prompts/${currentPromptName}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ content: elPromptTextarea.value })
- });
- if (res.ok) {
- elPromptStatus.textContent = 'Saved!';
- elPromptStatus.style.color = 'var(--success)';
- setTimeout(() => elPromptStatus.textContent = '', 2000);
- } else {
- throw new Error("Failed to save");
- }
- } catch(e) {
- elPromptStatus.textContent = 'Save failed';
- elPromptStatus.style.color = 'var(--danger)';
- }
- });
- }
- }
- // Boot
- init();
|