/** * Workflow Handlers - Simplified 3-Step Workflow */ import { appState, setCroppedEmbryos, setCurrentEmbryoIndex, getEmbryoRemark, setEmbryoRemark } from '../state.js'; import { runClassification, evaluateEmbryo } from '../models/inference.js'; import { detectEmbryos, isYOLOAvailable } from '../models/yoloDetection.js'; import { showToast } from '../ui/toast.js'; import { displayClassificationStatus, displayFinalResults } from '../ui/results.js'; import { displayCroppedEmbryo } from '../ui/imageDisplay.js'; import { enableNextButton, disableNextButton, markStepCompleted } from '../ui/stepper.js'; import { showCropEditor, hideCropEditor } from '../ui/cropEditor.js'; import { showConfirmModal } from '../ui/modal.js'; import { initializeInlineDetection } from '../ui/inlineDetection.js'; /** * Classify if image contains embryo (auto-triggered on upload) */ export async function classifyImage(imageData) { const statusDiv = document.getElementById('classificationStatus'); try { if (statusDiv) { statusDiv.innerHTML = `

Analyzing image...

`; } showToast('Classifying image...', 'info'); const result = await runClassification(imageData); displayClassificationStatus(result); // Enable next button if classification successful and it's an embryo if (result && result.label === 'yes') { enableNextButton(1); showToast('Image classified successfully - Embryo detected!', 'success'); } else if (result && result.label === 'no') { disableNextButton(1); // Ensure button is disabled for non-embryo images showToast('No embryo detected in the image', 'warning'); if (statusDiv) { statusDiv.innerHTML = `

No embryo detected

Please upload an image containing an embryo to continue.

`; } } } catch (error) { console.error('Classification error:', error); showToast(`Classification failed: ${error.message}`, 'error'); if (statusDiv) { statusDiv.innerHTML = `

Classification failed

${error.message}

`; } } } /** * Show embryo image in Step 2 and then trigger detection */ export async function showEmbryoImageAndDetect() { // First, display the original embryo image const step2Content = document.getElementById('step2'); if (!step2Content || !step2Content.classList.contains('active')) { console.error('Step 2 not active yet, waiting...'); await new Promise(resolve => setTimeout(resolve, 200)); } const canvas = document.getElementById('detectionCanvas'); // Show the original embryo image on the canvas first if (canvas && appState.currentImage) { try { const ctx = canvas.getContext('2d'); const { loadImage } = await import('../utils/imageUtils.js'); const imageElement = await loadImage(appState.currentImage); // Set canvas size to match image canvas.width = imageElement.width; canvas.height = imageElement.height; // Calculate scale factor for display const container = canvas.parentElement; if (container) { const maxWidth = container.clientWidth - 40; const maxHeight = 600; const scaleFactor = Math.min( maxWidth / imageElement.width, maxHeight / imageElement.height, 1 ); canvas.style.width = `${imageElement.width * scaleFactor}px`; canvas.style.height = `${imageElement.height * scaleFactor}px`; } // Draw the original image ctx.drawImage(imageElement, 0, 0); } catch (error) { console.error('Error displaying embryo image:', error); } } // Wait a moment to let the UI update and show the image await new Promise(resolve => setTimeout(resolve, 500)); // Now trigger detection await detectAndDisplayEmbryos(); } /** * Detect and display embryos (triggered on Step 2) */ export async function detectAndDisplayEmbryos() { if (!isYOLOAvailable()) { showToast('Detection model not available', 'error'); return; } // Verify Step 2 is visible const step2Content = document.getElementById('step2'); if (!step2Content || !step2Content.classList.contains('active')) { console.error('Step 2 not active yet, waiting...'); // Wait and retry await new Promise(resolve => setTimeout(resolve, 200)); } try { showToast('Detecting embryos...', 'info'); const detections = await detectEmbryos(appState.currentImage); if (detections.length === 0) { showToast('No embryos detected - You can add manually', 'info'); // Initialize with empty array to allow manual addition setCroppedEmbryos([]); await displayDetectedEmbryosForSelection([]); return; } setCroppedEmbryos(detections); await displayDetectedEmbryosForSelection(detections); showToast(`Detected ${detections.length} embryo(s) - Click to select`, 'success'); } catch (error) { console.error('Detection error:', error); showToast(`Detection failed: ${error.message}`, 'error'); } } /** * Display detected embryos for selection (Step 2) - Using inline detection view */ async function displayDetectedEmbryosForSelection(detections) { // Wait a bit for DOM to be ready (Step 2 to be visible) await new Promise(resolve => setTimeout(resolve, 100)); // Initialize the inline detection canvas with all embryos (or empty array) await initializeInlineDetection(appState.currentImage, detections); // Enable next button only if there are embryos (either detected or manually added) if (detections.length > 0) { enableNextButton(2); } } /** * Select embryo for analysis (updates selection in Step 2) */ function selectEmbryoForAnalysis(index) { setCurrentEmbryoIndex(index); displayCroppedEmbryo(index, appState.croppedEmbryos[index], appState.croppedEmbryos.length); // Update active thumbnail document.querySelectorAll('.embryo-thumbnail-wrapper').forEach((wrapper, i) => { const thumbnail = wrapper.querySelector('.embryo-thumbnail'); if (thumbnail) { thumbnail.classList.toggle('active', i === index); } }); const selectionInfo = document.getElementById('selectionInfo'); if (selectionInfo) { selectionInfo.innerHTML = `

Embryo ${index + 1} of ${appState.croppedEmbryos.length} selected

Click 'Analyze Selected Embryo' to continue

`; } // Show crop editor for selected embryo showCropEditor(index); // Update navigation buttons updateNavigationButtons(); } /** * Navigate between embryos in Step 2 */ export function navigateEmbryo(direction) { const newIndex = appState.currentEmbryoIndex + direction; if (newIndex >= 0 && newIndex < appState.croppedEmbryos.length) { selectEmbryoForAnalysis(newIndex); } // Update navigation button states updateNavigationButtons(); } /** * Update navigation button states */ function updateNavigationButtons() { const prevBtn = document.getElementById('prevEmbryoBtn'); const nextBtn = document.getElementById('nextEmbryoBtn'); if (prevBtn) { prevBtn.disabled = appState.currentEmbryoIndex === 0; } if (nextBtn) { nextBtn.disabled = appState.currentEmbryoIndex >= appState.croppedEmbryos.length - 1; } } /** * Discard an embryo from the analysis */ async function discardEmbryo(index) { if (appState.croppedEmbryos.length <= 1) { showToast('Cannot discard the last embryo', 'warning'); return; } const confirmed = await showConfirmModal( `Are you sure you want to discard Embryo ${index + 1}?`, 'Discard Embryo' ); if (!confirmed) { return; } // Remove embryo from array appState.croppedEmbryos.splice(index, 1); // Adjust current index if necessary if (appState.currentEmbryoIndex >= appState.croppedEmbryos.length) { appState.currentEmbryoIndex = Math.max(0, appState.croppedEmbryos.length - 1); } // Update the display redisplayEmbryos(); showToast('Embryo discarded', 'success'); } /** * Redisplay all embryos after changes (e.g., discard) */ function redisplayEmbryos() { const thumbnailContainer = document.getElementById('embryoThumbnails'); if (!thumbnailContainer) return; thumbnailContainer.innerHTML = ''; appState.croppedEmbryos.forEach((detection, index) => { const thumbnailWrapper = document.createElement('div'); thumbnailWrapper.className = 'embryo-thumbnail-wrapper'; thumbnailWrapper.setAttribute('data-embryo-index', index); const thumbnail = document.createElement('div'); thumbnail.className = 'embryo-thumbnail'; if (index === appState.currentEmbryoIndex) thumbnail.classList.add('active'); const img = document.createElement('img'); img.src = detection.imageData; img.alt = `Embryo ${index + 1}`; // Add discard button with closure to capture current index const discardBtn = document.createElement('button'); discardBtn.className = 'discard-embryo-btn'; discardBtn.innerHTML = '×'; discardBtn.title = 'Discard this embryo'; discardBtn.addEventListener('click', ((currentIndex) => { return (e) => { e.stopPropagation(); discardEmbryo(currentIndex); }; })(index)); thumbnail.appendChild(img); thumbnail.appendChild(discardBtn); // Add click handler with closure to capture current index thumbnail.addEventListener('click', ((currentIndex) => { return () => selectEmbryoForAnalysis(currentIndex); })(index)); thumbnailWrapper.appendChild(thumbnail); thumbnailContainer.appendChild(thumbnailWrapper); }); // Select current embryo (index has been adjusted in discardEmbryo) if (appState.croppedEmbryos.length > 0) { selectEmbryoForAnalysis(appState.currentEmbryoIndex); } } /** * Evaluate all embryos in background (triggered when entering Step 3) */ export async function evaluateAllEmbryos() { const resultsDiv = document.getElementById('finalResults'); try { showToast('Processing all embryos...', 'info'); if (resultsDiv) { resultsDiv.innerHTML = `

Analyzing ${appState.croppedEmbryos.length} embryo(s)...

This may take a few moments

`; } // Process all embryos in parallel const evaluationPromises = appState.croppedEmbryos.map(async (embryo, index) => { try { const result = await evaluateEmbryo(embryo.imageData); return { index, embryo, result, success: true }; } catch (error) { console.error(`Error evaluating embryo ${index + 1}:`, error); return { index, embryo, result: null, success: false, error: error.message }; } }); const results = await Promise.all(evaluationPromises); // Store results in appState appState.embryoResults = results; // Display all embryos with their results displayAllEmbryosWithResults(results); // Mark step as completed markStepCompleted(3); const successCount = results.filter(r => r.success).length; if (successCount === results.length) { showToast(`Successfully processed all ${results.length} embryos`, 'success'); } else { showToast(`Processed ${successCount} of ${results.length} embryos (${results.length - successCount} failed)`, 'warning'); } } catch (error) { console.error('Evaluation error:', error); showToast(`Evaluation failed: ${error.message}`, 'error'); if (resultsDiv) { resultsDiv.innerHTML = `

Evaluation failed

${error.message}

`; } } } /** * Display all embryos with clickable thumbnails in Step 3 */ function displayAllEmbryosWithResults(results) { const resultsDiv = document.getElementById('finalResults'); if (!resultsDiv) return; resultsDiv.innerHTML = `

All Embryos Results

Click on any embryo to view its detailed results

`; const grid = document.getElementById('embryoResultsGrid'); results.forEach((resultData, index) => { const card = document.createElement('div'); card.className = 'embryo-result-card'; card.setAttribute('data-embryo-index', index); const hasRemark = getEmbryoRemark(index).trim().length > 0; if (resultData.success) { const grade = resultData.result.grade || 'Unknown'; const quality = resultData.result.quality || 'Unknown'; card.innerHTML = `
Embryo ${index + 1}
${grade}
Embryo ${index + 1} ${quality}
`; } else { card.innerHTML = `
Embryo ${index + 1}
Error
Embryo ${index + 1} Failed
`; } card.addEventListener('click', () => showEmbryoDetailedResult(index, resultData)); grid.appendChild(card); }); // Auto-select first embryo if (results.length > 0) { showEmbryoDetailedResult(0, results[0]); } } /** * Show detailed result for a specific embryo */ function showEmbryoDetailedResult(index, resultData) { const detailsDiv = document.getElementById('selectedEmbryoDetails'); if (!detailsDiv) return; // Update active card document.querySelectorAll('.embryo-result-card').forEach((card, i) => { card.classList.toggle('active', i === index); }); const currentRemark = getEmbryoRemark(index); if (!resultData.success) { detailsDiv.innerHTML = `

Embryo ${index + 1} - Processing Failed

${resultData.error || 'Unknown error occurred'}

`; // Add event listener to save remarks when changed const remarkTextarea = document.getElementById(`embryoRemark${index}`); if (remarkTextarea) { remarkTextarea.addEventListener('input', (e) => { setEmbryoRemark(index, e.target.value); }); } return; } const result = resultData.result; detailsDiv.innerHTML = `

Embryo ${index + 1} - Detailed Results

Embryo ${index + 1}
Quality Assessment: ${result.quality || 'Unknown'}
Predicted Grade: ${result.grade || 'Unknown'}
Actual Grade:
${result.confidence ? `
Confidence: ${(result.confidence * 100).toFixed(1)}%
` : ''} ${result.poorGoodConfidence ? `
Quality Confidence: ${(result.poorGoodConfidence * 100).toFixed(1)}%
` : ''}
`; // Add event listener to save remarks when changed const remarkTextarea = document.getElementById(`embryoRemark${index}`); if (remarkTextarea) { remarkTextarea.addEventListener('input', (e) => { setEmbryoRemark(index, e.target.value); }); } // Parse existing actualGrade if available const existingGrade = resultData.embryo.actualGrade || ''; if (existingGrade) { // Parse grade like "3AA" into number and letters const match = existingGrade.match(/^(\d)([A-C])([A-C])$/); if (match) { const numSelect = document.getElementById(`actualGradeNum${index}`); const letter1Select = document.getElementById(`actualGradeLetter1${index}`); const letter2Select = document.getElementById(`actualGradeLetter2${index}`); if (numSelect) numSelect.value = match[1]; if (letter1Select) letter1Select.value = match[2]; if (letter2Select) letter2Select.value = match[3]; } } // Function to update the combined grade const updateActualGrade = () => { const numSelect = document.getElementById(`actualGradeNum${index}`); const letter1Select = document.getElementById(`actualGradeLetter1${index}`); const letter2Select = document.getElementById(`actualGradeLetter2${index}`); const gradePreview = document.getElementById(`gradePreview${index}`); const num = numSelect?.value || ''; const letter1 = letter1Select?.value || ''; const letter2 = letter2Select?.value || ''; const combinedGrade = num + letter1 + letter2; // Update preview if (gradePreview) { gradePreview.textContent = combinedGrade || '-'; } // Save to embryo object const embryo = appState.croppedEmbryos[index]; if (embryo) { embryo.actualGrade = combinedGrade; } }; // Add event listeners to all three selects const numSelect = document.getElementById(`actualGradeNum${index}`); const letter1Select = document.getElementById(`actualGradeLetter1${index}`); const letter2Select = document.getElementById(`actualGradeLetter2${index}`); if (numSelect) { numSelect.addEventListener('change', updateActualGrade); } if (letter1Select) { letter1Select.addEventListener('change', updateActualGrade); } if (letter2Select) { letter2Select.addEventListener('change', updateActualGrade); } // Initial update updateActualGrade(); } /** * Legacy function - kept for compatibility but redirects to new function */ export async function evaluateSelectedEmbryo() { return evaluateAllEmbryos(); }