Embryo-One's picture
Upload 49 files
ed9f15f verified
/**
* 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 = `
<div style="text-align: center;">
<div class="spinner" style="width: 30px; height: 30px; margin: 10px auto;"></div>
<p>Analyzing image...</p>
</div>
`;
}
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 = `
<p style="color: var(--warning-color);"><strong>No embryo detected</strong></p>
<p>Please upload an image containing an embryo to continue.</p>
`;
}
}
} catch (error) {
console.error('Classification error:', error);
showToast(`Classification failed: ${error.message}`, 'error');
if (statusDiv) {
statusDiv.innerHTML = `
<p style="color: var(--error-color);"><strong>Classification failed</strong></p>
<p>${error.message}</p>
`;
}
}
}
/**
* 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 = `
<p><strong>Embryo ${index + 1} of ${appState.croppedEmbryos.length} selected</strong></p>
<p>Click 'Analyze Selected Embryo' to continue</p>
`;
}
// 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 = `
<div style="text-align: center;">
<div class="spinner" style="width: 40px; height: 40px; margin: 20px auto;"></div>
<p>Analyzing ${appState.croppedEmbryos.length} embryo(s)...</p>
<p style="font-size: 0.9em; color: var(--text-secondary);">This may take a few moments</p>
</div>
`;
}
// 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 = `
<p style="color: var(--error-color);"><strong>Evaluation failed</strong></p>
<p>${error.message}</p>
`;
}
}
}
/**
* Display all embryos with clickable thumbnails in Step 3
*/
function displayAllEmbryosWithResults(results) {
const resultsDiv = document.getElementById('finalResults');
if (!resultsDiv) return;
resultsDiv.innerHTML = `
<div class="all-embryos-results">
<h3>All Embryos Results</h3>
<p style="margin-bottom: 20px; color: var(--text-secondary);">
Click on any embryo to view its detailed results
</p>
<div class="embryo-results-grid" id="embryoResultsGrid"></div>
<div class="selected-embryo-details" id="selectedEmbryoDetails" style="margin-top: 30px;"></div>
</div>
`;
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 = `
<div class="embryo-result-thumbnail">
<img src="${resultData.embryo.imageData}" alt="Embryo ${index + 1}">
<div class="embryo-result-badge">${grade}</div>
</div>
<div class="embryo-result-info">
<strong>Embryo ${index + 1}</strong>
<span class="quality-indicator quality-${quality.toLowerCase()}">${quality}</span>
</div>
`;
} else {
card.innerHTML = `
<div class="embryo-result-thumbnail">
<img src="${resultData.embryo.imageData}" alt="Embryo ${index + 1}">
<div class="embryo-result-badge error">Error</div>
</div>
<div class="embryo-result-info">
<strong>Embryo ${index + 1}</strong>
<span class="quality-indicator quality-error">Failed</span>
</div>
`;
}
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 = `
<div class="detailed-result-container">
<h3>Embryo ${index + 1} - Processing Failed</h3>
<div class="error-message">
<p>${resultData.error || 'Unknown error occurred'}</p>
</div>
<div class="remarks-section">
<label for="embryoRemark${index}">Remarks:</label>
<textarea
id="embryoRemark${index}"
class="remarks-textarea"
placeholder="Add your observations, notes, or comments about this embryo..."
data-embryo-index="${index}"
>${currentRemark}</textarea>
</div>
</div>
`;
// 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 = `
<div class="detailed-result-container">
<div class="result-header">
<h3>Embryo ${index + 1} - Detailed Results</h3>
</div>
<div class="result-content">
<div class="result-image-section">
<img src="${resultData.embryo.imageData}" alt="Embryo ${index + 1}" class="result-embryo-image">
</div>
<div class="result-details-section">
<div class="result-row">
<span class="result-label">Quality Assessment:</span>
<span class="result-value quality-${result.quality?.toLowerCase() || 'unknown'}">${result.quality || 'Unknown'}</span>
</div>
<div class="result-row">
<span class="result-label">Predicted Grade:</span>
<span class="result-value grade-value">${result.grade || 'Unknown'}</span>
</div>
<div class="result-row">
<span class="result-label">Actual Grade:</span>
<div class="actual-grade-selectors">
<select id="actualGradeNum${index}" class="grade-select" data-embryo-index="${index}">
<option value="">-</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
</select>
<select id="actualGradeLetter1${index}" class="grade-select" data-embryo-index="${index}">
<option value="">-</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</select>
<select id="actualGradeLetter2${index}" class="grade-select" data-embryo-index="${index}">
<option value="">-</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
</select>
<span class="grade-preview" id="gradePreview${index}"></span>
</div>
</div>
${result.confidence ? `
<div class="result-row">
<span class="result-label">Confidence:</span>
<span class="result-value">${(result.confidence * 100).toFixed(1)}%</span>
</div>
` : ''}
${result.poorGoodConfidence ? `
<div class="result-row">
<span class="result-label">Quality Confidence:</span>
<span class="result-value">${(result.poorGoodConfidence * 100).toFixed(1)}%</span>
</div>
` : ''}
<div class="remarks-section">
<label for="embryoRemark${index}">Remarks:</label>
<textarea
id="embryoRemark${index}"
class="remarks-textarea"
placeholder="Add your observations, notes, or comments about this embryo..."
data-embryo-index="${index}"
>${currentRemark}</textarea>
</div>
</div>
</div>
</div>
`;
// 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();
}