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 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.6 提取' }, { 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(); setupFloatingApplyToTooltips(); startStatusPolling(); } function setupFloatingApplyToTooltips() { if (window.__applyToTooltipReady) return; window.__applyToTooltipReady = true; let floatingTooltip = null; let activeTarget = null; const hideTooltip = () => { activeTarget = null; if (floatingTooltip) { floatingTooltip.remove(); floatingTooltip = null; } }; const positionTooltip = () => { if (!floatingTooltip || !activeTarget) return; const rect = activeTarget.getBoundingClientRect(); const tipRect = floatingTooltip.getBoundingClientRect(); const gap = 8; const margin = 12; let left = rect.left + rect.width / 2 - tipRect.width / 2; left = Math.max(margin, Math.min(left, window.innerWidth - tipRect.width - margin)); let top = rect.bottom + gap; if (top + tipRect.height > window.innerHeight - margin) { top = rect.top - tipRect.height - gap; } top = Math.max(margin, top); floatingTooltip.style.left = `${left}px`; floatingTooltip.style.top = `${top}px`; floatingTooltip.style.visibility = 'visible'; floatingTooltip.style.opacity = '1'; }; document.addEventListener('pointerover', (event) => { const target = event.target.closest('.apply-to-path-item.has-tooltip'); if (!target) return; const sourceTooltip = target.querySelector('.apply-to-tooltip'); if (!sourceTooltip) return; activeTarget = target; if (!floatingTooltip) { floatingTooltip = document.createElement('div'); floatingTooltip.className = 'floating-apply-to-tooltip'; document.body.appendChild(floatingTooltip); } floatingTooltip.innerHTML = sourceTooltip.innerHTML; floatingTooltip.style.visibility = 'hidden'; floatingTooltip.style.opacity = '0'; requestAnimationFrame(positionTooltip); }); document.addEventListener('pointerout', (event) => { const target = event.target.closest('.apply-to-path-item.has-tooltip'); if (!target || target.contains(event.relatedTarget)) return; hideTooltip(); }); window.addEventListener('scroll', hideTooltip, true); window.addEventListener('resize', hideTooltip); } // 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 totalStatsHtml = ''; const detailedCaseObj = rawCasesObj['case'] || rawCasesObj['case_detailed']; if (detailedCaseObj) { const cd = detailedCaseObj; let uniqueCases = new Set(); 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; const uniqueKey = cId || cUrl || Math.random().toString(); uniqueCases.add(uniqueKey); if (c.workflow) calcWorkflow++; if (c.capability && c.capability.length > 0) calcCapabilities++; if (cId) { if (c.workflow) detailMap[cId] = { ...detailMap[cId], workflow: c.workflow }; if (c.capability) detailMap[cId] = { ...detailMap[cId], capability: c.capability }; } if (cUrl) { if (c.workflow) detailMapByUrl[cUrl] = { ...detailMapByUrl[cUrl], workflow: c.workflow }; if (c.capability) detailMapByUrl[cUrl] = { ...detailMapByUrl[cUrl], capability: c.capability }; } }); } const displayTotal = uniqueCases.size > 0 ? uniqueCases.size : (cd.total !== undefined ? cd.total : 0); const displayWorkflowSuccess = calcWorkflow > 0 ? calcWorkflow : (cd.workflow_success !== undefined && cd.workflow_success !== null ? cd.workflow_success : (cd.success !== undefined && cd.success !== null ? cd.success : 0)); const displayCapabilitiesSuccess = calcCapabilities > 0 ? calcCapabilities : (cd.capabilities_success !== undefined && cd.capabilities_success !== null ? cd.capabilities_success : 0); if (cd.total !== undefined || uniqueCases.size > 0) { totalStatsHtml = `
${displayTotal} 未被过滤的帖子总数
${displayWorkflowSuccess} 工序提取成功数
${displayCapabilitiesSuccess} 能力提取成功数
`; } } let filteredStatsHtml = ''; if (rawCasesObj['filtered_cases']) { const fObj = rawCasesObj['filtered_cases']; let totalFiltered = 0; if (fObj.total !== undefined) totalFiltered = fObj.total; else if (fObj.cases) totalFiltered = fObj.cases.length; else if (fObj.sources) totalFiltered = fObj.sources.length; else if (fObj.by_reason) { Object.values(fObj.by_reason).forEach(r => { if (r.sources) totalFiltered += r.sources.length; else if (r.cases) totalFiltered += r.cases.length; }); } else if (Array.isArray(fObj)) totalFiltered = fObj.length; filteredStatsHtml = `
${totalFiltered} 被过滤的帖子数
`; } 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 = ''; html += `
`; html += ``; if (hasFiltered) html += ``; if (hasExternal) html += ``; html += ``; html += ``; html += `
`; const renderPaneContent = (pList, paneType) => { let paneHtml = ''; if (paneType === 'total' && typeof totalStatsHtml !== 'undefined' && totalStatsHtml) { paneHtml += totalStatsHtml; } else if (paneType === 'filtered_cases' && typeof filteredStatsHtml !== 'undefined' && filteredStatsHtml) { paneHtml += filteredStatsHtml; } let totalCases = 0; let seenIds = new Set(); let groupedHtml = {}; const getGroupKey = (c, p) => (p === 'filtered_cases' && c.filter_reason) ? `🚫 过滤原因: ${c.filter_reason}` : 'default'; let allCases = []; 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 && p !== 'filtered_cases') { paneHtml += `
🛑 ${p} 提示: ${rawCasesObj[p].reason}
`; } let cases = []; if (Array.isArray(rawCasesObj[p])) { cases = rawCasesObj[p]; } else if (rawCasesObj[p].cases) { cases = rawCasesObj[p].cases; } else if (rawCasesObj[p].sources) { cases = rawCasesObj[p].sources; } else if (rawCasesObj[p].by_reason) { Object.entries(rawCasesObj[p].by_reason).forEach(([reasonKey, reasonObj]) => { if (reasonObj.sources && Array.isArray(reasonObj.sources)) { reasonObj.sources.forEach(src => { if (!src.filter_reason) src.filter_reason = reasonKey; cases.push(src); }); } }); } 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) => { allCases.push({ c: c, p: p, originalIdx: idx }); }); } }); allCases.sort((aObj, bObj) => { const getScore = (item) => { const iId = item.case_id || (item._raw && item._raw.case_id) || (item.post && item.post.channel_content_id); const iUrl = item.source_url || item.url || (item.post && item.post.link); const mapped = sourceMap[iId] || sourceMap[iUrl] || (item._raw && sourceMap[item._raw.case_id]) || item; return mapped.evaluation && mapped.evaluation.quality ? (mapped.evaluation.quality.overall_score || 0) : 0; }; return getScore(bObj.c) - getScore(aObj.c); }); allCases.forEach(itemObj => { const c = itemObj.c; const p = itemObj.p; const idx = itemObj.originalIdx; 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) || ''; const groupKey = getGroupKey(c, p); if (!groupedHtml[groupKey]) groupedHtml[groupKey] = ''; 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 || (post.body_text ? post.body_text.substring(0, 30) + '...' : '') || cId || '无标题'; 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()}` : ''; const score = s.evaluation && s.evaluation.quality ? s.evaluation.quality.overall_score : null; let scoreBadge = ''; if (score !== null && score !== undefined) { let color = score >= 80 ? '#10b981' : (score >= 60 ? '#f59e0b' : '#ef4444'); scoreBadge = `
⭐️ ${score}
`; } let actionBtn = ''; if (p === 'source_ex') { const isImported = !!sourceMap[cId] || !!sourceMap[cUrl]; if (isImported) { actionBtn = ``; } else { actionBtn = ``; } } groupedHtml[groupKey] += `
${platBadge} ${scoreBadge} ${actionBtn} ${allImages.length > 0 ? `` : ''}
${title}
${allImages.length === 0 ? snippetHtml : ''}
👤 ${author}
❤️ ${likes}
`; } else { groupedHtml[groupKey] += `
📝 旧版格式 / 解析失败
点击查看详情
`; } }); if (totalCases === 0 && pList.length > 0 && !paneHtml.includes('解析失败') && !paneHtml.includes('未进行')) { paneHtml += `
暂无数据
`; } else { Object.entries(groupedHtml).forEach(([groupName, gHtml]) => { if (gHtml) { if (groupName !== 'default') { paneHtml += `

${groupName}

`; } paneHtml += `
${gHtml}
`; } }); } return paneHtml; }; html += `
${renderPaneContent([...channelPlatforms, 'source'], 'total')}
`; 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); } }; function selectSubTab(tabName) { document.querySelectorAll('#json-raw .sub-tab-btn').forEach(btn => { btn.classList.remove('active'); }); const activeBtn = document.querySelector(`#json-raw .sub-tab-btn[data-target="sub-tab-${tabName}"]`); if (activeBtn) activeBtn.classList.add('active'); document.querySelectorAll('#json-raw .sub-tab-pane').forEach(pane => { pane.classList.add('hidden'); }); document.getElementById(`sub-tab-${tabName}`).classList.remove('hidden'); } window.deleteClusterItems = async function (indices) { if (currentSelectedIndex === null) return; const data = window.dataCache[currentSelectedIndex]?.cluster; if (!data) return; let newData = Array.isArray(data) ? [...data] : { ...data }; let count = 0; // Sort indices descending to avoid shifting issues if it's an array indices.sort((a, b) => b - a); if (Array.isArray(newData)) { indices.forEach(i => { newData.splice(i, 1); count++; }); } else { const keys = Object.keys(newData); indices.forEach(i => { delete newData[keys[i]]; count++; }); } if (!confirm(`确定要删除选中的 ${count} 个项目吗?`)) return; try { const res = await fetch(`/api/requirements/${currentSelectedIndex}/save_cluster`, { method: 'POST', body: JSON.stringify(newData), headers: { 'Content-Type': 'application/json' } }); if (res.ok) { fetchRequirementData(currentSelectedIndex); } else { alert('删除失败'); } } catch (e) { console.error(e); alert('删除失败'); } }; window.deleteSingleCluster = function (idx) { deleteClusterItems([idx]); }; window.deleteSelectedClusters = function () { const checkboxes = document.querySelectorAll('.cluster-checkbox:checked'); const indices = Array.from(checkboxes).map(cb => parseInt(cb.dataset.idx)); if (indices.length === 0) { alert('请先选择要删除的项目'); return; } deleteClusterItems(indices); }; window.clearAllClusters = async function () { if (currentSelectedIndex === null) return; if (!confirm('确定要清空全部聚类结果吗?')) return; try { const res = await fetch(`/api/requirements/${currentSelectedIndex}/save_cluster`, { method: 'POST', body: JSON.stringify(Array.isArray(window.dataCache[currentSelectedIndex]?.cluster) ? [] : {}), headers: { 'Content-Type': 'application/json' } }); if (res.ok) { fetchRequirementData(currentSelectedIndex); } else { alert('清空失败'); } } catch (e) { console.error(e); alert('清空失败'); } }; function renderClusterDeletable(clusterData) { if (!clusterData || (Array.isArray(clusterData) && clusterData.length === 0) || (typeof clusterData === 'object' && Object.keys(clusterData).length === 0)) { return `
暂无聚类结果数据,请导入 JSON 文件
`; } let html = `
`; html += `
`; const items = Array.isArray(clusterData) ? clusterData : Object.entries(clusterData).map(([k, v]) => ({ key: k, value: v })); items.forEach((item, idx) => { const displayData = Array.isArray(clusterData) ? item : item.value; const displayKey = Array.isArray(clusterData) ? `导入记录 #${idx + 1}` : `Key: ${item.key}`; html += `
${renderJSON(displayData)}
`; }); html += `
`; return html; } 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 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(); window.dataCache = window.dataCache || {}; window.dataCache[index] = data; 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 jsonRaw.innerHTML = renderDataOrRaw(rawCasesClone, renderRawCases); const clusterData = window.dataCache[index].cluster; const oldActiveWorkflowTab = jsonBlueprint.querySelector('.sub-tab-btn.active')?.dataset?.target || 'sub-tab-workflow-cases'; const workflowCasesHtml = renderAggregatedPerCaseData(casesList, 'workflow'); let bpHtml = `
`; bpHtml += `
`; bpHtml += ``; bpHtml += ``; bpHtml += `
`; bpHtml += `
${workflowCasesHtml}
`; bpHtml += `
导入本地生成的 JSON (如 process.json 等) 将保存为 cluster.json 并支持单项删除与多选清除。
${renderClusterDeletable(clusterData)}
`; bpHtml += `
`; jsonBlueprint.innerHTML = bpHtml; const clusterFileInput = document.getElementById('input-upload-cluster'); if (clusterFileInput) { clusterFileInput.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_cluster`, { method: 'POST', body: formData }); if (res.ok) { fetchRequirementData(index); } else { alert('上传失败'); } } catch (err) { console.error(err); alert('上传出错'); } }; } 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); updateStats(); } } 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 if (req.status === 'failed') 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 (jsonRaw) jsonRaw.textContent = 'Loading...'; fetchRequirementData(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.6_extract": "workflow-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'); } }); // 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; 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 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)'; } }); } const btnUpdateSchema = document.getElementById('update-schema-btn'); if (btnUpdateSchema) { btnUpdateSchema.addEventListener('click', async () => { if (!currentPromptName) return; const originalText = btnUpdateSchema.innerHTML; btnUpdateSchema.innerHTML = ' 更新中...'; btnUpdateSchema.disabled = true; try { // First save the prompt so the backend reads the latest content const reqBody = { content: elPromptTextarea.value }; if (elSchemaTextarea) reqBody.schema_content = elSchemaTextarea.value; let res = await fetch(`/api/prompts/${currentPromptName}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(reqBody) }); if (!res.ok) { const errData = await res.json(); throw new Error(errData.detail || "保存 Prompt 失败"); } // Then call the schema update API res = await fetch(`/api/prompts/${currentPromptName}/update_schema`, { method: 'POST' }); if (res.ok) { const data = await res.json(); if (elSchemaTextarea && data.schema_content) { elSchemaTextarea.value = data.schema_content; } btnUpdateSchema.innerHTML = ' 更新成功'; setTimeout(() => { btnUpdateSchema.innerHTML = originalText; btnUpdateSchema.disabled = false; }, 2000); } else { const errData = await res.json(); throw new Error(errData.detail || "更新 Schema 失败"); } } catch (e) { alert(e.message || '更新失败'); btnUpdateSchema.innerHTML = ' 失败'; setTimeout(() => { btnUpdateSchema.innerHTML = originalText; btnUpdateSchema.disabled = false; }, 2000); } }); } } // 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", "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; let platCases = []; if (Array.isArray(ctx.rawCasesObj[plat])) { platCases = ctx.rawCasesObj[plat]; } else if (ctx.rawCasesObj[plat].cases) { platCases = ctx.rawCasesObj[plat].cases; } else if (ctx.rawCasesObj[plat].sources) { platCases = ctx.rawCasesObj[plat].sources; } else if (ctx.rawCasesObj[plat].by_reason) { Object.entries(ctx.rawCasesObj[plat].by_reason).forEach(([reasonKey, reasonObj]) => { if (reasonObj.sources && Array.isArray(reasonObj.sources)) { reasonObj.sources.forEach(src => { if (!src.filter_reason) src.filter_reason = reasonKey; platCases.push(src); }); } }); } 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}`; 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]); if (plat !== 'filtered_cases' && plat !== 'source' && plat !== 'source_ex' && !mappedS) return; 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; const publishedTime = post.publish_timestamp || post.published_at || '-'; const likeCount = post.like_count !== undefined ? post.like_count : (post.likes !== undefined ? post.likes : '-'); const collectCount = post.collect_count !== undefined ? post.collect_count : (post.collects !== undefined ? post.collects : '-'); const commentCount = post.comment_count !== undefined ? post.comment_count : (post.comments !== undefined ? post.comments : '-'); const shareCount = post.share_count !== undefined ? post.share_count : (post.shares !== undefined ? post.shares : '-'); const isFiltered = (c._actualPlatform === 'filtered_cases'); let filterActionHtml = ''; if (isFiltered) { filterActionHtml = ``; } else { filterActionHtml = ``; } const headerHtml = `

${title}

${filterActionHtml}
${workflowUrl ? `原文 ↗` : ''} 平台: ${platformName}
Published ${publishedTime}
Likes ${likeCount}
Collects ${collectCount}
Comments ${commentCount}
Shares ${shareCount}
`; 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}
` : '
无正文
'}
`; // Evaluation Panel if (s.evaluation && Object.keys(s.evaluation).length > 0) { const renderEvalNode = (node, indent = 0) => { let html = ''; if (typeof node === 'object' && node !== null) { Object.entries(node).forEach(([k, v]) => { html += `
${k.replace(/_/g, ' ')}`; if (typeof v === 'object' && v !== null) { html += `
${renderEvalNode(v, 0)}
`; } else { const valColor = typeof v === 'number' ? '#3b82f6' : 'var(--text-main)'; html += `${String(v).replace(//g, '>')}`; } html += `
`; }); } return html; }; mainScrollableHtml += `

📊 质量评估 (Evaluation)

${renderEvalNode(s.evaluation)}
`; } // 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.description) { actionStr = item.action.description; } else if (item.method && !item.method.includes('[')) { actionStr = item.method; } else if (item.steps && Array.isArray(item.steps)) { const hasAnyValidIO = item.steps.some(s => hasValidIO(s.inputs) || hasValidIO(s.outputs) || s.body); if (hasAnyValidIO) { actionStr = item.steps.map(s => { if (s.action && s.action.description) { return s.action.description; } if (s.body) return s.body.length > 20 ? s.body.substring(0, 20) + '...' : s.body; if (s.method) return s.method; if (s.phase) return s.phase; return '未知'; }).join(' ➔ '); } else { actionStr = item.method || item.name || type === 'workflow' ? '工作流' : `节点 ${idx + 1}`; } } if (hasValidIO(item.inputs) || hasValidIO(item.outputs)) { title = buildFullTitle(item.inputs, item.outputs, actionStr, item.method || item.name || `节点 ${idx + 1}`); } else { title = String(actionStr).replace(//g, '>'); } } else { const escapeHtml = (s) => String(s).replace(//g, '>'); title = escapeHtml(item.method || item.name || ''); if (!title && item.action && item.action.description) { title = escapeHtml(item.action.description); } if (!title && item.body) { const actText = item.body.length > 50 ? item.body.substring(0, 50) + '...' : item.body; 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, suggest: it.suggest_apply_to }; if (it.apply_to_draft) return { key: 'apply_to_draft', val: it.apply_to_draft, suggest: null }; if (it.apply_to) return { key: 'apply_to', val: it.apply_to, suggest: it.suggest_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, suggestApplyTo = null) => { if (!valObj || typeof valObj !== 'object') return '-'; const escapeApplyToText = (s) => String(s).replace(//g, '>'); const renderPathParts = (pathValue, highlight = false) => { const pathStr = String(pathValue || '').trim(); if (!pathStr) return ''; const parts = pathStr.split('/'); const leaf = parts.pop(); const prefix = parts.length > 0 ? parts.join('/') + '/' : ''; const leafStyle = highlight ? 'background:#eff6ff; color:#2563eb; border:1px solid #bfdbfe;' : ''; return ` ${prefix ? `${escapeApplyToText(prefix)}` : ''} ${escapeApplyToText(leaf)} `; }; let res = '
'; let hasRows = false; Object.entries(valObj).forEach(([k, v]) => { if (Array.isArray(v) && v.length > 0) { hasRows = true; res += `
${escapeApplyToText(k)}
`; 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 || pathObj.body_excerpt || pathObj.category_id) ) { tooltipHtml = `
${pathObj.category_id ? `id: ${pathObj.category_id}` : ''} ${pathObj.rationale ? `${escapeApplyToText(pathObj.rationale)}` : ''} ${pathObj.body_excerpt ? `${escapeApplyToText(pathObj.body_excerpt)}` : ''}
`; } let htmlParts = ''; if (pathStr && elementStr) { htmlParts = `${escapeApplyToText(pathStr)}${escapeApplyToText(elementStr)}`; } else if (pathStr) { htmlParts = renderPathParts(pathStr); } if (htmlParts) { res += `${htmlParts}${tooltipHtml}`; } }); res += `
`; } }); if (typeof suggestApplyTo === 'string' && suggestApplyTo.trim()) { hasRows = true; res += `
最优
${renderPathParts(suggestApplyTo, true)}
`; } res += `
`; return hasRows ? 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, applyToData.suggest)}
`; } if (item.action && typeof item.action === 'object' && (item.action.description || item.action.reasoning)) { const actionDescription = item.action.description ? String(item.action.description).replace(//g, '>') : ''; const actionReasoning = item.action.reasoning ? String(item.action.reasoning).replace(//g, '>') : ''; html += `
action
${actionDescription ? ` ${actionDescription} ${actionReasoning ? `` : ''} ` : ''}
`; } // Stage rendering removed per request // Render effects if (item.effects && Array.isArray(item.effects) && item.effects.length > 0) { let effectsHtml = ''; item.effects.forEach(effectItem => { if (typeof effectItem === 'string') { effectsHtml += `
  • ${effectItem.replace(//g, '>')}
  • `; } else if (typeof effectItem === 'object' && effectItem !== null) { const stmt = effectItem.statement ? effectItem.statement.replace(//g, '>') : 'Effect'; let detailsHtml = ''; const excludeKeys = ['statement']; Object.entries(effectItem).forEach(([k, v]) => { if (!excludeKeys.includes(k) && v !== null && v !== undefined && v !== '' && (!Array.isArray(v) || v.length > 0)) { let valStr = ''; if (Array.isArray(v)) { valStr = ``; } else if (typeof v === 'object') { valStr = JSON.stringify(v); } else { valStr = String(v).replace(//g, '>'); } if (Array.isArray(v)) { detailsHtml += `
    ${k.replace(/_/g, ' ')}:
    ${valStr}
    `; } else { detailsHtml += `
    ${k.replace(/_/g, ' ')}: ${valStr}
    `; } } }); effectsHtml += `
  • ${stmt}
    ${detailsHtml}
  • `; } }); html += `
    effects
      ${effectsHtml}
    `; } // 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 : ''; const relation = isValid(io.relation) ? io.relation.replace(//g, '>') : ''; let content = ''; if (mod) { content += `${mod}`; } if (desc) { content += desc; } let extraHtml = ''; if (relation) { extraHtml = `
    ↳ relation: ${relation}
    `; } 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}${extraHtml}
    `; }).join(''); }; // Render steps array specially if (item.steps && Array.isArray(item.steps)) { const allCapabilities = (parentItem && parentItem.capability) || []; const escapeHtml = (s) => String(s).replace(//g, '>'); const minWidth = 1250; const renderAction = (src) => { if (!src) return '-'; if (src.action && src.action.description) { const description = escapeHtml(src.action.description); const reasoning = src.action.reasoning ? escapeHtml(src.action.reasoning) : ''; return ` ${description} ${reasoning ? `${reasoning}` : ''} `; } if (src.method) return escapeHtml(src.method); if (src.description) return escapeHtml(src.description); return '-'; }; const renderTools = (tools) => { if (!tools || !Array.isArray(tools) || tools.length === 0) return '-'; return tools.map(t => `${escapeHtml(t)}`).join(''); }; const renderEffects = (effects) => { if (!effects || !Array.isArray(effects) || effects.length === 0) return '-'; const renderKeyTag = (keyText) => `${keyText}`; return `
    ${effects.map(effect => { if (typeof effect === 'string') { return `
    ${renderKeyTag('效果')}${escapeHtml(effect)}
    `; } if (typeof effect !== 'object' || effect === null) return ''; const statement = effect.statement ? escapeHtml(effect.statement) : '效果'; const criteria = effect.criteria ? escapeHtml(effect.criteria) : ''; const judgeMethod = effect.judge_method ? escapeHtml(effect.judge_method) : ''; const negativeExamples = Array.isArray(effect.negative_examples) && effect.negative_examples.length > 0 ? `
    ` + effect.negative_examples.map(ex => `${escapeHtml(ex)}`).join('') + `
    ` : ''; return `
    ${statement}
    ${criteria ? `
    ${renderKeyTag('判断标准')}${criteria}
    ` : ''} ${judgeMethod ? `
    ${renderKeyTag('评判方式')}${judgeMethod}
    ` : ''} ${negativeExamples ? `
    ${renderKeyTag('负面示例')}${negativeExamples}
    ` : ''}
    `; }).join('')}
    `; }; const getStepCapabilities = (step) => { if (!step || !step.step_id) return []; return allCapabilities.filter(capability => { const refStepId = capability.workflow_step_ref && capability.workflow_step_ref.step_id; return refStepId === step.step_id || ( capability.capability_id && ( capability.capability_id === `c_${step.step_id}` || capability.capability_id.startsWith(`c_${step.step_id}_`) ) ); }); }; const matchedCapabilities = new Set(); const renderCapabilityColumns = (capability) => { const applyTo = capability && (capability.apply_to_draft || capability.apply_to_grounding || capability.apply_to); const suggestApplyTo = capability && capability.apply_to_draft ? null : capability && capability.suggest_apply_to; return ` ${capability && capability.capability_id ? `${escapeHtml(capability.capability_id)}` : '-'} ${capability && capability.inputs && capability.inputs.length > 0 ? renderDataObjList(capability.inputs) : '-'} ${renderAction(capability)} ${capability && capability.outputs && capability.outputs.length > 0 ? renderDataObjList(capability.outputs) : '-'}
    ${applyTo ? renderApplyToVal(applyTo, suggestApplyTo) : '-'}
    ${capability && capability.body ? escapeHtml(capability.body) : '-'}
    ${capability ? renderEffects(capability.effects) : '-'}
    ${capability ? renderTools(capability.tools) : '-'}
    `; }; html += `
    steps
    `; item.steps.forEach((step, stepIdx) => { const stepCapabilities = getStepCapabilities(step); stepCapabilities.forEach(capability => matchedCapabilities.add(capability)); const capabilitiesToRender = stepCapabilities.length > 0 ? stepCapabilities : [null]; const rowspan = capabilitiesToRender.length; capabilitiesToRender.forEach((capability, capabilityIdx) => { html += ` ${capabilityIdx === 0 ? ` ` : ''} ${renderCapabilityColumns(capability)} `; }); }); allCapabilities.filter(capability => !matchedCapabilities.has(capability)).forEach(capability => { html += ` ${renderCapabilityColumns(capability)} `; }); html += `
    序号 阶段 输入 动作 输出 作用域 做法 效果 工具
    ${step.order || stepIdx + 1} ${step.phase ? `${escapeHtml(step.phase)}` : '-'}
    - 独立能力
    `; } // 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', 'suggest_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 global Claude SDK checkbox if it's checked in the UI const cbClaudeSdk = document.getElementById('check-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}`); } }; window.toggleCaseFilter = async function (caseId, isRestore) { if (currentSelectedIndex === null) return; const reqIndex = currentSelectedIndex; let reason = "manual_delete"; if (!isRestore) { reason = prompt("请输入移除原因 (默认: manual_delete):", "manual_delete"); if (reason === null) return; // Cancelled if (!reason.trim()) reason = "manual_delete"; } try { const action = isRestore ? 'restore' : 'filter'; const res = await fetch(`/api/requirements/${reqIndex}/cases/${action}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ case_id: caseId, reason: reason }) }); if (!res.ok) { const err = await res.json(); alert('操作失败: ' + (err.detail || '未知错误')); return; } // Close modal and refresh data document.getElementById('case-detail-modal').classList.add('hidden'); document.getElementById('btn-refresh-data').click(); } catch (e) { console.error(e); alert('操作失败'); } }; init();