Spaces:
Running
Running
| <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> |