|
|
@@ -177,14 +177,19 @@ function renderRawCases(rawCasesObj) {
|
|
|
const detailedCaseObj = rawCasesObj['case'] || rawCasesObj['case_detailed'];
|
|
|
if (detailedCaseObj) {
|
|
|
const cd = detailedCaseObj;
|
|
|
- let calcWorkflow = 0;
|
|
|
- let calcCapabilities = 0;
|
|
|
+ let uniqueCases = new Set();
|
|
|
+ let uniqueWorkflow = new Set();
|
|
|
+ let uniqueCapabilities = new Set();
|
|
|
+
|
|
|
if (cd.cases) {
|
|
|
cd.cases.forEach(c => {
|
|
|
const cId = c.case_id || (c._raw && c._raw.case_id);
|
|
|
const cUrl = c.source_url || c.url;
|
|
|
- if (c.workflow) calcWorkflow++;
|
|
|
- if (c.capabilities && c.capabilities.length > 0) calcCapabilities++;
|
|
|
+ const uniqueKey = cId || cUrl || Math.random().toString();
|
|
|
+ uniqueCases.add(uniqueKey);
|
|
|
+
|
|
|
+ if (c.workflow) uniqueWorkflow.add(uniqueKey);
|
|
|
+ if (c.capabilities && c.capabilities.length > 0) uniqueCapabilities.add(uniqueKey);
|
|
|
|
|
|
if (cId) {
|
|
|
if (c.workflow) detailMap[cId] = { ...detailMap[cId], workflow: c.workflow };
|
|
|
@@ -196,13 +201,15 @@ function renderRawCases(rawCasesObj) {
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
- if (cd.total !== undefined) {
|
|
|
- const displayWorkflowSuccess = cd.workflow_success !== undefined && cd.workflow_success !== null ? cd.workflow_success : (cd.success !== undefined && cd.success !== null ? cd.success : calcWorkflow);
|
|
|
- const displayCapabilitiesSuccess = cd.capabilities_success !== undefined && cd.capabilities_success !== null ? cd.capabilities_success : calcCapabilities;
|
|
|
-
|
|
|
+
|
|
|
+ const displayTotal = uniqueCases.size > 0 ? uniqueCases.size : (cd.total !== undefined ? cd.total : 0);
|
|
|
+ const displayWorkflowSuccess = uniqueCases.size > 0 ? uniqueWorkflow.size : (cd.workflow_success !== undefined && cd.workflow_success !== null ? cd.workflow_success : (cd.success !== undefined && cd.success !== null ? cd.success : 0));
|
|
|
+ const displayCapabilitiesSuccess = uniqueCases.size > 0 ? uniqueCapabilities.size : (cd.capabilities_success !== undefined && cd.capabilities_success !== null ? cd.capabilities_success : 0);
|
|
|
+
|
|
|
+ if (cd.total !== undefined || uniqueCases.size > 0) {
|
|
|
totalStatsHtml = `<div style="display:flex; gap:1rem; margin-bottom:1rem; padding:0.5rem 1rem; background:rgba(0, 0, 0, 0.03); border-radius:8px; align-items:center;">
|
|
|
<div style="flex:1; text-align:center; display:flex; flex-direction:column; gap:2px;">
|
|
|
- <span style="font-size:1.3rem; color:var(--text-main); font-weight:bold; line-height:1;">${cd.total}</span>
|
|
|
+ <span style="font-size:1.3rem; color:var(--text-main); font-weight:bold; line-height:1;">${displayTotal}</span>
|
|
|
<span style="color:var(--text-muted); font-size:0.75rem;">未被过滤的帖子总数</span>
|
|
|
</div>
|
|
|
<div style="flex:1; text-align:center; display:flex; flex-direction:column; gap:2px;">
|
|
|
@@ -266,6 +273,8 @@ function renderRawCases(rawCasesObj) {
|
|
|
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) {
|
|
|
@@ -305,9 +314,28 @@ function renderRawCases(rawCasesObj) {
|
|
|
</div>`;
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
cases.forEach((c, idx) => {
|
|
|
- totalCases++;
|
|
|
+ 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) || '';
|
|
|
|
|
|
@@ -330,7 +358,7 @@ function renderRawCases(rawCasesObj) {
|
|
|
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 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);
|
|
|
@@ -338,6 +366,13 @@ function renderRawCases(rawCasesObj) {
|
|
|
|
|
|
const platBadge = p.startsWith('case_') ? `<span class="tag" style="position:absolute; top:8px; left:8px; background:rgba(0,0,0,0.6); color:#fff; padding:2px 6px; border-radius:4px; font-size:0.7em; z-index:2; backdrop-filter: blur(4px); border: 1px solid rgba(255,255,255,0.2);">${p.replace('case_', '').toUpperCase()}</span>` : '';
|
|
|
|
|
|
+ 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 = `<div style="position:absolute; top:8px; right:8px; background:rgba(0,0,0,0.7); color:${color}; font-weight:bold; padding:2px 8px; border-radius:12px; font-size:0.8em; z-index:2; backdrop-filter: blur(4px); border: 1px solid rgba(255,255,255,0.2);">⭐️ ${score}</div>`;
|
|
|
+ }
|
|
|
+
|
|
|
let actionBtn = '';
|
|
|
if (p === 'source_ex') {
|
|
|
const isImported = !!sourceMap[cId] || !!sourceMap[cUrl];
|
|
|
@@ -350,6 +385,7 @@ function renderRawCases(rawCasesObj) {
|
|
|
|
|
|
groupedHtml[groupKey] += `<div class="masonry-card" style="position:relative;" onclick="openCaseDetail('${p}', ${idx})">
|
|
|
${platBadge}
|
|
|
+ ${scoreBadge}
|
|
|
${actionBtn}
|
|
|
${allImages.length > 0 ? `<img class="cover-img" src="${coverImgUrl}" onerror="this.onerror=null; this.src='${fallbackImgUrl}';">` : ''}
|
|
|
<div class="masonry-card-info">
|
|
|
@@ -367,9 +403,6 @@ function renderRawCases(rawCasesObj) {
|
|
|
</div>`;
|
|
|
}
|
|
|
});
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
if (totalCases === 0 && pList.length > 0 && !paneHtml.includes('解析失败') && !paneHtml.includes('未进行')) {
|
|
|
paneHtml += `<div style="padding:1rem; color:var(--text-muted); text-align:center;">暂无数据</div>`;
|
|
|
} else {
|
|
|
@@ -437,18 +470,126 @@ window.importAllExternalCases = async function() {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-window.selectSubTab = function(p) {
|
|
|
+function selectSubTab(tabName) {
|
|
|
document.querySelectorAll('#json-raw .sub-tab-btn').forEach(btn => {
|
|
|
btn.classList.remove('active');
|
|
|
- if(btn.textContent === p.replace('case_', '').toUpperCase()) btn.classList.add('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');
|
|
|
});
|
|
|
- const target = document.getElementById(`sub-tab-${p}`);
|
|
|
- if (target) target.classList.remove('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 `<div style="color:var(--text-muted); padding:2rem; text-align:center;">暂无聚类结果数据,请导入 JSON 文件</div>`;
|
|
|
+ }
|
|
|
+
|
|
|
+ let html = `<div style="margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center;">
|
|
|
+ <button class="btn btn-danger btn-small" onclick="deleteSelectedClusters()" style="background-color: var(--danger); border-color: var(--danger); color: white;">🗑️ 删除选中项</button>
|
|
|
+ <button class="btn btn-secondary btn-small" onclick="clearAllClusters()">🧹 清空全部</button>
|
|
|
+ </div>`;
|
|
|
+
|
|
|
+ html += `<div style="display:flex; flex-direction:column; gap:1rem;">`;
|
|
|
+
|
|
|
+ const items = Array.isArray(clusterData) ? clusterData : Object.entries(clusterData).map(([k,v]) => ({key: k, value: v}));
|
|
|
+
|
|
|
+ items.forEach((item, idx) => {
|
|
|
+ const displayData = Array.isArray(clusterData) ? item : item.value;
|
|
|
+ const displayKey = Array.isArray(clusterData) ? `导入记录 #${idx + 1}` : `Key: ${item.key}`;
|
|
|
+
|
|
|
+ html += `<div style="background: rgba(0,0,0,0.02); border: 1px solid var(--border-glass); border-radius: 8px; padding: 1rem;">
|
|
|
+ <div style="display:flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; border-bottom: 1px solid rgba(0,0,0,0.05); padding-bottom: 0.5rem;">
|
|
|
+ <label style="display:flex; align-items:center; gap:8px; cursor:pointer; font-weight:bold; color:var(--text-main);">
|
|
|
+ <input type="checkbox" class="cluster-checkbox" data-idx="${idx}" style="cursor:pointer; width:16px; height:16px;">
|
|
|
+ ${displayKey}
|
|
|
+ </label>
|
|
|
+ <button class="btn btn-secondary btn-small" onclick="deleteSingleCluster(${idx})" style="color:var(--danger); border-color:rgba(239, 68, 68, 0.2); background:rgba(239, 68, 68, 0.05);">❌ 删除此项</button>
|
|
|
+ </div>
|
|
|
+ <div style="background: #1e1e1e; color: #d4d4d4; padding: 1rem; border-radius: 6px; font-family: monospace; font-size: 0.85rem; overflow-x: auto; max-height: 400px;">
|
|
|
+ ${renderJSON(displayData)}
|
|
|
+ </div>
|
|
|
+ </div>`;
|
|
|
+ });
|
|
|
+
|
|
|
+ html += `</div>`;
|
|
|
+ return html;
|
|
|
+}
|
|
|
+
|
|
|
window.selectGenericSubTab = function(prefix, targetId) {
|
|
|
const parentContainer = document.getElementById(`container-${prefix}`);
|
|
|
if (!parentContainer) return;
|
|
|
@@ -947,9 +1088,24 @@ function renderAggregatedPerCaseData(cases, type) {
|
|
|
let hasData = false;
|
|
|
let displayIndex = 1;
|
|
|
|
|
|
- cases.forEach((c, idx) => {
|
|
|
+ // Sort cases by score
|
|
|
+ const sortedCases = [...cases].sort((a, b) => {
|
|
|
+ const aId = a.case_id || (a._raw && a._raw.case_id);
|
|
|
+ const bId = b.case_id || (b._raw && b._raw.case_id);
|
|
|
+ const sourceMap = window._currentRawCasesContext ? window._currentRawCasesContext.sourceMap || {} : {};
|
|
|
+ const aMapped = sourceMap[aId] || {};
|
|
|
+ const bMapped = sourceMap[bId] || {};
|
|
|
+ const aScore = aMapped.evaluation && aMapped.evaluation.quality ? (aMapped.evaluation.quality.overall_score || 0) : 0;
|
|
|
+ const bScore = bMapped.evaluation && bMapped.evaluation.quality ? (bMapped.evaluation.quality.overall_score || 0) : 0;
|
|
|
+ return bScore - aScore;
|
|
|
+ });
|
|
|
+
|
|
|
+ sortedCases.forEach((c, idx) => {
|
|
|
const cId = c.case_id || (c._raw && c._raw.case_id) || `temp_${idx}`;
|
|
|
- const title = (c.post && c.post.title) || c.title || cId || `案例 ${idx + 1}`;
|
|
|
+ const sourceMap = window._currentRawCasesContext ? window._currentRawCasesContext.sourceMap || {} : {};
|
|
|
+ const mappedS = sourceMap[cId] || {};
|
|
|
+ const postObj = mappedS.post || c.post || c || {};
|
|
|
+ const title = postObj.title || c.title || postObj.desc || (postObj.body_text ? postObj.body_text.substring(0, 30) + '...' : '') || cId || `案例 ${idx + 1}`;
|
|
|
|
|
|
hasData = true; // Always render if there is a case, so the user can click Rerun.
|
|
|
let items = null;
|
|
|
@@ -970,6 +1126,13 @@ function renderAggregatedPerCaseData(cases, type) {
|
|
|
publishStrHtml = `<span style="font-size: 0.75em; font-weight: normal; color: #94a3b8; background: #f1f5f9; border: 1px solid #e2e8f0; padding: 2px 8px; border-radius: 12px; margin-left: 8px;">发布于: ${dateStr}</span>`;
|
|
|
}
|
|
|
|
|
|
+ const score = mappedS.evaluation && mappedS.evaluation.quality ? mappedS.evaluation.quality.overall_score : null;
|
|
|
+ let scoreBadge = '';
|
|
|
+ if (score !== null && score !== undefined) {
|
|
|
+ let color = score >= 80 ? '#10b981' : (score >= 60 ? '#f59e0b' : '#ef4444');
|
|
|
+ scoreBadge = `<span style="font-size: 0.85em; font-weight: bold; color: #fff; background: ${color}; padding: 2px 8px; border-radius: 12px; margin-left: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">⭐️ ${score}</span>`;
|
|
|
+ }
|
|
|
+
|
|
|
const typeLabel = type === 'workflow' ? '工序' : '能力';
|
|
|
|
|
|
// Add to sidebar
|
|
|
@@ -991,19 +1154,25 @@ function renderAggregatedPerCaseData(cases, type) {
|
|
|
contentHtml += `<h3 style="margin: 0; padding: 1rem; color: var(--text-main); font-size: 1.2rem; display: flex; align-items: center; gap: 10px; user-select: none; flex: 1;">`;
|
|
|
contentHtml += `<span class="case-arrow" style="font-size: 0.8em; color: var(--text-muted); width: 16px; display: inline-block;">▶</span>`;
|
|
|
contentHtml += `<span style="color: #64748b; font-size: 1.1rem; font-weight: bold; font-family: monospace;">#${idx + 1}</span>`;
|
|
|
- contentHtml += `<span>${title.replace(/</g, '<').replace(/>/g, '>')}</span>${publishStrHtml}</h3>`;
|
|
|
+ contentHtml += `<span>${title.replace(/</g, '<').replace(/>/g, '>')}</span>${scoreBadge}${publishStrHtml}</h3>`;
|
|
|
contentHtml += btnHtml;
|
|
|
contentHtml += `</div>`;
|
|
|
|
|
|
// Collapsible Post Info
|
|
|
- const post = c.post || c || {};
|
|
|
+ const post = mappedS.post || c.post || c || {};
|
|
|
+
|
|
|
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 bodyText = post.body_text || post.body || post.desc || '';
|
|
|
let mediaHtml = '';
|
|
|
- if (images.length > 0) {
|
|
|
+ if (allImages.length > 0) {
|
|
|
mediaHtml = `<div style="display:flex; gap:8px; overflow-x:auto; margin-bottom: 1rem; padding-bottom: 8px;">`;
|
|
|
- images.forEach(img => {
|
|
|
- mediaHtml += `<img src="${img}" style="height: 150px; border-radius: 6px; object-fit: cover; flex-shrink: 0;">`;
|
|
|
+ allImages.forEach((img, i) => {
|
|
|
+ const coverImgUrl = `/output/${window._currentRawCasesContext.reqId}/raw_cases/images/${cId}/${i.toString().padStart(2, '0')}.jpg`;
|
|
|
+ mediaHtml += `<img src="${coverImgUrl}" onerror="this.onerror=null; this.src='${img}';" style="height: 150px; border-radius: 6px; object-fit: cover; flex-shrink: 0;">`;
|
|
|
});
|
|
|
mediaHtml += `</div>`;
|
|
|
}
|
|
|
@@ -1043,6 +1212,9 @@ async function fetchRequirementData(index) {
|
|
|
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) {
|
|
|
@@ -1054,11 +1226,61 @@ async function fetchRequirementData(index) {
|
|
|
}
|
|
|
|
|
|
if (jsonStrategy) jsonStrategy.innerHTML = ''; // Tab removed
|
|
|
- jsonBlueprint.innerHTML = renderAggregatedPerCaseData(casesList, 'workflow');
|
|
|
- jsonCaps.innerHTML = renderAggregatedPerCaseData(casesList, 'capabilities');
|
|
|
|
|
|
jsonRaw.innerHTML = renderDataOrRaw(rawCasesClone, renderRawCases);
|
|
|
|
|
|
+ const clusterData = window.dataCache[index].cluster;
|
|
|
+
|
|
|
+ const oldActiveWorkflowTab = jsonBlueprint.querySelector('.sub-tab-btn.active')?.dataset?.target || 'sub-tab-workflow-cluster';
|
|
|
+
|
|
|
+ const workflowCasesHtml = renderAggregatedPerCaseData(casesList, 'workflow');
|
|
|
+ let bpHtml = `<div id="container-workflow">`;
|
|
|
+ bpHtml += `<div class="sub-tabs-container" style="margin-bottom: 1rem; border-bottom: 1px solid var(--border-glass); padding-bottom: 8px; display: flex; gap: 8px;">`;
|
|
|
+ bpHtml += `<button class="sub-tab-btn ${oldActiveWorkflowTab === 'sub-tab-workflow-cases' ? 'active' : ''}" data-target="sub-tab-workflow-cases" onclick="selectGenericSubTab('workflow', 'sub-tab-workflow-cases')">📊 案例解析页</button>`;
|
|
|
+ bpHtml += `<button class="sub-tab-btn ${oldActiveWorkflowTab === 'sub-tab-workflow-cluster' ? 'active' : ''}" data-target="sub-tab-workflow-cluster" onclick="selectGenericSubTab('workflow', 'sub-tab-workflow-cluster')">🧩 聚类结果 (Cluster)</button>`;
|
|
|
+ bpHtml += `</div>`;
|
|
|
+
|
|
|
+ bpHtml += `<div id="sub-tab-workflow-cases" class="sub-tab-pane ${oldActiveWorkflowTab === 'sub-tab-workflow-cases' ? '' : 'hidden'}">${workflowCasesHtml}</div>`;
|
|
|
+ bpHtml += `<div id="sub-tab-workflow-cluster" class="sub-tab-pane ${oldActiveWorkflowTab === 'sub-tab-workflow-cluster' ? '' : 'hidden'}">
|
|
|
+ <div style="margin-bottom: 1.5rem; padding: 1.2rem; background: rgba(0,0,0,0.02); border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px dashed rgba(0,0,0,0.1);">
|
|
|
+ <span style="color: var(--text-muted); font-size: 0.9em;">导入本地生成的 JSON (如 process.json 等) 将保存为 cluster.json 并支持单项删除与多选清除。</span>
|
|
|
+ <div>
|
|
|
+ <input type="file" id="input-upload-cluster" accept=".json" style="display:none;">
|
|
|
+ <button class="btn btn-primary btn-small" onclick="document.getElementById('input-upload-cluster').click()">📥 导入 JSON</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div id="cluster-preview-content">${renderClusterDeletable(clusterData)}</div>
|
|
|
+ </div>`;
|
|
|
+ bpHtml += `</div>`;
|
|
|
+ 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('上传出错');
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+ jsonCaps.innerHTML = renderAggregatedPerCaseData(casesList, 'capabilities');
|
|
|
+
|
|
|
const btnUpload = document.getElementById('btn-upload-source-ex');
|
|
|
const fileInput = document.getElementById('input-upload-source-ex');
|
|
|
if (btnUpload && fileInput) {
|
|
|
@@ -1138,6 +1360,7 @@ async function pollStatus() {
|
|
|
}
|
|
|
});
|
|
|
renderTaskList(requirements);
|
|
|
+ updateStats();
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
@@ -1160,6 +1383,7 @@ function renderTaskList(list) {
|
|
|
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) {
|
|
|
@@ -1531,7 +1755,6 @@ function setupEventListeners() {
|
|
|
const req = requirements.find(r => r.index === currentSelectedIndex);
|
|
|
if (req) {
|
|
|
req.requirement = newText;
|
|
|
- elDetailTitle.textContent = newText;
|
|
|
renderTaskList(requirements);
|
|
|
}
|
|
|
|
|
|
@@ -2034,8 +2257,19 @@ window.renderSingleCaseDetail = function(idx) {
|
|
|
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 = `<button onclick="window.toggleCaseFilter('${cId}', true)" style="padding: 6px 12px; background: #3b82f6; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9em; font-weight: bold; margin-left: auto;">↩️ 恢复至总库</button>`;
|
|
|
+ } else {
|
|
|
+ filterActionHtml = `<button onclick="window.toggleCaseFilter('${cId}', false)" style="padding: 6px 12px; background: #ef4444; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.9em; font-weight: bold; margin-left: auto;">🗑️ 移至被过滤</button>`;
|
|
|
+ }
|
|
|
+
|
|
|
const headerHtml = `
|
|
|
- <h2 style="margin: 0 0 0.5rem 0; font-size: 1.4em; color: var(--text-main);">${title}</h2>
|
|
|
+ <div style="display:flex; justify-content: space-between; align-items: flex-start; gap: 1rem;">
|
|
|
+ <h2 style="margin: 0 0 0.5rem 0; font-size: 1.4em; color: var(--text-main); flex: 1;">${title}</h2>
|
|
|
+ ${filterActionHtml}
|
|
|
+ </div>
|
|
|
<div style="display: flex; gap: 12px; margin-bottom: 0.8rem;">
|
|
|
${workflowUrl ? `<a href="${workflowUrl}" target="_blank" style="color: var(--accent-primary); text-decoration: none; font-size: 0.9em;">原文 ↗</a>` : ''}
|
|
|
<span style="color: var(--text-muted); font-size: 0.9em;">平台: ${platformName}</span>
|
|
|
@@ -2230,17 +2464,25 @@ window.renderStructuredData = function(items, type, parentItem = null) {
|
|
|
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) {
|
|
|
+ if (item.action && typeof item.action === 'string') {
|
|
|
+ actionStr = item.action;
|
|
|
+ } else if (item.action && item.action.main_action) {
|
|
|
actionStr = item.action.main_action;
|
|
|
+ } else if (item.body) {
|
|
|
+ actionStr = item.body.length > 20 ? item.body.substring(0, 20) + '...' : item.body;
|
|
|
} 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));
|
|
|
+ const hasAnyValidIO = item.steps.some(s => hasValidIO(s.inputs) || hasValidIO(s.outputs) || s.body);
|
|
|
if (hasAnyValidIO) {
|
|
|
actionStr = item.steps.map(s => {
|
|
|
+ if (s.action && typeof s.action === 'string') {
|
|
|
+ return s.action;
|
|
|
+ }
|
|
|
if (s.action && s.action.main_action) {
|
|
|
return s.action.mechanism ? `[${s.action.main_action}] ${s.action.mechanism}` : s.action.main_action;
|
|
|
}
|
|
|
+ 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 '未知';
|
|
|
@@ -2258,10 +2500,17 @@ window.renderStructuredData = function(items, type, parentItem = null) {
|
|
|
} else {
|
|
|
const escapeHtml = (s) => String(s).replace(/</g, '<').replace(/>/g, '>');
|
|
|
title = escapeHtml(item.method || item.name || '');
|
|
|
+ if (!title && item.action && typeof item.action === 'string') {
|
|
|
+ title = escapeHtml(item.action);
|
|
|
+ }
|
|
|
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 && 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}`);
|
|
|
}
|
|
|
@@ -2601,10 +2850,16 @@ window.renderStructuredData = function(items, type, parentItem = null) {
|
|
|
const minWidth = 1250;
|
|
|
const renderAction = (src) => {
|
|
|
if (!src) return '-';
|
|
|
+ if (src.action && typeof src.action === 'string') {
|
|
|
+ return `<span style="font-weight: 600; color: #0f172a; font-size: 1.05em;">${escapeHtml(src.action)}</span>`;
|
|
|
+ }
|
|
|
if (src.action && src.action.main_action) {
|
|
|
- const mainAction = escapeHtml(src.action.main_action);
|
|
|
- const mechanism = src.action.mechanism ? escapeHtml(src.action.mechanism) : '';
|
|
|
- return `<span class="data-type-badge" style="background:#e0e7ff;color:#3730a3;font-weight:normal;margin-right:6px;margin-bottom:2px;display:inline-block;">${mainAction}</span>${mechanism}`;
|
|
|
+ const mainAction = `<span style="font-weight: 600; color: #0f172a; font-size: 1.05em;">${escapeHtml(src.action.main_action)}</span>`;
|
|
|
+ const mechanism = src.action.mechanism ? ` <span style="color: #64748b; font-size: 0.9em;">(${escapeHtml(src.action.mechanism)})</span>` : '';
|
|
|
+ return `${mainAction}${mechanism}`;
|
|
|
+ }
|
|
|
+ if (src.body) {
|
|
|
+ return `<div class="fragment-text">${escapeHtml(src.body)}</div>`;
|
|
|
}
|
|
|
if (src.method) return escapeHtml(src.method);
|
|
|
if (src.description) return escapeHtml(src.description);
|
|
|
@@ -2616,24 +2871,26 @@ window.renderStructuredData = function(items, type, parentItem = null) {
|
|
|
};
|
|
|
const renderEffects = (effects) => {
|
|
|
if (!effects || !Array.isArray(effects) || effects.length === 0) return '-';
|
|
|
+ const renderKeyTag = (keyText) => `<span style="display:inline-block; padding: 2px 6px; background: #f1f5f9; color: #475569; border-radius: 4px; font-size: 0.85em; font-weight: 500; margin-right: 6px; border: 1px solid #e2e8f0; vertical-align: middle; white-space: nowrap;">${keyText}</span>`;
|
|
|
return `<div style="display:flex; flex-direction:column; gap:8px;">${effects.map(effect => {
|
|
|
if (typeof effect === 'string') {
|
|
|
- return `<div class="effect-item"><span class="data-type-badge effect-index">效果</span><span>${escapeHtml(effect)}</span></div>`;
|
|
|
+ return `<div class="effect-item"><div style="margin-bottom: 2px; display: flex; align-items: flex-start; line-height: 1.6;">${renderKeyTag('效果')}<span style="flex:1;">${escapeHtml(effect)}</span></div></div>`;
|
|
|
}
|
|
|
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.slice(0, 2).map(ex => `<span class="effect-negative">${escapeHtml(ex)}</span>`).join('')
|
|
|
+ ? `<div style="display:flex; flex-direction:column; gap:4px; flex:1;">` +
|
|
|
+ effect.negative_examples.map(ex => `<span style="background: #f8fafc; color: #64748b; padding: 2px 8px; border-radius: 4px; font-size: 0.9em; border: 1px solid #cbd5e1; display: inline-block; word-break: break-word;">${escapeHtml(ex)}</span>`).join('') +
|
|
|
+ `</div>`
|
|
|
: '';
|
|
|
return `<div class="effect-item">
|
|
|
- <div class="effect-content">
|
|
|
- <div>${judgeMethod ? `<span class="data-type-badge effect-method">${judgeMethod}</span>` : ''}<span class="effect-statement">${statement}</span></div>
|
|
|
- ${criteria ? `<div class="effect-criteria">${criteria}</div>` : ''}
|
|
|
- ${negativeExamples ? `<div class="effect-meta">
|
|
|
- ${negativeExamples}
|
|
|
- </div>` : ''}
|
|
|
+ <div class="effect-content" style="font-size: 0.95em; line-height: 1.6;">
|
|
|
+ <div style="font-weight: 600; margin-bottom: 6px; color: #0f172a; font-size: 1.05em;">${statement}</div>
|
|
|
+ ${criteria ? `<div style="margin-bottom: 4px; display: flex; align-items: flex-start;">${renderKeyTag('判断标准')}<span style="flex:1; color: #334155;">${criteria}</span></div>` : ''}
|
|
|
+ ${judgeMethod ? `<div style="margin-bottom: 4px; display: flex; align-items: flex-start;">${renderKeyTag('评判方式')}<span style="flex:1; color: #334155;">${judgeMethod}</span></div>` : ''}
|
|
|
+ ${negativeExamples ? `<div style="display: flex; align-items: flex-start;">${renderKeyTag('负面示例')}${negativeExamples}</div>` : ''}
|
|
|
</div>
|
|
|
</div>`;
|
|
|
}).join('')}</div>`;
|
|
|
@@ -2658,13 +2915,13 @@ window.renderStructuredData = function(items, type, parentItem = null) {
|
|
|
<span class="row-expand-icon">▶</span>
|
|
|
${fragment && fragment.fragment_id ? `<span style="display:inline-block; color:#94a3b8; font-size:0.85em; font-weight:400;">${escapeHtml(fragment.fragment_id)}</span>` : '-'}
|
|
|
</td>
|
|
|
- <td class="fragment-cell">${renderAction(fragment)}</td>
|
|
|
<td class="fragment-cell">${fragment && fragment.inputs && fragment.inputs.length > 0 ? renderDataObjList(fragment.inputs) : '-'}</td>
|
|
|
+ <td class="fragment-cell"><div class="fragment-clamp">${renderAction(fragment)}</div></td>
|
|
|
<td class="fragment-cell">${fragment && fragment.outputs && fragment.outputs.length > 0 ? renderDataObjList(fragment.outputs) : '-'}</td>
|
|
|
<td class="fragment-cell"><div class="fragment-clamp">${fragment ? renderEffects(fragment.effects) : '-'}</div></td>
|
|
|
+ <td class="fragment-cell"><div class="fragment-clamp fragment-text" style="color: #475569;">${fragment && fragment.body ? escapeHtml(fragment.body) : '-'}</div></td>
|
|
|
<td class="fragment-cell" style="font-size:0.9em;"><div class="fragment-clamp">${applyTo ? renderApplyToVal(applyTo) : '-'}</div></td>
|
|
|
<td class="fragment-cell"><div class="fragment-clamp">${fragment ? renderTools(fragment.tools) : '-'}</div></td>
|
|
|
- <td class="fragment-cell"><div class="fragment-clamp fragment-text">${fragment && fragment.body ? escapeHtml(fragment.body) : '-'}</div></td>
|
|
|
`;
|
|
|
};
|
|
|
|
|
|
@@ -2699,13 +2956,13 @@ window.renderStructuredData = function(items, type, parentItem = null) {
|
|
|
<th style="padding: 12px 10px; width: 60px;">序号</th>
|
|
|
<th style="padding: 12px 10px; width: 90px;">阶段</th>
|
|
|
<th style="padding: 12px 8px; width: 90px;"></th>
|
|
|
- <th style="padding: 12px 10px; width: 160px;">动作</th>
|
|
|
<th style="padding: 12px 10px; width: 180px;">输入</th>
|
|
|
+ <th style="padding: 12px 10px; width: 140px;">动作/做法</th>
|
|
|
<th style="padding: 12px 10px; width: 180px;">输出</th>
|
|
|
- <th style="padding: 12px 10px; width: 220px;">效果</th>
|
|
|
+ <th style="padding: 12px 10px; width: 360px;">效果</th>
|
|
|
+ <th style="padding: 12px 10px; width: 180px;">用法</th>
|
|
|
<th style="padding: 12px 10px; width: 260px;">作用域</th>
|
|
|
<th style="padding: 12px 10px; width: 130px;">工具</th>
|
|
|
- <th style="padding: 12px 10px; width: 280px;">做法</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>`;
|
|
|
@@ -2879,4 +3136,38 @@ window.triggerSingleCaseRerun = async function(step, caseIndex) {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+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();
|