| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991 |
- function renderJSON(obj) {
- if (obj === null) return `<span class="json-null">null</span>`;
- if (typeof obj === 'number') return `<span class="json-number">${obj}</span>`;
- if (typeof obj === 'boolean') return `<span class="json-boolean">${obj}</span>`;
- if (typeof obj === 'string') {
- const escaped = obj.replace(/</g, '<').replace(/>/g, '>');
- return `<span class="json-string">"${escaped}"</span>`;
- }
- if (Array.isArray(obj)) {
- if (obj.length === 0) return '[]';
- let html = '<div class="json-array">[<div class="json-children">';
- obj.forEach((val, i) => {
- html += `<div class="json-item">${renderJSON(val)}${i < obj.length - 1 ? ',' : ''}</div>`;
- });
- html += '</div>]</div>';
- return html;
- }
- if (typeof obj === 'object') {
- const keys = Object.keys(obj);
- if (keys.length === 0) return '{}';
- let html = '<div class="json-object">{<div class="json-children">';
- keys.forEach((k, i) => {
- html += `<div class="json-prop"><span class="json-key">"${k}"</span>: ${renderJSON(obj[k])}${i < keys.length - 1 ? ',' : ''}</div>`;
- });
- html += '</div>}</div>';
- return html;
- }
- return String(obj);
- }
- function renderDataOrRaw(dataObj, renderFunc) {
- if (!dataObj) return '<p style="color:var(--text-muted)">无可用数据</p>';
- let safeRaw = "";
- if (dataObj.error) {
- safeRaw = dataObj.raw_content ? dataObj.raw_content.replace(/</g, "<").replace(/>/g, ">") : "文件为空。";
- return `<div style="padding:1rem; background:rgba(239, 68, 68, 0.1); border:1px solid var(--danger); border-radius:8px;">
- <h3 style="color:var(--danger); margin-bottom:0.5rem">⚠️ JSON 解析失败</h3>
- <pre style="white-space: pre-wrap; font-family: monospace; font-size: 0.85rem; color: var(--text-muted); overflow-x: auto">${safeRaw}</pre>
- </div>`;
- } else {
- safeRaw = JSON.stringify(dataObj, null, 2).replace(/</g, "<").replace(/>/g, ">");
- }
- // Check global toggle state
- const isRaw = document.getElementById('global-json-toggle') && document.getElementById('global-json-toggle').checked;
- return `
- <div class="data-view-container">
- <div class="data-view-ui" style="${isRaw ? 'display:none;' : ''}">
- ${renderFunc(dataObj)}
- </div>
- <div class="data-view-raw" style="${isRaw ? '' : 'display:none;'}">
- <div style="padding:1rem; background:rgba(0, 0, 0, 0.05); border:1px solid rgba(0, 0, 0, 0.1); border-radius:8px;">
- <h3 style="color:var(--text-muted); margin-bottom:0.5rem">📝 JSON 原文</h3>
- <pre style="white-space: pre-wrap; font-family: monospace; font-size: 0.85rem; color: var(--text-muted); overflow-x: auto">${safeRaw}</pre>
- </div>
- </div>
- </div>`;
- }
- function renderRawCases(rawCasesObj) {
- if (!rawCasesObj || Object.keys(rawCasesObj).length === 0) return '无原始案例数据';
- const reqId = requirements[currentSelectedIndex]?.id || "001";
- // Build maps
- const sourceMap = {};
- const sourceData = rawCasesObj['source'];
- const sourceList = sourceData ? (Array.isArray(sourceData) ? sourceData : sourceData.sources) : null;
- if (sourceList) {
- sourceList.forEach(s => {
- const sId = s.case_id || (s._raw && s._raw.case_id);
- const sUrl = s.source_url || s.url;
- if (sId) sourceMap[sId] = s;
- if (sUrl) sourceMap[sUrl] = s;
- });
- }
- const detailMap = {};
- const detailMapByUrl = {};
- // Cache context for modal
- window._currentRawCasesContext = {
- rawCasesObj, sourceMap, detailMap, detailMapByUrl, reqId
- };
- let totalStatsHtml = '';
- const detailedCaseObj = rawCasesObj['case'] || rawCasesObj['case_detailed'];
- if (detailedCaseObj) {
- const cd = detailedCaseObj;
- let uniqueCases = new Set();
- let calcWorkflow = 0;
- let calcCapabilities = 0;
- if (cd.cases) {
- cd.cases.forEach(c => {
- const cId = c.case_id || (c._raw && c._raw.case_id);
- const cUrl = c.source_url || c.url;
- const uniqueKey = cId || cUrl || Math.random().toString();
- uniqueCases.add(uniqueKey);
- const workflowGroups = getWorkflowGroups(c);
- const capabilityItems = getCapabilityItems(c);
- if (workflowGroups.length > 0) calcWorkflow += workflowGroups.length;
- if (capabilityItems.length > 0) calcCapabilities += capabilityItems.length;
- if (cId) {
- if (workflowGroups.length > 0) detailMap[cId] = { ...detailMap[cId], workflow_groups: c.workflow_groups };
- }
- if (cUrl) {
- if (workflowGroups.length > 0) detailMapByUrl[cUrl] = { ...detailMapByUrl[cUrl], workflow_groups: c.workflow_groups };
- }
- });
- }
- const displayTotal = uniqueCases.size > 0 ? uniqueCases.size : (cd.total !== undefined ? cd.total : 0);
- const displayWorkflowSuccess = calcWorkflow > 0 ? calcWorkflow : (cd.workflow_success !== undefined && cd.workflow_success !== null ? cd.workflow_success : (cd.success !== undefined && cd.success !== null ? cd.success : 0));
- const displayCapabilitiesSuccess = calcCapabilities > 0 ? calcCapabilities : (cd.capabilities_success !== undefined && cd.capabilities_success !== null ? cd.capabilities_success : 0);
- if (cd.total !== undefined || uniqueCases.size > 0) {
- totalStatsHtml = `<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;">${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;">
- <span style="font-size:1.3rem; color:var(--success); font-weight:bold; line-height:1;">${displayWorkflowSuccess}</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;">
- <span style="font-size:1.3rem; color:var(--accent-primary); font-weight:bold; line-height:1;">${displayCapabilitiesSuccess}</span>
- <span style="color:var(--text-muted); font-size:0.75rem;">能力提取成功数</span>
- </div>
- </div>`;
- }
- }
- let filteredStatsHtml = '';
- if (rawCasesObj['filtered_cases']) {
- const fObj = rawCasesObj['filtered_cases'];
- let totalFiltered = 0;
- if (fObj.total !== undefined) totalFiltered = fObj.total;
- else if (fObj.cases) totalFiltered = fObj.cases.length;
- else if (fObj.sources) totalFiltered = fObj.sources.length;
- else if (fObj.by_reason) {
- Object.values(fObj.by_reason).forEach(r => {
- if (r.sources) totalFiltered += r.sources.length;
- else if (r.cases) totalFiltered += r.cases.length;
- });
- } else if (Array.isArray(fObj)) totalFiltered = fObj.length;
- filteredStatsHtml = `<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;">${totalFiltered}</span>
- <span style="color:var(--text-muted); font-size:0.75rem;">被过滤的帖子数</span>
- </div>
- </div>`;
- }
- const allPlatforms = Object.keys(rawCasesObj).filter(p => p !== 'source' && p !== 'case_detailed' && p !== 'case' && p !== 'images');
- const channelPlatforms = allPlatforms.filter(p => p !== 'filtered_cases' && p !== 'source_ex');
- const hasFiltered = allPlatforms.includes('filtered_cases');
- const hasExternal = allPlatforms.includes('source_ex');
- let html = '';
- html += `<div class="sub-tabs">`;
- html += `<button class="sub-tab-btn active" onclick="selectSubTab('total')">TOTAL</button>`;
- if (hasFiltered) html += `<button class="sub-tab-btn" onclick="selectSubTab('filtered_cases')">FILTERED</button>`;
- if (hasExternal) html += `<button class="sub-tab-btn" onclick="selectSubTab('source_ex')">EXTERNAL</button>`;
- html += `<button class="sub-tab-btn" id="btn-upload-source-ex" style="background-color: var(--primary); margin-left: auto;">+ 上传 source_ex</button>`;
- html += `<input type="file" id="input-upload-source-ex" accept=".json" style="display:none;">`;
- html += `</div><div class="sub-tab-contents">`;
- const renderPaneContent = (pList, paneType) => {
- let paneHtml = '';
- if (paneType === 'total' && typeof totalStatsHtml !== 'undefined' && totalStatsHtml) {
- paneHtml += totalStatsHtml;
- } else if (paneType === 'filtered_cases' && typeof filteredStatsHtml !== 'undefined' && filteredStatsHtml) {
- paneHtml += filteredStatsHtml;
- }
- let totalCases = 0;
- let seenIds = new Set();
- let groupedHtml = {};
- const getGroupKey = (c, p) => (p === 'filtered_cases' && c.filter_reason) ? `🚫 过滤原因: ${c.filter_reason}` : 'default';
- let allCases = [];
- pList.forEach(p => {
- if (!rawCasesObj[p]) return;
- if (rawCasesObj[p].error) {
- const safeRaw = rawCasesObj[p].raw_content ? rawCasesObj[p].raw_content.replace(/</g, "<").replace(/>/g, ">") : "文件为空。";
- paneHtml += `<div style="padding:1rem; background:rgba(239, 68, 68, 0.1); border:1px solid var(--danger); border-radius:8px; margin-bottom:1rem;">
- <h3 style="color:var(--danger); margin-bottom:0.5rem">⚠️ ${p} JSON 解析失败</h3>
- <pre style="white-space: pre-wrap; font-family: monospace; font-size: 0.85rem; color: var(--text-muted); overflow-x: auto">${safeRaw}</pre>
- </div>`;
- return;
- }
- if (rawCasesObj[p].reason && p !== 'filtered_cases') {
- paneHtml += `<div style="padding:0.5rem 1rem; background:rgba(239, 68, 68, 0.1); border:1px solid var(--danger); border-radius:6px; margin-bottom:1rem; color:var(--danger); font-size:0.9rem;">🛑 ${p} 提示: ${rawCasesObj[p].reason}</div>`;
- }
- let cases = [];
- if (Array.isArray(rawCasesObj[p])) {
- cases = rawCasesObj[p];
- } else if (rawCasesObj[p].cases) {
- cases = rawCasesObj[p].cases;
- } else if (rawCasesObj[p].sources) {
- cases = rawCasesObj[p].sources;
- } else if (rawCasesObj[p].by_reason) {
- Object.entries(rawCasesObj[p].by_reason).forEach(([reasonKey, reasonObj]) => {
- if (reasonObj.sources && Array.isArray(reasonObj.sources)) {
- reasonObj.sources.forEach(src => {
- if (!src.filter_reason) src.filter_reason = reasonKey;
- cases.push(src);
- });
- }
- });
- }
- if (cases.length > 0) {
- if (!rawCasesObj['source'] && p !== 'source_ex' && p !== 'filtered_cases' && p !== 'source') {
- paneHtml += `<div style="padding:1rem; background:rgba(0, 0, 0, 0.05); border:1px solid rgba(0, 0, 0, 0.1); border-radius:8px; margin-bottom:1rem;">
- <h3 style="color:var(--text-main); margin-bottom:0.5rem">📝 ${p} 原始爬取数据 (未进行 1.5 数据源提取)</h3>
- <div style="font-family: monospace; font-size: 0.85rem; overflow-x: auto">${renderJSON(rawCasesObj[p])}</div>
- </div>`;
- return;
- }
- cases.forEach((c, idx) => {
- allCases.push({ c: c, p: p, originalIdx: idx });
- });
- }
- });
- allCases.sort((aObj, bObj) => {
- const getScore = (item) => {
- const iId = item.case_id || (item._raw && item._raw.case_id) || (item.post && item.post.channel_content_id);
- const iUrl = item.source_url || item.url || (item.post && item.post.link);
- const mapped = sourceMap[iId] || sourceMap[iUrl] || (item._raw && sourceMap[item._raw.case_id]) || item;
- return mapped.evaluation && mapped.evaluation.quality ? (mapped.evaluation.quality.overall_score || 0) : 0;
- };
- return getScore(bObj.c) - getScore(aObj.c);
- });
- allCases.forEach(itemObj => {
- const c = itemObj.c;
- const p = itemObj.p;
- const idx = itemObj.originalIdx;
- totalCases++;
- const cId = c.case_id || (c._raw && c._raw.case_id) || (c.post && c.post.channel_content_id) || `temp_${p}_${idx}`;
- const cUrl = c.source_url || c.url || (c.post && c.post.link) || '';
- const groupKey = getGroupKey(c, p);
- if (!groupedHtml[groupKey]) groupedHtml[groupKey] = '';
- if (cId || cUrl || c.post) {
- const mappedS = sourceMap[cId] || sourceMap[cUrl] || (c._raw && sourceMap[c._raw.case_id]);
- if (p !== 'filtered_cases' && p !== 'source' && p !== 'source_ex' && !mappedS) return;
- if (cId && seenIds.has(cId)) return;
- if (cId) seenIds.add(cId);
- const s = mappedS || c;
- const post = s.post || s || {};
- const images = post.images || [];
- const xImages = post.image_url_list || [];
- const ytThumbnails = post.thumbnails && post.thumbnails.length > 0 ? [post.thumbnails[post.thumbnails.length - 1].url] : [];
- const allImages = [...images, ...xImages.map(img => img.image_url), ...ytThumbnails].filter(Boolean);
- const coverImgUrl = allImages.length > 0 ? `/output/${reqId}/raw_cases/images/${cId}/00.jpg` : '';
- const fallbackImgUrl = allImages.length > 0 ? allImages[0] : '';
- const title = post.title || c.title || post.desc || (post.body_text ? post.body_text.substring(0, 30) + '...' : '') || cId || '无标题';
- const author = post.channel_account_name || s.author || '-';
- const likes = post.like_count !== undefined ? post.like_count : (post.likes !== undefined ? post.likes : '-');
- const snippetStr = (post.body_text || post.body || '').substring(0, 100);
- const snippetHtml = snippetStr ? `<div class="masonry-card-snippet">${snippetStr.replace(/</g, "<").replace(/>/g, ">")}...</div>` : '';
- 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];
- if (isImported) {
- actionBtn = `<button disabled style="position:absolute; top:8px; right:8px; background:#10b981; color:#fff; border:none; padding:4px 8px; border-radius:4px; font-size:0.7em; z-index:3; cursor:not-allowed; font-weight: bold; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">✅ 已导入</button>`;
- } else {
- actionBtn = `<button onclick="importExternalCase(event, '${cId}')" style="position:absolute; top:8px; right:8px; background:var(--accent-primary); color:#fff; border:none; padding:4px 8px; border-radius:4px; font-size:0.7em; z-index:3; cursor:pointer; font-weight: bold; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">📥 导入 Source</button>`;
- }
- }
- 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">
- <div class="masonry-card-title">${title}</div>
- ${allImages.length === 0 ? snippetHtml : ''}
- <div class="masonry-card-stats">
- <div class="masonry-card-author">👤 ${author}</div>
- <div class="masonry-card-likes">❤️ ${likes}</div>
- </div>
- </div>
- </div>`;
- } else {
- groupedHtml[groupKey] += `<div class="masonry-card" style="padding:12px; font-family:monospace; font-size:0.8em;" onclick="openCaseDetail('${p}', ${idx})">
- 📝 旧版格式 / 解析失败<br>点击查看详情
- </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 {
- Object.entries(groupedHtml).forEach(([groupName, gHtml]) => {
- if (gHtml) {
- if (groupName !== 'default') {
- paneHtml += `<h3 style="margin-top: 1rem; margin-bottom: 0.5rem; font-size: 1.1rem; color: var(--text-main); border-left: 4px solid var(--accent-primary); padding-left: 8px;">${groupName}</h3>`;
- }
- paneHtml += `<div class="masonry-grid">${gHtml}</div>`;
- }
- });
- }
- return paneHtml;
- };
- html += `<div id="sub-tab-total" class="sub-tab-pane">${renderPaneContent([...channelPlatforms, 'source'], 'total')}</div>`;
- if (hasFiltered) html += `<div id="sub-tab-filtered_cases" class="sub-tab-pane hidden">${renderPaneContent(['filtered_cases'], 'filtered_cases')}</div>`;
- if (hasExternal) {
- html += `<div id="sub-tab-source_ex" class="sub-tab-pane hidden">`;
- html += `<div style="margin-bottom: 1rem; text-align: right;"><button onclick="importAllExternalCases()" class="btn btn-primary btn-small">📥 全部导入 Source</button></div>`;
- html += renderPaneContent(['source_ex'], 'source_ex');
- html += `</div>`;
- }
- html += `</div>`;
- return html;
- }
- window.importExternalCase = async function (e, caseId) {
- e.stopPropagation();
- if (!confirm('确定要将该外部数据导入到 source 吗?')) return;
- try {
- const res = await fetch(`/api/requirements/${currentSelectedIndex}/import_source_ex`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ case_id: caseId })
- });
- const data = await res.json();
- if (data.status === 'success') {
- fetchRequirementData(currentSelectedIndex); // Refresh data
- } else {
- alert('导入失败: ' + (data.detail || data.message || JSON.stringify(data)));
- }
- } catch (err) {
- alert('导入出错: ' + err);
- }
- };
- window.importAllExternalCases = async function () {
- if (!confirm('确定要将所有外部数据全部导入到 source 吗?')) return;
- try {
- const res = await fetch(`/api/requirements/${currentSelectedIndex}/import_all_source_ex`, {
- method: 'POST'
- });
- const data = await res.json();
- if (data.status === 'success') {
- alert(`成功导入 ${data.count} 条数据!`);
- fetchRequirementData(currentSelectedIndex); // Refresh data
- } else {
- alert('全部导入失败: ' + (data.detail || data.message || JSON.stringify(data)));
- }
- } catch (err) {
- alert('导入出错: ' + err);
- }
- };
- function selectSubTab(tabName) {
- document.querySelectorAll('#json-raw .sub-tab-btn').forEach(btn => {
- btn.classList.remove('active');
- });
- const activeBtn = document.querySelector(`#json-raw .sub-tab-btn[data-target="sub-tab-${tabName}"]`);
- if (activeBtn) activeBtn.classList.add('active');
- document.querySelectorAll('#json-raw .sub-tab-pane').forEach(pane => {
- pane.classList.add('hidden');
- });
- document.getElementById(`sub-tab-${tabName}`).classList.remove('hidden');
- }
- window.deleteClusterItems = async function (indices) {
- if (currentSelectedIndex === null) return;
- const data = window.dataCache[currentSelectedIndex]?.cluster;
- if (!data) return;
- let newData = Array.isArray(data) ? [...data] : { ...data };
- let count = 0;
- // Sort indices descending to avoid shifting issues if it's an array
- indices.sort((a, b) => b - a);
- if (Array.isArray(newData)) {
- indices.forEach(i => { newData.splice(i, 1); count++; });
- } else {
- const keys = Object.keys(newData);
- indices.forEach(i => { delete newData[keys[i]]; count++; });
- }
- if (!confirm(`确定要删除选中的 ${count} 个项目吗?`)) return;
- try {
- const res = await fetch(`/api/requirements/${currentSelectedIndex}/save_cluster`, {
- method: 'POST',
- body: JSON.stringify(newData),
- headers: { 'Content-Type': 'application/json' }
- });
- if (res.ok) {
- fetchRequirementData(currentSelectedIndex);
- } else {
- alert('删除失败');
- }
- } catch (e) {
- console.error(e);
- alert('删除失败');
- }
- };
- window.deleteSingleCluster = function (idx) {
- deleteClusterItems([idx]);
- };
- window.deleteSelectedClusters = function () {
- const checkboxes = document.querySelectorAll('.cluster-checkbox:checked');
- const indices = Array.from(checkboxes).map(cb => parseInt(cb.dataset.idx));
- if (indices.length === 0) {
- alert('请先选择要删除的项目');
- return;
- }
- deleteClusterItems(indices);
- };
- window.clearAllClusters = async function () {
- if (currentSelectedIndex === null) return;
- if (!confirm('确定要清空全部聚类结果吗?')) return;
- try {
- const res = await fetch(`/api/requirements/${currentSelectedIndex}/save_cluster`, {
- method: 'POST',
- body: JSON.stringify(Array.isArray(window.dataCache[currentSelectedIndex]?.cluster) ? [] : {}),
- headers: { 'Content-Type': 'application/json' }
- });
- if (res.ok) {
- fetchRequirementData(currentSelectedIndex);
- } else {
- alert('清空失败');
- }
- } catch (e) {
- console.error(e);
- alert('清空失败');
- }
- };
- function renderClusterDeletable(clusterData) {
- if (!clusterData || (Array.isArray(clusterData) && clusterData.length === 0) || (typeof clusterData === 'object' && Object.keys(clusterData).length === 0)) {
- return `<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;
- 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 `<div id="container-${tabPrefix}">
- <div class="sub-tabs">
- <button class="sub-tab-btn active" data-target="sub-tab-${tabPrefix}-main" onclick="selectGenericSubTab('${tabPrefix}', 'sub-tab-${tabPrefix}-main')">最终版本</button>
- <button class="sub-tab-btn" data-target="sub-tab-${tabPrefix}-temp" onclick="selectGenericSubTab('${tabPrefix}', 'sub-tab-${tabPrefix}-temp')">中间过程 (Temp)</button>
- </div>
- <div class="sub-tab-contents">
- <div id="sub-tab-${tabPrefix}-main" class="sub-tab-pane">
- ${mainHtml}
- </div>
- <div id="sub-tab-${tabPrefix}-temp" class="sub-tab-pane hidden">
- ${tempHtml}
- </div>
- </div>
- </div>`;
- }
- function renderCaseTags(caseRefs) {
- if (!caseRefs || caseRefs.length === 0) return '';
- let html = `<div class="tags-container" style="gap:0.8rem; flex-wrap:wrap; margin-top: 0.5rem;">`;
- caseRefs.forEach(ref => {
- let caseId = null;
- let title = ref;
- const matchNew = ref.match(/^([a-z]+)_([^\s::]+)(?:[::\s]+(.*))?/);
- if (matchNew && matchNew[1] !== 'case') {
- caseId = `${matchNew[1]}_${matchNew[2]}`;
- title = matchNew[3] || ref;
- } else {
- const matchA = ref.match(/^case_([a-z]+)_([a-zA-Z0-9]+)(?:[::\s]+(.*))?/);
- if (matchA) {
- caseId = `${matchA[1]}-case_${matchA[2]}`;
- title = matchA[3] || ref;
- } else {
- const matchB = ref.match(/^([a-z]+)[\/\s](case_[a-zA-Z0-9]+)(?:[::\s]+(.*))?/);
- if (matchB) {
- caseId = `${matchB[1]}-${matchB[2]}`;
- title = matchB[3] || ref;
- } else {
- const matchC = ref.match(/^case_([a-zA-Z0-9]+)_([a-z]+)(?:[::\s]+(.*))?/);
- if (matchC) {
- caseId = `${matchC[2]}-case_${matchC[1]}`;
- title = matchC[3] || ref;
- }
- }
- }
- }
- if (caseId) {
- html += `<a href="#" onclick="jumpToCase('${caseId}'); return false;" class="badge-emoji primary" style="text-decoration:none; white-space:normal; text-align:left; line-height:1.4">
- <strong>🔍 ${caseId.replace('-', ' ')}</strong><br>
- <span style="font-size:0.75rem">${title.substring(0, 40) + (title.length > 40 ? '...' : '')}</span>
- </a>`;
- } else {
- html += `<span class="badge-emoji" style="white-space:normal; text-align:left; line-height:1.4; font-size:0.75rem">${ref}</span>`;
- }
- });
- html += `</div>`;
- return html;
- }
- function renderCapabilities(capsObj) {
- if (!capsObj || (!capsObj.extracted_capabilities && !capsObj.capabilities)) {
- return `<div class="data-card" style="border-color: var(--warning)"><div class="card-header"><div class="card-title">⚠️ 未知或非标准格式 (Capabilities)</div></div><div class="card-body" style="font-family: monospace; font-size: 0.85rem; overflow-x: auto;">${renderJSON(capsObj)}</div></div>`;
- }
- const caps = capsObj.capabilities || capsObj.extracted_capabilities;
- if (caps.length === 0) return '<p>未提取到能力。</p>';
- let html = ``;
- caps.forEach(cap => {
- if (!cap.name && !cap.能力名称 && !cap.description && !cap.能力描述 && !cap.id) {
- html += `<div class="data-card"><div class="card-header"><div class="card-title">⚠️ 非标准能力项</div></div><div class="card-body" style="font-family: monospace; font-size: 0.85rem; overflow-x: auto;">${renderJSON(cap)}</div></div>`;
- return;
- }
- const isNew = cap.is_new ? '<span class="badge-emoji success">✨ 新能力</span>' : '';
- html += `<div class="data-card">
- <div class="card-header">
- <div class="card-title">⚡ [${cap.id || 'N/A'}] ${cap.name || cap.能力名称 || '未命名'}</div>
- ${isNew}
- </div>
- <div class="card-body">
- <p>${cap.description || cap.能力描述 || ''}</p>`;
- if (cap.enriched_details) {
- const ed = cap.enriched_details;
- if (ed.execution_process) {
- html += `<div class="card-section"><div class="section-title">🚀 执行流程</div><p style="white-space:pre-wrap;">${ed.execution_process}</p></div>`;
- }
- if (ed.core_parameters) {
- html += `<div class="card-section"><div class="section-title">⚙️ 核心参数</div><p style="white-space:pre-wrap;">${ed.core_parameters}</p></div>`;
- }
- if (ed.effects) {
- html += `<div class="card-section"><div class="section-title">✨ 影响效果</div><p style="white-space:pre-wrap;">${ed.effects}</p></div>`;
- }
- if (ed.visual_notes) {
- html += `<div class="card-section"><div class="section-title">🖼️ 视觉备注</div><p style="white-space:pre-wrap;">${ed.visual_notes}</p></div>`;
- }
- } else {
- html += `<div class="card-section"><div class="section-title">✨ 影响 (Effects)</div><ul>`;
- if (cap.effects) cap.effects.forEach(eff => html += `<li>${eff}</li>`);
- html += `</ul></div>`;
- if (cap.implements && Object.keys(cap.implements).length > 0) {
- html += `<div class="card-section"><div class="section-title">🛠️ 实现工具 (Tools)</div><div class="tags-container">`;
- for (const [tool, args] of Object.entries(cap.implements)) {
- html += `<span class="badge-emoji primary" title="${args}">🔧 ${tool}</span>`;
- }
- html += `</div></div>`;
- }
- }
- if (cap.case_references && cap.case_references.length > 0) {
- html += `<div class="card-section"><div class="section-title">📌 来源案例</div>${renderCaseTags(cap.case_references)}</div>`;
- }
- html += `</div></div>`;
- });
- return html;
- }
- function renderFragmentsGrid(fragments) {
- if (!fragments || fragments.length === 0) return '<p>没有片段数据。</p>';
- window.allFragmentsMap = {}; // Reset map
- let html = `<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; padding: 10px;">`;
- fragments.forEach(f => {
- let cid = f.capability_id || ('temp_cap_' + Math.random().toString(36).substring(7));
- window.allFragmentsMap[cid] = f;
-
- let tools = f.tools && f.tools.length > 0 ? f.tools.join(', ') : '无工具';
- let actionDesc = f.action && f.action.description ? f.action.description : (f.capability_id || 'unknown');
- let bodyText = f.body || f.body_excerpt || f.rationale || '';
- if (bodyText.length > 180) bodyText = bodyText.substring(0, 180) + '...';
-
- let inputs = f.inputs ? f.inputs.map(i => i.modality || '').filter(Boolean).join('+') : '';
- let outputs = f.outputs ? f.outputs.map(o => o.modality || '').filter(Boolean).join('+') : '';
- let ioStr = (inputs && outputs) ? `${inputs} -> ${outputs}` : (f.action && f.action.description ? f.action.description : '转换');
- let applyHtml = '';
- if (f.apply_to) {
- Object.values(f.apply_to).forEach(arr => {
- arr.forEach(item => {
- const parts = item.category_path ? item.category_path.split('/') : [];
- const label = parts.length > 0 ? parts[parts.length - 1] : '标签';
- applyHtml += `<span class="frag-apply-pill">${label}</span>`;
- });
- });
- }
- const caseShort = f._caseId ? f._caseId.split('-').pop() : 'case';
- html += `<div class="frag" onclick="openFragDetail('${cid}')" style="cursor:pointer;">
- <div class="frag-head">
- <span class="case-badge" onclick="jumpToCase('${f._caseId}'); event.stopPropagation();" style="cursor:pointer" title="点击跳转案例">${caseShort}</span>
- <span class="frag-badge">${f._workflowId || 'w'}</span>
- <span style="margin-left: auto; font-weight: 600; color: var(--accent-primary, #3b82f6);">${ioStr}</span>
- </div>
- <div class="frag-sig">${actionDesc} [${tools}]</div>
- <div class="frag-body">${bodyText}</div>
- ${applyHtml ? `<div class="frag-apply">${applyHtml}</div>` : ''}
- </div>`;
- });
- html += `</div>`;
- return html;
- }
- window.openFragDetail = function(fragId) {
- const f = window.allFragmentsMap[fragId];
- if (!f) return;
- const modal = document.getElementById('frag-detail-modal');
- const body = document.getElementById('frag-detail-modal-body');
-
- let html = `<div style="background:#fff; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">`;
-
- let inputs = f.inputs ? f.inputs.map(i => i.modality || '').filter(Boolean).join('+') : '';
- let outputs = f.outputs ? f.outputs.map(o => o.modality || '').filter(Boolean).join('+') : '';
- let ioStr = (inputs && outputs) ? `${inputs} → ${outputs}` : (f.action && f.action.description ? f.action.description : '转换');
- html += `<div style="display:flex; gap: 8px; margin-bottom: 16px;">
- <span class="case-badge">${f._caseId ? f._caseId.split('-').pop() : 'case'}</span>
- <span class="frag-badge">${f._workflowId || 'w'}</span>
- <span class="badge-emoji warning">${f.capability_id || 'unknown'}</span>
- </div>`;
- html += `<h4 style="margin: 0 0 10px 0; color: #334155;">I/O 模态</h4>`;
- html += `<div style="font-size: 14px; margin-bottom: 8px;"><strong>${ioStr}</strong></div>`;
-
- if (f.inputs && f.inputs.length > 0) {
- html += `<div style="font-size: 13px; color: #64748b; margin-bottom: 4px;">IN <span style="margin-left: 8px;">${f.inputs.map(i => `<span style="background: #e0e7ff; color: #3730a3; padding: 2px 6px; border-radius: 4px; margin-right: 4px;">${i.description || '输入'} [${i.modality || '未知'}]</span>`).join('')}</span></div>`;
- }
- if (f.outputs && f.outputs.length > 0) {
- html += `<div style="font-size: 13px; color: #64748b; margin-bottom: 16px;">OUT <span style="margin-left: 8px;">${f.outputs.map(o => `<span style="background: #fce7f3; color: #9d174d; padding: 2px 6px; border-radius: 4px; margin-right: 4px;">${o.description || '输出'} [${o.modality || '未知'}]</span>`).join('')}</span></div>`;
- }
- if (f.body || f.body_excerpt || f.rationale) {
- html += `<h4 style="margin: 20px 0 10px 0; color: #334155;">BODY</h4>`;
- html += `<div style="background: #f8fafc; padding: 12px; border-left: 4px solid #cbd5e1; border-radius: 4px; font-size: 14px; color: #475569; line-height: 1.6; white-space: pre-wrap;">${f.body || f.body_excerpt || f.rationale}</div>`;
- }
- if (f.apply_to) {
- html += `<h4 style="margin: 20px 0 10px 0; color: #334155;">APPLY TO</h4>`;
- Object.keys(f.apply_to).forEach(k => {
- f.apply_to[k].forEach(item => {
- html += `<div style="margin-bottom: 12px;">
- <div style="font-size: 13px; font-weight: 600; color: #2563eb; margin-bottom: 4px;">${item.category_path || k}</div>
- <div style="font-size: 13px; color: #64748b; padding-left: 10px; border-left: 2px solid #e2e8f0;">${item.rationale || item.body_excerpt || '无推理说明'}</div>
- </div>`;
- });
- });
- }
- if (f.suggest_apply_to && f.suggest_apply_to.length > 0) {
- html += `<h4 style="margin: 20px 0 10px 0; color: #334155;">SUGGEST APPLY TO <span style="font-size: 12px; font-weight: normal; color: #8b5cf6;">(建议新增节点)</span></h4>`;
- f.suggest_apply_to.forEach(item => {
- html += `<div style="margin-bottom: 12px;">
- <div style="font-size: 13px; font-weight: 600; color: #8b5cf6; margin-bottom: 4px;">${item.path || '新节点'}</div>
- <div style="font-size: 13px; color: #64748b; padding-left: 10px; border-left: 2px solid #e2e8f0;">${item.rationale || item.body_excerpt || '无推理说明'}</div>
- </div>`;
- });
- }
- if (f.effects && f.effects.length > 0) {
- html += `<h4 style="margin: 20px 0 10px 0; color: #334155;">EFFECTS</h4>`;
- f.effects.forEach((eff, i) => {
- html += `<div style="background: #f1f5f9; padding: 12px; border-radius: 6px; margin-bottom: 8px;">
- <div style="font-size: 14px; font-weight: 600; color: #1e293b; margin-bottom: 8px;">#${i} ${eff.statement || 'Effect'}</div>
- <div style="font-size: 13px; color: #475569; margin-bottom: 4px;"><strong>判定标准:</strong> ${eff.criteria || '无'}</div>
- <div style="font-size: 13px; color: #475569; margin-bottom: 4px;"><strong>判定方式:</strong> ${eff.judge_method || '未知'}</div>`;
- if (eff.negative_examples && eff.negative_examples.length > 0) {
- html += `<div style="font-size: 13px; color: #475569; margin-top: 8px;"><strong>反例:</strong><ul style="margin: 4px 0 0 20px;">${eff.negative_examples.map(ex => `<li>${ex}</li>`).join('')}</ul></div>`;
- }
- html += `</div>`;
- });
- }
-
- html += `<h4 style="margin: 20px 0 10px 0; color: #334155;">其他</h4>`;
- let tools = f.tools && f.tools.length > 0 ? f.tools.join(', ') : '无';
- html += `<div style="font-size: 13px; color: #475569; margin-bottom: 4px;"><strong>tools:</strong> ${tools}</div>`;
- if (f.artifact_type) html += `<div style="font-size: 13px; color: #475569; margin-bottom: 4px;"><strong>artifact_type:</strong> ${f.artifact_type}</div>`;
- if (f.control_target) html += `<div style="font-size: 13px; color: #475569; margin-bottom: 4px;"><strong>control_target:</strong> ${Array.isArray(f.control_target) ? f.control_target.join(', ') : f.control_target}</div>`;
-
- html += `</div>`;
- body.innerHTML = html;
- modal.classList.remove('hidden');
- };
- function renderBlueprint(bpObj) {
- if (!bpObj || (!bpObj.blueprints && !bpObj.clusters)) {
- return `<div class="data-card" style="border-color: var(--warning)"><div class="card-header"><div class="card-title">⚠️ 未知或非标准格式 (Blueprint)</div></div><div class="card-body" style="font-family: monospace; font-size: 0.85rem; overflow-x: auto;">${renderJSON(bpObj)}</div></div>`;
- }
- let html = ``;
- // New process.json format
- if (bpObj.clusters) {
- html += `<div style="margin-bottom: 1.5rem; padding: 1rem; background: rgba(0,0,0,0.2); border-radius: 8px;">
- <h3 style="color:var(--text-main); margin-bottom:0.8rem">🎯 需求</h3>
- <p style="color:var(--text-muted)">${bpObj.requirement || ''}</p>
- </div>`;
- bpObj.clusters.forEach(c => {
- if (!c.cluster_id && !c.cluster_name && !c.工序步骤) {
- html += `<div class="data-card"><div class="card-header"><div class="card-title">⚠️ 非标准聚类项</div></div><div class="card-body" style="font-family: monospace; font-size: 0.85rem; overflow-x: auto;">${renderJSON(c)}</div></div>`;
- return;
- }
- const score = c.score || 0;
- const deg = score * 360;
- const scoreClass = score >= 0.8 ? '' : (score >= 0.5 ? 'medium' : 'low');
- html += `<div class="data-card">
- <div class="card-header">
- <div class="card-title">🧩 [${c.cluster_id || 'N/A'}] ${c.cluster_name || '未命名'}</div>
- </div>
- <div class="card-body">
- <div class="score-container" style="margin-bottom:1rem">
- <div class="score-circle ${scoreClass}" style="--score-deg: ${deg}deg"><span>${Math.round(score * 100)}%</span></div>
- <div class="score-text"><strong>匹配度得分</strong></div>
- </div>
- <div class="card-section"><div class="section-title">🧠 解释 (Explanation)</div>
- <p>${c.explanation || ''}</p></div>
- <div class="card-section"><div class="section-title">📍 工序步骤</div><div class="phase-list">`;
- if (c.工序步骤) c.工序步骤.forEach(step => {
- html += `<div class="phase-item">
- <div class="phase-title">步骤 ${step.步骤序号}</div>
- <div>${step.步骤描述 || ''}</div>
- </div>`;
- });
- html += `</div></div>`;
- if (c.关联案例 && c.关联案例.length > 0) {
- html += `<div class="card-section"><div class="section-title">📌 关联案例</div>${renderCaseTags(c.关联案例)}</div>`;
- }
- html += `</div></div>`;
- });
- return html;
- }
- // Old blueprint format
- if (bpObj.distilled_cases && bpObj.distilled_cases.length > 0) {
- html += `<div style="margin-bottom: 1.5rem; padding: 1rem; background: rgba(0,0,0,0.2); border-radius: 8px;">
- <h3 style="color:var(--text-main); margin-bottom:0.8rem">📚 蓝图来源案例</h3>
- <div class="tags-container" style="gap:0.8rem">`;
- bpObj.distilled_cases.forEach(c => {
- let targetId = c.id;
- const matchA = targetId.match(/^case_([a-z]+)_(\d+)/);
- if (matchA) {
- targetId = `${matchA[1]}-case_${matchA[2]}`;
- } else {
- const matchB = targetId.match(/^([a-z]+)[\/\s](case_\d+)/);
- if (matchB) {
- targetId = `${matchB[1]}-${matchB[2]}`;
- } else {
- const matchC = targetId.match(/^case_(\d+)_([a-z]+)/);
- if (matchC) targetId = `${matchC[2]}-case_${matchC[1]}`;
- }
- }
- html += `<a href="#" onclick="jumpToCase('${targetId}'); return false;" class="badge-emoji primary" style="text-decoration:none; white-space:normal; text-align:left; line-height:1.4">
- <strong>🔍 ${c.id}</strong><br>
- <span style="font-size:0.75rem">${c.title ? c.title.substring(0, 40) + (c.title.length > 40 ? '...' : '') : 'View Source'}</span>
- </a>`;
- });
- html += `</div></div>`;
- }
- if (bpObj.blueprints) bpObj.blueprints.forEach(bp => {
- if (!bp.name && !bp.phases) {
- html += `<div class="data-card"><div class="card-header"><div class="card-title">⚠️ 非标准蓝图项</div></div><div class="card-body" style="font-family: monospace; font-size: 0.85rem; overflow-x: auto;">${renderJSON(bp)}</div></div>`;
- return;
- }
- html += `<div class="data-card">
- <div class="card-header">
- <div class="card-title">🗺️ ${bp.name || '未命名'}</div>
- </div>
- <div class="card-body">
- <div class="card-section"><div class="section-title">🧠 推理逻辑 (Reasoning)</div>
- <p>${bp.reasoning || ''}</p></div>
- <div class="card-section"><div class="section-title">📍 阶段 (Phases)</div><div class="phase-list">`;
- if (bp.phases) bp.phases.forEach(ph => {
- html += `<div class="phase-item">
- <div class="phase-title">${ph.phase || ''}</div>
- <div>${ph.description || ''}</div>
- </div>`;
- });
- html += `</div></div></div>`;
- });
- return html;
- }
- function renderStrategy(stratObj) {
- if (!stratObj || (!stratObj.strategies && !stratObj.workflow)) {
- return `<div class="data-card" style="border-color: var(--warning)"><div class="card-header"><div class="card-title">⚠️ 未知或非标准格式 (Strategy)</div></div><div class="card-body" style="font-family: monospace; font-size: 0.85rem; overflow-x: auto;">${renderJSON(stratObj)}</div></div>`;
- }
- let html = `<div style="margin-bottom: 1.5rem; padding: 1rem; background: rgba(0,0,0,0.2); border-radius: 8px;">
- <h3 style="color:var(--text-main); margin-bottom:0.5rem">🎯 需求描述</h3>
- <p style="color:var(--text-muted)">${stratObj.requirement || ''}</p>
- </div>`;
- // New Workflow Format
- if (stratObj.workflow) {
- stratObj.workflow.forEach(strat => {
- if (!strat.cluster_id && !strat.cluster_name && !strat.steps) {
- html += `<div class="data-card"><div class="card-header"><div class="card-title">⚠️ 非标准工作流项</div></div><div class="card-body" style="font-family: monospace; font-size: 0.85rem; overflow-x: auto;">${renderJSON(strat)}</div></div>`;
- return;
- }
- const score = strat.score || 0;
- const deg = score * 360;
- const scoreClass = score >= 0.8 ? '' : (score >= 0.5 ? 'medium' : 'low');
- let scoreHtml = `<div class="score-container">
- <div class="score-circle ${scoreClass}" style="--score-deg: ${deg}deg"><span>${Math.round(score * 100)}%</span></div>
- <div class="score-text"><strong>匹配得分</strong><br>${strat.explanation || ''}</div>
- </div>`;
- html += `<div class="data-card">
- <div class="card-header">
- <div class="card-title">🎯 [${strat.cluster_id}] ${strat.cluster_name || '未命名'}</div>
- </div>
- <div class="card-body">
- ${scoreHtml}
- ${(strat.关联案例 && strat.关联案例.length > 0) ? `<div class="card-section" style="margin-bottom: 1rem;"><div class="section-title">📌 关联案例</div>${renderCaseTags(strat.关联案例)}</div>` : ''}
- <div class="card-section"><div class="section-title">🧱 工作流步骤</div><div class="phase-list">`;
- if (strat.steps) strat.steps.forEach(step => {
- html += `<div class="phase-item">
- <div class="phase-title">步骤 ${step.步骤序号}</div>
- <div style="margin-bottom:0.5rem">${step.步骤描述}</div>
- <div class="tags-container">`;
- if (step.capabilities) {
- step.capabilities.forEach(cap => {
- html += `<div style="margin-top: 0.5rem; padding: 0.5rem; background: rgba(0, 0, 0, 0.05); border-radius: 4px; width: 100%;">
- <div style="font-weight: bold; margin-bottom: 0.3rem;">⚡ [${cap.id}] ${cap.name}</div>
- <div style="font-size: 0.8rem; color: var(--text-muted);">${cap.description}</div>
- ${(cap.case_references && cap.case_references.length > 0) ? `<div style="margin-top: 0.5rem;">${renderCaseTags(cap.case_references)}</div>` : ''}
- </div>`;
- });
- }
- html += `</div></div>`;
- });
- html += `</div></div></div></div>`;
- });
- 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 += `<div class="data-card"><div class="card-header"><div class="card-title">⚠️ 非标准策略项</div></div><div class="card-body" style="font-family: monospace; font-size: 0.85rem; overflow-x: auto;">${renderJSON(strat)}</div></div>`;
- return;
- }
- const isSelected = strat.is_selected;
- const icon = isSelected ? '🎯' : '🥈';
- const badge = isSelected ? '<span class="badge-emoji success">⭐ 被选中的策略</span>' : '<span class="badge-emoji warning">备选策略</span>';
- 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 = `<div class="score-container">
- <div class="score-circle ${scoreClass}" style="--score-deg: ${deg}deg"><span>${Math.round(score * 100)}%</span></div>
- <div class="score-text"><strong>覆盖率得分</strong><br>${strat.coverage_explanation || ''}</div>
- </div>`;
- }
- html += `<div class="data-card" style="${isSelected ? 'border-color: var(--accent-primary); box-shadow: 0 0 15px rgba(59,130,246,0.1);' : ''}">
- <div class="card-header">
- <div class="card-title">${icon} ${strat.name || '未命名'}</div>
- ${badge}
- </div>
- <div class="card-body">
- ${scoreHtml}
- <div class="tags-container" style="margin-bottom:1rem">
- <span class="badge-emoji">📥 来源: ${strat.source || 'N/A'}</span>
- </div>`;
- if (strat.reasoning) html += `<div class="card-section"><div class="section-title">🧠 推理逻辑 (Reasoning)</div><p>${strat.reasoning}</p></div>`;
- if (strat.why_not) html += `<div class="card-section"><div class="section-title">❌ 为何未被选中</div><p>${strat.why_not}</p></div>`;
- if (strat.workflow_outline && strat.workflow_outline.length > 0) {
- html += `<div class="card-section"><div class="section-title">🧱 工作流大纲</div><div class="phase-list">`;
- strat.workflow_outline.forEach(wo => {
- html += `<div class="phase-item">
- <div class="phase-title">${wo.phase}</div>
- <div style="margin-bottom:0.5rem">${wo.description}</div>
- <div class="tags-container">`;
- if (wo.capabilities) {
- wo.capabilities.forEach(cap => html += `<span class="badge-emoji primary">⚡ ${cap.name}</span>`);
- }
- html += `</div></div>`;
- });
- html += `</div></div>`;
- }
- html += `</div></div>`;
- });
- }
- if (stratObj.uncovered_requirements && stratObj.uncovered_requirements.length > 0) {
- html += `<div class="data-card" style="border-color: var(--danger)">
- <div class="card-header"><div class="card-title">⚠️ Uncovered Requirements</div></div>
- <div class="card-body"><ul>`;
- stratObj.uncovered_requirements.forEach(req => html += `<li>${req}</li>`);
- html += `</ul></div></div>`;
- }
- return html;
- }
|