| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142 |
- 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 = '<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', '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 = '<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);
- }
- });
- }
- // 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 = `<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;
- }
- });
- }
|