bhd82 commited on
Commit
d8c0d49
·
verified ·
1 Parent(s): 47c1b66

Upload 22 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ 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
+ models/efficientnetb0_skin_cancer_model_final_69.keras filter=lfs diff=lfs merge=lfs -text
37
+ static/images/logo.svg filter=lfs diff=lfs merge=lfs -text
app.py ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify, redirect, url_for
2
+ import os
3
+ import numpy as np
4
+ from PIL import Image
5
+ import tensorflow as tf
6
+ from tensorflow.keras.applications.mobilenet_v2 import MobileNetV2, preprocess_input as mobilenet_preprocess
7
+ from tensorflow.keras.applications.efficientnet import EfficientNetB0, preprocess_input as efficientnet_preprocess
8
+ from tensorflow.keras.preprocessing import image
9
+ import io
10
+
11
+ app = Flask(__name__)
12
+
13
+ # Load models
14
+ models = {
15
+ 'MobileNetV2': None,
16
+ 'EfficientNetB0': None
17
+ }
18
+
19
+ # Class labels for HAM10000 dataset
20
+ class_labels = {
21
+ 'akiec': 'Actinic Keratosis',
22
+ 'bcc': 'Basal Cell Carcinoma',
23
+ 'bkl': 'Benign Keratosis',
24
+ 'df': 'Dermatofibroma',
25
+ 'mel': 'Melanoma',
26
+ 'nv': 'Melanocytic Nevus',
27
+ 'vasc': 'Vascular Lesion'
28
+ }
29
+
30
+ # Class descriptions
31
+ class_descriptions = {
32
+ 'akiec': {
33
+ 'name': 'Actinic Keratosis',
34
+ 'description': 'A precancerous growth caused by sun damage that may develop into skin cancer if left untreated.'
35
+ },
36
+ 'bcc': {
37
+ 'name': 'Basal Cell Carcinoma',
38
+ 'description': 'The most common type of skin cancer, usually appearing as a shiny bump or pink patch on sun-exposed areas.'
39
+ },
40
+ 'bkl': {
41
+ 'name': 'Benign Keratosis',
42
+ 'description': 'A non-cancerous growth that appears as a waxy, scaly, slightly raised growth on the skin.'
43
+ },
44
+ 'df': {
45
+ 'name': 'Dermatofibroma',
46
+ 'description': 'A common benign skin growth that often appears as a small, firm bump on the skin.'
47
+ },
48
+ 'mel': {
49
+ 'name': 'Melanoma',
50
+ 'description': 'The most serious form of skin cancer that develops in the cells that produce melanin.'
51
+ },
52
+ 'nv': {
53
+ 'name': 'Melanocytic Nevus',
54
+ 'description': 'A common mole that appears as a small, dark brown spot caused by clusters of pigmented cells.'
55
+ },
56
+ 'vasc': {
57
+ 'name': 'Vascular Lesion',
58
+ 'description': 'An abnormality of blood vessels that can appear as red or purple marks on the skin.'
59
+ }
60
+ }
61
+
62
+ def load_model(model_name):
63
+ """Load the specified model"""
64
+ global models
65
+
66
+ if model_name not in models:
67
+ # Initialize models dictionary if needed
68
+ initialize_models()
69
+
70
+ if models[model_name] is None:
71
+ if model_name == 'MobileNetV2':
72
+ try:
73
+ # Load the entire model instead of just weights
74
+ model_path = 'models/mobilenetv2_ham10000_finetuned_74.h5'
75
+ app.logger.info(f"Loading MobileNetV2 model from {model_path}")
76
+
77
+ # Check if file exists
78
+ if not os.path.exists(model_path):
79
+ app.logger.error(f"Model file not found: {model_path}")
80
+ raise FileNotFoundError(f"Model file not found: {model_path}")
81
+
82
+ # Load the model with custom objects if needed
83
+ models[model_name] = tf.keras.models.load_model(model_path, compile=False)
84
+
85
+ # Compile the model
86
+ models[model_name].compile(
87
+ optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001),
88
+ loss='categorical_crossentropy',
89
+ metrics=['accuracy']
90
+ )
91
+
92
+ app.logger.info("MobileNetV2 model loaded and compiled successfully")
93
+
94
+ except Exception as e:
95
+ app.logger.error(f"Error loading MobileNetV2 model: {str(e)}")
96
+ # Try an alternative approach - load model with custom_objects
97
+ try:
98
+ app.logger.info("Attempting to load model with custom_objects...")
99
+ models[model_name] = tf.keras.models.load_model(
100
+ 'models/mobilenetv2_ham10000_finetuned_74.h5',
101
+ custom_objects={
102
+ 'Adam': tf.keras.optimizers.Adam,
103
+ 'categorical_crossentropy': tf.keras.losses.categorical_crossentropy
104
+ },
105
+ compile=False
106
+ )
107
+
108
+ # Compile the model
109
+ models[model_name].compile(
110
+ optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001),
111
+ loss='categorical_crossentropy',
112
+ metrics=['accuracy']
113
+ )
114
+
115
+ app.logger.info("MobileNetV2 model loaded with custom_objects successfully")
116
+ except Exception as e2:
117
+ app.logger.error(f"Second attempt failed: {str(e2)}")
118
+ raise RuntimeError(f"Failed to load MobileNetV2 model: {str(e)} and {str(e2)}")
119
+
120
+ elif model_name == 'EfficientNetB0':
121
+ try:
122
+ # Load the entire model instead of just weights
123
+ model_path = 'models/efficientnetb0_skin_cancer_model_final_69.keras'
124
+ app.logger.info(f"Loading EfficientNetB0 model from {model_path}")
125
+
126
+ # Check if file exists
127
+ if not os.path.exists(model_path):
128
+ app.logger.error(f"Model file not found: {model_path}")
129
+ raise FileNotFoundError(f"Model file not found: {model_path}")
130
+
131
+ # Load the model with custom objects if needed
132
+ models[model_name] = tf.keras.models.load_model(model_path, compile=False)
133
+
134
+ # Compile the model
135
+ models[model_name].compile(
136
+ optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001),
137
+ loss='categorical_crossentropy',
138
+ metrics=['accuracy']
139
+ )
140
+
141
+ app.logger.info("EfficientNetB0 model loaded and compiled successfully")
142
+
143
+ except Exception as e:
144
+ app.logger.error(f"Error loading EfficientNetB0 model: {str(e)}")
145
+ # Try an alternative approach - load model with custom_objects
146
+ try:
147
+ app.logger.info("Attempting to load model with custom_objects...")
148
+ models[model_name] = tf.keras.models.load_model(
149
+ model_path,
150
+ custom_objects={
151
+ 'Adam': tf.keras.optimizers.Adam,
152
+ 'categorical_crossentropy': tf.keras.losses.categorical_crossentropy
153
+ },
154
+ compile=False
155
+ )
156
+
157
+ # Compile the model
158
+ models[model_name].compile(
159
+ optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001),
160
+ loss='categorical_crossentropy',
161
+ metrics=['accuracy']
162
+ )
163
+
164
+ app.logger.info("EfficientNetB0 model loaded with custom_objects successfully")
165
+ except Exception as e2:
166
+ app.logger.error(f"Second attempt failed: {str(e2)}")
167
+ raise RuntimeError(f"Failed to load EfficientNetB0 model: {str(e)} and {str(e2)}")
168
+ else:
169
+ app.logger.error(f"Unknown model name: {model_name}")
170
+ raise ValueError(f"Unknown model name: {model_name}")
171
+
172
+ return models[model_name]
173
+
174
+ def preprocess_image(img, model_name):
175
+ """Preprocess the image for the specified model"""
176
+ img = img.resize((224, 224))
177
+ img_array = image.img_to_array(img)
178
+ img_array = np.expand_dims(img_array, axis=0)
179
+
180
+ if model_name == 'MobileNetV2':
181
+ return mobilenet_preprocess(img_array)
182
+ elif model_name == 'EfficientNetB0':
183
+ return efficientnet_preprocess(img_array)
184
+
185
+ def predict_image(img, model_name):
186
+ """Make a prediction on an image using the specified model"""
187
+ app.logger.info(f"Preparing image for prediction with {model_name}")
188
+
189
+ # Resize the image to the required input size
190
+ img = img.resize((224, 224))
191
+
192
+ # Convert to numpy array and preprocess
193
+ img_array = np.array(img)
194
+ img_array = img_array / 255.0 # Normalize to [0,1]
195
+ img_array = np.expand_dims(img_array, axis=0) # Add batch dimension
196
+
197
+ # Load the model if not already loaded
198
+ model = load_model(model_name)
199
+
200
+ # Make prediction
201
+ app.logger.info("Running prediction...")
202
+ preds = model.predict(img_array)
203
+ app.logger.info(f"Raw prediction values: {preds}")
204
+
205
+ # Check for NaN values
206
+ if np.isnan(preds).any():
207
+ app.logger.warning("NaN values detected in predictions. Replacing with zeros.")
208
+ preds = np.nan_to_num(preds, nan=0.0)
209
+
210
+ # Define class names for HAM10000 dataset
211
+ class_names = ['akiec', 'bcc', 'bkl', 'df', 'mel', 'nv', 'vasc']
212
+ full_names = {
213
+ 'akiec': 'Actinic Keratosis',
214
+ 'bcc': 'Basal Cell Carcinoma',
215
+ 'bkl': 'Benign Keratosis',
216
+ 'df': 'Dermatofibroma',
217
+ 'mel': 'Melanoma',
218
+ 'nv': 'Melanocytic Nevus',
219
+ 'vasc': 'Vascular Lesion'
220
+ }
221
+
222
+ # Format predictions
223
+ predictions = []
224
+ for i, class_name in enumerate(class_names):
225
+ confidence = float(preds[0][i])
226
+ # Ensure confidence is a valid number
227
+ if np.isnan(confidence) or np.isinf(confidence):
228
+ confidence = 0.0
229
+
230
+ predictions.append({
231
+ 'class': class_name,
232
+ 'name': full_names[class_name],
233
+ 'confidence': confidence
234
+ })
235
+
236
+ # Sort by confidence (highest first)
237
+ predictions = sorted(predictions, key=lambda x: x['confidence'], reverse=True)
238
+
239
+ app.logger.info(f"Prediction completed: {predictions}")
240
+ return predictions
241
+
242
+ @app.route('/')
243
+ def index():
244
+ return render_template('index.html')
245
+
246
+ @app.route('/detect')
247
+ def detect():
248
+ return render_template('detect.html',
249
+ class_descriptions=class_descriptions,
250
+ condition_info=class_descriptions)
251
+
252
+ @app.route('/about')
253
+ def about():
254
+ return render_template('about.html')
255
+
256
+ @app.route('/faq')
257
+ def faq():
258
+ return render_template('faq.html')
259
+
260
+ @app.route('/contact')
261
+ def contact():
262
+ return render_template('contact.html')
263
+
264
+ @app.route('/predict', methods=['POST'])
265
+ def predict():
266
+ # Debug information
267
+ app.logger.info(f"Request received: {request.method} {request.path}")
268
+ app.logger.info(f"Request form data: {request.form}")
269
+ app.logger.info(f"Request files: {request.files}")
270
+
271
+ if 'file' not in request.files:
272
+ app.logger.error("No file part in the request")
273
+ return jsonify({'error': 'No file part in the request'}), 400
274
+
275
+ file = request.files['file']
276
+ app.logger.info(f"File received: {file.filename}, content type: {file.content_type}")
277
+
278
+ if file.filename == '':
279
+ app.logger.error("No selected file (empty filename)")
280
+ return jsonify({'error': 'No selected file'}), 400
281
+
282
+ # Get the model name from the request and handle aliases
283
+ model_name = request.form.get('model', 'MobileNetV2')
284
+ app.logger.info(f"Using model: {model_name}")
285
+
286
+ # Handle model name aliases
287
+ if model_name.lower() in ['mobilenet', 'mobilenetv2', 'mobile']:
288
+ model_name = 'MobileNetV2'
289
+ elif model_name.lower() in ['efficientnet', 'efficientnetb0', 'efficient']:
290
+ model_name = 'EfficientNetB0'
291
+
292
+ # Check if the model is supported
293
+ if model_name not in models:
294
+ app.logger.error(f"Unsupported model: {model_name}")
295
+ return jsonify({'error': f'Unsupported model: {model_name}. Supported models are: {", ".join(models.keys())}'}), 400
296
+
297
+ try:
298
+ # Check if model files exist
299
+ if model_name == 'MobileNetV2' and not os.path.exists('models/mobilenetv2_ham10000_finetuned_74.h5'):
300
+ app.logger.error("MobileNetV2 model file not found")
301
+ return jsonify({'error': 'Model file not found. Please ensure the model is properly installed.'}), 500
302
+
303
+ if model_name == 'EfficientNetB0' and not os.path.exists('models/efficientnetb0_skin_cancer_model_final_69.keras'):
304
+ app.logger.error("EfficientNetB0 model file not found")
305
+ return jsonify({'error': 'Model file not found. Please ensure the model is properly installed.'}), 500
306
+
307
+ # Read and process the image
308
+ img_bytes = file.read()
309
+ if not img_bytes:
310
+ app.logger.error("Empty file content")
311
+ return jsonify({'error': 'Empty file content'}), 400
312
+
313
+ app.logger.info(f"Image bytes length: {len(img_bytes)}")
314
+
315
+ try:
316
+ img = Image.open(io.BytesIO(img_bytes)).convert('RGB')
317
+ app.logger.info(f"Image opened successfully: {img.size}")
318
+ except Exception as img_error:
319
+ app.logger.error(f"Failed to open image: {str(img_error)}")
320
+ return jsonify({'error': f'Failed to open image: {str(img_error)}'}), 400
321
+
322
+ # Make prediction
323
+ app.logger.info("Starting prediction...")
324
+ predictions = predict_image(img, model_name)
325
+ app.logger.info(f"Prediction completed: {predictions}")
326
+
327
+ return jsonify({
328
+ 'predictions': predictions
329
+ })
330
+
331
+ except Exception as e:
332
+ import traceback
333
+ app.logger.error(f"Prediction error: {str(e)}")
334
+ app.logger.error(traceback.format_exc())
335
+ return jsonify({'error': str(e)}), 500
336
+
337
+ @app.route('/privacy')
338
+ def privacy():
339
+ return render_template('privacy.html')
340
+
341
+ @app.route('/terms')
342
+ def terms():
343
+ return render_template('terms.html')
344
+
345
+ @app.errorhandler(404)
346
+ def page_not_found(e):
347
+ return render_template('404.html'), 404
348
+
349
+ @app.errorhandler(500)
350
+ def server_error(e):
351
+ return render_template('500.html'), 500
352
+
353
+ if __name__ == '__main__':
354
+ # Create models directory if it doesn't exist
355
+ os.makedirs('models', exist_ok=True)
356
+
357
+ # Check if model files exist, otherwise print a warning
358
+ if not os.path.exists('models/mobilenetv2_ham10000_finetuned_74.h5'):
359
+ print("Warning: MobileNetV2 model file not found. Please download or train the model.")
360
+
361
+ if not os.path.exists('models/efficientnetb0_skin_cancer_model_final_69.keras'):
362
+ print("Warning: EfficientNetB0 model file not found. Please download or train the model.")
363
+
364
+ app.run(debug=True)
models/efficientnetb0_skin_cancer_model_final_69.keras ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3df235db448d5f5dc0ee93b0221d89293aeb76976bb83906fcffcf59310c18f6
3
+ size 84869384
models/mobilenetv2_ham10000_finetuned_74.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f91c6fb315643e3c340e231e6512f380d8f95b7633bc55be1e1832540c477f7a
3
+ size 31291560
requirements.txt ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ absl-py==2.1.0
2
+ astunparse==1.6.3
3
+ blinker==1.9.0
4
+ certifi==2025.1.31
5
+ charset-normalizer==3.4.1
6
+ click==8.1.8
7
+ Flask==3.1.0
8
+ flatbuffers==25.2.10
9
+ gast==0.6.0
10
+ google-pasta==0.2.0
11
+ grpcio==1.71.0
12
+ h5py==3.13.0
13
+ idna==3.10
14
+ itsdangerous==2.2.0
15
+ Jinja2==3.1.6
16
+ keras==3.9.0
17
+ libclang==18.1.1
18
+ Markdown==3.7
19
+ markdown-it-py==3.0.0
20
+ MarkupSafe==3.0.2
21
+ mdurl==0.1.2
22
+ ml_dtypes==0.5.1
23
+ namex==0.0.8
24
+ numpy==2.1.3
25
+ opt_einsum==3.4.0
26
+ optree==0.14.1
27
+ packaging==24.2
28
+ pillow==11.1.0
29
+ protobuf==5.29.3
30
+ Pygments==2.19.1
31
+ requests==2.32.3
32
+ rich==13.9.4
33
+ setuptools==76.0.0
34
+ six==1.17.0
35
+ tensorboard==2.19.0
36
+ tensorboard-data-server==0.7.2
37
+ tensorflow==2.19.0
38
+ termcolor==2.5.0
39
+ typing_extensions==4.12.2
40
+ urllib3==2.3.0
41
+ Werkzeug==3.1.3
42
+ wheel==0.45.1
43
+ wrapt==1.17.2
static/css/.styles.css.kate-swp ADDED
Binary file (9.88 kB). View file
 
static/css/detect.css ADDED
@@ -0,0 +1,620 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Detection Page Specific Styles */
2
+ .detect-header {
3
+ background-color: var(--primary-color);
4
+ color: white;
5
+ padding: var(--spacing-xl) 0;
6
+ text-align: center;
7
+ }
8
+
9
+ .detect-header h1 {
10
+ color: white;
11
+ margin-bottom: var(--spacing-sm);
12
+ }
13
+
14
+ .detection-tool {
15
+ padding: var(--spacing-xl) 0;
16
+ background-color: var(--background-color);
17
+ }
18
+
19
+ .tool-container {
20
+ display: flex;
21
+ gap: var(--spacing-xl);
22
+ margin-bottom: var(--spacing-xl);
23
+ }
24
+
25
+ .tool-sidebar {
26
+ flex: 0 0 300px;
27
+ }
28
+
29
+ .tool-main {
30
+ flex: 1;
31
+ background-color: white;
32
+ border-radius: var(--radius-lg);
33
+ padding: var(--spacing-xl);
34
+ box-shadow: var(--shadow-md);
35
+ }
36
+
37
+ .info-box {
38
+ background-color: #e3f2fd;
39
+ border-radius: var(--radius-md);
40
+ padding: var(--spacing-lg);
41
+ margin-bottom: var(--spacing-lg);
42
+ }
43
+
44
+ .info-box h3 {
45
+ display: flex;
46
+ align-items: center;
47
+ color: var(--primary-color);
48
+ font-size: var(--font-size-lg);
49
+ margin-bottom: var(--spacing-md);
50
+ }
51
+
52
+ .info-box h3 i {
53
+ margin-right: var(--spacing-sm);
54
+ }
55
+
56
+ .info-box ul {
57
+ list-style-type: disc;
58
+ padding-left: var(--spacing-lg);
59
+ }
60
+
61
+ .info-box ul li {
62
+ margin-bottom: var(--spacing-sm);
63
+ }
64
+
65
+ .model-info {
66
+ background-color: white;
67
+ border-radius: var(--radius-md);
68
+ padding: var(--spacing-lg);
69
+ box-shadow: var(--shadow-sm);
70
+ }
71
+
72
+ .model-info h3 {
73
+ font-size: var(--font-size-lg);
74
+ margin-bottom: var(--spacing-md);
75
+ }
76
+
77
+ .model-info-item {
78
+ margin-bottom: var(--spacing-md);
79
+ }
80
+
81
+ .model-info-item h4 {
82
+ font-size: var(--font-size-md);
83
+ color: var(--primary-color);
84
+ margin-bottom: var(--spacing-xs);
85
+ }
86
+
87
+ .model-info-item p {
88
+ font-size: var(--font-size-sm);
89
+ margin-bottom: 0;
90
+ }
91
+
92
+ .form-group {
93
+ margin-bottom: var(--spacing-lg);
94
+ }
95
+
96
+ .form-group label {
97
+ display: block;
98
+ margin-bottom: var(--spacing-sm);
99
+ font-weight: var(--font-weight-medium);
100
+ }
101
+
102
+ .model-select {
103
+ width: 100%;
104
+ padding: 0.75rem;
105
+ border: 1px solid var(--border-color);
106
+ border-radius: var(--radius-md);
107
+ font-family: var(--font-family);
108
+ font-size: var(--font-size-md);
109
+ background-color: white;
110
+ transition: border-color var(--transition-fast);
111
+ }
112
+
113
+ .model-select:focus {
114
+ outline: none;
115
+ border-color: var(--primary-color);
116
+ }
117
+
118
+ .upload-container {
119
+ margin-bottom: var(--spacing-lg);
120
+ }
121
+
122
+ .upload-box {
123
+ border: 2px dashed var(--border-color);
124
+ border-radius: var(--radius-md);
125
+ padding: var(--spacing-xl);
126
+ text-align: center;
127
+ cursor: pointer;
128
+ transition: border-color var(--transition-fast);
129
+ position: relative;
130
+ min-height: 200px;
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: center;
134
+ }
135
+
136
+ .upload-box:hover {
137
+ border-color: var(--primary-color);
138
+ }
139
+
140
+ .upload-prompt {
141
+ display: flex;
142
+ flex-direction: column;
143
+ align-items: center;
144
+ }
145
+
146
+ .upload-icon {
147
+ font-size: 3rem;
148
+ color: var(--primary-color);
149
+ margin-bottom: var(--spacing-md);
150
+ }
151
+
152
+ .browse-text {
153
+ color: var(--primary-color);
154
+ font-weight: var(--font-weight-medium);
155
+ }
156
+
157
+ .file-types {
158
+ font-size: var(--font-size-sm);
159
+ color: var(--text-tertiary);
160
+ margin-top: var(--spacing-sm);
161
+ }
162
+
163
+ .preview-container {
164
+ width: 100%;
165
+ height: 100%;
166
+ position: absolute;
167
+ top: 0;
168
+ left: 0;
169
+ display: flex;
170
+ align-items: center;
171
+ justify-content: center;
172
+ padding: var(--spacing-md);
173
+ }
174
+
175
+ .preview-container img {
176
+ max-height: 200px;
177
+ max-width: 100%;
178
+ border-radius: var(--radius-sm);
179
+ }
180
+
181
+ .remove-image {
182
+ position: absolute;
183
+ top: 10px;
184
+ right: 10px;
185
+ background-color: rgba(0, 0, 0, 0.5);
186
+ color: white;
187
+ border: none;
188
+ width: 30px;
189
+ height: 30px;
190
+ border-radius: var(--radius-round);
191
+ display: flex;
192
+ align-items: center;
193
+ justify-content: center;
194
+ cursor: pointer;
195
+ transition: background-color var(--transition-fast);
196
+ }
197
+
198
+ .remove-image:hover {
199
+ background-color: var(--danger-color);
200
+ }
201
+
202
+ .analyze-button {
203
+ width: 100%;
204
+ padding: 1rem;
205
+ font-size: var(--font-size-lg);
206
+ }
207
+
208
+ .analyze-button:disabled {
209
+ background-color: var(--text-tertiary);
210
+ border-color: var(--text-tertiary);
211
+ cursor: not-allowed;
212
+ }
213
+
214
+ .loading {
215
+ display: none;
216
+ text-align: center;
217
+ margin: var(--spacing-xl) 0;
218
+ }
219
+
220
+ .spinner {
221
+ margin: 0 auto;
222
+ width: 70px;
223
+ text-align: center;
224
+ }
225
+
226
+ .spinner > div {
227
+ width: 18px;
228
+ height: 18px;
229
+ background-color: var(--primary-color);
230
+ border-radius: 100%;
231
+ display: inline-block;
232
+ animation: sk-bouncedelay 1.4s infinite ease-in-out both;
233
+ }
234
+
235
+ .spinner .bounce1 {
236
+ animation-delay: -0.32s;
237
+ }
238
+
239
+ .spinner .bounce2 {
240
+ animation-delay: -0.16s;
241
+ }
242
+
243
+ @keyframes sk-bouncedelay {
244
+ 0%, 80%, 100% {
245
+ transform: scale(0);
246
+ } 40% {
247
+ transform: scale(1.0);
248
+ }
249
+ }
250
+
251
+ .error-message {
252
+ display: flex;
253
+ align-items: center;
254
+ background-color: #ffebee;
255
+ border-radius: var(--radius-md);
256
+ padding: var(--spacing-lg);
257
+ margin: var(--spacing-lg) 0;
258
+ }
259
+
260
+ .error-icon {
261
+ font-size: 2rem;
262
+ color: var(--danger-color);
263
+ margin-right: var(--spacing-lg);
264
+ }
265
+
266
+ .error-content h3 {
267
+ color: var(--danger-color);
268
+ margin-bottom: var(--spacing-xs);
269
+ }
270
+
271
+ .result-card {
272
+ margin-top: var(--spacing-xl);
273
+ background-color: white;
274
+ border-radius: var(--radius-lg);
275
+ box-shadow: var(--shadow-md);
276
+ overflow: hidden;
277
+ }
278
+
279
+ .result-header {
280
+ background-color: var(--primary-color);
281
+ color: white;
282
+ padding: var(--spacing-lg);
283
+ display: flex;
284
+ justify-content: space-between;
285
+ align-items: center;
286
+ }
287
+
288
+ .result-header h2 {
289
+ color: white;
290
+ margin-bottom: 0;
291
+ display: flex;
292
+ align-items: center;
293
+ }
294
+
295
+ .result-header h2 i {
296
+ margin-right: var(--spacing-sm);
297
+ }
298
+
299
+ .model-badge {
300
+ background-color: rgba(255, 255, 255, 0.2);
301
+ padding: 0.25rem 0.75rem;
302
+ border-radius: var(--radius-md);
303
+ font-size: var(--font-size-sm);
304
+ font-weight: var(--font-weight-medium);
305
+ }
306
+
307
+ .result-summary {
308
+ display: flex;
309
+ padding: var(--spacing-lg);
310
+ border-bottom: 1px solid var(--border-color);
311
+ }
312
+
313
+ .result-image-container {
314
+ flex: 0 0 40%;
315
+ margin-right: var(--spacing-lg);
316
+ }
317
+
318
+ .result-image-container img {
319
+ width: 100%;
320
+ border-radius: var(--radius-md);
321
+ box-shadow: var(--shadow-sm);
322
+ }
323
+
324
+ .result-details {
325
+ flex: 1;
326
+ }
327
+
328
+ .prediction-box {
329
+ background-color: #f5f5f5;
330
+ border-radius: var(--radius-md);
331
+ padding: var(--spacing-lg);
332
+ margin-bottom: var(--spacing-lg);
333
+ }
334
+
335
+ .prediction-box h3 {
336
+ font-size: var(--font-size-md);
337
+ color: var(--text-secondary);
338
+ margin-bottom: var(--spacing-sm);
339
+ }
340
+
341
+ .prediction-name {
342
+ font-size: var(--font-size-xl);
343
+ font-weight: var(--font-weight-bold);
344
+ margin-bottom: var(--spacing-sm);
345
+ color: var(--primary-color);
346
+ }
347
+
348
+ .prediction-description {
349
+ margin-bottom: var(--spacing-md);
350
+ font-size: var(--font-size-md);
351
+ }
352
+
353
+ .confidence-meter {
354
+ display: flex;
355
+ align-items: center;
356
+ margin-top: var(--spacing-md);
357
+ }
358
+
359
+ .confidence-label {
360
+ margin-right: var(--spacing-sm);
361
+ font-weight: var(--font-weight-medium);
362
+ font-size: var(--font-size-sm);
363
+ }
364
+
365
+ .confidence-bar {
366
+ flex: 1;
367
+ height: 10px;
368
+ background-color: #e0e0e0;
369
+ border-radius: var(--radius-sm);
370
+ overflow: hidden;
371
+ margin-right: var(--spacing-sm);
372
+ }
373
+
374
+ .confidence-fill {
375
+ height: 100%;
376
+ background-color: var(--primary-color);
377
+ width: 0%;
378
+ transition: width var(--transition-normal);
379
+ }
380
+
381
+ .confidence-value {
382
+ font-weight: var(--font-weight-bold);
383
+ font-size: var(--font-size-sm);
384
+ }
385
+
386
+ .severity-indicator {
387
+ background-color: #f5f5f5;
388
+ border-radius: var(--radius-md);
389
+ padding: var(--spacing-lg);
390
+ }
391
+
392
+ .severity-indicator h3 {
393
+ font-size: var(--font-size-md);
394
+ margin-bottom: var(--spacing-sm);
395
+ }
396
+
397
+ .severity-level {
398
+ display: flex;
399
+ align-items: center;
400
+ }
401
+
402
+ .severity-level i {
403
+ margin-right: var(--spacing-sm);
404
+ }
405
+
406
+ .severity-text {
407
+ font-weight: var(--font-weight-medium);
408
+ }
409
+
410
+ .severity-low {
411
+ color: var(--success-color);
412
+ }
413
+
414
+ .severity-moderate {
415
+ color: var(--warning-color);
416
+ }
417
+
418
+ .severity-high, .severity-very-high {
419
+ color: var(--danger-color);
420
+ }
421
+
422
+ .result-tabs {
423
+ display: flex;
424
+ border-bottom: 1px solid var(--border-color);
425
+ }
426
+
427
+ .tab-button {
428
+ padding: var(--spacing-md) var(--spacing-lg);
429
+ background: none;
430
+ border: none;
431
+ border-bottom: 3px solid transparent;
432
+ cursor: pointer;
433
+ font-weight: var(--font-weight-medium);
434
+ transition: all var(--transition-fast);
435
+ }
436
+
437
+ .tab-button:hover {
438
+ background-color: rgba(0, 0, 0, 0.02);
439
+ }
440
+
441
+ .tab-button.active {
442
+ color: var(--primary-color);
443
+ border-bottom-color: var(--primary-color);
444
+ }
445
+
446
+ .tab-content {
447
+ padding: var(--spacing-lg);
448
+ }
449
+
450
+ .tab-pane {
451
+ display: none;
452
+ }
453
+
454
+ .tab-pane.active {
455
+ display: block;
456
+ }
457
+
458
+ .tab-pane h3 {
459
+ margin-bottom: var(--spacing-md);
460
+ font-size: var(--font-size-lg);
461
+ }
462
+
463
+ .probabilities-grid {
464
+ display: grid;
465
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
466
+ gap: var(--spacing-md);
467
+ }
468
+
469
+ .probability-item {
470
+ background-color: #f5f5f5;
471
+ border-radius: var(--radius-md);
472
+ padding: var(--spacing-md);
473
+ display: flex;
474
+ flex-direction: column;
475
+ }
476
+
477
+ .probability-class {
478
+ font-weight: var(--font-weight-medium);
479
+ margin-bottom: var(--spacing-xs);
480
+ }
481
+
482
+ .probability-bar {
483
+ height: 8px;
484
+ background-color: #e0e0e0;
485
+ border-radius: var(--radius-sm);
486
+ overflow: hidden;
487
+ margin: var(--spacing-xs) 0;
488
+ }
489
+
490
+ .probability-fill {
491
+ height: 100%;
492
+ background-color: var(--primary-color);
493
+ }
494
+
495
+ .probability-value {
496
+ font-size: var(--font-size-sm);
497
+ color: var(--text-secondary);
498
+ align-self: flex-end;
499
+ }
500
+
501
+ .info-resources ul {
502
+ list-style-type: none;
503
+ padding: 0;
504
+ }
505
+
506
+ .info-resources ul li {
507
+ margin-bottom: var(--spacing-sm);
508
+ }
509
+
510
+ .info-resources ul li a {
511
+ display: flex;
512
+ align-items: center;
513
+ padding: var(--spacing-sm);
514
+ background-color: #f5f5f5;
515
+ border-radius: var(--radius-md);
516
+ transition: background-color var(--transition-fast);
517
+ }
518
+
519
+ .info-resources ul li a:hover {
520
+ background-color: #e0e0e0;
521
+ }
522
+
523
+ .info-resources ul li a i {
524
+ margin-right: var(--spacing-sm);
525
+ color: var(--primary-color);
526
+ }
527
+
528
+ .next-steps-content {
529
+ margin-bottom: var(--spacing-lg);
530
+ }
531
+
532
+ .next-step {
533
+ display: flex;
534
+ margin-bottom: var(--spacing-md);
535
+ padding: var(--spacing-md);
536
+ background-color: #f5f5f5;
537
+ border-radius: var(--radius-md);
538
+ }
539
+
540
+ .step-icon {
541
+ flex: 0 0 50px;
542
+ height: 50px;
543
+ background-color: var(--primary-light);
544
+ color: white;
545
+ border-radius: var(--radius-round);
546
+ display: flex;
547
+ align-items: center;
548
+ justify-content: center;
549
+ font-size: 1.25rem;
550
+ margin-right: var(--spacing-md);
551
+ }
552
+
553
+ .step-content {
554
+ flex: 1;
555
+ }
556
+
557
+ .step-content h4 {
558
+ margin-bottom: var(--spacing-xs);
559
+ }
560
+
561
+ .step-content p {
562
+ margin-bottom: 0;
563
+ font-size: var(--font-size-sm);
564
+ }
565
+
566
+ .disclaimer-box {
567
+ background-color: #fff8e1;
568
+ border-left: 4px solid var(--warning-color);
569
+ padding: var(--spacing-md);
570
+ border-radius: var(--radius-md);
571
+ }
572
+
573
+ .disclaimer-box p {
574
+ margin-bottom: 0;
575
+ font-size: var(--font-size-sm);
576
+ }
577
+
578
+ .result-actions {
579
+ display: flex;
580
+ justify-content: flex-end;
581
+ gap: var(--spacing-md);
582
+ padding: var(--spacing-lg);
583
+ border-top: 1px solid var(--border-color);
584
+ }
585
+
586
+ @media (max-width: 992px) {
587
+ .tool-container {
588
+ flex-direction: column;
589
+ }
590
+
591
+ .tool-sidebar {
592
+ flex: 0 0 auto;
593
+ width: 100%;
594
+ }
595
+ }
596
+
597
+ @media (max-width: 768px) {
598
+ .result-summary {
599
+ flex-direction: column;
600
+ }
601
+
602
+ .result-image-container {
603
+ flex: 0 0 auto;
604
+ margin-right: 0;
605
+ margin-bottom: var(--spacing-lg);
606
+ }
607
+
608
+ .result-tabs {
609
+ overflow-x: auto;
610
+ white-space: nowrap;
611
+ }
612
+
613
+ .tab-button {
614
+ padding: var(--spacing-md) var(--spacing-sm);
615
+ }
616
+
617
+ .probabilities-grid {
618
+ grid-template-columns: 1fr;
619
+ }
620
+ }
static/css/styles.css ADDED
@@ -0,0 +1,1556 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary-color: #2A5C82;
3
+ --primary-light: #3a7cb2;
4
+ --primary-dark: #1d4060;
5
+ --secondary-color: #5BA4E6;
6
+ --secondary-light: #7bbcf7;
7
+ --secondary-dark: #4183c4;
8
+ --accent-color: #FF6B6B;
9
+ --success-color: #4CAF50;
10
+ --warning-color: #FFC107;
11
+ --danger-color: #F44336;
12
+ --info-color: #2196F3;
13
+ --background-color: #f8f9fa;
14
+ --surface-color: #ffffff;
15
+ --text-primary: #333333;
16
+ --text-secondary: #666666;
17
+ --text-tertiary: #999999;
18
+ --border-color: #e0e0e0;
19
+ --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.05);
20
+ --shadow-md: 0 4px 8px rgba(0, 0, 0, 0.1);
21
+ --shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.1);
22
+ --radius-sm: 4px;
23
+ --radius-md: 8px;
24
+ --radius-lg: 16px;
25
+ --radius-xl: 24px;
26
+ --radius-round: 50%;
27
+ --spacing-xs: 0.25rem;
28
+ --spacing-sm: 0.5rem;
29
+ --spacing-md: 1rem;
30
+ --spacing-lg: 1.5rem;
31
+ --spacing-xl: 2rem;
32
+ --spacing-xxl: 3rem;
33
+ --font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
34
+ --font-size-xs: 0.75rem;
35
+ --font-size-sm: 0.875rem;
36
+ --font-size-md: 1rem;
37
+ --font-size-lg: 1.125rem;
38
+ --font-size-xl: 1.25rem;
39
+ --font-size-xxl: 1.5rem;
40
+ --font-size-display: 2rem;
41
+ --font-size-jumbo: 2.5rem;
42
+ --font-weight-light: 300;
43
+ --font-weight-regular: 400;
44
+ --font-weight-medium: 500;
45
+ --font-weight-semibold: 600
46
+ --font-weight-semibold: 600;
47
+ --font-weight-bold: 700;
48
+ --line-height-tight: 1.2;
49
+ --line-height-normal: 1.5;
50
+ --line-height-loose: 1.8;
51
+ --transition-fast: 0.2s ease;
52
+ --transition-normal: 0.3s ease;
53
+ --transition-slow: 0.5s ease;
54
+ --container-max-width: 1200px;
55
+ --container-padding: 1rem;
56
+ }
57
+
58
+ /* Reset & Base Styles */
59
+ *, *::before, *::after {
60
+ box-sizing: border-box;
61
+ margin: 0;
62
+ padding: 0;
63
+ }
64
+
65
+ html {
66
+ font-size: 16px;
67
+ scroll-behavior: smooth;
68
+ }
69
+
70
+ body {
71
+ font-family: var(--font-family);
72
+ font-size: var(--font-size-md);
73
+ line-height: var(--line-height-normal);
74
+ color: var(--text-primary);
75
+ background-color: var(--background-color);
76
+ min-height: 100vh;
77
+ display: flex;
78
+ flex-direction: column;
79
+ }
80
+
81
+ main {
82
+ flex: 1;
83
+ }
84
+
85
+ img {
86
+ max-width: 100%;
87
+ height: auto;
88
+ }
89
+
90
+ a {
91
+ color: var(--primary-color);
92
+ text-decoration: none;
93
+ transition: color var(--transition-fast);
94
+ }
95
+
96
+ a:hover {
97
+ color: var(--primary-light);
98
+ }
99
+
100
+ ul, ol {
101
+ list-style-position: inside;
102
+ }
103
+
104
+ /* Typography */
105
+ h1, h2, h3, h4, h5, h6 {
106
+ margin-bottom: var(--spacing-md);
107
+ font-weight: var(--font-weight-semibold);
108
+ line-height: var(--line-height-tight);
109
+ color: var(--text-primary);
110
+ }
111
+
112
+ h1 {
113
+ font-size: var(--font-size-jumbo);
114
+ }
115
+
116
+ h2 {
117
+ font-size: var(--font-size-display);
118
+ }
119
+
120
+ h3 {
121
+ font-size: var(--font-size-xxl);
122
+ }
123
+
124
+ h4 {
125
+ font-size: var(--font-size-xl);
126
+ }
127
+
128
+ h5 {
129
+ font-size: var(--font-size-lg);
130
+ }
131
+
132
+ h6 {
133
+ font-size: var(--font-size-md);
134
+ }
135
+
136
+ p {
137
+ margin-bottom: var(--spacing-md);
138
+ }
139
+
140
+ .text-center {
141
+ text-align: center;
142
+ }
143
+
144
+ .text-right {
145
+ text-align: right;
146
+ }
147
+
148
+ .text-primary {
149
+ color: var(--primary-color);
150
+ }
151
+
152
+ .text-secondary {
153
+ color: var(--secondary-color);
154
+ }
155
+
156
+ .text-accent {
157
+ color: var(--accent-color);
158
+ }
159
+
160
+ .text-success {
161
+ color: var(--success-color);
162
+ }
163
+
164
+ .text-warning {
165
+ color: var(--warning-color);
166
+ }
167
+
168
+ .text-danger {
169
+ color: var(--danger-color);
170
+ }
171
+
172
+ .text-info {
173
+ color: var(--info-color);
174
+ }
175
+
176
+ /* Layout */
177
+ .container {
178
+ width: 100%;
179
+ max-width: var(--container-max-width);
180
+ margin: 0 auto;
181
+ padding: 0 var(--container-padding);
182
+ }
183
+
184
+ .section-title {
185
+ text-align: center;
186
+ margin-bottom: var(--spacing-xl);
187
+ position: relative;
188
+ padding-bottom: var(--spacing-md);
189
+ }
190
+
191
+ .section-title::after {
192
+ content: '';
193
+ position: absolute;
194
+ bottom: 0;
195
+ left: 50%;
196
+ transform: translateX(-50%);
197
+ width: 60px;
198
+ height: 3px;
199
+ background-color: var(--primary-color);
200
+ }
201
+
202
+ /* Buttons */
203
+ .btn {
204
+ display: inline-block;
205
+ font-weight: var(--font-weight-medium);
206
+ text-align: center;
207
+ white-space: nowrap;
208
+ vertical-align: middle;
209
+ user-select: none;
210
+ border: 1px solid transparent;
211
+ padding: 0.75rem 1.5rem;
212
+ font-size: var(--font-size-md);
213
+ line-height: 1.5;
214
+ border-radius: var(--radius-md);
215
+ transition: all var(--transition-fast);
216
+ cursor: pointer;
217
+ }
218
+
219
+ .btn:focus, .btn:hover {
220
+ text-decoration: none;
221
+ }
222
+
223
+ .btn-primary {
224
+ color: white;
225
+ background-color: var(--primary-color);
226
+ border-color: var(--primary-color);
227
+ }
228
+
229
+ .btn-primary:hover, .btn-primary:focus {
230
+ background-color: var(--primary-dark);
231
+ border-color: var(--primary-dark);
232
+ color: white;
233
+ }
234
+
235
+ .btn-secondary {
236
+ color: white;
237
+ background-color: var(--secondary-color);
238
+ border-color: var(--secondary-color);
239
+ }
240
+
241
+ .btn-secondary:hover, .btn-secondary:focus {
242
+ background-color: var(--secondary-dark);
243
+ border-color: var(--secondary-dark);
244
+ color: white;
245
+ }
246
+
247
+ .btn-accent {
248
+ color: white;
249
+ background-color: var(--accent-color);
250
+ border-color: var(--accent-color);
251
+ }
252
+
253
+ .btn-accent:hover, .btn-accent:focus {
254
+ background-color: #e55c5c;
255
+ border-color: #e55c5c;
256
+ color: white;
257
+ }
258
+
259
+ .btn-outline {
260
+ color: var(--primary-color);
261
+ background-color: transparent;
262
+ border-color: var(--primary-color);
263
+ }
264
+
265
+ .btn-outline:hover, .btn-outline:focus {
266
+ color: white;
267
+ background-color: var(--primary-color);
268
+ border-color: var(--primary-color);
269
+ }
270
+
271
+ .btn-sm {
272
+ padding: 0.5rem 1rem;
273
+ font-size: var(--font-size-sm);
274
+ }
275
+
276
+ .btn-lg {
277
+ padding: 1rem 2rem;
278
+ font-size: var(--font-size-lg);
279
+ }
280
+
281
+ .btn-block {
282
+ display: block;
283
+ width: 100%;
284
+ }
285
+
286
+ .btn-icon {
287
+ display: inline-flex;
288
+ align-items: center;
289
+ justify-content: center;
290
+ }
291
+
292
+ .btn-icon i {
293
+ margin-right: 0.5rem;
294
+ }
295
+
296
+ /* Navbar */
297
+ .navbar {
298
+ background-color: var(--surface-color);
299
+ box-shadow: var(--shadow-sm);
300
+ padding: 1rem 0;
301
+ position: sticky;
302
+ top: 0;
303
+ z-index: 1000;
304
+ }
305
+
306
+ .navbar .container {
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: space-between;
310
+ }
311
+
312
+ .logo {
313
+ display: flex;
314
+ align-items: center;
315
+ font-weight: var(--font-weight-bold);
316
+ font-size: var(--font-size-xl);
317
+ color: var(--primary-color);
318
+ }
319
+
320
+ .logo img {
321
+ height: 40px;
322
+ margin-right: var(--spacing-sm);
323
+ }
324
+
325
+ .nav-links {
326
+ display: flex;
327
+ align-items: center;
328
+ }
329
+
330
+ .nav-links a {
331
+ margin-left: var(--spacing-lg);
332
+ color: var(--text-primary);
333
+ font-weight: var(--font-weight-medium);
334
+ position: relative;
335
+ }
336
+
337
+ .nav-links a:hover {
338
+ color: var(--primary-color);
339
+ }
340
+
341
+ .nav-links a.active {
342
+ color: var(--primary-color);
343
+ }
344
+
345
+ .nav-links a.active::after {
346
+ content: '';
347
+ position: absolute;
348
+ bottom: -5px;
349
+ left: 0;
350
+ width: 100%;
351
+ height: 2px;
352
+ background-color: var(--primary-color);
353
+ }
354
+
355
+ .mobile-menu-toggle {
356
+ display: none;
357
+ background: none;
358
+ border: none;
359
+ cursor: pointer;
360
+ }
361
+
362
+ .bar {
363
+ display: block;
364
+ width: 25px;
365
+ height: 3px;
366
+ margin: 5px auto;
367
+ background-color: var(--text-primary);
368
+ transition: all var(--transition-fast);
369
+ }
370
+
371
+ /* Hero Section */
372
+ .hero {
373
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
374
+ color: white;
375
+ padding: var(--spacing-xxl) 0;
376
+ text-align: center;
377
+ position: relative;
378
+ overflow: hidden;
379
+ }
380
+
381
+ .hero::before {
382
+ content: '';
383
+ position: absolute;
384
+ top: 0;
385
+ left: 0;
386
+ width: 100%;
387
+ height: 100%;
388
+ background-image: url('../images/hero-bg.jpg');
389
+ background-size: cover;
390
+ background-position: center;
391
+ opacity: 0.15;
392
+ z-index: 0;
393
+ }
394
+
395
+ .hero-content {
396
+ position: relative;
397
+ z-index: 1;
398
+ max-width: 800px;
399
+ margin: 0 auto;
400
+ }
401
+
402
+ .hero h1 {
403
+ font-size: var(--font-size-jumbo);
404
+ margin-bottom: var(--spacing-md);
405
+ color: white;
406
+ }
407
+
408
+ .hero-subtitle {
409
+ font-size: var(--font-size-xl);
410
+ margin-bottom: var(--spacing-xl);
411
+ font-weight: var(--font-weight-light);
412
+ }
413
+
414
+ .hero-buttons {
415
+ display: flex;
416
+ justify-content: center;
417
+ gap: var(--spacing-md);
418
+ }
419
+
420
+ /* How It Works Section */
421
+ .how-it-works {
422
+ padding: var(--spacing-xxl) 0;
423
+ background-color: var(--surface-color);
424
+ }
425
+
426
+ .steps {
427
+ display: flex;
428
+ justify-content: space-between;
429
+ gap: var(--spacing-xl);
430
+ }
431
+
432
+ .step {
433
+ flex: 1;
434
+ text-align: center;
435
+ padding: var(--spacing-lg);
436
+ background-color: white;
437
+ border-radius: var(--radius-md);
438
+ box-shadow: var(--shadow-md);
439
+ transition: transform var(--transition-normal);
440
+ }
441
+
442
+ .step:hover {
443
+ transform: translateY(-5px);
444
+ }
445
+
446
+ .step-icon {
447
+ width: 80px;
448
+ height: 80px;
449
+ margin: 0 auto var(--spacing-md);
450
+ background-color: var(--primary-light);
451
+ color: white;
452
+ border-radius: var(--radius-round);
453
+ display: flex;
454
+ align-items: center;
455
+ justify-content: center;
456
+ font-size: 2rem;
457
+ }
458
+
459
+ .step h3 {
460
+ margin-bottom: var(--spacing-sm);
461
+ }
462
+
463
+ /* Models Section */
464
+ .models {
465
+ padding: var(--spacing-xxl) 0;
466
+ background-color: var(--background-color);
467
+ }
468
+
469
+ .model-cards {
470
+ display: flex;
471
+ justify-content: center;
472
+ gap: var(--spacing-xl);
473
+ }
474
+
475
+ .model-card {
476
+ flex: 1;
477
+ max-width: 400px;
478
+ padding: var(--spacing-xl);
479
+ background-color: white;
480
+ border-radius: var(--radius-lg);
481
+ box-shadow: var(--shadow-md);
482
+ transition: transform var(--transition-normal), box-shadow var(--transition-normal);
483
+ }
484
+
485
+ .model-card:hover {
486
+ transform: translateY(-5px);
487
+ box-shadow: var(--shadow-lg);
488
+ }
489
+
490
+ .model-icon {
491
+ width: 70px;
492
+ height: 70px;
493
+ margin: 0 auto var(--spacing-md);
494
+ background-color: var(--secondary-light);
495
+ color: white;
496
+ border-radius: var(--radius-round);
497
+ display: flex;
498
+ align-items: center;
499
+ justify-content: center;
500
+ font-size: 1.75rem;
501
+ }
502
+
503
+ .model-card h3 {
504
+ text-align: center;
505
+ margin-bottom: var(--spacing-md);
506
+ }
507
+
508
+ .model-features {
509
+ margin-top: var(--spacing-md);
510
+ list-style: none;
511
+ }
512
+
513
+ .model-features li {
514
+ padding: var(--spacing-sm) 0;
515
+ display: flex;
516
+ align-items: center;
517
+ }
518
+
519
+ .model-features li i {
520
+ color: var(--success-color);
521
+ margin-right: var(--spacing-sm);
522
+ }
523
+
524
+ /* Stats Section */
525
+ .stats {
526
+ padding: var(--spacing-xxl) 0;
527
+ background-color: var(--primary-color);
528
+ color: white;
529
+ }
530
+
531
+ .stats-grid {
532
+ display: flex;
533
+ justify-content: space-around;
534
+ text-align: center;
535
+ }
536
+
537
+ .stat-item {
538
+ padding: var(--spacing-lg);
539
+ }
540
+
541
+ .stat-number {
542
+ font-size: 3rem;
543
+ font-weight: var(--font-weight-bold);
544
+ margin-bottom: var(--spacing-xs);
545
+ line-height: 1;
546
+ }
547
+
548
+ .stat-label {
549
+ font-size: var(--font-size-lg);
550
+ font-weight: var(--font-weight-medium);
551
+ margin-bottom: var(--spacing-sm);
552
+ color: var(--secondary-light);
553
+ }
554
+
555
+ .stat-desc {
556
+ font-size: var(--font-size-sm);
557
+ max-width: 200px;
558
+ margin: 0 auto;
559
+ }
560
+
561
+ /* CTA Section */
562
+ .cta {
563
+ padding: var(--spacing-xxl) 0;
564
+ background-color: var(--secondary-color);
565
+ color: white;
566
+ text-align: center;
567
+ }
568
+
569
+ .cta h2 {
570
+ color: white;
571
+ margin-bottom: var(--spacing-sm);
572
+ }
573
+
574
+ .cta p {
575
+ margin-bottom: var(--spacing-lg);
576
+ font-size: var(--font-size-lg);
577
+ }
578
+
579
+ /* Disclaimer Section */
580
+ .disclaimer {
581
+ padding: var(--spacing-xl) 0;
582
+ background-color: var(--surface-color);
583
+ }
584
+
585
+ .disclaimer-box {
586
+ background-color: #fff8e1;
587
+ border-left: 4px solid var(--warning-color);
588
+ padding: var(--spacing-lg);
589
+ border-radius: var(--radius-md);
590
+ }
591
+
592
+ .disclaimer-box h3 {
593
+ color: #856404;
594
+ display: flex;
595
+ align-items: center;
596
+ margin-bottom: var(--spacing-sm);
597
+ }
598
+
599
+ .disclaimer-box h3 i {
600
+ margin-right: var(--spacing-sm);
601
+ }
602
+
603
+ .disclaimer-box p {
604
+ margin-bottom: 0;
605
+ color: #856404;
606
+ }
607
+
608
+ /* Footer */
609
+ footer {
610
+ background-color: #2c3e50;
611
+ color: white;
612
+ padding: var(--spacing-xl) 0 var(--spacing-md);
613
+ }
614
+
615
+ .footer-grid {
616
+ display: grid;
617
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
618
+ gap: var(--spacing-xl);
619
+ margin-bottom: var(--spacing-xl);
620
+ }
621
+
622
+ .footer-col h3 {
623
+ color: white;
624
+ margin-bottom: var(--spacing-md);
625
+ font-size: var(--font-size-lg);
626
+ }
627
+
628
+ .footer-col p {
629
+ color: #b3b3b3;
630
+ margin-bottom: var(--spacing-md);
631
+ }
632
+
633
+ .footer-col ul {
634
+ list-style: none;
635
+ }
636
+
637
+ .footer-col ul li {
638
+ margin-bottom: var(--spacing-sm);
639
+ }
640
+
641
+ .footer-col ul li a {
642
+ color: #b3b3b3;
643
+ transition: color var(--transition-fast);
644
+ }
645
+
646
+ .footer-col ul li a:hover {
647
+ color: white;
648
+ }
649
+
650
+ .social-links {
651
+ display: flex;
652
+ gap: var(--spacing-sm);
653
+ margin-top: var(--spacing-md);
654
+ }
655
+
656
+ .social-links a {
657
+ display: flex;
658
+ align-items: center;
659
+ justify-content: center;
660
+ width: 36px;
661
+ height: 36px;
662
+ background-color: rgba(255, 255, 255, 0.1);
663
+ border-radius: var(--radius-round);
664
+ color: white;
665
+ transition: background-color var(--transition-fast);
666
+ }
667
+
668
+ .social-links a:hover {
669
+ background-color: var(--primary-color);
670
+ }
671
+
672
+ .footer-bottom {
673
+ text-align: center;
674
+ padding-top: var(--spacing-lg);
675
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
676
+ color: #b3b3b3;
677
+ font-size: var(--font-size-sm);
678
+ }
679
+
680
+ /* Utility Classes */
681
+ .hidden {
682
+ display: none !important;
683
+ }
684
+
685
+ .visible {
686
+ display: block !important;
687
+ }
688
+
689
+ .flex {
690
+ display: flex;
691
+ }
692
+
693
+ .flex-column {
694
+ flex-direction: column;
695
+ }
696
+
697
+ .items-center {
698
+ align-items: center;
699
+ }
700
+
701
+ .justify-center {
702
+ justify-content: center;
703
+ }
704
+
705
+ .justify-between {
706
+ justify-content: space-between;
707
+ }
708
+
709
+ .text-center {
710
+ text-align: center;
711
+ }
712
+
713
+ .mt-1 { margin-top: var(--spacing-xs); }
714
+ .mt-2 { margin-top: var(--spacing-sm); }
715
+ .mt-3 { margin-top: var(--spacing-md); }
716
+ .mt-4 { margin-top: var(--spacing-lg); }
717
+ .mt-5 { margin-top: var(--spacing-xl); }
718
+
719
+ .mb-1 { margin-bottom: var(--spacing-xs); }
720
+ .mb-2 { margin-bottom: var(--spacing-sm); }
721
+ .mb-3 { margin-bottom: var(--spacing-md); }
722
+ .mb-4 { margin-bottom: var(--spacing-lg); }
723
+ .mb-5 { margin-bottom: var(--spacing-xl); }
724
+
725
+ .mx-auto { margin-left: auto; margin-right: auto; }
726
+
727
+ .p-1 { padding: var(--spacing-xs); }
728
+ .p-2 { padding: var(--spacing-sm); }
729
+ .p-3 { padding: var(--spacing-md); }
730
+ .p-4 { padding: var(--spacing-lg); }
731
+ .p-5 { padding: var(--spacing-xl); }
732
+
733
+ /* Error Pages */
734
+ .error-container {
735
+ text-align: center;
736
+ padding: var(--spacing-xxl) var(--spacing-lg);
737
+ max-width: 800px;
738
+ margin: 0 auto;
739
+ }
740
+
741
+ .error-container h1 {
742
+ font-size: var(--font-size-jumbo);
743
+ color: var(--danger-color);
744
+ margin-bottom: var(--spacing-lg);
745
+ }
746
+
747
+ .error-container p {
748
+ font-size: var(--font-size-lg);
749
+ margin-bottom: var(--spacing-xl);
750
+ color: var(--text-secondary);
751
+ }
752
+
753
+ .actions {
754
+ display: flex;
755
+ gap: var(--spacing-md);
756
+ justify-content: center;
757
+ margin-top: var(--spacing-xl);
758
+ }
759
+
760
+ /* FAQ Section */
761
+ .faq-preview {
762
+ padding: var(--spacing-xxl) 0;
763
+ background-color: var(--background-color);
764
+ }
765
+
766
+ .faq-items {
767
+ margin-bottom: var(--spacing-xl);
768
+ }
769
+
770
+ .faq-item {
771
+ background-color: white;
772
+ border-radius: var(--radius-md);
773
+ margin-bottom: var(--spacing-md);
774
+ box-shadow: var(--shadow-sm);
775
+ overflow: hidden;
776
+ }
777
+
778
+ .faq-question {
779
+ padding: var(--spacing-lg);
780
+ display: flex;
781
+ justify-content: space-between;
782
+ align-items: center;
783
+ cursor: pointer;
784
+ transition: background-color var(--transition-fast);
785
+ }
786
+
787
+ .faq-question:hover {
788
+ background-color: rgba(0, 0, 0, 0.02);
789
+ }
790
+
791
+ .faq-question h3 {
792
+ margin-bottom: 0;
793
+ font-size: var(--font-size-lg);
794
+ }
795
+
796
+ .faq-toggle {
797
+ color: var(--primary-color);
798
+ transition: transform var(--transition-fast);
799
+ }
800
+
801
+ .faq-item.active .faq-toggle {
802
+ transform: rotate(180deg);
803
+ }
804
+
805
+ .faq-answer {
806
+ padding: 0 var(--spacing-lg);
807
+ max-height: 0;
808
+ overflow: hidden;
809
+ transition: max-height var(--transition-normal), padding var(--transition-normal);
810
+ }
811
+
812
+ .faq-item.active .faq-answer {
813
+ padding: 0 var(--spacing-lg) var(--spacing-lg);
814
+ max-height: 500px;
815
+ }
816
+
817
+ .faq-more {
818
+ text-align: center;
819
+ }
820
+
821
+ /* Responsive Design */
822
+ @media (max-width: 992px) {
823
+ .steps, .model-cards {
824
+ flex-direction: column;
825
+ align-items: center;
826
+ }
827
+
828
+ .step, .model-card {
829
+ width: 100%;
830
+ max-width: 500px;
831
+ margin-bottom: var(--spacing-lg);
832
+ }
833
+
834
+ .stats-grid {
835
+ flex-wrap: wrap;
836
+ }
837
+
838
+ .stat-item {
839
+ flex: 0 0 50%;
840
+ margin-bottom: var(--spacing-lg);
841
+ }
842
+ }
843
+
844
+ @media (max-width: 768px) {
845
+ :root {
846
+ --font-size-jumbo: 2rem;
847
+ --font-size-display: 1.75rem;
848
+ }
849
+
850
+ .mobile-menu-toggle {
851
+ display: block;
852
+ z-index: 1001;
853
+ }
854
+
855
+ .nav-links {
856
+ position: fixed;
857
+ top: 0;
858
+ right: -100%;
859
+ width: 70%;
860
+ height: 100vh;
861
+ background-color: white;
862
+ flex-direction: column;
863
+ align-items: flex-start;
864
+ padding: 80px var(--spacing-lg) var(--spacing-lg);
865
+ transition: right var(--transition-normal);
866
+ box-shadow: var(--shadow-lg);
867
+ z-index: 1000;
868
+ }
869
+
870
+ .nav-links.active {
871
+ right: 0;
872
+ }
873
+
874
+ .nav-links a {
875
+ margin: var(--spacing-sm) 0;
876
+ font-size: var(--font-size-lg);
877
+ width: 100%;
878
+ padding: var(--spacing-sm) 0;
879
+ }
880
+
881
+ .hero-buttons {
882
+ flex-direction: column;
883
+ gap: var(--spacing-sm);
884
+ }
885
+
886
+ .footer-grid {
887
+ grid-template-columns: 1fr;
888
+ }
889
+
890
+ .stat-item {
891
+ flex: 0 0 100%;
892
+ }
893
+ }
894
+
895
+ @media (max-width: 576px) {
896
+ .container {
897
+ padding: 0 var(--spacing-md);
898
+ }
899
+
900
+ .hero {
901
+ padding: var(--spacing-xl) 0;
902
+ }
903
+
904
+ .hero h1 {
905
+ font-size: 1.75rem;
906
+ }
907
+
908
+ .hero-subtitle {
909
+ font-size: var(--font-size-md);
910
+ }
911
+
912
+ .section-title {
913
+ font-size: var(--font-size-xl);
914
+ }
915
+ }
916
+ /* Detection Page Specific Styles */
917
+ .detect-header {
918
+ background-color: var(--primary-color);
919
+ color: white;
920
+ padding: var(--spacing-xl) 0;
921
+ text-align: center;
922
+ }
923
+
924
+ .detect-header h1 {
925
+ color: white;
926
+ margin-bottom: var(--spacing-sm);
927
+ }
928
+
929
+ .detection-tool {
930
+ padding: var(--spacing-xl) 0;
931
+ background-color: var(--background-color);
932
+ }
933
+
934
+ .tool-container {
935
+ display: flex;
936
+ gap: var(--spacing-xl);
937
+ margin-bottom: var(--spacing-xl);
938
+ }
939
+
940
+ .tool-sidebar {
941
+ flex: 0 0 300px;
942
+ }
943
+
944
+ .tool-main {
945
+ flex: 1;
946
+ background-color: white;
947
+ border-radius: var(--radius-lg);
948
+ padding: var(--spacing-xl);
949
+ box-shadow: var(--shadow-md);
950
+ }
951
+
952
+ .info-box {
953
+ background-color: #e3f2fd;
954
+ border-radius: var(--radius-md);
955
+ padding: var(--spacing-lg);
956
+ margin-bottom: var(--spacing-lg);
957
+ }
958
+
959
+ .info-box h3 {
960
+ display: flex;
961
+ align-items: center;
962
+ color: var(--primary-color);
963
+ font-size: var(--font-size-lg);
964
+ margin-bottom: var(--spacing-md);
965
+ }
966
+
967
+ .info-box h3 i {
968
+ margin-right: var(--spacing-sm);
969
+ }
970
+
971
+ .info-box ul {
972
+ list-style-type: disc;
973
+ padding-left: var(--spacing-lg);
974
+ }
975
+
976
+ .info-box ul li {
977
+ margin-bottom: var(--spacing-sm);
978
+ }
979
+
980
+ .model-info {
981
+ background-color: white;
982
+ border-radius: var(--radius-md);
983
+ padding: var(--spacing-lg);
984
+ box-shadow: var(--shadow-sm);
985
+ }
986
+
987
+ .model-info h3 {
988
+ font-size: var(--font-size-lg);
989
+ margin-bottom: var(--spacing-md);
990
+ }
991
+
992
+ .model-info-item {
993
+ margin-bottom: var(--spacing-md);
994
+ }
995
+
996
+ .model-info-item h4 {
997
+ font-size: var(--font-size-md);
998
+ color: var(--primary-color);
999
+ margin-bottom: var(--spacing-xs);
1000
+ }
1001
+
1002
+ .model-info-item p {
1003
+ font-size: var(--font-size-sm);
1004
+ margin-bottom: 0;
1005
+ }
1006
+
1007
+ .form-group {
1008
+ margin-bottom: var(--spacing-lg);
1009
+ }
1010
+
1011
+ .form-group label {
1012
+ display: block;
1013
+ margin-bottom: var(--spacing-sm);
1014
+ font-weight: var(--font-weight-medium);
1015
+ }
1016
+
1017
+ .model-select {
1018
+ width: 100%;
1019
+ padding: 0.75rem;
1020
+ border: 1px solid var(--border-color);
1021
+ border-radius: var(--radius-md);
1022
+ font-family: var(--font-family);
1023
+ font-size: var(--font-size-md);
1024
+ background-color: white;
1025
+ transition: border-color var(--transition-fast);
1026
+ }
1027
+
1028
+ .model-select:focus {
1029
+ outline: none;
1030
+ border-color: var(--primary-color);
1031
+ }
1032
+
1033
+ .upload-container {
1034
+ margin-bottom: var(--spacing-lg);
1035
+ }
1036
+
1037
+ .upload-box {
1038
+ border: 2px dashed var(--border-color);
1039
+ border-radius: var(--radius-md);
1040
+ padding: var(--spacing-xl);
1041
+ text-align: center;
1042
+ cursor: pointer;
1043
+ transition: border-color var(--transition-fast);
1044
+ position: relative;
1045
+ min-height: 200px;
1046
+ display: flex;
1047
+ align-items: center;
1048
+ justify-content: center;
1049
+ }
1050
+
1051
+ .upload-box:hover {
1052
+ border-color: var(--primary-color);
1053
+ }
1054
+
1055
+ .upload-prompt {
1056
+ display: flex;
1057
+ flex-direction: column;
1058
+ align-items: center;
1059
+ }
1060
+
1061
+ .upload-icon {
1062
+ font-size: 3rem;
1063
+ color: var(--primary-color);
1064
+ margin-bottom: var(--spacing-md);
1065
+ }
1066
+
1067
+ .browse-text {
1068
+ color: var(--primary-color);
1069
+ font-weight: var(--font-weight-medium);
1070
+ }
1071
+
1072
+ .file-types {
1073
+ font-size: var(--font-size-sm);
1074
+ color: var(--text-tertiary);
1075
+ margin-top: var(--spacing-sm);
1076
+ }
1077
+
1078
+ .preview-container {
1079
+ width: 100%;
1080
+ height: 100%;
1081
+ position: absolute;
1082
+ top: 0;
1083
+ left: 0;
1084
+ display: flex;
1085
+ align-items: center;
1086
+ justify-content: center;
1087
+ padding: var(--spacing-md);
1088
+ }
1089
+
1090
+ .preview-container img {
1091
+ max-height: 200px;
1092
+ max-width: 100%;
1093
+ border-radius: var(--radius-sm);
1094
+ }
1095
+
1096
+ .remove-image {
1097
+ position: absolute;
1098
+ top: 10px;
1099
+ right: 10px;
1100
+ background-color: rgba(0, 0, 0, 0.5);
1101
+ color: white;
1102
+ border: none;
1103
+ width: 30px;
1104
+ height: 30px;
1105
+ border-radius: var(--radius-round);
1106
+ display: flex;
1107
+ align-items: center;
1108
+ justify-content: center;
1109
+ cursor: pointer;
1110
+ transition: background-color var(--transition-fast);
1111
+ }
1112
+
1113
+ .remove-image:hover {
1114
+ background-color: var(--danger-color);
1115
+ }
1116
+
1117
+ .analyze-button {
1118
+ width: 100%;
1119
+ padding: 1rem;
1120
+ font-size: var(--font-size-lg);
1121
+ }
1122
+
1123
+ .analyze-button:disabled {
1124
+ background-color: var(--text-tertiary);
1125
+ border-color: var(--text-tertiary);
1126
+ cursor: not-allowed;
1127
+ }
1128
+
1129
+ .loading {
1130
+ display: none;
1131
+ text-align: center;
1132
+ margin: var(--spacing-xl) 0;
1133
+ }
1134
+
1135
+ .spinner {
1136
+ margin: 0 auto;
1137
+ width: 70px;
1138
+ text-align: center;
1139
+ }
1140
+
1141
+ .spinner > div {
1142
+ width: 18px;
1143
+ height: 18px;
1144
+ background-color: var(--primary-color);
1145
+ border-radius: 100%;
1146
+ display: inline-block;
1147
+ animation: sk-bouncedelay 1.4s infinite ease-in-out both;
1148
+ }
1149
+
1150
+ .spinner .bounce1 {
1151
+ animation-delay: -0.32s;
1152
+ }
1153
+
1154
+ .spinner .bounce2 {
1155
+ animation-delay: -0.16s;
1156
+ }
1157
+
1158
+ @keyframes sk-bouncedelay {
1159
+ 0%, 80%, 100% {
1160
+ transform: scale(0);
1161
+ } 40% {
1162
+ transform: scale(1.0);
1163
+ }
1164
+ }
1165
+
1166
+ .error-message {
1167
+ display: flex;
1168
+ align-items: center;
1169
+ background-color: #ffebee;
1170
+ border-radius: var(--radius-md);
1171
+ padding: var(--spacing-lg);
1172
+ margin: var(--spacing-lg) 0;
1173
+ }
1174
+
1175
+ .error-icon {
1176
+ font-size: 2rem;
1177
+ color: var(--danger-color);
1178
+ margin-right: var(--spacing-lg);
1179
+ }
1180
+
1181
+ .error-content h3 {
1182
+ color: var(--danger-color);
1183
+ margin-bottom: var(--spacing-xs);
1184
+ }
1185
+
1186
+ .result-card {
1187
+ margin-top: var(--spacing-xl);
1188
+ background-color: white;
1189
+ border-radius: var(--radius-lg);
1190
+ box-shadow: var(--shadow-md);
1191
+ overflow: hidden;
1192
+ }
1193
+
1194
+ .result-header {
1195
+ background-color: var(--primary-color);
1196
+ color: white;
1197
+ padding: var(--spacing-lg);
1198
+ display: flex;
1199
+ justify-content: space-between;
1200
+ align-items: center;
1201
+ }
1202
+
1203
+ .result-header h2 {
1204
+ color: white;
1205
+ margin-bottom: 0;
1206
+ display: flex;
1207
+ align-items: center;
1208
+ }
1209
+
1210
+ .result-header h2 i {
1211
+ margin-right: var(--spacing-sm);
1212
+ }
1213
+
1214
+ .model-badge {
1215
+ background-color: rgba(255, 255, 255, 0.2);
1216
+ padding: 0.25rem 0.75rem;
1217
+ border-radius: var(--radius-md);
1218
+ font-size: var(--font-size-sm);
1219
+ font-weight: var(--font-weight-medium);
1220
+ }
1221
+
1222
+ .result-summary {
1223
+ display: flex;
1224
+ padding: var(--spacing-lg);
1225
+ border-bottom: 1px solid var(--border-color);
1226
+ }
1227
+
1228
+ .result-image-container {
1229
+ flex: 0 0 40%;
1230
+ margin-right: var(--spacing-lg);
1231
+ }
1232
+
1233
+ .result-image-container img {
1234
+ width: 100%;
1235
+ border-radius: var(--radius-md);
1236
+ box-shadow: var(--shadow-sm);
1237
+ }
1238
+
1239
+ .result-details {
1240
+ flex: 1;
1241
+ }
1242
+
1243
+ .prediction-box {
1244
+ background-color: #f5f5f5;
1245
+ border-radius: var(--radius-md);
1246
+ padding: var(--spacing-lg);
1247
+ margin-bottom: var(--spacing-lg);
1248
+ }
1249
+
1250
+ .prediction-box h3 {
1251
+ font-size: var(--font-size-md);
1252
+ color: var(--text-secondary);
1253
+ margin-bottom: var(--spacing-sm);
1254
+ }
1255
+
1256
+ .prediction-name {
1257
+ font-size: var(--font-size-xl);
1258
+ font-weight: var(--font-weight-bold);
1259
+ margin-bottom: var(--spacing-sm);
1260
+ color: var(--primary-color);
1261
+ }
1262
+
1263
+ .prediction-description {
1264
+ margin-bottom: var(--spacing-md);
1265
+ font-size: var(--font-size-md);
1266
+ }
1267
+
1268
+ .confidence-meter {
1269
+ display: flex;
1270
+ align-items: center;
1271
+ margin-top: var(--spacing-md);
1272
+ }
1273
+
1274
+ .confidence-label {
1275
+ margin-right: var(--spacing-sm);
1276
+ font-weight: var(--font-weight-medium);
1277
+ font-size: var(--font-size-sm);
1278
+ }
1279
+
1280
+ .confidence-bar {
1281
+ flex: 1;
1282
+ height: 10px;
1283
+ background-color: #e0e0e0;
1284
+ border-radius: var(--radius-sm);
1285
+ overflow: hidden;
1286
+ margin-right: var(--spacing-sm);
1287
+ }
1288
+
1289
+ .confidence-fill {
1290
+ height: 100%;
1291
+ background-color: var(--primary-color);
1292
+ width: 0%;
1293
+ transition: width var(--transition-normal);
1294
+ }
1295
+
1296
+ .confidence-value {
1297
+ font-weight: var(--font-weight-bold);
1298
+ font-size: var(--font-size-sm);
1299
+ }
1300
+
1301
+ .severity-indicator {
1302
+ background-color: #f5f5f5;
1303
+ border-radius: var(--radius-md);
1304
+ padding: var(--spacing-lg);
1305
+ }
1306
+
1307
+ .severity-indicator h3 {
1308
+ font-size: var(--font-size-md);
1309
+ margin-bottom: var(--spacing-sm);
1310
+ }
1311
+
1312
+ .severity-level {
1313
+ display: flex;
1314
+ align-items: center;
1315
+ }
1316
+
1317
+ .severity-level i {
1318
+ margin-right: var(--spacing-sm);
1319
+ }
1320
+
1321
+ .severity-text {
1322
+ font-weight: var(--font-weight-medium);
1323
+ }
1324
+
1325
+ .severity-low {
1326
+ color: var(--success-color);
1327
+ }
1328
+
1329
+ .severity-moderate {
1330
+ color: var(--warning-color);
1331
+ }
1332
+
1333
+ .severity-high, .severity-very-high {
1334
+ color: var(--danger-color);
1335
+ }
1336
+
1337
+ .result-tabs {
1338
+ display: flex;
1339
+ border-bottom: 1px solid var(--border-color);
1340
+ }
1341
+
1342
+ .tab-button {
1343
+ padding: var(--spacing-md) var(--spacing-lg);
1344
+ background: none;
1345
+ border: none;
1346
+ border-bottom: 3px solid transparent;
1347
+ cursor: pointer;
1348
+ font-weight: var(--font-weight-medium);
1349
+ transition: all var(--transition-fast);
1350
+ }
1351
+
1352
+ .tab-button:hover {
1353
+ background-color: rgba(0, 0, 0, 0.02);
1354
+ }
1355
+
1356
+ .tab-button.active {
1357
+ color: var(--primary-color);
1358
+ border-bottom-color: var(--primary-color);
1359
+ }
1360
+
1361
+ .tab-content {
1362
+ padding: var(--spacing-lg);
1363
+ }
1364
+
1365
+ .tab-pane {
1366
+ display: none;
1367
+ }
1368
+
1369
+ .tab-pane.active {
1370
+ display: block;
1371
+ }
1372
+
1373
+ .tab-pane h3 {
1374
+ margin-bottom: var(--spacing-md);
1375
+ font-size: var(--font-size-lg);
1376
+ }
1377
+
1378
+ .probabilities-grid {
1379
+ display: grid;
1380
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
1381
+ gap: var(--spacing-md);
1382
+ }
1383
+
1384
+ .probability-item {
1385
+ background-color: #f5f5f5;
1386
+ border-radius: var(--radius-md);
1387
+ padding: var(--spacing-md);
1388
+ display: flex;
1389
+ flex-direction: column;
1390
+ }
1391
+
1392
+ .probability-class {
1393
+ font-weight: var(--font-weight-medium);
1394
+ margin-bottom: var(--spacing-xs);
1395
+ }
1396
+
1397
+ .probability-bar {
1398
+ height: 8px;
1399
+ background-color: #e0e0e0;
1400
+ border-radius: var(--radius-sm);
1401
+ overflow: hidden;
1402
+ margin: var(--spacing-xs) 0;
1403
+ }
1404
+
1405
+ .probability-fill {
1406
+ height: 100%;
1407
+ background-color: var(--primary-color);
1408
+ }
1409
+
1410
+ .probability-value {
1411
+ font-size: var(--font-size-sm);
1412
+ color: var(--text-secondary);
1413
+ align-self: flex-end;
1414
+ }
1415
+
1416
+ .info-resources ul {
1417
+ list-style-type: none;
1418
+ padding: 0;
1419
+ }
1420
+
1421
+ .info-resources ul li {
1422
+ margin-bottom: var(--spacing-sm);
1423
+ }
1424
+
1425
+ .info-resources ul li a {
1426
+ display: flex;
1427
+ align-items: center;
1428
+ padding: var(--spacing-sm);
1429
+ background-color: #f5f5f5;
1430
+ border-radius: var(--radius-md);
1431
+ transition: background-color var(--transition-fast);
1432
+ }
1433
+
1434
+ .info-resources ul li a:hover {
1435
+ background-color: #e0e0e0;
1436
+ }
1437
+
1438
+ .info-resources ul li a i {
1439
+ margin-right: var(--spacing-sm);
1440
+ color: var(--primary-color);
1441
+ }
1442
+
1443
+ .next-steps-content {
1444
+ margin-bottom: var(--spacing-lg);
1445
+ }
1446
+
1447
+ .next-step {
1448
+ display: flex;
1449
+ margin-bottom: var(--spacing-md);
1450
+ padding: var(--spacing-md);
1451
+ background-color: #f5f5f5;
1452
+ border-radius: var(--radius-md);
1453
+ }
1454
+
1455
+ .step-icon {
1456
+ flex: 0 0 50px;
1457
+ height: 50px;
1458
+ background-color: var(--primary-light);
1459
+ color: white;
1460
+ border-radius: var(--radius-round);
1461
+ display: flex;
1462
+ align-items: center;
1463
+ justify-content: center;
1464
+ font-size: 1.25rem;
1465
+ margin-right: var(--spacing-md);
1466
+ }
1467
+
1468
+ .step-content {
1469
+ flex: 1;
1470
+ }
1471
+
1472
+ .step-content h4 {
1473
+ margin-bottom: var(--spacing-xs);
1474
+ }
1475
+
1476
+ .step-content p {
1477
+ margin-bottom: 0;
1478
+ font-size: var(--font-size-sm);
1479
+ }
1480
+
1481
+ .disclaimer-box {
1482
+ background-color: #fff8e1;
1483
+ border-left: 4px solid var(--warning-color);
1484
+ padding: var(--spacing-md);
1485
+ border-radius: var(--radius-md);
1486
+ }
1487
+
1488
+ .disclaimer-box p {
1489
+ margin-bottom: 0;
1490
+ font-size: var(--font-size-sm);
1491
+ }
1492
+
1493
+ .result-actions {
1494
+ display: flex;
1495
+ justify-content: flex-end;
1496
+ gap: var(--spacing-md);
1497
+ padding: var(--spacing-lg);
1498
+ border-top: 1px solid var(--border-color);
1499
+ }
1500
+
1501
+ @media (max-width: 992px) {
1502
+ .tool-container {
1503
+ flex-direction: column;
1504
+ }
1505
+
1506
+ .tool-sidebar {
1507
+ flex: 0 0 auto;
1508
+ width: 100%;
1509
+ }
1510
+ }
1511
+
1512
+ @media (max-width: 768px) {
1513
+ .result-summary {
1514
+ flex-direction: column;
1515
+ }
1516
+
1517
+ .result-image-container {
1518
+ flex: 0 0 auto;
1519
+ margin-right: 0;
1520
+ margin-bottom: var(--spacing-lg);
1521
+ }
1522
+
1523
+ .result-tabs {
1524
+ overflow-x: auto;
1525
+ white-space: nowrap;
1526
+ }
1527
+
1528
+ .tab-button {
1529
+ padding: var(--spacing-md) var(--spacing-sm);
1530
+ }
1531
+
1532
+ .probabilities-grid {
1533
+ grid-template-columns: 1fr;
1534
+ }
1535
+ }
1536
+
1537
+ /* Add to styles.css */
1538
+ .menu-open {
1539
+ overflow: hidden;
1540
+ }
1541
+
1542
+ .nav-links.active {
1543
+ right: 0;
1544
+ }
1545
+
1546
+ .mobile-menu-toggle.active .bar:nth-child(1) {
1547
+ transform: translateY(8px) rotate(45deg);
1548
+ }
1549
+
1550
+ .mobile-menu-toggle.active .bar:nth-child(2) {
1551
+ opacity: 0;
1552
+ }
1553
+
1554
+ .mobile-menu-toggle.active .bar:nth-child(3) {
1555
+ transform: translateY(-8px) rotate(-45deg);
1556
+ }
static/images/hero-bg.jpg ADDED
Binary file (38.4 kB). View file
 
static/images/loading.gif ADDED
static/images/logo.svg ADDED

Git LFS Details

  • SHA256: ddc5997b289083754ae43f61600ce7a212d9c6704b087583507a2b7ea8a93863
  • Pointer size: 131 Bytes
  • Size of remote file: 338 kB
static/js/detect.js ADDED
@@ -0,0 +1,550 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ // At the beginning of your file, add this to ensure CLASS_DESCRIPTIONS is defined
3
+ // This assumes your class descriptions are passed from the Flask backend
4
+ const CLASS_DESCRIPTIONS = window.CLASS_DESCRIPTIONS || {
5
+ 'akiec': { name: 'Actinic Keratosis', description: 'A precancerous growth caused by sun damage.' },
6
+ 'bcc': { name: 'Basal Cell Carcinoma', description: 'The most common type of skin cancer.' },
7
+ 'bkl': { name: 'Benign Keratosis', description: 'A non-cancerous growth on the skin.' },
8
+ 'df': { name: 'Dermatofibroma', description: 'A common benign skin growth.' },
9
+ 'mel': { name: 'Melanoma', description: 'The most serious form of skin cancer.' },
10
+ 'nv': { name: 'Melanocytic Nevus', description: 'A common mole.' },
11
+ 'vasc': { name: 'Vascular Lesion', description: 'An abnormality of blood vessels.' }
12
+ };
13
+
14
+ // Define CONDITION_INFO if not already defined
15
+ const CONDITION_INFO = window.CONDITION_INFO || {
16
+ 'akiec': {
17
+ severity: 'moderate',
18
+ description: 'Actinic Keratosis is a precancerous growth caused by sun damage.',
19
+ resources: [{ name: 'Mayo Clinic', url: 'https://www.mayoclinic.org/diseases-conditions/actinic-keratosis/symptoms-causes/syc-20354969' }]
20
+ },
21
+ 'bcc': {
22
+ severity: 'high',
23
+ description: 'Basal Cell Carcinoma is the most common type of skin cancer.',
24
+ resources: [{ name: 'Skin Cancer Foundation', url: 'https://www.skincancer.org/skin-cancer-information/basal-cell-carcinoma/' }]
25
+ },
26
+ 'bkl': {
27
+ severity: 'low',
28
+ description: 'Benign Keratosis is a non-cancerous growth that appears as a waxy, scaly growth on the skin.',
29
+ resources: [{ name: 'American Academy of Dermatology', url: 'https://www.aad.org/public/diseases/bumps-and-growths/seborrheic-keratoses' }]
30
+ },
31
+ 'df': {
32
+ severity: 'low',
33
+ description: 'Dermatofibroma is a common benign skin growth that often appears as a small, firm bump.',
34
+ resources: [{ name: 'DermNet NZ', url: 'https://dermnetnz.org/topics/dermatofibroma' }]
35
+ },
36
+ 'mel': {
37
+ severity: 'very high',
38
+ description: 'Melanoma is the most serious form of skin cancer that develops in the cells that produce melanin.',
39
+ resources: [{ name: 'American Cancer Society', url: 'https://www.cancer.org/cancer/melanoma-skin-cancer.html' }]
40
+ },
41
+ 'nv': {
42
+ severity: 'low',
43
+ description: 'Melanocytic Nevus is a common mole that appears as a small, dark brown spot caused by clusters of pigmented cells.',
44
+ resources: [{ name: 'Cleveland Clinic', url: 'https://my.clevelandclinic.org/health/diseases/21880-moles' }]
45
+ },
46
+ 'vasc': {
47
+ severity: 'moderate',
48
+ description: 'Vascular Lesion is an abnormality of blood vessels that can appear as red or purple marks on the skin.',
49
+ resources: [{ name: 'Stanford Health Care', url: 'https://stanfordhealthcare.org/medical-conditions/skin-hair-and-nails/vascular-malformations.html' }]
50
+ }
51
+ };
52
+
53
+
54
+ document.addEventListener('DOMContentLoaded', function() {
55
+ // DOM Elements
56
+ const uploadBox = document.getElementById('upload-box');
57
+ const fileInput = document.getElementById('file-input');
58
+ const previewContainer = document.getElementById('preview-container');
59
+ const previewImage = document.getElementById('image-preview')
60
+ const removeImageBtn = document.getElementById('remove-image');
61
+ const analyzeBtn = document.getElementById('analyze-button');
62
+ const uploadForm = document.getElementById('upload-form');
63
+ const loadingIndicator = document.getElementById('loading');
64
+ const resultContainer = document.getElementById('result');
65
+ const errorMessage = document.getElementById('error-message');
66
+ const errorText = document.getElementById('error-text');
67
+ const modelSelect = document.getElementById('model-select');
68
+ const modelUsedBadge = document.getElementById('model-used-badge');
69
+
70
+ // Tab elements
71
+ const tabButtons = document.querySelectorAll('.tab-button');
72
+ const tabPanes = document.querySelectorAll('.tab-pane');
73
+
74
+ // Result elements
75
+ const resultImage = document.getElementById('result-image');
76
+ const predictionElement = document.getElementById('prediction');
77
+ const descriptionElement = document.getElementById('description');
78
+ const confidenceElement = document.getElementById('confidence');
79
+ const confidenceFill = document.getElementById('confidence-fill');
80
+ const probabilitiesContainer = document.getElementById('probabilities');
81
+ const severityIndicator = document.getElementById('severity-indicator');
82
+ const infoConditionName = document.getElementById('info-condition-name');
83
+ const conditionInformation = document.getElementById('condition-information');
84
+ const resourcesList = document.getElementById('resources-list');
85
+ const saveResultBtn = document.getElementById('save-result');
86
+ const newAnalysisBtn = document.getElementById('new-analysis');
87
+
88
+ // State
89
+ let fileSelected = false;
90
+
91
+ // Event Listeners
92
+ uploadBox.addEventListener('click', function() {
93
+ fileInput.click();
94
+ });
95
+
96
+ fileInput.addEventListener('change', handleFileSelect);
97
+
98
+ removeImageBtn.addEventListener('click', function(e) {
99
+ e.stopPropagation();
100
+ resetUpload();
101
+ });
102
+
103
+ uploadForm.addEventListener('submit', handleFormSubmit);
104
+
105
+ tabButtons.forEach(button => {
106
+ button.addEventListener('click', function() {
107
+ const tabName = this.getAttribute('data-tab');
108
+ switchTab(tabName);
109
+ });
110
+ });
111
+
112
+ newAnalysisBtn.addEventListener('click', resetAll);
113
+
114
+ saveResultBtn.addEventListener('click', saveResults);
115
+
116
+ // Functions
117
+ function handleFileSelect(e) {
118
+ const file = e.target.files[0];
119
+
120
+ if (!file) return;
121
+
122
+ if (!file.type.match('image.*')) {
123
+ showError('Please select an image file (JPEG, PNG, etc.)');
124
+ return;
125
+ }
126
+
127
+ const reader = new FileReader();
128
+
129
+ reader.onload = function(e) {
130
+ previewImage.src = e.target.result;
131
+ previewContainer.classList.remove('hidden');
132
+ removeImageBtn.classList.remove('hidden');
133
+ uploadBox.classList.add('has-image');
134
+ fileSelected = true;
135
+ analyzeBtn.disabled = false;
136
+ };
137
+
138
+ reader.readAsDataURL(file);
139
+ }
140
+
141
+ function resetUpload() {
142
+ fileInput.value = '';
143
+ previewContainer.classList.add('hidden');
144
+ removeImageBtn.classList.add('hidden');
145
+ uploadBox.classList.remove('has-image');
146
+ fileSelected = false;
147
+ analyzeBtn.disabled = true;
148
+ }
149
+
150
+ function handleFormSubmit(e) {
151
+ e.preventDefault();
152
+
153
+ if (!fileSelected) {
154
+ showError('Please select an image to analyze');
155
+ return;
156
+ }
157
+
158
+ // Hide any previous results or errors
159
+ resultContainer.classList.add('hidden');
160
+ errorMessage.classList.add('hidden');
161
+
162
+ // Show loading indicator
163
+ loadingIndicator.style.display = 'block';
164
+
165
+ // Get the selected model
166
+ const selectedModel = modelSelect.value;
167
+
168
+ // Create form data
169
+ const formData = new FormData();
170
+ formData.append('file', fileInput.files[0]);
171
+ formData.append('model', selectedModel);
172
+
173
+ console.log('Uploading file:', fileInput.files[0].name);
174
+ console.log('Selected model:', selectedModel);
175
+
176
+ // Send the request
177
+ fetch('/predict', {
178
+ method: 'POST',
179
+ body: formData
180
+ })
181
+ .then(response => {
182
+ if (!response.ok) {
183
+ return response.json().then(data => {
184
+ throw new Error(data.error || `Server error: ${response.status}`);
185
+ }).catch(e => {
186
+ // If we can't parse JSON, use the status text
187
+ throw new Error(`Server error: ${response.status} ${response.statusText}`);
188
+ });
189
+ }
190
+ return response.json();
191
+ })
192
+ .then(data => {
193
+ // Hide loading indicator
194
+ loadingIndicator.style.display = 'none';
195
+
196
+ // Check if we have predictions
197
+ if (!data.predictions || data.predictions.length === 0) {
198
+ throw new Error('No predictions returned from the server');
199
+ }
200
+
201
+ // Display results
202
+ displayResults(data, selectedModel);
203
+ })
204
+ .catch(error => {
205
+ // Hide loading indicator
206
+ loadingIndicator.style.display = 'none';
207
+
208
+ // Show error message
209
+ showError(`Error: ${error.message}`);
210
+ console.error('Error:', error);
211
+ });
212
+ }
213
+
214
+ function displayResults(data, modelName) {
215
+ // Set the model badge
216
+ modelUsedBadge.textContent = modelName;
217
+
218
+ // Set the result image
219
+ resultImage.src = previewImage.src;
220
+
221
+ // Get the top prediction
222
+ const topPrediction = data.predictions[0];
223
+ const predictionClass = topPrediction.class;
224
+ const confidence = topPrediction.confidence;
225
+
226
+ // Set prediction details
227
+ predictionElement.textContent = CLASS_DESCRIPTIONS[predictionClass].name;
228
+ descriptionElement.textContent = CLASS_DESCRIPTIONS[predictionClass].description;
229
+
230
+ // Set confidence
231
+ const confidencePercent = Math.round(confidence * 100);
232
+ confidenceElement.textContent = confidencePercent + '%';
233
+ confidenceFill.style.width = confidencePercent + '%';
234
+
235
+ // Set confidence color based on value
236
+ if (confidencePercent >= 80) {
237
+ confidenceFill.style.backgroundColor = 'var(--success-color)';
238
+ } else if (confidencePercent >= 50) {
239
+ confidenceFill.style.backgroundColor = 'var(--warning-color)';
240
+ } else {
241
+ confidenceFill.style.backgroundColor = 'var(--danger-color)';
242
+ }
243
+
244
+ // Display severity indicator
245
+ const conditionInfo = CONDITION_INFO[predictionClass];
246
+ let severityHTML = '';
247
+
248
+ if (conditionInfo && conditionInfo.severity) {
249
+ let severityClass = '';
250
+ let severityIcon = '';
251
+
252
+ switch (conditionInfo.severity) {
253
+ case 'low':
254
+ severityClass = 'severity-low';
255
+ severityIcon = 'fa-check-circle';
256
+ break;
257
+ case 'moderate':
258
+ severityClass = 'severity-moderate';
259
+ severityIcon = 'fa-exclamation-circle';
260
+ break;
261
+ case 'high':
262
+ severityClass = 'severity-high';
263
+ severityIcon = 'fa-exclamation-triangle';
264
+ break;
265
+ case 'very high':
266
+ severityClass = 'severity-very-high';
267
+ severityIcon = 'fa-radiation';
268
+ break;
269
+ }
270
+
271
+ severityHTML = `
272
+ <h3>Severity Level</h3>
273
+ <div class="severity-level ${severityClass}">
274
+ <i class="fas ${severityIcon}"></i>
275
+ <span class="severity-text">${conditionInfo.severity.charAt(0).toUpperCase() + conditionInfo.severity.slice(1)}</span>
276
+ </div>
277
+ <p class="mt-2">This is based on typical characteristics of this condition.</p>
278
+ `;
279
+ }
280
+
281
+ severityIndicator.innerHTML = severityHTML;
282
+
283
+ // Display all probabilities
284
+ probabilitiesContainer.innerHTML = '';
285
+ data.predictions.forEach(prediction => {
286
+ const probabilityPercent = Math.round(prediction.confidence * 100);
287
+ const probabilityHTML = `
288
+ <div class="probability-item">
289
+ <div class="probability-class">${CLASS_DESCRIPTIONS[prediction.class].name}</div>
290
+ <div class="probability-bar">
291
+ <div class="probability-fill" style="width: ${probabilityPercent}%"></div>
292
+ </div>
293
+ <div class="probability-value">${probabilityPercent}%</div>
294
+ </div>
295
+ `;
296
+ probabilitiesContainer.innerHTML += probabilityHTML;
297
+ });
298
+
299
+ // Set condition information
300
+ infoConditionName.textContent = CLASS_DESCRIPTIONS[predictionClass].name;
301
+
302
+ if (conditionInfo && conditionInfo.description) {
303
+ conditionInformation.textContent = conditionInfo.description;
304
+ } else {
305
+ conditionInformation.textContent = CLASS_DESCRIPTIONS[predictionClass].description;
306
+ }
307
+
308
+ // Set resources
309
+ resourcesList.innerHTML = '';
310
+ if (conditionInfo && conditionInfo.resources) {
311
+ conditionInfo.resources.forEach(resource => {
312
+ const resourceHTML = `
313
+ <li>
314
+ <a href="${resource.url}" target="_blank">
315
+ <i class="fas fa-external-link-alt"></i>
316
+ ${resource.name}
317
+ </a>
318
+ </li>
319
+ `;
320
+ resourcesList.innerHTML += resourceHTML;
321
+ });
322
+ }
323
+
324
+ // Show the result container
325
+ resultContainer.classList.remove('hidden');
326
+
327
+ // Switch to the first tab
328
+ switchTab('probabilities');
329
+
330
+ // Scroll to results
331
+ resultContainer.scrollIntoView({ behavior: 'smooth' });
332
+ }
333
+
334
+ function showError(message) {
335
+ errorText.textContent = message;
336
+ errorMessage.classList.remove('hidden');
337
+ }
338
+
339
+ function switchTab(tabName) {
340
+ // Update active tab button
341
+ tabButtons.forEach(button => {
342
+ if (button.getAttribute('data-tab') === tabName) {
343
+ button.classList.add('active');
344
+ } else {
345
+ button.classList.remove('active');
346
+ }
347
+ });
348
+
349
+ // Update active tab pane
350
+ tabPanes.forEach(pane => {
351
+ if (pane.id === tabName + '-tab') {
352
+ pane.classList.add('active');
353
+ } else {
354
+ pane.classList.remove('active');
355
+ }
356
+ });
357
+ }
358
+
359
+ function resetAll() {
360
+ resetUpload();
361
+ resultContainer.classList.add('hidden');
362
+ errorMessage.classList.add('hidden');
363
+ }
364
+
365
+ function saveResults() {
366
+ // Create a canvas element
367
+ const canvas = document.createElement('canvas');
368
+ const ctx = canvas.getContext('2d');
369
+
370
+ // Set canvas dimensions
371
+ canvas.width = 800;
372
+ canvas.height = 1200;
373
+
374
+ // Fill background
375
+ ctx.fillStyle = '#ffffff';
376
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
377
+
378
+ // Draw header
379
+ ctx.fillStyle = '#2A5C82';
380
+ ctx.fillRect(0, 0, canvas.width, 100);
381
+
382
+ // Draw header text
383
+ ctx.font = 'bold 30px Poppins, sans-serif';
384
+ ctx.fillStyle = '#ffffff';
385
+ ctx.textAlign = 'center';
386
+ ctx.fillText('SkinAI Analysis Results', canvas.width / 2, 60);
387
+
388
+ // Load and draw the image
389
+ const img = new Image();
390
+ img.onload = function() {
391
+ // Calculate image dimensions to maintain aspect ratio
392
+ const maxWidth = 400;
393
+ const maxHeight = 300;
394
+ let width = img.width;
395
+ let height = img.height;
396
+
397
+ if (width > height) {
398
+ if (width > maxWidth) {
399
+ height *= maxWidth / width;
400
+ width = maxWidth;
401
+ }
402
+ } else {
403
+ if (height > maxHeight) {
404
+ width *= maxHeight / height;
405
+ height = maxHeight;
406
+ }
407
+ }
408
+
409
+ // Draw image
410
+ const x = (canvas.width - width) / 2;
411
+ ctx.drawImage(img, x, 130, width, height);
412
+
413
+ // Draw prediction info
414
+ ctx.fillStyle = '#333333';
415
+ ctx.font = 'bold 24px Poppins, sans-serif';
416
+ ctx.textAlign = 'center';
417
+ ctx.fillText('Primary Prediction', canvas.width / 2, 480);
418
+
419
+ ctx.font = 'bold 28px Poppins, sans-serif';
420
+ ctx.fillStyle = '#2A5C82';
421
+ ctx.fillText(predictionElement.textContent, canvas.width / 2, 520);
422
+
423
+ // Draw confidence
424
+ ctx.font = '18px Poppins, sans-serif';
425
+ ctx.fillStyle = '#333333';
426
+ ctx.fillText(`Confidence: ${confidenceElement.textContent}`, canvas.width / 2, 550);
427
+
428
+ // Draw description
429
+ ctx.font = '16px Poppins, sans-serif';
430
+ ctx.fillStyle = '#666666';
431
+ ctx.textAlign = 'left';
432
+
433
+ // Wrap text function
434
+ const wrapText = function(context, text, x, y, maxWidth, lineHeight) {
435
+ const words = text.split(' ');
436
+ let line = '';
437
+ let testLine = '';
438
+ let lineArray = [];
439
+
440
+ for (let n = 0; n < words.length; n++) {
441
+ testLine = line + words[n] + ' ';
442
+ const metrics = context.measureText(testLine);
443
+ const testWidth = metrics.width;
444
+
445
+ if (testWidth > maxWidth && n > 0) {
446
+ lineArray.push([line, x, y]);
447
+ line = words[n] + ' ';
448
+ y += lineHeight;
449
+ } else {
450
+ line = testLine;
451
+ }
452
+ }
453
+
454
+ lineArray.push([line, x, y]);
455
+ return lineArray;
456
+ };
457
+
458
+ const descriptionLines = wrapText(ctx, descriptionElement.textContent, 100, 600, canvas.width - 200, 25);
459
+
460
+ for (let i = 0; i < descriptionLines.length; i++) {
461
+ ctx.fillText(descriptionLines[i][0], descriptionLines[i][1], descriptionLines[i][2]);
462
+ }
463
+
464
+ // Draw probabilities title
465
+ ctx.font = 'bold 24px Poppins, sans-serif';
466
+ ctx.fillStyle = '#333333';
467
+ ctx.textAlign = 'center';
468
+ ctx.fillText('Class Probabilities', canvas.width / 2, 700);
469
+
470
+ // Draw probabilities
471
+ const predictions = Array.from(document.querySelectorAll('.probability-item'));
472
+ let yPos = 740;
473
+
474
+ predictions.slice(0, 5).forEach(prediction => {
475
+ const className = prediction.querySelector('.probability-class').textContent;
476
+ const probabilityValue = prediction.querySelector('.probability-value').textContent;
477
+
478
+ ctx.font = '16px Poppins, sans-serif';
479
+ ctx.fillStyle = '#333333';
480
+ ctx.textAlign = 'left';
481
+ ctx.fillText(className, 200, yPos);
482
+
483
+ ctx.textAlign = 'right';
484
+ ctx.fillText(probabilityValue, canvas.width - 200, yPos);
485
+
486
+ // Draw probability bar background
487
+ ctx.fillStyle = '#e0e0e0';
488
+ ctx.fillRect(200, yPos + 10, 400, 10);
489
+
490
+ // Draw probability bar fill
491
+ ctx.fillStyle = '#2A5C82';
492
+ const width = parseInt(probabilityValue) * 4; // Scale to 400px max width
493
+ ctx.fillRect(200, yPos + 10, width, 10);
494
+
495
+ yPos += 40;
496
+ });
497
+
498
+ // Draw disclaimer
499
+ ctx.fillStyle = '#fff8e1';
500
+ ctx.fillRect(100, 1000, canvas.width - 200, 100);
501
+
502
+ ctx.font = 'bold 16px Poppins, sans-serif';
503
+ ctx.fillStyle = '#856404';
504
+ ctx.textAlign = 'center';
505
+ ctx.fillText('Important Disclaimer', canvas.width / 2, 1030);
506
+
507
+ ctx.font = '14px Poppins, sans-serif';
508
+ ctx.fillText('This AI analysis is for educational purposes only and is not a medical diagnosis.', canvas.width / 2, 1060);
509
+ ctx.fillText('Always consult a healthcare professional for medical concerns.', canvas.width / 2, 1080);
510
+
511
+ // Draw date
512
+ const date = new Date();
513
+ ctx.font = '12px Poppins, sans-serif';
514
+ ctx.fillStyle = '#999999';
515
+ ctx.textAlign = 'right';
516
+ ctx.fillText(`Generated on: ${date.toLocaleDateString()} ${date.toLocaleTimeString()}`, canvas.width - 100, 1150);
517
+
518
+ // Convert to image and download
519
+ const dataUrl = canvas.toDataURL('image/png');
520
+ const link = document.createElement('a');
521
+ link.download = 'skinai-analysis.png';
522
+ link.href = dataUrl;
523
+ link.click();
524
+ };
525
+
526
+ img.src = resultImage.src;
527
+ }
528
+ });
529
+
530
+ // FAQ Accordion
531
+ document.addEventListener('DOMContentLoaded', function() {
532
+ const faqItems = document.querySelectorAll('.faq-item');
533
+
534
+ faqItems.forEach(item => {
535
+ const question = item.querySelector('.faq-question');
536
+
537
+ question.addEventListener('click', () => {
538
+ // Toggle active class on the clicked item
539
+ item.classList.toggle('active');
540
+
541
+ // Close other items
542
+ faqItems.forEach(otherItem => {
543
+ if (otherItem !== item) {
544
+ otherItem.classList.remove('active');
545
+ }
546
+ });
547
+ });
548
+ });
549
+ });
550
+
static/js/main.js ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // main.js - Core site functionality
2
+
3
+ // Mobile menu toggle
4
+ document.addEventListener('DOMContentLoaded', function() {
5
+ const mobileMenuToggle = document.querySelector('.mobile-menu-toggle');
6
+ const navLinks = document.querySelector('.nav-links');
7
+
8
+ if (mobileMenuToggle && navLinks) {
9
+ mobileMenuToggle.addEventListener('click', function() {
10
+ this.classList.toggle('active');
11
+ navLinks.classList.toggle('active');
12
+ document.body.classList.toggle('menu-open');
13
+ });
14
+ }
15
+
16
+ // Close menu when clicking outside
17
+ document.addEventListener('click', function(e) {
18
+ if (!e.target.closest('.nav-links') &&
19
+ !e.target.closest('.mobile-menu-toggle')) {
20
+ mobileMenuToggle.classList.remove('active');
21
+ navLinks.classList.remove('active');
22
+ document.body.classList.remove('menu-open');
23
+ }
24
+ });
25
+ });
26
+
27
+ // Smooth scroll for anchor links
28
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
29
+ anchor.addEventListener('click', function(e) {
30
+ e.preventDefault();
31
+ document.querySelector(this.getAttribute('href')).scrollIntoView({
32
+ behavior: 'smooth'
33
+ });
34
+ });
35
+ });
templates/404.html ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div class="error-container">
5
+ <h1>404 - Page Not Found</h1>
6
+ <p>The page you're looking for doesn't exist.</p>
7
+ <a href="{{ url_for('index') }}" class="cta-button">Return Home</a>
8
+ </div>
9
+ {% endblock %}
templates/500.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div class="error-container">
5
+ <h1>500 - Server Error</h1>
6
+ <p>Something went wrong on our end. We're working to fix it!</p>
7
+ <div class="actions">
8
+ <a href="{{ url_for('index') }}" class="cta-button">Return Home</a>
9
+ <a href="{{ url_for('detect') }}" class="cta-button">Try Again</a>
10
+ </div>
11
+ </div>
12
+ {% endblock %}
templates/about.html ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <section class="container">
5
+ <h1>About SkinAI</h1>
6
+ <p>Your about page content here...</p>
7
+ </section>
8
+ {% endblock %}
templates/base.html ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <meta name="description" content="SkinAI - Advanced AI-powered skin lesion analysis for early detection of skin cancer">
7
+ <meta name="keywords" content="skin cancer, melanoma, AI detection, dermatology, machine learning">
8
+ <meta name="author" content="SkinAI Team">
9
+
10
+ <title>{% block title %}SkinAI - AI-Powered Skin Cancer Detection{% endblock %}</title>
11
+
12
+ <!-- Favicon -->
13
+ <link rel="shortcut icon" href="{{ url_for('static', filename='images/favicon.ico') }}" type="image/x-icon">
14
+
15
+ <!-- Fonts -->
16
+ <link rel="preconnect" href="https://fonts.googleapis.com">
17
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
18
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
19
+
20
+ <!-- CSS -->
21
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
22
+
23
+ <!-- Font Awesome -->
24
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
25
+
26
+ {% block extra_css %}{% endblock %}
27
+ </head>
28
+ <body>
29
+ <header>
30
+ <nav class="navbar">
31
+ <div class="container">
32
+ <a href="{{ url_for('index') }}" class="logo">
33
+ <img src="{{ url_for('static', filename='images/logo.svg') }}" alt="SkinAI Logo">
34
+ <span>SkinAI</span>
35
+ </a>
36
+
37
+ <button class="mobile-menu-toggle" aria-label="Toggle menu">
38
+ <span class="bar"></span>
39
+ <span class="bar"></span>
40
+ <span class="bar"></span>
41
+ </button>
42
+
43
+ <div class="nav-links">
44
+ <a href="{{ url_for('index') }}" class="{% if request.path == url_for('index') %}active{% endif %}">Home</a>
45
+ <a href="{{ url_for('detect') }}" class="{% if request.path == url_for('detect') %}active{% endif %}">Detect</a>
46
+ <a href="{{ url_for('about') }}" class="{% if request.path == url_for('about') %}active{% endif %}">About</a>
47
+ <a href="{{ url_for('faq') }}" class="{% if request.path == url_for('faq') %}active{% endif %}">FAQ</a>
48
+ </div>
49
+ </div>
50
+ </nav>
51
+ </header>
52
+
53
+ <main>
54
+ {% block content %}{% endblock %}
55
+ </main>
56
+
57
+ <footer>
58
+ <div class="container">
59
+ <div class="footer-grid">
60
+ <div class="footer-col">
61
+ <h3>SkinAI</h3>
62
+ <p>Advanced AI-powered skin lesion analysis for early detection of skin cancer.</p>
63
+ <div class="social-links">
64
+ <a href="#" aria-label="Twitter"><i class="fab fa-twitter"></i></a>
65
+ <a href="#" aria-label="Facebook"><i class="fab fa-facebook-f"></i></a>
66
+ <a href="#" aria-label="Instagram"><i class="fab fa-instagram"></i></a>
67
+ <a href="#" aria-label="LinkedIn"><i class="fab fa-linkedin-in"></i></a>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="footer-col">
72
+ <h3>Quick Links</h3>
73
+ <ul>
74
+ <li><a href="{{ url_for('index') }}">Home</a></li>
75
+ <li><a href="{{ url_for('detect') }}">Detect</a></li>
76
+ <li><a href="{{ url_for('about') }}">About</a></li>
77
+ <li><a href="{{ url_for('faq') }}">FAQ</a></li>
78
+ </ul>
79
+ </div>
80
+
81
+ <div class="footer-col">
82
+ <h3>Legal</h3>
83
+ <ul>
84
+ <li><a href="{{ url_for('privacy') }}">Privacy Policy</a></li>
85
+ <li><a href="{{ url_for('terms') }}">Terms of Service</a></li>
86
+ <li><a href="#">Cookie Policy</a></li>
87
+ </ul>
88
+ </div>
89
+
90
+ <div class="footer-col">
91
+ <h3>Important Notice</h3>
92
+ <p>This tool is for educational purposes only and is not a substitute for professional medical advice. Always consult with a healthcare professional for medical concerns.</p>
93
+ </div>
94
+ </div>
95
+
96
+ <div class="footer-bottom">
97
+ <p>&copy; 2023 SkinAI. All rights reserved.</p>
98
+ </div>
99
+ </div>
100
+ </footer>
101
+
102
+ <!-- Core JavaScript -->
103
+ <script src="{{ url_for('static', filename='js/main.js') }}"></script>
104
+
105
+ {% block extra_js %}{% endblock %}
106
+
107
+ <!-- Analytics (replace with your own) -->
108
+ <script async defer>
109
+ // Google Analytics or other analytics code
110
+ </script>
111
+ </body>
112
+ </html>
templates/contact.html ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <section class="container">
5
+ <h1>Contact Us</h1>
6
+ <!-- Add contact form -->
7
+ </section>
8
+ {% endblock %}
9
+
templates/detect.html ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Skin Lesion Analysis - SkinAI{% endblock %}
4
+
5
+ {% block extra_css %}
6
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/detect.css') }}">
7
+ {% endblock %}
8
+
9
+ {% block content %}
10
+ <section class="detect-header">
11
+ <div class="container">
12
+ <h1>Skin Lesion Analysis</h1>
13
+ <p>Upload a clear image of your skin lesion for AI-powered analysis</p>
14
+ </div>
15
+ </section>
16
+
17
+ <section class="detection-tool">
18
+ <div class="container">
19
+ <div class="tool-container">
20
+ <div class="tool-sidebar">
21
+ <div class="info-box">
22
+ <h3><i class="fas fa-info-circle"></i> How to get the best results</h3>
23
+ <ul>
24
+ <li>Use good lighting conditions</li>
25
+ <li>Keep the lesion centered in the image</li>
26
+ <li>Use a clear, in-focus image</li>
27
+ <li>Include only the area of concern</li>
28
+ </ul>
29
+ </div>
30
+
31
+ <div class="model-info">
32
+ <h3>About Our Models</h3>
33
+ <div class="model-info-item">
34
+ <h4>MobileNetV2</h4>
35
+ <p>Optimized for speed and efficiency with 74% accuracy on the HAM10000 dataset.</p>
36
+ </div>
37
+ <div class="model-info-item">
38
+ <h4>EfficientNetB0</h4>
39
+ <p>Higher accuracy model with 69% accuracy on skin cancer classification.</p>
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ <div class="tool-main">
45
+ <form id="upload-form" enctype="multipart/form-data" action="/predict" method="POST">
46
+ <div class="form-group">
47
+ <label for="model-select">Select AI Model:</label>
48
+ <select id="model-select" name="model" class="model-select">
49
+ <option value="mobilenet">MobileNetV2</option>
50
+ <option value="efficientnet">EfficientNetB0</option>
51
+ </select>
52
+ </div>
53
+
54
+ <div class="upload-container">
55
+ <div id="upload-box" class="upload-box">
56
+ <input type="file" name="file" id="file-input" hidden accept=".jpg,.jpeg,.png">
57
+ <div id="preview-container" class="preview-container hidden">
58
+ <img id="image-preview" src="#" alt="Image preview">
59
+ <button type="button" id="remove-image" class="remove-image" aria-label="Remove image">
60
+ <i class="fas fa-times"></i>
61
+ </button>
62
+ </div>
63
+ <div id="upload-prompt" class="upload-prompt">
64
+ <div class="upload-icon">
65
+ <i class="fas fa-cloud-upload-alt"></i>
66
+ </div>
67
+ <p>Drag & drop your image here or <span class="browse-text">browse</span></p>
68
+ <p class="file-types">Supported formats: JPG, JPEG, PNG</p>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <button type="submit" id="analyze-button" class="btn btn-primary analyze-button" disabled>
74
+ <i class="fas fa-search"></i> Analyze Image
75
+ </button>
76
+ </form>
77
+
78
+ <div id="loading" class="loading">
79
+ <div class="spinner">
80
+ <div class="bounce1"></div>
81
+ <div class="bounce2"></div>
82
+ <div class="bounce3"></div>
83
+ </div>
84
+ <p>Analyzing your image...</p>
85
+ </div>
86
+
87
+ <div id="error-message" class="error-message hidden">
88
+ <div class="error-icon">
89
+ <i class="fas fa-exclamation-circle"></i>
90
+ </div>
91
+ <div class="error-content">
92
+ <h3>Error</h3>
93
+ <p id="error-text">An error occurred. Please try again.</p>
94
+ </div>
95
+ </div>
96
+
97
+ <div id="result" class="result-card hidden">
98
+ <div class="result-header">
99
+ <h2><i class="fas fa-chart-pie"></i> Analysis Results</h2>
100
+ <span class="model-badge" id="model-used-badge">MobileNetV2</span>
101
+ </div>
102
+
103
+ <div class="result-summary">
104
+ <div class="result-image-container">
105
+ <img id="result-image" src="#" alt="Analyzed image">
106
+ </div>
107
+
108
+ <div class="result-details">
109
+ <div class="prediction-box">
110
+ <h3>Primary Prediction</h3>
111
+ <div class="prediction-name" id="prediction"></div>
112
+ <div class="prediction-description" id="description"></div>
113
+ <div class="confidence-meter">
114
+ <div class="confidence-label">Confidence:</div>
115
+ <div class="confidence-bar">
116
+ <div id="confidence-fill" class="confidence-fill"></div>
117
+ </div>
118
+ <div id="confidence" class="confidence-value">0%</div>
119
+ </div>
120
+ </div>
121
+
122
+ <div class="severity-indicator" id="severity-indicator">
123
+ <!-- Will be populated by JS based on prediction -->
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <div class="result-tabs">
129
+ <button class="tab-button active" data-tab="probabilities">Probabilities</button>
130
+ <button class="tab-button" data-tab="information">Information</button>
131
+ <button class="tab-button" data-tab="next-steps">Next Steps</button>
132
+ </div>
133
+
134
+ <div class="tab-content">
135
+ <div id="probabilities-tab" class="tab-pane active">
136
+ <h3>Class Probabilities</h3>
137
+ <div id="probabilities" class="probabilities-grid"></div>
138
+ </div>
139
+
140
+ <div id="information-tab" class="tab-pane">
141
+ <h3>About <span id="info-condition-name"></span></h3>
142
+ <div id="condition-information"></div>
143
+
144
+ <div class="info-resources">
145
+ <h4>Additional Resources</h4>
146
+ <ul id="resources-list">
147
+ <!-- Will be populated by JS based on prediction -->
148
+ </ul>
149
+ </div>
150
+ </div>
151
+
152
+ <div id="next-steps-tab" class="tab-pane">
153
+ <h3>Recommended Next Steps</h3>
154
+ <div class="next-steps-content">
155
+ <div class="next-step">
156
+ <div class="step-icon"><i class="fas fa-user-md"></i></div>
157
+ <div class="step-content">
158
+ <h4>Consult a Dermatologist</h4>
159
+ <p>This AI analysis is not a medical diagnosis. If you have concerns about a skin lesion, please consult with a dermatologist or healthcare provider.</p>
160
+ </div>
161
+ </div>
162
+
163
+ <div class="next-step">
164
+ <div class="step-icon"><i class="fas fa-notes-medical"></i></div>
165
+ <div class="step-content">
166
+ <h4>Keep Records</h4>
167
+ <p>Save this analysis and take regular photos of the lesion to track any changes over time.</p>
168
+ </div>
169
+ </div>
170
+
171
+ <div class="next-step">
172
+ <div class="step-icon"><i class="fas fa-shield-alt"></i></div>
173
+ <div class="step-content">
174
+ <h4>Sun Protection</h4>
175
+ <p>Regardless of the analysis result, practice sun safety by using sunscreen, wearing protective clothing, and avoiding peak sun hours.</p>
176
+ </div>
177
+ </div>
178
+ </div>
179
+
180
+ <div class="disclaimer-box">
181
+ <p><strong>Important:</strong> SkinAI is an educational tool and not a substitute for professional medical advice. Always consult healthcare professionals for medical concerns.</p>
182
+ </div>
183
+ </div>
184
+ </div>
185
+
186
+ <div class="result-actions">
187
+ <button id="save-result" class="btn btn-secondary">
188
+ <i class="fas fa-download"></i> Save Results
189
+ </button>
190
+ <button id="new-analysis" class="btn btn-primary">
191
+ <i class="fas fa-redo"></i> New Analysis
192
+ </button>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </section>
199
+
200
+ <section class="faq-preview">
201
+ <div class="container">
202
+ <h2>Frequently Asked Questions</h2>
203
+ <div class="faq-items">
204
+ <div class="faq-item">
205
+ <div class="faq-question">
206
+ <h3>How accurate is the AI analysis?</h3>
207
+ <span class="faq-toggle"><i class="fas fa-chevron-down"></i></span>
208
+ </div>
209
+ <div class="faq-answer">
210
+ <p>Our models have been trained on large datasets of dermatological images. MobileNetV2 has approximately 74% accuracy on the HAM10000 dataset, while EfficientNetB0 has about 69% accuracy on skin cancer classification. However, AI analysis should never replace professional medical diagnosis.</p>
211
+ </div>
212
+ </div>
213
+
214
+ <div class="faq-item">
215
+ <div class="faq-question">
216
+ <h3>Is my data private and secure?</h3>
217
+ <span class="faq-toggle"><i class="fas fa-chevron-down"></i></span>
218
+ </div>
219
+ <div class="faq-answer">
220
+ <p>Yes. We do not store your uploaded images on our servers. All processing is done in real-time, and images are not saved after analysis. Your privacy is our priority.</p>
221
+ </div>
222
+ </div>
223
+
224
+ <div class="faq-item">
225
+ <div class="faq-question">
226
+ <h3>Which skin conditions can the AI detect?</h3>
227
+ <span class="faq-toggle"><i class="fas fa-chevron-down"></i></span>
228
+ </div>
229
+ <div class="faq-answer">
230
+ <p>Our AI models can analyze seven types of skin lesions: Actinic keratoses, Basal cell carcinoma, Benign keratosis-like lesions, Dermatofibroma, Melanoma, Melanocytic nevi, and Vascular lesions.</p>
231
+ </div>
232
+ </div>
233
+ </div>
234
+
235
+ <div class="faq-more">
236
+ <a href="{{ url_for('faq') }}" class="btn btn-outline">View All FAQs</a>
237
+ </div>
238
+ </div>
239
+ </section>
240
+ {% endblock %}
241
+
242
+ {% block extra_js %}
243
+ <script src="{{ url_for('static', filename='js/detect.js') }}"></script>
244
+ <script>
245
+ // Make class descriptions available to JavaScript
246
+ window.CLASS_DESCRIPTIONS = {{ class_descriptions|tojson }};
247
+ window.CONDITION_INFO = {{ condition_info|tojson }};
248
+ </script>
249
+ {% endblock %}
templates/faq.html ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <section class="container">
5
+ <h1>Frequently Asked Questions</h1>
6
+ <!-- Add FAQ content -->
7
+ </section>
8
+ {% endblock %}
9
+
templates/index.html ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}SkinAI - AI-Powered Skin Cancer Detection{% endblock %}
4
+
5
+ {% block content %}
6
+ <section class="hero">
7
+ <div class="container">
8
+ <div class="hero-content">
9
+ <h1>Early Detection Saves Lives</h1>
10
+ <p class="hero-subtitle">Advanced AI-powered analysis of skin lesions using state-of-the-art deep learning models</p>
11
+ <div class="hero-buttons">
12
+ <a href="{{ url_for('detect') }}" class="btn btn-primary">Analyze Your Skin</a>
13
+ <a href="{{ url_for('about') }}" class="btn btn-secondary">Learn More</a>
14
+ </div>
15
+ </div>
16
+ </div>
17
+ </section>
18
+
19
+ <section class="how-it-works">
20
+ <div class="container">
21
+ <h2 class="section-title">How It Works</h2>
22
+ <div class="steps">
23
+ <div class="step">
24
+ <div class="step-icon">
25
+ <i class="fas fa-camera"></i>
26
+ </div>
27
+ <h3>Upload Image</h3>
28
+ <p>Take a clear photo of your skin lesion or upload an existing image</p>
29
+ </div>
30
+ <div class="step">
31
+ <div class="step-icon">
32
+ <i class="fas fa-robot"></i>
33
+ </div>
34
+ <h3>AI Analysis</h3>
35
+ <p>Our advanced AI models analyze the image with high precision</p>
36
+ </div>
37
+ <div class="step">
38
+ <div class="step-icon">
39
+ <i class="fas fa-chart-bar"></i>
40
+ </div>
41
+ <h3>Get Results</h3>
42
+ <p>Receive detailed analysis with probability scores for different conditions</p>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </section>
47
+
48
+ <section class="models">
49
+ <div class="container">
50
+ <h2 class="section-title">Our AI Models</h2>
51
+ <div class="model-cards">
52
+ <div class="model-card">
53
+ <div class="model-icon">
54
+ <i class="fas fa-mobile-alt"></i>
55
+ </div>
56
+ <h3>MobileNetV2</h3>
57
+ <p>Lightweight CNN model optimized for mobile devices with 74% accuracy on the HAM10000 dataset</p>
58
+ <ul class="model-features">
59
+ <li><i class="fas fa-check"></i> Fast processing</li>
60
+ <li><i class="fas fa-check"></i> Low resource usage</li>
61
+ <li><i class="fas fa-check"></i> Optimized for mobile</li>
62
+ </ul>
63
+ </div>
64
+
65
+ <div class="model-card">
66
+ <div class="model-icon">
67
+ <i class="fas fa-brain"></i>
68
+ </div>
69
+ <h3>EfficientNetB0</h3>
70
+ <p>State-of-the-art architecture with optimal accuracy of 69% on skin cancer classification</p>
71
+ <ul class="model-features">
72
+ <li><i class="fas fa-check"></i> Higher accuracy</li>
73
+ <li><i class="fas fa-check"></i> Advanced feature extraction</li>
74
+ <li><i class="fas fa-check"></i> Balanced performance</li>
75
+ </ul>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </section>
80
+
81
+ <section class="stats">
82
+ <div class="container">
83
+ <div class="stats-grid">
84
+ <div class="stat-item">
85
+ <div class="stat-number">5+</div>
86
+ <div class="stat-label">Million</div>
87
+ <div class="stat-desc">Skin cancer cases diagnosed yearly worldwide</div>
88
+ </div>
89
+ <div class="stat-item">
90
+ <div class="stat-number">99%</div>
91
+ <div class="stat-label">Survival Rate</div>
92
+ <div class="stat-desc">When melanoma is detected early</div>
93
+ </div>
94
+ <div class="stat-item">
95
+ <div class="stat-number">7</div>
96
+ <div class="stat-label">Types</div>
97
+ <div class="stat-desc">Of skin lesions our AI can detect</div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </section>
102
+
103
+ <section class="cta">
104
+ <div class="container">
105
+ <h2>Ready to check your skin?</h2>
106
+ <p>Early detection is key to successful treatment of skin cancer.</p>
107
+ <a href="{{ url_for('detect') }}" class="btn btn-primary">Get Started Now</a>
108
+ </div>
109
+ </section>
110
+
111
+ <section class="disclaimer">
112
+ <div class="container">
113
+ <div class="disclaimer-box">
114
+ <h3><i class="fas fa-exclamation-triangle"></i> Medical Disclaimer</h3>
115
+ <p>SkinAI is designed to be an educational tool only and is not a substitute for professional medical advice, diagnosis, or treatment. Always seek the advice of your physician or other qualified health provider with any questions you may have regarding a medical condition.</p>
116
+ </div>
117
+ </div>
118
+ </section>
119
+ {% endblock %}
templates/privacy.html ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <section class="container">
5
+ <h1>Privacy</h1>
6
+ <!-- Add contact form -->
7
+ </section>
8
+ {% endblock %}
templates/terms.html ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <section class="container">
5
+ <h1>Terms</h1>
6
+ <!-- Add contact form -->
7
+ </section>
8
+ {% endblock %}