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 = '
Error loading data. Is the backend running?
'; } } function renderJSON(obj) { if (obj === null) return `null`; if (typeof obj === 'number') return `${obj}`; if (typeof obj === 'boolean') return `${obj}`; if (typeof obj === 'string') { const escaped = obj.replace(//g, '>'); return `"${escaped}"`; } if (Array.isArray(obj)) { if (obj.length === 0) return '[]'; let html = '
[
'; obj.forEach((val, i) => { html += `
${renderJSON(val)}${i < obj.length - 1 ? ',' : ''}
`; }); html += '
]
'; return html; } if (typeof obj === 'object') { const keys = Object.keys(obj); if (keys.length === 0) return '{}'; let html = '
{
'; keys.forEach((k, i) => { html += `
"${k}": ${renderJSON(obj[k])}${i < keys.length - 1 ? ',' : ''}
`; }); html += '
}
'; 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 = `
`; platforms.forEach((p, i) => { const name = p.replace('case_', '').toUpperCase(); html += ``; }); html += `
`; platforms.forEach((p, i) => { html += `
`; const cases = rawCasesObj[p].cases || []; if (cases.length === 0) { html += `

No cases found for this platform.

`; } else { const platCode = p.replace('case_', ''); cases.forEach(c => { html += `
πŸ“ ${c.title || 'Untitled'}
πŸ”— View Source
πŸ“± Platform: ${c.platform || p} ❀️ Likes: ${c.metrics?.likes || 0} πŸ’¬ Comments: ${c.metrics?.comments || 0} πŸ”„ Shares: ${c.metrics?.shares || 0}
`; if (c.user_feedback) { html += `
πŸ—£οΈ User Feedback
`; if (Array.isArray(c.user_feedback)) { html += `
    `; c.user_feedback.forEach(fb => html += `
  • ${fb}
  • `); html += `
`; } else { html += `

${c.user_feedback}

`; } html += `
`; } if (c.workflow_process) { html += `
🧱 Workflow Process
`; if (Array.isArray(c.workflow_process)) { html += `
`; c.workflow_process.forEach(wp => html += `
${wp}
`); html += `
`; } else { html += `

${c.workflow_process}

`; } html += `
`; } if (c.images && c.images.length > 0) { html += `
πŸ–ΌοΈ Images
`; } html += `
`; }); } html += `
`; }); html += `
`; return html; } function renderCapabilities(capsObj) { if (!capsObj || !capsObj.extracted_capabilities) return 'No capabilities data'; const caps = capsObj.extracted_capabilities; if (caps.length === 0) return '

No capabilities extracted.

'; let html = ``; caps.forEach(cap => { const isNew = cap.is_new ? '✨ New Capability' : ''; html += `
⚑ [${cap.id || 'N/A'}] ${cap.name || 'Unnamed'}
${isNew}

${cap.description || ''}

✨ Effects
    `; if (cap.effects) cap.effects.forEach(eff => html += `
  • ${eff}
  • `); html += `
`; if (cap.implements && Object.keys(cap.implements).length > 0) { html += `
πŸ› οΈ Implements Tools
`; for (const [tool, args] of Object.entries(cap.implements)) { html += `πŸ”§ ${tool}`; } html += `
`; } if (cap.case_references && cap.case_references.length > 0) { html += `
πŸ“Œ Source Cases
`; 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 += ` πŸ” ${caseId.replace('-', ' ')}
${title.substring(0, 40) + (title.length>40?'...':'')}
`; } else { html += `${ref}`; } }); html += `
`; } html += `
`; }); 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 += `

πŸ“š Source Cases for Blueprint

`; 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 += ` πŸ” ${c.id}
${c.title ? c.title.substring(0, 40) + (c.title.length>40?'...':'') : 'View Source'}
`; }); html += `
`; } bpObj.blueprints.forEach(bp => { html += `
πŸ—ΊοΈ ${bp.name || 'Unnamed'}
🧠 Reasoning

${bp.reasoning || ''}

πŸ“ Phases
`; if (bp.phases) bp.phases.forEach(ph => { html += `
${ph.phase || ''}
${ph.description || ''}
`; }); html += `
`; }); return html; } function renderStrategy(stratObj) { if (!stratObj || !stratObj.strategies) return 'No strategy data'; let html = `

🎯 Requirement

${stratObj.requirement || ''}

`; 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 ? '⭐ Selected Strategy' : 'Alternative'; 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 = `
${Math.round(score * 100)}%
Coverage Score
${strat.coverage_explanation || ''}
`; } html += `
${icon} ${strat.name || 'Unnamed'}
${badge}
${scoreHtml}
πŸ“₯ Source: ${strat.source || 'N/A'}
`; if (strat.reasoning) html += `
🧠 Reasoning

${strat.reasoning}

`; if (strat.why_not) html += `
❌ Why Not Selected

${strat.why_not}

`; if (strat.workflow_outline && strat.workflow_outline.length > 0) { html += `
🧱 Workflow Outline
`; strat.workflow_outline.forEach(wo => { html += `
${wo.phase}
${wo.description}
`; if (wo.capabilities) { wo.capabilities.forEach(cap => html += `⚑ ${cap.name}`); } html += `
`; }); html += `
`; } html += `
`; }); if (stratObj.uncovered_requirements && stratObj.uncovered_requirements.length > 0) { html += `
⚠️ Uncovered Requirements
    `; stratObj.uncovered_requirements.forEach(req => html += `
  • ${req}
  • `); html += `
`; } 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) : '

No strategy data

'; jsonBlueprint.innerHTML = data.blueprint ? renderBlueprint(data.blueprint) : '

No blueprint data

'; jsonCaps.innerHTML = data.capabilities ? renderCapabilities(data.capabilities) : '

No capabilities data

'; jsonRaw.innerHTML = data.raw_cases ? renderRawCases(data.raw_cases) : '

No raw cases data

'; } 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 = 'Running'; else if (req.status === 'completed') statusTag = 'Complete'; else if (req.status === 'partial') statusTag = 'Partial'; else statusTag = 'Pending'; let memoHtml = ''; if (req.memo && req.memo.trim() !== '') { memoHtml = `
${req.memo}
`; } div.innerHTML = `
#${req.id}
${req.requirement}
${memoHtml}
${statusTag} Cases: ${req.raw_cases_count}
`; 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 = ` Total: ${total} Done: ${completed} ${running > 0 ? `Running: ${running}` : ''} `; } 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();