mindslabmamo / index.html
mabrokma's picture
Can you optimize the veiw of the app and make it looks fancy and allow the four views to be seen together ? - Follow Up Deployment
64c1112 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dual-View Mammogram Reporting Tool</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pako@2.1.0/dist/pako.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dicom-parser@1.8.15/dist/dicomParser.min.js"></script>
<style>
:root {
--primary-color: #2563eb;
--primary-hover: #1d4ed8;
--secondary-color: #64748b;
--accent-color: #f1f5f9;
--border-color: #e2e8f0;
--text-primary: #1e293b;
--text-secondary: #64748b;
}
.image-container {
position: relative;
overflow: hidden;
background-color: #000;
touch-action: none;
cursor: grab;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.image-container.grabbing {
cursor: grabbing;
}
.image-canvas {
display: block;
max-width: 100%;
max-height: 100%;
margin: 0 auto;
}
.slider-container {
position: relative;
padding: 0 0.5rem;
margin-bottom: 0.75rem;
}
.slider-container label {
display: block;
margin-bottom: 0.25rem;
font-size: 0.75rem;
color: var(--text-secondary);
font-weight: 500;
}
.slider-value {
position: absolute;
right: 0.5rem;
top: 0;
font-size: 0.7rem;
color: var(--text-secondary);
background: var(--accent-color);
padding: 0.1rem 0.4rem;
border-radius: 4px;
}
.form-section {
margin-bottom: 1.25rem;
border: 1px solid var(--border-color);
border-radius: 0.75rem;
overflow: hidden;
background: white;
}
.form-section-title {
background-color: var(--accent-color);
padding: 0.75rem 1rem;
font-weight: 600;
border-bottom: 1px solid var(--border-color);
color: var(--text-primary);
font-size: 0.95rem;
}
.form-section-content {
padding: 1rem;
}
.table-container {
overflow-x: auto;
border-radius: 0.5rem;
border: 1px solid var(--border-color);
}
.table-auto {
min-width: 100%;
border-collapse: collapse;
}
.table-auto th, .table-auto td {
padding: 0.6rem;
border: 1px solid var(--border-color);
font-size: 0.875rem;
}
.table-auto th {
background-color: var(--accent-color);
font-weight: 600;
text-align: left;
color: var(--text-primary);
}
.dicom-tag {
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
background-color: var(--accent-color);
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
font-size: 0.75rem;
color: var(--text-secondary);
}
.tab-button {
padding: 0.6rem 1.2rem;
border: 1px solid var(--border-color);
background-color: var(--accent-color);
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
transition: all 0.2s ease;
}
.tab-button:hover {
background-color: #e2e8f0;
}
.tab-button.active {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.viewer-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 2rem;
}
.viewer-panel {
background: white;
border-radius: 12px;
padding: 1.25rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border: 1px solid var(--border-color);
}
.viewer-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.viewer-title {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
}
.image-tools {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
margin-bottom: 1rem;
}
.tool-btn {
padding: 0.5rem;
border: 1px solid var(--border-color);
background: white;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
font-weight: 500;
transition: all 0.2s ease;
}
.tool-btn:hover {
background: var(--accent-color);
border-color: var(--primary-color);
}
.tool-btn.active {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
.open-btn {
background: var(--primary-color);
color: white;
padding: 0.6rem 1.2rem;
border: none;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.open-btn:hover {
background: var(--primary-hover);
}
@media (max-width: 1024px) {
.main-container {
flex-direction: column;
}
.viewer-grid {
grid-template-columns: 1fr;
}
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
h1, h2, h3 {
font-weight: 600;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-6">
<h1 class="text-3xl font-bold text-gray-800 mb-6">Dual-View Mammogram Reporting Tool</h1>
<div class="main-container flex flex-col xl:flex-row gap-8">
<!-- Left side - All four image viewers in grid -->
<div class="viewer-container flex-1">
<div class="viewer-grid">
<!-- Left CC View -->
<div class="viewer-panel">
<div class="viewer-header">
<h3 class="viewer-title">Left CC View</h3>
<button id="openBtnLeftCC" class="open-btn">
Open Image
</button>
</div>
<div class="image-tools">
<button class="tool-btn zoom-in-btn-left-cc">+ Zoom</button>
<button class="tool-btn zoom-out-btn-left-cc">- Zoom</button>
<button class="tool-btn pan-btn-left-cc">Pan</button>
<button class="tool-btn reset-btn-left-cc">Reset</button>
</div>
<div class="image-container mb-4" style="height: 280px;">
<canvas id="canvasLeftCC" class="image-canvas"></canvas>
</div>
<div class="slider-container">
<label for="levelSliderLeftCC">Window Level</label>
<span id="levelValueLeftCC" class="slider-value">2048</span>
<input type="range" id="levelSliderLeftCC" min="0" max="4095" value="2048" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
<div class="slider-container">
<label for="widthSliderLeftCC">Window Width</label>
<span id="widthValueLeftCC" class="slider-value">4095</span>
<input type="range" id="widthSliderLeftCC" min="1" max="4095" value="4095" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
<div class="dicom-info mt-4 p-3 bg-gray-100 rounded hidden" id="dicomInfoLeftCC">
<h3 class="font-semibold mb-2 text-sm">DICOM Information</h3>
<div class="grid grid-cols-2 gap-2 text-xs" id="dicomTagsLeftCC"></div>
</div>
</div>
<!-- Left MLO View -->
<div class="viewer-panel">
<div class="viewer-header">
<h3 class="viewer-title">Left MLO View</h3>
<button id="openBtnLeftMLO" class="open-btn">
Open Image
</button>
</div>
<div class="image-tools">
<button class="tool-btn zoom-in-btn-left-mlo">+ Zoom</button>
<button class="tool-btn zoom-out-btn-left-mlo">- Zoom</button>
<button class="tool-btn pan-btn-left-mlo">Pan</button>
<button class="tool-btn reset-btn-left-mlo">Reset</button>
</div>
<div class="image-container mb-4" style="height: 280px;">
<canvas id="canvasLeftMLO" class="image-canvas"></canvas>
</div>
<div class="slider-container">
<label for="levelSliderLeftMLO">Window Level</label>
<span id="levelValueLeftMLO" class="slider-value">2048</span>
<input type="range" id="levelSliderLeftMLO" min="0" max="4095" value="2048" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
<div class="slider-container">
<label for="widthSliderLeftMLO">Window Width</label>
<span id="widthValueLeftMLO" class="slider-value">4095</span>
<input type="range" id="widthSliderLeftMLO" min="1" max="4095" value="4095" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
<div class="dicom-info mt-4 p-3 bg-gray-100 rounded hidden" id="dicomInfoLeftMLO">
<h3 class="font-semibold mb-2 text-sm">DICOM Information</h3>
<div class="grid grid-cols-2 gap-2 text-xs" id="dicomTagsLeftMLO"></div>
</div>
</div>
<!-- Right CC View -->
<div class="viewer-panel">
<div class="viewer-header">
<h3 class="viewer-title">Right CC View</h3>
<button id="openBtnRightCC" class="open-btn">
Open Image
</button>
</div>
<div class="image-tools">
<button class="tool-btn zoom-in-btn-right-cc">+ Zoom</button>
<button class="tool-btn zoom-out-btn-right-cc">- Zoom</button>
<button class="tool-btn pan-btn-right-cc">Pan</button>
<button class="tool-btn reset-btn-right-cc">Reset</button>
</div>
<div class="image-container mb-4" style="height: 280px;">
<canvas id="canvasRightCC" class="image-canvas"></canvas>
</div>
<div class="slider-container">
<label for="levelSliderRightCC">Window Level</label>
<span id="levelValueRightCC" class="slider-value">2048</span>
<input type="range" id="levelSliderRightCC" min="0" max="4095" value="2048" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
<div class="slider-container">
<label for="widthSliderRightCC">Window Width</label>
<span id="widthValueRightCC" class="slider-value">4095</span>
<input type="range" id="widthSliderRightCC" min="1" max="4095" value="4095" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
<div class="dicom-info mt-4 p-3 bg-gray-100 rounded hidden" id="dicomInfoRightCC">
<h3 class="font-semibold mb-2 text-sm">DICOM Information</h3>
<div class="grid grid-cols-2 gap-2 text-xs" id="dicomTagsRightCC"></div>
</div>
</div>
<!-- Right MLO View -->
<div class="viewer-panel">
<div class="viewer-header">
<h3 class="viewer-title">Right MLO View</h3>
<button id="openBtnRightMLO" class="open-btn">
Open Image
</button>
</div>
<div class="image-tools">
<button class="tool-btn zoom-in-btn-right-mlo">+ Zoom</button>
<button class="tool-btn zoom-out-btn-right-mlo">- Zoom</button>
<button class="tool-btn pan-btn-right-mlo">Pan</button>
<button class="tool-btn reset-btn-right-mlo">Reset</button>
</div>
<div class="image-container mb-4" style="height: 280px;">
<canvas id="canvasRightMLO" class="image-canvas"></canvas>
</div>
<div class="slider-container">
<label for="levelSliderRightMLO">Window Level</label>
<span id="levelValueRightMLO" class="slider-value">2048</span>
<input type="range" id="levelSliderRightMLO" min="0" max="4095" value="2048" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
<div class="slider-container">
<label for="widthSliderRightMLO">Window Width</label>
<span id="widthValueRightMLO" class="slider-value">4095</span>
<input type="range" id="widthSliderRightMLO" min="1" max="4095" value="4095" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
</div>
<div class="dicom-info mt-4 p-3 bg-gray-100 rounded hidden" id="dicomInfoRightMLO">
<h3 class="font-semibold mb-2 text-sm">DICOM Information</h3>
<div class="grid grid-cols-2 gap-2 text-xs" id="dicomTagsRightMLO"></div>
</div>
</div>
</div>
</div>
<!-- Right side - Reporting form -->
<div class="form-container bg-white rounded-xl shadow-md p-6 flex-1 xl:max-w-2xl overflow-y-auto" style="max-height: 100vh;">
<div class="tabs flex border-b mb-4">
<button class="tab-button active" data-tab="patient">Patient Info</button>
<button class="tab-button" data-tab="left">Left Breast</button>
<button class="tab-button" data-tab="right">Right Breast</button>
<button class="tab-button" data-tab="reasoning">Reasoning</button>
<button class="tab-button" data-tab="final">Final</button>
</div>
<!-- Patient Info -->
<div id="patient-tab" class="tab-content active">
<div class="form-section">
<div class="form-section-title">Patient Information</div>
<div class="form-section-content">
<div class="mb-4">
<label for="patientId" class="block text-sm font-medium text-gray-700 mb-1">Patient ID</label>
<input type="text" id="patientId" class="w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
<div class="mb-4">
<label for="age" class="block text-sm font-medium text-gray-700 mb-1">Age</label>
<input type="text" id="age" class="w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
<div class="mb-4">
<label for="density" class="block text-sm font-medium text-gray-700 mb-1">Breast Density</label>
<select id="density" class="w-full px-3 py-2 border border-gray-300 rounded-md">
<option value=""></option>
<option value="A: Almost entirely fatty">A: Almost entirely fatty</option>
<option value="B: Scattered areas of fibroglandular density">B: Scattered areas of fibroglandular density</option>
<option value="C: Heterogeneously dense">C: Heterogeneously dense</option>
<option value="D: Extremely dense">D: Extremely dense</option>
</select>
</div>
</div>
</div>
</div>
<!-- Left Breast Findings -->
<div id="left-tab" class="tab-content">
<div class="form-section">
<div class="form-section-title">Left Breast Findings</div>
<div class="form-section-content">
<div class="mb-4">
<label for="leftLocation" class="block text-sm font-medium text-gray-700 mb-1">Lesion Location (M/W/Q)</label>
<input type="text" id="leftLocation" class="w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
<div class="mb-4">
<label for="leftType" class="block text-sm font-medium text-gray-700 mb-1">Lesion Type</label>
<select id="leftType" class="w-full px-3 py-2 border border-gray-300 rounded-md">
<option value=""></option>
<option value="Mass">Mass</option>
<option value="Calcification">Calcification</option>
<option value="Asymmetry">Asymmetry</option>
<option value="Architectural Distortion">Architectural Distortion</option>
<option value="Other">Other</option>
</select>
</div>
<div class="mb-4">
<label for="leftShape" class="block text-sm font-medium text-gray-700 mb-1">Lesion Shape</label>
<input type="text" id="leftShape" class="w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
<div class="mb-4">
<label for="leftMargins" class="block text-sm font-medium text-gray-700 mb-1">Lesion Margins</label>
<input type="text" id="leftMargins" class="w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
<div class="mb-4">
<label for="leftAssoc" class="block text-sm font-medium text-gray-700 mb-1">Associated Findings</label>
<textarea id="leftAssoc" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md"></textarea>
</div>
<div class="mb-4">
<label for="leftImpression" class="block text-sm font-medium text-gray-700 mb-1">Initial Impression</label>
<textarea id="leftImpression" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md"></textarea>
</div>
<div class="mb-4">
<label for="leftAction" class="block text-sm font-medium text-gray-700 mb-1">Recommended Action</label>
<select id="leftAction" class="w-full px-3 py-2 border border-gray-300 rounded-md">
<option value=""></option>
<option value="Assign BI-RADS">Assign BI-RADS</option>
<option value="Request additional view">Request additional view</option>
<option value="Request ultrasound">Request ultrasound</option>
<option value="Biopsy">Biopsy</option>
<option value="Short-term follow-up">Short-term follow-up</option>
</select>
</div>
<div class="mb-4">
<label for="leftBirads" class="block text-sm font-medium text-gray-700 mb-1">Final BI-RADS Category</label>
<select id="leftBirads" class="w-full px-3 py-2 border border-gray-300 rounded-md">
<option value=""></option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
</div>
<div class="mb-4">
<label for="leftRationale" class="block text-sm font-medium text-gray-700 mb-1">Rationale / Reasoning</label>
<textarea id="leftRationale" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md"></textarea>
</div>
</div>
</div>
</div>
<!-- Right Breast Findings -->
<div id="right-tab" class="tab-content">
<div class="form-section">
<div class="form-section-title">Right Breast Findings</div>
<div class="form-section-content">
<div class="mb-4">
<label for="rightLocation" class="block text-sm font-medium text-gray-700 mb-1">Lesion Location (M/W/Q)</label>
<input type="text" id="rightLocation" class="w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
<div class="mb-4">
<label for="rightType" class="block text-sm font-medium text-gray-700 mb-1">Lesion Type</label>
<select id="rightType" class="w-full px-3 py-2 border border-gray-300 rounded-md">
<option value=""></option>
<option value="Mass">Mass</option>
<option value="Calcification">Calcification</option>
<option value="Asymmetry">Asymmetry</option>
<option value="Architectural Distortion">Architectural Distortion</option>
<option value="Other">Other</option>
</select>
</div>
<div class="mb-4">
<label for="rightShape" class="block text-sm font-medium text-gray-700 mb-1">Lesion Shape</label>
<input type="text" id="rightShape" class="w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
<div class="mb-4">
<label for="rightMargins" class="block text-sm font-medium text-gray-700 mb-1">Lesion Margins</label>
<input type="text" id="rightMargins" class="w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
<div class="mb-4">
<label for="rightAssoc" class="block text-sm font-medium text-gray-700 mb-1">Associated Findings</label>
<textarea id="rightAssoc" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md"></textarea>
</div>
<div class="mb-4">
<label for="rightImpression" class="block text-sm font-medium text-gray-700 mb-1">Initial Impression</label>
<textarea id="rightImpression" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md"></textarea>
</div>
<div class="mb-4">
<label for="rightAction" class="block text-sm font-medium text-gray-700 mb-1">Recommended Action</label>
<select id="rightAction" class="w-full px-3 py-2 border border-gray-300 rounded-md">
<option value=""></option>
<option value="Assign BI-RADS">Assign BI-RADS</option>
<option value="Request additional view">Request additional view</option>
<option value="Request ultrasound">Request ultrasound</option>
<option value="Biopsy">Biopsy</option>
<option value="Short-term follow-up">Short-term follow-up</option>
</select>
</div>
<div class="mb-4">
<label for="rightBirads" class="block text-sm font-medium text-gray-700 mb-1">Final BI-RADS Category</label>
<select id="rightBirads" class="w-full px-3 py-2 border border-gray-300 rounded-md">
<option value=""></option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
</div>
<div class="mb-4">
<label for="rightRationale" class="block text-sm font-medium text-gray-700 mb-1">Rationale / Reasoning</label>
<textarea id="rightRationale" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md"></textarea>
</div>
</div>
</div>
</div>
<!-- Clinical Reasoning -->
<div id="reasoning-tab" class="tab-content">
<div class="form-section">
<div class="form-section-title">Clinical Reasoning Sequence</div>
<div class="form-section-content">
<div class="table-container">
<table class="table-auto">
<thead>
<tr>
<th>Step</th>
<th>Action Taken</th>
<th>Reasoning</th>
</tr>
</thead>
<tbody id="reasoningTableBody">
<!-- Rows will be added here -->
</tbody>
</table>
</div>
<button id="addReasoningStep" class="mt-4 bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">
Add Step
</button>
</div>
</div>
</div>
<!-- Final Recommendation -->
<div id="final-tab" class="tab-content">
<div class="form-section">
<div class="form-section-title">Final Recommendation</div>
<div class="form-section-content">
<div class="mb-4">
<label for="overallAssessment" class="block text-sm font-medium text-gray-700 mb-1">Overall Assessment</label>
<select id="overallAssessment" class="w-full px-3 py-2 border border-gray-300 rounded-md">
<option value=""></option>
<option value="Benign">Benign</option>
<option value="Suspicion of Malignancy">Suspicion of Malignancy</option>
</select>
</div>
<div class="mb-4">
<label for="nextStep" class="block text-sm font-medium text-gray-700 mb-1">Recommended Next Step</label>
<textarea id="nextStep" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md"></textarea>
</div>
</div>
</div>
<div class="mt-6">
<button id="saveReport" class="w-full bg-green-500 hover:bg-green-600 text-white px-4 py-3 rounded-lg font-semibold">
Save Report
</button>
</div>
</div>
</div>
</div>
</div>
<script>
// Global variables
let pixelArray1 = null, pixelArray2 = null;
let dicomData1 = null, dicomData2 = null;
let patientInfoLoaded = false;
let currentZoom1 = 1, currentZoom2 = 1;
let isPanning1 = false, isPanning2 = false;
let panStartX1 = 0, panStartY1 = 0, panStartX2 = 0, panStartY2 = 0;
let offsetX1 = 0, offsetY1 = 0, offsetX2 = 0, offsetY2 = 0;
// DOM elements
const canvas1 = document.getElementById('canvas1');
const canvas2 = document.getElementById('canvas2');
const ctx1 = canvas1.getContext('2d');
const ctx2 = canvas2.getContext('2d');
// Initialize canvas sizes
function initCanvasSize(canvas, container) {
const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
canvas.width = containerWidth;
canvas.height = containerHeight;
}
// Initialize canvas sizes on load
document.addEventListener('DOMContentLoaded', () => {
const container1 = document.querySelector('#canvas1').parentElement;
const container2 = document.querySelector('#canvas2').parentElement;
initCanvasSize(canvas1, container1);
initCanvasSize(canvas2, container2);
// Set up event listeners for window resize
window.addEventListener('resize', () => {
initCanvasSize(canvas1, container1);
initCanvasSize(canvas2, container2);
if (pixelArray1) updateImageDisplay(1);
if (pixelArray2) updateImageDisplay(2);
});
// Set up tab switching
document.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', () => {
const tabId = button.getAttribute('data-tab');
// Update active tab button
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
button.classList.add('active');
// Update active tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`${tabId}-tab`).classList.add('active');
});
});
// Set up sliders
setupSliders(1);
setupSliders(2);
// Set up image open buttons
document.getElementById('openBtn1').addEventListener('click', () => openImageFile(1));
document.getElementById('openBtn2').addEventListener('click', () => openImageFile(2));
// Set up zoom/pan buttons for viewer 1
document.querySelector('.zoom-in-btn1').addEventListener('click', () => zoomImage(1, 1.2));
document.querySelector('.zoom-out-btn1').addEventListener('click', () => zoomImage(1, 0.8));
document.querySelector('.pan-btn1').addEventListener('click', () => togglePanMode(1));
document.querySelector('.reset-btn1').addEventListener('click', () => resetView(1));
// Set up zoom/pan buttons for viewer 2
document.querySelector('.zoom-in-btn2').addEventListener('click', () => zoomImage(2, 1.2));
document.querySelector('.zoom-out-btn2').addEventListener('click', () => zoomImage(2, 0.8));
document.querySelector('.pan-btn2').addEventListener('click', () => togglePanMode(2));
document.querySelector('.reset-btn2').addEventListener('click', () => resetView(2));
// Set up mouse events for panning
setupPanningEvents(1);
setupPanningEvents(2);
// Set up reasoning table
document.getElementById('addReasoningStep').addEventListener('click', addReasoningStep);
// Set up save report button
document.getElementById('saveReport').addEventListener('click', saveReport);
});
function setupSliders(viewId) {
const levelSlider = document.getElementById(`levelSlider${viewId}`);
const widthSlider = document.getElementById(`widthSlider${viewId}`);
const levelValue = document.getElementById(`levelValue${viewId}`);
const widthValue = document.getElementById(`widthValue${viewId}`);
levelSlider.addEventListener('input', () => {
levelValue.textContent = levelSlider.value;
updateImageDisplay(viewId);
});
widthSlider.addEventListener('input', () => {
widthValue.textContent = widthSlider.value;
updateImageDisplay(viewId);
});
}
function setupPanningEvents(viewId) {
const canvas = document.getElementById(`canvas${viewId}`);
const panBtn = document.querySelector(`.pan-btn${viewId}`);
let isDragging = false;
let lastX = 0, lastY = 0;
// Pan with mouse drag
canvas.addEventListener('mousedown', (e) => {
isDragging = true;
lastX = e.clientX;
lastY = e.clientY;
canvas.style.cursor = 'grabbing';
});
canvas.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
lastX = e.clientX;
lastY = e.clientY;
if (viewId === 1 && pixelArray1) {
offsetX1 += dx;
offsetY1 += dy;
updateImageDisplay(1);
} else if (viewId === 2 && pixelArray2) {
offsetX2 += dx;
offsetY2 += dy;
updateImageDisplay(2);
}
});
canvas.addEventListener('mouseup', () => {
isDragging = false;
canvas.style.cursor = 'grab';
});
canvas.addEventListener('mouseleave', () => {
isDragging = false;
canvas.style.cursor = 'default';
});
// Zoom with mouse wheel
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = -e.deltaY;
const zoomFactor = delta > 0 ? 1.1 : 0.9;
// Get mouse position relative to canvas
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// Calculate image position before zoom
const imgX = (mouseX - (viewId === 1 ? offsetX1 : offsetX2)) / (viewId === 1 ? currentZoom1 : currentZoom2);
const imgY = (mouseY - (viewId === 1 ? offsetY1 : offsetY2)) / (viewId === 1 ? currentZoom1 : currentZoom2);
// Apply zoom
if (viewId === 1) {
currentZoom1 *= zoomFactor;
// Adjust offset to zoom toward mouse position
offsetX1 = mouseX - imgX * currentZoom1;
offsetY1 = mouseY - imgY * currentZoom1;
} else {
currentZoom2 *= zoomFactor;
offsetX2 = mouseX - imgX * currentZoom2;
offsetY2 = mouseY - imgY * currentZoom2;
}
updateImageDisplay(viewId);
});
// Set cursor style
canvas.style.cursor = 'grab';
}
function togglePanMode(viewId) {
if (viewId === 1) {
isPanning1 = !isPanning1;
document.querySelector('.pan-btn1').textContent = isPanning1 ? 'Panning (Click to stop)' : 'Pan';
document.querySelector('.pan-btn1').classList.toggle('bg-blue-500', isPanning1);
document.querySelector('.pan-btn1').classList.toggle('text-white', isPanning1);
} else {
isPanning2 = !isPanning2;
document.querySelector('.pan-btn2').textContent = isPanning2 ? 'Panning (Click to stop)' : 'Pan';
document.querySelector('.pan-btn2').classList.toggle('bg-blue-500', isPanning2);
document.querySelector('.pan-btn2').classList.toggle('text-white', isPanning2);
}
}
function zoomImage(viewId, factor) {
if (viewId === 1) {
currentZoom1 *= factor;
updateImageDisplay(1);
} else {
currentZoom2 *= factor;
updateImageDisplay(2);
}
}
function resetView(viewId) {
if (viewId === 1) {
currentZoom1 = 1;
offsetX1 = 0;
offsetY1 = 0;
updateImageDisplay(1);
} else {
currentZoom2 = 1;
offsetX2 = 0;
offsetY2 = 0;
updateImageDisplay(2);
}
}
function openImageFile(viewId) {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.dcm,.npy,.png,.jpg,.jpeg,.bmp';
input.onchange = e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
const extension = file.name.split('.').pop().toLowerCase();
reader.onload = function(event) {
try {
let pixelArray = null;
let dicomData = null;
if (extension === 'dcm') {
// Parse DICOM file
const arrayBuffer = event.target.result;
const byteArray = new Uint8Array(arrayBuffer);
dicomData = dicomParser.parseDicom(byteArray);
// Extract pixel data
const pixelDataElement = dicomData.elements.x7fe00010;
if (!pixelDataElement) throw new Error("No pixel data found in DICOM file");
const pixelData = dicomParser.explicitElementToString(dicomData, pixelDataElement);
const bitsAllocated = dicomData.int16('x00280100') || 16;
const bitsStored = dicomData.int16('x00280101') || bitsAllocated;
const highBit = dicomData.int16('x00280102') || (bitsStored - 1);
const pixelRepresentation = dicomData.int16('x00280103') || 0;
const rows = dicomData.int16('x00280010');
const columns = dicomData.int16('x00280011');
// Get rescale parameters if present
const rescaleIntercept = dicomData.floatString('x00281052') || 0;
const rescaleSlope = dicomData.floatString('x00281053') || 1;
// Convert pixel data to array
if (bitsAllocated === 16) {
pixelArray = new Uint16Array(rows * columns);
for (let i = 0; i < pixelArray.length; i++) {
const offset = i * 2;
let value = (pixelData.charCodeAt(offset) << 8) + pixelData.charCodeAt(offset + 1);
// Handle signed data
if (pixelRepresentation === 1 && (value & (1 << highBit))) {
value |= ~((1 << (highBit + 1)) - 1);
}
// Apply rescale
value = value * rescaleSlope + rescaleIntercept;
pixelArray[i] = value;
}
} else if (bitsAllocated === 8) {
pixelArray = new Uint8Array(rows * columns);
for (let i = 0; i < pixelArray.length; i++) {
let value = pixelData.charCodeAt(i);
// Handle signed data
if (pixelRepresentation === 1 && (value & (1 << highBit))) {
value |= ~((1 << (highBit + 1)) - 1);
}
// Apply rescale
value = value * rescaleSlope + rescaleIntercept;
pixelArray[i] = value;
}
} else {
throw new Error(`Unsupported bits allocated: ${bitsAllocated}`);
}
pixelArray = pixelArrayTo2D(pixelArray, columns, rows);
// Get window center/width from DICOM if available
try {
const windowCenter = dicomData.floatString('x00281050');
const windowWidth = dicomData.floatString('x00281051');
if (windowCenter && windowWidth) {
// Store for later use in setupViewerWithImage
dicomData.windowCenter = windowCenter;
dicomData.windowWidth = windowWidth;
}
} catch (e) {
// Ignore if windowing parameters not found
}
// Populate patient info if not already loaded
if (!patientInfoLoaded) {
populatePatientInfo(dicomData);
patientInfoLoaded = true;
}
// Show DICOM info
showDicomInfo(viewId, dicomData);
} else if (extension === 'npy') {
// For demo purposes, we'll just create a random array
// In a real app, you'd need a proper NPY parser
const size = 1024;
pixelArray = new Array(size);
for (let i = 0; i < size; i++) {
pixelArray[i] = new Array(size);
for (let j = 0; j < size; j++) {
pixelArray[i][j] = Math.floor(Math.random() * 4096);
}
}
} else if (['png', 'jpg', 'jpeg', 'bmp'].includes(extension)) {
// Load image file
const img = new Image();
img.onload = function() {
// Draw image to canvas and get pixel data
const tempCanvas = document.createElement('canvas');
tempCanvas.width = img.width;
tempCanvas.height = img.height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(img, 0, 0);
const imageData = tempCtx.getImageData(0, 0, img.width, img.height);
const data = imageData.data;
// Convert to grayscale 2D array
pixelArray = new Array(img.height);
for (let y = 0; y < img.height; y++) {
pixelArray[y] = new Array(img.width);
for (let x = 0; x < img.width; x++) {
const i = (y * img.width + x) * 4;
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
pixelArray[y][x] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
}
}
// Store and display
if (viewId === 1) {
pixelArray1 = pixelArray;
dicomData1 = dicomData;
} else {
pixelArray2 = pixelArray;
dicomData2 = dicomData;
}
setupViewerWithImage(viewId);
};
img.src = event.target.result;
return;
} else {
alert(`Unsupported file format: ${extension}`);
return;
}
// Store the pixel array and dicom data
if (viewId === 1) {
pixelArray1 = pixelArray;
dicomData1 = dicomData;
} else {
pixelArray2 = pixelArray;
dicomData2 = dicomData;
}
setupViewerWithImage(viewId);
} catch (error) {
alert(`Error loading file: ${error.message}`);
console.error(error);
}
};
if (extension === 'dcm') {
reader.readAsArrayBuffer(file);
} else {
reader.readAsDataURL(file);
}
};
input.click();
}
function pixelArrayTo2D(pixelArray, width, height) {
const result = new Array(height);
for (let y = 0; y < height; y++) {
result[y] = new Array(width);
for (let x = 0; x < width; x++) {
result[y][x] = pixelArray[y * width + x];
}
}
return result;
}
function populatePatientInfo(dicomData) {
document.getElementById('patientId').value = dicomData.string('x00100020') || '';
// Try to get age from PatientAge tag (0010,1010)
const age = dicomData.string('x00101010');
if (age) {
// DICOM age format is 3 digits followed by D, M, or Y (e.g., "045Y")
const ageValue = parseInt(age.substring(0, 3));
const ageUnit = age.substring(3);
if (!isNaN(ageValue)) {
if (ageUnit === 'Y') {
document.getElementById('age').value = ageValue;
} else if (ageUnit === 'M') {
document.getElementById('age').value = Math.floor(ageValue / 12);
} else if (ageUnit === 'D') {
document.getElementById('age').value = Math.floor(ageValue / 365);
}
}
}
}
function showDicomInfo(viewId, dicomData) {
const container = document.getElementById(`dicomInfo${viewId}`);
const tagsContainer = document.getElementById(`dicomTags${viewId}`);
container.classList.remove('hidden');
tagsContainer.innerHTML = '';
// List of DICOM tags to display
const tagsToShow = [
{ tag: 'x00080008', name: 'Image Type' },
{ tag: 'x00080016', name: 'SOP Class UID' },
{ tag: 'x00080018', name: 'SOP Instance UID' },
{ tag: 'x00080020', name: 'Study Date' },
{ tag: 'x00080030', name: 'Study Time' },
{ tag: 'x00080050', name: 'Accession Number' },
{ tag: 'x00080060', name: 'Modality' },
{ tag: 'x00080070', name: 'Manufacturer' },
{ tag: 'x00081030', name: 'Study Description' },
{ tag: 'x0008103e', name: 'Series Description' },
{ tag: 'x00100010', name: 'Patient Name' },
{ tag: 'x00100020', name: 'Patient ID' },
{ tag: 'x00100030', name: 'Patient Birth Date' },
{ tag: 'x00100040', name: 'Patient Sex' },
{ tag: 'x00101010', name: 'Patient Age' },
{ tag: 'x00101020', name: 'Patient Size' },
{ tag: 'x00101030', name: 'Patient Weight' },
{ tag: 'x0020000d', name: 'Study Instance UID' },
{ tag: 'x0020000e', name: 'Series Instance UID' },
{ tag: 'x00200010', name: 'Study ID' },
{ tag: 'x00200011', name: 'Series Number' },
{ tag: 'x00200013', name: 'Instance Number' },
{ tag: 'x00280010', name: 'Rows' },
{ tag: 'x00280011', name: 'Columns' },
{ tag: 'x00280100', name: 'Bits Allocated' },
{ tag: 'x00280101', name: 'Bits Stored' },
{ tag: 'x00280102', name: 'High Bit' },
{ tag: 'x00280103', name: 'Pixel Representation' },
{ tag: 'x00281050', name: 'Window Center' },
{ tag: 'x00281051', name: 'Window Width' },
{ tag: 'x00281052', name: 'Rescale Intercept' },
{ tag: 'x00281053', name: 'Rescale Slope' },
{ tag: 'x00282110', name: 'Lossy Image Compression' },
{ tag: 'x00282112', name: 'Lossy Image Compression Ratio' }
];
tagsToShow.forEach(tagInfo => {
try {
const value = dicomData.string(tagInfo.tag);
if (value) {
const div = document.createElement('div');
div.innerHTML = `<span class="font-medium">${tagInfo.name}:</span> <span class="dicom-tag">${value}</span>`;
tagsContainer.appendChild(div);
}
} catch (e) {
// Tag not found, skip
}
});
}
function setupViewerWithImage(viewId) {
const pixelArray = viewId === 1 ? pixelArray1 : pixelArray2;
const dicomData = viewId === 1 ? dicomData1 : dicomData2;
if (!pixelArray) return;
// Get min and max pixel values
let pixelMin = Infinity, pixelMax = -Infinity;
for (let y = 0; y < pixelArray.length; y++) {
for (let x = 0; x < pixelArray[y].length; x++) {
const val = pixelArray[y][x];
if (val < pixelMin) pixelMin = val;
if (val > pixelMax) pixelMax = val;
}
}
// Set window level/width from DICOM or use defaults
let wc, ww;
if (dicomData) {
try {
wc = dicomData.floatString('x00281050') || pixelMin + (pixelMax - pixelMin) / 2;
ww = dicomData.floatString('x00281051') || pixelMax - pixelMin;
} catch (e) {
wc = pixelMin + (pixelMax - pixelMin) / 2;
ww = pixelMax - pixelMin;
}
} else {
wc = pixelMin + (pixelMax - pixelMin) / 2;
ww = pixelMax - pixelMin;
}
// Update sliders
const levelSlider = document.getElementById(`levelSlider${viewId}`);
const widthSlider = document.getElementById(`widthSlider${viewId}`);
const levelValue = document.getElementById(`levelValue${viewId}`);
const widthValue = document.getElementById(`widthValue${viewId}`);
levelSlider.min = pixelMin;
levelSlider.max = pixelMax;
levelSlider.value = wc;
levelValue.textContent = wc;
widthSlider.min = 1;
widthSlider.max = pixelMax - pixelMin > 0 ? pixelMax - pixelMin : 1;
widthSlider.value = ww;
widthValue.textContent = ww;
// Reset view
if (viewId === 1) {
currentZoom1 = 1;
offsetX1 = 0;
offsetY1 = 0;
} else {
currentZoom2 = 1;
offsetX2 = 0;
offsetY2 = 0;
}
updateImageDisplay(viewId);
}
function updateImageDisplay(viewId) {
const pixelArray = viewId === 1 ? pixelArray1 : pixelArray2;
if (!pixelArray) return;
const canvas = viewId === 1 ? canvas1 : canvas2;
const ctx = viewId === 1 ? ctx1 : ctx2;
const levelSlider = document.getElementById(`levelSlider${viewId}`);
const widthSlider = document.getElementById(`widthSlider${viewId}`);
const level = parseFloat(levelSlider.value);
const width = parseFloat(widthSlider.value);
// Calculate window min/max
const windowMin = level - width / 2;
const windowMax = level + width / 2;
// Create image data
const height = pixelArray.length;
const imgWidth = pixelArray[0].length;
// Create a temporary canvas for the original image
const tempCanvas = document.createElement('canvas');
tempCanvas.width = imgWidth;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d');
const imageData = tempCtx.createImageData(imgWidth, height);
// Apply windowing to the pixel data
for (let y = 0; y < height; y++) {
for (let x = 0; x < imgWidth; x++) {
let pixelValue = pixelArray[y][x];
// Apply windowing
if (windowMax > windowMin) {
pixelValue = Math.max(windowMin, Math.min(pixelValue, windowMax));
pixelValue = ((pixelValue - windowMin) / (windowMax - windowMin)) * 255;
} else {
pixelValue = 0;
}
const i = (y * imgWidth + x) * 4;
imageData.data[i] = pixelValue; // R
imageData.data[i + 1] = pixelValue; // G
imageData.data[i + 2] = pixelValue; // B
imageData.data[i + 3] = 255; // A
}
}
// Put the image data to the temporary canvas
tempCtx.putImageData(imageData, 0, 0);
// Clear the main canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Calculate scaled dimensions based on zoom
const zoom = viewId === 1 ? currentZoom1 : currentZoom2;
const offsetX = viewId === 1 ? offsetX1 : offsetX2;
const offsetY = viewId === 1 ? offsetY1 : offsetY2;
const scaledWidth = imgWidth * zoom;
const scaledHeight = height * zoom;
// Calculate position to center the image
let x = (canvas.width - scaledWidth) / 2 + offsetX;
let y = (canvas.height - scaledHeight) / 2 + offsetY;
// Draw the scaled image with panning
ctx.drawImage(tempCanvas, 0, 0, imgWidth, height, x, y, scaledWidth, scaledHeight);
}
function addReasoningStep() {
const tbody = document.getElementById('reasoningTableBody');
const row = document.createElement('tr');
const stepCell = document.createElement('td');
stepCell.textContent = tbody.children.length + 1;
const actionCell = document.createElement('td');
const actionInput = document.createElement('input');
actionInput.type = 'text';
actionInput.className = 'w-full px-2 py-1 border border-gray-300 rounded';
actionCell.appendChild(actionInput);
const reasoningCell = document.createElement('td');
const reasoningInput = document.createElement('input');
reasoningInput.type = 'text';
reasoningInput.className = 'w-full px-2 py-1 border border-gray-300 rounded';
reasoningCell.appendChild(reasoningInput);
row.appendChild(stepCell);
row.appendChild(actionCell);
row.appendChild(reasoningCell);
tbody.appendChild(row);
}
function saveReport() {
if (!pixelArray1 && !pixelArray2) {
alert('Please open at least one image file before saving.');
return;
}
const reportData = {
patient_info: {
patient_id: document.getElementById('patientId').value,
age: document.getElementById('age').value,
breast_density: document.getElementById('density').value
},
left_breast_findings: {
location: document.getElementById('leftLocation').value,
type: document.getElementById('leftType').value,
shape: document.getElementById('leftShape').value,
margins: document.getElementById('leftMargins').value,
associated_findings: document.getElementById('leftAssoc').value,
initial_impression: document.getElementById('leftImpression').value,
recommended_action: document.getElementById('leftAction').value,
birads_category: document.getElementById('leftBirads').value,
rationale: document.getElementById('leftRationale').value
},
right_breast_findings: {
location: document.getElementById('rightLocation').value,
type: document.getElementById('rightType').value,
shape: document.getElementById('rightShape').value,
margins: document.getElementById('rightMargins').value,
associated_findings: document.getElementById('rightAssoc').value,
initial_impression: document.getElementById('rightImpression').value,
recommended_action: document.getElementById('rightAction').value,
birads_category: document.getElementById('rightBirads').value,
rationale: document.getElementById('rightRationale').value
},
clinical_reasoning: [],
final_recommendation: {
overall_assessment: document.getElementById('overallAssessment').value,
recommended_next_step: document.getElementById('nextStep').value
}
};
// Add reasoning steps
const reasoningRows = document.querySelectorAll('#reasoningTableBody tr');
reasoningRows.forEach(row => {
const cells = row.querySelectorAll('td');
reportData.clinical_reasoning.push({
step: cells[0].textContent,
action: cells[1].querySelector('input').value,
reasoning: cells[2].querySelector('input').value
});
});
// Create JSON blob
const blob = new Blob([JSON.stringify(reportData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
// Create download link
const a = document.createElement('a');
a.href = url;
a.download = 'mammogram_report.json';
document.body.appendChild(a);
a.click();
// Clean up
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 0);
alert('Report saved successfully!');
}
</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=mabrokma/mindslabmamo" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>