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 elSchemaTextarea = document.getElementById('schema-textarea'); const elPromptStatus = document.getElementById('prompt-save-status'); // DOM Elements const elReqSelector = document.getElementById('req-selector'); 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 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'); const PIPELINE_STEPS = [ { id: 'research', label: '1.1 分布式爬取' }, { id: 'source', label: '1.5 提取数据源' }, { id: 'generate-case', label: '1.6 生成 case.json' }, { id: 'workflow-extract', label: '1.6a 工作流提取' }, { id: 'capability-extract-1', label: '1.6b 原子能力提取' }, { id: 'apply-grounding', label: '1.7 场景映射' }, { id: 'process-cluster', label: '2.1.1 工序聚类' }, { id: 'process-score', label: '2.1.2 工序打分' }, { id: 'capability-extract', label: '2.2.1 能力提取' }, { id: 'capability-enrich', label: '2.2.2 能力丰富化' }, { id: 'strategy', label: '3.0 策略组装' } ]; let chainStartNode = null; let chainEndNode = null; let currentPipelineStatus = {}; // Initialize async function init() { await fetchRequirements(); setupEventListeners(); startStatusPolling(); } // Fetch Data async function fetchRequirements() { try { const res = await fetch('/api/requirements'); requirements = await res.json(); requirements.sort((a, b) => b.index - a.index); renderTaskList(requirements); updateStats(); } catch (e) { console.error("Failed to fetch requirements", e); elReqSelector.innerHTML = ''; } } 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 '

无可用数据

'; let safeRaw = ""; if (dataObj.error) { safeRaw = dataObj.raw_content ? dataObj.raw_content.replace(//g, ">") : "文件为空。"; return `

⚠️ JSON 解析失败

${safeRaw}
`; } else { safeRaw = JSON.stringify(dataObj, null, 2).replace(//g, ">"); } // Check global toggle state const isRaw = document.getElementById('global-json-toggle') && document.getElementById('global-json-toggle').checked; return `
${renderFunc(dataObj)}

📝 JSON 原文

${safeRaw}
`; } function renderRawCases(rawCasesObj) { if (!rawCasesObj || Object.keys(rawCasesObj).length === 0) return '无原始案例数据'; const reqId = requirements[currentSelectedIndex]?.id || "001"; // Build maps const sourceMap = {}; const sourceData = rawCasesObj['source']; const sourceList = sourceData ? (Array.isArray(sourceData) ? sourceData : sourceData.sources) : null; if (sourceList) { sourceList.forEach(s => { const sId = s.case_id || (s._raw && s._raw.case_id); const sUrl = s.source_url || s.url; if (sId) sourceMap[sId] = s; if (sUrl) sourceMap[sUrl] = s; }); } const detailMap = {}; const detailMapByUrl = {}; // Cache context for modal window._currentRawCasesContext = { rawCasesObj, sourceMap, detailMap, detailMapByUrl, reqId }; let statsHtml = ''; const detailedCaseObj = rawCasesObj['case'] || rawCasesObj['case_detailed']; if (detailedCaseObj) { const cd = detailedCaseObj; let calcWorkflow = 0; let calcCapabilities = 0; if (cd.cases) { cd.cases.forEach(c => { const cId = c.case_id || (c._raw && c._raw.case_id); const cUrl = c.source_url || c.url; if (c.workflow) calcWorkflow++; if (c.capabilities && c.capabilities.length > 0) calcCapabilities++; if (cId) { if (c.workflow) detailMap[cId] = { ...detailMap[cId], workflow: c.workflow }; if (c.capabilities) detailMap[cId] = { ...detailMap[cId], capabilities: c.capabilities }; } if (cUrl) { if (c.workflow) detailMapByUrl[cUrl] = { ...detailMapByUrl[cUrl], workflow: c.workflow }; if (c.capabilities) detailMapByUrl[cUrl] = { ...detailMapByUrl[cUrl], capabilities: c.capabilities }; } }); } if (cd.total !== undefined) { const displayWorkflowSuccess = cd.workflow_success !== undefined && cd.workflow_success !== null ? cd.workflow_success : (cd.success !== undefined && cd.success !== null ? cd.success : calcWorkflow); const displayCapabilitiesSuccess = cd.capabilities_success !== undefined && cd.capabilities_success !== null ? cd.capabilities_success : calcCapabilities; statsHtml = `
${cd.total} 帖子总计
${displayWorkflowSuccess} 工序提取成功数
${displayCapabilitiesSuccess} 能力提取成功数
`; } } const allPlatforms = Object.keys(rawCasesObj).filter(p => p !== 'source' && p !== 'case_detailed' && p !== 'case' && p !== 'images'); const channelPlatforms = allPlatforms.filter(p => p !== 'filtered_cases' && p !== 'source_ex'); const hasFiltered = allPlatforms.includes('filtered_cases'); const hasExternal = allPlatforms.includes('source_ex'); let html = statsHtml; html += `
`; html += ``; if (hasFiltered) html += ``; if (hasExternal) html += ``; html += ``; html += ``; html += `
`; const renderPaneContent = (pList) => { let paneHtml = ''; let totalCases = 0; let gridHtml = ''; let seenIds = new Set(); pList.forEach(p => { if (!rawCasesObj[p]) return; if (rawCasesObj[p].error) { const safeRaw = rawCasesObj[p].raw_content ? rawCasesObj[p].raw_content.replace(//g, ">") : "文件为空。"; paneHtml += `

⚠️ ${p} JSON 解析失败

${safeRaw}
`; return; } if (rawCasesObj[p].reason) { paneHtml += `
🛑 ${p} 过滤原因: ${rawCasesObj[p].reason}
`; } const cases = Array.isArray(rawCasesObj[p]) ? rawCasesObj[p] : (rawCasesObj[p].cases || rawCasesObj[p].sources || []); if (cases.length > 0) { if (!rawCasesObj['source'] && p !== 'source_ex' && p !== 'filtered_cases' && p !== 'source') { paneHtml += `

📝 ${p} 原始爬取数据 (未进行 1.5 数据源提取)

${renderJSON(rawCasesObj[p])}
`; return; } cases.forEach((c, idx) => { totalCases++; const cId = c.case_id || (c._raw && c._raw.case_id) || (c.post && c.post.channel_content_id) || `temp_${p}_${idx}`; const cUrl = c.source_url || c.url || (c.post && c.post.link) || ''; if (cId || cUrl || c.post) { const mappedS = sourceMap[cId] || sourceMap[cUrl] || (c._raw && sourceMap[c._raw.case_id]); if (p !== 'filtered_cases' && p !== 'source' && p !== 'source_ex' && !mappedS) return; if (cId && seenIds.has(cId)) return; if (cId) seenIds.add(cId); const s = mappedS || c; const post = s.post || s || {}; const images = post.images || []; const xImages = post.image_url_list || []; const ytThumbnails = post.thumbnails && post.thumbnails.length > 0 ? [post.thumbnails[post.thumbnails.length - 1].url] : []; const allImages = [...images, ...xImages.map(img => img.image_url), ...ytThumbnails].filter(Boolean); const coverImgUrl = allImages.length > 0 ? `/output/${reqId}/raw_cases/images/${cId}/00.jpg` : ''; const fallbackImgUrl = allImages.length > 0 ? allImages[0] : ''; const title = post.title || c.title || post.desc || '无标题'; const author = post.channel_account_name || s.author || '-'; const likes = post.like_count !== undefined ? post.like_count : (post.likes !== undefined ? post.likes : '-'); const snippetStr = (post.body_text || post.body || '').substring(0, 100); const snippetHtml = snippetStr ? `
${snippetStr.replace(//g, ">")}...
` : ''; const platBadge = p.startsWith('case_') ? `${p.replace('case_', '').toUpperCase()}` : ''; let actionBtn = ''; if (p === 'source_ex') { const isImported = !!sourceMap[cId] || !!sourceMap[cUrl]; if (isImported) { actionBtn = ``; } else { actionBtn = ``; } } gridHtml += `
${platBadge} ${actionBtn} ${allImages.length > 0 ? `` : ''}
${title}
${allImages.length === 0 ? snippetHtml : ''}
👤 ${author}
❤️ ${likes}
`; } else { gridHtml += `
📝 旧版格式 / 解析失败
点击查看详情
`; } }); } }); if (totalCases === 0 && pList.length > 0 && !paneHtml.includes('解析失败') && !paneHtml.includes('未进行')) { paneHtml += `
暂无数据
`; } else if (gridHtml) { paneHtml += `
${gridHtml}
`; } return paneHtml; }; html += `
${renderPaneContent([...channelPlatforms, 'source'])}
`; if (hasFiltered) html += ``; if (hasExternal) { html += ``; } html += `
`; return html; } window.importExternalCase = async function(e, caseId) { e.stopPropagation(); if (!confirm('确定要将该外部数据导入到 source 吗?')) return; try { const res = await fetch(`/api/requirements/${currentSelectedIndex}/import_source_ex`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({case_id: caseId}) }); const data = await res.json(); if (data.status === 'success') { fetchRequirementData(currentSelectedIndex); // Refresh data } else { alert('导入失败: ' + (data.detail || data.message || JSON.stringify(data))); } } catch (err) { alert('导入出错: ' + err); } }; window.importAllExternalCases = async function() { if (!confirm('确定要将所有外部数据全部导入到 source 吗?')) return; try { const res = await fetch(`/api/requirements/${currentSelectedIndex}/import_all_source_ex`, { method: 'POST' }); const data = await res.json(); if (data.status === 'success') { alert(`成功导入 ${data.count} 条数据!`); fetchRequirementData(currentSelectedIndex); // Refresh data } else { alert('全部导入失败: ' + (data.detail || data.message || JSON.stringify(data))); } } catch (err) { alert('导入出错: ' + err); } }; 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.selectGenericSubTab = function(prefix, targetId) { const parentContainer = document.getElementById(`container-${prefix}`); if (!parentContainer) return; parentContainer.querySelectorAll('.sub-tab-btn').forEach(btn => { btn.classList.remove('active'); }); const activeBtn = parentContainer.querySelector(`[data-target="${targetId}"]`); if (activeBtn) activeBtn.classList.add('active'); parentContainer.querySelectorAll('.sub-tab-pane').forEach(pane => { pane.classList.add('hidden'); }); const activePane = document.getElementById(targetId); if (activePane) activePane.classList.remove('hidden'); }; function renderWithSubTabs(dataMain, dataTemp, renderFn, tabPrefix) { if (!dataTemp || Object.keys(dataTemp).length === 0) { return renderDataOrRaw(dataMain, renderFn); } const mainHtml = renderDataOrRaw(dataMain, renderFn); const tempHtml = renderDataOrRaw(dataTemp, renderFn); return `
${mainHtml}
`; } function renderCaseTags(caseRefs) { if (!caseRefs || caseRefs.length === 0) return ''; let html = `
`; caseRefs.forEach(ref => { let caseId = null; let title = ref; const matchNew = ref.match(/^([a-z]+)_([^\s::]+)(?:[::\s]+(.*))?/); if (matchNew && matchNew[1] !== 'case') { caseId = `${matchNew[1]}_${matchNew[2]}`; title = matchNew[3] || ref; } else { const matchA = ref.match(/^case_([a-z]+)_([a-zA-Z0-9]+)(?:[::\s]+(.*))?/); if (matchA) { caseId = `${matchA[1]}-case_${matchA[2]}`; title = matchA[3] || ref; } else { const matchB = ref.match(/^([a-z]+)[\/\s](case_[a-zA-Z0-9]+)(?:[::\s]+(.*))?/); if (matchB) { caseId = `${matchB[1]}-${matchB[2]}`; title = matchB[3] || ref; } else { const matchC = ref.match(/^case_([a-zA-Z0-9]+)_([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 += `
`; return html; } function renderCapabilities(capsObj) { if (!capsObj || (!capsObj.extracted_capabilities && !capsObj.capabilities)) { return `
⚠️ 未知或非标准格式 (Capabilities)
${renderJSON(capsObj)}
`; } const caps = capsObj.capabilities || capsObj.extracted_capabilities; if (caps.length === 0) return '

未提取到能力。

'; let html = ``; caps.forEach(cap => { if (!cap.name && !cap.能力名称 && !cap.description && !cap.能力描述 && !cap.id) { html += `
⚠️ 非标准能力项
${renderJSON(cap)}
`; return; } const isNew = cap.is_new ? '✨ 新能力' : ''; html += `
⚡ [${cap.id || 'N/A'}] ${cap.name || cap.能力名称 || '未命名'}
${isNew}

${cap.description || cap.能力描述 || ''}

`; if (cap.enriched_details) { const ed = cap.enriched_details; if (ed.execution_process) { html += `
🚀 执行流程

${ed.execution_process}

`; } if (ed.core_parameters) { html += `
⚙️ 核心参数

${ed.core_parameters}

`; } if (ed.effects) { html += `
✨ 影响效果

${ed.effects}

`; } if (ed.visual_notes) { html += `
🖼️ 视觉备注

${ed.visual_notes}

`; } } else { html += `
✨ 影响 (Effects)
    `; if (cap.effects) cap.effects.forEach(eff => html += `
  • ${eff}
  • `); html += `
`; if (cap.implements && Object.keys(cap.implements).length > 0) { html += `
🛠️ 实现工具 (Tools)
`; for (const [tool, args] of Object.entries(cap.implements)) { html += `🔧 ${tool}`; } html += `
`; } } if (cap.case_references && cap.case_references.length > 0) { html += `
📌 来源案例
${renderCaseTags(cap.case_references)}
`; } html += `
`; }); return html; } function renderBlueprint(bpObj) { if (!bpObj || (!bpObj.blueprints && !bpObj.clusters)) { return `
⚠️ 未知或非标准格式 (Blueprint)
${renderJSON(bpObj)}
`; } let html = ``; // New process.json format if (bpObj.clusters) { html += `

🎯 需求

${bpObj.requirement || ''}

`; bpObj.clusters.forEach(c => { if (!c.cluster_id && !c.cluster_name && !c.工序步骤) { html += `
⚠️ 非标准聚类项
${renderJSON(c)}
`; return; } const score = c.score || 0; const deg = score * 360; const scoreClass = score >= 0.8 ? '' : (score >= 0.5 ? 'medium' : 'low'); html += `
🧩 [${c.cluster_id || 'N/A'}] ${c.cluster_name || '未命名'}
${Math.round(score*100)}%
匹配度得分
🧠 解释 (Explanation)

${c.explanation || ''}

📍 工序步骤
`; if (c.工序步骤) c.工序步骤.forEach(step => { html += `
步骤 ${step.步骤序号}
${step.步骤描述 || ''}
`; }); html += `
`; if (c.关联案例 && c.关联案例.length > 0) { html += `
📌 关联案例
${renderCaseTags(c.关联案例)}
`; } html += `
`; }); return html; } // Old blueprint format if (bpObj.distilled_cases && bpObj.distilled_cases.length > 0) { html += `

📚 蓝图来源案例

`; 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 += `
`; } if (bpObj.blueprints) bpObj.blueprints.forEach(bp => { if (!bp.name && !bp.phases) { html += `
⚠️ 非标准蓝图项
${renderJSON(bp)}
`; return; } html += `
🗺️ ${bp.name || '未命名'}
🧠 推理逻辑 (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 && !stratObj.workflow)) { return `
⚠️ 未知或非标准格式 (Strategy)
${renderJSON(stratObj)}
`; } let html = `

🎯 需求描述

${stratObj.requirement || ''}

`; // New Workflow Format if (stratObj.workflow) { stratObj.workflow.forEach(strat => { if (!strat.cluster_id && !strat.cluster_name && !strat.steps) { html += `
⚠️ 非标准工作流项
${renderJSON(strat)}
`; return; } const score = strat.score || 0; const deg = score * 360; const scoreClass = score >= 0.8 ? '' : (score >= 0.5 ? 'medium' : 'low'); let scoreHtml = `
${Math.round(score * 100)}%
匹配得分
${strat.explanation || ''}
`; html += `
🎯 [${strat.cluster_id}] ${strat.cluster_name || '未命名'}
${scoreHtml} ${(strat.关联案例 && strat.关联案例.length > 0) ? `
📌 关联案例
${renderCaseTags(strat.关联案例)}
` : ''}
🧱 工作流步骤
`; if (strat.steps) strat.steps.forEach(step => { html += `
步骤 ${step.步骤序号}
${step.步骤描述}
`; if (step.capabilities) { step.capabilities.forEach(cap => { html += `
⚡ [${cap.id}] ${cap.name}
${cap.description}
${(cap.case_references && cap.case_references.length > 0) ? `
${renderCaseTags(cap.case_references)}
` : ''}
`; }); } html += `
`; }); html += `
`; }); return html; } if (stratObj.strategies) { stratObj.strategies.sort((a,b) => (b.is_selected === true) - (a.is_selected === true)); stratObj.strategies.forEach(strat => { if (!strat.name && !strat.workflow_outline) { html += `
⚠️ 非标准策略项
${renderJSON(strat)}
`; return; } const isSelected = strat.is_selected; const icon = isSelected ? '🎯' : '🥈'; const badge = isSelected ? '⭐ 被选中的策略' : '备选策略'; 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)}%
覆盖率得分
${strat.coverage_explanation || ''}
`; } html += `
${icon} ${strat.name || '未命名'}
${badge}
${scoreHtml}
📥 来源: ${strat.source || 'N/A'}
`; if (strat.reasoning) html += `
🧠 推理逻辑 (Reasoning)

${strat.reasoning}

`; if (strat.why_not) html += `
❌ 为何未被选中

${strat.why_not}

`; if (strat.workflow_outline && strat.workflow_outline.length > 0) { html += `
🧱 工作流大纲
`; 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-pill[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'); let list = await res.json(); // Handle DAG nodes document.querySelectorAll('.prompt-node').forEach(node => { const p = node.dataset.prompt; if (list.includes(p)) { node.onclick = () => selectPrompt(p, node); // Remove from list so it doesn't appear in "other" list = list.filter(item => item !== p); } else { node.style.opacity = 0.5; // gray out if not found node.style.cursor = 'not-allowed'; } }); const elOtherPromptsList = document.getElementById('other-prompts-list'); if (elOtherPromptsList) { elOtherPromptsList.innerHTML = ''; list.forEach((p) => { const div = document.createElement('div'); div.className = 'prompt-tab'; div.textContent = p; div.onclick = () => selectPrompt(p, div); elOtherPromptsList.appendChild(div); }); } // Select the first prompt by default (maybe researcher.prompt) const firstNode = document.querySelector('.prompt-node[data-prompt="researcher.prompt"]'); if (firstNode) { selectPrompt('researcher.prompt', firstNode); } } 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')); document.querySelectorAll('.prompt-node').forEach(el => el.classList.remove('active')); if (tabEl) tabEl.classList.add('active'); elPromptTextarea.value = 'Loading...'; elPromptTextarea.disabled = true; if (elSchemaTextarea) { elSchemaTextarea.value = 'Loading...'; elSchemaTextarea.disabled = true; } try { const res = await fetch(`/api/prompts/${name}`); const data = await res.json(); elPromptTextarea.value = data.content || ''; if (elSchemaTextarea) { elSchemaTextarea.value = data.schema_content || ''; } } catch (e) { elPromptTextarea.value = 'Error loading prompt.'; if (elSchemaTextarea) elSchemaTextarea.value = ''; } elPromptTextarea.disabled = false; if (elSchemaTextarea) elSchemaTextarea.disabled = false; elPromptStatus.textContent = ''; } function renderAggregatedPerCaseData(cases, type) { if (!cases || !Array.isArray(cases) || cases.length === 0) { return '
暂无案例数据
'; } let sidebarHtml = `
`; sidebarHtml += ``; contentHtml += `
`; if (!hasData) { return `
当前需求的所有案例均无提取的${type === 'workflow' ? '工序' : '能力'}
`; } return `
${sidebarHtml}${contentHtml}
`; } async function fetchRequirementData(index) { try { const res = await fetch(`/api/requirements/${index}/data`); const data = await res.json(); let rawCasesClone = null; let casesList = []; if (data.raw_cases) { rawCasesClone = { ...data.raw_cases }; const detailedCaseObj = data.raw_cases['case'] || data.raw_cases['case_detailed']; if (detailedCaseObj && detailedCaseObj.cases) { casesList = detailedCaseObj.cases; } } if (jsonStrategy) jsonStrategy.innerHTML = ''; // Tab removed jsonBlueprint.innerHTML = renderAggregatedPerCaseData(casesList, 'workflow'); jsonCaps.innerHTML = renderAggregatedPerCaseData(casesList, 'capabilities'); jsonRaw.innerHTML = renderDataOrRaw(rawCasesClone, renderRawCases); const btnUpload = document.getElementById('btn-upload-source-ex'); const fileInput = document.getElementById('input-upload-source-ex'); if (btnUpload && fileInput) { btnUpload.onclick = () => fileInput.click(); fileInput.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; const formData = new FormData(); formData.append('file', file); try { const res = await fetch(`/api/requirements/${index}/upload_source_ex`, { method: 'POST', body: formData }); if (res.ok) { alert('上传成功!'); fetchRequirementData(index); } else { alert('上传失败'); } } catch (err) { console.error(err); alert('上传出错'); } }; } if (rawCasesClone && Object.keys(rawCasesClone).length > 0) { currentAvailablePlatforms = Object.keys(rawCasesClone) .filter(p => p.startsWith('case_') && p !== 'case_detailed' && p !== '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) { elReqSelector.innerHTML = ''; list.forEach(req => { const option = document.createElement('option'); option.value = req.index; let statusMarker = ''; if (req.status === 'running') statusMarker = '🚀'; else if (req.status === 'completed') statusMarker = '✅'; else if (req.status === 'partial') statusMarker = '⚠️'; else statusMarker = '⏳'; option.textContent = `${statusMarker} [#${req.id}] ${req.requirement} (Cases: ${req.raw_cases_count})`; if (currentSelectedIndex === req.index) { option.selected = true; } elReqSelector.appendChild(option); }); } 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; // Sync dropdown if called from somewhere else if (elReqSelector.value != index) { elReqSelector.value = index; } // Update Detail UI elEmptyState.classList.add('hidden'); elDetailView.classList.remove('hidden'); updateDetailBannerStatus(activeRuns[index] ? activeRuns[index].status : req.status); // Fetch data if (jsonStrategy) jsonStrategy.textContent = 'Loading...'; if (jsonBlueprint) jsonBlueprint.textContent = 'Loading...'; if (jsonCaps) jsonCaps.textContent = 'Loading...'; if (jsonRaw) 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 checkSdk = document.getElementById('check-claude-sdk').checked; const mode = document.getElementById('select-force-phase').value; const platforms = document.getElementById('input-platforms').value; let only_step = null; let start_from = null; let end_at = null; let restart_mode = null; let phase = null; if (mode === "custom_range") { if (!chainStartNode) { alert('请在下方链条中选择至少一个节点作为起点。'); return; } if (chainStartNode === chainEndNode || !chainEndNode) { only_step = chainStartNode.replace(/-1$/, ''); } else { start_from = chainStartNode.replace(/-1$/, ''); end_at = chainEndNode.replace(/-1$/, ''); } } else if (mode.startsWith("step_")) { const stepMap = { "step_1.1": "research", "step_1.5": "source", "step_1.6": "generate-case", "step_1.6a": "workflow-extract", "step_1.6b": "capability-extract", "step_1.7": "apply-grounding", "step_2.1.1": "process-cluster", "step_2.1.2": "process-score", "step_2.2.1": "capability-extract", "step_2.2.2": "capability-enrich", "step_3.0": "strategy" }; only_step = stepMap[mode]; if (mode === "step_1.1") { restart_mode = "single_platforms"; // triggers backend's platform specific clearing } } else if (mode.startsWith("phase")) { if (mode === "phase1") { phase = 1; } else if (mode === "phase2") { phase = 2; } else if (mode === "phase3") { phase = 3; } } else { restart_mode = "smart"; } const groupPlatforms = document.getElementById('group-platforms'); const platformsToSend = groupPlatforms && groupPlatforms.style.display !== 'none' ? platforms : ""; const requestData = { platforms: platformsToSend, use_claude_sdk: checkSdk, restart_mode: restart_mode, phase: phase, only_step: only_step, start_from: start_from, end_at: end_at }; 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() { // Global JSON Toggle const globalJsonToggle = document.getElementById('global-json-toggle'); if (globalJsonToggle) { globalJsonToggle.addEventListener('change', (e) => { const isRaw = e.target.checked; document.querySelectorAll('.data-view-ui').forEach(el => el.style.display = isRaw ? 'none' : ''); document.querySelectorAll('.data-view-raw').forEach(el => el.style.display = isRaw ? '' : 'none'); }); } // Dropdown change elReqSelector.addEventListener('change', (e) => { const val = e.target.value; if (val) { selectRequirement(parseInt(val)); } else { currentSelectedIndex = null; elEmptyState.classList.remove('hidden'); elDetailView.classList.add('hidden'); } }); // Toggle Memo document.getElementById('btn-toggle-memo').addEventListener('click', () => { const memo = document.getElementById('memo-container'); memo.classList.toggle('hidden'); }); // Refresh Data without changing page position const btnRefresh = document.getElementById('btn-refresh-data'); if (btnRefresh) { btnRefresh.addEventListener('click', async () => { if (currentSelectedIndex === null) { alert("请先选择一个需求项目!"); return; } const oldText = btnRefresh.innerHTML; btnRefresh.innerHTML = '🔄 刷新中...'; btnRefresh.disabled = true; const modalCaseDetail = document.getElementById('case-detail-modal'); const isModalOpen = modalCaseDetail && !modalCaseDetail.classList.contains('hidden'); const activeSidebarItem = document.querySelector('.modal-sidebar-item.active'); const activeCaseIdx = activeSidebarItem ? parseInt(activeSidebarItem.id.replace('sidebar-item-', '')) : null; try { await fetchRequirementData(currentSelectedIndex); if (isModalOpen && activeCaseIdx !== null && typeof window.renderSingleCaseDetail === 'function') { window.renderSingleCaseDetail(activeCaseIdx); } } catch (e) { console.error("Failed to refresh data", e); } finally { btnRefresh.innerHTML = oldText; btnRefresh.disabled = false; } }); } // Tabs document.querySelectorAll('.tab-btn-pill').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.tab-btn-pill').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', async () => { 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(','); } // Fetch status and render chain await fetchAndRenderPipelineChain(currentSelectedIndex); } }); 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'); const chainContainer = document.getElementById('pipeline-chain-container'); if (selectForcePhase && groupPlatforms) { selectForcePhase.addEventListener('change', (e) => { const val = e.target.value; if (['smart', 'phase1', 'step_1.1'].includes(val) || (val === 'custom_range' && chainStartNode === 'research')) { groupPlatforms.style.display = 'block'; } else { groupPlatforms.style.display = 'none'; } if (val === 'custom_range') { chainContainer.classList.remove('hidden'); } else { chainContainer.classList.add('hidden'); } }); } document.getElementById('btn-reset-chain').addEventListener('click', () => { chainStartNode = null; chainEndNode = null; renderPipelineChain(); }); // Add Requirement Modal Events const modalAddReq = document.getElementById('add-req-modal'); const inputAddReq = document.getElementById('input-new-req'); document.getElementById('btn-add-req').addEventListener('click', () => { inputAddReq.value = ""; modalAddReq.classList.remove('hidden'); }); document.getElementById('btn-close-add-req').addEventListener('click', () => { modalAddReq.classList.add('hidden'); }); document.getElementById('btn-cancel-add-req').addEventListener('click', () => { modalAddReq.classList.add('hidden'); }); document.getElementById('btn-submit-add-req').addEventListener('click', async () => { const val = inputAddReq.value.trim(); if (!val) { alert('需求内容不能为空'); return; } try { const res = await fetch(`/api/requirements`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ requirement: val }) }); if (res.ok) { modalAddReq.classList.add('hidden'); await fetchRequirements(); } else { alert('添加失败'); } } catch (e) { console.error(e); alert('网络错误'); } }); const modalEditReq = document.getElementById('edit-req-modal'); const inputEditReq = document.getElementById('edit-req-textarea'); document.getElementById('btn-edit-req').addEventListener('click', () => { if (currentSelectedIndex !== null) { const req = requirements.find(r => r.index === currentSelectedIndex); if (req) { inputEditReq.value = req.requirement; modalEditReq.classList.remove('hidden'); } } }); const closeEditReq = () => modalEditReq.classList.add('hidden'); document.getElementById('btn-close-edit-req').addEventListener('click', closeEditReq); document.getElementById('btn-cancel-edit-req').addEventListener('click', closeEditReq); const saveRequirementText = async (runAfter) => { if (currentSelectedIndex === null) return; const newText = inputEditReq.value.trim(); if (!newText) { alert('需求文本不能为空'); return; } try { const res = await fetch(`/api/requirements/${currentSelectedIndex}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ requirement: newText }) }); if (!res.ok) { const err = await res.json(); alert('保存失败: ' + (err.detail || '未知错误')); return; } // Update locally const req = requirements.find(r => r.index === currentSelectedIndex); if (req) { req.requirement = newText; elDetailTitle.textContent = newText; renderTaskList(requirements); } closeEditReq(); if (runAfter) { document.getElementById('btn-open-run-modal').click(); } } catch (e) { console.error(e); alert('保存请求失败'); } }; document.getElementById('btn-save-edit-req').addEventListener('click', () => saveRequirementText(false)); document.getElementById('btn-save-run-edit-req').addEventListener('click', () => saveRequirementText(true)); 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 reqBody = { content: elPromptTextarea.value }; if (elSchemaTextarea) reqBody.schema_content = elSchemaTextarea.value; const res = await fetch(`/api/prompts/${currentPromptName}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(reqBody) }); if (res.ok) { elPromptStatus.textContent = 'Saved!'; elPromptStatus.style.color = 'var(--success)'; setTimeout(() => elPromptStatus.textContent = '', 2000); } else { const errData = await res.json(); throw new Error(errData.detail || "Failed to save"); } } catch(e) { elPromptStatus.textContent = e.message || 'Save failed'; elPromptStatus.style.color = 'var(--danger)'; } }); } } // Boot // ---------------------------------------------------- // Pipeline Chain Visualization Logic // ---------------------------------------------------- async function fetchAndRenderPipelineChain(index) { try { const res = await fetch(`/api/requirements/${index}/pipeline-status`); if (res.ok) { currentPipelineStatus = await res.json(); } else { currentPipelineStatus = {}; } } catch (e) { console.error("Failed to fetch pipeline status", e); currentPipelineStatus = {}; } chainStartNode = null; chainEndNode = null; renderPipelineChain(); } function bindPipelineChainEvents() { PIPELINE_STEPS.forEach((step) => { const node = document.getElementById('node-' + step.id); if (!node) return; node.addEventListener('click', () => { if (!chainStartNode) { chainStartNode = step.id; } else if (!chainEndNode) { if (chainStartNode === step.id) { chainEndNode = step.id; // Double click = single step mode } else { chainEndNode = step.id; } } else { chainStartNode = step.id; // Reset and start new chainEndNode = null; } renderPipelineChain(); // Toggle platforms group if research is selected const groupPlatforms = document.getElementById('group-platforms'); if (groupPlatforms && document.getElementById('select-force-phase').value === 'custom_range') { const startIndex = PIPELINE_STEPS.findIndex(s => s.id === chainStartNode); const endIndex = chainEndNode ? PIPELINE_STEPS.findIndex(s => s.id === chainEndNode) : startIndex; const finalStart = Math.min(startIndex, endIndex); const finalEnd = Math.max(startIndex, endIndex); if (finalStart === 0) { // 'research' is index 0 groupPlatforms.style.display = 'block'; } else { groupPlatforms.style.display = 'none'; } } }); }); } function renderPipelineChain() { if (chainStartNode && chainEndNode) { const startIndex = PIPELINE_STEPS.findIndex(s => s.id === chainStartNode); const endIndex = PIPELINE_STEPS.findIndex(s => s.id === chainEndNode); if (endIndex < startIndex) { const tempId = chainStartNode; chainStartNode = chainEndNode; chainEndNode = tempId; } } const LINEAR_PREFIX = ["research", "source", "generate-case", "workflow-extract", "capability-extract-1", "apply-grounding"]; const BRANCH_21 = ["process-cluster", "process-score"]; const BRANCH_22 = ["capability-extract", "capability-enrich"]; const STRATEGY = "strategy"; let activeSteps = new Set(); if (chainStartNode && !chainEndNode) { activeSteps.add(chainStartNode); } else if (chainStartNode && chainEndNode) { const start = chainStartNode; const end = chainEndNode; const start_in_linear = LINEAR_PREFIX.includes(start); const start_in_21 = BRANCH_21.includes(start); const start_in_22 = BRANCH_22.includes(start); const end_in_linear = LINEAR_PREFIX.includes(end); const end_in_21 = BRANCH_21.includes(end); const end_in_22 = BRANCH_22.includes(end); const end_is_strategy = end === STRATEGY; // 1. Linear Prefix LINEAR_PREFIX.forEach(s => { const s_idx = LINEAR_PREFIX.indexOf(s); const start_idx = LINEAR_PREFIX.indexOf(start); if (start_idx >= 0 && s_idx >= start_idx) { if (end_in_linear) { if (s_idx <= LINEAR_PREFIX.indexOf(end)) activeSteps.add(s); } else { activeSteps.add(s); } } }); // 2. Branches if (end_is_strategy || (!end_in_21 && !end_in_22 && !end_in_linear)) { if (!start_in_21 && !start_in_22) { BRANCH_21.forEach(s => activeSteps.add(s)); BRANCH_22.forEach(s => activeSteps.add(s)); } else if (start_in_21) { const idx = BRANCH_21.indexOf(start); BRANCH_21.slice(idx).forEach(s => activeSteps.add(s)); BRANCH_22.forEach(s => activeSteps.add(s)); } else if (start_in_22) { const idx = BRANCH_22.indexOf(start); BRANCH_22.slice(idx).forEach(s => activeSteps.add(s)); BRANCH_21.forEach(s => activeSteps.add(s)); } } else if (end_in_21) { const end_idx = BRANCH_21.indexOf(end); const start_idx = start_in_21 ? BRANCH_21.indexOf(start) : 0; BRANCH_21.slice(start_idx, end_idx + 1).forEach(s => activeSteps.add(s)); } else if (end_in_22) { const end_idx = BRANCH_22.indexOf(end); const start_idx = start_in_22 ? BRANCH_22.indexOf(start) : 0; BRANCH_22.slice(start_idx, end_idx + 1).forEach(s => activeSteps.add(s)); } // 3. Strategy if (end_is_strategy) { activeSteps.add("strategy"); } } PIPELINE_STEPS.forEach((step) => { const node = document.getElementById('node-' + step.id); if (!node) return; node.className = 'chain-node'; if (currentPipelineStatus[step.id]) { node.classList.add('completed'); } else { node.classList.add('missing'); } if (activeSteps.has(step.id)) { if ((chainStartNode && !chainEndNode) || step.id === chainStartNode || step.id === chainEndNode) { node.classList.add('selected'); } else { node.classList.add('selected-range'); } } }); } bindPipelineChainEvents(); // Case Detail Modal Listeners const btnCloseCaseDetail = document.getElementById('btn-close-case-detail'); const modalCaseDetail = document.getElementById('case-detail-modal'); if (btnCloseCaseDetail && modalCaseDetail) { btnCloseCaseDetail.addEventListener('click', () => { modalCaseDetail.classList.add('hidden'); }); // Close on escape document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && !modalCaseDetail.classList.contains('hidden')) { modalCaseDetail.classList.add('hidden'); } }); // Close on click outside modalCaseDetail.addEventListener('click', (e) => { if (e.target === modalCaseDetail) { modalCaseDetail.classList.add('hidden'); } }); } window.openCaseDetail = function(p, initialIdx) { if (!window._currentRawCasesContext) return; const ctx = window._currentRawCasesContext; // Determine the list of platforms to aggregate. let platformsToAggregate = [p]; if (p !== 'filtered_cases' && p !== 'source_ex') { const crawlerPlatforms = Object.keys(ctx.rawCasesObj).filter(k => k !== 'source' && k !== 'case_detailed' && k !== 'case' && k !== 'images' && k !== 'filtered_cases' && k !== 'source_ex'); platformsToAggregate = [...crawlerPlatforms, 'source']; } let casesList = []; let globalInitialIdx = 0; const seenIds = new Set(); platformsToAggregate.forEach(plat => { if (!ctx.rawCasesObj[plat]) return; const platCases = ctx.rawCasesObj[plat].cases || ctx.rawCasesObj[plat].sources || []; platCases.forEach((c, idx) => { const cId = c.case_id || (c._raw && c._raw.case_id) || (c.post && c.post.channel_content_id) || `temp_${plat}_${idx}`; if (seenIds.has(cId)) return; seenIds.add(cId); const augmentedC = { ...c, _actualPlatform: plat }; if (plat === p && idx === initialIdx) { globalInitialIdx = casesList.length; } casesList.push(augmentedC); }); }); window._currentModalCases = casesList; window._currentModalPlatform = p; window._currentModalContext = ctx; window._currentModalIdx = globalInitialIdx; // Build Sidebar let sidebarHtml = ''; // Build Main Content Skeleton const mainHtml = `
`; document.getElementById('case-detail-modal-body').innerHTML = sidebarHtml + mainHtml; // Render the selected case window.renderSingleCaseDetail(globalInitialIdx); document.getElementById('case-detail-modal').classList.remove('hidden'); // Scroll sidebar to active item setTimeout(() => { const activeItem = document.getElementById(`sidebar-item-${globalInitialIdx}`); if (activeItem) activeItem.scrollIntoView({ block: 'nearest' }); }, 10); }; window.renderSingleCaseDetail = function(idx) { window._currentModalIdx = idx; const ctx = window._currentModalContext; const c = window._currentModalCases[idx]; if (!c) return; // Update Sidebar Active State document.querySelectorAll('.modal-sidebar-item').forEach(el => el.classList.remove('active')); const activeEl = document.getElementById(`sidebar-item-${idx}`); if (activeEl) activeEl.classList.add('active'); const p = c._actualPlatform || window._currentModalPlatform; const platCode = p.replace('case_', ''); const cId = c.case_id || (c._raw && c._raw.case_id) || (c.post && c.post.channel_content_id) || `temp_${idx}`; const cUrl = c.source_url || c.url || (c.post && c.post.link) || ''; const mappedS = ctx.sourceMap[cId] || ctx.sourceMap[cUrl] || (c._raw && ctx.sourceMap[c._raw.case_id]); const s = mappedS || c; const post = s.post || s || {}; const platformName = s.platform || (s._raw && s._raw.platform) || platCode; const title = post.title || c.title || '无标题'; const workflowUrl = s.source_url || s.url || cUrl; // Header const headerHtml = `

${title}

${workflowUrl ? `原文 ↗` : ''} 平台: ${platformName}
`; document.getElementById('modal-main-header').innerHTML = headerHtml; // Media & Body let mediaHtml = ''; const images = post.images || []; const xImages = post.image_url_list || []; const ytThumbnails = post.thumbnails && post.thumbnails.length > 0 ? [post.thumbnails[post.thumbnails.length - 1].url] : []; const allImages = [...images, ...xImages.map(img => img.image_url), ...ytThumbnails].filter(Boolean); if (allImages.length > 0) { mediaHtml += ``; } 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 += `
`; } const bodyText = post.body_text || post.body || ''; // Source Panel let mainScrollableHtml = `
原始内容 / SOURCE POSTS 点击展开/折叠
${mediaHtml} ${bodyText ? `
${bodyText}
` : '
无正文
'}
`; // Extracted Data const wf = ctx.detailMap[cId] || (workflowUrl ? ctx.detailMapByUrl[workflowUrl] : null) || c; const detailedCaseObj = ctx.rawCasesObj['case'] || ctx.rawCasesObj['case_detailed']; const caseJsonCases = (detailedCaseObj && detailedCaseObj.cases) || []; const realCaseIndex = caseJsonCases.findIndex(jc => (jc.case_id === cId) || (jc._raw && jc._raw.case_id === cId) || (jc.post && jc.post.channel_content_id === cId) ); const caseIndexToPass = realCaseIndex >= 0 ? (caseJsonCases[realCaseIndex].index || (realCaseIndex + 1)) : -1; const btnWorkflowHtml = caseIndexToPass !== -1 ? `` : ''; const btnCapabilityHtml = caseIndexToPass !== -1 ? `` : ''; mainScrollableHtml += `

提取的工序 (Strategy)

${btnWorkflowHtml}

提取的能力 (Capability)

${btnCapabilityHtml}
`; document.getElementById('modal-main-scrollable').innerHTML = mainScrollableHtml; }; window.switchDetailTab = function(tabId) { document.querySelectorAll('.detail-tab-btn').forEach(btn => btn.classList.remove('active')); document.querySelectorAll('.detail-tab-content').forEach(content => content.style.display = 'none'); document.getElementById(`tab-btn-${tabId}`).classList.add('active'); document.getElementById(`tab-content-${tabId}`).style.display = 'block'; }; window.renderStructuredData = function(items, type, parentItem = null) { if (!items || items.length === 0) { return `
暂无${type === 'workflow' ? '工序' : '能力'}数据
`; } const formatIOs = (ios) => { if (!ios || !Array.isArray(ios) || ios.length === 0) return ''; const escapeHtml = (s) => String(s).replace(//g, '>'); return ios.map(io => { const desc = escapeHtml(io.description || io.role || '未知'); const mod = escapeHtml(io.modality || '未知'); return `${desc}[${mod}]`; }).join(' + '); }; const buildFullTitle = (inputs, outputs, actionStr, fallbackTitle) => { const escapeHtml = (s) => String(s).replace(//g, '>'); const inStr = formatIOs(inputs) || '无'; const outStr = formatIOs(outputs) || '无'; let parts = []; parts.push(inStr); if (actionStr) parts.push(`${escapeHtml(actionStr)}`); parts.push(outStr); return parts.join(' ➔ '); }; let html = ''; items.forEach((item, idx) => { let title = ''; const hasValidIO = (arr) => Array.isArray(arr) && arr.length > 0 && (arr[0].role || arr[0].description); if (hasValidIO(item.inputs) || hasValidIO(item.outputs) || (item.steps && item.steps.length > 0)) { let actionStr = ''; if (item.action && item.action.main_action) { actionStr = item.action.main_action; } else if (item.method && !item.method.includes('[')) { actionStr = item.method; } else if (item.steps && Array.isArray(item.steps)) { actionStr = item.steps.map(s => { if (s.action && s.action.main_action) { return s.action.mechanism ? `[${s.action.main_action}] ${s.action.mechanism}` : s.action.main_action; } return '未知'; }).join(' ➔ '); } title = buildFullTitle(item.inputs, item.outputs, actionStr, item.method || item.name || `节点 ${idx + 1}`); } else { const escapeHtml = (s) => String(s).replace(//g, '>'); title = escapeHtml(item.method || item.name || ''); if (!title && item.action && item.action.main_action) { const actText = item.action.mechanism ? `[${item.action.main_action}] ${item.action.mechanism}` : item.action.main_action; title = escapeHtml(actText); } if (!title) { title = escapeHtml(type === 'workflow' ? '工作流' : `节点 ${idx + 1}`); } } // Tree node tags (from apply_to keys) or unstructured_what fallback const getApplyToField = (it) => { if (it.apply_to_grounding) return { key: 'apply_to_grounding', val: it.apply_to_grounding }; if (it.apply_to_draft) return { key: 'apply_to_draft', val: it.apply_to_draft }; if (it.apply_to) return { key: 'apply_to', val: it.apply_to }; return null; }; const applyToData = getApplyToField(item); let treeNodeTags = ''; if (applyToData && typeof applyToData.val === 'object') { const allLeafs = []; Object.values(applyToData.val).forEach(v => { if (Array.isArray(v)) { v.forEach(pathObj => { let leaf = ''; if (typeof pathObj === 'object' && pathObj !== null) { if (pathObj.element) leaf = pathObj.element; else if (pathObj.category_path || pathObj.path) { leaf = (pathObj.category_path || pathObj.path).split('/').pop(); } } else { leaf = String(pathObj).split('/').pop(); } if (leaf) allLeafs.push(leaf); }); } }); const uniqueLeafs = [...new Set(allLeafs)]; const badgeStyle = 'background: #f8fafc; color: #475569; border: 1px solid #cbd5e1; border-radius: 12px; padding: 2px 10px; font-size: 0.85em; font-weight: normal; margin-right: 6px; display: inline-block; white-space: nowrap;'; treeNodeTags = uniqueLeafs.map(leaf => `${leaf.replace(//g, '>')}`).join(''); } else if (item.unstructured_what && Array.isArray(item.unstructured_what)) { const badgeStyle = 'background: #f8fafc; color: #475569; border: 1px solid #cbd5e1; border-radius: 12px; padding: 2px 10px; font-size: 0.85em; font-weight: normal; margin-right: 6px; display: inline-block; white-space: nowrap;'; treeNodeTags = item.unstructured_what.map(t => `${String(t).replace(//g, '>')}`).join(''); } html += `
${title}
${treeNodeTags}
`; const renderApplyToVal = (valObj, isIdeal = false) => { if (!valObj || typeof valObj !== 'object') return '-'; let res = '
'; Object.entries(valObj).forEach(([k, v]) => { if (Array.isArray(v) && v.length > 0) { res += `
${k}
`; let idealBadges = []; v.forEach(pathObj => { let pathStr = ''; let elementStr = ''; if (typeof pathObj === 'object' && pathObj !== null) { pathStr = pathObj.category_path || pathObj.path || ''; elementStr = pathObj.element || ''; } else { pathStr = String(pathObj); } let tooltipHtml = ''; if (typeof pathObj === 'object' && pathObj !== null && pathObj.rationale) { tooltipHtml = `
${pathObj.category_id ? `id: ${pathObj.category_id}` : ''} ${pathObj.rationale.replace(//g, '>')}
`; } let htmlParts = ''; if (pathStr && elementStr) { htmlParts = `${pathStr}${elementStr}`; } else if (pathStr) { const parts = pathStr.split('/'); const leaf = parts.pop(); const prefix = parts.length > 0 ? parts.join('/') + '/' : ''; htmlParts = ` ${prefix ? `${prefix}` : ''} ${leaf} `; } if (htmlParts) { res += `${htmlParts}${tooltipHtml}`; } if (typeof pathObj === 'object' && pathObj !== null && pathObj.ideal_path) { let fullNormalPath = pathStr || ''; if (elementStr) { if (!fullNormalPath.endsWith('/')) fullNormalPath += '/'; fullNormalPath += elementStr; } if (fullNormalPath !== pathObj.ideal_path) { const normalParts = fullNormalPath.split('/'); const idealParts = pathObj.ideal_path.split('/'); const idealLeaf = idealParts.pop(); let prefixHtml = ''; if (idealParts.length > 0) { prefixHtml += ``; for (let i = 0; i < idealParts.length; i++) { if (i === 0 && idealParts[0] === '') { prefixHtml += '/'; continue; } const p = idealParts[i]; const isNew = i >= normalParts.length || p !== normalParts[i]; if (isNew) { prefixHtml += `${p}/`; } else { prefixHtml += `${p}/`; } } prefixHtml += ``; } const isLeafNew = idealParts.length >= normalParts.length || idealLeaf !== normalParts[idealParts.length]; const leafHtml = isLeafNew ? `${idealLeaf}` : `${idealLeaf}`; const idealHtml = `${prefixHtml}${leafHtml}`; idealBadges.push(`${idealHtml}${tooltipHtml}`); } } }); if (idealBadges.length > 0) { res += `
` + idealBadges.join(''); } res += `
`; } }); res += `
`; return res === '
' ? '-' : res; }; // Render apply_to / apply_to_grounding at workflow level (if it exists) if (applyToData && typeof applyToData.val === 'object' && Object.keys(applyToData.val).length > 0) { html += `
${applyToData.key}
${renderApplyToVal(applyToData.val)}
`; } // Stage rendering removed per request // Render effects if (item.effects && Array.isArray(item.effects) && item.effects.length > 0) { html += `
effects
    ${item.effects.map(li => `
  • ${li.replace(//g, '>')}
  • `).join('')}
`; } // Render confidence fields const formatDate = (ts) => { if (!ts) return '-'; if (typeof ts === 'string' && ts.includes('-')) return ts; const num = Number(ts); if (isNaN(num) || num <= 0) return '-'; const d = new Date(num > 10000000000 ? num : num * 1000); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; }; html += `
置信度
Maturity: ${String((item.maturity || (parentItem && parentItem.maturity)) || '-').replace(//g, '>')}
Validation: ${(item.validation_count !== undefined && item.validation_count !== null) ? String(item.validation_count).replace(//g, '>') : (parentItem && parentItem.validation_count !== undefined && parentItem.validation_count !== null) ? String(parentItem.validation_count).replace(//g, '>') : '-'}
Published: ${formatDate(item.published_at || (parentItem && parentItem.published_at))}
Last Verified: ${formatDate(item.last_verified_at || (parentItem && parentItem.last_verified_at))}
Created: ${formatDate(item.created_at || (parentItem && parentItem.created_at))}
Updated: ${formatDate(item.updated_at || (parentItem && parentItem.updated_at))}
`; // Render feedback if available const feedbackVal = item.feedback || (parentItem && parentItem.feedback); if (feedbackVal) { let feedbackHtml = ''; if (typeof feedbackVal === 'object' && feedbackVal !== null) { Object.entries(feedbackVal).forEach(([k, v]) => { if (v !== null && v !== undefined && String(v).trim() !== '') { feedbackHtml += `
${k}: ${v}
`; } }); if (feedbackHtml !== '') { feedbackHtml = `
${feedbackHtml}
`; } } else if (typeof feedbackVal === 'string' && feedbackVal.trim() !== '') { feedbackHtml = `
${String(feedbackVal).replace(//g, '>')}
`; } if (feedbackHtml !== '') { html += `
feedback
${feedbackHtml}
`; } } // Render body if (item.body && typeof item.body === 'string') { html += `
body
${item.body.replace(//g, '>')}
`; } // Helper for inputs/outputs (Moved up so it can be used by steps) const renderDataObjList = (list) => { const isValid = (v) => v !== null && v !== undefined && String(v).toLowerCase() !== 'null' && String(v).toLowerCase() !== 'none' && String(v).trim() !== ''; return list.map(io => { const desc = isValid(io.description) ? io.description.replace(//g, '>') : ''; const mod = isValid(io.modality) ? io.modality : ''; let content = ''; if (mod) { content += `${mod}`; } if (desc) { content += desc; } if (!content) { const keys = Object.keys(io); if (keys.length === 1 && typeof io[keys[0]] === 'string') { content = `${keys[0]}${io[keys[0]]}`; } else { content = `未知`; } } return `
${content}
`; }).join(''); }; // Render steps array specially if (item.steps && Array.isArray(item.steps)) { html += `
steps
`; const escapeHtml = (s) => String(s).replace(//g, '>'); item.steps.forEach((step, stepIdx) => { let actionText = '未知'; if (step.action && step.action.main_action) { actionText = step.action.mechanism ? `[${step.action.main_action}] ${step.action.mechanism}` : step.action.main_action; } else if (step.method) { actionText = step.method; } else if (step.description) { actionText = step.description; } let stepTitle = ''; const hasValidIO = (arr) => Array.isArray(arr) && arr.length > 0 && (arr[0].role || arr[0].description); if (hasValidIO(step.inputs) || hasValidIO(step.outputs)) { stepTitle = buildFullTitle(step.inputs, step.outputs, actionText, step.method || step.description || `步骤 ${step.order || stepIdx + 1}`); } else { stepTitle = escapeHtml(step.method || step.description || ''); if (!stepTitle) stepTitle = escapeHtml(actionText); if (!stepTitle || stepTitle === '未知') stepTitle = escapeHtml(`步骤 ${step.order || stepIdx + 1}`); } let actionHtml = escapeHtml(actionText); if (step.action && step.action.main_action) { const badgeHtml = `${escapeHtml(step.action.main_action)}`; actionHtml = step.action.mechanism ? badgeHtml + escapeHtml(step.action.mechanism) : badgeHtml; } else { const match = actionText.match(/^\[(.*?)\]\s*(.*)$/); if (match) { actionHtml = `${escapeHtml(match[1])}${escapeHtml(match[2])}`; } } let toolsHtml = ''; if (step.tools && step.tools.length > 0) { toolsHtml = step.tools.map(t => `${escapeHtml(t)}`).join(''); } html += ` `; }); html += `
序号 阶段 操作流 输入 动作 输出 作用域 做法 工具
${step.order || stepIdx + 1} ${step.phase ? `${escapeHtml(step.phase)}` : '-'}
${stepTitle}
${step.inputs && Array.isArray(step.inputs) && step.inputs.length > 0 ? renderDataObjList(step.inputs) : '-'}
${actionHtml}
${step.outputs && Array.isArray(step.outputs) && step.outputs.length > 0 ? renderDataObjList(step.outputs) : '-'}
${renderApplyToVal(step.apply_to_draft || step.apply_to_grounding || step.apply_to)}
${step.body ? escapeHtml(step.body) : '-'}
${toolsHtml || '-'}
`; } // Render inputs if (item.inputs && Array.isArray(item.inputs) && item.inputs.length > 0) { html += `
inputs
${renderDataObjList(item.inputs)}
`; } else if (item.inputs && typeof item.inputs === 'object' && Object.keys(item.inputs).length > 0 && !Array.isArray(item.inputs)) { // Fallback for old schema html += `
inputs
`; Object.entries(item.inputs).forEach(([k, v]) => { html += `
${k}${v}
`; }); html += `
`; } // Render outputs if (item.outputs && Array.isArray(item.outputs) && item.outputs.length > 0) { html += `
outputs
${renderDataObjList(item.outputs)}
`; } else if (item.outputs && typeof item.outputs === 'object' && Object.keys(item.outputs).length > 0 && !Array.isArray(item.outputs)) { // Fallback for old schema html += `
outputs
`; Object.entries(item.outputs).forEach(([k, v]) => { html += `
${k}${v}
`; }); html += `
`; } // Render tools (for non-step items like capabilities) if (item.tools && Array.isArray(item.tools) && item.tools.length > 0) { html += `
tools
${item.tools.map(t => `${t}`).join('')}
`; } // Dynamic fallback for any other unhandled keys const handledKeys = [ 'method', 'name', 'action', 'unstructured_what', 'apply_to_grounding', 'apply_to_draft', 'apply_to', 'stage', 'effects', 'body', 'steps', 'inputs', 'outputs', 'tools' ]; Object.keys(item).forEach(k => { if (!handledKeys.includes(k)) { let v = item[k]; if (v === null || v === undefined || v === '') return; let displayHtml = ''; if (typeof v === 'object') { if (Array.isArray(v)) { if (v.length === 0) return; if (typeof v[0] === 'object') { displayHtml = `
${JSON.stringify(v, null, 2).replace(//g, '>')}
`; } else { displayHtml = v.map(vi => `${String(vi).replace(//g, '>')}`).join(''); } } else { if (Object.keys(v).length === 0) return; displayHtml = `
${JSON.stringify(v, null, 2).replace(//g, '>')}
`; } } else { displayHtml = String(v).replace(//g, '>'); } html += `
${k}
${displayHtml}
`; } }); html += `
`; }); return html; }; window.triggerSingleCaseRerun = async function(step, caseIndex) { if (typeof currentSelectedIndex === 'undefined' || currentSelectedIndex === null) { alert("请先选择一个需求项目!"); return; } const confirmMsg = `确定要针对当前 Case 单独重跑 [${step}] 步骤吗?\n注意:这会覆盖现有的提取结果!`; if (!confirm(confirmMsg)) return; try { const payload = { use_claude_sdk: false, // Default only_step: step, case_index: caseIndex }; // Use Claude SDK if it's checked in the UI const cbClaudeSdk = document.getElementById('cb-use-claude-sdk'); if (cbClaudeSdk) { payload.use_claude_sdk = cbClaudeSdk.checked; } const res = await fetch(`/api/pipeline/run/${currentSelectedIndex}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (res.ok) { alert(`✅ 单Case重跑已触发 (${step})!请在 Pipeline 终端查看进度。`); // Show the logs modal instead of non-existent logs tab const btnViewLogs = document.getElementById('btn-view-logs'); if (btnViewLogs) { btnViewLogs.click(); } const modalCaseDetail = document.getElementById('case-detail-modal'); if (modalCaseDetail) modalCaseDetail.classList.add('hidden'); } else { const err = await res.json(); alert(`启动重跑失败: ${err.detail || JSON.stringify(err)}`); } } catch(e) { console.error("Error triggering single case rerun:", e); alert(`发生错误: ${e.message}`); } }; init();