| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093 |
- let requirements = [];
- let currentSelectedIndex = null;
- let activeRuns = {};
- let statusInterval = null;
- let currentAvailablePlatforms = ['xhs', 'youtube', 'bili', 'x'];
- let currentPromptName = null;
- const modalPrompts = document.getElementById('prompts-modal');
- const elPromptList = document.getElementById('prompt-list');
- const elPromptTextarea = document.getElementById('prompt-textarea');
- const elSchemaTextarea = document.getElementById('schema-textarea');
- const elPromptStatus = document.getElementById('prompt-save-status');
- // DOM Elements
- const elReqSelector = document.getElementById('req-selector');
- const elStatsContainer = document.getElementById('stats-container');
- const elMainContent = document.getElementById('main-content');
- const elEmptyState = document.getElementById('empty-state');
- const elDetailView = document.getElementById('detail-view');
- const elStatusBanner = document.getElementById('status-banner');
- const elStatusText = document.getElementById('status-text');
- // Form logic
- const selectForcePhase = document.getElementById('select-force-phase');
- const groupPlatforms = document.getElementById('group-platforms');
- function updateRunModalVisibility() {
- const val = document.getElementById('select-force-phase').value;
- const groupPlatforms = document.getElementById('group-platforms');
- const groupModel = document.getElementById('group-model');
-
- let showPlatforms = false;
- let showModel = false;
- if (val === 'custom_range') {
- if (chainStartNode) {
- const startIndex = PIPELINE_STEPS.findIndex(s => s.id === chainStartNode);
- const endIndex = chainEndNode ? PIPELINE_STEPS.findIndex(s => s.id === chainEndNode) : startIndex;
- const finalStart = Math.min(startIndex, endIndex);
- const finalEnd = Math.max(startIndex, endIndex);
- if (finalStart === 0) { // 'research' is index 0
- showPlatforms = true;
- }
- // 'apply-grounding' is index 4 in PIPELINE_STEPS
- const applyIndex = PIPELINE_STEPS.findIndex(s => s.id === 'apply-grounding');
- if (applyIndex >= finalStart && applyIndex <= finalEnd) {
- showModel = true;
- }
- }
- } else if (val.startsWith('step_')) {
- if (val === 'step_1.1') showPlatforms = true;
- if (val === 'step_2.2') showModel = true;
- } else if (val === 'phase1') {
- showPlatforms = true;
- } else if (val === 'smart') {
- showPlatforms = true;
- // Depending on smart mode logic, we might not always need to show it, but default is fine
- }
- if (groupPlatforms) groupPlatforms.style.display = showPlatforms ? 'block' : 'none';
- if (groupModel) groupModel.style.display = showModel ? 'block' : 'none';
- }
- if (selectForcePhase) {
- selectForcePhase.addEventListener('change', updateRunModalVisibility);
- }
- const jsonStrategy = document.getElementById('json-strategy');
- const jsonBlueprint = document.getElementById('json-blueprint');
- const jsonCapability = document.getElementById('json-capability');
- const jsonSource = document.getElementById('json-source');
- const jsonRaw = document.getElementById('json-raw');
- const modalRun = document.getElementById('run-modal');
- const modalLogs = document.getElementById('logs-modal');
- const terminalLogs = document.getElementById('terminal-logs');
- const modalFragDetail = document.getElementById('frag-detail-modal');
- const btnCloseFragDetail = document.getElementById('btn-close-frag-detail');
- if (btnCloseFragDetail) btnCloseFragDetail.onclick = () => modalFragDetail.classList.add('hidden');
- window.allFragmentsMap = {};
- const PIPELINE_STEPS = [
- { id: 'research', label: '1.1 分布式爬取' },
- { id: 'source', label: '1.5 提取数据源' },
- { id: 'generate-case', label: '1.6 生成 case.json' },
- { id: 'decode-workflow', label: '2.1 解析工序' },
- { id: 'apply-grounding', label: '2.2 场景映射' },
- { id: 'process-cluster', label: '2.1.1 工序聚类' },
- { id: 'process-score', label: '2.1.2 工序打分' },
- { id: 'capability-extract', label: '2.2.1 能力提取' },
- { id: 'capability-enrich', label: '2.2.2 能力丰富化' },
- { id: 'strategy', label: '3.0 策略组装' }
- ];
- let chainStartNode = null;
- let chainEndNode = null;
- let currentPipelineStatus = {};
- // Initialize
- async function init() {
- await fetchRequirements();
- setupEventListeners();
- setupFloatingApplyToTooltips();
- startStatusPolling();
- }
- function setupFloatingApplyToTooltips() {
- if (window.__applyToTooltipReady) return;
- window.__applyToTooltipReady = true;
- let floatingTooltip = null;
- let activeTarget = null;
- const hideTooltip = () => {
- activeTarget = null;
- if (floatingTooltip) {
- floatingTooltip.remove();
- floatingTooltip = null;
- }
- };
- const positionTooltip = () => {
- if (!floatingTooltip || !activeTarget) return;
- const rect = activeTarget.getBoundingClientRect();
- const tipRect = floatingTooltip.getBoundingClientRect();
- const gap = 8;
- const margin = 12;
- let left = rect.left + rect.width / 2 - tipRect.width / 2;
- left = Math.max(margin, Math.min(left, window.innerWidth - tipRect.width - margin));
- let top = rect.bottom + gap;
- if (top + tipRect.height > window.innerHeight - margin) {
- top = rect.top - tipRect.height - gap;
- }
- top = Math.max(margin, top);
- floatingTooltip.style.left = `${left}px`;
- floatingTooltip.style.top = `${top}px`;
- floatingTooltip.style.visibility = 'visible';
- floatingTooltip.style.opacity = '1';
- };
- document.addEventListener('pointerover', (event) => {
- const target = event.target.closest('.apply-to-path-item.has-tooltip');
- if (!target) return;
- const sourceTooltip = target.querySelector('.apply-to-tooltip');
- if (!sourceTooltip) return;
- activeTarget = target;
- if (!floatingTooltip) {
- floatingTooltip = document.createElement('div');
- floatingTooltip.className = 'floating-apply-to-tooltip';
- document.body.appendChild(floatingTooltip);
- }
- floatingTooltip.innerHTML = sourceTooltip.innerHTML;
- floatingTooltip.style.visibility = 'hidden';
- floatingTooltip.style.opacity = '0';
- requestAnimationFrame(positionTooltip);
- });
- document.addEventListener('pointerout', (event) => {
- const target = event.target.closest('.apply-to-path-item.has-tooltip');
- if (!target || target.contains(event.relatedTarget)) return;
- hideTooltip();
- });
- document.addEventListener('pointerover', (event) => {
- const target = event.target.closest('.apply-to-evidence-note[data-excerpt-key]');
- if (!target) return;
- const row = target.closest('tr');
- if (!row) return;
- row.querySelectorAll(`.body-excerpt-highlight[data-excerpt-key="${target.dataset.excerptKey}"]`).forEach(el => {
- el.classList.add('active');
- });
- });
- document.addEventListener('pointerout', (event) => {
- const target = event.target.closest('.apply-to-evidence-note[data-excerpt-key]');
- if (!target || target.contains(event.relatedTarget)) return;
- const row = target.closest('tr');
- if (!row) return;
- row.querySelectorAll(`.body-excerpt-highlight[data-excerpt-key="${target.dataset.excerptKey}"]`).forEach(el => {
- el.classList.remove('active');
- });
- });
- window.addEventListener('scroll', hideTooltip, true);
- window.addEventListener('resize', hideTooltip);
- }
- function makeExcerptKey(text) {
- let hash = 0;
- const str = String(text || '');
- for (let i = 0; i < str.length; i += 1) {
- hash = ((hash << 5) - hash) + str.charCodeAt(i);
- hash |= 0;
- }
- return `e${Math.abs(hash)}`;
- }
- function getWorkflowGroups(item) {
- if (!item || !item.decode_workflow) return [];
- // decode_workflow is an object (not an array), so we wrap it in an array to maintain compatibility
- return [item.decode_workflow];
- }
- function getWorkflowItems(item) {
- return getWorkflowGroups(item)
- .filter(group => group.steps || group.workflow)
- .map(group => {
- const wf = group.workflow || group;
- return {
- ...wf,
- workflow_id: wf.workflow_id || group.workflow_id,
- capability: Array.isArray(group.capability) ? group.capability : []
- };
- });
- }
- function getCapabilityItems(item) {
- return getWorkflowGroups(item).flatMap(group => Array.isArray(group.capability) ? group.capability : []);
- }
- // Fetch Data
- async function fetchRequirements() {
- try {
- const res = await fetch('/api/requirements');
- requirements = await res.json();
- requirements.sort((a, b) => b.index - a.index);
- renderTaskList(requirements);
- updateStats();
- } catch (e) {
- console.error("Failed to fetch requirements", e);
- elReqSelector.innerHTML = '<option value="">加载数据失败。请检查后端是否运行。</option>';
- }
- }
- window.selectSubTab = function (p) {
- document.querySelectorAll('.sub-tab-btn').forEach(b => {
- b.classList.remove('active');
- if (b.textContent === p.replace('case_', '').toUpperCase()) b.classList.add('active');
- });
- document.querySelectorAll('.sub-tab-pane').forEach(pane => pane.classList.add('hidden'));
- const target = document.getElementById('sub-tab-' + p);
- if (target) target.classList.remove('hidden');
- };
- window.hoverWorkflowStep = function(scopeId, stepId, outputIdx) {
- const el = document.getElementById(`output-num-${scopeId}-${stepId}-${outputIdx}`);
- if (el) {
- el.classList.add('hl-target');
- }
- };
- window.unhoverWorkflowStep = function(scopeId, stepId, outputIdx) {
- const el = document.getElementById(`output-num-${scopeId}-${stepId}-${outputIdx}`);
- if (el) {
- el.classList.remove('hl-target');
- }
- };
- window.jumpToWorkflowStep = function(scopeId, stepId, outputIdx) {
- const el = document.getElementById(`output-num-${scopeId}-${stepId}-${outputIdx}`);
- if (el) {
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
- el.classList.remove('hl-flash');
- void el.offsetWidth; // trigger reflow
- el.classList.add('hl-flash');
- }
- };
- window.jumpToCase = function (caseId) {
- // Switch to raw tab
- document.querySelector('.tab-btn-pill[data-target="tab-raw"]').click();
- // Find the case card
- const targetCard = document.getElementById('case-card-' + caseId);
- if (targetCard) {
- // Find which sub-tab pane it's inside
- const pane = targetCard.closest('.sub-tab-pane');
- if (pane) {
- const platformId = pane.id.replace('sub-tab-', '');
- selectSubTab(platformId);
- }
- // Scroll into view
- targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
- // Add highlight
- targetCard.classList.remove('highlight-pulse');
- void targetCard.offsetWidth; // Trigger reflow
- targetCard.classList.add('highlight-pulse');
- } else {
- alert("Case not found in raw cases data.");
- }
- };
- async function fetchPromptsList() {
- try {
- const res = await fetch('/api/prompts');
- let list = await res.json();
- // Handle DAG nodes
- document.querySelectorAll('.prompt-node').forEach(node => {
- const p = node.dataset.prompt;
- if (list.includes(p)) {
- node.onclick = () => selectPrompt(p, node);
- // Remove from list so it doesn't appear in "other"
- list = list.filter(item => item !== p);
- } else {
- node.style.opacity = 0.5; // gray out if not found
- node.style.cursor = 'not-allowed';
- }
- });
- const elOtherPromptsList = document.getElementById('other-prompts-list');
- if (elOtherPromptsList) {
- elOtherPromptsList.innerHTML = '';
- list.forEach((p) => {
- const div = document.createElement('div');
- div.className = 'prompt-tab';
- div.textContent = p;
- div.onclick = () => selectPrompt(p, div);
- elOtherPromptsList.appendChild(div);
- });
- }
- // Select the first prompt by default (maybe researcher.prompt)
- const firstNode = document.querySelector('.prompt-node[data-prompt="researcher.prompt"]');
- if (firstNode) {
- selectPrompt('researcher.prompt', firstNode);
- }
- } catch (e) {
- console.error("Failed to load prompts", e);
- }
- }
- async function selectPrompt(name, tabEl) {
- currentPromptName = name;
- document.querySelectorAll('.prompt-tab').forEach(el => el.classList.remove('active'));
- document.querySelectorAll('.prompt-node').forEach(el => el.classList.remove('active'));
- if (tabEl) tabEl.classList.add('active');
- elPromptTextarea.value = 'Loading...';
- elPromptTextarea.disabled = true;
- if (elSchemaTextarea) {
- elSchemaTextarea.value = 'Loading...';
- elSchemaTextarea.disabled = true;
- }
- try {
- const res = await fetch(`/api/prompts/${name}`);
- const data = await res.json();
- elPromptTextarea.value = data.content || '';
- if (elSchemaTextarea) {
- elSchemaTextarea.value = data.schema_content || '';
- }
- } catch (e) {
- elPromptTextarea.value = 'Error loading prompt.';
- if (elSchemaTextarea) elSchemaTextarea.value = '';
- }
- elPromptTextarea.disabled = false;
- if (elSchemaTextarea) elSchemaTextarea.disabled = false;
- elPromptStatus.textContent = '';
- }
- function renderAggregatedPerCaseData(cases, type) {
- if (!cases || !Array.isArray(cases) || cases.length === 0) {
- return '<div style="color:var(--text-muted); padding: 1rem; text-align: center;">暂无案例数据</div>';
- }
- let sidebarHtml = `<div class="case-sidebar" style="width: 56px; flex-shrink: 0; position: sticky; top: 0; align-self: flex-start; height: calc(100vh - 100px); overflow-y: auto; background: #fff;">`;
- sidebarHtml += `<div class="sidebar-nav-list" style="padding: 6px;">`;
- let contentHtml = `<div class="case-content-area" style="flex: 1; min-width: 0; padding-left: 1.25rem; padding-right: 2rem; border-left: 1px solid #f1f3f4; margin-left: -1px;">`;
- let hasData = false;
- let displayIndex = 1;
- // Sort cases by score
- const sortedCases = [...cases].sort((a, b) => {
- const aId = a.case_id || (a._raw && a._raw.case_id);
- const bId = b.case_id || (b._raw && b._raw.case_id);
- const sourceMap = window._currentRawCasesContext ? window._currentRawCasesContext.sourceMap || {} : {};
- const aMapped = sourceMap[aId] || {};
- const bMapped = sourceMap[bId] || {};
- const aScore = aMapped.evaluation && aMapped.evaluation.quality ? (aMapped.evaluation.quality.overall_score || 0) : 0;
- const bScore = bMapped.evaluation && bMapped.evaluation.quality ? (bMapped.evaluation.quality.overall_score || 0) : 0;
- return bScore - aScore;
- });
- sortedCases.forEach((c, idx) => {
- const cId = c.case_id || (c._raw && c._raw.case_id) || `temp_${idx}`;
- const sourceMap = window._currentRawCasesContext ? window._currentRawCasesContext.sourceMap || {} : {};
- const mappedS = sourceMap[cId] || {};
- const postObj = mappedS.post || c.post || c || {};
- const title = postObj.title || c.title || postObj.desc || (postObj.body_text ? postObj.body_text.substring(0, 30) + '...' : '') || cId || `案例 ${idx + 1}`;
- hasData = true; // Always render if there is a case, so the user can click Rerun.
- let items = null;
- if (type === 'workflow') {
- const workflowItems = getWorkflowItems(c);
- items = workflowItems.length > 0 ? workflowItems : null;
- } else if (type === 'capabilities') {
- const capabilityItems = getCapabilityItems(c);
- items = capabilityItems.length > 0 ? capabilityItems : null;
- }
- const targetId = `case-${type}-${idx}`;
- const navIndex = displayIndex++;
- let publishStrHtml = '';
- if (c.published_at) {
- const dateObj = new Date(c.published_at);
- const dateStr = isNaN(dateObj.getTime()) ? c.published_at : dateObj.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
- publishStrHtml = `<span style="font-size: 0.75em; font-weight: normal; color: #94a3b8; background: #f1f5f9; border: 1px solid #e2e8f0; padding: 2px 8px; border-radius: 12px; margin-left: 8px;">发布于: ${dateStr}</span>`;
- }
- const score = mappedS.evaluation && mappedS.evaluation.quality ? mappedS.evaluation.quality.overall_score : null;
- let scoreBadge = '';
- if (score !== null && score !== undefined) {
- let color = score >= 80 ? '#10b981' : (score >= 60 ? '#f59e0b' : '#ef4444');
- scoreBadge = `<span style="font-size: 0.85em; font-weight: bold; color: #fff; background: ${color}; padding: 2px 8px; border-radius: 12px; margin-left: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">⭐️ ${score}</span>`;
- }
- const typeLabel = type === 'workflow' ? '工序' : '能力';
- // Add to sidebar
- sidebarHtml += `<div class="sidebar-nav-item" title="${title.replace(/"/g, '"')}" style="min-height: 34px; padding: 0; margin-bottom: 6px; display:flex; align-items:center; justify-content:center; font-family: monospace; font-weight: 700; color:#64748b;" onclick="
- document.querySelectorAll('.sidebar-nav-item').forEach(el => el.classList.remove('active'));
- this.classList.add('active');
- document.getElementById('${targetId}').scrollIntoView({behavior: 'smooth'})
- ">
- ${navIndex}
- </div>`;
- contentHtml += `<div id="${targetId}" class="case-section" style="margin-bottom: 3.5rem; padding-top: 1rem;">`;
- const caseIndexToPass = c.index || (idx + 1);
- const btnHtml = `<div style="display: flex; gap: 8px;">
- <button class="btn btn-secondary" style="font-size: 0.85em; padding: 0.4rem 0.8rem; border-radius: 4px;" onclick="event.stopPropagation(); triggerSingleCaseRerun('decode-workflow', ${caseIndexToPass})">🔄 重跑工序解析</button>
- </div>`;
- contentHtml += `<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; margin-bottom: 1.2rem;" onclick="const content = this.nextElementSibling; content.classList.toggle('hidden'); this.querySelector('.case-arrow').textContent = content.classList.contains('hidden') ? '▶' : '▼';">`;
- contentHtml += `<h3 style="margin: 0; padding: 1rem; color: var(--text-main); font-size: 1.2rem; display: flex; align-items: center; gap: 10px; user-select: none; flex: 1;">`;
- contentHtml += `<span class="case-arrow" style="font-size: 0.8em; color: var(--text-muted); width: 16px; display: inline-block;">▶</span>`;
- contentHtml += `<span style="color: #64748b; font-size: 1.1rem; font-weight: bold; font-family: monospace;">#${idx + 1}</span>`;
- contentHtml += `<span>${title.replace(/</g, '<').replace(/>/g, '>')}</span>${scoreBadge}${publishStrHtml}</h3>`;
- contentHtml += btnHtml;
- contentHtml += `</div>`;
- // Collapsible Post Info
- const post = mappedS.post || c.post || c || {};
- const images = post.images || [];
- const xImages = post.image_url_list || [];
- const ytThumbnails = post.thumbnails && post.thumbnails.length > 0 ? [post.thumbnails[post.thumbnails.length - 1].url] : [];
- const allImages = [...images, ...xImages.map(img => img.image_url), ...ytThumbnails].filter(Boolean);
- const bodyText = post.body_text || post.body || post.desc || '';
- let mediaHtml = '';
- if (allImages.length > 0) {
- mediaHtml = `<div style="display:flex; gap:8px; overflow-x:auto; margin-bottom: 1rem; padding-bottom: 8px;">`;
- allImages.forEach((img, i) => {
- const coverImgUrl = `/output/${window._currentRawCasesContext.reqId}/raw_cases/images/${cId}/${i.toString().padStart(2, '0')}.jpg`;
- mediaHtml += `<img src="${coverImgUrl}" onerror="this.onerror=null; this.src='${img}';" style="height: 150px; border-radius: 6px; object-fit: cover; flex-shrink: 0;">`;
- });
- mediaHtml += `</div>`;
- }
- contentHtml += `<div class="case-post-info hidden" style="padding: 1.5rem; background: #f8fafc; border-radius: 8px; margin-bottom: 1.5rem; border: 1px solid #e2e8f0;">
- ${mediaHtml}
- <div style="font-size: 0.95em; color: #334155; white-space: pre-wrap; line-height: 1.6; word-break: break-all;">${bodyText.replace(/</g, '<').replace(/>/g, '>')}</div>
- </div>`;
- // Always Expanded Structured Data
- contentHtml += window.renderStructuredData(items, type, c);
- // Add JSON toggle at the bottom of the case section
- const caseJsonStr = JSON.stringify(c, null, 2).replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
- contentHtml += `<div style="margin-top: 1.5rem; border-top: 1px dashed rgba(0,0,0,0.1); padding-top: 1rem;">
- <div style="font-size: 0.8rem; color: var(--text-muted); cursor: pointer; display: flex; align-items: center; gap: 4px;" onclick="const content = this.nextElementSibling; content.classList.toggle('hidden'); this.querySelector('.arrow').textContent = content.classList.contains('hidden') ? '▶' : '▼';">
- <span class="arrow">▶</span> 查看 raw JSON
- </div>
- <div class="json-container hidden" style="margin-top: 0.8rem;"><pre style="background: #1e1e1e; color: #d4d4d4; padding: 1rem; border-radius: 6px; font-size: 0.8rem; overflow-x: auto; max-height: 400px; margin: 0;"><code>${caseJsonStr}</code></pre></div>
- </div>`;
- contentHtml += `</div>`;
- });
- sidebarHtml += `</div></div>`;
- contentHtml += `</div>`;
- if (!hasData) {
- return `<div style="color:var(--text-muted); padding: 1rem; text-align: center;">当前需求的所有案例均无提取的${type === 'workflow' ? '工序' : '能力'}</div>`;
- }
- return `<div style="display: flex; align-items: stretch; position: relative;">${sidebarHtml}${contentHtml}</div>`;
- }
- async function fetchRequirementData(index) {
- try {
- const res = await fetch(`/api/requirements/${index}/data`);
- const data = await res.json();
- window.dataCache = window.dataCache || {};
- window.dataCache[index] = data;
- let rawCasesClone = null;
- let casesList = [];
- if (data.raw_cases) {
- rawCasesClone = { ...data.raw_cases };
- const detailedCaseObj = data.raw_cases['case'] || data.raw_cases['case_detailed'];
- if (detailedCaseObj && detailedCaseObj.cases) {
- casesList = detailedCaseObj.cases;
- }
- }
- if (jsonStrategy) jsonStrategy.innerHTML = ''; // Tab removed
- if (jsonCapability) {
- const reqStr = (index + 1).toString().padStart(3, '0');
- jsonCapability.innerHTML = `
- <div id="container-capability" style="height: 100%; width: 100%;">
- <iframe src="/static/viz_fragment.html?req=${reqStr}&v=19" style="width: 100%; height: 100%; border: none; background: var(--bg-primary);"></iframe>
- </div>
- `;
- }
- jsonRaw.innerHTML = renderDataOrRaw(rawCasesClone, renderRawCases);
- const clusterData = window.dataCache[index].cluster;
- const oldActiveWorkflowTab = jsonBlueprint.querySelector('.sub-tab-btn.active')?.dataset?.target || 'sub-tab-workflow-cases';
- const workflowCasesHtml = renderAggregatedPerCaseData(casesList, 'workflow');
- let bpHtml = `<div id="container-workflow">`;
- bpHtml += `<div class="sub-tabs-container" style="margin-bottom: 1rem; border-bottom: 1px solid var(--border-glass); padding-bottom: 8px; display: flex; gap: 8px;">`;
- bpHtml += `<button class="sub-tab-btn ${oldActiveWorkflowTab === 'sub-tab-workflow-cases' ? 'active' : ''}" data-target="sub-tab-workflow-cases" onclick="selectGenericSubTab('workflow', 'sub-tab-workflow-cases')">📊 案例解析页</button>`;
- bpHtml += `<button class="sub-tab-btn ${oldActiveWorkflowTab === 'sub-tab-workflow-cluster' ? 'active' : ''}" data-target="sub-tab-workflow-cluster" onclick="selectGenericSubTab('workflow', 'sub-tab-workflow-cluster')">🧩 聚类结果 (Cluster)</button>`;
- bpHtml += `</div>`;
- bpHtml += `<div id="sub-tab-workflow-cases" class="sub-tab-pane ${oldActiveWorkflowTab === 'sub-tab-workflow-cases' ? '' : 'hidden'}">${workflowCasesHtml}</div>`;
- bpHtml += `<div id="sub-tab-workflow-cluster" class="sub-tab-pane ${oldActiveWorkflowTab === 'sub-tab-workflow-cluster' ? '' : 'hidden'}">
- <div style="margin-bottom: 1.5rem; padding: 1.2rem; background: rgba(0,0,0,0.02); border-radius: 8px; display: flex; justify-content: space-between; align-items: center; border: 1px dashed rgba(0,0,0,0.1);">
- <span style="color: var(--text-muted); font-size: 0.9em;">导入本地生成的 JSON (如 process.json 等) 将保存为 cluster.json 并支持单项删除与多选清除。</span>
- <div>
- <input type="file" id="input-upload-cluster" accept=".json" style="display:none;">
- <button class="btn btn-primary btn-small" onclick="document.getElementById('input-upload-cluster').click()">📥 导入 JSON</button>
- </div>
- </div>
- <div id="cluster-preview-content">${renderClusterDeletable(clusterData)}</div>
- </div>`;
- bpHtml += `</div>`;
- jsonBlueprint.innerHTML = bpHtml;
- const clusterFileInput = document.getElementById('input-upload-cluster');
- if (clusterFileInput) {
- clusterFileInput.onchange = async (e) => {
- const file = e.target.files[0];
- if (!file) return;
- const formData = new FormData();
- formData.append('file', file);
- try {
- const res = await fetch(`/api/requirements/${index}/upload_cluster`, {
- method: 'POST',
- body: formData
- });
- if (res.ok) {
- fetchRequirementData(index);
- } else {
- alert('上传失败');
- }
- } catch (err) {
- console.error(err);
- alert('上传出错');
- }
- };
- }
- const btnUpload = document.getElementById('btn-upload-source-ex');
- const fileInput = document.getElementById('input-upload-source-ex');
- if (btnUpload && fileInput) {
- btnUpload.onclick = () => fileInput.click();
- fileInput.onchange = async (e) => {
- const file = e.target.files[0];
- if (!file) return;
- const formData = new FormData();
- formData.append('file', file);
- try {
- const res = await fetch(`/api/requirements/${index}/upload_source_ex`, {
- method: 'POST',
- body: formData
- });
- if (res.ok) {
- alert('上传成功!');
- fetchRequirementData(index);
- } else {
- alert('上传失败');
- }
- } catch (err) {
- console.error(err);
- alert('上传出错');
- }
- };
- }
- if (rawCasesClone && Object.keys(rawCasesClone).length > 0) {
- currentAvailablePlatforms = Object.keys(rawCasesClone)
- .filter(p => p.startsWith('case_') && p !== 'case_detailed' && p !== 'case')
- .map(p => p.replace('case_', ''));
- if (currentAvailablePlatforms.length === 0) {
- currentAvailablePlatforms = ['xhs', 'youtube', 'bili', 'x'];
- }
- } else {
- currentAvailablePlatforms = ['xhs', 'youtube', 'bili', 'x'];
- }
- } catch (e) {
- console.error("Failed to fetch data", e);
- }
- }
- async function pollStatus() {
- try {
- const res = await fetch('/api/pipeline/status');
- const statusData = await res.json();
- let needsListUpdate = false;
- // Check if any status changed
- for (const [idxStr, runInfo] of Object.entries(statusData)) {
- const idx = parseInt(idxStr);
- if (!activeRuns[idx] || activeRuns[idx].status !== runInfo.status) {
- needsListUpdate = true;
- }
- activeRuns[idx] = runInfo;
- // Update logs if modal is open for this index
- if (currentSelectedIndex === idx && !modalLogs.classList.contains('hidden')) {
- terminalLogs.textContent = runInfo.logs.join('');
- terminalLogs.scrollTop = terminalLogs.scrollHeight;
- }
- // Update detail view banner if this is the selected one
- if (currentSelectedIndex === idx) {
- updateDetailBannerStatus(runInfo.status);
- }
- }
- if (needsListUpdate) {
- // update in requirements array
- requirements.forEach(req => {
- if (activeRuns[req.index]) {
- req.status = activeRuns[req.index].status;
- }
- });
- renderTaskList(requirements);
- updateStats();
- }
- } catch (e) {
- console.error("Failed to poll status", e);
- }
- }
- function startStatusPolling() {
- if (statusInterval) clearInterval(statusInterval);
- statusInterval = setInterval(pollStatus, 2000);
- }
- // Render
- function renderTaskList(list) {
- elReqSelector.innerHTML = '<option value="">-- 选择一个需求 --</option>';
- list.forEach(req => {
- const option = document.createElement('option');
- option.value = req.index;
- let statusMarker = '';
- if (req.status === 'running') statusMarker = '🚀';
- else if (req.status === 'completed') statusMarker = '✅';
- else if (req.status === 'partial') statusMarker = '⚠️';
- else if (req.status === 'failed') statusMarker = '❌';
- else statusMarker = '⏳';
- option.textContent = `${statusMarker} [#${req.id}] ${req.requirement} (Cases: ${req.raw_cases_count})`;
- if (currentSelectedIndex === req.index) {
- option.selected = true;
- }
- elReqSelector.appendChild(option);
- });
- }
- function updateStats() {
- const total = requirements.length;
- const completed = requirements.filter(r => r.status === 'completed').length;
- const running = requirements.filter(r => r.status === 'running').length;
- elStatsContainer.innerHTML = `
- <span>Total: ${total}</span>
- <span style="color:var(--success)">Done: ${completed}</span>
- ${running > 0 ? `<span style="color:var(--accent-primary)">Running: ${running}</span>` : ''}
- `;
- }
- function selectRequirement(index) {
- currentSelectedIndex = index;
- const req = requirements.find(r => r.index === index);
- if (!req) return;
- // Sync dropdown if called from somewhere else
- if (elReqSelector.value != index) {
- elReqSelector.value = index;
- }
- // Update Detail UI
- elEmptyState.classList.add('hidden');
- elDetailView.classList.remove('hidden');
- updateDetailBannerStatus(activeRuns[index] ? activeRuns[index].status : req.status);
- // Fetch data
- if (jsonStrategy) jsonStrategy.textContent = 'Loading...';
- if (jsonBlueprint) jsonBlueprint.textContent = 'Loading...';
- if (jsonRaw) jsonRaw.textContent = 'Loading...';
- fetchRequirementData(index);
- }
- function updateDetailBannerStatus(status) {
- const btnStop = document.getElementById('btn-stop-pipeline');
- if (status === 'running') {
- elStatusBanner.classList.remove('hidden');
- elStatusBanner.style.background = 'rgba(59, 130, 246, 0.1)';
- elStatusText.textContent = 'Pipeline is currently running...';
- elStatusBanner.querySelector('.status-indicator').style.display = 'block';
- elStatusBanner.querySelector('.status-indicator').style.background = 'var(--accent-primary)';
- if (btnStop) btnStop.style.display = 'inline-block';
- } else if (status === 'failed') {
- elStatusBanner.classList.remove('hidden');
- elStatusBanner.style.background = 'rgba(239, 68, 68, 0.1)';
- elStatusText.textContent = 'Pipeline run failed.';
- elStatusBanner.querySelector('.status-indicator').style.display = 'block';
- elStatusBanner.querySelector('.status-indicator').style.background = 'var(--danger)';
- if (btnStop) btnStop.style.display = 'none';
- } else {
- elStatusBanner.classList.add('hidden');
- if (btnStop) btnStop.style.display = 'none';
- }
- }
- // Actions
- async function triggerRun() {
- if (currentSelectedIndex === null) return;
- const checkSdkEl = document.getElementById('check-claude-sdk');
- const selectModelEl = document.getElementById('select-model');
- const checkSdk = checkSdkEl ? checkSdkEl.checked : (selectModelEl && selectModelEl.value === 'sdk');
- const mode = document.getElementById('select-force-phase').value;
- const platforms = document.getElementById('input-platforms').value;
- let only_step = null;
- let start_from = null;
- let end_at = null;
- let restart_mode = null;
- let phase = null;
- if (mode === "custom_range") {
- if (!chainStartNode) {
- alert('请在下方链条中选择至少一个节点作为起点。');
- return;
- }
- if (chainStartNode === chainEndNode || !chainEndNode) {
- only_step = chainStartNode.replace(/-1$/, '');
- } else {
- start_from = chainStartNode.replace(/-1$/, '');
- end_at = chainEndNode.replace(/-1$/, '');
- }
- } else if (mode.startsWith("step_")) {
- const stepMap = {
- "step_1.1": "research",
- "step_1.5": "source",
- "step_1.6": "generate-case",
- "step_2.1": "decode-workflow",
- "step_2.2": "apply-grounding",
- "step_2.1.1": "process-cluster",
- "step_2.1.2": "process-score",
- "step_2.2.1": "capability-extract",
- "step_2.2.2": "capability-enrich",
- "step_3.0": "strategy"
- };
- only_step = stepMap[mode];
- if (mode === "step_1.1") {
- restart_mode = "single_platforms"; // triggers backend's platform specific clearing
- }
- } else if (mode.startsWith("phase")) {
- if (mode === "phase1") {
- phase = 1;
- } else if (mode === "phase2") {
- phase = 2;
- } else if (mode === "phase3") {
- phase = 3;
- }
- } else {
- restart_mode = "smart";
- }
- const groupPlatforms = document.getElementById('group-platforms');
- const platformsToSend = groupPlatforms && groupPlatforms.style.display !== 'none' ? platforms : "";
- const requestData = {
- platforms: platformsToSend,
- use_claude_sdk: checkSdk,
- restart_mode: restart_mode,
- phase: phase,
- only_step: only_step,
- start_from: start_from,
- end_at: end_at
- };
- modalRun.classList.add('hidden');
- // Optimistic UI update
- const req = requirements.find(r => r.index === currentSelectedIndex);
- if (req) req.status = 'running';
- activeRuns[currentSelectedIndex] = { status: 'running', logs: ['Initializing request...'] };
- renderTaskList(requirements);
- updateDetailBannerStatus('running');
- try {
- const res = await fetch(`/api/pipeline/run/${currentSelectedIndex}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(requestData)
- });
- if (!res.ok) {
- const err = await res.json();
- alert("Error: " + err.detail);
- }
- } catch (e) {
- console.error("Run failed", e);
- alert("Failed to trigger run");
- }
- }
- // Event Listeners
- function setupEventListeners() {
- setupScriptEvents();
- setupLogViewerEvents();
- // Global JSON Toggle
- const globalJsonToggle = document.getElementById('global-json-toggle');
- if (globalJsonToggle) {
- globalJsonToggle.addEventListener('change', (e) => {
- const isRaw = e.target.checked;
- document.querySelectorAll('.data-view-ui').forEach(el => el.style.display = isRaw ? 'none' : '');
- document.querySelectorAll('.data-view-raw').forEach(el => el.style.display = isRaw ? '' : 'none');
- });
- }
- // Dropdown change
- elReqSelector.addEventListener('change', (e) => {
- const val = e.target.value;
- if (val) {
- selectRequirement(parseInt(val));
- } else {
- currentSelectedIndex = null;
- elEmptyState.classList.remove('hidden');
- elDetailView.classList.add('hidden');
- }
- });
- // Refresh Data without changing page position
- const btnRefresh = document.getElementById('btn-refresh-data');
- if (btnRefresh) {
- btnRefresh.addEventListener('click', async () => {
- if (currentSelectedIndex === null) {
- alert("请先选择一个需求项目!");
- return;
- }
- const oldText = btnRefresh.innerHTML;
- btnRefresh.innerHTML = '🔄 刷新中...';
- btnRefresh.disabled = true;
- const modalCaseDetail = document.getElementById('case-detail-modal');
- const isModalOpen = modalCaseDetail && !modalCaseDetail.classList.contains('hidden');
- const activeSidebarItem = document.querySelector('.modal-sidebar-item.active');
- const activeCaseIdx = activeSidebarItem ? parseInt(activeSidebarItem.id.replace('sidebar-item-', '')) : null;
- try {
- await fetchRequirementData(currentSelectedIndex);
- if (isModalOpen && activeCaseIdx !== null && typeof window.renderSingleCaseDetail === 'function') {
- window.renderSingleCaseDetail(activeCaseIdx);
- }
- } catch (e) {
- console.error("Failed to refresh data", e);
- } finally {
- btnRefresh.innerHTML = oldText;
- btnRefresh.disabled = false;
- }
- });
- }
- // Tabs
- document.querySelectorAll('.tab-btn-pill').forEach(btn => {
- btn.addEventListener('click', () => {
- // Un-active sibling buttons
- const tabGroup = btn.closest('.data-tabs-pill') || document;
- tabGroup.querySelectorAll('.tab-btn-pill').forEach(b => b.classList.remove('active'));
- btn.classList.add('active');
- // Find target content
- const targetId = btn.dataset.target;
- const targetEl = document.getElementById(targetId);
- if (targetEl) {
- // Find parent container of the target to hide siblings
- const contentGroup = targetEl.parentElement;
- Array.from(contentGroup.children).forEach(child => {
- if (child.classList.contains('tab-content') || child.classList.contains('adv-tab-content')) {
- child.classList.remove('active');
- child.classList.add('hidden'); // hidden for adv-tab-content
- }
- });
- // Show target
- targetEl.classList.add('active');
- targetEl.classList.remove('hidden');
- }
- });
- });
- // Modals
- document.getElementById('btn-open-run-modal').addEventListener('click', async () => {
- if (currentSelectedIndex !== null) {
- modalRun.classList.remove('hidden');
- const selectForcePhase = document.getElementById('select-force-phase');
- if (selectForcePhase) selectForcePhase.dispatchEvent(new Event('change'));
- const inputPlatforms = document.getElementById('input-platforms');
- if (inputPlatforms && currentAvailablePlatforms && currentAvailablePlatforms.length > 0) {
- inputPlatforms.value = currentAvailablePlatforms.join(',');
- }
- // Fetch status and render chain
- await fetchAndRenderPipelineChain(currentSelectedIndex);
- }
- });
- document.getElementById('btn-close-modal').addEventListener('click', () => {
- modalRun.classList.add('hidden');
- });
- document.getElementById('btn-cancel-run').addEventListener('click', () => {
- modalRun.classList.add('hidden');
- });
- const selectForcePhase = document.getElementById('select-force-phase');
- const groupPlatforms = document.getElementById('group-platforms');
- const chainContainer = document.getElementById('pipeline-chain-container');
- if (selectForcePhase) {
- selectForcePhase.addEventListener('change', (e) => {
- const val = e.target.value;
- if (groupPlatforms) {
- if (['smart', 'phase1', 'step_1.1'].includes(val) || (val === 'custom_range' && chainStartNode === 'research')) {
- groupPlatforms.style.display = 'block';
- } else {
- groupPlatforms.style.display = 'none';
- }
- }
- const selectModel = document.getElementById('select-model');
- const groupModel = selectModel ? selectModel.parentElement : null;
- if (groupModel) {
- if (['phase1', 'step_1.1'].includes(val)) {
- groupModel.style.display = 'none';
- } else {
- groupModel.style.display = 'block';
- }
- }
- if (val === 'custom_range') {
- chainContainer.classList.remove('hidden');
- } else {
- chainContainer.classList.add('hidden');
- }
- });
-
- // Trigger initial state
- selectForcePhase.dispatchEvent(new Event('change'));
- }
- document.getElementById('btn-reset-chain').addEventListener('click', () => {
- chainStartNode = null;
- chainEndNode = null;
- renderPipelineChain();
- });
- // Add Requirement Modal Events
- const modalAddReq = document.getElementById('add-req-modal');
- const inputAddReq = document.getElementById('input-new-req');
- document.getElementById('btn-add-req').addEventListener('click', () => {
- inputAddReq.value = "";
- modalAddReq.classList.remove('hidden');
- });
- document.getElementById('btn-close-add-req').addEventListener('click', () => {
- modalAddReq.classList.add('hidden');
- });
- document.getElementById('btn-cancel-add-req').addEventListener('click', () => {
- modalAddReq.classList.add('hidden');
- });
- document.getElementById('btn-submit-add-req').addEventListener('click', async () => {
- const val = inputAddReq.value.trim();
- if (!val) {
- alert('需求内容不能为空');
- return;
- }
- try {
- const res = await fetch(`/api/requirements`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ requirement: val })
- });
- if (res.ok) {
- modalAddReq.classList.add('hidden');
- await fetchRequirements();
- } else {
- alert('添加失败');
- }
- } catch (e) {
- console.error(e);
- alert('网络错误');
- }
- });
- const modalEditReq = document.getElementById('edit-req-modal');
- const inputEditReq = document.getElementById('edit-req-textarea');
- document.getElementById('btn-edit-req').addEventListener('click', () => {
- if (currentSelectedIndex !== null) {
- const req = requirements.find(r => r.index === currentSelectedIndex);
- if (req) {
- inputEditReq.value = req.requirement;
- modalEditReq.classList.remove('hidden');
- }
- }
- });
- const closeEditReq = () => modalEditReq.classList.add('hidden');
- document.getElementById('btn-close-edit-req').addEventListener('click', closeEditReq);
- document.getElementById('btn-cancel-edit-req').addEventListener('click', closeEditReq);
- const saveRequirementText = async (runAfter) => {
- if (currentSelectedIndex === null) return;
- const newText = inputEditReq.value.trim();
- if (!newText) {
- alert('需求文本不能为空');
- return;
- }
- try {
- const res = await fetch(`/api/requirements/${currentSelectedIndex}`, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ requirement: newText })
- });
- if (!res.ok) {
- const err = await res.json();
- alert('保存失败: ' + (err.detail || '未知错误'));
- return;
- }
- // Update locally
- const req = requirements.find(r => r.index === currentSelectedIndex);
- if (req) {
- req.requirement = newText;
- renderTaskList(requirements);
- }
- closeEditReq();
- if (runAfter) {
- document.getElementById('btn-open-run-modal').click();
- }
- } catch (e) {
- console.error(e);
- alert('保存请求失败');
- }
- };
- document.getElementById('btn-save-edit-req').addEventListener('click', () => saveRequirementText(false));
- document.getElementById('btn-save-run-edit-req').addEventListener('click', () => saveRequirementText(true));
- document.getElementById('btn-confirm-run').addEventListener('click', triggerRun);
- document.getElementById('btn-view-logs').addEventListener('click', () => {
- modalLogs.classList.remove('hidden');
- if (activeRuns[currentSelectedIndex]) {
- terminalLogs.textContent = activeRuns[currentSelectedIndex].logs.join('');
- terminalLogs.scrollTop = terminalLogs.scrollHeight;
- } else {
- terminalLogs.textContent = 'No logs available.';
- }
- });
- const btnStop = document.getElementById('btn-stop-pipeline');
- if (btnStop) {
- btnStop.addEventListener('click', async () => {
- if (currentSelectedIndex === null) return;
- if (!confirm('Are you sure you want to stop the running pipeline?')) return;
- try {
- const res = await fetch(`/api/pipeline/stop/${currentSelectedIndex}`, { method: 'POST' });
- if (!res.ok) {
- const err = await res.json();
- alert("Error stopping pipeline: " + err.detail);
- }
- } catch (e) {
- console.error("Failed to stop pipeline", e);
- alert("Failed to stop pipeline");
- }
- });
- }
- document.getElementById('btn-close-logs').addEventListener('click', () => {
- modalLogs.classList.add('hidden');
- });
- const btnOpenPrompts = document.getElementById('btn-open-prompts');
- if (btnOpenPrompts) {
- btnOpenPrompts.addEventListener('click', () => {
- modalPrompts.classList.remove('hidden');
- fetchPromptsList();
- });
- }
- const btnClosePrompts = document.getElementById('btn-close-prompts');
- if (btnClosePrompts) {
- btnClosePrompts.addEventListener('click', () => {
- modalPrompts.classList.add('hidden');
- });
- }
- const btnSavePrompt = document.getElementById('btn-save-prompt');
- if (btnSavePrompt) {
- btnSavePrompt.addEventListener('click', async () => {
- if (!currentPromptName) return;
- elPromptStatus.textContent = 'Saving...';
- elPromptStatus.style.color = 'var(--text-muted)';
- try {
- const reqBody = { content: elPromptTextarea.value };
- if (elSchemaTextarea) reqBody.schema_content = elSchemaTextarea.value;
- const res = await fetch(`/api/prompts/${currentPromptName}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(reqBody)
- });
- if (res.ok) {
- elPromptStatus.textContent = 'Saved!';
- elPromptStatus.style.color = 'var(--success)';
- setTimeout(() => elPromptStatus.textContent = '', 2000);
- } else {
- const errData = await res.json();
- throw new Error(errData.detail || "Failed to save");
- }
- } catch (e) {
- elPromptStatus.textContent = e.message || 'Save failed';
- elPromptStatus.style.color = 'var(--danger)';
- }
- });
- }
- const btnUpdateSchema = document.getElementById('update-schema-btn');
- if (btnUpdateSchema) {
- btnUpdateSchema.addEventListener('click', async () => {
- if (!currentPromptName) return;
- const originalText = btnUpdateSchema.innerHTML;
- btnUpdateSchema.innerHTML = '<span style="font-size: 1.1em;">⏳</span> 更新中...';
- btnUpdateSchema.disabled = true;
- try {
- // First save the prompt so the backend reads the latest content
- const reqBody = { content: elPromptTextarea.value };
- if (elSchemaTextarea) reqBody.schema_content = elSchemaTextarea.value;
- let res = await fetch(`/api/prompts/${currentPromptName}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(reqBody)
- });
- if (!res.ok) {
- const errData = await res.json();
- throw new Error(errData.detail || "保存 Prompt 失败");
- }
- // Then call the schema update API
- res = await fetch(`/api/prompts/${currentPromptName}/update_schema`, {
- method: 'POST'
- });
- if (res.ok) {
- const data = await res.json();
- if (elSchemaTextarea && data.schema_content) {
- elSchemaTextarea.value = data.schema_content;
- }
- btnUpdateSchema.innerHTML = '<span style="font-size: 1.1em;">✅</span> 更新成功';
- setTimeout(() => {
- btnUpdateSchema.innerHTML = originalText;
- btnUpdateSchema.disabled = false;
- }, 2000);
- } else {
- const errData = await res.json();
- throw new Error(errData.detail || "更新 Schema 失败");
- }
- } catch (e) {
- alert(e.message || '更新失败');
- btnUpdateSchema.innerHTML = '<span style="font-size: 1.1em;">❌</span> 失败';
- setTimeout(() => {
- btnUpdateSchema.innerHTML = originalText;
- btnUpdateSchema.disabled = false;
- }, 2000);
- }
- });
- }
- }
- // Boot
- // ----------------------------------------------------
- // Pipeline Chain Visualization Logic
- // ----------------------------------------------------
- async function fetchAndRenderPipelineChain(index) {
- try {
- const res = await fetch(`/api/requirements/${index}/pipeline-status`);
- if (res.ok) {
- currentPipelineStatus = await res.json();
- } else {
- currentPipelineStatus = {};
- }
- } catch (e) {
- console.error("Failed to fetch pipeline status", e);
- currentPipelineStatus = {};
- }
- chainStartNode = null;
- chainEndNode = null;
- renderPipelineChain();
- }
- function bindPipelineChainEvents() {
- PIPELINE_STEPS.forEach((step) => {
- const node = document.getElementById('node-' + step.id);
- if (!node) return;
- node.addEventListener('click', () => {
- if (!chainStartNode) {
- chainStartNode = step.id;
- } else if (!chainEndNode) {
- if (chainStartNode === step.id) {
- chainEndNode = step.id; // Double click = single step mode
- } else {
- chainEndNode = step.id;
- }
- } else {
- chainStartNode = step.id; // Reset and start new
- chainEndNode = null;
- }
- renderPipelineChain();
- updateRunModalVisibility();
- });
- });
- }
- function renderPipelineChain() {
- if (chainStartNode && chainEndNode) {
- const startIndex = PIPELINE_STEPS.findIndex(s => s.id === chainStartNode);
- const endIndex = PIPELINE_STEPS.findIndex(s => s.id === chainEndNode);
- if (endIndex < startIndex) {
- const tempId = chainStartNode;
- chainStartNode = chainEndNode;
- chainEndNode = tempId;
- }
- }
- const LINEAR_PREFIX = ["research", "source", "generate-case", "decode-workflow", "apply-grounding"];
- const BRANCH_21 = ["process-cluster", "process-score"];
- const BRANCH_22 = ["capability-extract", "capability-enrich"];
- const STRATEGY = "strategy";
- let activeSteps = new Set();
- if (chainStartNode && !chainEndNode) {
- activeSteps.add(chainStartNode);
- } else if (chainStartNode && chainEndNode) {
- const start = chainStartNode;
- const end = chainEndNode;
- const start_in_linear = LINEAR_PREFIX.includes(start);
- const start_in_21 = BRANCH_21.includes(start);
- const start_in_22 = BRANCH_22.includes(start);
- const end_in_linear = LINEAR_PREFIX.includes(end);
- const end_in_21 = BRANCH_21.includes(end);
- const end_in_22 = BRANCH_22.includes(end);
- const end_is_strategy = end === STRATEGY;
- // 1. Linear Prefix
- LINEAR_PREFIX.forEach(s => {
- const s_idx = LINEAR_PREFIX.indexOf(s);
- const start_idx = LINEAR_PREFIX.indexOf(start);
- if (start_idx >= 0 && s_idx >= start_idx) {
- if (end_in_linear) {
- if (s_idx <= LINEAR_PREFIX.indexOf(end)) activeSteps.add(s);
- } else {
- activeSteps.add(s);
- }
- }
- });
- // 2. Branches
- if (end_is_strategy || (!end_in_21 && !end_in_22 && !end_in_linear)) {
- if (!start_in_21 && !start_in_22) {
- BRANCH_21.forEach(s => activeSteps.add(s));
- BRANCH_22.forEach(s => activeSteps.add(s));
- } else if (start_in_21) {
- const idx = BRANCH_21.indexOf(start);
- BRANCH_21.slice(idx).forEach(s => activeSteps.add(s));
- BRANCH_22.forEach(s => activeSteps.add(s));
- } else if (start_in_22) {
- const idx = BRANCH_22.indexOf(start);
- BRANCH_22.slice(idx).forEach(s => activeSteps.add(s));
- BRANCH_21.forEach(s => activeSteps.add(s));
- }
- } else if (end_in_21) {
- const end_idx = BRANCH_21.indexOf(end);
- const start_idx = start_in_21 ? BRANCH_21.indexOf(start) : 0;
- BRANCH_21.slice(start_idx, end_idx + 1).forEach(s => activeSteps.add(s));
- } else if (end_in_22) {
- const end_idx = BRANCH_22.indexOf(end);
- const start_idx = start_in_22 ? BRANCH_22.indexOf(start) : 0;
- BRANCH_22.slice(start_idx, end_idx + 1).forEach(s => activeSteps.add(s));
- }
- // 3. Strategy
- if (end_is_strategy) {
- activeSteps.add("strategy");
- }
- }
- PIPELINE_STEPS.forEach((step) => {
- const node = document.getElementById('node-' + step.id);
- if (!node) return;
- node.className = 'chain-node';
- if (currentPipelineStatus[step.id]) {
- node.classList.add('completed');
- } else {
- node.classList.add('missing');
- }
- if (activeSteps.has(step.id)) {
- if ((chainStartNode && !chainEndNode) || step.id === chainStartNode || step.id === chainEndNode) {
- node.classList.add('selected');
- } else {
- node.classList.add('selected-range');
- }
- }
- });
- }
- bindPipelineChainEvents();
- // Case Detail Modal Listeners
- const btnCloseCaseDetail = document.getElementById('btn-close-case-detail');
- const modalCaseDetail = document.getElementById('case-detail-modal');
- if (btnCloseCaseDetail && modalCaseDetail) {
- btnCloseCaseDetail.addEventListener('click', () => {
- modalCaseDetail.classList.add('hidden');
- });
- // Close on escape
- document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape' && !modalCaseDetail.classList.contains('hidden')) {
- modalCaseDetail.classList.add('hidden');
- }
- });
- // Close on click outside
- modalCaseDetail.addEventListener('click', (e) => {
- if (e.target === modalCaseDetail) {
- modalCaseDetail.classList.add('hidden');
- }
- });
- }
- window.triggerSingleCaseRerun = async function (step, caseIndex) {
- if (typeof currentSelectedIndex === 'undefined' || currentSelectedIndex === null) {
- alert("请先选择一个需求项目!");
- return;
- }
- const confirmMsg = `确定要针对当前 Case 单独重跑 [${step}] 步骤吗?\n注意:这会覆盖现有的提取结果!`;
- if (!confirm(confirmMsg)) return;
- try {
- const payload = {
- use_claude_sdk: false, // Default
- only_step: step,
- case_index: caseIndex
- };
- // Use global Claude SDK checkbox if it's checked in the UI
- const cbClaudeSdk = document.getElementById('check-claude-sdk');
- if (cbClaudeSdk) {
- payload.use_claude_sdk = cbClaudeSdk.checked;
- }
- const res = await fetch(`/api/pipeline/run/${currentSelectedIndex}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(payload)
- });
- if (res.ok) {
- alert(`✅ 单Case重跑已触发 (${step})!请在 Pipeline 终端查看进度。`);
- // Show the logs modal instead of non-existent logs tab
- const btnViewLogs = document.getElementById('btn-view-logs');
- if (btnViewLogs) {
- btnViewLogs.click();
- }
- const modalCaseDetail = document.getElementById('case-detail-modal');
- if (modalCaseDetail) modalCaseDetail.classList.add('hidden');
- } else {
- const err = await res.json();
- alert(`启动重跑失败: ${err.detail || JSON.stringify(err)}`);
- }
- } catch (e) {
- console.error("Error triggering single case rerun:", e);
- alert(`发生错误: ${e.message}`);
- }
- };
- window.toggleCaseFilter = async function (caseId, isRestore) {
- if (currentSelectedIndex === null) return;
- const reqIndex = currentSelectedIndex;
- let reason = "manual_delete";
- if (!isRestore) {
- reason = prompt("请输入移除原因 (默认: manual_delete):", "manual_delete");
- if (reason === null) return; // Cancelled
- if (!reason.trim()) reason = "manual_delete";
- }
- try {
- const action = isRestore ? 'restore' : 'filter';
- const res = await fetch(`/api/requirements/${reqIndex}/cases/${action}`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ case_id: caseId, reason: reason })
- });
- if (!res.ok) {
- const err = await res.json();
- alert('操作失败: ' + (err.detail || '未知错误'));
- return;
- }
- // Close modal and refresh data
- document.getElementById('case-detail-modal').classList.add('hidden');
- document.getElementById('btn-refresh-data').click();
- } catch (e) {
- console.error(e);
- alert('操作失败');
- }
- };
- function renderFileTree(files, container, fileClickHandler, fileActionHtmlBuilder = null, actionHandler = null) {
- container.innerHTML = '';
- const tree = { folders: {}, files: [] };
- files.forEach(f => {
- const path = f.path || f.name;
- const parts = path.split('/');
- let current = tree;
- for (let i = 0; i < parts.length - 1; i++) {
- if (!current.folders[parts[i]]) current.folders[parts[i]] = { folders: {}, files: [] };
- current = current.folders[parts[i]];
- }
- current.files.push({ ...f, baseName: parts[parts.length - 1], fullPath: path });
- });
- function createNode(node, level, parentEl) {
- Object.keys(node.folders).sort().forEach(folderName => {
- const folderDiv = document.createElement('div');
- folderDiv.style.marginLeft = level > 0 ? '12px' : '0';
-
- const header = document.createElement('div');
- header.style.cursor = 'pointer';
- header.style.padding = '4px';
- header.style.display = 'flex';
- header.style.alignItems = 'center';
- header.innerHTML = `<span style="margin-right:5px; display:inline-block; font-size: 0.8em; transform: rotate(90deg); transition: transform 0.2s;" class="folder-icon">▶</span> <span style="font-weight: 500; color: #475569;">📁 ${folderName}</span>`;
-
- const content = document.createElement('div');
- content.style.display = 'block';
-
- header.onclick = () => {
- const isHidden = content.style.display === 'none';
- content.style.display = isHidden ? 'block' : 'none';
- header.querySelector('.folder-icon').style.transform = isHidden ? 'rotate(90deg)' : 'rotate(0deg)';
- };
-
- folderDiv.appendChild(header);
- folderDiv.appendChild(content);
- parentEl.appendChild(folderDiv);
-
- createNode(node.folders[folderName], level + 1, content);
- });
-
- node.files.sort((a,b) => a.baseName.localeCompare(b.baseName)).forEach(f => {
- const fileDiv = document.createElement('div');
- fileDiv.style.marginLeft = level > 0 ? '20px' : '0';
- fileDiv.style.padding = '3px 8px';
- fileDiv.style.cursor = 'pointer';
- fileDiv.style.borderRadius = '4px';
- fileDiv.style.display = 'flex';
- fileDiv.style.justifyContent = 'space-between';
- fileDiv.style.alignItems = 'center';
- fileDiv.style.wordBreak = 'break-all';
-
- const nameSpan = document.createElement('span');
- nameSpan.textContent = `📄 ${f.baseName}`;
- fileDiv.appendChild(nameSpan);
-
- if (fileActionHtmlBuilder) {
- const actionSpan = document.createElement('span');
- actionSpan.innerHTML = fileActionHtmlBuilder(f);
- fileDiv.appendChild(actionSpan);
- if (actionHandler) actionHandler(actionSpan, f);
- }
-
- fileDiv.onmouseover = () => fileDiv.style.background = 'rgba(255,255,255,0.5)';
- fileDiv.onmouseout = () => fileDiv.style.background = 'transparent';
- if (fileClickHandler) {
- fileDiv.onclick = (e) => {
- if (e.target.tagName === 'A') return;
- fileClickHandler(f, fileDiv);
- };
- }
- parentEl.appendChild(fileDiv);
- });
- }
-
- createNode(tree, 0, container);
- if (files.length === 0) {
- container.innerHTML = '<div style="color:var(--text-muted); padding:10px;">没有文件</div>';
- }
- }
- function setupLogViewerEvents() {
- const logReqSelector = document.getElementById('log-req-selector');
- const btnRefreshLogs = document.getElementById('btn-refresh-logs');
- const logFilesTree = document.getElementById('log-files-tree');
- const logViewerContent = document.getElementById('log-viewer-content');
- const logViewerTitle = document.getElementById('log-viewer-title');
- if (!logReqSelector || !btnRefreshLogs) return;
- function populateSelector() {
- logReqSelector.innerHTML = '';
- requirements.forEach(req => {
- const opt = document.createElement('option');
- opt.value = req.index;
- opt.textContent = `[${(req.index + 1).toString().padStart(3, '0')}] ${req.requirement.substring(0, 30)}...`;
- if (currentSelectedIndex === req.index) {
- opt.selected = true;
- }
- logReqSelector.appendChild(opt);
- });
- }
- const advLogsTabBtn = document.querySelector('.tab-btn-pill[data-target="adv-logs"]');
- if (advLogsTabBtn) {
- advLogsTabBtn.addEventListener('click', () => {
- populateSelector();
- btnRefreshLogs.click();
- });
- }
- btnRefreshLogs.addEventListener('click', async () => {
- const reqIndex = parseInt(logReqSelector.value);
- if (isNaN(reqIndex)) return;
- btnRefreshLogs.textContent = '刷新中...';
- btnRefreshLogs.disabled = true;
- logFilesTree.innerHTML = '';
- logViewerContent.textContent = '';
- logViewerTitle.innerHTML = '<span>选择一个文件查看</span>';
- try {
- const res = await fetch(`/api/requirements/${reqIndex}/files`);
- if (res.ok) {
- const data = await res.json();
- renderFileTree(data.files, logFilesTree, async (f) => {
- logViewerTitle.innerHTML = `<span>${f.fullPath}</span> <a href="/api/requirements/${reqIndex}/files/raw?path=${encodeURIComponent(f.fullPath)}" target="_blank" style="font-size:0.8em; color:#4f46e5;">新窗口打开</a>`;
- logViewerContent.textContent = '加载中...';
- try {
- if (f.fullPath.match(/\.(jpg|jpeg|png|gif)$/i)) {
- logViewerContent.innerHTML = `<img src="/api/requirements/${reqIndex}/files/raw?path=${encodeURIComponent(f.fullPath)}" style="max-width: 100%; border-radius: 4px;">`;
- } else {
- const contentRes = await fetch(`/api/requirements/${reqIndex}/files/content?path=${encodeURIComponent(f.fullPath)}`);
- if (contentRes.ok) {
- const contentData = await contentRes.json();
- logViewerContent.textContent = contentData.content;
- } else {
- logViewerContent.textContent = '无法读取文件内容 (可能不是文本格式)';
- }
- }
- } catch(e) {
- logViewerContent.textContent = '读取出错';
- }
- });
- } else {
- logFilesTree.innerHTML = '<div style="color:var(--danger); padding:10px;">加载失败</div>';
- }
- } catch (e) {
- logFilesTree.innerHTML = '<div style="color:var(--danger); padding:10px;">加载出错</div>';
- }
- btnRefreshLogs.textContent = '刷新文件列表';
- btnRefreshLogs.disabled = false;
- });
- }
- init();
- function setupScriptEvents() {
- const selector = document.getElementById('script-selector');
- const btnUpload = document.getElementById('btn-upload-script');
- const inputUpload = document.getElementById('script-upload-input');
- const btnDelete = document.getElementById('btn-delete-script');
- const formContainer = document.getElementById('script-form-container');
- const argsForm = document.getElementById('script-args-form');
- const btnRun = document.getElementById('btn-run-script');
- const btnStop = document.getElementById('btn-stop-script');
- const runOutput = document.getElementById('script-run-output');
- const runStatus = document.getElementById('script-run-status');
- const generatedFiles = document.getElementById('script-generated-files');
- const workspaceContainer = document.getElementById('script-workspace-container');
- const workspaceFiles = document.getElementById('script-workspace-files');
- const btnRefreshWorkspace = document.getElementById('btn-refresh-workspace');
- if (!selector) return;
- let currentScriptArgs = [];
- let currentProcessController = null;
- async function loadScripts() {
- try {
- const res = await fetch('/api/scripts');
- if (res.ok) {
- const data = await res.json();
- const currentVal = selector.value;
- selector.innerHTML = '<option value="">-- 请选择或上传常驻工具脚本 --</option>';
- data.scripts.forEach(s => {
- const opt = document.createElement('option');
- opt.value = `${s.folder}/${s.name}`;
- opt.textContent = `${s.name} (${s.folder})`;
- selector.appendChild(opt);
- });
- if (currentVal && data.scripts.some(s => `${s.folder}/${s.name}` === currentVal)) {
- selector.value = currentVal;
- } else {
- selector.value = '';
- }
- }
- } catch (e) {
- console.error("Failed to load scripts", e);
- }
- }
- async function loadScriptWorkspace(folder) {
- if (!folder) return;
- try {
- const res = await fetch(`/api/scripts/${folder}/files`);
- if (res.ok) {
- const data = await res.json();
- renderFileTree(data.files, workspaceFiles, null, (f) => {
- return `
- <a href="/api/scripts/files/${f.fullPath}?inline=true" target="_blank" style="margin-right:8px; font-size:0.85em;">预览</a>
- <a href="/api/scripts/files/${f.fullPath}" target="_blank" style="margin-right:8px; font-size:0.85em;">下载</a>
- <a href="javascript:void(0)" class="del-script-file" style="font-size:0.85em; color:#ef4444; text-decoration:none;">删除</a>
- `;
- }, (actionSpan, f) => {
- const delBtn = actionSpan.querySelector('.del-script-file');
- delBtn.onclick = async (e) => {
- e.preventDefault();
- if (!confirm(`确定删除文件 ${f.fullPath} 吗?`)) return;
- try {
- const res = await fetch(`/api/scripts/files/${f.fullPath}`, { method: 'DELETE' });
- if (res.ok) loadScriptWorkspace(folder);
- else alert('删除失败');
- } catch (err) { alert('删除出错'); }
- };
- });
- }
- } catch (e) {
- console.error("Failed to load workspace files", e);
- }
- }
- async function selectScript() {
- const val = selector.value;
- if (!val) {
- formContainer.classList.add('hidden');
- workspaceContainer.classList.add('hidden');
- btnDelete.classList.add('hidden');
- return;
- }
-
- btnDelete.classList.remove('hidden');
- formContainer.classList.remove('hidden');
- workspaceContainer.classList.remove('hidden');
- argsForm.innerHTML = '解析参数中...';
-
- const [folder, filename] = val.split('/');
- try {
- const res = await fetch(`/api/scripts/${folder}/${filename}/parse`);
- if (res.ok) {
- const data = await res.json();
- currentScriptArgs = data.args || [];
- argsForm.innerHTML = '';
- if (currentScriptArgs.length === 0) {
- argsForm.innerHTML = '<div style="color:var(--text-muted); font-size:0.9em;">该脚本无需参数,或无法解析参数。</div>';
- } else {
- currentScriptArgs.forEach((arg, i) => {
- const div = document.createElement('div');
- div.className = 'form-group';
- div.style.marginBottom = '5px';
-
- const label = document.createElement('label');
- label.style.fontWeight = 'bold';
- label.textContent = (arg.names ? arg.names.join(', ') : '参数') + (arg.required ? ' *' : '');
-
- const desc = document.createElement('div');
- desc.style.fontSize = '0.85em';
- desc.style.color = 'var(--text-muted)';
- desc.textContent = arg.desc || '';
-
- div.appendChild(label);
- div.appendChild(desc);
-
- const isOutputArg = (arg.names && arg.names.some(n => /^-[oO]$|out|save|write/i.test(n))) ||
- (arg.desc && /(输出|保存|写入)/.test(arg.desc));
- const isFileArg = !isOutputArg && arg.action_type === '_StoreAction' && (
- (arg.type && arg.type.includes('FileType')) ||
- (arg.names && arg.names.some(n => /file|path|dir|input/i.test(n))) ||
- (arg.desc && /(文件|目录|路径)/.test(arg.desc))
- );
- if (arg.action_type === '_StoreTrueAction' || arg.action_type === '_StoreFalseAction') {
- const input = document.createElement('input');
- input.type = 'checkbox';
- input.id = `script-arg-${i}`;
- input.checked = arg.default || false;
- div.appendChild(input);
- } else if (isFileArg) {
- const fileContainer = document.createElement('div');
- fileContainer.style.display = 'flex';
- fileContainer.style.alignItems = 'center';
- fileContainer.style.gap = '8px';
- fileContainer.style.flexWrap = 'wrap';
- const tagContainer = document.createElement('div');
- tagContainer.id = `script-arg-tag-${i}`;
-
- const hiddenInput = document.createElement('input');
- hiddenInput.type = 'hidden';
- hiddenInput.id = `script-arg-${i}`;
- if (arg.default !== null && arg.default !== undefined) {
- hiddenInput.value = arg.default;
- tagContainer.innerHTML = `<span class="structured-badge" style="background:#e0e7ff; color:#3730a3;">📄 ${arg.default} <span style="cursor:pointer; margin-left:4px;" onclick="document.getElementById('script-arg-${i}').value=''; this.parentElement.remove();">×</span></span>`;
- }
- const fileSelect = document.createElement('select');
- fileSelect.className = 'glass-input';
- fileSelect.style.flex = '1';
- fileSelect.style.minWidth = '150px';
- fileSelect.innerHTML = '<option value="">-- 从已上传的输入文件选择 --</option>';
-
- fetch(`/api/scripts/${folder}/files`).then(r => r.json()).then(data => {
- if (data.files) {
- data.files.filter(f => f.path.startsWith('inputs/')).forEach(f => {
- const opt = document.createElement('option');
- opt.value = f.path;
- opt.textContent = f.path.replace('inputs/', '');
- fileSelect.appendChild(opt);
- });
- }
- });
- fileSelect.onchange = () => {
- if (fileSelect.value) {
- hiddenInput.value = fileSelect.value;
- tagContainer.innerHTML = `<span class="structured-badge" style="background:#e0e7ff; color:#3730a3;">📄 ${fileSelect.value} <span style="cursor:pointer; margin-left:4px;" onclick="document.getElementById('script-arg-${i}').value=''; this.parentElement.remove();">×</span></span>`;
- fileSelect.value = '';
- }
- };
- const uploadBtn = document.createElement('button');
- uploadBtn.className = 'btn btn-secondary btn-small';
- uploadBtn.textContent = '上传新文件';
-
- const realFileInput = document.createElement('input');
- realFileInput.type = 'file';
- realFileInput.style.display = 'none';
-
- uploadBtn.onclick = (e) => {
- e.preventDefault();
- realFileInput.click();
- };
- realFileInput.onchange = async (e) => {
- const file = e.target.files[0];
- if (!file) return;
- uploadBtn.textContent = '上传中...';
- uploadBtn.disabled = true;
- const formData = new FormData();
- formData.append('file', file);
- try {
- const res = await fetch(`/api/scripts/${folder}/upload_data`, { method: 'POST', body: formData });
- if (res.ok) {
- const data = await res.json();
- hiddenInput.value = data.filename;
- tagContainer.innerHTML = `<span class="structured-badge" style="background:#e0e7ff; color:#3730a3;">📄 ${data.filename} <span style="cursor:pointer; margin-left:4px;" onclick="document.getElementById('script-arg-${i}').value=''; this.parentElement.remove();">×</span></span>`;
- if (!Array.from(fileSelect.options).some(o => o.value === data.filename)) {
- const opt = document.createElement('option');
- opt.value = data.filename;
- opt.textContent = data.filename.replace('inputs/', '');
- fileSelect.appendChild(opt);
- }
- loadScriptWorkspace(folder);
- } else { alert('上传失败'); }
- } catch (err) { alert('上传出错'); }
- uploadBtn.textContent = '上传新文件';
- uploadBtn.disabled = false;
- realFileInput.value = '';
- };
- fileContainer.appendChild(tagContainer);
- fileContainer.appendChild(hiddenInput);
- fileContainer.appendChild(fileSelect);
- fileContainer.appendChild(uploadBtn);
- fileContainer.appendChild(realFileInput);
- div.appendChild(fileContainer);
- } else {
- const input = document.createElement('input');
- input.type = 'text';
- input.className = 'glass-input';
- input.id = `script-arg-${i}`;
- if (arg.default !== null && arg.default !== undefined) {
- input.value = arg.default;
- }
- input.placeholder = arg.required ? '必填' : '选填';
- div.appendChild(input);
- }
-
- argsForm.appendChild(div);
- });
- }
- } else {
- argsForm.innerHTML = '<div style="color:var(--danger);">解析脚本参数失败</div>';
- }
-
- await loadScriptWorkspace(folder);
- } catch (e) {
- argsForm.innerHTML = '<div style="color:var(--danger);">解析出错</div>';
- }
- }
- selector.addEventListener('change', selectScript);
-
- const advScriptTabBtn = document.querySelector('.tab-btn-pill[data-target="adv-script"]');
- if (advScriptTabBtn) {
- advScriptTabBtn.addEventListener('click', loadScripts);
- }
- btnUpload.addEventListener('click', () => inputUpload.click());
- inputUpload.addEventListener('change', async (e) => {
- const file = e.target.files[0];
- if (!file) return;
-
- const statusEl = document.getElementById('script-upload-status');
- statusEl.textContent = '上传中...';
-
- const formData = new FormData();
- formData.append('file', file);
- try {
- const res = await fetch('/api/scripts/upload', {
- method: 'POST',
- body: formData
- });
- if (res.ok) {
- statusEl.textContent = '上传成功!';
- await loadScripts();
- const data = await res.json();
- selector.value = `${data.folder}/${data.filename}`;
- selectScript();
- setTimeout(() => statusEl.textContent = '', 3000);
- } else {
- statusEl.textContent = '上传失败';
- }
- } catch (err) {
- statusEl.textContent = '上传出错';
- }
- });
- btnDelete.addEventListener('click', async () => {
- const val = selector.value;
- if (!val) return;
- const [folder] = val.split('/');
- if (!confirm(`确定要删除目录 ${folder} 下的所有脚本及临时文件吗?`)) return;
-
- try {
- const res = await fetch(`/api/scripts/${folder}`, { method: 'DELETE' });
- if (res.ok) {
- await loadScripts();
- selectScript();
- } else {
- alert('删除失败');
- }
- } catch (e) {
- alert('删除出错');
- }
- });
- btnRefreshWorkspace.addEventListener('click', () => {
- const val = selector.value;
- if (val) loadScriptWorkspace(val.split('/')[0]);
- });
- btnStop.addEventListener('click', async () => {
- const val = selector.value;
- if (!val) return;
- const [folder] = val.split('/');
-
- try {
- await fetch(`/api/scripts/${folder}/stop`, { method: 'POST' });
- } catch (e) {
- console.error(e);
- }
- });
- btnRun.addEventListener('click', async () => {
- const val = selector.value;
- if (!val) return;
- const [folder, filename] = val.split('/');
-
- const reqArgs = [];
- let missingReq = false;
-
- currentScriptArgs.forEach((arg, i) => {
- const input = document.getElementById(`script-arg-${i}`);
- let value;
- if (input.type === 'checkbox') {
- value = input.checked;
- } else {
- value = input.value.trim();
- }
-
- if (arg.required && !value && input.type !== 'checkbox') {
- missingReq = true;
- input.style.borderColor = 'red';
- } else {
- if (input.type !== 'checkbox') input.style.borderColor = '';
- reqArgs.push({
- name: arg.names ? arg.names[0] : null,
- value: value,
- is_positional: arg.is_positional
- });
- }
- });
-
- if (missingReq) {
- alert("请填写所有必填参数");
- return;
- }
-
- btnRun.disabled = true;
- btnRun.textContent = '运行中...';
- btnStop.classList.remove('hidden');
- runOutput.textContent = '';
- runStatus.textContent = '运行中';
- runStatus.style.color = '#3b82f6';
- generatedFiles.innerHTML = '';
- generatedFiles.classList.add('hidden');
-
- currentProcessController = new AbortController();
-
- try {
- const res = await fetch('/api/scripts/run', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- folder: folder,
- filename: filename,
- args: reqArgs
- }),
- signal: currentProcessController.signal
- });
-
- const reader = res.body.getReader();
- const decoder = new TextDecoder('utf-8');
- let buffer = '';
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
-
- buffer += decoder.decode(value, { stream: true });
- const lines = buffer.split('\n');
- buffer = lines.pop() || '';
-
- for (const line of lines) {
- if (!line.trim()) continue;
- try {
- const data = JSON.parse(line);
- if (data.stdout !== undefined) {
- runOutput.textContent += data.stdout;
- } else if (data.stderr !== undefined) {
- runOutput.textContent += data.stderr;
- }
-
- if (data.returncode !== undefined) {
- if (data.returncode === 0) {
- runStatus.textContent = '运行成功';
- runStatus.style.color = '#10b981';
- } else {
- runStatus.textContent = '运行失败或中断';
- runStatus.style.color = '#ef4444';
- }
-
- if (data.files && data.files.length > 0) {
- generatedFiles.classList.remove('hidden');
- generatedFiles.innerHTML = '<strong>生成产物:</strong>';
- data.files.forEach(f => {
- const a = document.createElement('a');
- a.href = `/api/scripts/files/${f.name}`;
- a.target = '_blank';
- a.className = 'structured-badge';
- a.style.background = '#d1fae5';
- a.style.color = '#047857';
- a.style.textDecoration = 'none';
- a.textContent = `📄 ${f.name.split('/').pop()}`;
- generatedFiles.appendChild(a);
- });
- }
-
- loadScriptWorkspace(folder);
- }
- } catch(e) {
- runOutput.textContent += line + '\n';
- }
- }
- runOutput.scrollTop = runOutput.scrollHeight;
- }
- } catch (e) {
- runStatus.textContent = '网络错误或中断';
- runStatus.style.color = '#ef4444';
- } finally {
- btnRun.disabled = false;
- btnRun.textContent = '运行脚本';
- btnStop.classList.add('hidden');
- currentProcessController = null;
- }
- });
- }
|