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 `null`; if (typeof obj === 'number') return `${obj}`; if (typeof obj === 'boolean') return `${obj}`; if (typeof obj === 'string') { const escaped = obj.replace(//g, '>'); return `"${escaped}"`; } if (Array.isArray(obj)) { if (obj.length === 0) return '[]'; let html = '
[
'; obj.forEach((val, i) => { html += `
${renderJSON(val)}${i < obj.length - 1 ? ',' : ''}
`; }); html += '
]
'; return html; } if (typeof obj === 'object') { const keys = Object.keys(obj); if (keys.length === 0) return '{}'; let html = '
{
'; keys.forEach((k, i) => { html += `
"${k}": ${renderJSON(obj[k])}${i < keys.length - 1 ? ',' : ''}
`; }); html += '
}
'; return html; } return String(obj); } function renderDataOrRaw(dataObj, renderFunc) { if (!dataObj) return '

No data available

'; if (dataObj.error) { const safeRaw = dataObj.raw_content ? dataObj.raw_content.replace(//g, ">") : "Empty file."; return `

⚠️ JSON Parsing Failed

${safeRaw}
`; } return renderFunc(dataObj); } function renderRawCases(rawCasesObj) { if (!rawCasesObj || Object.keys(rawCasesObj).length === 0) return 'No raw cases data'; const platforms = Object.keys(rawCasesObj); let html = `
`; platforms.forEach((p, i) => { const name = p.replace('case_', '').toUpperCase(); html += ``; }); html += `
`; platforms.forEach((p, i) => { html += `
`; 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 += `
📝 ${c.title || 'Untitled'}
🔗 View Source
📱 Platform: ${c.platform || p} ❤️ Likes: ${c.metrics?.likes || 0} 💬 Comments: ${c.metrics?.comments || 0} 🔄 Shares: ${c.metrics?.shares || 0}
`; if (c.user_feedback) { html += `
🗣️ User Feedback
`; if (Array.isArray(c.user_feedback)) { html += `
    `; c.user_feedback.forEach(fb => html += `
  • ${fb}
  • `); html += `
`; } else { html += `

${c.user_feedback}

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

${c.workflow_process}

`; } html += `
`; } if (c.images && c.images.length > 0) { html += `
🖼️ Images
`; } html += `
`; }); } html += `
`; }); html += ``; return html; } function renderSourceCases(sourceObj) { if (!sourceObj) return '

No source data available

'; if (sourceObj.error) { const safeRaw = sourceObj.raw_content ? sourceObj.raw_content.replace(//g, ">") : "Empty file."; return `

⚠️ JSON Parsing Failed

${safeRaw}
`; } if (!sourceObj.sources) { return `

⚠️ Non-standard source format

${renderJSON(sourceObj)}
`; } const sources = sourceObj.sources; let html = ``; html += `
`; // Container for searching if (sources.length === 0) { html += `

Source file is empty.

`; } else { sources.forEach((s, idx) => { const post = s.post || {}; let mediaHtml = ''; // Handle images (XHS uses images string array, X uses image_url_list object array) const images = post.images || []; const xImages = post.image_url_list || []; const allImages = [...images, ...xImages.map(img => img.image_url)].filter(Boolean); if (allImages.length > 0) { mediaHtml += `
`; allImages.forEach(imgUrl => { mediaHtml += ``; }); mediaHtml += `
`; } // Handle videos const videos = post.videos || []; const xVideos = post.video_url_list || []; const allVideos = [...videos, ...xVideos.map(vid => vid.video_url)].filter(Boolean); if (allVideos.length > 0) { mediaHtml += `
`; allVideos.forEach(vidUrl => { mediaHtml += ``; }); mediaHtml += `
`; } html += `
📝 [${s.platform || post.channel || 'Unknown'}] ${post.title || 'Untitled'}
${s.source_url ? `🔗 View Source` : ''}
📱 Platform: ${s.platform || post.channel || 'N/A'} ❤️ Likes: ${post.like_count || 0} ${s.channel_content_id ? `🆔 ID: ${s.channel_content_id}` : ''}
${post.body_text || 'No content available.'}
${mediaHtml}
`; }); } html += `
`; return html; } window.selectSubTab = function(p) { document.querySelectorAll('#json-raw .sub-tab-btn').forEach(btn => { btn.classList.remove('active'); if(btn.textContent === p.replace('case_', '').toUpperCase()) btn.classList.add('active'); }); document.querySelectorAll('#json-raw .sub-tab-pane').forEach(pane => { pane.classList.add('hidden'); }); const target = document.getElementById(`sub-tab-${p}`); if (target) target.classList.remove('hidden'); }; window.filterSources = function(query) { const q = query.toLowerCase(); const cards = document.querySelectorAll('#sub-tab-source .data-card'); cards.forEach(card => { if (card.textContent.toLowerCase().includes(q)) { card.style.display = 'block'; } else { card.style.display = 'none'; } }); }; function renderCapabilities(capsObj) { if (!capsObj || !capsObj.extracted_capabilities) return 'No capabilities data'; const caps = capsObj.extracted_capabilities; if (caps.length === 0) return '

No capabilities extracted.

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

${cap.description || ''}

✨ Effects
    `; if (cap.effects) cap.effects.forEach(eff => html += `
  • ${eff}
  • `); html += `
`; if (cap.implements && Object.keys(cap.implements).length > 0) { html += `
🛠️ Implements Tools
`; for (const [tool, args] of Object.entries(cap.implements)) { html += `🔧 ${tool}`; } html += `
`; } if (cap.case_references && cap.case_references.length > 0) { html += `
📌 Source Cases
`; cap.case_references.forEach(ref => { let caseId = null; let title = ref; const matchA = ref.match(/^case_([a-z]+)_(\d+)(?:[::\s]+(.*))?/); if (matchA) { caseId = `${matchA[1]}-case_${matchA[2]}`; title = matchA[3] || ref; } else { const matchB = ref.match(/^([a-z]+)[\/\s](case_\d+)(?:[::\s]+(.*))?/); if (matchB) { caseId = `${matchB[1]}-${matchB[2]}`; title = matchB[3] || ref; } else { const matchC = ref.match(/^case_(\d+)_([a-z]+)(?:[::\s]+(.*))?/); if (matchC) { caseId = `${matchC[2]}-case_${matchC[1]}`; title = matchC[3] || ref; } } } if (caseId) { html += ` 🔍 ${caseId.replace('-', ' ')}
${title.substring(0, 40) + (title.length>40?'...':'')}
`; } else { html += `${ref}`; } }); html += `
`; } html += `
`; }); return html; } function renderBlueprint(bpObj) { if (!bpObj || !bpObj.blueprints) return 'No blueprint data'; let html = ``; if (bpObj.distilled_cases && bpObj.distilled_cases.length > 0) { html += `

📚 Source Cases for Blueprint

`; bpObj.distilled_cases.forEach(c => { let targetId = c.id; const matchA = targetId.match(/^case_([a-z]+)_(\d+)/); if (matchA) { targetId = `${matchA[1]}-case_${matchA[2]}`; } else { const matchB = targetId.match(/^([a-z]+)[\/\s](case_\d+)/); if (matchB) { targetId = `${matchB[1]}-${matchB[2]}`; } else { const matchC = targetId.match(/^case_(\d+)_([a-z]+)/); if (matchC) targetId = `${matchC[2]}-case_${matchC[1]}`; } } html += ` 🔍 ${c.id}
${c.title ? c.title.substring(0, 40) + (c.title.length>40?'...':'') : 'View Source'}
`; }); html += `
`; } bpObj.blueprints.forEach(bp => { html += `
🗺️ ${bp.name || 'Unnamed'}
🧠 Reasoning

${bp.reasoning || ''}

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

🎯 Requirement

${stratObj.requirement || ''}

`; stratObj.strategies.sort((a,b) => (b.is_selected === true) - (a.is_selected === true)); stratObj.strategies.forEach(strat => { const isSelected = strat.is_selected; const icon = isSelected ? '🎯' : '🥈'; const badge = isSelected ? '⭐ Selected Strategy' : 'Alternative'; let scoreHtml = ''; if (strat.coverage_score !== undefined) { const score = strat.coverage_score; const deg = score * 360; const scoreClass = score >= 0.8 ? '' : (score >= 0.5 ? 'medium' : 'low'); scoreHtml = `
${Math.round(score * 100)}%
Coverage Score
${strat.coverage_explanation || ''}
`; } html += `
${icon} ${strat.name || 'Unnamed'}
${badge}
${scoreHtml}
📥 Source: ${strat.source || 'N/A'}
`; if (strat.reasoning) html += `
🧠 Reasoning

${strat.reasoning}

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

${strat.why_not}

`; if (strat.workflow_outline && strat.workflow_outline.length > 0) { html += `
🧱 Workflow Outline
`; strat.workflow_outline.forEach(wo => { html += `
${wo.phase}
${wo.description}
`; if (wo.capabilities) { wo.capabilities.forEach(cap => html += `⚡ ${cap.name}`); } html += `
`; }); html += `
`; } html += `
`; }); if (stratObj.uncovered_requirements && stratObj.uncovered_requirements.length > 0) { html += `
⚠️ Uncovered Requirements
    `; stratObj.uncovered_requirements.forEach(req => html += `
  • ${req}
  • `); html += `
`; } return html; } window.selectSubTab = function(p) { document.querySelectorAll('.sub-tab-btn').forEach(b => { b.classList.remove('active'); if(b.textContent === p.replace('case_', '').toUpperCase()) b.classList.add('active'); }); document.querySelectorAll('.sub-tab-pane').forEach(pane => pane.classList.add('hidden')); const target = document.getElementById('sub-tab-' + p); if(target) target.classList.remove('hidden'); }; window.jumpToCase = function(caseId) { // Switch to raw tab document.querySelector('.tab-btn[data-target="tab-raw"]').click(); // Find the case card const targetCard = document.getElementById('case-card-' + caseId); if (targetCard) { // Find which sub-tab pane it's inside const pane = targetCard.closest('.sub-tab-pane'); if (pane) { const platformId = pane.id.replace('sub-tab-', ''); selectSubTab(platformId); } // Scroll into view targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Add highlight targetCard.classList.remove('highlight-pulse'); void targetCard.offsetWidth; // Trigger reflow targetCard.classList.add('highlight-pulse'); } else { alert("Case not found in raw cases data."); } }; async function fetchMemo(index) { const elTextarea = document.getElementById('memo-textarea'); const elStatus = document.getElementById('memo-status'); if(!elTextarea) return; elTextarea.value = 'Loading...'; elTextarea.disabled = true; try { const res = await fetch(`/api/requirements/${index}/memo`); const data = await res.json(); elTextarea.value = data.memo || ''; elStatus.textContent = ''; } catch (e) { elTextarea.value = ''; console.error("Failed to fetch memo", e); } elTextarea.disabled = false; } async function fetchPromptsList() { try { const res = await fetch('/api/prompts'); const list = await res.json(); if(!elPromptList) return; elPromptList.innerHTML = ''; list.forEach((p, idx) => { const div = document.createElement('div'); div.className = 'prompt-tab'; div.textContent = p; div.onclick = () => selectPrompt(p, div); elPromptList.appendChild(div); if (idx === 0) selectPrompt(p, div); }); } catch (e) { console.error("Failed to load prompts", e); } } async function selectPrompt(name, tabEl) { currentPromptName = name; document.querySelectorAll('.prompt-tab').forEach(el => el.classList.remove('active')); tabEl.classList.add('active'); elPromptTextarea.value = 'Loading...'; elPromptTextarea.disabled = true; try { const res = await fetch(`/api/prompts/${name}`); const data = await res.json(); elPromptTextarea.value = data.content || ''; } catch (e) { elPromptTextarea.value = 'Error loading prompt.'; } elPromptTextarea.disabled = false; elPromptStatus.textContent = ''; } async function fetchRequirementData(index) { try { const res = await fetch(`/api/requirements/${index}/data`); const data = await res.json(); jsonStrategy.innerHTML = 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();