koesan commited on
Commit
a1af663
·
1 Parent(s): 228f73d

Initial deployment: Breast Cancer Classification App

Browse files
Files changed (6) hide show
  1. .gitattributes +1 -0
  2. Dockerfile +27 -0
  3. app.py +117 -0
  4. cancer_model.h5 +3 -0
  5. requirements.txt +5 -0
  6. templates/index.html +469 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ cancer_model.h5 filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ libgl1-mesa-glx \
8
+ libglib2.0-0 \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy requirements first for better caching
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # Copy application files
16
+ COPY app.py .
17
+ COPY cancer_model.h5 .
18
+ COPY templates/ templates/
19
+
20
+ # Create uploads directory
21
+ RUN mkdir -p uploads
22
+
23
+ # Expose port
24
+ EXPOSE 7860
25
+
26
+ # Run the application
27
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ import numpy as np
4
+ from flask import Flask, request, render_template, jsonify
5
+ from werkzeug.utils import secure_filename
6
+ import tensorflow as tf
7
+ from tensorflow.keras.models import load_model
8
+
9
+ app = Flask(__name__)
10
+ app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
11
+ app.config['UPLOAD_FOLDER'] = 'uploads'
12
+
13
+ # Create uploads folder if it doesn't exist
14
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
15
+
16
+ # Load the model
17
+ print("Loading model...")
18
+ model = load_model('cancer_model.h5', compile=False)
19
+ print("Model loaded successfully!")
20
+
21
+ def resize_with_padding(img, target_size):
22
+ """Resize image while maintaining aspect ratio and add padding"""
23
+ height, width = img.shape[:2]
24
+ target_width, target_height = target_size
25
+
26
+ # Calculate scaling factor
27
+ scale = min(target_width / width, target_height / height)
28
+ new_width = int(width * scale)
29
+ new_height = int(height * scale)
30
+
31
+ # Resize image
32
+ resized_image = cv2.resize(img, (new_width, new_height), interpolation=cv2.INTER_AREA)
33
+
34
+ # Calculate padding
35
+ pad_width = target_width - new_width
36
+ pad_height = target_height - new_height
37
+ top = pad_height // 2
38
+ bottom = pad_height - top
39
+ left = pad_width // 2
40
+ right = pad_width - left
41
+
42
+ # Add black padding
43
+ padded_image = cv2.copyMakeBorder(resized_image, top, bottom, left, right,
44
+ cv2.BORDER_CONSTANT, value=[0, 0, 0])
45
+ return padded_image
46
+
47
+ def left_or_right(img):
48
+ """Normalize left/right breast orientation"""
49
+ height, width = img.shape[:2]
50
+ left_half = img[:, :width // 2]
51
+ right_half = img[:, width // 2:]
52
+ left_intensity = np.sum(left_half)
53
+ right_intensity = np.sum(right_half)
54
+ return img if left_intensity > right_intensity else cv2.flip(img, 1)
55
+
56
+ def predict_image(image_path):
57
+ """Make prediction on uploaded image"""
58
+ # Read image
59
+ img = cv2.imread(image_path)
60
+
61
+ # Preprocess
62
+ img = resize_with_padding(img, (256, 256))
63
+ img = left_or_right(img)
64
+ img = img / 255.0
65
+ img = np.expand_dims(img, axis=0)
66
+
67
+ # Predict
68
+ prediction_prob = model.predict(img, verbose=0)
69
+ predicted_class = 1 if prediction_prob[0][0] > 0.5 else 0
70
+ confidence = float(prediction_prob[0][0] if predicted_class == 1 else 1 - prediction_prob[0][0])
71
+
72
+ result = {
73
+ 'class': 'Malignant' if predicted_class == 1 else 'Benign',
74
+ 'confidence': confidence * 100,
75
+ 'malignant_prob': float(prediction_prob[0][0]) * 100,
76
+ 'benign_prob': (1 - float(prediction_prob[0][0])) * 100
77
+ }
78
+
79
+ return result
80
+
81
+ @app.route('/')
82
+ def index():
83
+ """Render main page"""
84
+ return render_template('index.html')
85
+
86
+ @app.route('/predict', methods=['POST'])
87
+ def predict():
88
+ """Handle prediction request"""
89
+ if 'file' not in request.files:
90
+ return jsonify({'error': 'No file uploaded'}), 400
91
+
92
+ file = request.files['file']
93
+
94
+ if file.filename == '':
95
+ return jsonify({'error': 'No file selected'}), 400
96
+
97
+ if file:
98
+ # Save uploaded file
99
+ filename = secure_filename(file.filename)
100
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
101
+ file.save(filepath)
102
+
103
+ try:
104
+ # Make prediction
105
+ result = predict_image(filepath)
106
+
107
+ # Clean up
108
+ os.remove(filepath)
109
+
110
+ return jsonify(result)
111
+ except Exception as e:
112
+ if os.path.exists(filepath):
113
+ os.remove(filepath)
114
+ return jsonify({'error': str(e)}), 500
115
+
116
+ if __name__ == '__main__':
117
+ app.run(host='0.0.0.0', port=7860, debug=False)
cancer_model.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8f806b2e5b52f7c4fec7acd8266334af0541751caef4f0264d9126eac317f88c
3
+ size 157484576
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Flask==3.0.0
2
+ tensorflow==2.15.0
3
+ opencv-python-headless==4.8.1.78
4
+ numpy==1.24.3
5
+ Werkzeug==3.0.1
templates/index.html ADDED
@@ -0,0 +1,469 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Breast Cancer Classification - AI Diagnostic Tool</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 900px;
23
+ margin: 0 auto;
24
+ background: white;
25
+ border-radius: 20px;
26
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
27
+ overflow: hidden;
28
+ }
29
+
30
+ .header {
31
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
32
+ color: white;
33
+ padding: 30px;
34
+ text-align: center;
35
+ }
36
+
37
+ .header h1 {
38
+ font-size: 2.5em;
39
+ margin-bottom: 10px;
40
+ }
41
+
42
+ .header p {
43
+ font-size: 1.1em;
44
+ opacity: 0.9;
45
+ }
46
+
47
+ .content {
48
+ padding: 40px;
49
+ }
50
+
51
+ .info-box {
52
+ background: #f8f9fa;
53
+ border-left: 4px solid #667eea;
54
+ padding: 20px;
55
+ margin-bottom: 30px;
56
+ border-radius: 8px;
57
+ }
58
+
59
+ .info-box h3 {
60
+ color: #667eea;
61
+ margin-bottom: 10px;
62
+ }
63
+
64
+ .info-box ul {
65
+ margin-left: 20px;
66
+ line-height: 1.8;
67
+ }
68
+
69
+ .upload-section {
70
+ text-align: center;
71
+ padding: 40px;
72
+ border: 3px dashed #667eea;
73
+ border-radius: 15px;
74
+ margin-bottom: 30px;
75
+ transition: all 0.3s;
76
+ cursor: pointer;
77
+ }
78
+
79
+ .upload-section:hover {
80
+ border-color: #764ba2;
81
+ background: #f8f9fa;
82
+ }
83
+
84
+ .upload-section.drag-over {
85
+ background: #e3f2fd;
86
+ border-color: #2196F3;
87
+ }
88
+
89
+ .upload-icon {
90
+ font-size: 4em;
91
+ color: #667eea;
92
+ margin-bottom: 20px;
93
+ }
94
+
95
+ .upload-text {
96
+ font-size: 1.2em;
97
+ color: #666;
98
+ margin-bottom: 15px;
99
+ }
100
+
101
+ .file-input {
102
+ display: none;
103
+ }
104
+
105
+ .upload-button {
106
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
107
+ color: white;
108
+ padding: 12px 30px;
109
+ border: none;
110
+ border-radius: 25px;
111
+ font-size: 1.1em;
112
+ cursor: pointer;
113
+ transition: transform 0.2s;
114
+ }
115
+
116
+ .upload-button:hover {
117
+ transform: scale(1.05);
118
+ }
119
+
120
+ .preview-section {
121
+ display: none;
122
+ margin-bottom: 30px;
123
+ }
124
+
125
+ .preview-image {
126
+ max-width: 100%;
127
+ max-height: 400px;
128
+ border-radius: 10px;
129
+ box-shadow: 0 5px 15px rgba(0,0,0,0.2);
130
+ display: block;
131
+ margin: 0 auto;
132
+ }
133
+
134
+ .result-section {
135
+ display: none;
136
+ padding: 30px;
137
+ border-radius: 15px;
138
+ text-align: center;
139
+ }
140
+
141
+ .result-benign {
142
+ background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
143
+ color: white;
144
+ }
145
+
146
+ .result-malignant {
147
+ background: linear-gradient(135deg, #ee0979 0%, #ff6a00 100%);
148
+ color: white;
149
+ }
150
+
151
+ .result-title {
152
+ font-size: 2em;
153
+ margin-bottom: 20px;
154
+ }
155
+
156
+ .confidence-bar {
157
+ background: rgba(255,255,255,0.3);
158
+ border-radius: 10px;
159
+ height: 30px;
160
+ margin: 20px 0;
161
+ position: relative;
162
+ overflow: hidden;
163
+ }
164
+
165
+ .confidence-fill {
166
+ height: 100%;
167
+ background: white;
168
+ border-radius: 10px;
169
+ transition: width 1s ease;
170
+ display: flex;
171
+ align-items: center;
172
+ justify-content: center;
173
+ color: #667eea;
174
+ font-weight: bold;
175
+ }
176
+
177
+ .probabilities {
178
+ display: flex;
179
+ justify-content: space-around;
180
+ margin-top: 20px;
181
+ }
182
+
183
+ .prob-item {
184
+ flex: 1;
185
+ padding: 15px;
186
+ background: rgba(255,255,255,0.2);
187
+ border-radius: 10px;
188
+ margin: 0 10px;
189
+ }
190
+
191
+ .prob-label {
192
+ font-size: 0.9em;
193
+ margin-bottom: 5px;
194
+ }
195
+
196
+ .prob-value {
197
+ font-size: 1.8em;
198
+ font-weight: bold;
199
+ }
200
+
201
+ .loading {
202
+ display: none;
203
+ text-align: center;
204
+ padding: 30px;
205
+ }
206
+
207
+ .spinner {
208
+ border: 4px solid #f3f3f3;
209
+ border-top: 4px solid #667eea;
210
+ border-radius: 50%;
211
+ width: 50px;
212
+ height: 50px;
213
+ animation: spin 1s linear infinite;
214
+ margin: 0 auto 20px;
215
+ }
216
+
217
+ @keyframes spin {
218
+ 0% { transform: rotate(0deg); }
219
+ 100% { transform: rotate(360deg); }
220
+ }
221
+
222
+ .error {
223
+ display: none;
224
+ background: #ff5252;
225
+ color: white;
226
+ padding: 15px;
227
+ border-radius: 10px;
228
+ margin-bottom: 20px;
229
+ }
230
+
231
+ .try-again-button {
232
+ background: white;
233
+ color: #667eea;
234
+ padding: 12px 30px;
235
+ border: none;
236
+ border-radius: 25px;
237
+ font-size: 1.1em;
238
+ cursor: pointer;
239
+ margin-top: 20px;
240
+ transition: transform 0.2s;
241
+ }
242
+
243
+ .try-again-button:hover {
244
+ transform: scale(1.05);
245
+ }
246
+
247
+ .note {
248
+ background: #fff3cd;
249
+ border-left: 4px solid #ffc107;
250
+ padding: 15px;
251
+ margin-top: 30px;
252
+ border-radius: 8px;
253
+ font-size: 0.9em;
254
+ }
255
+
256
+ .footer {
257
+ background: #f8f9fa;
258
+ padding: 20px;
259
+ text-align: center;
260
+ color: #666;
261
+ font-size: 0.9em;
262
+ }
263
+ </style>
264
+ </head>
265
+ <body>
266
+ <div class="container">
267
+ <div class="header">
268
+ <h1>🔬 Breast Cancer Classification</h1>
269
+ <p>AI-Powered Mammogram Analysis</p>
270
+ </div>
271
+
272
+ <div class="content">
273
+ <div class="info-box">
274
+ <h3>📋 How to Use This Tool</h3>
275
+ <ul>
276
+ <li><strong>Upload Image:</strong> Click the upload area or drag & drop a mammogram image</li>
277
+ <li><strong>Supported Formats:</strong> JPG, JPEG, PNG</li>
278
+ <li><strong>Image Requirements:</strong> Clear mammogram image, preferably full breast view</li>
279
+ <li><strong>Classification:</strong> The AI will classify the image as Benign (non-cancerous) or Malignant (cancerous)</li>
280
+ <li><strong>Confidence Score:</strong> Shows the model's confidence in its prediction</li>
281
+ </ul>
282
+ </div>
283
+
284
+ <div class="info-box">
285
+ <h3>🤖 About the Model</h3>
286
+ <p>This tool uses an integrated ensemble of VGG16 and ResNet50V2 deep learning models trained on the CBIS-DDSM dataset. The model combines transfer learning with custom classification layers to analyze mammogram images and predict breast cancer classification.</p>
287
+ </div>
288
+
289
+ <div class="error" id="errorMessage"></div>
290
+
291
+ <div class="upload-section" id="uploadSection">
292
+ <div class="upload-icon">📤</div>
293
+ <div class="upload-text">Drag & Drop your mammogram image here</div>
294
+ <div style="margin: 20px 0;">or</div>
295
+ <input type="file" id="fileInput" class="file-input" accept="image/*">
296
+ <button class="upload-button" onclick="document.getElementById('fileInput').click()">
297
+ Choose File
298
+ </button>
299
+ </div>
300
+
301
+ <div class="preview-section" id="previewSection">
302
+ <h3 style="margin-bottom: 15px;">Uploaded Image:</h3>
303
+ <img id="previewImage" class="preview-image" alt="Preview">
304
+ <div style="text-align: center; margin-top: 20px;">
305
+ <button class="upload-button" onclick="analyzeImage()">
306
+ 🔍 Analyze Image
307
+ </button>
308
+ </div>
309
+ </div>
310
+
311
+ <div class="loading" id="loading">
312
+ <div class="spinner"></div>
313
+ <p>Analyzing mammogram image...</p>
314
+ </div>
315
+
316
+ <div class="result-section" id="resultSection">
317
+ <div class="result-title" id="resultTitle"></div>
318
+ <div class="confidence-bar">
319
+ <div class="confidence-fill" id="confidenceFill"></div>
320
+ </div>
321
+ <div class="probabilities">
322
+ <div class="prob-item">
323
+ <div class="prob-label">Benign Probability</div>
324
+ <div class="prob-value" id="benignProb">-</div>
325
+ </div>
326
+ <div class="prob-item">
327
+ <div class="prob-label">Malignant Probability</div>
328
+ <div class="prob-value" id="malignantProb">-</div>
329
+ </div>
330
+ </div>
331
+ <button class="try-again-button" onclick="resetAnalysis()">
332
+ 🔄 Analyze Another Image
333
+ </button>
334
+ </div>
335
+
336
+ <div class="note">
337
+ <strong>⚠️ Important Notice:</strong> This is an AI diagnostic assistance tool for educational and research purposes. It should NOT be used as a substitute for professional medical diagnosis. Always consult with qualified healthcare professionals for medical advice and diagnosis.
338
+ </div>
339
+ </div>
340
+
341
+ <div class="footer">
342
+ <p>Powered by VGG16 + ResNet50V2 Ensemble Model | CBIS-DDSM Dataset</p>
343
+ <p>© 2025 Breast Cancer Classification Project</p>
344
+ </div>
345
+ </div>
346
+
347
+ <script>
348
+ let selectedFile = null;
349
+
350
+ // File input change handler
351
+ document.getElementById('fileInput').addEventListener('change', function(e) {
352
+ handleFile(e.target.files[0]);
353
+ });
354
+
355
+ // Drag and drop handlers
356
+ const uploadSection = document.getElementById('uploadSection');
357
+
358
+ uploadSection.addEventListener('dragover', function(e) {
359
+ e.preventDefault();
360
+ uploadSection.classList.add('drag-over');
361
+ });
362
+
363
+ uploadSection.addEventListener('dragleave', function(e) {
364
+ e.preventDefault();
365
+ uploadSection.classList.remove('drag-over');
366
+ });
367
+
368
+ uploadSection.addEventListener('drop', function(e) {
369
+ e.preventDefault();
370
+ uploadSection.classList.remove('drag-over');
371
+ handleFile(e.dataTransfer.files[0]);
372
+ });
373
+
374
+ function handleFile(file) {
375
+ if (!file) return;
376
+
377
+ // Check if file is an image
378
+ if (!file.type.startsWith('image/')) {
379
+ showError('Please upload a valid image file (JPG, JPEG, PNG)');
380
+ return;
381
+ }
382
+
383
+ selectedFile = file;
384
+
385
+ // Show preview
386
+ const reader = new FileReader();
387
+ reader.onload = function(e) {
388
+ document.getElementById('previewImage').src = e.target.result;
389
+ document.getElementById('uploadSection').style.display = 'none';
390
+ document.getElementById('previewSection').style.display = 'block';
391
+ document.getElementById('errorMessage').style.display = 'none';
392
+ };
393
+ reader.readAsDataURL(file);
394
+ }
395
+
396
+ async function analyzeImage() {
397
+ if (!selectedFile) return;
398
+
399
+ // Show loading
400
+ document.getElementById('previewSection').style.display = 'none';
401
+ document.getElementById('loading').style.display = 'block';
402
+
403
+ // Create FormData
404
+ const formData = new FormData();
405
+ formData.append('file', selectedFile);
406
+
407
+ try {
408
+ const response = await fetch('/predict', {
409
+ method: 'POST',
410
+ body: formData
411
+ });
412
+
413
+ const data = await response.json();
414
+
415
+ if (response.ok) {
416
+ showResult(data);
417
+ } else {
418
+ showError(data.error || 'An error occurred during analysis');
419
+ }
420
+ } catch (error) {
421
+ showError('Failed to connect to the server. Please try again.');
422
+ } finally {
423
+ document.getElementById('loading').style.display = 'none';
424
+ }
425
+ }
426
+
427
+ function showResult(data) {
428
+ const resultSection = document.getElementById('resultSection');
429
+ const resultTitle = document.getElementById('resultTitle');
430
+ const confidenceFill = document.getElementById('confidenceFill');
431
+ const benignProb = document.getElementById('benignProb');
432
+ const malignantProb = document.getElementById('malignantProb');
433
+
434
+ // Update content
435
+ resultTitle.textContent = `Classification: ${data.class}`;
436
+ confidenceFill.style.width = data.confidence.toFixed(2) + '%';
437
+ confidenceFill.textContent = data.confidence.toFixed(2) + '%';
438
+ benignProb.textContent = data.benign_prob.toFixed(2) + '%';
439
+ malignantProb.textContent = data.malignant_prob.toFixed(2) + '%';
440
+
441
+ // Update styling based on classification
442
+ if (data.class === 'Benign') {
443
+ resultSection.className = 'result-section result-benign';
444
+ } else {
445
+ resultSection.className = 'result-section result-malignant';
446
+ }
447
+
448
+ resultSection.style.display = 'block';
449
+ }
450
+
451
+ function showError(message) {
452
+ const errorElement = document.getElementById('errorMessage');
453
+ errorElement.textContent = '❌ Error: ' + message;
454
+ errorElement.style.display = 'block';
455
+ document.getElementById('uploadSection').style.display = 'block';
456
+ document.getElementById('loading').style.display = 'none';
457
+ }
458
+
459
+ function resetAnalysis() {
460
+ selectedFile = null;
461
+ document.getElementById('fileInput').value = '';
462
+ document.getElementById('uploadSection').style.display = 'block';
463
+ document.getElementById('previewSection').style.display = 'none';
464
+ document.getElementById('resultSection').style.display = 'none';
465
+ document.getElementById('errorMessage').style.display = 'none';
466
+ }
467
+ </script>
468
+ </body>
469
+ </html>