PageSourceSearch

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

js 2pipe.app collected 2026-10-03 21:37:14 UTC 21,150 bytes, 580 lines download raw bytes

1// Questions data
2const questions = [
3    {
4        text: "1. Which types of reads do you want to assemble? (Select all that apply)",
5        options: [
6            "Short reads",
7            "Hi-Fi (PacBio) reads",
8            "Oxford Nanopore (ONT) reads",
9            "Hybrid",
10            "I don't know/It doesn't matter"
11        ],
12        multiSelect: true
13    },
14    {
15        text: "2. Do you want to handle multiple sample?",
16        options: [
17            "Yes",
18            "No",
19            "I don't know/It doesn't matter"
20        ]
21    },
22    {
23        text: "3. Do you need a pipeline with the ability to perform co-assembly and/or co-binning?",
24        options: [
25            "Yes",
26            "No",
27            "I don't know/It doesn't matter"
28        ]
29    },
30    {
31        text: "4. Are you looking for a pipeline with Graphical User Interface (GUI)?",
32        options: [
33            "Yes",
34            "No",
35            "I don't know/It doesn't matter"
36        ]
37    },
38    {
39        text: "5. Do you need a pipeline with native Cloud computing features (i.e. AWS, Azure)?",
40        options: [
41            "Yes",
42            "No",
43            "I don't know/It doesn't matter"
44        ]
45    },
46    {
47        text: "6. Which workflow manager do you prefer?",
48        options: [
49            "Nextflow",
50            "Snakemake",
51            "COMP Superscalar",
52            "Workflow Definition Language (WDL)",
53            "GenoPype",
54            "AWS environment",
55            "I don't know/It doesn't matter"
56        ]
57    },
58    {
59        text: "7. Do you need a pipeline with bin refinement?",
60        options: [
61            "Yes",
62            "No",
63            "I don't know/It doesn't matter"
64        ]
65    },
66    {
67        text: "8. Which execution options do you need? (Select all that apply)",
68        options: [
69            "Conda",
70            "Docker",
71            "Singularity",
72            "Other",
73            "I don't know/It doesn't matter"
74        ],
75        multiSelect: true
76    },
77    {
78        text: "9. Do you want to run the pipeline using external computational resources (i.e. services provided by Galaxy or MGnify)?",
79        options: [
80            "Yes",
81            "No",
82            "I don't know/It doesn't matter"
83        ]
84    },
85    {
86        text: "10. Which special options do you need? (Select all that apply)",
87        options: [
88            "Taxonomic profiling",
89            "Metabolic modeling",
90            "Ancient DNA",
91            "Eukaryotic MAGs",
92            "Viral MAGs",
93            "Reads grouped first and assembled in batches",
94            "Adaptable resource allocation",
95            "Metatranscriptome support",
96            "Plasmid assembly",
97            "Genotype recovery",
98            "Antimicrobial resistance gene prediction",
99            "Strain delineation",
100            "Phylogenetic tree generation",
101            "Pan-genome analysis",
102            "Visualization module",
103            "Direct sample comparison",
104            "Virulence factor annotation",
105            "I don't know/It doesn't matter"
106        ],
107        multiSelect: true
108    }
109];
110
111// Weight configuration for attributes
112const attributeWeights = {
113    readTypes: 2,           // High importance for read type matches
114    multiSample: 1,
115    coAssemblyCoBinning: 1,
116    GUI: 3,                 // High importance
117    Cloud: 1,
118    workflowManager: 1,
119    binRefinement: 1,
120    executionOptions: 1,    // Each matching option will add 1 point
121    externalComputationalResources: 3,
122    specialOptions: 1      // Each matching option will add 1 point
123};
124
125// State management
126let currentQuestionIndex = 0;
127const answers = new Array(questions.length).fill(null);
128
129// DOM elements
130const introSection = document.getElementById('intro-section');
131const questionSection = document.getElementById('question-section');
132const startBtn = document.getElementById('start-btn');
133const questionText = document.getElementById('question-text');
134const optionsContainer = document.getElementById('options-container');
135const prevBtn = document.getElementById('prev-btn');
136const nextBtn = document.getElementById('next-btn');
137const backToIntroBtn = document.getElementById('back-to-intro-btn');
138const progressBar = document.querySelector('.progress-bar');
139const questionContainer = document.querySelector('.question-container');
140
141// Theme toggle functionality
142const themeToggle = document.getElementById('theme-toggle');
143const themeIcon = themeToggle.querySelector('i');
144
145// Check for saved theme preference
146const savedTheme = localStorage.getItem('theme');
147if (savedTheme) {
148    document.documentElement.setAttribute('data-theme', savedTheme);
149    updateThemeIcon(savedTheme);
150}
151
152// Theme toggle click handler
153themeToggle.addEventListener('click', () => {
154    const currentTheme = document.documentElement.getAttribute('data-theme');
155    const newTheme = currentTheme === 'light' ? 'dark' : 'light';
156    
157    document.documentElement.setAttribute('data-theme', newTheme);
158    localStorage.setItem('theme', newTheme);
159    updateThemeIcon(newTheme);
160});
161
162// Update theme icon
163function updateThemeIcon(theme) {
164    themeIcon.className = theme === 'light' ? 'fas fa-sun' : 'fas fa-moon';
165}
166
167// Function to initialize back to top button
168function initializeBackToTopButton(container) {
169    const backToTopBtn = document.createElement('button');
170    backToTopBtn.className = 'back-to-top-btn';
171    backToTopBtn.innerHTML = '<i class="fas fa-arrow-up"></i>';
172    container.appendChild(backToTopBtn);
173
174    // Function to handle scroll event
175    function handleScroll() {
176        if (container.scrollTop > 300) {
177            backToTopBtn.classList.add('visible');
178        } else {
179            backToTopBtn.classList.remove('visible');
180        }
181    }
182
183    // Function to scroll to top
184    function scrollToTop() {
185        container.scrollTo({
186            top: 0,
187            behavior: 'smooth'
188        });
189    }
190
191    // Add scroll event listener
192    container.addEventListener('scroll', handleScroll);
193
194    // Add click event listener
195    backToTopBtn.addEventListener('click', scrollToTop);
196
197    return backToTopBtn;
198}
199
200// Functions
201function startQuestionnaire() {
202    introSection.style.display = 'none';
203    questionSection.style.display = 'block';
204    displayQuestion(currentQuestionIndex);
205    updateProgressBar();
206}
207
208function displayQuestion(index) {
209    const question = questions[index];
210    questionText.textContent = question.text;
211    
212    // Clear previous options
213    optionsContainer.innerHTML = '';
214    
215    // Create new options
216    question.options.forEach((option, i) => {
217        const optionElement = document.createElement('div');
218        optionElement.className = 'option';
219        
220        // Handle both single and multi-select cases
221        if (question.multiSelect) {
222            if (answers[index] && answers[index].includes(i)) {
223                optionElement.classList.add('selected');
224            }
225        } else {
226            if (answers[index] === i) {
227                optionElement.classList.add('selected');
228            }
229        }
230        
231        optionElement.textContent = option;
232        optionElement.addEventListener('click', () => selectOption(i));
233        optionsContainer.appendChild(optionElement);
234    });
235    
236    // Update navigation buttons
237    prevBtn.disabled = index === 0;
238    nextBtn.textContent = index === questions.length - 1 ? 'Finish' : 'Next';
239    
240    // For multi-select, next button is always enabled
241    if (!question.multiSelect) {
242        nextBtn.disabled = answers[index] === null;
243    }
244    
245    // Show/hide back button based on question index
246    backToIntroBtn.style.display = index === 0 ? 'block' : 'none';
247}
248
249function selectOption(optionIndex) {
250    const question = questions[currentQuestionIndex];
251    
252    if (question.multiSelect) {
253        // For multi-select questions, toggle the selection
254        const options = optionsContainer.querySelectorAll('.option');
255        const isSelected = options[optionIndex].classList.contains('selected');
256        
257        if (isSelected) {
258            options[optionIndex].classList.remove('selected');
259            // Remove from answers array if it exists
260            if (answers[currentQuestionIndex]) {
261                answers[currentQuestionIndex] = answers[currentQuestionIndex].filter(i => i !== optionIndex);
262            }
263        } else {
264            options[optionIndex].classList.add('selected');
265            // Initialize array if it doesn't exist
266            if (!answers[currentQuestionIndex]) {
267                answers[currentQuestionIndex] = [];
268            }
269            answers[currentQuestionIndex].push(optionIndex);
270        }
271        
272        // For multi-select, next button is always enabled
273        nextBtn.disabled = false;
274    } else {
275        // Original single-select logic
276        const options = optionsContainer.querySelectorAll('.option');
277        options.forEach(option => option.classList.remove('selected'));
278        options[optionIndex].classList.add('selected');
279        answers[currentQuestionIndex] = optionIndex;
280        nextBtn.disabled = false;
281    }
282}
283
284function showPreviousQuestion() {
285    if (currentQuestionIndex > 0) {
286        fadeTransition(() => {
287            currentQuestionIndex--;
288            displayQuestion(currentQuestionIndex);
289            updateProgressBar();
290        });
291    }
292}
293
294function showNextQuestion() {
295    if (currentQuestionIndex < questions.length - 1) {
296        fadeTransition(() => {
297            currentQuestionIndex++;
298            displayQuestion(currentQuestionIndex);
299            updateProgressBar();
300        });
301    } else {
302        // Handle form completion
303        handleCompletion();
304    }
305}
306
307function fadeTransition(callback) {
308    questionContainer.classList.add('fade-out');
309    setTimeout(() => {
310        callback();
311        questionContainer.classList.remove('fade-out');
312    }, 300);
313}
314
315function updateProgressBar() {
316    const progress = ((currentQuestionIndex + 1) / questions.length) * 100;
317    progressBar.style.width = `${progress}%`;
318}
319
320function getUserAnswers() {
321    const readTypeAnswers = answers[0] ? answers[0].map(index => questions[0].options[index]) : [];
322    return {
323        readTypes: readTypeAnswers,
324        multiSample: questions[1].options[answers[1]],
325        coAssemblyCoBinning: questions[2].options[answers[2]],
326        GUI: questions[3].options[answers[3]],
327        Cloud: questions[4].options[answers[4]],
328        workflowManager: questions[5].options[answers[5]],
329        binRefinement: questions[6].options[answers[6]],
330        executionOptions: answers[7] ? answers[7].map(index => questions[7].options[index]) : [],
331        externalComputationalResources: questions[8].options[answers[8]],
332        specialOptions: answers[9] ? answers[9].map(index => questions[9].options[index]) : []
333    };
334}
335
336function findBestMatch(userAnswers) {
337    let matches = [];
338    let totalScore = 0;
339    let meaningfulAnswers = 0;
340
341    // Helper function to check if an answer is meaningful (not "I don't know/It doesn't matter")
342    const isMeaningfulAnswer = (answer) => {
343        if (Array.isArray(answer)) {
344            return answer.some(a => a !== "I don't know/It doesn't matter");
345        }
346        return answer !== "I don't know/It doesn't matter";
347    };
348
349    // Count meaningful answers
350    Object.values(userAnswers).forEach(answer => {
351        if (isMeaningfulAnswer(answer)) {
352            meaningfulAnswers++;
353        }
354    });
355
356    // If less than 3 meaningful answers, return null to trigger the message
357    if (meaningfulAnswers < 3) {
358        return null;
359    }
360
361    preLoadedObjects.forEach(obj => {
362        let score = 0;
363        let matchDetails = [];
364
365        // Helper function to check attribute match
366        const checkAttribute = (attr, weight) => {
367            const userValue = userAnswers[attr];
368            const objValues = obj.attributes[attr];
369            
370            // Special handling for multi-select options (readTypes, executionOptions, and specialOptions)
371            if (attr === 'readTypes' || attr === 'executionOptions' || attr === 'specialOptions') {
372                if (userValue && objValues) {
373                    // Skip "Don't know/Doesn't matter" option in scoring
374                    const validUserValues = userValue.filter(v => v !== "I don't know/It doesn't matter");
375                    if (validUserValues.length === 0) return true;
376
377                    // Count how many user selections match the pipeline's options
378                    const matchingOptions = validUserValues.filter(option => objValues.includes(option));
379
380                    if (matchingOptions.length > 0) {
381                        // Add 1 point for each matching option
382                        score += matchingOptions.length * weight;
383                        matchDetails.push(`${attr}: ${matchingOptions.join(', ')}`);
384                    }
385                }
386                return true;
387            }
388            
389            // For Yes/No/Don't know questions
390            if (userValue === "Yes" || userValue === "No" || userValue === "I don't know/It doesn't matter") {
391                if (userValue === "I don't know/It doesn't matter") {
392                    // Don't add or subtract points for "Don't know/Doesn't matter"
393                    return true;
394                }
395                if (objValues && objValues.includes(userValue)) {
396                    if (userValue === "Yes") {
397                        score += weight;
398                        matchDetails.push(attr);
399                    }
400                    return true;
401                }
402            } 
403            // For other types of questions (like workflow manager)
404            else {
405                if (objValues && objValues.includes(userValue)) {
406                    score += weight;
407                    matchDetails.push(attr);
408                    return true;
409                }
410            }
411            return false;
412        };
413
414        // Check each attribute with its weight
415        checkAttribute('readTypes', attributeWeights.readTypes);
416        checkAttribute('multiSample', attributeWeights.multiSample);
417        checkAttribute('coAssemblyCoBinning', attributeWeights.coAssemblyCoBinning);
418        checkAttribute('GUI', attributeWeights.GUI);
419        checkAttribute('Cloud', attributeWeights.Cloud);
420        checkAttribute('workflowManager', attributeWeights.workflowManager);
421        checkAttribute('binRefinement', attributeWeights.binRefinement);
422        checkAttribute('executionOptions', attributeWeights.executionOptions);
423        checkAttribute('externalComputationalResources', attributeWeights.externalComputationalResources);
424        checkAttribute('specialOptions', attributeWeights.specialOptions);
425
426        matches.push({
427            object: obj,
428            score: score,
429            matchDetails: matchDetails
430        });
431    });
432
433    // Sort matches by score in descending order
434    matches.sort((a, b) => b.score - a.score);
435
436    // If no matches, return null
437    if (matches.length === 0) {
438        return null;
439    }
440
441    // Get the best score
442    const bestScore = matches[0].score;
443    
444    // Find all matches that share the best score
445    const bestMatches = matches.filter(match => match.score === bestScore);
446    
447    // If we have more than one best match, return all of them
448    if (bestMatches.length > 1) {
449        return {
450            bestMatches: bestMatches,
451            hasTies: true
452        };
453    }
454    
455    // If we have only one best match, return it along with the second best
456    return {
457        bestMatches: [matches[0], matches[1] || matches[0]],
458        hasTies: false
459    };
460}
461
462// Feature labels mapping
463const featureLabels = {
464    readTypes: "Read Types",
465    multiSample: "Multi-Sample Support",
466    coAssemblyCoBinning: "Co-Assembly/Co-Binning",
467    GUI: "Graphical User Interface (GUI)",
468    Cloud: "Cloud Computing Support",
469    workflowManager: "Workflow Manager",
470    binRefinement: "Bin Refinement",
471    executionOptions: "Execution Options",
472    externalComputationalResources: "External Computational Resources",
473    specialOptions: "Special Options"
474};
475
476function handleCompletion() {
477    const userAnswers = getUserAnswers();
478    const matchResult = findBestMatch(userAnswers);
479    
480    // Helper function to format match details
481    const formatMatchDetails = (details) => {
482        return details.map(detail => {
483            if (detail.startsWith('specialOptions:')) {
484                const options = detail.replace('specialOptions: ', '');
485                return `Special Options: ${options}`;
486            }
487            if (detail.startsWith('executionOptions:')) {
488                const options = detail.replace('executionOptions: ', '');
489                return `Execution Options: ${options}`;
490            }
491            if (detail.startsWith('readTypes:')) {
492                const options = detail.replace('readTypes: ', '');
493                return `Read Types: ${options}`;
494            }
495            return featureLabels[detail] || detail;
496        });
497    };
498    
499    // Clear the container and show results
500    questionContainer.innerHTML = `
501        <div class="question-content">
502            ${matchResult === null ? `
503                <div class="insufficient-input-message">
504                    <h2>More information is required</h2>
505                    <p>For us to provide accurate pipeline recommendations, we need more information about your needs.</p>
506                    <p>Please answer at least 3 questions with specific requirements.</p>
507                    <div class="restart-container">
508                        <button id="restart-btn" class="restart-btn">Start Over</button>
509                    </div>
510                </div>
511            ` : `
512                <h2>Recommended pipelines for MAG reconstruction</h2>
513                ${matchResult.hasTies ? 
514                    `<p class="tie-message">We found ${matchResult.bestMatches.length} pipelines that equally match your requirements!</p>` :
515                    `<p class="recommendation-message">Here are the best matching pipelines for your needs:</p>`
516                }
517                ${matchResult.bestMatches.map((match, index) => `
518                    <div class="recommendation-card ${!matchResult.hasTies && index === 1 ? 'secondary' : ''}">
519                        <h3>${match.object.name}</h3>
520                        <p class= "text-workflow">${match.object.description}</p>
521                        <div class="match-details">
522                            <p>Match Score: ${match.score}</p>
523                            <p>Matching Features:</p>
524                            <ul>
525                                ${formatMatchDetails(match.matchDetails).map(detail => `<li>${detail}</li>`).join('')}
526                            </ul>
527                        </div>
528                        <a href="${match.object.url}" target="_blank" class="pipeline-link">Match me with ${match.object.name}</a>
529                    </div>
530                `).join('')}
531                <div class="restart-container">
532                    <button id="restart-btn" class="restart-btn">Start Over</button>
533                </div>
534            `}
535        </div>
536    `;
537
538    // Add event listener for restart button
539    document.getElementById('restart-btn').addEventListener('click', restartQuestionnaire);
540    
541    // Ensure the user sees the beginning of the results instead of the bottom of the page
542    requestAnimationFrame(() => {
543        window.scrollTo({
544            top: 0,
545            behavior: 'smooth'
546        });
547    });
548
549    // Log the results for debugging
550    console.log('User Answers:', userAnswers);
551    console.log('Match Results:', matchResult);
552    if (matchResult) {
553        console.log('Best Matches:', matchResult.bestMatches);
554        console.log('Has Ties:', matchResult.hasTies);
555    }
556}
557
558function restartQuestionnaire() {
559    // Simply reload the page to start fresh
560    window.location.reload();
561}
562
563function backToIntroduction() {
564    // Reset the questionnaire state
565    currentQuestionIndex = 0;
566    answers.fill(null);
567    
568    // Hide question section and show intro section
569    questionSection.style.display = 'none';
570    introSection.style.display = 'block';
571    
572    // Reset progress bar
573    progressBar.style.width = '0%';
574}
575
576// Event listeners
577startBtn.addEventListener('click', startQuestionnaire);
578prevBtn.addEventListener('click', showPreviousQuestion);
579nextBtn.addEventListener('click', showNextQuestion);
580backToIntroBtn.addEventListener('click', backToIntroduction); 

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.