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 ``;
platforms.forEach((p, i) => {
const name = p.replace('case_', '').toUpperCase();
html += `${name} `;
});
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 += `
π± 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
`;
c.images.forEach(img => {
const url = typeof img === 'string' ? img : img.url;
const desc = (typeof img === 'object' ? img.description : '') || '';
html += `
${desc}
`;
});
html += `
`;
}
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 ? '
π§ 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 += `
${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 += `
`;
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();