bitars / index.html
edmarffilho's picture
its not working - Initial Deployment
4060858 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Breast Ultrasound Analysis Assistant</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.dropzone {
border: 2px dashed #ccc;
border-radius: 8px;
transition: all 0.3s;
}
.dropzone.active {
border-color: #4f46e5;
background-color: #f0f7ff;
}
.lesion-marker {
position: absolute;
background-color: rgba(239, 68, 68, 0.2);
border: 2px dashed rgba(239, 68, 68, 0.9);
cursor: move;
min-width: 30px;
min-height: 30px;
touch-action: none;
transition: all 0.3s;
}
.lesion-marker.high-vascularity {
border-color: rgba(255, 0, 0, 0.9);
background-color: rgba(255, 0, 0, 0.2);
}
.lesion-marker.medium-vascularity {
border-color: rgba(255, 165, 0, 0.9);
background-color: rgba(255, 165, 0, 0.2);
}
.lesion-marker::after {
content: attr(data-size);
position: absolute;
bottom: -20px;
left: 0;
right: 0;
text-align: center;
font-size: 12px;
color: white;
background: rgba(0,0,0,0.7);
padding: 2px;
}
.lesion-marker .resize-handle {
position: absolute;
width: 10px;
height: 10px;
background: rgba(239, 68, 68, 0.9);
border-radius: 50%;
right: -5px;
bottom: -5px;
cursor: nwse-resize;
}
.report-section {
transition: all 0.3s;
}
.report-section:hover {
background-color: #f8fafc;
}
#ultrasoundImageContainer {
position: relative;
overflow: hidden;
}
.birads-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-weight: bold;
text-transform: uppercase;
font-size: 0.75rem;
}
.birads-0 { background-color: #d1d5db; color: #111827; }
.birads-1 { background-color: #10b981; color: white; }
.birads-2 { background-color: #3b82f6; color: white; }
.birads-3 { background-color: #f59e0b; color: white; }
.birads-4 { background-color: #ef4444; color: white; }
.birads-5 { background-color: #991b1b; color: white; }
.birads-6 { background-color: #7e22ce; color: white; }
</style>
</head>
<body class="bg-gray-50 font-sans">
<div class="container mx-auto px-4 py-8">
<!-- Header -->
<header class="mb-8">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-indigo-800">Breast Ultrasound Analysis Assistant</h1>
<p class="text-gray-600">AI-powered tool for lesion detection and reporting</p>
</div>
<div class="flex items-center space-x-4">
<button class="bg-indigo-100 text-indigo-700 px-4 py-2 rounded-lg flex items-center">
<i class="fas fa-user-md mr-2"></i> Dr. Smith
</button>
<button class="bg-indigo-600 text-white px-4 py-2 rounded-lg flex items-center hover:bg-indigo-700 transition">
<i class="fas fa-sign-out-alt mr-2"></i> Logout
</button>
</div>
</div>
</header>
<!-- Main Content -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Left Panel - Image Upload and Analysis -->
<div class="lg:col-span-2 bg-white rounded-xl shadow-md overflow-hidden">
<div class="p-6 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-800">Upload Ultrasound Images</h2>
<p class="text-gray-500 text-sm mt-1">Drag & drop DICOM or JPEG images for analysis</p>
</div>
<!-- Dropzone -->
<div id="dropzone" class="dropzone m-6 p-12 text-center cursor-pointer">
<div class="flex flex-col items-center justify-center">
<i class="fas fa-cloud-upload-alt text-4xl text-indigo-500 mb-4"></i>
<p class="text-gray-600 mb-2">Drag & drop ultrasound images here</p>
<p class="text-gray-400 text-sm mb-4">or click to browse files</p>
<input type="file" id="fileInput" class="hidden" accept=".dcm,.jpeg,.jpg,.png" multiple>
<button id="browseBtn" class="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition">
Select Files
</button>
</div>
</div>
<!-- Image Display and Annotation -->
<div id="ultrasoundImageContainer" class="m-6 hidden">
<div class="flex justify-between items-center mb-4">
<h3 class="font-medium text-gray-700">Image Analysis</h3>
<div class="flex space-x-2">
<button id="prevBtn" class="bg-gray-200 text-gray-700 p-2 rounded hover:bg-gray-300">
<i class="fas fa-chevron-left"></i>
</button>
<span id="imageCounter" class="px-3 py-1 bg-gray-100 rounded">1/1</span>
<button id="nextBtn" class="bg-gray-200 text-gray-700 p-2 rounded hover:bg-gray-300">
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
<div class="relative bg-gray-100 rounded-lg overflow-hidden" style="min-height: 400px;">
<img id="ultrasoundImage" src="" alt="Ultrasound Image" class="w-full h-auto max-h-[500px] object-contain mx-auto">
<!-- Lesion markers will be added here dynamically -->
</div>
<div class="mt-4 flex flex-wrap gap-2">
<button id="autoDetectBtn" class="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition flex items-center">
<i class="fas fa-robot mr-2"></i> Auto-detect Lesions
</button>
<button id="addMarkerBtn" class="bg-gray-200 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-300 transition flex items-center">
<i class="fas fa-plus-circle mr-2"></i> Add Marker
</button>
<button id="clearMarkersBtn" class="bg-gray-200 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-300 transition flex items-center">
<i class="fas fa-trash-alt mr-2"></i> Clear Markers
</button>
</div>
</div>
<!-- Analysis Results -->
<div id="analysisResults" class="m-6 hidden">
<h3 class="font-medium text-gray-700 mb-3">Preliminary Analysis</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-blue-50 p-4 rounded-lg">
<div class="flex items-center mb-2">
<i class="fas fa-check-circle text-blue-500 mr-2"></i>
<span class="font-medium">Lesion Detection</span>
</div>
<p class="text-sm text-gray-600"><span id="lesionCount">0</span> potential lesions identified</p>
</div>
<div class="bg-purple-50 p-4 rounded-lg">
<div class="flex items-center mb-2">
<i class="fas fa-chart-bar text-purple-500 mr-2"></i>
<span class="font-medium">Size Measurement</span>
</div>
<p class="text-sm text-gray-600">Largest lesion: <span id="largestLesion">-</span> mm</p>
</div>
<div class="bg-green-50 p-4 rounded-lg">
<div class="flex items-center mb-2">
<i class="fas fa-shapes text-green-500 mr-2"></i>
<span class="font-medium">Shape Analysis</span>
</div>
<p class="text-sm text-gray-600" id="shapeAnalysis">-</p>
</div>
<div class="bg-yellow-50 p-4 rounded-lg">
<div class="flex items-center mb-2">
<i class="fas fa-bullseye text-yellow-500 mr-2"></i>
<span class="font-medium">Margin Assessment</span>
</div>
<p class="text-sm text-gray-600" id="marginAnalysis">-</p>
</div>
<div class="bg-pink-50 p-4 rounded-lg">
<div class="flex items-center mb-2">
<i class="fas fa-arrows-alt-h text-pink-500 mr-2"></i>
<span class="font-medium">Orientation</span>
</div>
<p class="text-sm text-gray-600" id="orientationAnalysis">-</p>
</div>
<div class="bg-teal-50 p-4 rounded-lg">
<div class="flex items-center mb-2">
<i class="fas fa-wave-square text-teal-500 mr-2"></i>
<span class="font-medium">Posterior Features</span>
</div>
<p class="text-sm text-gray-600" id="posteriorAnalysis">-</p>
</div>
</div>
</div>
</div>
<!-- Right Panel - Report Generation -->
<div class="bg-white rounded-xl shadow-md overflow-hidden">
<div class="p-6 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-800">Report Generator</h2>
<p class="text-gray-500 text-sm mt-1">Create a comprehensive ultrasound report</p>
</div>
<div class="p-6">
<!-- Patient Information -->
<div class="report-section mb-6 p-4 border border-gray-200 rounded-lg">
<div class="flex justify-between items-center mb-2">
<h3 class="font-medium text-gray-700">Patient Information</h3>
<button class="text-indigo-600 hover:text-indigo-800">
<i class="fas fa-edit"></i>
</button>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm text-gray-500">Patient ID</label>
<input type="text" class="w-full px-3 py-2 border border-gray-300 rounded mt-1" value="US-2023-00142">
</div>
<div>
<label class="block text-sm text-gray-500">Age</label>
<input type="text" class="w-full px-3 py-2 border border-gray-300 rounded mt-1" value="47">
</div>
<div>
<label class="block text-sm text-gray-500">Gender</label>
<select class="w-full px-3 py-2 border border-gray-300 rounded mt-1">
<option>Female</option>
<option>Male</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-500">Laterality</label>
<select class="w-full px-3 py-2 border border-gray-300 rounded mt-1">
<option>Right</option>
<option selected>Left</option>
<option>Bilateral</option>
</select>
</div>
</div>
</div>
<!-- Findings -->
<div class="report-section mb-6 p-4 border border-gray-200 rounded-lg">
<div class="flex justify-between items-center mb-2">
<h3 class="font-medium text-gray-700">Findings</h3>
<button class="text-indigo-600 hover:text-indigo-800">
<i class="fas fa-edit"></i>
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm text-gray-500">Number of Lesions</label>
<input type="number" id="reportLesionCount" class="w-full px-3 py-2 border border-gray-300 rounded mt-1" value="0" min="0">
</div>
<div>
<label class="block text-sm text-gray-500">Largest Lesion Size (mm)</label>
<input type="number" id="reportLargestLesion" class="w-full px-3 py-2 border border-gray-300 rounded mt-1" value="0" min="0" step="0.1">
</div>
<div>
<label class="block text-sm text-gray-500">Shape</label>
<select id="reportShape" class="w-full px-3 py-2 border border-gray-300 rounded mt-1">
<option value="">Select shape</option>
<option value="Oval">Oval</option>
<option value="Round">Round</option>
<option value="Irregular">Irregular</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-500">Margin</label>
<select id="reportMargin" class="w-full px-3 py-2 border border-gray-300 rounded mt-1">
<option value="">Select margin</option>
<option value="Circumscribed">Circumscribed</option>
<option value="Microlobulated">Microlobulated</option>
<option value="Indistinct">Indistinct</option>
<option value="Angular">Angular</option>
<option value="Spiculated">Spiculated</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-500">Echogenicity</label>
<select id="reportEchogenicity" class="w-full px-3 py-2 border border-gray-300 rounded mt-1">
<option value="">Select echogenicity</option>
<option value="Anechoic">Anechoic (fluid-filled)</option>
<option value="Hyperechoic">Hyperechoic (brighter than fat)</option>
<option value="Isoechoic">Isoechoic (same as fat)</option>
<option value="Hypoechoic">Hypoechoic (darker than fat)</option>
<option value="Complex">Complex (mixed components)</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-500">Architectural Distortion</label>
<select id="reportArchitecture" class="w-full px-3 py-2 border border-gray-300 rounded mt-1">
<option value="">Select architecture</option>
<option value="None">None</option>
<option value="Mild">Mild</option>
<option value="Moderate">Moderate</option>
<option value="Severe">Severe</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-500">Calcifications</label>
<select id="reportCalcifications" class="w-full px-3 py-2 border border-gray-300 rounded mt-1">
<option value="">Select calcifications</option>
<option value="None">None</option>
<option value="Macrocalcifications">Macrocalcifications (>0.5mm)</option>
<option value="Microcalcifications">Microcalcifications (≤0.5mm)</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-500">Orientation</label>
<select id="reportOrientation" class="w-full px-3 py-2 border border-gray-300 rounded mt-1">
<option value="">Select orientation</option>
<option value="Parallel">Parallel</option>
<option value="Antiparallel">Antiparallel</option>
</select>
</div>
<div>
<label class="block text-sm text-gray-500">Posterior Characteristics</label>
<select id="reportPosterior" class="w-full px-3 py-2 border border-gray-300 rounded mt-1">
<option value="">Select posterior feature</option>
<option value="Enhancement">Enhancement</option>
<option value="Shadowing">Shadowing</option>
<option value="No changes">No posterior changes</option>
</select>
</div>
</div>
</div>
<!-- BI-RADS Assessment -->
<div class="report-section mb-6 p-4 border border-gray-200 rounded-lg">
<div class="flex justify-between items-center mb-2">
<h3 class="font-medium text-gray-700">BI-RADS Assessment</h3>
<button class="text-indigo-600 hover:text-indigo-800">
<i class="fas fa-edit"></i>
</button>
</div>
<div class="space-y-3">
<div class="flex items-center">
<input type="radio" id="birads0" name="birads" value="0" class="mr-2">
<label for="birads0" class="flex items-center">
<span class="birads-badge birads-0 mr-2">0</span>
<span>Incomplete - Need additional imaging</span>
</label>
</div>
<div class="flex items-center">
<input type="radio" id="birads1" name="birads" value="1" class="mr-2">
<label for="birads1" class="flex items-center">
<span class="birads-badge birads-1 mr-2">1</span>
<span>Negative</span>
</label>
</div>
<div class="flex items-center">
<input type="radio" id="birads2" name="birads" value="2" class="mr-2">
<label for="birads2" class="flex items-center">
<span class="birads-badge birads-2 mr-2">2</span>
<span>Benign</span>
</label>
</div>
<div class="flex items-center">
<input type="radio" id="birads3" name="birads" value="3" class="mr-2">
<label for="birads3" class="flex items-center">
<span class="birads-badge birads-3 mr-2">3</span>
<span>Probably benign</span>
</label>
</div>
<div class="flex items-center">
<input type="radio" id="birads4" name="birads" value="4" class="mr-2" checked>
<label for="birads4" class="flex items-center">
<span class="birads-badge birads-4 mr-2">4</span>
<span>Suspicious</span>
</label>
</div>
<div class="flex items-center">
<input type="radio" id="birads5" name="birads" value="5" class="mr-2">
<label for="birads5" class="flex items-center">
<span class="birads-badge birads-5 mr-2">5</span>
<span>Highly suggestive of malignancy</span>
</label>
</div>
<div class="flex items-center">
<input type="radio" id="birads6" name="birads" value="6" class="mr-2">
<label for="birads6" class="flex items-center">
<span class="birads-badge birads-6 mr-2">6</span>
<span>Known biopsy-proven malignancy</span>
</label>
</div>
</div>
</div>
<!-- Recommendations -->
<div class="report-section mb-6 p-4 border border-gray-200 rounded-lg">
<div class="flex justify-between items-center mb-2">
<h3 class="font-medium text-gray-700">Recommendations</h3>
<button class="text-indigo-600 hover:text-indigo-800">
<i class="fas fa-edit"></i>
</button>
</div>
<textarea id="recommendations" class="w-full px-3 py-2 border border-gray-300 rounded mt-1 h-24" placeholder="Enter recommendations...">Follow-up ultrasound in 6 months recommended. Consider biopsy for the suspicious lesion.</textarea>
</div>
<!-- Report Actions -->
<div class="flex flex-col space-y-3">
<button id="generateReportBtn" class="bg-indigo-600 text-white px-4 py-3 rounded-lg hover:bg-indigo-700 transition flex items-center justify-center">
<i class="fas fa-file-medical mr-2"></i> Generate Final Report
</button>
<button class="bg-gray-200 text-gray-700 px-4 py-3 rounded-lg hover:bg-gray-300 transition flex items-center justify-center">
<i class="fas fa-save mr-2"></i> Save Draft
</button>
<button class="bg-gray-200 text-gray-700 px-4 py-3 rounded-lg hover:bg-gray-300 transition flex items-center justify-center">
<i class="fas fa-print mr-2"></i> Print Report
</button>
</div>
</div>
</div>
</div>
<!-- Report Preview Modal -->
<div id="reportModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] overflow-auto">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-indigo-800">Breast Ultrasound Report</h2>
<button id="closeModalBtn" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="border-b border-gray-200 pb-4 mb-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<p class="text-sm text-gray-500">Patient ID</p>
<p class="font-medium" id="modalPatientId">US-2023-00142</p>
</div>
<div>
<p class="text-sm text-gray-500">Age</p>
<p class="font-medium" id="modalPatientAge">47</p>
</div>
<div>
<p class="text-sm text-gray-500">Date</p>
<p class="font-medium" id="modalReportDate">June 15, 2023</p>
</div>
</div>
</div>
<div class="mb-6">
<h3 class="text-lg font-semibold mb-2">Findings</h3>
<div id="modalFindings" class="text-gray-700">
<p>Ultrasound examination of the left breast reveals:</p>
<ul class="list-disc pl-5 mt-2 space-y-1">
<li>1 hypoechoic lesion measuring 8.2 mm</li>
<li>Irregular shape with indistinct margins</li>
<li>Posterior acoustic shadowing present</li>
</ul>
</div>
</div>
<div class="mb-6">
<h3 class="text-lg font-semibold mb-2">Impression</h3>
<div id="modalImpression" class="text-gray-700">
<p>BI-RADS <span class="birads-badge birads-4">4</span>: Suspicious abnormality - biopsy should be considered.</p>
</div>
</div>
<div>
<h3 class="text-lg font-semibold mb-2">Recommendations</h3>
<div id="modalRecommendations" class="text-gray-700">
<p>1. Ultrasound-guided core needle biopsy of the suspicious lesion.</p>
<p>2. Follow-up imaging in 6 months if biopsy is not performed.</p>
</div>
</div>
<div class="mt-8 pt-4 border-t border-gray-200">
<div class="flex justify-between">
<div>
<p class="text-sm text-gray-500">Radiologist</p>
<p class="font-medium">Dr. Smith</p>
</div>
<div class="text-right">
<p class="text-sm text-gray-500">Date Signed</p>
<p class="font-medium">June 15, 2023</p>
</div>
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button class="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition">
<i class="fas fa-print mr-2"></i> Print
</button>
<button class="bg-gray-200 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-300 transition">
<i class="fas fa-download mr-2"></i> Download PDF
</button>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
try {
// DOM Elements
const dropzone = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
const browseBtn = document.getElementById('browseBtn');
const ultrasoundImageContainer = document.getElementById('ultrasoundImageContainer');
const ultrasoundImage = document.getElementById('ultrasoundImage');
const analysisResults = document.getElementById('analysisResults');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const imageCounter = document.getElementById('imageCounter');
const autoDetectBtn = document.getElementById('autoDetectBtn');
const addMarkerBtn = document.getElementById('addMarkerBtn');
const clearMarkersBtn = document.getElementById('clearMarkersBtn');
const lesionCount = document.getElementById('lesionCount');
const largestLesion = document.getElementById('largestLesion');
const shapeAnalysis = document.getElementById('shapeAnalysis');
const marginAnalysis = document.getElementById('marginAnalysis');
const generateReportBtn = document.getElementById('generateReportBtn');
const reportModal = document.getElementById('reportModal');
const closeModalBtn = document.getElementById('closeModalBtn');
// Variables
let uploadedImages = [];
let currentImageIndex = 0;
let markers = [];
let isAddingMarker = false;
// Event Listeners
browseBtn.addEventListener('click', function() {
fileInput.click();
});
fileInput.addEventListener('change', function(e) {
handleFileSelect(e);
});
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('active');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('active');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('active');
try {
const files = e.dataTransfer.files;
if (!files || files.length === 0) {
console.log('No files dropped');
return;
}
// Create new DataTransfer and add files
const dataTransfer = new DataTransfer();
Array.from(files).forEach(file => {
if (file.type.match('image.*') || file.name.match(/\.(jpe?g|png|dcm)$/i)) {
dataTransfer.items.add(file);
}
});
if (dataTransfer.files.length === 0) {
alert('Please drop valid image files (JPEG, PNG, or DICOM)');
return;
}
fileInput.files = dataTransfer.files;
handleFileSelect({ target: fileInput });
} catch (error) {
console.error('Error handling dropped files:', error);
alert('Error processing dropped files. Please try again.');
}
});
prevBtn.addEventListener('click', showPreviousImage);
nextBtn.addEventListener('click', showNextImage);
autoDetectBtn.addEventListener('click', autoDetectLesions);
addMarkerBtn.addEventListener('click', toggleMarkerMode);
clearMarkersBtn.addEventListener('click', clearMarkers);
generateReportBtn.addEventListener('click', showReportModal);
closeModalBtn.addEventListener('click', () => reportModal.classList.add('hidden'));
// Functions
function handleFileSelect(event) {
try {
const files = event.target.files;
if (!files || files.length === 0) {
console.error('No files selected');
return;
}
// Convert FileList to array and filter valid images
const imageFiles = Array.from(files).filter(file => {
const validTypes = ['image/jpeg', 'image/png', 'image/dicom', 'application/dicom'];
const validExtensions = /\.(jpe?g|png|dcm)$/i;
return validTypes.includes(file.type) || validExtensions.test(file.name);
});
if (imageFiles.length === 0) {
alert('Please select valid image files (JPEG, PNG, or DICOM)');
return;
}
uploadedImages = imageFiles;
currentImageIndex = 0;
// Display first image
displayImage(uploadedImages[0]);
// Show UI elements
ultrasoundImageContainer.classList.remove('hidden');
analysisResults.classList.remove('hidden');
// Update counter
updateImageCounter();
} catch (error) {
console.error('Error handling file selection:', error);
alert('Error processing files. Please try again.');
} finally {
// Reset input
event.target.value = '';
}
}
function displayImage(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = function(e) {
try {
ultrasoundImage.src = e.target.result;
clearMarkers();
simulateAnalysisResults();
resolve();
} catch (error) {
reject(error);
}
};
reader.onerror = function(error) {
reject(new Error('Failed to read file'));
};
try {
reader.readAsDataURL(file);
} catch (error) {
reject(error);
}
});
}
function showPreviousImage() {
if (currentImageIndex > 0) {
currentImageIndex--;
displayImage(uploadedImages[currentImageIndex]);
updateImageCounter();
}
}
function showNextImage() {
if (currentImageIndex < uploadedImages.length - 1) {
currentImageIndex++;
displayImage(uploadedImages[currentImageIndex]);
updateImageCounter();
}
}
function updateImageCounter() {
imageCounter.textContent = `${currentImageIndex + 1}/${uploadedImages.length}`;
}
function autoDetectLesions() {
// Simulate AI detection
const numLesions = Math.floor(Math.random() * 3) + 1; // 1-3 lesions
// Clear existing markers
clearMarkers();
// Add new markers
for (let i = 0; i < numLesions; i++) {
const x = 20 + Math.random() * 60; // 20-80% of width
const y = 20 + Math.random() * 60; // 20-80% of height
addMarker(x, y);
}
// Update analysis results
lesionCount.textContent = numLesions;
largestLesion.textContent = (Math.random() * 15 + 3).toFixed(1); // 3-18mm
const shapes = ['Oval', 'Round', 'Irregular'];
shapeAnalysis.textContent = shapes[Math.floor(Math.random() * shapes.length)];
const margins = ['Circumscribed', 'Microlobulated', 'Indistinct', 'Angular', 'Spiculated'];
marginAnalysis.textContent = margins[Math.floor(Math.random() * margins.length)];
// New analysis features
const orientations = ['Parallel', 'Antiparallel'];
document.getElementById('orientationAnalysis').textContent = orientations[Math.floor(Math.random() * orientations.length)];
const posteriorFeatures = ['Enhancement', 'Shadowing', 'No changes'];
document.getElementById('posteriorAnalysis').textContent = posteriorFeatures[Math.floor(Math.random() * posteriorFeatures.length)];
// Update report fields
document.getElementById('reportLesionCount').value = numLesions;
document.getElementById('reportLargestLesion').value = largestLesion.textContent;
document.getElementById('reportShape').value = shapeAnalysis.textContent;
document.getElementById('reportMargin').value = marginAnalysis.textContent;
document.getElementById('reportEchogenicity').value = ['Anechoic', 'Hyperechoic', 'Hypoechoic', 'Complex'][Math.floor(Math.random() * 4)];
document.getElementById('reportOrientation').value = document.getElementById('orientationAnalysis').textContent;
document.getElementById('reportPosterior').value = document.getElementById('posteriorAnalysis').textContent;
}
function toggleMarkerMode() {
isAddingMarker = !isAddingMarker;
if (isAddingMarker) {
addMarkerBtn.classList.remove('bg-gray-200');
addMarkerBtn.classList.add('bg-indigo-600', 'text-white');
ultrasoundImageContainer.style.cursor = 'crosshair';
} else {
addMarkerBtn.classList.remove('bg-indigo-600', 'text-white');
addMarkerBtn.classList.add('bg-gray-200');
ultrasoundImageContainer.style.cursor = 'default';
}
}
function addMarker(x, y) {
const marker = document.createElement('div');
marker.className = 'lesion-marker';
// Position marker at click coordinates
const imgRect = ultrasoundImage.getBoundingClientRect();
const containerRect = ultrasoundImageContainer.getBoundingClientRect();
// Randomize initial size slightly (40-80px)
const size = 40 + Math.random() * 40;
marker.style.left = (x - size/2) + 'px';
marker.style.top = (y - size/2) + 'px';
marker.style.width = size + 'px';
marker.style.height = size + 'px';
// Initial size display
updateMarkerSize(marker);
// Add color coding based on position (for vascularity simulation)
const yRatio = (y - containerRect.top) / containerRect.height;
const vascularity = yRatio > 0.7 ? 'High' : (yRatio > 0.4 ? 'Medium' : 'Low');
marker.style.borderColor = yRatio > 0.7 ? 'rgba(255, 0, 0, 0.9)' :
yRatio > 0.4 ? 'rgba(255, 165, 0, 0.9)' : 'rgba(239, 68, 68, 0.9)';
marker.style.backgroundColor = yRatio > 0.7 ? 'rgba(255, 0, 0, 0.2)' :
yRatio > 0.4 ? 'rgba(255, 165, 0, 0.2)' : 'rgba(239, 68, 68, 0.2)';
// Make marker draggable and resizable
makeDraggable(marker);
makeResizable(marker);
// Add double-click handler to remove marker
marker.addEventListener('dblclick', (e) => {
e.stopPropagation();
marker.remove();
markers = markers.filter(m => m !== marker);
updateLesionCount();
});
ultrasoundImageContainer.querySelector('.relative').appendChild(marker);
markers.push(marker);
// Auto-analyze lesion characteristics
analyzeLesion(marker);
updateLesionCount();
}
function makeDraggable(element) {
let isDragging = false;
let startX, startY, startLeft, startTop;
element.addEventListener('mousedown', (e) => {
// Only start drag if not clicking on resize handle
if (e.target === element) {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = parseInt(element.style.left) || 0;
startTop = parseInt(element.style.top) || 0;
e.preventDefault();
}
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
element.style.left = (startLeft + dx) + 'px';
element.style.top = (startTop + dy) + 'px';
// Update size display
updateMarkerSize(element);
});
document.addEventListener('mouseup', () => {
isDragging = false;
analyzeLesion(element);
});
}
function makeResizable(element) {
// Add resize handle
const resizeHandle = document.createElement('div');
resizeHandle.className = 'resize-handle';
element.appendChild(resizeHandle);
let isResizing = false;
let startX, startY, startWidth, startHeight;
resizeHandle.addEventListener('mousedown', (e) => {
isResizing = true;
startX = e.clientX;
startY = e.clientY;
startWidth = parseInt(document.defaultView.getComputedStyle(element).width, 10);
startHeight = parseInt(document.defaultView.getComputedStyle(element).height, 10);
e.preventDefault();
e.stopPropagation();
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const width = startWidth + (e.clientX - startX);
const height = startHeight + (e.clientY - startY);
element.style.width = Math.max(30, width) + 'px';
element.style.height = Math.max(30, height) + 'px';
// Update size display
updateMarkerSize(element);
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
analyzeLesion(element);
}
});
}
function updateMarkerSize(marker) {
const widthMm = (marker.offsetWidth * 0.1).toFixed(1);
const heightMm = (marker.offsetHeight * 0.1).toFixed(1);
marker.dataset.size = `${widthMm}×${heightMm} mm`;
}
function analyzeLesion(marker) {
try {
if (!marker || typeof marker.getBoundingClientRect !== 'function') {
throw new Error('Invalid marker element provided');
}
const rect = marker.getBoundingClientRect();
const imgRect = ultrasoundImage.getBoundingClientRect();
if (!imgRect.width || !imgRect.height) {
throw new Error('Invalid ultrasound image dimensions');
}
const measurements = calculateMeasurements(rect, imgRect, marker);
const shapeAnalysis = classifyShape(rect);
const marginAnalysis = classifyMargin(rect, measurements.circularity);
const orientationAnalysis = classifyOrientation(rect, measurements.position);
const echogenicityAnalysis = classifyEchogenicity(
shapeAnalysis.shape,
marginAnalysis.margin,
measurements.sizeMm
);
const posteriorAnalysis = classifyPosteriorFeatures(
echogenicityAnalysis.echogenicity,
shapeAnalysis.shape,
measurements.sizeMm
);
const architectureAnalysis = classifyArchitecturalDistortion(
marginAnalysis.margin,
orientationAnalysis.orientation,
measurements.sizeMm,
measurements.position
);
const calcificationAnalysis = classifyCalcifications(
echogenicityAnalysis.echogenicity,
posteriorAnalysis.posterior,
measurements.sizeMm,
shapeAnalysis.shape
);
return {
measurements,
characteristics: {
shape: shapeAnalysis,
margin: marginAnalysis,
orientation: orientationAnalysis,
echogenicity: echogenicityAnalysis,
posterior: posteriorAnalysis,
architecture: architectureAnalysis,
calcifications: calcificationAnalysis
},
metadata: {
timestamp: new Date().toISOString(),
version: '2.0',
analysisType: 'automated'
}
};
} catch (error) {
console.error('Error in lesion analysis:', error);
return {
error: error.message,
timestamp: new Date().toISOString()
};
}
}
function calculateMeasurements(rect, imgRect, marker) {
const widthRatio = rect.width / imgRect.width;
const heightRatio = rect.height / imgRect.height;
const aspectRatio = rect.width / rect.height;
let sizeMm = 0;
if (marker.dataset && marker.dataset.size && marker.dataset.size.includes('×')) {
try {
const sizes = marker.dataset.size.split('×').map(s => parseFloat(s.trim()));
const validSizes = sizes.filter(s => !isNaN(s) && s > 0);
if (validSizes.length > 0) {
sizeMm = Math.max(...validSizes);
}
} catch (e) {
sizeMm = Math.max(rect.width, rect.height) * 0.1;
}
}
const xPos = (rect.left + rect.width/2 - imgRect.left) / imgRect.width;
const yPos = (rect.top + rect.height/2 - imgRect.top) / imgRect.height;
const area = rect.width * rect.height;
const perimeter = 2 * (rect.width + rect.height);
const circularity = (4 * Math.PI * area) / (perimeter * perimeter);
return {
widthRatio,
heightRatio,
aspectRatio,
sizeMm,
position: { xPos, yPos },
area,
perimeter,
circularity
};
}
function classifyShape(rect) {
const area = rect.width * rect.height;
const aspectRatio = Math.max(rect.width, rect.height) / Math.min(rect.width, rect.height);
const perimeter = 2 * (rect.width + rect.height);
const circularity = (4 * Math.PI * area) / (perimeter * perimeter);
let shape, confidence, suspicion;
if (circularity > 0.85 && aspectRatio < 1.2) {
shape = 'Round';
confidence = circularity;
suspicion = 'Low';
} else if (circularity > 0.7 && aspectRatio < 1.8) {
shape = 'Oval';
confidence = circularity;
suspicion = 'Low';
} else {
shape = 'Irregular';
confidence = 1 - circularity;
suspicion = 'Higher';
}
return {
shape,
confidence: Math.round(confidence * 100) / 100,
suspicion,
metrics: {
circularity: Math.round(circularity * 100) / 100,
aspectRatio: Math.round(aspectRatio * 100) / 100
}
};
}
function classifyMargin(rect, circularity) {
const irregularityFactor = Math.pow(1 - circularity, 1.5);
const convexHullArea = rect.width * rect.height * (1 + irregularityFactor * 0.8);
const solidity = (rect.width * rect.height) / convexHullArea;
const aspectRatio = Math.max(rect.width, rect.height) / Math.min(rect.width, rect.height);
const elongationPenalty = aspectRatio > 2 ? 0.1 : 0;
const adjustedSolidity = solidity - elongationPenalty;
let margin, suspicion;
if (adjustedSolidity > 0.88) {
margin = 'Circumscribed';
suspicion = 'Low';
} else if (adjustedSolidity > 0.72) {
margin = 'Microlobulated';
suspicion = 'Low-Intermediate';
} else if (adjustedSolidity > 0.55) {
margin = 'Angular';
suspicion = 'Intermediate';
} else if (adjustedSolidity > 0.35) {
margin = 'Indistinct';
suspicion = 'Intermediate-High';
} else {
margin = 'Spiculated';
suspicion = 'High';
}
return {
margin,
suspicion,
metrics: {
solidity: Math.round(adjustedSolidity * 100) / 100,
irregularityScore: Math.round(irregularityFactor * 100) / 100
}
};
}
function classifyOrientation(rect, position) {
const aspectRatio = rect.width / rect.height;
const heightRatio = rect.height / rect.width;
const skinProximity = position.yPos;
let orientation, suspicion, rationale;
if (heightRatio > 1.2 || (skinProximity < 0.3 && heightRatio> 1.0)) {
orientation = 'Non-Parallel';
suspicion = 'Higher';
rationale = heightRatio > 1.2 ? 'Vertical growth pattern' : 'Vertical near skin surface';
} else {
orientation = 'Parallel';
suspicion = 'Lower';
rationale = 'Horizontal growth pattern';
}
return {
orientation,
suspicion,
rationale,
metrics: {
aspectRatio: Math.round(aspectRatio * 100) / 100,
heightRatio: Math.round(heightRatio * 100) / 100,
skinProximity: Math.round(skinProximity * 100) / 100
}
};
}
function classifyEchogenicity(shape, margin, size) {
const echoTypes = {
'Anechoic': { probability: 0.15, suspicion: 'Very Low' },
'Hypoechoic': { probability: 0.45, suspicion: 'Variable' },
'Hyperechoic': { probability: 0.25, suspicion: 'Low' },
'Isoechoic': { probability: 0.10, suspicion: 'Low' },
'Complex': { probability: 0.05, suspicion: 'High' }
};
let adjustedProbabilities = JSON.parse(JSON.stringify(echoTypes));
if (shape === 'Irregular') {
adjustedProbabilities['Complex'].probability *= 2;
adjustedProbabilities['Hypoechoic'].probability *= 1.3;
}
if (size > 20) {
adjustedProbabilities['Complex'].probability *= 1.5;
}
if (margin === 'Spiculated') {
adjustedProbabilities['Hypoechoic'].probability *= 1.8;
}
const totalProb = Object.values(adjustedProbabilities).reduce((sum, item) => sum + item.probability, 0);
Object.keys(adjustedProbabilities).forEach(key => {
adjustedProbabilities[key].probability /= totalProb;
});
const random = Math.random();
let cumulative = 0;
for (const [type, data] of Object.entries(adjustedProbabilities)) {
cumulative += data.probability;
if (random <= cumulative) { return { echogenicity: type, suspicion: data.suspicion, confidence:
Math.round(data.probability * 100) / 100, note: 'Simulated based on morphological features' }; } } }
function classifyPosteriorFeatures(echogenicity, shape, size) { const posteriorTypes={ 'No changes' : {
baseProb: 0.5, suspicion: 'Neutral' }, 'Enhancement' : { baseProb: 0.3, suspicion: 'Lower'
}, 'Shadowing' : { baseProb: 0.15, suspicion: 'Higher' }, 'Combined' : { baseProb: 0.05,
suspicion: 'Variable' } }; let adjustedProb=JSON.parse(JSON.stringify(posteriorTypes)); if
(echogenicity==='Anechoic' ) { adjustedProb['Enhancement'].baseProb *=3;
adjustedProb['Shadowing'].baseProb *=0.2; } if (echogenicity==='Hyperechoic' ) {
adjustedProb['Shadowing'].baseProb *=2; } if (shape==='Irregular' ) { adjustedProb['Shadowing'].baseProb
*=1.5; adjustedProb['Combined'].baseProb *=2; } if (size> 15) {
adjustedProb['Shadowing'].baseProb *= 1.3;
}
const total = Object.values(adjustedProb).reduce((sum, item) => sum + item.baseProb, 0);
const random = Math.random() * total;
let cumulative = 0;
for (const [type, data] of Object.entries(adjustedProb)) {
cumulative += data.baseProb;
if (random <= cumulative) { return { posterior: type, suspicion: data.suspicion, rationale:
getPosteriorRationale(type, echogenicity, shape) }; } } } function getPosteriorRationale(posterior,
echo, shape) { const rationales={ 'Enhancement' : `Acoustic enhancement, consistent with ${echo}
echopattern`, 'Shadowing' : `Acoustic shadowing, may indicate ${shape} mass density`, 'No changes'
: 'No significant posterior acoustic changes' , 'Combined'
: 'Mixed posterior features requiring further evaluation' }; return rationales[posterior]; }
function classifyArchitecturalDistortion(margin, orientation, size, position) { let
distortionScore=0; let factors=[]; switch (margin) { case 'Spiculated' : distortionScore +=8;
factors.push('Spiculated margins'); break; case 'Angular' : distortionScore +=5;
factors.push('Angular margins'); break; case 'Indistinct' : distortionScore +=3;
factors.push('Indistinct margins'); break; case 'Microlobulated' : distortionScore +=1;
factors.push('Microlobulated margins'); break; } if (orientation==='Non-Parallel' ) {
distortionScore +=2; factors.push('Non-parallel orientation'); } if (size> 20) {
distortionScore += 1;
factors.push('Large size');
}
if (position.xPos < 0.2 || position.xPos> 0.8) {
distortionScore += 1;
factors.push('Peripheral location');
}
let distortion, suspicion;
if (distortionScore >= 8) {
distortion = 'Severe';
suspicion = 'High';
} else if (distortionScore >= 5) {
distortion = 'Moderate';
suspicion = 'Intermediate-High';
} else if (distortionScore >= 2) {
distortion = 'Mild';
suspicion = 'Intermediate';
} else {
distortion = 'None';
suspicion = 'Low';
}
return {
distortion,
suspicion,
factors,
score: distortionScore
};
}
function classifyCalcifications(echogenicity, posterior, size, shape) {
const calcTypes = {
'None': { baseProb: 0.75, suspicion: 'Neutral' },
'Macrocalcifications': { baseProb: 0.15, suspicion: 'Low' },
'Microcalcifications': { baseProb: 0.08, suspicion: 'Variable' },
'Rim calcifications': { baseProb: 0.02, suspicion: 'Low' }
};
let adjustedProb = JSON.parse(JSON.stringify(calcTypes));
if (echogenicity === 'Hyperechoic') {
adjustedProb['Macrocalcifications'].baseProb *= 3;
adjustedProb['Microcalcifications'].baseProb *= 2;
}
if (posterior === 'Shadowing') {
adjustedProb['Macrocalcifications'].baseProb *= 2.5;
}
if (shape === 'Round' && size < 10) { adjustedProb['Rim calcifications'].baseProb *=5; } if
(shape==='Irregular' ) { adjustedProb['Microcalcifications'].baseProb *=1.8; } const
total=Object.values(adjustedProb).reduce((sum, item)=> sum + item.baseProb, 0);
const random = Math.random() * total;
let cumulative = 0;
for (const [type, data] of Object.entries(adjustedProb)) {
cumulative += data.baseProb;
if (random <= cumulative) { return { calcifications: type, suspicion: data.suspicion, note:
getCalcificationNote(type, echogenicity, posterior) }; } } } function
getCalcificationNote(calcType, echo, posterior) { const notes={ 'None'
: 'No calcifications identified' , 'Macrocalcifications' : `Coarse calcifications,
typically benign. Correlates with ${echo} echogenicity`, 'Microcalcifications'
: 'Fine calcifications requiring correlation with mammography' , 'Rim calcifications'
: 'Peripheral calcifications, likely in cyst wall' }; return notes[calcType]; }
// Update analysis display
shapeAnalysis.textContent = shape;
marginAnalysis.textContent = margin;
document.getElementById('orientationAnalysis').textContent = orientation;
document.getElementById('posteriorAnalysis').textContent = posterior;
// Update report fields
document.getElementById('reportShape').value = shape;
document.getElementById('reportMargin').value = margin;
document.getElementById('reportEchogenicity').value = echogenicity;
document.getElementById('reportOrientation').value = orientation;
document.getElementById('reportPosterior').value = posterior;
document.getElementById('reportArchitecture').value = architecture;
document.getElementById('reportCalcifications').value = calcifications;
// Update largest lesion size
largestLesion.textContent = sizeMm.toFixed(1);
document.getElementById('reportLargestLesion').value = sizeMm.toFixed(1);
}
function clearMarkers() {
markers.forEach(marker => marker.remove());
markers = [];
updateLesionCount();
}
function updateLesionCount() {
lesionCount.textContent = markers.length;
// For demo, update report field
document.getElementById('reportLesionCount').value = markers.length;
}
function simulateAnalysisResults() {
// Random results for demo
const hasLesions = Math.random() > 0.3; // 70% chance of lesions
if (hasLesions) {
const numLesions = Math.floor(Math.random() * 3) + 1; // 1-3 lesions
lesionCount.textContent = numLesions;
largestLesion.textContent = (Math.random() * 15 + 3).toFixed(1); // 3-18mm
const shapes = ['Oval', 'Round', 'Irregular'];
shapeAnalysis.textContent = shapes[Math.floor(Math.random() * shapes.length)];
const margins = ['Circumscribed', 'Microlobulated', 'Indistinct', 'Angular', 'Spiculated'];
marginAnalysis.textContent = margins[Math.floor(Math.random() * margins.length)];
} else {
lesionCount.textContent = '0';
largestLesion.textContent = '-';
shapeAnalysis.textContent = 'No significant lesions detected';
marginAnalysis.textContent = '-';
}
}
// Handle click on image to add marker
ultrasoundImageContainer.addEventListener('click', (e) => {
if (!isAddingMarker || e.target.classList.contains('lesion-marker')) return;
const container = ultrasoundImageContainer.querySelector('.relative');
const rect = container.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
addMarker(x, y);
});
function showReportModal() {
// Update modal content with current report data
document.getElementById('modalPatientId').textContent = document.querySelector('input[type="text"]').value;
document.getElementById('modalPatientAge').textContent = document.querySelector('input[value="47"]').value;
const today = new Date();
document.getElementById('modalReportDate').textContent = today.toLocaleDateString('en-US', {
year: 'numeric', month: 'long', day: 'numeric'
});
// Update findings
const findingsEl = document.getElementById('modalFindings');
findingsEl.innerHTML = `
<p>Ultrasound examination of the left breast reveals:</p>
<ul class="list-disc pl-5 mt-2 space-y-1">
<li>${document.getElementById('reportLesionCount').value} ${document.getElementById('reportEchogenicity').value.toLowerCase()} lesion(s)</li>
<li>Largest lesion measures ${document.getElementById('reportLargestLesion').value} mm</li>
<li>${document.getElementById('reportShape').value} shape with ${document.getElementById('reportMargin').value.toLowerCase()} margins</li>
<li>Orientation: ${document.getElementById('reportOrientation').value.toLowerCase()}</li>
<li>Posterior features: ${document.getElementById('reportPosterior').value.toLowerCase()}</li>
<li>Architectural distortion: ${document.getElementById('reportArchitecture').value.toLowerCase()}</li>
<li>Calcifications: ${document.getElementById('reportCalcifications').value.toLowerCase()}</li>
</ul>
`;
// Update impression with BI-RADS
const biradsValue = document.querySelector('input[name="birads"]:checked').value;
const biradsText = {
'0': 'Incomplete - Need additional imaging',
'1': 'Negative',
'2': 'Benign',
'3': 'Probably benign',
'4': 'Suspicious abnormality - biopsy should be considered',
'5': 'Highly suggestive of malignancy',
'6': 'Known biopsy-proven malignancy'
}[biradsValue];
document.getElementById('modalImpression').innerHTML = `
<p>BI-RADS <span class="birads-badge birads-${biradsValue}">${biradsValue}</span>: ${biradsText}.</p>
`;
// Update recommendations
document.getElementById('modalRecommendations').innerHTML = `
<p>${document.getElementById('recommendations').value}</p>
`;
// Show modal
reportModal.classList.remove('hidden');
}
} catch (error) {
console.error('Application initialization error:', error);
alert('An error occurred while initializing the application. Please refresh the page.');
}
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=edmarffilho/bitars" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>