app.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. let requirements = [];
  2. let currentSelectedIndex = null;
  3. let activeRuns = {};
  4. let statusInterval = null;
  5. let currentPromptName = null;
  6. const modalPrompts = document.getElementById('prompts-modal');
  7. const elPromptList = document.getElementById('prompt-list');
  8. const elPromptTextarea = document.getElementById('prompt-textarea');
  9. const elPromptStatus = document.getElementById('prompt-save-status');
  10. // DOM Elements
  11. const elTaskList = document.getElementById('task-list');
  12. const elSearchInput = document.getElementById('search-input');
  13. const elStatsContainer = document.getElementById('stats-container');
  14. const elMainContent = document.getElementById('main-content');
  15. const elEmptyState = document.getElementById('empty-state');
  16. const elDetailView = document.getElementById('detail-view');
  17. const elDetailId = document.getElementById('detail-id');
  18. const elDetailTitle = document.getElementById('detail-title');
  19. const elStatusBanner = document.getElementById('status-banner');
  20. const elStatusText = document.getElementById('status-text');
  21. // Form logic
  22. const selectForcePhase = document.getElementById('select-force-phase');
  23. const groupPlatforms = document.getElementById('group-platforms');
  24. if (selectForcePhase && groupPlatforms) {
  25. selectForcePhase.addEventListener('change', (e) => {
  26. const val = e.target.value;
  27. if (val.startsWith('phase2') || val === 'phase3') {
  28. groupPlatforms.style.display = 'none';
  29. } else {
  30. groupPlatforms.style.display = 'block';
  31. }
  32. });
  33. }
  34. const jsonStrategy = document.getElementById('json-strategy');
  35. const jsonBlueprint = document.getElementById('json-blueprint');
  36. const jsonCaps = document.getElementById('json-caps');
  37. const jsonRaw = document.getElementById('json-raw');
  38. // Modals
  39. const modalRun = document.getElementById('run-modal');
  40. const modalLogs = document.getElementById('logs-modal');
  41. const terminalLogs = document.getElementById('terminal-logs');
  42. // Initialize
  43. async function init() {
  44. await fetchRequirements();
  45. setupEventListeners();
  46. startStatusPolling();
  47. }
  48. // Fetch Data
  49. async function fetchRequirements() {
  50. try {
  51. const res = await fetch('/api/requirements');
  52. requirements = await res.json();
  53. renderTaskList(requirements);
  54. updateStats();
  55. } catch (e) {
  56. console.error("Failed to fetch requirements", e);
  57. elTaskList.innerHTML = '<div style="padding:1rem;color:var(--danger)">Error loading data. Is the backend running?</div>';
  58. }
  59. }
  60. function renderJSON(obj) {
  61. if (obj === null) return `<span class="json-null">null</span>`;
  62. if (typeof obj === 'number') return `<span class="json-number">${obj}</span>`;
  63. if (typeof obj === 'boolean') return `<span class="json-boolean">${obj}</span>`;
  64. if (typeof obj === 'string') {
  65. const escaped = obj.replace(/</g, '&lt;').replace(/>/g, '&gt;');
  66. return `<span class="json-string">"${escaped}"</span>`;
  67. }
  68. if (Array.isArray(obj)) {
  69. if (obj.length === 0) return '[]';
  70. let html = '<div class="json-array">[<div class="json-children">';
  71. obj.forEach((val, i) => {
  72. html += `<div class="json-item">${renderJSON(val)}${i < obj.length - 1 ? ',' : ''}</div>`;
  73. });
  74. html += '</div>]</div>';
  75. return html;
  76. }
  77. if (typeof obj === 'object') {
  78. const keys = Object.keys(obj);
  79. if (keys.length === 0) return '{}';
  80. let html = '<div class="json-object">{<div class="json-children">';
  81. keys.forEach((k, i) => {
  82. html += `<div class="json-prop"><span class="json-key">"${k}"</span>: ${renderJSON(obj[k])}${i < keys.length - 1 ? ',' : ''}</div>`;
  83. });
  84. html += '</div>}</div>';
  85. return html;
  86. }
  87. return String(obj);
  88. }
  89. function renderRawCases(rawCasesObj) {
  90. if (!rawCasesObj || Object.keys(rawCasesObj).length === 0) return 'No raw cases data';
  91. const platforms = Object.keys(rawCasesObj);
  92. let html = `<div class="sub-tabs">`;
  93. platforms.forEach((p, i) => {
  94. const name = p.replace('case_', '').toUpperCase();
  95. html += `<button class="sub-tab-btn ${i === 0 ? 'active' : ''}" onclick="selectSubTab('${p}')">${name}</button>`;
  96. });
  97. html += `</div><div class="sub-tab-contents">`;
  98. platforms.forEach((p, i) => {
  99. html += `<div id="sub-tab-${p}" class="sub-tab-pane ${i === 0 ? '' : 'hidden'}">`;
  100. const cases = rawCasesObj[p].cases || [];
  101. if (cases.length === 0) {
  102. html += `<p style="color:var(--text-muted)">No cases found for this platform.</p>`;
  103. } else {
  104. const platCode = p.replace('case_', '');
  105. cases.forEach(c => {
  106. html += `<div class="data-card" id="case-card-${platCode}-${c.id}">
  107. <div class="card-header">
  108. <div class="card-title">📝 ${c.title || 'Untitled'}</div>
  109. <a href="${c.source_url}" target="_blank" class="badge-emoji primary" style="text-decoration:none">🔗 View Source</a>
  110. </div>
  111. <div class="card-body">
  112. <div class="tags-container">
  113. <span class="badge-emoji">📱 Platform: ${c.platform || p}</span>
  114. <span class="badge-emoji">❤️ Likes: ${c.metrics?.likes || 0}</span>
  115. <span class="badge-emoji">💬 Comments: ${c.metrics?.comments || 0}</span>
  116. <span class="badge-emoji">🔄 Shares: ${c.metrics?.shares || 0}</span>
  117. </div>`;
  118. if (c.user_feedback) {
  119. html += `<div class="card-section"><div class="section-title">🗣️ User Feedback</div>`;
  120. if (Array.isArray(c.user_feedback)) {
  121. html += `<ul>`;
  122. c.user_feedback.forEach(fb => html += `<li>${fb}</li>`);
  123. html += `</ul>`;
  124. } else {
  125. html += `<p>${c.user_feedback}</p>`;
  126. }
  127. html += `</div>`;
  128. }
  129. if (c.workflow_process) {
  130. html += `<div class="card-section"><div class="section-title">🧱 Workflow Process</div>`;
  131. if (Array.isArray(c.workflow_process)) {
  132. html += `<div class="phase-list">`;
  133. c.workflow_process.forEach(wp => html += `<div class="phase-item">${wp}</div>`);
  134. html += `</div>`;
  135. } else {
  136. html += `<p>${c.workflow_process}</p>`;
  137. }
  138. html += `</div>`;
  139. }
  140. if (c.images && c.images.length > 0) {
  141. html += `<div class="card-section"><div class="section-title">🖼️ Images</div><div class="image-gallery">`;
  142. c.images.forEach(img => {
  143. const url = typeof img === 'string' ? img : img.url;
  144. const desc = (typeof img === 'object' ? img.description : '') || '';
  145. html += `<div class="image-item"><img src="${url}" alt="${desc}" title="${desc}"><div class="image-caption">${desc}</div></div>`;
  146. });
  147. html += `</div></div>`;
  148. }
  149. html += `</div></div>`;
  150. });
  151. }
  152. html += `</div>`;
  153. });
  154. html += `</div>`;
  155. return html;
  156. }
  157. function renderCapabilities(capsObj) {
  158. if (!capsObj || !capsObj.extracted_capabilities) return 'No capabilities data';
  159. const caps = capsObj.extracted_capabilities;
  160. if (caps.length === 0) return '<p>No capabilities extracted.</p>';
  161. let html = ``;
  162. caps.forEach(cap => {
  163. const isNew = cap.is_new ? '<span class="badge-emoji success">✨ New Capability</span>' : '';
  164. html += `<div class="data-card">
  165. <div class="card-header">
  166. <div class="card-title">⚡ [${cap.id || 'N/A'}] ${cap.name || 'Unnamed'}</div>
  167. ${isNew}
  168. </div>
  169. <div class="card-body">
  170. <p>${cap.description || ''}</p>
  171. <div class="card-section"><div class="section-title">✨ Effects</div><ul>`;
  172. if (cap.effects) cap.effects.forEach(eff => html += `<li>${eff}</li>`);
  173. html += `</ul></div>`;
  174. if (cap.implements && Object.keys(cap.implements).length > 0) {
  175. html += `<div class="card-section"><div class="section-title">🛠️ Implements Tools</div><div class="tags-container">`;
  176. for (const [tool, args] of Object.entries(cap.implements)) {
  177. html += `<span class="badge-emoji primary" title="${args}">🔧 ${tool}</span>`;
  178. }
  179. html += `</div></div>`;
  180. }
  181. if (cap.case_references && cap.case_references.length > 0) {
  182. html += `<div class="card-section"><div class="section-title">📌 Source Cases</div><div class="tags-container" style="gap:0.8rem">`;
  183. cap.case_references.forEach(ref => {
  184. let caseId = null;
  185. let title = ref;
  186. const matchA = ref.match(/^case_([a-z]+)_(\d+)(?:[::\s]+(.*))?/);
  187. if (matchA) {
  188. caseId = `${matchA[1]}-case_${matchA[2]}`;
  189. title = matchA[3] || ref;
  190. } else {
  191. const matchB = ref.match(/^([a-z]+)[\/\s](case_\d+)(?:[::\s]+(.*))?/);
  192. if (matchB) {
  193. caseId = `${matchB[1]}-${matchB[2]}`;
  194. title = matchB[3] || ref;
  195. } else {
  196. const matchC = ref.match(/^case_(\d+)_([a-z]+)(?:[::\s]+(.*))?/);
  197. if (matchC) {
  198. caseId = `${matchC[2]}-case_${matchC[1]}`;
  199. title = matchC[3] || ref;
  200. }
  201. }
  202. }
  203. if (caseId) {
  204. 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">
  205. <strong>🔍 ${caseId.replace('-', ' ')}</strong><br>
  206. <span style="font-size:0.75rem">${title.substring(0, 40) + (title.length>40?'...':'')}</span>
  207. </a>`;
  208. } else {
  209. html += `<span class="badge-emoji" style="white-space:normal; text-align:left; line-height:1.4; font-size:0.75rem">${ref}</span>`;
  210. }
  211. });
  212. html += `</div></div>`;
  213. }
  214. html += `</div></div>`;
  215. });
  216. return html;
  217. }
  218. function renderBlueprint(bpObj) {
  219. if (!bpObj || !bpObj.blueprints) return 'No blueprint data';
  220. let html = ``;
  221. if (bpObj.distilled_cases && bpObj.distilled_cases.length > 0) {
  222. html += `<div style="margin-bottom: 1.5rem; padding: 1rem; background: rgba(0,0,0,0.2); border-radius: 8px;">
  223. <h3 style="color:var(--text-main); margin-bottom:0.8rem">📚 Source Cases for Blueprint</h3>
  224. <div class="tags-container" style="gap:0.8rem">`;
  225. bpObj.distilled_cases.forEach(c => {
  226. let targetId = c.id;
  227. const matchA = targetId.match(/^case_([a-z]+)_(\d+)/);
  228. if (matchA) {
  229. targetId = `${matchA[1]}-case_${matchA[2]}`;
  230. } else {
  231. const matchB = targetId.match(/^([a-z]+)[\/\s](case_\d+)/);
  232. if (matchB) {
  233. targetId = `${matchB[1]}-${matchB[2]}`;
  234. } else {
  235. const matchC = targetId.match(/^case_(\d+)_([a-z]+)/);
  236. if (matchC) targetId = `${matchC[2]}-case_${matchC[1]}`;
  237. }
  238. }
  239. 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">
  240. <strong>🔍 ${c.id}</strong><br>
  241. <span style="font-size:0.75rem">${c.title ? c.title.substring(0, 40) + (c.title.length>40?'...':'') : 'View Source'}</span>
  242. </a>`;
  243. });
  244. html += `</div></div>`;
  245. }
  246. bpObj.blueprints.forEach(bp => {
  247. html += `<div class="data-card">
  248. <div class="card-header">
  249. <div class="card-title">🗺️ ${bp.name || 'Unnamed'}</div>
  250. </div>
  251. <div class="card-body">
  252. <div class="card-section"><div class="section-title">🧠 Reasoning</div>
  253. <p>${bp.reasoning || ''}</p></div>
  254. <div class="card-section"><div class="section-title">📍 Phases</div><div class="phase-list">`;
  255. if (bp.phases) bp.phases.forEach(ph => {
  256. html += `<div class="phase-item">
  257. <div class="phase-title">${ph.phase || ''}</div>
  258. <div>${ph.description || ''}</div>
  259. </div>`;
  260. });
  261. html += `</div></div></div>`;
  262. });
  263. return html;
  264. }
  265. function renderStrategy(stratObj) {
  266. if (!stratObj || !stratObj.strategies) return 'No strategy data';
  267. let html = `<div style="margin-bottom: 1.5rem; padding: 1rem; background: rgba(0,0,0,0.2); border-radius: 8px;">
  268. <h3 style="color:var(--text-main); margin-bottom:0.5rem">🎯 Requirement</h3>
  269. <p style="color:var(--text-muted)">${stratObj.requirement || ''}</p>
  270. </div>`;
  271. stratObj.strategies.sort((a,b) => (b.is_selected === true) - (a.is_selected === true));
  272. stratObj.strategies.forEach(strat => {
  273. const isSelected = strat.is_selected;
  274. const icon = isSelected ? '🎯' : '🥈';
  275. const badge = isSelected ? '<span class="badge-emoji success">⭐ Selected Strategy</span>' : '<span class="badge-emoji warning">Alternative</span>';
  276. let scoreHtml = '';
  277. if (strat.coverage_score !== undefined) {
  278. const score = strat.coverage_score;
  279. const deg = score * 360;
  280. const scoreClass = score >= 0.8 ? '' : (score >= 0.5 ? 'medium' : 'low');
  281. scoreHtml = `<div class="score-container">
  282. <div class="score-circle ${scoreClass}" style="--score-deg: ${deg}deg"><span>${Math.round(score * 100)}%</span></div>
  283. <div class="score-text"><strong>Coverage Score</strong><br>${strat.coverage_explanation || ''}</div>
  284. </div>`;
  285. }
  286. html += `<div class="data-card" style="${isSelected ? 'border-color: var(--accent-primary); box-shadow: 0 0 15px rgba(59,130,246,0.1);' : ''}">
  287. <div class="card-header">
  288. <div class="card-title">${icon} ${strat.name || 'Unnamed'}</div>
  289. ${badge}
  290. </div>
  291. <div class="card-body">
  292. ${scoreHtml}
  293. <div class="tags-container" style="margin-bottom:1rem">
  294. <span class="badge-emoji">📥 Source: ${strat.source || 'N/A'}</span>
  295. </div>`;
  296. if (strat.reasoning) html += `<div class="card-section"><div class="section-title">🧠 Reasoning</div><p>${strat.reasoning}</p></div>`;
  297. if (strat.why_not) html += `<div class="card-section"><div class="section-title">❌ Why Not Selected</div><p>${strat.why_not}</p></div>`;
  298. if (strat.workflow_outline && strat.workflow_outline.length > 0) {
  299. html += `<div class="card-section"><div class="section-title">🧱 Workflow Outline</div><div class="phase-list">`;
  300. strat.workflow_outline.forEach(wo => {
  301. html += `<div class="phase-item">
  302. <div class="phase-title">${wo.phase}</div>
  303. <div style="margin-bottom:0.5rem">${wo.description}</div>
  304. <div class="tags-container">`;
  305. if (wo.capabilities) {
  306. wo.capabilities.forEach(cap => html += `<span class="badge-emoji primary">⚡ ${cap.name}</span>`);
  307. }
  308. html += `</div></div>`;
  309. });
  310. html += `</div></div>`;
  311. }
  312. html += `</div></div>`;
  313. });
  314. if (stratObj.uncovered_requirements && stratObj.uncovered_requirements.length > 0) {
  315. html += `<div class="data-card" style="border-color: var(--danger)">
  316. <div class="card-header"><div class="card-title">⚠️ Uncovered Requirements</div></div>
  317. <div class="card-body"><ul>`;
  318. stratObj.uncovered_requirements.forEach(req => html += `<li>${req}</li>`);
  319. html += `</ul></div></div>`;
  320. }
  321. return html;
  322. }
  323. window.selectSubTab = function(p) {
  324. document.querySelectorAll('.sub-tab-btn').forEach(b => {
  325. b.classList.remove('active');
  326. if(b.textContent === p.replace('case_', '').toUpperCase()) b.classList.add('active');
  327. });
  328. document.querySelectorAll('.sub-tab-pane').forEach(pane => pane.classList.add('hidden'));
  329. const target = document.getElementById('sub-tab-' + p);
  330. if(target) target.classList.remove('hidden');
  331. };
  332. window.jumpToCase = function(caseId) {
  333. // Switch to raw tab
  334. document.querySelector('.tab-btn[data-target="tab-raw"]').click();
  335. // Find the case card
  336. const targetCard = document.getElementById('case-card-' + caseId);
  337. if (targetCard) {
  338. // Find which sub-tab pane it's inside
  339. const pane = targetCard.closest('.sub-tab-pane');
  340. if (pane) {
  341. const platformId = pane.id.replace('sub-tab-', '');
  342. selectSubTab(platformId);
  343. }
  344. // Scroll into view
  345. targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
  346. // Add highlight
  347. targetCard.classList.remove('highlight-pulse');
  348. void targetCard.offsetWidth; // Trigger reflow
  349. targetCard.classList.add('highlight-pulse');
  350. } else {
  351. alert("Case not found in raw cases data.");
  352. }
  353. };
  354. async function fetchMemo(index) {
  355. const elTextarea = document.getElementById('memo-textarea');
  356. const elStatus = document.getElementById('memo-status');
  357. if(!elTextarea) return;
  358. elTextarea.value = 'Loading...';
  359. elTextarea.disabled = true;
  360. try {
  361. const res = await fetch(`/api/requirements/${index}/memo`);
  362. const data = await res.json();
  363. elTextarea.value = data.memo || '';
  364. elStatus.textContent = '';
  365. } catch (e) {
  366. elTextarea.value = '';
  367. console.error("Failed to fetch memo", e);
  368. }
  369. elTextarea.disabled = false;
  370. }
  371. async function fetchPromptsList() {
  372. try {
  373. const res = await fetch('/api/prompts');
  374. const list = await res.json();
  375. if(!elPromptList) return;
  376. elPromptList.innerHTML = '';
  377. list.forEach((p, idx) => {
  378. const div = document.createElement('div');
  379. div.className = 'prompt-tab';
  380. div.textContent = p;
  381. div.onclick = () => selectPrompt(p, div);
  382. elPromptList.appendChild(div);
  383. if (idx === 0) selectPrompt(p, div);
  384. });
  385. } catch (e) {
  386. console.error("Failed to load prompts", e);
  387. }
  388. }
  389. async function selectPrompt(name, tabEl) {
  390. currentPromptName = name;
  391. document.querySelectorAll('.prompt-tab').forEach(el => el.classList.remove('active'));
  392. tabEl.classList.add('active');
  393. elPromptTextarea.value = 'Loading...';
  394. elPromptTextarea.disabled = true;
  395. try {
  396. const res = await fetch(`/api/prompts/${name}`);
  397. const data = await res.json();
  398. elPromptTextarea.value = data.content || '';
  399. } catch (e) {
  400. elPromptTextarea.value = 'Error loading prompt.';
  401. }
  402. elPromptTextarea.disabled = false;
  403. elPromptStatus.textContent = '';
  404. }
  405. async function fetchRequirementData(index) {
  406. try {
  407. const res = await fetch(`/api/requirements/${index}/data`);
  408. const data = await res.json();
  409. jsonStrategy.innerHTML = data.strategy ? renderStrategy(data.strategy) : '<p style="color:var(--text-muted)">No strategy data</p>';
  410. jsonBlueprint.innerHTML = data.blueprint ? renderBlueprint(data.blueprint) : '<p style="color:var(--text-muted)">No blueprint data</p>';
  411. jsonCaps.innerHTML = data.capabilities ? renderCapabilities(data.capabilities) : '<p style="color:var(--text-muted)">No capabilities data</p>';
  412. jsonRaw.innerHTML = data.raw_cases ? renderRawCases(data.raw_cases) : '<p style="color:var(--text-muted)">No raw cases data</p>';
  413. } catch (e) {
  414. console.error("Failed to fetch data", e);
  415. }
  416. }
  417. async function pollStatus() {
  418. try {
  419. const res = await fetch('/api/pipeline/status');
  420. const statusData = await res.json();
  421. let needsListUpdate = false;
  422. // Check if any status changed
  423. for(const [idxStr, runInfo] of Object.entries(statusData)) {
  424. const idx = parseInt(idxStr);
  425. if (!activeRuns[idx] || activeRuns[idx].status !== runInfo.status) {
  426. needsListUpdate = true;
  427. }
  428. activeRuns[idx] = runInfo;
  429. // Update logs if modal is open for this index
  430. if (currentSelectedIndex === idx && !modalLogs.classList.contains('hidden')) {
  431. terminalLogs.textContent = runInfo.logs.join('');
  432. terminalLogs.scrollTop = terminalLogs.scrollHeight;
  433. }
  434. // Update detail view banner if this is the selected one
  435. if (currentSelectedIndex === idx) {
  436. updateDetailBannerStatus(runInfo.status);
  437. }
  438. }
  439. if (needsListUpdate) {
  440. // update in requirements array
  441. requirements.forEach(req => {
  442. if (activeRuns[req.index]) {
  443. req.status = activeRuns[req.index].status;
  444. }
  445. });
  446. renderTaskList(requirements);
  447. }
  448. } catch (e) {
  449. console.error("Failed to poll status", e);
  450. }
  451. }
  452. function startStatusPolling() {
  453. if (statusInterval) clearInterval(statusInterval);
  454. statusInterval = setInterval(pollStatus, 2000);
  455. }
  456. // Render
  457. function renderTaskList(list) {
  458. elTaskList.innerHTML = '';
  459. list.forEach(req => {
  460. const div = document.createElement('div');
  461. div.className = `task-item ${currentSelectedIndex === req.index ? 'active' : ''}`;
  462. div.onclick = () => selectRequirement(req.index);
  463. let statusTag = '';
  464. if (req.status === 'running') statusTag = '<span class="tag running">Running</span>';
  465. else if (req.status === 'completed') statusTag = '<span class="tag success">Complete</span>';
  466. else if (req.status === 'partial') statusTag = '<span class="tag warning">Partial</span>';
  467. else statusTag = '<span class="tag">Pending</span>';
  468. let memoHtml = '';
  469. if (req.memo && req.memo.trim() !== '') {
  470. memoHtml = `<div class="task-memo" title="${req.memo}">${req.memo}</div>`;
  471. }
  472. div.innerHTML = `
  473. <div class="task-id">#${req.id}</div>
  474. <div class="task-req" title="${req.requirement}">${req.requirement}</div>
  475. ${memoHtml}
  476. <div class="task-tags">
  477. ${statusTag}
  478. <span class="tag">Cases: ${req.raw_cases_count}</span>
  479. </div>
  480. `;
  481. elTaskList.appendChild(div);
  482. });
  483. }
  484. function updateStats() {
  485. const total = requirements.length;
  486. const completed = requirements.filter(r => r.status === 'completed').length;
  487. const running = requirements.filter(r => r.status === 'running').length;
  488. elStatsContainer.innerHTML = `
  489. <span>Total: ${total}</span>
  490. <span style="color:var(--success)">Done: ${completed}</span>
  491. ${running > 0 ? `<span style="color:var(--accent-primary)">Running: ${running}</span>` : ''}
  492. `;
  493. }
  494. function selectRequirement(index) {
  495. currentSelectedIndex = index;
  496. const req = requirements.find(r => r.index === index);
  497. if (!req) return;
  498. // Update List UI
  499. document.querySelectorAll('.task-item').forEach(el => el.classList.remove('active'));
  500. // We re-render to be safe, but simple class toggle is better.
  501. renderTaskList(requirements);
  502. // Update Detail UI
  503. elEmptyState.classList.add('hidden');
  504. elDetailView.classList.remove('hidden');
  505. elDetailId.textContent = req.id;
  506. elDetailTitle.textContent = req.requirement;
  507. updateDetailBannerStatus(activeRuns[index] ? activeRuns[index].status : req.status);
  508. // Fetch data
  509. jsonStrategy.textContent = 'Loading...';
  510. jsonBlueprint.textContent = 'Loading...';
  511. jsonCaps.textContent = 'Loading...';
  512. jsonRaw.textContent = 'Loading...';
  513. fetchRequirementData(index);
  514. fetchMemo(index);
  515. }
  516. function updateDetailBannerStatus(status) {
  517. if (status === 'running') {
  518. elStatusBanner.classList.remove('hidden');
  519. elStatusBanner.style.background = 'rgba(59, 130, 246, 0.1)';
  520. elStatusText.textContent = 'Pipeline is currently running...';
  521. elStatusBanner.querySelector('.status-indicator').style.display = 'block';
  522. elStatusBanner.querySelector('.status-indicator').style.background = 'var(--accent-primary)';
  523. } else if (status === 'failed') {
  524. elStatusBanner.classList.remove('hidden');
  525. elStatusBanner.style.background = 'rgba(239, 68, 68, 0.1)';
  526. elStatusText.textContent = 'Pipeline run failed.';
  527. elStatusBanner.querySelector('.status-indicator').style.display = 'block';
  528. elStatusBanner.querySelector('.status-indicator').style.background = 'var(--danger)';
  529. } else {
  530. elStatusBanner.classList.add('hidden');
  531. }
  532. }
  533. // Actions
  534. async function triggerRun() {
  535. if (currentSelectedIndex === null) return;
  536. const requestData = {
  537. platforms: document.getElementById('input-platforms').value,
  538. skip_research: false,
  539. research_only: false,
  540. use_claude_sdk: document.getElementById('check-claude-sdk').checked,
  541. restart_mode: document.getElementById('select-force-phase').value
  542. };
  543. modalRun.classList.add('hidden');
  544. // Optimistic UI update
  545. const req = requirements.find(r => r.index === currentSelectedIndex);
  546. if (req) req.status = 'running';
  547. activeRuns[currentSelectedIndex] = { status: 'running', logs: ['Initializing request...'] };
  548. renderTaskList(requirements);
  549. updateDetailBannerStatus('running');
  550. try {
  551. const res = await fetch(`/api/pipeline/run/${currentSelectedIndex}`, {
  552. method: 'POST',
  553. headers: { 'Content-Type': 'application/json' },
  554. body: JSON.stringify(requestData)
  555. });
  556. if (!res.ok) {
  557. const err = await res.json();
  558. alert("Error: " + err.detail);
  559. }
  560. } catch (e) {
  561. console.error("Run failed", e);
  562. alert("Failed to trigger run");
  563. }
  564. }
  565. // Event Listeners
  566. function setupEventListeners() {
  567. // Search
  568. elSearchInput.addEventListener('input', (e) => {
  569. const query = e.target.value.toLowerCase();
  570. const filtered = requirements.filter(r =>
  571. r.requirement.toLowerCase().includes(query) ||
  572. r.id.includes(query)
  573. );
  574. renderTaskList(filtered);
  575. });
  576. // Tabs
  577. document.querySelectorAll('.tab-btn').forEach(btn => {
  578. btn.addEventListener('click', () => {
  579. document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
  580. document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
  581. btn.classList.add('active');
  582. document.getElementById(btn.dataset.target).classList.add('active');
  583. });
  584. });
  585. // Modals
  586. document.getElementById('btn-open-run-modal').addEventListener('click', () => {
  587. if (currentSelectedIndex !== null) {
  588. modalRun.classList.remove('hidden');
  589. }
  590. });
  591. document.getElementById('btn-close-modal').addEventListener('click', () => {
  592. modalRun.classList.add('hidden');
  593. });
  594. document.getElementById('btn-cancel-run').addEventListener('click', () => {
  595. modalRun.classList.add('hidden');
  596. });
  597. document.getElementById('btn-confirm-run').addEventListener('click', triggerRun);
  598. document.getElementById('btn-view-logs').addEventListener('click', () => {
  599. modalLogs.classList.remove('hidden');
  600. if (activeRuns[currentSelectedIndex]) {
  601. terminalLogs.textContent = activeRuns[currentSelectedIndex].logs.join('');
  602. terminalLogs.scrollTop = terminalLogs.scrollHeight;
  603. } else {
  604. terminalLogs.textContent = 'No logs available.';
  605. }
  606. });
  607. document.getElementById('btn-close-logs').addEventListener('click', () => {
  608. modalLogs.classList.add('hidden');
  609. });
  610. const btnSaveMemo = document.getElementById('btn-save-memo');
  611. if (btnSaveMemo) {
  612. btnSaveMemo.addEventListener('click', async () => {
  613. if (currentSelectedIndex === null) return;
  614. const elTextarea = document.getElementById('memo-textarea');
  615. const elStatus = document.getElementById('memo-status');
  616. elStatus.textContent = 'Saving...';
  617. elStatus.style.color = 'var(--text-muted)';
  618. try {
  619. const res = await fetch(`/api/requirements/${currentSelectedIndex}/memo`, {
  620. method: 'POST',
  621. headers: { 'Content-Type': 'application/json' },
  622. body: JSON.stringify({ memo: elTextarea.value })
  623. });
  624. if (res.ok) {
  625. elStatus.textContent = 'Saved!';
  626. elStatus.style.color = 'var(--success)';
  627. setTimeout(() => elStatus.textContent = '', 2000);
  628. const req = requirements.find(r => r.index === currentSelectedIndex);
  629. if (req) {
  630. req.memo = elTextarea.value;
  631. renderTaskList(requirements);
  632. }
  633. } else {
  634. throw new Error("Bad response");
  635. }
  636. } catch (e) {
  637. console.error("Failed to save memo", e);
  638. elStatus.textContent = 'Save failed';
  639. elStatus.style.color = 'var(--danger)';
  640. }
  641. });
  642. }
  643. const btnOpenPrompts = document.getElementById('btn-open-prompts');
  644. if (btnOpenPrompts) {
  645. btnOpenPrompts.addEventListener('click', () => {
  646. modalPrompts.classList.remove('hidden');
  647. fetchPromptsList();
  648. });
  649. }
  650. const btnClosePrompts = document.getElementById('btn-close-prompts');
  651. if (btnClosePrompts) {
  652. btnClosePrompts.addEventListener('click', () => {
  653. modalPrompts.classList.add('hidden');
  654. });
  655. }
  656. const btnSavePrompt = document.getElementById('btn-save-prompt');
  657. if (btnSavePrompt) {
  658. btnSavePrompt.addEventListener('click', async () => {
  659. if (!currentPromptName) return;
  660. elPromptStatus.textContent = 'Saving...';
  661. elPromptStatus.style.color = 'var(--text-muted)';
  662. try {
  663. const res = await fetch(`/api/prompts/${currentPromptName}`, {
  664. method: 'POST',
  665. headers: { 'Content-Type': 'application/json' },
  666. body: JSON.stringify({ content: elPromptTextarea.value })
  667. });
  668. if (res.ok) {
  669. elPromptStatus.textContent = 'Saved!';
  670. elPromptStatus.style.color = 'var(--success)';
  671. setTimeout(() => elPromptStatus.textContent = '', 2000);
  672. } else {
  673. throw new Error("Failed to save");
  674. }
  675. } catch(e) {
  676. elPromptStatus.textContent = 'Save failed';
  677. elPromptStatus.style.color = 'var(--danger)';
  678. }
  679. });
  680. }
  681. }
  682. // Boot
  683. init();