1// Tools Selector: build dropdowns from MAG_PIPELINES and suggest matching pipelines 2(function() { 3 if (typeof MAG_PIPELINES === 'undefined') return; 4 5 // Steps and fields mapping to MAG_PIPELINES keys 6 // Use the keys present in MAG_pipelines_from_xlsx.js attributes 7 const stepDefinitions = [ 8 { key: 'qc', label: 'Quality Control' }, 9 { key: 'assembly', label: 'Assembly' }, 10 { key: 'binning', label: 'Binning' }, 11 { key: 'qualityAssessment', label: 'Quality Assessment' }, 12 { key: 'refinement', label: 'Refinement' }, 13 { key: 'taxonomy', label: 'Taxonomy' }, 14 { key: 'functionalAnnotation', label: 'Functional Annotation' }, 15 { key: 'other', label: 'Other' } 16 ]; 17 18 // Build unique option sets per step by parsing MAG_PIPELINES text values 19 function extractOptionsForKey(key) { 20 const tokens = new Set(); 21 MAG_PIPELINES.forEach(p => { 22 const arr = p.attributes && Array.isArray(p.attributes[key]) ? p.attributes[key] : []; 23 arr.forEach(t => { 24 const s = (t || '').toString().trim(); 25 if (s) tokens.add(s); 26 }); 27 }); 28 return Array.from(tokens).sort((a, b) => a.localeCompare(b)); 29 } 30 function buildUI(container) { 31 container.innerHTML = ''; 32 33 const header = document.createElement('div'); 34 header.className = 'tools-header'; 35 header.innerHTML = '<h2>Select tools by workflow step</h2>'; 36 37 const form = document.createElement('div'); 38 form.className = 'tools-form'; 39 40 const selects = {}; 41 stepDefinitions.forEach(step => { 42 const group = document.createElement('div'); 43 group.className = 'tools-group'; 44 45 const label = document.createElement('label'); 46 label.textContent = step.label; 47 label.setAttribute('for', `tool-${step.key}`); 48 49 const select = document.createElement('select'); 50 select.id = `tool-${step.key}`; 51 select.className = 'tools-select'; 52 select.multiple = true; 53 select.size = 6; 54 55 // Note: multi-select has no explicit placeholder; leaving empty means 'Any' 56 57 extractOptionsForKey(step.key).forEach(opt => { 58 const option = document.createElement('option'); 59 option.value = opt; 60 option.textContent = opt; 61 // Enable multi-select via simple clicks (no Ctrl/Cmd needed) 62 option.addEventListener('mousedown', function(e) { 63 e.preventDefault(); 64 // Preserve scroll position to prevent jump 65 const prevScrollTop = select.scrollTop; 66 option.selected = !option.selected; 67 // Trigger change to recompute matches 68 const evt = new Event('change', { bubbles: true }); 69 select.dispatchEvent(evt); 70 // Restore scroll position 71 select.scrollTop = prevScrollTop; 72 }); 73 select.appendChild(option); 74 }); 75 76 select.addEventListener('change', updateSuggestions); 77 78 group.appendChild(label); 79 group.appendChild(select); 80 form.appendChild(group); 81 selects[step.key] = select; 82 }); 83 84 const results = document.createElement('div'); 85 results.id = 'tools-results'; 86 results.className = 'tools-results'; 87 88 container.appendChild(header); 89 container.appendChild(form); 90 const resultsTitle = document.createElement('h3'); 91 resultsTitle.className = 'tools-results-title'; 92 resultsTitle.textContent = 'Matching pipelines for your selection'; 93 container.appendChild(resultsTitle); 94 container.appendChild(results); 95 96 function updateSuggestions() { 97 const selections = {}; 98 stepDefinitions.forEach(step => { 99 const sel = selects[step.key]; 100 const chosen = Array.from(sel.selectedOptions).map(o =>
100 o.value).filter(Boolean); 101 if (chosen.length > 0) selections[step.key] = chosen.map(v => v.toLowerCase()); 102 }); 103 104 const strictKeys = ['assembly', 'binning']; 105 const matches = MAG_PIPELINES.map(p => { 106 let score = 0; 107 const matchedByStep = {}; 108 // Build original-casing lookup per step for this pipeline 109 const originalByKey = {}; 110 stepDefinitions.forEach(step => { 111 const arr = (p.attributes && Array.isArray(p.attributes[step.key]) ? p.attributes[step.key] : []); 112 const map = {}; 113 arr.forEach(x => { 114 const orig = (x || '').toString(); 115 map[orig.toLowerCase()] = orig; 116 }); 117 originalByKey[step.key] = map; 118 }); 119 Object.entries(selections).forEach(([key, arr]) => { 120 const fieldArr = (p.attributes && Array.isArray(p.attributes[key]) ? p.attributes[key] : []).map(x => (x || '').toString().toLowerCase()); 121 if (strictKeys.includes(key)) { 122 // Require ALL selected tools to be present for these steps 123 const allPresent = arr.every(val => fieldArr.includes(val)); 124 if (allPresent) { 125 score += arr.length; // reward matching all selected tools 126 // Map back to original casing 127 const origMatched = arr.map(v => originalByKey[key][v] || v); 128 matchedByStep[key] = origMatched; 129 } 130 } else { 131 // OR logic: count how many selected tokens are present 132 const matched = arr.filter(val => fieldArr.includes(val)); 133 if (matched.length > 0) { 134 score += matched.length; 135 const origMatched = matched.map(v => originalByKey[key][v] || v); 136 matchedByStep[key] = origMatched; 137 } 138 } 139 }); 140 return { pipeline: p, score, matchedByStep }; 141 }).filter(m => m.score > 0); 142 143 matches.sort((a, b) => b.score - a.score); 144 145 // total selected tools across all steps 146 const totalSelected = Object.values(selections).reduce((acc, arr) => acc + arr.length, 0); 147 renderResults(matches, totalSelected); 148 } 149 150 function renderResults(matches, totalCriteria) { 151 results.innerHTML = ''; 152 153 if (totalCriteria === 0) { 154 const info = document.createElement('p'); 155 info.textContent = 'Select one or more tools per step (Cmd/Ctrl-click) to get matches.'; 156 results.appendChild(info); 157 return; 158 } 159 160 if (matches.length === 0) { 161 const none = document.createElement('p'); 162 none.textContent = 'No pipelines match the current selection.'; 163 results.appendChild(none); 164 return; 165 } 166 167 const labelByKey = stepDefinitions.reduce((acc, s) => { acc[s.key] = s.label; return acc; }, {});
168 matches.slice(0, 5).forEach(m => { 169 const card = document.createElement('div'); 170 card.className = 'tools-recommendation-card'; 171 const matchedList = Object.entries(m.matchedByStep) 172 .map(([key, arr]) => `<li><strong>${labelByKey[key]}:</strong> ${arr.map(x => x).join(', ')}</li>`) 173 .join(''); 174 card.innerHTML = ` 175 <h3>${m.pipeline.name}</h3> 176 <p>Match Score: ${m.score} / ${totalCriteria}</p> 177 <div class="match-details"> 178 <p>Matching tools:</p> 179 <ul>${matchedList}</ul> 180 </div> 181 <a href="#" class="pipeline-link" data-name="${m.pipeline.name}">Match me with ${m.pipeline.name}</a> 182 `; 183 const link = card.querySelector('.pipeline-link'); 184 link.addEventListener('click', (e) => { 185 e.preventDefault(); 186 const workflowSection = document.getElementById('workflow-section'); 187 const pipelineSelect = document.getElementById('pipeline-select'); 188 if (workflowSection && pipelineSelect) { 189 document.getElementById('workflow-info-section').style.display = 'none'; 190 workflowSection.style.display = 'block'; 191 pipelineSelect.value = findPipelineIdByName(m.pipeline.name) || ''; 192 const event = new Event('change'); 193 pipelineSelect.dispatchEvent(event); 194 } 195 }); 196 results.appendChild(card); 197 }); 198 } 199 200 renderResults([], 0); 201 } 202 203 function findPipelineIdByName(name) { 204 // Try to map by name from preLoadedObjects when possible 205 if (typeof preLoadedObjects !== 'undefined') { 206 const p = preLoadedObjects.find(x => x.name.toLowerCase() === name.toLowerCase()); 207 return p ? p.id : null; 208 } 209 return null; 210 } 211 212 // Expose embed function 213 window.renderToolsSelectorInto = function(container) { 214 if (!container) return; 215 buildUI(container); 216 }; 217})(); 218 219 220
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.