| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224 |
- window.openCaseDetail = function (p, initialIdx) {
- if (!window._currentRawCasesContext) return;
- const ctx = window._currentRawCasesContext;
- // Determine the list of platforms to aggregate.
- let platformsToAggregate = [p];
- if (p !== 'filtered_cases' && p !== 'source_ex') {
- const crawlerPlatforms = Object.keys(ctx.rawCasesObj).filter(k => k !== 'source' && k !== 'case_detailed' && k !== 'case' && k !== 'images' && k !== 'filtered_cases' && k !== 'source_ex');
- platformsToAggregate = [...crawlerPlatforms, 'source'];
- }
- let casesList = [];
- let globalInitialIdx = 0;
- const seenIds = new Set();
- platformsToAggregate.forEach(plat => {
- if (!ctx.rawCasesObj[plat]) return;
- let platCases = [];
- if (Array.isArray(ctx.rawCasesObj[plat])) {
- platCases = ctx.rawCasesObj[plat];
- } else if (ctx.rawCasesObj[plat].cases) {
- platCases = ctx.rawCasesObj[plat].cases;
- } else if (ctx.rawCasesObj[plat].sources) {
- platCases = ctx.rawCasesObj[plat].sources;
- } else if (ctx.rawCasesObj[plat].by_reason) {
- Object.entries(ctx.rawCasesObj[plat].by_reason).forEach(([reasonKey, reasonObj]) => {
- if (reasonObj.sources && Array.isArray(reasonObj.sources)) {
- reasonObj.sources.forEach(src => {
- if (!src.filter_reason) src.filter_reason = reasonKey;
- platCases.push(src);
- });
- }
- });
- }
- platCases.forEach((c, idx) => {
- const cId = c.case_id || (c._raw && c._raw.case_id) || (c.post && c.post.channel_content_id) || `temp_${plat}_${idx}`;
- const cUrl = c.source_url || c.url || (c.post && c.post.link) || '';
- const mappedS = ctx.sourceMap[cId] || ctx.sourceMap[cUrl] || (c._raw && ctx.sourceMap[c._raw.case_id]);
- if (plat !== 'filtered_cases' && plat !== 'source' && plat !== 'source_ex' && !mappedS) return;
- if (seenIds.has(cId)) return;
- seenIds.add(cId);
- const augmentedC = { ...c, _actualPlatform: plat };
- if (plat === p && idx === initialIdx) {
- globalInitialIdx = casesList.length;
- }
- casesList.push(augmentedC);
- });
- });
- window._currentModalCases = casesList;
- window._currentModalPlatform = p;
- window._currentModalContext = ctx;
- window._currentModalIdx = globalInitialIdx;
- // Build Sidebar
- let sidebarHtml = '<div class="modal-sidebar">';
- casesList.forEach((c, idx) => {
- 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 title = post.title || c.title || '无标题';
- let metaHtml = '';
- const wf = ctx.detailMap[cId] || (cUrl ? ctx.detailMapByUrl[cUrl] : null) || c;
- const workflowGroups = getWorkflowGroups(wf);
- const capabilityItems = getCapabilityItems(wf);
- if (workflowGroups.length > 0) metaHtml += `<span>工作流 ${workflowGroups.length}</span>`;
- if (capabilityItems.length > 0) metaHtml += `<span>能力 ${capabilityItems.length}</span>`;
- if (!metaHtml) metaHtml = '<span>无提取</span>';
- sidebarHtml += `<div class="modal-sidebar-item ${idx === globalInitialIdx ? 'active' : ''}" id="sidebar-item-${idx}" onclick="window.renderSingleCaseDetail(${idx})">
- <div class="sidebar-item-index">${idx + 1}.</div>
- <div class="sidebar-item-content">
- <div class="sidebar-item-title">${title}</div>
- <div class="sidebar-item-meta">${metaHtml}</div>
- </div>
- </div>`;
- });
- sidebarHtml += '</div>';
- // Build Main Content Skeleton
- const mainHtml = `
- <div class="modal-main-content">
- <div id="modal-main-header" style="padding: 1.5rem 1.5rem 0 1.5rem; flex-shrink: 0;"></div>
- <div class="modal-main-scrollable" id="modal-main-scrollable"></div>
- </div>
- `;
- document.getElementById('case-detail-modal-body').innerHTML = sidebarHtml + mainHtml;
- // Render the selected case
- window.renderSingleCaseDetail(globalInitialIdx);
- document.getElementById('case-detail-modal').classList.remove('hidden');
- // Scroll sidebar to active item
- setTimeout(() => {
- const activeItem = document.getElementById(`sidebar-item-${globalInitialIdx}`);
- if (activeItem) activeItem.scrollIntoView({ block: 'nearest' });
- }, 10);
- };
- window.renderSingleCaseDetail = function (idx) {
- window._currentModalIdx = idx;
- const ctx = window._currentModalContext;
- const c = window._currentModalCases[idx];
- if (!c) return;
- // Update Sidebar Active State
- document.querySelectorAll('.modal-sidebar-item').forEach(el => el.classList.remove('active'));
- const activeEl = document.getElementById(`sidebar-item-${idx}`);
- if (activeEl) activeEl.classList.add('active');
- const p = c._actualPlatform || window._currentModalPlatform;
- const platCode = p.replace('case_', '');
- const cId = c.case_id || (c._raw && c._raw.case_id) || (c.post && c.post.channel_content_id) || `temp_${idx}`;
- const cUrl = c.source_url || c.url || (c.post && c.post.link) || '';
- const mappedS = ctx.sourceMap[cId] || ctx.sourceMap[cUrl] || (c._raw && ctx.sourceMap[c._raw.case_id]);
- const s = mappedS || c;
- const post = s.post || s || {};
- const platformName = s.platform || (s._raw && s._raw.platform) || platCode;
- const title = post.title || c.title || '无标题';
- const workflowUrl = s.source_url || s.url || cUrl;
- const publishedTime = post.publish_timestamp || post.published_at || '-';
- const likeCount = post.like_count !== undefined ? post.like_count : (post.likes !== undefined ? post.likes : '-');
- const collectCount = post.collect_count !== undefined ? post.collect_count : (post.collects !== undefined ? post.collects : '-');
- const commentCount = post.comment_count !== undefined ? post.comment_count : (post.comments !== undefined ? post.comments : '-');
- const shareCount = post.share_count !== undefined ? post.share_count : (post.shares !== undefined ? post.shares : '-');
- const isFiltered = (c._actualPlatform === 'filtered_cases');
- let filterActionHtml = '';
- if (isFiltered) {
- filterActionHtml = `<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 = `
- <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>
- </div>
- <div style="display: flex; gap: 10px; margin-bottom: 1rem; flex-wrap: wrap;">
- <div style="display: flex; flex-direction: column; background: rgba(0,0,0,0.02); border: 1px solid rgba(0,0,0,0.06); padding: 4px 10px; border-radius: 6px;">
- <span style="font-size: 0.7em; color: var(--text-muted); text-transform: uppercase;">Published</span>
- <span style="font-size: 0.9rem; font-weight: 500; color: var(--text-main);">${publishedTime}</span>
- </div>
- <div style="display: flex; flex-direction: column; background: rgba(0,0,0,0.02); border: 1px solid rgba(0,0,0,0.06); padding: 4px 10px; border-radius: 6px;">
- <span style="font-size: 0.7em; color: var(--text-muted); text-transform: uppercase;">Likes</span>
- <span style="font-size: 0.9rem; font-weight: 500; color: var(--text-main);">${likeCount}</span>
- </div>
- <div style="display: flex; flex-direction: column; background: rgba(0,0,0,0.02); border: 1px solid rgba(0,0,0,0.06); padding: 4px 10px; border-radius: 6px;">
- <span style="font-size: 0.7em; color: var(--text-muted); text-transform: uppercase;">Collects</span>
- <span style="font-size: 0.9rem; font-weight: 500; color: var(--text-main);">${collectCount}</span>
- </div>
- <div style="display: flex; flex-direction: column; background: rgba(0,0,0,0.02); border: 1px solid rgba(0,0,0,0.06); padding: 4px 10px; border-radius: 6px;">
- <span style="font-size: 0.7em; color: var(--text-muted); text-transform: uppercase;">Comments</span>
- <span style="font-size: 0.9rem; font-weight: 500; color: var(--text-main);">${commentCount}</span>
- </div>
- <div style="display: flex; flex-direction: column; background: rgba(0,0,0,0.02); border: 1px solid rgba(0,0,0,0.06); padding: 4px 10px; border-radius: 6px;">
- <span style="font-size: 0.7em; color: var(--text-muted); text-transform: uppercase;">Shares</span>
- <span style="font-size: 0.9rem; font-weight: 500; color: var(--text-main);">${shareCount}</span>
- </div>
- </div>
- `;
- 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) {
- const lightboxArr = allImages.map((imgUrl, i) => ({
- local: `/output/${ctx.reqId}/raw_cases/images/${cId}/${String(i).padStart(2, '0')}.jpg`,
- remote: imgUrl
- }));
- const lightboxStr = JSON.stringify(lightboxArr).replace(/"/g, '"');
- mediaHtml += `<div class="image-gallery" style="margin-bottom: 1rem;">`;
- allImages.forEach((imgUrl, imgIdx) => {
- const localPath = lightboxArr[imgIdx].local;
- mediaHtml += `<div class="image-item" style="cursor:pointer" onclick="event.stopPropagation(); window.openLightbox(${lightboxStr}, ${imgIdx})">
- <img src="${localPath}" onerror="this.onerror=null; this.src='${imgUrl}';">
- </div>`;
- });
- mediaHtml += `</div>`;
- }
- 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 += `<div style="display:flex; flex-wrap:wrap; gap:8px; margin-bottom: 1rem;">`;
- allVideos.forEach(vidUrl => {
- mediaHtml += `<video controls src="${vidUrl}" style="height: 200px; border-radius: 6px; border: 1px solid rgba(0, 0, 0, 0.1);"></video>`;
- });
- mediaHtml += `</div>`;
- }
- const bodyText = post.body_text || post.body || '';
- // Source Panel
- let mainScrollableHtml = `
- <div class="source-panel open" id="source-panel">
- <div class="source-panel-header" onclick="document.getElementById('source-panel').classList.toggle('open')">
- <span>原始内容 / SOURCE POSTS</span>
- <span style="font-size: 0.8em; color: var(--text-muted);">点击展开/折叠</span>
- </div>
- <div class="source-panel-body">
- ${mediaHtml}
- ${bodyText ? `<div style="font-size:0.9rem; line-height:1.6; color:var(--text-main); white-space: pre-wrap;">${bodyText}</div>` : '<div style="color:var(--text-muted)">无正文</div>'}
- </div>
- </div>
- `;
- // Evaluation Panel
- if (s.evaluation && Object.keys(s.evaluation).length > 0) {
- const renderEvalNode = (node, indent = 0) => {
- let html = '';
- if (typeof node === 'object' && node !== null) {
- Object.entries(node).forEach(([k, v]) => {
- html += `<div style="display: flex; flex-direction: column; padding-left: ${indent}px; margin-bottom: 8px;">
- <span style="color: var(--text-muted); font-size: 0.85rem; font-weight: bold; text-transform: uppercase;">${k.replace(/_/g, ' ')}</span>`;
- if (typeof v === 'object' && v !== null) {
- html += `<div style="margin-top: 4px; border-left: 2px solid rgba(0,0,0,0.1); padding-left: 8px;">${renderEvalNode(v, 0)}</div>`;
- } else {
- const valColor = typeof v === 'number' ? '#3b82f6' : 'var(--text-main)';
- html += `<span style="font-weight: 500; font-size: 0.95rem; color: ${valColor}; margin-top: 2px;">${String(v).replace(/</g, '<').replace(/>/g, '>')}</span>`;
- }
- html += `</div>`;
- });
- }
- return html;
- };
- mainScrollableHtml += `
- <div class="case-section" style="margin-top: 1rem;">
- <div style="background: rgba(0,0,0,0.02); border: 1px solid rgba(0,0,0,0.05); border-radius: 8px; padding: 1rem;">
- <h3 style="margin: 0 0 1rem 0; color: var(--text-main); font-size: 1.1rem; display: flex; align-items: center; gap: 10px;">
- <span style="color: #10b981;">📊</span> 质量评估 (Evaluation)
- </h3>
- <div style="display: flex; flex-direction: column; gap: 4px;">
- ${renderEvalNode(s.evaluation)}
- </div>
- </div>
- </div>`;
- }
- // 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 ? `<button class="btn btn-secondary" style="font-size: 0.8em; padding: 0.3rem 0.6rem; border-radius: 4px;" onclick="event.stopPropagation(); triggerSingleCaseRerun('workflow-extract', ${caseIndexToPass})">🔄 重跑工序</button>` : '';
- const btnCapabilityHtml = caseIndexToPass !== -1 ? `<button class="btn btn-secondary" style="font-size: 0.8em; padding: 0.3rem 0.6rem; border-radius: 4px;" onclick="event.stopPropagation(); triggerSingleCaseRerun('workflow-extract', ${caseIndexToPass})">🔄 重跑能力</button>` : '';
- mainScrollableHtml += `
- <div class="case-section" style="margin-top: 2rem;">
- <div style="display: flex; align-items: center; justify-content: space-between; background: rgba(0,0,0,0.02); border-radius: 8px; padding-right: 1rem; cursor: pointer;" onclick="const content = this.nextElementSibling; content.classList.toggle('hidden'); this.querySelector('.case-arrow').textContent = content.classList.contains('hidden') ? '▶' : '▼';">
- <h3 style="margin: 0; padding: 1rem; color: var(--text-main); font-size: 1.1rem; display: flex; align-items: center; gap: 10px; user-select: none;">
- <span class="case-arrow" style="font-size: 0.8em; color: var(--text-muted); width: 16px; display: inline-block;">▶</span>
- <span style="color: var(--accent-primary);">⚡</span> 提取的工序 (Strategy)
- </h3>
- ${btnWorkflowHtml}
- </div>
- <div class="hidden" style="padding-top: 1.2rem;">
- ${window.renderStructuredData(getWorkflowItems(wf), 'workflow', wf)}
- </div>
- </div>
-
- <div class="case-section" style="margin-top: 1rem; margin-bottom: 2rem;">
- <div style="display: flex; align-items: center; justify-content: space-between; background: rgba(0,0,0,0.02); border-radius: 8px; padding-right: 1rem; cursor: pointer;" onclick="const content = this.nextElementSibling; content.classList.toggle('hidden'); this.querySelector('.case-arrow').textContent = content.classList.contains('hidden') ? '▶' : '▼';">
- <h3 style="margin: 0; padding: 1rem; color: var(--text-main); font-size: 1.1rem; display: flex; align-items: center; gap: 10px; user-select: none;">
- <span class="case-arrow" style="font-size: 0.8em; color: var(--text-muted); width: 16px; display: inline-block;">▶</span>
- <span style="color: var(--accent-secondary);">✨</span> 提取的能力 (Capability)
- </h3>
- ${btnCapabilityHtml}
- </div>
- <div class="hidden" style="padding-top: 1.2rem;">
- ${window.renderStructuredData(getCapabilityItems(wf), 'capabilities', wf)}
- </div>
- </div>
- `;
- 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 `<div style="color:var(--text-muted); padding: 1rem;">暂无${type === 'workflow' ? '工序' : '能力'}数据</div>`;
- }
- const formatIOs = (ios) => {
- if (!ios || !Array.isArray(ios) || ios.length === 0) return '';
- const escapeHtml = (s) => String(s).replace(/</g, '<').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, '<').replace(/>/g, '>');
- const inStr = formatIOs(inputs) || '无';
- const outStr = formatIOs(outputs) || '无';
- let parts = [];
- parts.push(inStr);
- if (actionStr) parts.push(`<strong>${escapeHtml(actionStr)}</strong>`);
- parts.push(outStr);
- return parts.join(' ➔ ');
- };
- let html = '';
- items.forEach((item, idx) => {
- const scopeId = 'scope_' + Math.random().toString(36).substr(2, 9);
- let title = '';
- const hasValidIO = (arr) => Array.isArray(arr) && arr.length > 0 && (arr[0].role || arr[0].description);
- if (hasValidIO(item.inputs) || hasValidIO(item.outputs) || (item.steps && item.steps.length > 0)) {
- let actionStr = '';
- if (item.action && item.action.description) {
- actionStr = item.action.description;
- } else if (item.method && !item.method.includes('[')) {
- actionStr = item.method;
- } else if (item.steps && Array.isArray(item.steps)) {
- const hasAnyValidIO = item.steps.some(s => hasValidIO(s.inputs) || hasValidIO(s.outputs) || s.body);
- if (hasAnyValidIO) {
- actionStr = item.steps.map(s => {
- if (s.action && s.action.description) {
- return s.action.description;
- }
- if (s.body) return s.body.length > 20 ? s.body.substring(0, 20) + '...' : s.body;
- if (s.method) return s.method;
- if (s.phase) return s.phase;
- return '未知';
- }).join(' ➔ ');
- } else {
- actionStr = item.method || item.name || type === 'workflow' ? '工作流' : `节点 ${idx + 1}`;
- }
- }
- if (hasValidIO(item.inputs) || hasValidIO(item.outputs)) {
- title = buildFullTitle(item.inputs, item.outputs, actionStr, item.method || item.name || `节点 ${idx + 1}`);
- } else {
- title = String(actionStr).replace(/</g, '<').replace(/>/g, '>');
- }
- } else {
- const escapeHtml = (s) => String(s).replace(/</g, '<').replace(/>/g, '>');
- title = escapeHtml(item.method || item.name || '');
- if (!title && item.action && item.action.description) {
- title = escapeHtml(item.action.description);
- }
- if (!title && item.body) {
- const actText = item.body.length > 50 ? item.body.substring(0, 50) + '...' : item.body;
- title = escapeHtml(actText);
- }
- if (!title) {
- title = escapeHtml(type === 'workflow' ? '工作流' : `节点 ${idx + 1}`);
- }
- }
- if (type === 'workflow' && item.workflow_id) {
- title = `${title} <span style="color:#94a3b8; font-family:monospace; font-size:0.75em;">${String(item.workflow_id).replace(/</g, '<').replace(/>/g, '>')}</span>`;
- }
- // Tree node tags (from apply_to keys) or unstructured_what fallback
- const getApplyToField = (it) => {
- if (it.apply_to_grounding) return { key: 'apply_to_grounding', val: it.apply_to_grounding, suggest: it.suggest_apply_to };
- if (it.apply_to_draft) return { key: 'apply_to_draft', val: it.apply_to_draft, suggest: null };
- if (it.apply_to) return { key: 'apply_to', val: it.apply_to, suggest: it.suggest_apply_to };
- return null;
- };
- const applyToData = getApplyToField(item);
- let treeNodeTags = '';
- if (applyToData && typeof applyToData.val === 'object') {
- const allLeafs = [];
- Object.values(applyToData.val).forEach(v => {
- if (Array.isArray(v)) {
- v.forEach(pathObj => {
- let leaf = '';
- if (typeof pathObj === 'object' && pathObj !== null) {
- if (pathObj.element) leaf = pathObj.element;
- else if (pathObj.category_path || pathObj.path) {
- leaf = (pathObj.category_path || pathObj.path).split('/').pop();
- }
- } else {
- leaf = String(pathObj).split('/').pop();
- }
- if (leaf) allLeafs.push(leaf);
- });
- }
- });
- const uniqueLeafs = [...new Set(allLeafs)];
- const badgeStyle = 'background: #f8fafc; color: #475569; border: 1px solid #cbd5e1; border-radius: 12px; padding: 2px 10px; font-size: 0.85em; font-weight: normal; margin-right: 6px; display: inline-block; white-space: nowrap;';
- treeNodeTags = uniqueLeafs.map(leaf => `<span class="unstruct-badge" style="${badgeStyle}">${leaf.replace(/</g, '<').replace(/>/g, '>')}</span>`).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 => `<span class="unstruct-badge" style="${badgeStyle}">${String(t).replace(/</g, '<').replace(/>/g, '>')}</span>`).join('');
- }
- html += `<div class="structured-card">
- <div class="structured-card-title-row" style="display:flex; align-items:center; flex-wrap:wrap; gap: 8px; margin-bottom: 1rem;">
- <div class="structured-card-title" style="margin:0;">${title}</div>
- ${treeNodeTags}
- </div>
- `;
- const renderApplyToVal = (valObj, suggestApplyTo = null) => {
- if (!valObj || typeof valObj !== 'object') return '-';
- const escapeApplyToText = (s) => String(s).replace(/</g, '<').replace(/>/g, '>');
- const renderPathParts = (pathValue, highlight = false) => {
- const pathStr = String(pathValue || '').trim();
- if (!pathStr) return '';
- const parts = pathStr.split('/');
- const leaf = parts.pop();
- const prefix = parts.length > 0 ? parts.join('/') + '/' : '';
- const leafStyle = highlight
- ? 'background:#eff6ff; color:#2563eb; border:1px solid #bfdbfe;'
- : '';
- return `
- ${prefix ? `<span class="apply-to-path-prefix">${escapeApplyToText(prefix)}</span>` : ''}
- <span class="apply-to-path-leaf" style="${leafStyle}">${escapeApplyToText(leaf)}</span>
- `;
- };
- const renderTooltip = (pathObj) => {
- if (
- typeof pathObj !== 'object'
- || pathObj === null
- || !(pathObj.rationale || pathObj.body_excerpt || pathObj.body_excerpt_note || pathObj.body_excerpt_type || pathObj.category_id)
- ) {
- return '';
- }
- return `
- <div class="apply-to-tooltip">
- ${pathObj.category_id ? `<span class="tooltip-id">id: ${pathObj.category_id}</span>` : ''}
- ${pathObj.rationale ? `<span class="tooltip-rationale">${escapeApplyToText(pathObj.rationale)}</span>` : ''}
- ${pathObj.body_excerpt ? `<span class="tooltip-body-excerpt">${escapeApplyToText(pathObj.body_excerpt)}</span>` : ''}
- ${pathObj.body_excerpt_note ? `<span class="tooltip-body-note">${escapeApplyToText(pathObj.body_excerpt_note)}</span>` : ''}
- ${pathObj.body_excerpt_type ? `<span class="tooltip-body-note">type: ${escapeApplyToText(pathObj.body_excerpt_type)}</span>` : ''}
- </div>
- `;
- };
- const renderEvidence = (pathObj) => {
- if (typeof pathObj !== 'object' || pathObj === null) return '';
- if (!('body_excerpt' in pathObj) && !('body_excerpt_note' in pathObj) && !('body_excerpt_type' in pathObj)) return '';
- const excerpt = pathObj.body_excerpt || '';
- const note = pathObj.body_excerpt_note || '';
- const excerptType = pathObj.body_excerpt_type || '';
- const excerptKey = excerpt ? makeExcerptKey(excerpt) : '';
- return `<div class="apply-to-evidence">
- <div class="apply-to-evidence-row">
- <span class="apply-to-evidence-label">关联做法 <span class="apply-to-evidence-type ${excerptType ? '' : 'empty'}">${excerptType ? escapeApplyToText(excerptType) : 'type: 空'}</span></span>
- <span class="apply-to-evidence-value apply-to-evidence-note ${note ? '' : 'empty'}" ${excerptKey ? `data-excerpt-key="${excerptKey}"` : ''}>${note ? escapeApplyToText(note) : '空'}</span>
- </div>
- </div>`;
- };
- let res = '<div style="display:flex; flex-direction:column; gap:6px;">';
- let hasRows = false;
- Object.entries(valObj).forEach(([k, v]) => {
- if (Array.isArray(v) && v.length > 0) {
- hasRows = true;
- res += `<div class="apply-to-subrow">
- <span class="apply-to-key-badge" style="background: rgba(0,0,0,0.05); color: #475569;">${escapeApplyToText(k)}</span>
- <div class="apply-to-values" style="display:flex; flex-wrap:wrap; gap:4px;">`;
- 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 = renderTooltip(pathObj);
- let htmlParts = '';
- if (pathStr && elementStr) {
- htmlParts = `<span class="apply-to-path-prefix">${escapeApplyToText(pathStr)}</span><span class="apply-to-path-leaf" style="margin-left: 4px;">${escapeApplyToText(elementStr)}</span>`;
- } else if (pathStr) {
- htmlParts = renderPathParts(pathStr);
- }
- if (htmlParts) {
- const evidenceHtml = renderEvidence(pathObj);
- res += `<span class="apply-to-path-block"><span class="apply-to-path-item ${tooltipHtml ? 'has-tooltip' : ''}">${htmlParts}${tooltipHtml}</span>${evidenceHtml}</span>`;
- }
- });
- res += `</div></div>`;
- }
- });
- const suggestItems = Array.isArray(suggestApplyTo)
- ? suggestApplyTo
- : (typeof suggestApplyTo === 'string' && suggestApplyTo.trim() ? [{ path: suggestApplyTo }] : []);
- if (suggestItems.length > 0) {
- hasRows = true;
- res += `<div class="apply-to-subrow">
- <span class="apply-to-key-badge" style="background:#eff6ff; color:#2563eb; border-color:#bfdbfe;">建议</span>
- <div class="apply-to-values" style="display:flex; flex-wrap:wrap; gap:4px;">`;
- suggestItems.forEach(item => {
- const pathStr = typeof item === 'object' && item !== null ? item.path : String(item || '');
- const tooltipHtml = renderTooltip(item);
- if (pathStr) {
- const evidenceHtml = renderEvidence(item);
- res += `<span class="apply-to-path-block"><span class="apply-to-path-item ${tooltipHtml ? 'has-tooltip' : ''}" style="border: 2px dashed #94a3b8; background: transparent;">${renderPathParts(pathStr, true)}${tooltipHtml}</span>${evidenceHtml}</span>`;
- }
- });
- res += `</div></div>`;
- }
- res += `</div>`;
- return hasRows ? res : '-';
- };
- // Render apply_to / apply_to_grounding at workflow level (if it exists)
- if (applyToData && typeof applyToData.val === 'object' && Object.keys(applyToData.val).length > 0) {
- html += `<div class="structured-row">
- <div class="structured-label">${applyToData.key}</div>
- <div class="structured-value">${renderApplyToVal(applyToData.val, applyToData.suggest)}</div>
- </div>`;
- }
- if (item.action && typeof item.action === 'object' && (item.action.description || item.action.reasoning)) {
- const actionDescription = item.action.description ? String(item.action.description).replace(/</g, '<').replace(/>/g, '>') : '';
- const actionReasoning = item.action.reasoning ? String(item.action.reasoning).replace(/</g, '<').replace(/>/g, '>') : '';
- html += `<div class="structured-row">
- <div class="structured-label">action</div>
- <div class="structured-value">
- <style>
- .action-description-tooltip:hover .action-reasoning-popover { display:block !important; }
- </style>
- ${actionDescription ? `<span class="action-description-tooltip" style="position:relative; display:inline-block;">
- <span class="data-type-badge" style="background:#e0e7ff;color:#3730a3;font-weight:normal;margin-right:6px;">${actionDescription}</span>
- ${actionReasoning ? `<span class="action-reasoning-popover" style="display:none; position:absolute; left:0; top:calc(100% + 6px); z-index:50; width:300px; max-width:60vw; padding:8px 10px; border-radius:8px; background:#0f172a; color:#f8fafc; box-shadow:0 10px 25px rgba(15,23,42,0.18); font-size:0.86em; line-height:1.5; white-space:normal; font-weight:400;">${actionReasoning}</span>` : ''}
- </span>` : ''}
- </div>
- </div>`;
- }
- // Stage rendering removed per request
- // Render effects
- if (item.effects && Array.isArray(item.effects) && item.effects.length > 0) {
- let effectsHtml = '';
- item.effects.forEach(effectItem => {
- if (typeof effectItem === 'string') {
- effectsHtml += `<li style="margin-bottom: 4px;">${effectItem.replace(/</g, '<').replace(/>/g, '>')}</li>`;
- } else if (typeof effectItem === 'object' && effectItem !== null) {
- const stmt = effectItem.statement ? effectItem.statement.replace(/</g, '<').replace(/>/g, '>') : 'Effect';
- let detailsHtml = '';
- const excludeKeys = ['statement'];
- Object.entries(effectItem).forEach(([k, v]) => {
- if (!excludeKeys.includes(k) && v !== null && v !== undefined && v !== '' && (!Array.isArray(v) || v.length > 0)) {
- let valStr = '';
- if (Array.isArray(v)) {
- valStr = `<ul style="margin: 2px 0 0 20px; padding: 0;">` + v.map(vi => `<li>${String(vi).replace(/</g, '<').replace(/>/g, '>')}</li>`).join('') + `</ul>`;
- } else if (typeof v === 'object') {
- valStr = JSON.stringify(v);
- } else {
- valStr = String(v).replace(/</g, '<').replace(/>/g, '>');
- }
- if (Array.isArray(v)) {
- detailsHtml += `<div style="margin-top: 6px; font-size: 0.9em;">
- <div style="color: var(--text-muted); font-weight: 500; margin-bottom: 2px; text-transform: capitalize;">${k.replace(/_/g, ' ')}:</div>
- <div style="color: var(--text-main);">${valStr}</div>
- </div>`;
- } else {
- detailsHtml += `<div style="margin-top: 4px; font-size: 0.9em;">
- <span style="color: var(--text-muted); font-weight: 500; text-transform: capitalize;">${k.replace(/_/g, ' ')}:</span>
- <span style="color: var(--text-main);">${valStr}</span>
- </div>`;
- }
- }
- });
- effectsHtml += `<li style="list-style: none; margin-bottom: 8px; margin-left: -20px;">
- <details style="background: rgba(0,0,0,0.02); border: 1px solid rgba(0,0,0,0.06); border-radius: 6px; padding: 6px 10px;">
- <summary style="cursor: pointer; font-weight: 500; color: var(--accent-primary); outline: none;">
- ${stmt}
- </summary>
- <div style="padding-top: 8px; margin-top: 6px; border-top: 1px dashed rgba(0,0,0,0.1);">
- ${detailsHtml}
- </div>
- </details>
- </li>`;
- }
- });
- html += `<div class="structured-row">
- <div class="structured-label">effects</div>
- <div class="structured-value">
- <ul class="effects-list">
- ${effectsHtml}
- </ul>
- </div>
- </div>`;
- }
- // 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 += `<div class="structured-row">
- <div class="structured-label" style="display:flex; align-items:center; gap:4px;">
- 置信度
- </div>
- <div class="structured-value" style="display: flex; flex-wrap: wrap; gap: 8px; font-size: 0.85em;">
- <div style="background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.06); padding: 4px 8px; border-radius: 6px; display:flex; gap: 6px;">
- <span style="color: var(--text-muted);">Maturity:</span>
- <span style="font-weight: 500; color: var(--text-main);">${String((item.maturity || (parentItem && parentItem.maturity)) || '-').replace(/</g, '<').replace(/>/g, '>')}</span>
- </div>
- <div style="background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.06); padding: 4px 8px; border-radius: 6px; display:flex; gap: 6px;">
- <span style="color: var(--text-muted);">Validation:</span>
- <span style="font-weight: 500; color: var(--text-main);">${(item.validation_count !== undefined && item.validation_count !== null) ? String(item.validation_count).replace(/</g, '<').replace(/>/g, '>') : (parentItem && parentItem.validation_count !== undefined && parentItem.validation_count !== null) ? String(parentItem.validation_count).replace(/</g, '<').replace(/>/g, '>') : '-'}</span>
- </div>
- <div style="background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.06); padding: 4px 8px; border-radius: 6px; display:flex; gap: 6px;">
- <span style="color: var(--text-muted);">Published:</span>
- <span style="font-weight: 500; color: var(--text-main);">${formatDate(item.published_at || (parentItem && parentItem.published_at))}</span>
- </div>
- <div style="background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.06); padding: 4px 8px; border-radius: 6px; display:flex; gap: 6px;">
- <span style="color: var(--text-muted);">Last Verified:</span>
- <span style="font-weight: 500; color: var(--text-main);">${formatDate(item.last_verified_at || (parentItem && parentItem.last_verified_at))}</span>
- </div>
- <div style="background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.06); padding: 4px 8px; border-radius: 6px; display:flex; gap: 6px;">
- <span style="color: var(--text-muted);">Created:</span>
- <span style="font-weight: 500; color: var(--text-main);">${formatDate(item.created_at || (parentItem && parentItem.created_at))}</span>
- </div>
- <div style="background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.06); padding: 4px 8px; border-radius: 6px; display:flex; gap: 6px;">
- <span style="color: var(--text-muted);">Updated:</span>
- <span style="font-weight: 500; color: var(--text-main);">${formatDate(item.updated_at || (parentItem && parentItem.updated_at))}</span>
- </div>
- </div>
- </div>`;
- // 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 += `<div style="background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.06); padding: 4px 8px; border-radius: 6px; display:flex; gap: 6px;">
- <span style="color: var(--text-muted);">${k}:</span>
- <span style="font-weight: 500; color: var(--text-main);">${v}</span>
- </div>`;
- }
- });
- if (feedbackHtml !== '') {
- feedbackHtml = `<div style="display: flex; flex-wrap: wrap; gap: 8px; font-size: 0.85em;">${feedbackHtml}</div>`;
- }
- } else if (typeof feedbackVal === 'string' && feedbackVal.trim() !== '') {
- feedbackHtml = `<div style="color: var(--text-main); font-size: 0.95em; line-height: 1.5; white-space: pre-wrap; padding-top: 2px;">${String(feedbackVal).replace(/</g, '<').replace(/>/g, '>')}</div>`;
- }
- if (feedbackHtml !== '') {
- html += `<div class="structured-row">
- <div class="structured-label">feedback</div>
- <div class="structured-value" style="display: flex; align-items: center;">
- ${feedbackHtml}
- </div>
- </div>`;
- }
- }
- // Render body
- if (item.body && typeof item.body === 'string') {
- html += `<div class="structured-row">
- <div class="structured-label">body</div>
- <div class="structured-value">${item.body.replace(/</g, '<').replace(/>/g, '>')}</div>
- </div>`;
- }
- // 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() !== '';
- const escapeHtml = (s) => String(s).replace(/</g, '<').replace(/>/g, '>');
- return list.map(io => {
- if (!io) return '';
- if (typeof io === 'string') return `<div style="margin-bottom: 4px; padding: 2px; color: var(--text-main); font-weight: 500;">${escapeHtml(io)}</div>`;
- const desc = isValid(io.description) ? io.description.replace(/</g, '<').replace(/>/g, '>') : '';
- const mod = isValid(io.modality) ? io.modality : '';
- const relation = isValid(io.relation) ? io.relation.replace(/</g, '<').replace(/>/g, '>') : '';
- let modalityIcon = '';
- let modBorder = '#e2e8f0';
- let modColor = '#64748b';
- let modBg = '#ffffff';
-
- if (mod) {
- if (mod === '文本') { modalityIcon = 'T'; }
- else if (mod === '图片') { modalityIcon = '🖼️'; modColor = '#10b981'; }
- else if (mod === '视频') { modalityIcon = '▶️'; modColor = '#ef4444'; }
- else if (mod === '音频') { modalityIcon = '🎵'; modColor = '#ec4899'; }
- else if (mod === '参数') { modalityIcon = '⚙️'; modBg = '#f8fafc'; }
- else { modalityIcon = escapeHtml(mod[0] || '?'); }
- }
- const modalityHtml = mod ? `<span title="${escapeHtml(mod)}" style="display:inline-flex; align-items:center; justify-content:center; width:24px; height:24px; background:${modBg}; border:1px solid ${modBorder}; border-radius:4px; font-size:13px; font-weight:600; color:${modColor}; margin-right:6px; vertical-align:middle; margin-bottom:2px;">${modalityIcon}</span>` : '';
- let relationHtml = '';
- if (relation) {
- const relStr = String(relation);
- const relMatch = relStr.match(/\[(来源|去向)\.(.+)\]/);
- if (relMatch) {
- const dir = relMatch[1];
- const target = relMatch[2];
- let targetLabel = target;
- let targetStepId = '';
- let targetDirection = '';
- let badgeBg = '#eff6ff'; // blue-50
- let badgeColor = '#3b82f6'; // blue-500
- if (target === '原始输入') {
- targetLabel = '初始 ①';
- badgeBg = '#eef2ff'; // indigo-50
- badgeColor = '#6366f1'; // indigo-500
- }
- else if (target === '最终输出') {
- targetLabel = '最终输出';
- badgeBg = '#f0fdfa'; // teal-50
- badgeColor = '#14b8a6'; // teal-500
- }
- else if (target.startsWith('s') && target.length >= 3) {
- const stepNumMatch = target.match(/s(\d+)([IO]?)/);
- if (stepNumMatch) {
- targetLabel = `步骤${stepNumMatch[1]}`;
- targetStepId = `s${stepNumMatch[1]}`;
- targetDirection = stepNumMatch[2] === 'I' ? 'input' : (stepNumMatch[2] === 'O' ? 'output' : '');
- }
- }
-
- let clickAttr = targetStepId ? `onclick="event.stopPropagation(); window.jumpToWorkflowStep('${scopeId}', '${targetStepId}', '${targetDirection}')" onmouseenter="this.style.filter='brightness(0.95)'; window.hoverWorkflowStep('${scopeId}', '${targetStepId}', '${targetDirection}')" onmouseleave="this.style.filter='none'; window.unhoverWorkflowStep('${scopeId}', '${targetStepId}', '${targetDirection}')" style="cursor:pointer; ` : `onmouseenter="this.style.filter='brightness(0.95)'" onmouseleave="this.style.filter='none'" style="`;
- relationHtml = `<span title="${escapeHtml(relStr)}" ${clickAttr} display:inline-flex; align-items:center; justify-content:center; background:${badgeBg}; color:${badgeColor}; padding:3px 10px; border-radius:12px; font-size:12px; font-weight:600; margin-right:6px; vertical-align:middle; margin-bottom:2px; transition:all 0.2s;">${targetLabel}</span>`;
- } else {
- relationHtml = `<span style="color: #64748b; font-size: 0.85em; margin-right:6px; vertical-align:middle; margin-bottom:2px;">${escapeHtml(relation)}</span>`;
- }
- }
- let content = desc;
- if (!content) {
- const keys = Object.keys(io).filter(k => k !== 'modality' && k !== 'relation');
- if (keys.length > 0) {
- content = keys.map(k => `<span class="data-type-badge">${k}</span><span class="io-desc" style="margin-left:6px;">${io[k]}</span>`).join(' ');
- }
- }
- return `<div style="margin-bottom: 12px; line-height: 1.6; word-wrap: break-word;">
- ${modalityHtml}${relationHtml}<span style="color: var(--text-main); font-weight: 500; vertical-align:middle;">${content}</span>
- </div>`;
- }).join('');
- };
- // Render steps array specially
- if (item.steps && Array.isArray(item.steps)) {
- const allCapabilities = (item && item.capability) || (parentItem && parentItem.capability) || [];
- const escapeHtml = (s) => String(s).replace(/</g, '<').replace(/>/g, '>');
- const minWidth = 1250;
- const renderAction = (src) => {
- if (!src) return '-';
- if (src.action && src.action.description) {
- const description = escapeHtml(src.action.description);
- const reasoning = src.action.reasoning ? escapeHtml(src.action.reasoning) : '';
- return `<span class="action-description-tooltip">
- <span class="data-type-badge" style="background:#e0e7ff;color:#3730a3;font-weight:normal;margin-right:6px;margin-bottom:2px;display:inline-block;">${description}</span>
- ${reasoning ? `<span class="action-reasoning-popover">${reasoning}</span>` : ''}
- </span>`;
- }
- if (src.method) return escapeHtml(src.method);
- if (src.description) return escapeHtml(src.description);
- return '-';
- };
- const renderTools = (tools) => {
- if (!tools || !Array.isArray(tools) || tools.length === 0) return '-';
- return tools.map(t => `<span class="structured-badge tool-badge" style="display:inline-block; margin:2px;">${escapeHtml(t)}</span>`).join('');
- };
- 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"><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
- ? `<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" 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>`;
- };
- const getStepCapabilities = (step) => {
- if (!step || !step.step_id) return [];
- const workflowId = item && item.workflow_id;
- return allCapabilities.filter(capability => {
- const ref = capability.workflow_step_ref || {};
- const refStepId = ref.step_id;
- const refWorkflowId = ref.workflow_id;
- if (refStepId === step.step_id && (!workflowId || !refWorkflowId || refWorkflowId === workflowId)) {
- return true;
- }
- return capability.capability_id && new RegExp(`^c_${workflowId || 'w[0-9]+'}_${step.step_id}_[0-9]+$`).test(capability.capability_id);
- });
- };
- const matchedCapabilities = new Set();
- const collectBodyExcerpts = (applyTo, suggestApplyTo) => {
- const excerpts = [];
- const addExcerpt = (entry) => {
- if (entry && typeof entry === 'object' && typeof entry.body_excerpt === 'string' && entry.body_excerpt.trim()) {
- excerpts.push(entry.body_excerpt.trim());
- }
- };
- if (applyTo && typeof applyTo === 'object') {
- Object.values(applyTo).forEach(items => {
- if (Array.isArray(items)) items.forEach(addExcerpt);
- });
- }
- if (Array.isArray(suggestApplyTo)) {
- suggestApplyTo.forEach(addExcerpt);
- }
- return [...new Set(excerpts)].sort((a, b) => b.length - a.length);
- };
- const renderBodyWithExcerptHighlights = (body, applyTo, suggestApplyTo) => {
- if (!body) return '-';
- const text = String(body);
- const excerpts = collectBodyExcerpts(applyTo, suggestApplyTo);
- if (excerpts.length === 0) return escapeHtml(text);
- let out = '';
- let i = 0;
- while (i < text.length) {
- const match = excerpts.find(excerpt => text.startsWith(excerpt, i));
- if (match) {
- out += `<span class="body-excerpt-highlight" data-excerpt-key="${makeExcerptKey(match)}">${escapeHtml(match)}</span>`;
- i += match.length;
- } else {
- out += escapeHtml(text[i]);
- i += 1;
- }
- }
- return out;
- };
- const renderCapabilityColumns = (capability, step) => {
- const applyTo = capability && (capability.apply_to_draft || capability.apply_to_grounding || capability.apply_to);
- const suggestApplyTo = capability && capability.apply_to_draft ? null : capability && capability.suggest_apply_to;
- const bodyHtml = capability && capability.body ? renderBodyWithExcerptHighlights(capability.body, applyTo, suggestApplyTo) : '-';
- return `
- <td class="capability-cell" style="font-family: monospace;">
- <span class="row-expand-icon">▶</span>
- ${capability && capability.capability_id ? `<span style="display:inline-block; color:#94a3b8; font-size:0.85em; font-weight:400;">${escapeHtml(capability.capability_id)}</span>` : '-'}
- </td>
- <td class="capability-cell step-input-cell" data-step-id="${step ? escapeHtml(step.step_id) : ''}">${capability && capability.inputs && capability.inputs.length > 0 ? renderDataObjList(capability.inputs) : '-'}</td>
- <td class="capability-cell">${renderAction(capability)}</td>
- <td class="capability-cell step-output-cell" data-step-id="${step ? escapeHtml(step.step_id) : ''}">${capability && capability.outputs && capability.outputs.length > 0 ? renderDataObjList(capability.outputs) : '-'}</td>
- <td class="capability-cell" style="font-size:0.9em;"><div class="capability-clamp apply-to-clamp">${applyTo ? renderApplyToVal(applyTo, suggestApplyTo) : '-'}</div></td>
- <td class="capability-cell"><div class="capability-clamp capability-text">${bodyHtml}</div></td>
- <td class="capability-cell"><div class="capability-clamp">${capability ? renderEffects(capability.effects) : '-'}</div></td>
- <td class="capability-cell"><div class="capability-clamp">${capability ? renderTools(capability.tools) : '-'}</div></td>
- `;
- };
- html += `<div class="structured-row">
- <div class="structured-label">steps</div>
- <div class="structured-value" style="width: 100%; overflow-x: auto; padding-bottom: 8px;">
- <style>
- .decode-table tbody tr { transition: background 0.2s; }
- .decode-table td { border-bottom: 1px solid rgba(0,0,0,0.05); word-break: break-word; }
- td.hl-target {
- background-color: #fff1f2 !important;
- border: 2px solid #e11d48 !important;
- color: #e11d48 !important;
- z-index: 10;
- position: relative;
- }
- @keyframes cell-flash-red {
- 0%, 30% { background-color: #fff1f2; border: 2px solid #e11d48; color: #e11d48; box-shadow: 0 0 0 2px rgba(225, 29, 72, 0.2); }
- 100% { border-color: transparent; background-color: transparent; }
- }
- td.hl-flash {
- animation: cell-flash-red 1.6s ease-out;
- }
- .chip {
- display: inline-flex; align-items: center; gap: 4px;
- border-radius: 5px; font-size: 11.5px; font-weight: 500;
- text-decoration: none; vertical-align: middle; line-height: 1.4; padding: 2px 8px;
- }
- .chip-tag { font-weight: 600; }
- .chip-idx {
- display: inline-flex; align-items: center; justify-content: center;
- font-size: 13px; line-height: 1;
- }
- .ref-chip {
- background: var(--bg); color: var(--c); border: 1px solid var(--bd);
- cursor: pointer; transition: transform 0.1s, box-shadow 0.1s;
- }
- .ref-chip:hover {
- transform: translateY(-1px); box-shadow: 0 2px 6px rgba(0,0,0,0.10);
- }
- .ref-chip:hover .chip-tag { text-decoration: underline; }
- .init-chip {
- background: #f4f4f5; color: #52525b; border: 1px solid #d4d4d8;
- }
- .tag-list { display: flex; flex-wrap: wrap; gap: 4px; }
- .tag { display: inline-block; padding: 1px 7px; border-radius: 4px; font-size: 11px; font-weight: 500; line-height: 1.5; white-space: nowrap; }
- .tag-action { background: #e0e7ff; color: #3730a3; border: 1px solid #c7d2fe; }
- .tag-role { background: #ecfeff; color: #155e75; border: 1px solid #a5f3fc; }
- .tag-purpose { background: #f5f3ff; color: #6d28d9; border: 1px solid #ddd6fe; }
- .tool-name { font-size: 13px; font-weight: 600; color: #312e81; line-height: 1.35; }
- .tool-method { font-family: "SF Mono", "Menlo", "Consolas", monospace; font-size: 11px; color: #6366f1; line-height: 1.4; margin-top: 3px; padding-top: 3px; border-top: 1px dashed #ddd6fe; word-break: break-all; }
- .tool-legacy { font-family: "SF Mono", "Menlo", "Consolas", monospace; font-size: 12px; color: #3730a3; word-break: break-all; }
- .step-name-text { font-weight: 600; font-size: 13.5px; line-height: 1.4; }
- .category-pill { display: inline-block; padding: 2px 8px; border-radius: 4px; background: #f1f5f9; border: 1px solid #cbd5e1; color: #334155; font-size: 11px; font-weight: 500; line-height: 1.5; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
- .empty-cell { background: #fafafa !important; }
- .empty-val { color: #d4d4d8; font-style: italic; font-size: 12px; }
- </style>
- <div style="overflow-x: auto; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-top: 8px; max-width: 100%;">
- <table class="decode-table" style="width: 100%; min-width: 1400px; border-collapse: collapse; font-size: 0.85em; text-align: left; table-layout: fixed;">
- <thead>
- <tr style="background: #1f2937; color: white; text-align: center;">
- <th rowspan="2" style="padding: 12px 8px; border-right: 1px solid #374151; width: 3%;">Id</th>
- <th rowspan="2" style="padding: 12px 8px; border-right: 1px solid #374151; width: 10%;">步骤名称</th>
- <th rowspan="2" style="padding: 12px 8px; border-right: 1px solid #374151; width: 5%;">动作</th>
- <th rowspan="2" style="padding: 12px 8px; border-right: 1px solid #374151; width: 5%;">角色</th>
- <th rowspan="2" style="padding: 12px 8px; border-right: 1px solid #374151; width: 5%;">目的</th>
- <th colspan="4" style="padding: 8px; border-right: 1px solid #374151; background: #4f46e5;">步骤输入</th>
- <th rowspan="2" style="padding: 12px 8px; border-right: 1px solid #374151; width: 6%;">工具</th>
- <th colspan="4" style="padding: 8px; background: #d97706; border-right: 1px solid #374151;">步骤输出</th>
- <th rowspan="2" style="padding: 12px 8px; width: 11%;">来源</th>
- </tr>
- <tr style="color: white; text-align: center; font-size: 0.85rem;">
- <th style="padding: 6px; background: #6366f1; border-right: 1px solid #818cf8; border-top: 1px solid #818cf8; width: 3%;">模态</th>
- <th style="padding: 6px; background: #6366f1; border-right: 1px solid #818cf8; border-top: 1px solid #818cf8; width: 5%;">领域类型</th>
- <th style="padding: 6px; background: #6366f1; border-right: 1px solid #818cf8; border-top: 1px solid #818cf8; width: 5%;">来源</th>
- <th style="padding: 6px; background: #6366f1; border-right: 1px solid #374151; border-top: 1px solid #818cf8; width: 16%;">value</th>
- <th style="padding: 6px; background: #f59e0b; border-right: 1px solid #fbbf24; border-top: 1px solid #fbbf24; width: 2%;">#</th>
- <th style="padding: 6px; background: #f59e0b; border-right: 1px solid #fbbf24; border-top: 1px solid #fbbf24; width: 3%;">模态</th>
- <th style="padding: 6px; background: #f59e0b; border-right: 1px solid #fbbf24; border-top: 1px solid #fbbf24; width: 5%;">领域类型</th>
- <th style="padding: 6px; background: #f59e0b; border-right: 1px solid #374151; border-top: 1px solid #fbbf24; width: 16%;">value</th>
- </tr>
- </thead>
- <tbody>`;
- const CIRCLE_NUMS = ['①','②','③','④','⑤','⑥','⑦','⑧','⑨','⑩'];
- const ROW_COLORS = ['#4f46e5', '#0d9488', '#e11d48', '#7c3aed', '#0284c7', '#d97706', '#65a30d', '#db2777'];
- const stepSourceMap = {};
- let initCount = 0;
-
- // First pass: collect outputs and init inputs
- item.steps.forEach((step, sIdx) => {
- if (step.outputs) {
- step.outputs.forEach((out, outIdx) => {
- if (out.id) {
- stepSourceMap[out.id] = { step_id: step.step_id, order: sIdx, index: outIdx, type: out.type, color: ROW_COLORS[sIdx % ROW_COLORS.length], hasMultiple: step.outputs.length > 1 };
- }
- });
- }
- if (step.inputs) {
- step.inputs.forEach(inp => {
- if (inp.source_id && inp.source_id.startsWith('init_input')) {
- if (!stepSourceMap[inp.source_id]) {
- stepSourceMap[inp.source_id] = { isInit: true, order: initCount++ };
- }
- }
- });
- }
- });
- const getModalityHtml = (mod) => {
- let modalityIcon = escapeHtml(mod ? mod[0] : '?');
- let modBorder = '#e2e8f0';
- let modColor = '#64748b';
- let modBg = '#ffffff';
- if (mod === '文本') { modalityIcon = 'T'; modColor = '#3b82f6'; modBg = '#eff6ff'; modBorder = '#bfdbfe'; }
- else if (mod === '图片') { modalityIcon = '🖼️'; modColor = '#10b981'; modBg = '#ecfdf5'; modBorder = '#a7f3d0'; }
- else if (mod === '视频' || mod === '视频片断') { modalityIcon = '▶️'; modColor = '#ef4444'; modBg = '#fef2f2'; modBorder = '#fecaca'; }
- else if (mod === '音频') { modalityIcon = '🎵'; modColor = '#ec4899'; modBg = '#fdf2f8'; modBorder = '#fbcfe8'; }
- else if (mod === '图片内容组') { modalityIcon = '🖼️'; modColor = '#10b981'; modBg = '#ecfdf5'; modBorder = '#a7f3d0'; }
- return `<span title="${escapeHtml(mod || '')}" style="display:inline-flex; align-items:center; justify-content:center; width:28px; height:28px; background:${modBg}; border:1px solid ${modBorder}; border-radius:4px; font-size:14px; font-weight:600; color:${modColor};">${modalityIcon}</span>`;
- };
- item.steps.forEach((step, stepIdx) => {
- const sColor = ROW_COLORS[stepIdx % ROW_COLORS.length];
- const inputs = step.inputs || [];
- const outputs = step.outputs || [];
- const maxRows = Math.max(inputs.length, outputs.length, 1);
- const hoverAttr = `onmouseenter="this.style.background='rgba(0,0,0,0.03)';" onmouseleave="this.style.background='white';"`;
- for (let i = 0; i < maxRows; i++) {
- const rowBorderBottom = (i === maxRows - 1) ? `2px solid ${sColor}` : `1px dashed #e5e7eb`;
- html += `<tr ${hoverAttr} style="background: white; border-bottom: ${rowBorderBottom};">`;
-
- if (i === 0) {
- const renderTagList = (items, cssClass) => {
- if (!items || !Array.isArray(items) || items.length === 0) return '<span class="empty-val">—</span>';
- const tags = items.map(t => `<span class="tag ${cssClass}">${escapeHtml(t)}</span>`).join('');
- return `<div class="tag-list">${tags}</div>`;
- };
- const rolesHtml = step.roles ? renderTagList(step.roles, 'tag-role') : (step.stage ? `<div class="tag-list"><span class="tag tag-role">${escapeHtml(step.stage)}</span></div>` : '<span class="empty-val">—</span>');
- const actionsHtml = step.actions ? renderTagList(step.actions, 'tag-action') : '<span class="empty-val">—</span>';
- const purposesHtml = step.purposes ? renderTagList(step.purposes, 'tag-purpose') : '<span class="empty-val">—</span>';
-
- const stepIdAttr = `id="step-s${escapeHtml(step.step_id || '')}"`;
- html += `
- <td ${stepIdAttr} rowspan="${maxRows}" style="background: ${sColor}; color: white; text-align: center; font-weight: bold; font-size: 1.1rem; border-right: 1px solid #e5e7eb; vertical-align: middle;">${escapeHtml(step.step_id || (stepIdx + 1))}</td>
- <td rowspan="${maxRows}" style="padding: 10px; border-right: 1px solid #e5e7eb; vertical-align: top;"><div class="step-name-text">${escapeHtml(step.name || '')}</div></td>
- <td rowspan="${maxRows}" style="padding: 10px; border-right: 1px solid #e5e7eb; vertical-align: top;">${actionsHtml}</td>
- <td rowspan="${maxRows}" style="padding: 10px; border-right: 1px solid #e5e7eb; vertical-align: top;">${rolesHtml}</td>
- <td rowspan="${maxRows}" style="padding: 10px; border-right: 1px solid #e5e7eb; vertical-align: top;">${purposesHtml}</td>
- `;
- }
-
- const inp = inputs[i];
- if (inp) {
- // resolve modality/category based on source
- const sInfo = stepSourceMap[inp.source_id];
- let modalityHtml = getModalityHtml(inp.modality || inp.type);
- let categoryHtml = inp.category ? `<span class="category-pill">${escapeHtml(inp.category)}</span>` : '<span class="empty-val">—</span>';
- let sourceHtml = escapeHtml(inp.source_id || '-');
-
- if (sInfo) {
- if (sInfo.isInit) {
- sourceHtml = `<span class="chip init-chip" title="初始输入 #${sInfo.order}"><span class="chip-tag">初始</span><span class="chip-idx">${CIRCLE_NUMS[sInfo.order % 10]}</span></span>`;
- } else {
- const bdColor = sInfo.color + '40';
- const clickAttr = `onclick="event.stopPropagation(); window.jumpToWorkflowStep('${scopeId}', 's${sInfo.step_id}', ${sInfo.index})" onmouseenter="window.hoverWorkflowStep('${scopeId}', 's${sInfo.step_id}', ${sInfo.index})" onmouseleave="window.unhoverWorkflowStep('${scopeId}', 's${sInfo.step_id}', ${sInfo.index})"`;
- const circleHtml = sInfo.hasMultiple ? `<span class="chip-idx">${CIRCLE_NUMS[sInfo.index % 10]}</span>` : '';
- sourceHtml = `<a class="chip ref-chip" ${clickAttr} title="Jump to Step ${sInfo.step_id}" style="--c:${sInfo.color}; --bg:${sInfo.color}10; --bd:${bdColor};"><span class="chip-tag">步骤${sInfo.step_id}</span>${circleHtml}</a>`;
-
- // Source dictates modality & category overrides if available
- const sourceOutputObj = item.steps[sInfo.order] && item.steps[sInfo.order].outputs && item.steps[sInfo.order].outputs[sInfo.index];
- if (sourceOutputObj) {
- modalityHtml = getModalityHtml(sourceOutputObj.modality || sourceOutputObj.type || inp.modality || inp.type);
- if (sourceOutputObj.category) {
- categoryHtml = `<span class="category-pill">${escapeHtml(sourceOutputObj.category)}</span>`;
- }
- }
- }
- }
-
- html += `
- <td style="padding: 6px; text-align: center; border-right: 1px solid #e5e7eb; vertical-align: middle; background: #fafbfd;">${modalityHtml}</td>
- <td style="padding: 6px; text-align: center; border-right: 1px solid #e5e7eb; vertical-align: middle; background: #fafbfd;">${categoryHtml}</td>
- <td style="padding: 6px; text-align: center; border-right: 1px solid #e5e7eb; vertical-align: middle; background: #fafbfd;">${sourceHtml}</td>
- <td style="padding: 8px; border-right: 1px solid #e5e7eb; font-size: 0.85rem; color: #374151; vertical-align: top; line-height: 1.5; background: #fafbfd;">${inp.value ? escapeHtml(inp.value) : '<span class="empty-val">—</span>'}</td>
- `;
- } else {
- html += `<td class="empty-cell" style="border-right: 1px solid #e5e7eb;"></td><td class="empty-cell" style="border-right: 1px solid #e5e7eb;"></td><td class="empty-cell" style="border-right: 1px solid #e5e7eb;"></td><td class="empty-cell" style="border-right: 1px solid #e5e7eb;"></td>`;
- }
-
- if (i === 0) {
- let toolHtml = '<span class="empty-val">—</span>';
- if (step.tool_name || step.tool_method) {
- let parts = [];
- if (step.tool_name) parts.push(`<div class="tool-name">${escapeHtml(step.tool_name)}</div>`);
- if (step.tool_method) parts.push(`<div class="tool-method">${escapeHtml(step.tool_method)}</div>`);
- toolHtml = parts.join('');
- } else if (step.tool) {
- toolHtml = `<div class="tool-legacy">${escapeHtml(step.tool)}</div>`;
- }
- html += `<td rowspan="${maxRows}" style="padding: 10px; border-right: 1px solid #e5e7eb; vertical-align: middle; text-align: center; background: #f5f5fa;">${toolHtml}</td>`;
- }
-
- const out = outputs[i];
- if (out) {
- const typeHtml = getModalityHtml(out.modality || out.type);
- let categoryHtml = out.category ? `<span class="category-pill">${escapeHtml(out.category)}</span>` : '<span class="empty-val">—</span>';
- const cNum = CIRCLE_NUMS[i % 10];
- html += `
- <td id="output-num-${scopeId}-s${escapeHtml(step.step_id || '')}-${i}" style="padding: 6px; text-align: center; border-right: 1px solid #e5e7eb; color: ${sColor}; font-weight: bold; font-size: 1.1rem; vertical-align: middle; background: #fefaf5; transition: all 0.2s;">${cNum}</td>
- <td style="padding: 6px; text-align: center; border-right: 1px solid #e5e7eb; vertical-align: middle; background: #fefaf5;">${typeHtml}</td>
- <td style="padding: 6px; text-align: center; border-right: 1px solid #e5e7eb; vertical-align: middle; background: #fefaf5;">${categoryHtml}</td>
- <td style="padding: 8px; font-size: 0.85rem; color: #374151; vertical-align: top; line-height: 1.5; background: #fefaf5; border-right: 1px solid #e5e7eb;">${out.value ? escapeHtml(out.value) : '<span class="empty-val">—</span>'}</td>
- `;
- } else {
- html += `<td class="empty-cell" style="border-right: 1px solid #e5e7eb;"></td><td class="empty-cell" style="border-right: 1px solid #e5e7eb;"></td><td class="empty-cell" style="border-right: 1px solid #e5e7eb;"></td><td class="empty-cell" style="border-right: 1px solid #e5e7eb;"></td>`;
- }
- if (i === 0) {
- html += `<td rowspan="${maxRows}" style="padding: 10px; color: #4b5563; font-size: 0.85rem; vertical-align: top; background: #fafafa;">${escapeHtml(step.source_ref || '')}</td>`;
- }
-
- html += `</tr>`;
- }
- });
- html += `</tbody></table></div></div></div>`;
- }
- // Render inputs
- if (item.inputs && Array.isArray(item.inputs) && item.inputs.length > 0) {
- html += `<div class="structured-row">
- <div class="structured-label">inputs</div>
- <div class="structured-value" style="display:flex; flex-direction:column; gap:6px;">
- ${renderDataObjList(item.inputs)}
- </div>
- </div>`;
- } else if (item.inputs && typeof item.inputs === 'object' && Object.keys(item.inputs).length > 0 && !Array.isArray(item.inputs)) {
- // Fallback for old schema
- html += `<div class="structured-row"><div class="structured-label">inputs</div><div class="structured-value" style="display:flex; flex-direction:column; gap:6px;">`;
- Object.entries(item.inputs).forEach(([k, v]) => {
- html += `<div class="io-item"><span class="data-type-badge">${k}</span><span class="io-desc">${v}</span></div>`;
- });
- html += `</div></div>`;
- }
- // Render outputs
- if (item.outputs && Array.isArray(item.outputs) && item.outputs.length > 0) {
- html += `<div class="structured-row">
- <div class="structured-label">outputs</div>
- <div class="structured-value" style="display:flex; flex-direction:column; gap:6px;">
- ${renderDataObjList(item.outputs)}
- </div>
- </div>`;
- } else if (item.outputs && typeof item.outputs === 'object' && Object.keys(item.outputs).length > 0 && !Array.isArray(item.outputs)) {
- // Fallback for old schema
- html += `<div class="structured-row"><div class="structured-label">outputs</div><div class="structured-value" style="display:flex; flex-direction:column; gap:6px;">`;
- Object.entries(item.outputs).forEach(([k, v]) => {
- html += `<div class="io-item"><span class="data-type-badge">${k}</span><span class="io-desc">${v}</span></div>`;
- });
- html += `</div></div>`;
- }
- // Render tools (for non-step items like capabilities)
- if (item.tools && Array.isArray(item.tools) && item.tools.length > 0) {
- html += `<div class="structured-row">
- <div class="structured-label">tools</div>
- <div class="structured-value">
- ${item.tools.map(t => `<span class="structured-badge tool-badge">${t}</span>`).join('')}
- </div>
- </div>`;
- }
- // Dynamic fallback for any other unhandled keys
- const handledKeys = [
- 'method', 'name', 'action', 'unstructured_what', 'apply_to_grounding', 'apply_to_draft', 'apply_to',
- 'suggest_apply_to', 'stage', 'effects', 'body', 'steps', 'inputs', 'outputs', 'tools',
- 'workflow_id', 'capability', 'source', 'channel_content_id'
- ];
- 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 = `<pre style="margin:0; background:rgba(0,0,0,0.03); padding:8px; border-radius:4px; font-size:0.85em; overflow-x:auto;">${JSON.stringify(v, null, 2).replace(/</g, '<').replace(/>/g, '>')}</pre>`;
- } else {
- displayHtml = v.map(vi => `<span class="structured-badge">${String(vi).replace(/</g, '<').replace(/>/g, '>')}</span>`).join('');
- }
- } else {
- if (Object.keys(v).length === 0) return;
- displayHtml = `<pre style="margin:0; background:rgba(0,0,0,0.03); padding:8px; border-radius:4px; font-size:0.85em; overflow-x:auto;">${JSON.stringify(v, null, 2).replace(/</g, '<').replace(/>/g, '>')}</pre>`;
- }
- } else {
- displayHtml = String(v).replace(/</g, '<').replace(/>/g, '>');
- }
- html += `<div class="structured-row">
- <div class="structured-label">${k}</div>
- <div class="structured-value">${displayHtml}</div>
- </div>`;
- }
- });
- html += `</div>`;
- });
- return html;
- };
|