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.