let requirements = [];
let currentSelectedIndex = null;
let activeRuns = {};
let statusInterval = null;
let currentAvailablePlatforms = ['xhs', 'youtube', 'bili', 'x'];
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 jsonSource = document.getElementById('json-source');
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 += `
`;
if (rawCasesObj[p].error) {
const safeRaw = rawCasesObj[p].raw_content ? rawCasesObj[p].raw_content.replace(//g, ">") : "Empty file.";
html += `
⚠️ JSON Parsing Failed
${safeRaw}
`;
return;
}
const cases = rawCasesObj[p].cases || [];
if (cases.length === 0) {
html += `
⚠️ Non-standard format (No cases array found)
${renderJSON(rawCasesObj[p])}
`;
} 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 renderSourceCases(sourceObj) {
if (!sourceObj) 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 = renderDataOrRaw(data.strategy, renderStrategy);
jsonBlueprint.innerHTML = renderDataOrRaw(data.blueprint, renderBlueprint);
jsonCaps.innerHTML = renderDataOrRaw(data.capabilities, renderCapabilities);
let rawCasesClone = null;
if (data.raw_cases) {
rawCasesClone = { ...data.raw_cases };
if (rawCasesClone['source']) {
jsonSource.innerHTML = renderSourceCases(rawCasesClone['source']);
delete rawCasesClone['source'];
} else {
jsonSource.innerHTML = '
No source data available
';
}
} else {
jsonSource.innerHTML = '
No source data available
';
}
jsonRaw.innerHTML = renderDataOrRaw(rawCasesClone, renderRawCases);
if (rawCasesClone && Object.keys(rawCasesClone).length > 0) {
currentAvailablePlatforms = Object.keys(rawCasesClone)
.filter(p => p.startsWith('case_'))
.map(p => p.replace('case_', ''));
if (currentAvailablePlatforms.length === 0) {
currentAvailablePlatforms = ['xhs', 'youtube', 'bili', 'x'];
}
} else {
currentAvailablePlatforms = ['xhs', 'youtube', 'bili', 'x'];
}
} 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) {
const btnStop = document.getElementById('btn-stop-pipeline');
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)';
if (btnStop) btnStop.style.display = 'inline-block';
} 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)';
if (btnStop) btnStop.style.display = 'none';
} else {
elStatusBanner.classList.add('hidden');
if (btnStop) btnStop.style.display = 'none';
}
}
// 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');
const selectForcePhase = document.getElementById('select-force-phase');
if (selectForcePhase) selectForcePhase.dispatchEvent(new Event('change'));
const inputPlatforms = document.getElementById('input-platforms');
if (inputPlatforms && currentAvailablePlatforms && currentAvailablePlatforms.length > 0) {
inputPlatforms.value = currentAvailablePlatforms.join(',');
}
}
});
document.getElementById('btn-close-modal').addEventListener('click', () => {
modalRun.classList.add('hidden');
});
document.getElementById('btn-cancel-run').addEventListener('click', () => {
modalRun.classList.add('hidden');
});
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 (['smart', 'phase1_platforms', 'single_platforms'].includes(val)) {
groupPlatforms.style.display = 'block';
} else {
groupPlatforms.style.display = 'none';
}
});
}
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.';
}
});
const btnStop = document.getElementById('btn-stop-pipeline');
if (btnStop) {
btnStop.addEventListener('click', async () => {
if (currentSelectedIndex === null) return;
if (!confirm('Are you sure you want to stop the running pipeline?')) return;
try {
const res = await fetch(`/api/pipeline/stop/${currentSelectedIndex}`, { method: 'POST' });
if (!res.ok) {
const err = await res.json();
alert("Error stopping pipeline: " + err.detail);
}
} catch (e) {
console.error("Failed to stop pipeline", e);
alert("Failed to stop pipeline");
}
});
}
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();