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 = '
`;
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 = `
📥 导入 Source `;
}
}
groupedHtml[groupKey] += `
${platBadge}
${scoreBadge}
${actionBtn}
${allImages.length > 0 ? `
` : ''}
${title}
${allImages.length === 0 ? snippetHtml : ''}
`;
} 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 += `
${renderPaneContent(['filtered_cases'], 'filtered_cases')}
`;
if (hasExternal) {
html += `
`;
html += `
📥 全部导入 Source
`;
html += renderPaneContent(['source_ex'], 'source_ex');
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 `
🧠 推理逻辑 (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 `
`;
}
let html = `
🎯 需求描述
${stratObj.requirement || ''}
`;
// New Workflow Format
if (stratObj.workflow) {
stratObj.workflow.forEach(strat => {
if (!strat.cluster_id && !strat.cluster_name && !strat.steps) {
html += `
`;
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 += `
${scoreHtml}
${(strat.关联案例 && strat.关联案例.length > 0) ? `
📌 关联案例
${renderCaseTags(strat.关联案例)}
` : ''}
🧱 工作流步骤
`;
if (strat.steps) strat.steps.forEach(step => {
html += `
步骤 ${step.步骤序号}
${step.步骤描述}
`;
});
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 += `
`;
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 += `
${scoreHtml}
📥 来源: ${strat.source || 'N/A'}
`;
if (strat.reasoning) html += `
🧠 推理逻辑 (Reasoning)
${strat.reasoning}
`;
if (strat.why_not) html += `
`;
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 += `
`;
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 = ``;
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 += `🧩 聚类结果 (Cluster) `;
bpHtml += `
`;
bpHtml += `
${workflowCasesHtml}
`;
bpHtml += `
${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 += `
`;
allImages.forEach((imgUrl, imgIdx) => {
const localPath = `/output/${ctx.reqId}/raw_cases/images/${cId}/${String(imgIdx).padStart(2, '0')}.jpg`;
mediaHtml += `
`;
});
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 = `
${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}
${window.renderStructuredData(wf && wf.workflow ? [wf.workflow] : null, 'workflow', wf)}
▶
✨ 提取的能力 (Capability)
${btnCapabilityHtml}
${window.renderStructuredData(wf && wf.capability ? wf.capability : null, 'capabilities', wf)}
`;
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 += `
`;
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 ? `${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 = `
` + v.map(vi => `${String(vi).replace(//g, '>')} `).join('') + ` `;
} 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 += `
`;
}
// 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 += `
`;
}
}
// 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 ? `
${step.order || stepIdx + 1}
${step.phase ? `${escapeHtml(step.phase)} ` : '-'}
` : ''}
${renderCapabilityColumns(capability)}
`;
});
});
allCapabilities.filter(capability => !matchedCapabilities.has(capability)).forEach(capability => {
html += `
-
独立能力
${renderCapabilityColumns(capability)}
`;
});
html += `
`;
}
// 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 += `
`;
}
});
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();