PageSourceSearch

https://2pipe.app/toolsSelector.js

js 2pipe.app collected 2026-10-03 21:37:13 UTC 9,744 bytes, 220 lines download raw bytes

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.