|
@@ -0,0 +1,767 @@
|
|
|
|
|
+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();
|