let requirements = []; let currentSelectedIndex = null; let activeRuns = {}; let statusInterval = null; let currentAvailablePlatforms = ['xhs', 'youtube', 'bili', 'x', 'douyin', 'sph']; 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 = ''; } } 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 '
暂无案例数据
'; } let sidebarHtml = `
`; sidebarHtml += ``; contentHtml += `
`; if (!hasData) { return `
当前需求的所有案例均无提取的${type === 'workflow' ? '工序' : '能力'}
`; } return `
${sidebarHtml}${contentHtml}
`; } async function fetchRequirementData(index) { try { const res = await fetch(`/api/requirements/${index}/data`); const data = await res.json(); 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 = `
`; } 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 = `
`; bpHtml += `
`; bpHtml += ``; bpHtml += ``; bpHtml += `
`; bpHtml += `
${workflowCasesHtml}
`; bpHtml += `
导入本地生成的 JSON (如 process.json 等) 将保存为 cluster.json 并支持单项删除与多选清除。
${renderClusterDeletable(clusterData)}
`; bpHtml += `
`; 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', 'douyin', 'sph']; } } else { currentAvailablePlatforms = ['xhs', 'youtube', 'bili', 'x', 'douyin', 'sph']; } } catch (e) { console.error("Failed to fetch data", e); } // Automatically re-apply search filter on newly loaded data if (typeof applySearchFilter === 'function') { applySearchFilter(); } } 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 = ''; 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 = ` Total: ${total} Done: ${completed} ${running > 0 ? `Running: ${running}` : ''} `; } function selectRequirement(index) { currentSelectedIndex = index; const req = requirements.find(r => r.index === index); if (!req) return; // Sync dropdown if called from somewhere else if (elReqSelector.value != index) { elReqSelector.value = index; } // Update Detail UI elEmptyState.classList.add('hidden'); elDetailView.classList.remove('hidden'); updateDetailBannerStatus(activeRuns[index] ? activeRuns[index].status : req.status); // Fetch data if (jsonStrategy) jsonStrategy.textContent = 'Loading...'; if (jsonBlueprint) jsonBlueprint.textContent = 'Loading...'; if (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 = ' 更新中...'; 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 = ' 更新成功'; 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 = ' 失败'; setTimeout(() => { btnUpdateSchema.innerHTML = originalText; btnUpdateSchema.disabled = false; }, 2000); } }); } // Search input character matching for Case tab const searchInput = document.querySelector('.search-input'); if (searchInput) { searchInput.addEventListener('input', () => { applySearchFilter(); }); } } window.applySearchFilter = function() { const searchInput = document.querySelector('.search-input'); if (!searchInput) return; const query = searchInput.value.toLowerCase().trim(); // Filter raw case cards (on "案例" page) const cards = document.querySelectorAll('#json-raw .masonry-card'); cards.forEach(card => { const text = card.textContent.toLowerCase(); if (text.includes(query)) { card.style.display = ''; } else { card.style.display = 'none'; } }); // Handle empty group headers and grids const grids = document.querySelectorAll('#json-raw .masonry-grid'); grids.forEach(grid => { const visibleCards = Array.from(grid.querySelectorAll('.masonry-card')).filter(card => card.style.display !== 'none'); const prevSibling = grid.previousElementSibling; if (visibleCards.length > 0) { grid.style.display = ''; if (prevSibling && prevSibling.tagName === 'H3') { prevSibling.style.display = ''; } } else { grid.style.display = 'none'; if (prevSibling && prevSibling.tagName === 'H3') { prevSibling.style.display = 'none'; } } }); }; // 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 = ` 📁 ${folderName}`; 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 = '
没有文件
'; } } 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 = '选择一个文件查看'; 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 = `${f.fullPath} 新窗口打开`; logViewerContent.textContent = '加载中...'; try { if (f.fullPath.match(/\.(jpg|jpeg|png|gif)$/i)) { logViewerContent.innerHTML = ``; } 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 = '
加载失败
'; } } catch (e) { logFilesTree.innerHTML = '
加载出错
'; } 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 = ''; 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 ` 预览 下载 删除 `; }, (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 = '
该脚本无需参数,或无法解析参数。
'; } 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 = `📄 ${arg.default} ×`; } const fileSelect = document.createElement('select'); fileSelect.className = 'glass-input'; fileSelect.style.flex = '1'; fileSelect.style.minWidth = '150px'; fileSelect.innerHTML = ''; 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 = `📄 ${fileSelect.value} ×`; 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 = `📄 ${data.filename} ×`; 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 = '
解析脚本参数失败
'; } await loadScriptWorkspace(folder); } catch (e) { argsForm.innerHTML = '
解析出错
'; } } 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 = '生成产物:'; 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; } }); }