let requirements = [];
let currentSelectedIndex = null;
let activeRuns = {};
let statusInterval = null;
let currentAvailablePlatforms = ['xhs', 'youtube', 'bili', 'x'];
let currentPromptName = null;
const modalPrompts = document.getElementById('prompts-modal');
const elPromptList = document.getElementById('prompt-list');
const elPromptTextarea = document.getElementById('prompt-textarea');
const elSchemaTextarea = document.getElementById('schema-textarea');
const elPromptStatus = document.getElementById('prompt-save-status');
// DOM Elements
const elReqSelector = document.getElementById('req-selector');
const elStatsContainer = document.getElementById('stats-container');
const elMainContent = document.getElementById('main-content');
const elEmptyState = document.getElementById('empty-state');
const elDetailView = document.getElementById('detail-view');
const elStatusBanner = document.getElementById('status-banner');
const elStatusText = document.getElementById('status-text');
// Form logic
const selectForcePhase = document.getElementById('select-force-phase');
const groupPlatforms = document.getElementById('group-platforms');
if (selectForcePhase && groupPlatforms) {
selectForcePhase.addEventListener('change', (e) => {
const val = e.target.value;
if (val.startsWith('phase2') || val === 'phase3') {
groupPlatforms.style.display = 'none';
} else {
groupPlatforms.style.display = 'block';
}
});
}
const jsonStrategy = document.getElementById('json-strategy');
const jsonBlueprint = document.getElementById('json-blueprint');
const jsonCaps = document.getElementById('json-caps');
const jsonSource = document.getElementById('json-source');
const jsonRaw = document.getElementById('json-raw');
// Modals
const modalRun = document.getElementById('run-modal');
const modalLogs = document.getElementById('logs-modal');
const terminalLogs = document.getElementById('terminal-logs');
const PIPELINE_STEPS = [
{ id: 'research', label: '1.1 分布式爬取' },
{ id: 'source', label: '1.5 提取数据源' },
{ id: 'generate-case', label: '1.6 生成 case.json' },
{ id: 'workflow-extract', label: '1.6a 工作流提取' },
{ id: 'capability-extract-1', label: '1.6b 原子能力提取' },
{ id: 'apply-grounding', label: '1.7 场景映射' },
{ id: 'process-cluster', label: '2.1.1 工序聚类' },
{ id: 'process-score', label: '2.1.2 工序打分' },
{ id: 'capability-extract', label: '2.2.1 能力提取' },
{ id: 'capability-enrich', label: '2.2.2 能力丰富化' },
{ id: 'strategy', label: '3.0 策略组装' }
];
let chainStartNode = null;
let chainEndNode = null;
let currentPipelineStatus = {};
// Initialize
async function init() {
await fetchRequirements();
setupEventListeners();
startStatusPolling();
}
// Fetch Data
async function fetchRequirements() {
try {
const res = await fetch('/api/requirements');
requirements = await res.json();
requirements.sort((a, b) => b.index - a.index);
renderTaskList(requirements);
updateStats();
} catch (e) {
console.error("Failed to fetch requirements", e);
elReqSelector.innerHTML = '加载数据失败。请检查后端是否运行。 ';
}
}
function renderJSON(obj) {
if (obj === null) return `null `;
if (typeof obj === 'number') return `${obj} `;
if (typeof obj === 'boolean') return `${obj} `;
if (typeof obj === 'string') {
const escaped = obj.replace(//g, '>');
return `"${escaped}" `;
}
if (Array.isArray(obj)) {
if (obj.length === 0) return '[]';
let html = '
`;
const renderPaneContent = (pList) => {
let paneHtml = '';
let totalCases = 0;
let gridHtml = '';
let seenIds = new Set();
pList.forEach(p => {
if (!rawCasesObj[p]) return;
if (rawCasesObj[p].error) {
const safeRaw = rawCasesObj[p].raw_content ? rawCasesObj[p].raw_content.replace(//g, ">") : "文件为空。";
paneHtml += `
⚠️ ${p} JSON 解析失败
${safeRaw}
`;
return;
}
if (rawCasesObj[p].reason) {
paneHtml += `
🛑 ${p} 过滤原因: ${rawCasesObj[p].reason}
`;
}
const cases = Array.isArray(rawCasesObj[p]) ? rawCasesObj[p] : (rawCasesObj[p].cases || rawCasesObj[p].sources || []);
if (cases.length > 0) {
if (!rawCasesObj['source'] && p !== 'source_ex' && p !== 'filtered_cases' && p !== 'source') {
paneHtml += `
📝 ${p} 原始爬取数据 (未进行 1.5 数据源提取)
${renderJSON(rawCasesObj[p])}
`;
return;
}
cases.forEach((c, idx) => {
totalCases++;
const cId = c.case_id || (c._raw && c._raw.case_id) || (c.post && c.post.channel_content_id) || `temp_${p}_${idx}`;
const cUrl = c.source_url || c.url || (c.post && c.post.link) || '';
if (cId || cUrl || c.post) {
const mappedS = sourceMap[cId] || sourceMap[cUrl] || (c._raw && sourceMap[c._raw.case_id]);
if (p !== 'filtered_cases' && p !== 'source' && p !== 'source_ex' && !mappedS) return;
if (cId && seenIds.has(cId)) return;
if (cId) seenIds.add(cId);
const s = mappedS || c;
const post = s.post || s || {};
const images = post.images || [];
const xImages = post.image_url_list || [];
const ytThumbnails = post.thumbnails && post.thumbnails.length > 0 ? [post.thumbnails[post.thumbnails.length - 1].url] : [];
const allImages = [...images, ...xImages.map(img => img.image_url), ...ytThumbnails].filter(Boolean);
const coverImgUrl = allImages.length > 0 ? `/output/${reqId}/raw_cases/images/${cId}/00.jpg` : '';
const fallbackImgUrl = allImages.length > 0 ? allImages[0] : '';
const title = post.title || c.title || post.desc || '无标题';
const author = post.channel_account_name || s.author || '-';
const likes = post.like_count !== undefined ? post.like_count : (post.likes !== undefined ? post.likes : '-');
const snippetStr = (post.body_text || post.body || '').substring(0, 100);
const snippetHtml = snippetStr ? `
${snippetStr.replace(//g, ">")}...
` : '';
const platBadge = p.startsWith('case_') ? `
${p.replace('case_', '').toUpperCase()} ` : '';
let actionBtn = '';
if (p === 'source_ex') {
const isImported = !!sourceMap[cId] || !!sourceMap[cUrl];
if (isImported) {
actionBtn = `
✅ 已导入 `;
} else {
actionBtn = `
📥 导入 Source `;
}
}
gridHtml += `
${platBadge}
${actionBtn}
${allImages.length > 0 ? `
` : ''}
${title}
${allImages.length === 0 ? snippetHtml : ''}
`;
} else {
gridHtml += `
📝 旧版格式 / 解析失败 点击查看详情
`;
}
});
}
});
if (totalCases === 0 && pList.length > 0 && !paneHtml.includes('解析失败') && !paneHtml.includes('未进行')) {
paneHtml += `
暂无数据
`;
} else if (gridHtml) {
paneHtml += `
${gridHtml}
`;
}
return paneHtml;
};
html += `
${renderPaneContent([...channelPlatforms, 'source'])}
`;
if (hasFiltered) html += `
${renderPaneContent(['filtered_cases'])}
`;
if (hasExternal) {
html += `
`;
html += `
📥 全部导入 Source
`;
html += renderPaneContent(['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);
}
};
window.selectSubTab = function(p) {
document.querySelectorAll('#json-raw .sub-tab-btn').forEach(btn => {
btn.classList.remove('active');
if(btn.textContent === p.replace('case_', '').toUpperCase()) btn.classList.add('active');
});
document.querySelectorAll('#json-raw .sub-tab-pane').forEach(pane => {
pane.classList.add('hidden');
});
const target = document.getElementById(`sub-tab-${p}`);
if (target) target.classList.remove('hidden');
};
window.selectGenericSubTab = function(prefix, targetId) {
const parentContainer = document.getElementById(`container-${prefix}`);
if (!parentContainer) return;
parentContainer.querySelectorAll('.sub-tab-btn').forEach(btn => {
btn.classList.remove('active');
});
const activeBtn = parentContainer.querySelector(`[data-target="${targetId}"]`);
if (activeBtn) activeBtn.classList.add('active');
parentContainer.querySelectorAll('.sub-tab-pane').forEach(pane => {
pane.classList.add('hidden');
});
const activePane = document.getElementById(targetId);
if (activePane) activePane.classList.remove('hidden');
};
function renderWithSubTabs(dataMain, dataTemp, renderFn, tabPrefix) {
if (!dataTemp || Object.keys(dataTemp).length === 0) {
return renderDataOrRaw(dataMain, renderFn);
}
const mainHtml = renderDataOrRaw(dataMain, renderFn);
const tempHtml = renderDataOrRaw(dataTemp, renderFn);
return `
🧠 推理逻辑 (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 fetchMemo(index) {
const elTextarea = document.getElementById('memo-textarea');
const elStatus = document.getElementById('memo-status');
if(!elTextarea) return;
elTextarea.value = 'Loading...';
elTextarea.disabled = true;
try {
const res = await fetch(`/api/requirements/${index}/memo`);
const data = await res.json();
elTextarea.value = data.memo || '';
elStatus.textContent = '';
} catch (e) {
elTextarea.value = '';
console.error("Failed to fetch memo", e);
}
elTextarea.disabled = false;
}
async function fetchPromptsList() {
try {
const res = await fetch('/api/prompts');
let list = await res.json();
// Handle DAG nodes
document.querySelectorAll('.prompt-node').forEach(node => {
const p = node.dataset.prompt;
if (list.includes(p)) {
node.onclick = () => selectPrompt(p, node);
// Remove from list so it doesn't appear in "other"
list = list.filter(item => item !== p);
} else {
node.style.opacity = 0.5; // gray out if not found
node.style.cursor = 'not-allowed';
}
});
const elOtherPromptsList = document.getElementById('other-prompts-list');
if (elOtherPromptsList) {
elOtherPromptsList.innerHTML = '';
list.forEach((p) => {
const div = document.createElement('div');
div.className = 'prompt-tab';
div.textContent = p;
div.onclick = () => selectPrompt(p, div);
elOtherPromptsList.appendChild(div);
});
}
// Select the first prompt by default (maybe researcher.prompt)
const firstNode = document.querySelector('.prompt-node[data-prompt="researcher.prompt"]');
if (firstNode) {
selectPrompt('researcher.prompt', firstNode);
}
} catch (e) {
console.error("Failed to load prompts", e);
}
}
async function selectPrompt(name, tabEl) {
currentPromptName = name;
document.querySelectorAll('.prompt-tab').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.prompt-node').forEach(el => el.classList.remove('active'));
if (tabEl) tabEl.classList.add('active');
elPromptTextarea.value = 'Loading...';
elPromptTextarea.disabled = true;
if (elSchemaTextarea) {
elSchemaTextarea.value = 'Loading...';
elSchemaTextarea.disabled = true;
}
try {
const res = await fetch(`/api/prompts/${name}`);
const data = await res.json();
elPromptTextarea.value = data.content || '';
if (elSchemaTextarea) {
elSchemaTextarea.value = data.schema_content || '';
}
} catch (e) {
elPromptTextarea.value = 'Error loading prompt.';
if (elSchemaTextarea) elSchemaTextarea.value = '';
}
elPromptTextarea.disabled = false;
if (elSchemaTextarea) elSchemaTextarea.disabled = false;
elPromptStatus.textContent = '';
}
function renderAggregatedPerCaseData(cases, type) {
if (!cases || !Array.isArray(cases) || cases.length === 0) {
return '
暂无案例数据
';
}
let sidebarHtml = ``;
if (!hasData) {
return `
当前需求的所有案例均无提取的${type === 'workflow' ? '工序' : '能力'}
`;
}
return `
${sidebarHtml}${contentHtml}
`;
}
async function fetchRequirementData(index) {
try {
const res = await fetch(`/api/requirements/${index}/data`);
const data = await res.json();
let rawCasesClone = null;
let casesList = [];
if (data.raw_cases) {
rawCasesClone = { ...data.raw_cases };
const detailedCaseObj = data.raw_cases['case'] || data.raw_cases['case_detailed'];
if (detailedCaseObj && detailedCaseObj.cases) {
casesList = detailedCaseObj.cases;
}
}
if (jsonStrategy) jsonStrategy.innerHTML = ''; // Tab removed
jsonBlueprint.innerHTML = renderAggregatedPerCaseData(casesList, 'workflow');
jsonCaps.innerHTML = renderAggregatedPerCaseData(casesList, 'capabilities');
jsonRaw.innerHTML = renderDataOrRaw(rawCasesClone, renderRawCases);
const btnUpload = document.getElementById('btn-upload-source-ex');
const fileInput = document.getElementById('input-upload-source-ex');
if (btnUpload && fileInput) {
btnUpload.onclick = () => fileInput.click();
fileInput.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch(`/api/requirements/${index}/upload_source_ex`, {
method: 'POST',
body: formData
});
if (res.ok) {
alert('上传成功!');
fetchRequirementData(index);
} else {
alert('上传失败');
}
} catch (err) {
console.error(err);
alert('上传出错');
}
};
}
if (rawCasesClone && Object.keys(rawCasesClone).length > 0) {
currentAvailablePlatforms = Object.keys(rawCasesClone)
.filter(p => p.startsWith('case_') && p !== 'case_detailed' && p !== 'case')
.map(p => p.replace('case_', ''));
if (currentAvailablePlatforms.length === 0) {
currentAvailablePlatforms = ['xhs', 'youtube', 'bili', 'x'];
}
} else {
currentAvailablePlatforms = ['xhs', 'youtube', 'bili', 'x'];
}
} catch (e) {
console.error("Failed to fetch data", e);
}
}
async function pollStatus() {
try {
const res = await fetch('/api/pipeline/status');
const statusData = await res.json();
let needsListUpdate = false;
// Check if any status changed
for(const [idxStr, runInfo] of Object.entries(statusData)) {
const idx = parseInt(idxStr);
if (!activeRuns[idx] || activeRuns[idx].status !== runInfo.status) {
needsListUpdate = true;
}
activeRuns[idx] = runInfo;
// Update logs if modal is open for this index
if (currentSelectedIndex === idx && !modalLogs.classList.contains('hidden')) {
terminalLogs.textContent = runInfo.logs.join('');
terminalLogs.scrollTop = terminalLogs.scrollHeight;
}
// Update detail view banner if this is the selected one
if (currentSelectedIndex === idx) {
updateDetailBannerStatus(runInfo.status);
}
}
if (needsListUpdate) {
// update in requirements array
requirements.forEach(req => {
if (activeRuns[req.index]) {
req.status = activeRuns[req.index].status;
}
});
renderTaskList(requirements);
}
} catch (e) {
console.error("Failed to poll status", e);
}
}
function startStatusPolling() {
if (statusInterval) clearInterval(statusInterval);
statusInterval = setInterval(pollStatus, 2000);
}
// Render
function renderTaskList(list) {
elReqSelector.innerHTML = '
-- 选择一个需求 -- ';
list.forEach(req => {
const option = document.createElement('option');
option.value = req.index;
let statusMarker = '';
if (req.status === 'running') statusMarker = '🚀';
else if (req.status === 'completed') statusMarker = '✅';
else if (req.status === 'partial') statusMarker = '⚠️';
else statusMarker = '⏳';
option.textContent = `${statusMarker} [#${req.id}] ${req.requirement} (Cases: ${req.raw_cases_count})`;
if (currentSelectedIndex === req.index) {
option.selected = true;
}
elReqSelector.appendChild(option);
});
}
function updateStats() {
const total = requirements.length;
const completed = requirements.filter(r => r.status === 'completed').length;
const running = requirements.filter(r => r.status === 'running').length;
elStatsContainer.innerHTML = `
Total: ${total}
Done: ${completed}
${running > 0 ? `
Running: ${running} ` : ''}
`;
}
function selectRequirement(index) {
currentSelectedIndex = index;
const req = requirements.find(r => r.index === index);
if (!req) return;
// Sync dropdown if called from somewhere else
if (elReqSelector.value != index) {
elReqSelector.value = index;
}
// Update Detail UI
elEmptyState.classList.add('hidden');
elDetailView.classList.remove('hidden');
updateDetailBannerStatus(activeRuns[index] ? activeRuns[index].status : req.status);
// Fetch data
if (jsonStrategy) jsonStrategy.textContent = 'Loading...';
if (jsonBlueprint) jsonBlueprint.textContent = 'Loading...';
if (jsonCaps) jsonCaps.textContent = 'Loading...';
if (jsonRaw) jsonRaw.textContent = 'Loading...';
fetchRequirementData(index);
fetchMemo(index);
}
function updateDetailBannerStatus(status) {
const btnStop = document.getElementById('btn-stop-pipeline');
if (status === 'running') {
elStatusBanner.classList.remove('hidden');
elStatusBanner.style.background = 'rgba(59, 130, 246, 0.1)';
elStatusText.textContent = 'Pipeline is currently running...';
elStatusBanner.querySelector('.status-indicator').style.display = 'block';
elStatusBanner.querySelector('.status-indicator').style.background = 'var(--accent-primary)';
if (btnStop) btnStop.style.display = 'inline-block';
} else if (status === 'failed') {
elStatusBanner.classList.remove('hidden');
elStatusBanner.style.background = 'rgba(239, 68, 68, 0.1)';
elStatusText.textContent = 'Pipeline run failed.';
elStatusBanner.querySelector('.status-indicator').style.display = 'block';
elStatusBanner.querySelector('.status-indicator').style.background = 'var(--danger)';
if (btnStop) btnStop.style.display = 'none';
} else {
elStatusBanner.classList.add('hidden');
if (btnStop) btnStop.style.display = 'none';
}
}
// Actions
async function triggerRun() {
if (currentSelectedIndex === null) return;
const checkSdk = document.getElementById('check-claude-sdk').checked;
const mode = document.getElementById('select-force-phase').value;
const platforms = document.getElementById('input-platforms').value;
let only_step = null;
let start_from = null;
let end_at = null;
let restart_mode = null;
let phase = null;
if (mode === "custom_range") {
if (!chainStartNode) {
alert('请在下方链条中选择至少一个节点作为起点。');
return;
}
if (chainStartNode === chainEndNode || !chainEndNode) {
only_step = chainStartNode.replace(/-1$/, '');
} else {
start_from = chainStartNode.replace(/-1$/, '');
end_at = chainEndNode.replace(/-1$/, '');
}
} else if (mode.startsWith("step_")) {
const stepMap = {
"step_1.1": "research",
"step_1.5": "source",
"step_1.6": "generate-case",
"step_1.6a": "workflow-extract",
"step_1.6b": "capability-extract",
"step_1.7": "apply-grounding",
"step_2.1.1": "process-cluster",
"step_2.1.2": "process-score",
"step_2.2.1": "capability-extract",
"step_2.2.2": "capability-enrich",
"step_3.0": "strategy"
};
only_step = stepMap[mode];
if (mode === "step_1.1") {
restart_mode = "single_platforms"; // triggers backend's platform specific clearing
}
} else if (mode.startsWith("phase")) {
if (mode === "phase1") {
phase = 1;
} else if (mode === "phase2") {
phase = 2;
} else if (mode === "phase3") {
phase = 3;
}
} else {
restart_mode = "smart";
}
const groupPlatforms = document.getElementById('group-platforms');
const platformsToSend = groupPlatforms && groupPlatforms.style.display !== 'none' ? platforms : "";
const requestData = {
platforms: platformsToSend,
use_claude_sdk: checkSdk,
restart_mode: restart_mode,
phase: phase,
only_step: only_step,
start_from: start_from,
end_at: end_at
};
modalRun.classList.add('hidden');
// Optimistic UI update
const req = requirements.find(r => r.index === currentSelectedIndex);
if (req) req.status = 'running';
activeRuns[currentSelectedIndex] = { status: 'running', logs: ['Initializing request...'] };
renderTaskList(requirements);
updateDetailBannerStatus('running');
try {
const res = await fetch(`/api/pipeline/run/${currentSelectedIndex}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData)
});
if (!res.ok) {
const err = await res.json();
alert("Error: " + err.detail);
}
} catch (e) {
console.error("Run failed", e);
alert("Failed to trigger run");
}
}
// Event Listeners
function setupEventListeners() {
// Global JSON Toggle
const globalJsonToggle = document.getElementById('global-json-toggle');
if (globalJsonToggle) {
globalJsonToggle.addEventListener('change', (e) => {
const isRaw = e.target.checked;
document.querySelectorAll('.data-view-ui').forEach(el => el.style.display = isRaw ? 'none' : '');
document.querySelectorAll('.data-view-raw').forEach(el => el.style.display = isRaw ? '' : 'none');
});
}
// Dropdown change
elReqSelector.addEventListener('change', (e) => {
const val = e.target.value;
if (val) {
selectRequirement(parseInt(val));
} else {
currentSelectedIndex = null;
elEmptyState.classList.remove('hidden');
elDetailView.classList.add('hidden');
}
});
// Toggle Memo
document.getElementById('btn-toggle-memo').addEventListener('click', () => {
const memo = document.getElementById('memo-container');
memo.classList.toggle('hidden');
});
// Refresh Data without changing page position
const btnRefresh = document.getElementById('btn-refresh-data');
if (btnRefresh) {
btnRefresh.addEventListener('click', async () => {
if (currentSelectedIndex === null) {
alert("请先选择一个需求项目!");
return;
}
const oldText = btnRefresh.innerHTML;
btnRefresh.innerHTML = '🔄 刷新中...';
btnRefresh.disabled = true;
const modalCaseDetail = document.getElementById('case-detail-modal');
const isModalOpen = modalCaseDetail && !modalCaseDetail.classList.contains('hidden');
const activeSidebarItem = document.querySelector('.modal-sidebar-item.active');
const activeCaseIdx = activeSidebarItem ? parseInt(activeSidebarItem.id.replace('sidebar-item-', '')) : null;
try {
await fetchRequirementData(currentSelectedIndex);
if (isModalOpen && activeCaseIdx !== null && typeof window.renderSingleCaseDetail === 'function') {
window.renderSingleCaseDetail(activeCaseIdx);
}
} catch (e) {
console.error("Failed to refresh data", e);
} finally {
btnRefresh.innerHTML = oldText;
btnRefresh.disabled = false;
}
});
}
// Tabs
document.querySelectorAll('.tab-btn-pill').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn-pill').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
btn.classList.add('active');
document.getElementById(btn.dataset.target).classList.add('active');
});
});
// Modals
document.getElementById('btn-open-run-modal').addEventListener('click', async () => {
if (currentSelectedIndex !== null) {
modalRun.classList.remove('hidden');
const selectForcePhase = document.getElementById('select-force-phase');
if (selectForcePhase) selectForcePhase.dispatchEvent(new Event('change'));
const inputPlatforms = document.getElementById('input-platforms');
if (inputPlatforms && currentAvailablePlatforms && currentAvailablePlatforms.length > 0) {
inputPlatforms.value = currentAvailablePlatforms.join(',');
}
// Fetch status and render chain
await fetchAndRenderPipelineChain(currentSelectedIndex);
}
});
document.getElementById('btn-close-modal').addEventListener('click', () => {
modalRun.classList.add('hidden');
});
document.getElementById('btn-cancel-run').addEventListener('click', () => {
modalRun.classList.add('hidden');
});
const selectForcePhase = document.getElementById('select-force-phase');
const groupPlatforms = document.getElementById('group-platforms');
const chainContainer = document.getElementById('pipeline-chain-container');
if (selectForcePhase && groupPlatforms) {
selectForcePhase.addEventListener('change', (e) => {
const val = e.target.value;
if (['smart', 'phase1', 'step_1.1'].includes(val) || (val === 'custom_range' && chainStartNode === 'research')) {
groupPlatforms.style.display = 'block';
} else {
groupPlatforms.style.display = 'none';
}
if (val === 'custom_range') {
chainContainer.classList.remove('hidden');
} else {
chainContainer.classList.add('hidden');
}
});
}
document.getElementById('btn-reset-chain').addEventListener('click', () => {
chainStartNode = null;
chainEndNode = null;
renderPipelineChain();
});
// Add Requirement Modal Events
const modalAddReq = document.getElementById('add-req-modal');
const inputAddReq = document.getElementById('input-new-req');
document.getElementById('btn-add-req').addEventListener('click', () => {
inputAddReq.value = "";
modalAddReq.classList.remove('hidden');
});
document.getElementById('btn-close-add-req').addEventListener('click', () => {
modalAddReq.classList.add('hidden');
});
document.getElementById('btn-cancel-add-req').addEventListener('click', () => {
modalAddReq.classList.add('hidden');
});
document.getElementById('btn-submit-add-req').addEventListener('click', async () => {
const val = inputAddReq.value.trim();
if (!val) {
alert('需求内容不能为空');
return;
}
try {
const res = await fetch(`/api/requirements`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ requirement: val })
});
if (res.ok) {
modalAddReq.classList.add('hidden');
await fetchRequirements();
} else {
alert('添加失败');
}
} catch (e) {
console.error(e);
alert('网络错误');
}
});
const modalEditReq = document.getElementById('edit-req-modal');
const inputEditReq = document.getElementById('edit-req-textarea');
document.getElementById('btn-edit-req').addEventListener('click', () => {
if (currentSelectedIndex !== null) {
const req = requirements.find(r => r.index === currentSelectedIndex);
if (req) {
inputEditReq.value = req.requirement;
modalEditReq.classList.remove('hidden');
}
}
});
const closeEditReq = () => modalEditReq.classList.add('hidden');
document.getElementById('btn-close-edit-req').addEventListener('click', closeEditReq);
document.getElementById('btn-cancel-edit-req').addEventListener('click', closeEditReq);
const saveRequirementText = async (runAfter) => {
if (currentSelectedIndex === null) return;
const newText = inputEditReq.value.trim();
if (!newText) {
alert('需求文本不能为空');
return;
}
try {
const res = await fetch(`/api/requirements/${currentSelectedIndex}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ requirement: newText })
});
if (!res.ok) {
const err = await res.json();
alert('保存失败: ' + (err.detail || '未知错误'));
return;
}
// Update locally
const req = requirements.find(r => r.index === currentSelectedIndex);
if (req) {
req.requirement = newText;
elDetailTitle.textContent = newText;
renderTaskList(requirements);
}
closeEditReq();
if (runAfter) {
document.getElementById('btn-open-run-modal').click();
}
} catch (e) {
console.error(e);
alert('保存请求失败');
}
};
document.getElementById('btn-save-edit-req').addEventListener('click', () => saveRequirementText(false));
document.getElementById('btn-save-run-edit-req').addEventListener('click', () => saveRequirementText(true));
document.getElementById('btn-confirm-run').addEventListener('click', triggerRun);
document.getElementById('btn-view-logs').addEventListener('click', () => {
modalLogs.classList.remove('hidden');
if (activeRuns[currentSelectedIndex]) {
terminalLogs.textContent = activeRuns[currentSelectedIndex].logs.join('');
terminalLogs.scrollTop = terminalLogs.scrollHeight;
} else {
terminalLogs.textContent = 'No logs available.';
}
});
const btnStop = document.getElementById('btn-stop-pipeline');
if (btnStop) {
btnStop.addEventListener('click', async () => {
if (currentSelectedIndex === null) return;
if (!confirm('Are you sure you want to stop the running pipeline?')) return;
try {
const res = await fetch(`/api/pipeline/stop/${currentSelectedIndex}`, { method: 'POST' });
if (!res.ok) {
const err = await res.json();
alert("Error stopping pipeline: " + err.detail);
}
} catch (e) {
console.error("Failed to stop pipeline", e);
alert("Failed to stop pipeline");
}
});
}
document.getElementById('btn-close-logs').addEventListener('click', () => {
modalLogs.classList.add('hidden');
});
const btnSaveMemo = document.getElementById('btn-save-memo');
if (btnSaveMemo) {
btnSaveMemo.addEventListener('click', async () => {
if (currentSelectedIndex === null) return;
const elTextarea = document.getElementById('memo-textarea');
const elStatus = document.getElementById('memo-status');
elStatus.textContent = 'Saving...';
elStatus.style.color = 'var(--text-muted)';
try {
const res = await fetch(`/api/requirements/${currentSelectedIndex}/memo`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ memo: elTextarea.value })
});
if (res.ok) {
elStatus.textContent = 'Saved!';
elStatus.style.color = 'var(--success)';
setTimeout(() => elStatus.textContent = '', 2000);
const req = requirements.find(r => r.index === currentSelectedIndex);
if (req) {
req.memo = elTextarea.value;
renderTaskList(requirements);
}
} else {
throw new Error("Bad response");
}
} catch (e) {
console.error("Failed to save memo", e);
elStatus.textContent = 'Save failed';
elStatus.style.color = 'var(--danger)';
}
});
}
const btnOpenPrompts = document.getElementById('btn-open-prompts');
if (btnOpenPrompts) {
btnOpenPrompts.addEventListener('click', () => {
modalPrompts.classList.remove('hidden');
fetchPromptsList();
});
}
const btnClosePrompts = document.getElementById('btn-close-prompts');
if (btnClosePrompts) {
btnClosePrompts.addEventListener('click', () => {
modalPrompts.classList.add('hidden');
});
}
const btnSavePrompt = document.getElementById('btn-save-prompt');
if (btnSavePrompt) {
btnSavePrompt.addEventListener('click', async () => {
if (!currentPromptName) return;
elPromptStatus.textContent = 'Saving...';
elPromptStatus.style.color = 'var(--text-muted)';
try {
const reqBody = { content: elPromptTextarea.value };
if (elSchemaTextarea) reqBody.schema_content = elSchemaTextarea.value;
const res = await fetch(`/api/prompts/${currentPromptName}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(reqBody)
});
if (res.ok) {
elPromptStatus.textContent = 'Saved!';
elPromptStatus.style.color = 'var(--success)';
setTimeout(() => elPromptStatus.textContent = '', 2000);
} else {
const errData = await res.json();
throw new Error(errData.detail || "Failed to save");
}
} catch(e) {
elPromptStatus.textContent = e.message || 'Save failed';
elPromptStatus.style.color = 'var(--danger)';
}
});
}
}
// Boot
// ----------------------------------------------------
// Pipeline Chain Visualization Logic
// ----------------------------------------------------
async function fetchAndRenderPipelineChain(index) {
try {
const res = await fetch(`/api/requirements/${index}/pipeline-status`);
if (res.ok) {
currentPipelineStatus = await res.json();
} else {
currentPipelineStatus = {};
}
} catch (e) {
console.error("Failed to fetch pipeline status", e);
currentPipelineStatus = {};
}
chainStartNode = null;
chainEndNode = null;
renderPipelineChain();
}
function bindPipelineChainEvents() {
PIPELINE_STEPS.forEach((step) => {
const node = document.getElementById('node-' + step.id);
if (!node) return;
node.addEventListener('click', () => {
if (!chainStartNode) {
chainStartNode = step.id;
} else if (!chainEndNode) {
if (chainStartNode === step.id) {
chainEndNode = step.id; // Double click = single step mode
} else {
chainEndNode = step.id;
}
} else {
chainStartNode = step.id; // Reset and start new
chainEndNode = null;
}
renderPipelineChain();
// Toggle platforms group if research is selected
const groupPlatforms = document.getElementById('group-platforms');
if (groupPlatforms && document.getElementById('select-force-phase').value === 'custom_range') {
const startIndex = PIPELINE_STEPS.findIndex(s => s.id === chainStartNode);
const endIndex = chainEndNode ? PIPELINE_STEPS.findIndex(s => s.id === chainEndNode) : startIndex;
const finalStart = Math.min(startIndex, endIndex);
const finalEnd = Math.max(startIndex, endIndex);
if (finalStart === 0) { // 'research' is index 0
groupPlatforms.style.display = 'block';
} else {
groupPlatforms.style.display = 'none';
}
}
});
});
}
function renderPipelineChain() {
if (chainStartNode && chainEndNode) {
const startIndex = PIPELINE_STEPS.findIndex(s => s.id === chainStartNode);
const endIndex = PIPELINE_STEPS.findIndex(s => s.id === chainEndNode);
if (endIndex < startIndex) {
const tempId = chainStartNode;
chainStartNode = chainEndNode;
chainEndNode = tempId;
}
}
const LINEAR_PREFIX = ["research", "source", "generate-case", "workflow-extract", "capability-extract-1", "apply-grounding"];
const BRANCH_21 = ["process-cluster", "process-score"];
const BRANCH_22 = ["capability-extract", "capability-enrich"];
const STRATEGY = "strategy";
let activeSteps = new Set();
if (chainStartNode && !chainEndNode) {
activeSteps.add(chainStartNode);
} else if (chainStartNode && chainEndNode) {
const start = chainStartNode;
const end = chainEndNode;
const start_in_linear = LINEAR_PREFIX.includes(start);
const start_in_21 = BRANCH_21.includes(start);
const start_in_22 = BRANCH_22.includes(start);
const end_in_linear = LINEAR_PREFIX.includes(end);
const end_in_21 = BRANCH_21.includes(end);
const end_in_22 = BRANCH_22.includes(end);
const end_is_strategy = end === STRATEGY;
// 1. Linear Prefix
LINEAR_PREFIX.forEach(s => {
const s_idx = LINEAR_PREFIX.indexOf(s);
const start_idx = LINEAR_PREFIX.indexOf(start);
if (start_idx >= 0 && s_idx >= start_idx) {
if (end_in_linear) {
if (s_idx <= LINEAR_PREFIX.indexOf(end)) activeSteps.add(s);
} else {
activeSteps.add(s);
}
}
});
// 2. Branches
if (end_is_strategy || (!end_in_21 && !end_in_22 && !end_in_linear)) {
if (!start_in_21 && !start_in_22) {
BRANCH_21.forEach(s => activeSteps.add(s));
BRANCH_22.forEach(s => activeSteps.add(s));
} else if (start_in_21) {
const idx = BRANCH_21.indexOf(start);
BRANCH_21.slice(idx).forEach(s => activeSteps.add(s));
BRANCH_22.forEach(s => activeSteps.add(s));
} else if (start_in_22) {
const idx = BRANCH_22.indexOf(start);
BRANCH_22.slice(idx).forEach(s => activeSteps.add(s));
BRANCH_21.forEach(s => activeSteps.add(s));
}
} else if (end_in_21) {
const end_idx = BRANCH_21.indexOf(end);
const start_idx = start_in_21 ? BRANCH_21.indexOf(start) : 0;
BRANCH_21.slice(start_idx, end_idx + 1).forEach(s => activeSteps.add(s));
} else if (end_in_22) {
const end_idx = BRANCH_22.indexOf(end);
const start_idx = start_in_22 ? BRANCH_22.indexOf(start) : 0;
BRANCH_22.slice(start_idx, end_idx + 1).forEach(s => activeSteps.add(s));
}
// 3. Strategy
if (end_is_strategy) {
activeSteps.add("strategy");
}
}
PIPELINE_STEPS.forEach((step) => {
const node = document.getElementById('node-' + step.id);
if (!node) return;
node.className = 'chain-node';
if (currentPipelineStatus[step.id]) {
node.classList.add('completed');
} else {
node.classList.add('missing');
}
if (activeSteps.has(step.id)) {
if ((chainStartNode && !chainEndNode) || step.id === chainStartNode || step.id === chainEndNode) {
node.classList.add('selected');
} else {
node.classList.add('selected-range');
}
}
});
}
bindPipelineChainEvents();
// Case Detail Modal Listeners
const btnCloseCaseDetail = document.getElementById('btn-close-case-detail');
const modalCaseDetail = document.getElementById('case-detail-modal');
if (btnCloseCaseDetail && modalCaseDetail) {
btnCloseCaseDetail.addEventListener('click', () => {
modalCaseDetail.classList.add('hidden');
});
// Close on escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !modalCaseDetail.classList.contains('hidden')) {
modalCaseDetail.classList.add('hidden');
}
});
// Close on click outside
modalCaseDetail.addEventListener('click', (e) => {
if (e.target === modalCaseDetail) {
modalCaseDetail.classList.add('hidden');
}
});
}
window.openCaseDetail = function(p, initialIdx) {
if (!window._currentRawCasesContext) return;
const ctx = window._currentRawCasesContext;
// Determine the list of platforms to aggregate.
let platformsToAggregate = [p];
if (p !== 'filtered_cases' && p !== 'source_ex') {
const crawlerPlatforms = Object.keys(ctx.rawCasesObj).filter(k => k !== 'source' && k !== 'case_detailed' && k !== 'case' && k !== 'images' && k !== 'filtered_cases' && k !== 'source_ex');
platformsToAggregate = [...crawlerPlatforms, 'source'];
}
let casesList = [];
let globalInitialIdx = 0;
const seenIds = new Set();
platformsToAggregate.forEach(plat => {
if (!ctx.rawCasesObj[plat]) return;
const platCases = ctx.rawCasesObj[plat].cases || ctx.rawCasesObj[plat].sources || [];
platCases.forEach((c, idx) => {
const cId = c.case_id || (c._raw && c._raw.case_id) || (c.post && c.post.channel_content_id) || `temp_${plat}_${idx}`;
if (seenIds.has(cId)) return;
seenIds.add(cId);
const augmentedC = { ...c, _actualPlatform: plat };
if (plat === p && idx === initialIdx) {
globalInitialIdx = casesList.length;
}
casesList.push(augmentedC);
});
});
window._currentModalCases = casesList;
window._currentModalPlatform = p;
window._currentModalContext = ctx;
window._currentModalIdx = globalInitialIdx;
// Build Sidebar
let sidebarHtml = '';
// Build Main Content Skeleton
const mainHtml = `
`;
document.getElementById('case-detail-modal-body').innerHTML = sidebarHtml + mainHtml;
// Render the selected case
window.renderSingleCaseDetail(globalInitialIdx);
document.getElementById('case-detail-modal').classList.remove('hidden');
// Scroll sidebar to active item
setTimeout(() => {
const activeItem = document.getElementById(`sidebar-item-${globalInitialIdx}`);
if (activeItem) activeItem.scrollIntoView({ block: 'nearest' });
}, 10);
};
window.renderSingleCaseDetail = function(idx) {
window._currentModalIdx = idx;
const ctx = window._currentModalContext;
const c = window._currentModalCases[idx];
if (!c) return;
// Update Sidebar Active State
document.querySelectorAll('.modal-sidebar-item').forEach(el => el.classList.remove('active'));
const activeEl = document.getElementById(`sidebar-item-${idx}`);
if (activeEl) activeEl.classList.add('active');
const p = c._actualPlatform || window._currentModalPlatform;
const platCode = p.replace('case_', '');
const cId = c.case_id || (c._raw && c._raw.case_id) || (c.post && c.post.channel_content_id) || `temp_${idx}`;
const cUrl = c.source_url || c.url || (c.post && c.post.link) || '';
const mappedS = ctx.sourceMap[cId] || ctx.sourceMap[cUrl] || (c._raw && ctx.sourceMap[c._raw.case_id]);
const s = mappedS || c;
const post = s.post || s || {};
const platformName = s.platform || (s._raw && s._raw.platform) || platCode;
const title = post.title || c.title || '无标题';
const workflowUrl = s.source_url || s.url || cUrl;
// Header
const headerHtml = `
${title}
${workflowUrl ? `
原文 ↗ ` : ''}
平台: ${platformName}
`;
document.getElementById('modal-main-header').innerHTML = headerHtml;
// Media & Body
let mediaHtml = '';
const images = post.images || [];
const xImages = post.image_url_list || [];
const ytThumbnails = post.thumbnails && post.thumbnails.length > 0 ? [post.thumbnails[post.thumbnails.length - 1].url] : [];
const allImages = [...images, ...xImages.map(img => img.image_url), ...ytThumbnails].filter(Boolean);
if (allImages.length > 0) {
mediaHtml += `
`;
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}
` : '
无正文
'}
`;
// 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.capabilities ? wf.capabilities : 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.main_action) {
actionStr = item.action.main_action;
} else if (item.method && !item.method.includes('[')) {
actionStr = item.method;
} else if (item.steps && Array.isArray(item.steps)) {
actionStr = item.steps.map(s => {
if (s.action && s.action.main_action) {
return s.action.mechanism ? `[${s.action.main_action}] ${s.action.mechanism}` : s.action.main_action;
}
return '未知';
}).join(' ➔ ');
}
title = buildFullTitle(item.inputs, item.outputs, actionStr, item.method || item.name || `节点 ${idx + 1}`);
} else {
const escapeHtml = (s) => String(s).replace(//g, '>');
title = escapeHtml(item.method || item.name || '');
if (!title && item.action && item.action.main_action) {
const actText = item.action.mechanism ? `[${item.action.main_action}] ${item.action.mechanism}` : item.action.main_action;
title = escapeHtml(actText);
}
if (!title) {
title = escapeHtml(type === 'workflow' ? '工作流' : `节点 ${idx + 1}`);
}
}
// Tree node tags (from apply_to keys) or unstructured_what fallback
const getApplyToField = (it) => {
if (it.apply_to_grounding) return { key: 'apply_to_grounding', val: it.apply_to_grounding };
if (it.apply_to_draft) return { key: 'apply_to_draft', val: it.apply_to_draft };
if (it.apply_to) return { key: 'apply_to', val: it.apply_to };
return null;
};
const applyToData = getApplyToField(item);
let treeNodeTags = '';
if (applyToData && typeof applyToData.val === 'object') {
const allLeafs = [];
Object.values(applyToData.val).forEach(v => {
if (Array.isArray(v)) {
v.forEach(pathObj => {
let leaf = '';
if (typeof pathObj === 'object' && pathObj !== null) {
if (pathObj.element) leaf = pathObj.element;
else if (pathObj.category_path || pathObj.path) {
leaf = (pathObj.category_path || pathObj.path).split('/').pop();
}
} else {
leaf = String(pathObj).split('/').pop();
}
if (leaf) allLeafs.push(leaf);
});
}
});
const uniqueLeafs = [...new Set(allLeafs)];
const badgeStyle = 'background: #f8fafc; color: #475569; border: 1px solid #cbd5e1; border-radius: 12px; padding: 2px 10px; font-size: 0.85em; font-weight: normal; margin-right: 6px; display: inline-block; white-space: nowrap;';
treeNodeTags = uniqueLeafs.map(leaf => `
${leaf.replace(//g, '>')} `).join('');
} else if (item.unstructured_what && Array.isArray(item.unstructured_what)) {
const badgeStyle = 'background: #f8fafc; color: #475569; border: 1px solid #cbd5e1; border-radius: 12px; padding: 2px 10px; font-size: 0.85em; font-weight: normal; margin-right: 6px; display: inline-block; white-space: nowrap;';
treeNodeTags = item.unstructured_what.map(t => `
${String(t).replace(//g, '>')} `).join('');
}
html += `
`;
const renderApplyToVal = (valObj, isIdeal = false) => {
if (!valObj || typeof valObj !== 'object') return '-';
let res = '
';
Object.entries(valObj).forEach(([k, v]) => {
if (Array.isArray(v) && v.length > 0) {
res += `
${k}
`;
let idealBadges = [];
v.forEach(pathObj => {
let pathStr = '';
let elementStr = '';
if (typeof pathObj === 'object' && pathObj !== null) {
pathStr = pathObj.category_path || pathObj.path || '';
elementStr = pathObj.element || '';
} else {
pathStr = String(pathObj);
}
let tooltipHtml = '';
if (typeof pathObj === 'object' && pathObj !== null && pathObj.rationale) {
tooltipHtml = `
${pathObj.category_id ? `id: ${pathObj.category_id} ` : ''}
${pathObj.rationale.replace(//g, '>')}
`;
}
let htmlParts = '';
if (pathStr && elementStr) {
htmlParts = `
${pathStr} ${elementStr} `;
} else if (pathStr) {
const parts = pathStr.split('/');
const leaf = parts.pop();
const prefix = parts.length > 0 ? parts.join('/') + '/' : '';
htmlParts = `
${prefix ? `
${prefix} ` : ''}
${leaf}
`;
}
if (htmlParts) {
res += `
${htmlParts}${tooltipHtml} `;
}
if (typeof pathObj === 'object' && pathObj !== null && pathObj.ideal_path) {
let fullNormalPath = pathStr || '';
if (elementStr) {
if (!fullNormalPath.endsWith('/')) fullNormalPath += '/';
fullNormalPath += elementStr;
}
if (fullNormalPath !== pathObj.ideal_path) {
const normalParts = fullNormalPath.split('/');
const idealParts = pathObj.ideal_path.split('/');
const idealLeaf = idealParts.pop();
let prefixHtml = '';
if (idealParts.length > 0) {
prefixHtml += `
`;
for (let i = 0; i < idealParts.length; i++) {
if (i === 0 && idealParts[0] === '') {
prefixHtml += '/';
continue;
}
const p = idealParts[i];
const isNew = i >= normalParts.length || p !== normalParts[i];
if (isNew) {
prefixHtml += `${p} /`;
} else {
prefixHtml += `${p}/`;
}
}
prefixHtml += ` `;
}
const isLeafNew = idealParts.length >= normalParts.length || idealLeaf !== normalParts[idealParts.length];
const leafHtml = isLeafNew
? `
${idealLeaf} `
: `
${idealLeaf} `;
const idealHtml = `${prefixHtml}${leafHtml}`;
idealBadges.push(`
${idealHtml}${tooltipHtml} `);
}
}
});
if (idealBadges.length > 0) {
res += `
` + idealBadges.join('');
}
res += `
`;
}
});
res += `
`;
return res === '
' ? '-' : res;
};
// Render apply_to / apply_to_grounding at workflow level (if it exists)
if (applyToData && typeof applyToData.val === 'object' && Object.keys(applyToData.val).length > 0) {
html += `
${applyToData.key}
${renderApplyToVal(applyToData.val)}
`;
}
// Stage rendering removed per request
// Render effects
if (item.effects && Array.isArray(item.effects) && item.effects.length > 0) {
html += `
effects
${item.effects.map(li => `${li.replace(//g, '>')} `).join('')}
`;
}
// Render confidence fields
const formatDate = (ts) => {
if (!ts) return '-';
if (typeof ts === 'string' && ts.includes('-')) return ts;
const num = Number(ts);
if (isNaN(num) || num <= 0) return '-';
const d = new Date(num > 10000000000 ? num : num * 1000);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
};
html += `
置信度
Maturity:
${String((item.maturity || (parentItem && parentItem.maturity)) || '-').replace(//g, '>')}
Validation:
${(item.validation_count !== undefined && item.validation_count !== null) ? String(item.validation_count).replace(//g, '>') : (parentItem && parentItem.validation_count !== undefined && parentItem.validation_count !== null) ? String(parentItem.validation_count).replace(//g, '>') : '-'}
Published:
${formatDate(item.published_at || (parentItem && parentItem.published_at))}
Last Verified:
${formatDate(item.last_verified_at || (parentItem && parentItem.last_verified_at))}
Created:
${formatDate(item.created_at || (parentItem && parentItem.created_at))}
Updated:
${formatDate(item.updated_at || (parentItem && parentItem.updated_at))}
`;
// Render feedback if available
const feedbackVal = item.feedback || (parentItem && parentItem.feedback);
if (feedbackVal) {
let feedbackHtml = '';
if (typeof feedbackVal === 'object' && feedbackVal !== null) {
Object.entries(feedbackVal).forEach(([k, v]) => {
if (v !== null && v !== undefined && String(v).trim() !== '') {
feedbackHtml += `
${k}:
${v}
`;
}
});
if (feedbackHtml !== '') {
feedbackHtml = `
${feedbackHtml}
`;
}
} else if (typeof feedbackVal === 'string' && feedbackVal.trim() !== '') {
feedbackHtml = `
${String(feedbackVal).replace(//g, '>')}
`;
}
if (feedbackHtml !== '') {
html += `
`;
}
}
// Render body
if (item.body && typeof item.body === 'string') {
html += `
body
${item.body.replace(//g, '>')}
`;
}
// Helper for inputs/outputs (Moved up so it can be used by steps)
const renderDataObjList = (list) => {
const isValid = (v) => v !== null && v !== undefined && String(v).toLowerCase() !== 'null' && String(v).toLowerCase() !== 'none' && String(v).trim() !== '';
return list.map(io => {
const desc = isValid(io.description) ? io.description.replace(//g, '>') : '';
const mod = isValid(io.modality) ? io.modality : '';
let content = '';
if (mod) {
content += `
${mod} `;
}
if (desc) {
content += desc;
}
if (!content) {
const keys = Object.keys(io);
if (keys.length === 1 && typeof io[keys[0]] === 'string') {
content = `
${keys[0]} ${io[keys[0]]} `;
} else {
content = `
未知 `;
}
}
return `
${content}
`;
}).join('');
};
// Render steps array specially
if (item.steps && Array.isArray(item.steps)) {
html += `
steps
序号
阶段
操作流
输入
动作
输出
作用域
做法
工具
`;
const escapeHtml = (s) => String(s).replace(//g, '>');
item.steps.forEach((step, stepIdx) => {
let actionText = '未知';
if (step.action && step.action.main_action) {
actionText = step.action.mechanism ? `[${step.action.main_action}] ${step.action.mechanism}` : step.action.main_action;
} else if (step.method) {
actionText = step.method;
} else if (step.description) {
actionText = step.description;
}
let stepTitle = '';
const hasValidIO = (arr) => Array.isArray(arr) && arr.length > 0 && (arr[0].role || arr[0].description);
if (hasValidIO(step.inputs) || hasValidIO(step.outputs)) {
stepTitle = buildFullTitle(step.inputs, step.outputs, actionText, step.method || step.description || `步骤 ${step.order || stepIdx + 1}`);
} else {
stepTitle = escapeHtml(step.method || step.description || '');
if (!stepTitle) stepTitle = escapeHtml(actionText);
if (!stepTitle || stepTitle === '未知') stepTitle = escapeHtml(`步骤 ${step.order || stepIdx + 1}`);
}
let actionHtml = escapeHtml(actionText);
if (step.action && step.action.main_action) {
const badgeHtml = `${escapeHtml(step.action.main_action)} `;
actionHtml = step.action.mechanism ? badgeHtml + escapeHtml(step.action.mechanism) : badgeHtml;
} else {
const match = actionText.match(/^\[(.*?)\]\s*(.*)$/);
if (match) {
actionHtml = `${escapeHtml(match[1])} ${escapeHtml(match[2])}`;
}
}
let toolsHtml = '';
if (step.tools && step.tools.length > 0) {
toolsHtml = step.tools.map(t => `${escapeHtml(t)} `).join('');
}
html += `
▶ ${step.order || stepIdx + 1}
${step.phase ? `${escapeHtml(step.phase)} ` : '-'}
${stepTitle}
${step.inputs && Array.isArray(step.inputs) && step.inputs.length > 0 ? renderDataObjList(step.inputs) : '-'}
${actionHtml}
${step.outputs && Array.isArray(step.outputs) && step.outputs.length > 0 ? renderDataObjList(step.outputs) : '-'}
${renderApplyToVal(step.apply_to_draft || step.apply_to_grounding || step.apply_to)}
${step.body ? escapeHtml(step.body) : '-'}
${toolsHtml || '-'}
`;
});
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',
'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 Claude SDK if it's checked in the UI
const cbClaudeSdk = document.getElementById('cb-use-claude-sdk');
if (cbClaudeSdk) {
payload.use_claude_sdk = cbClaudeSdk.checked;
}
const res = await fetch(`/api/pipeline/run/${currentSelectedIndex}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (res.ok) {
alert(`✅ 单Case重跑已触发 (${step})!请在 Pipeline 终端查看进度。`);
// Show the logs modal instead of non-existent logs tab
const btnViewLogs = document.getElementById('btn-view-logs');
if (btnViewLogs) {
btnViewLogs.click();
}
const modalCaseDetail = document.getElementById('case-detail-modal');
if (modalCaseDetail) modalCaseDetail.classList.add('hidden');
} else {
const err = await res.json();
alert(`启动重跑失败: ${err.detail || JSON.stringify(err)}`);
}
} catch(e) {
console.error("Error triggering single case rerun:", e);
alert(`发生错误: ${e.message}`);
}
};
init();