OnsAouedi commited on
Commit
dbadf8f
·
verified ·
1 Parent(s): d62b7bc

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +30 -0
  2. requirements.txt +13 -0
  3. templates/vessel_inference.html +681 -0
Dockerfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copy requirements first for better caching
11
+ COPY requirements.txt .
12
+
13
+ # Install Python dependencies
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Copy application files
17
+ COPY . .
18
+
19
+ # Create necessary directories
20
+ RUN mkdir -p uploads results
21
+
22
+ # Expose port
23
+ EXPOSE 7860
24
+
25
+ # Set environment variables
26
+ ENV PYTHONPATH=/app
27
+ ENV FLASK_APP=app.py
28
+
29
+ # Run the application
30
+ CMD ["python", "app.py"]
requirements.txt CHANGED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core ML and Data Processing
2
+ torch>=1.9.0,<2.1.0
3
+ pandas>=1.3.0,<2.1.0
4
+ numpy>=1.21.0,<1.25.0
5
+ tqdm>=4.62.0
6
+
7
+ # Web Interface
8
+ flask>=2.0.0,<3.0.0
9
+ flask-socketio>=5.1.0,<6.0.0
10
+ werkzeug>=2.0.0,<3.0.0
11
+
12
+ # Additional utilities
13
+ pathlib
templates/vessel_inference.html ADDED
@@ -0,0 +1,681 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>🚢 Vessel Trajectory Inference</title>
7
+ <script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
8
+ <!-- Chart.js for error distribution plot -->
9
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
10
+ <style>
11
+ * {
12
+ margin: 0;
13
+ padding: 0;
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ body {
18
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
19
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
20
+ min-height: 100vh;
21
+ padding: 20px;
22
+ }
23
+
24
+ .container {
25
+ max-width: 900px;
26
+ margin: 0 auto;
27
+ background: white;
28
+ border-radius: 15px;
29
+ box-shadow: 0 20px 40px rgba(0,0,0,0.1);
30
+ overflow: hidden;
31
+ }
32
+
33
+ .header {
34
+ background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
35
+ color: white;
36
+ padding: 30px;
37
+ text-align: center;
38
+ }
39
+
40
+ .header h1 {
41
+ font-size: 2.5rem;
42
+ margin-bottom: 10px;
43
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
44
+ }
45
+
46
+ .header p {
47
+ font-size: 1.1rem;
48
+ opacity: 0.9;
49
+ margin-bottom: 5px;
50
+ }
51
+
52
+ .subtitle {
53
+ font-size: 0.95rem !important;
54
+ font-style: italic;
55
+ opacity: 0.8 !important;
56
+ }
57
+
58
+ .content {
59
+ padding: 40px;
60
+ }
61
+
62
+ .data-format-info {
63
+ background: #e8f4fd;
64
+ border: 2px solid #3498db;
65
+ border-radius: 10px;
66
+ padding: 20px;
67
+ margin-bottom: 30px;
68
+ }
69
+
70
+ .data-format-info h3 {
71
+ color: #2c3e50;
72
+ margin-bottom: 15px;
73
+ font-size: 1.3rem;
74
+ }
75
+
76
+ .required-columns {
77
+ display: grid;
78
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
79
+ gap: 10px;
80
+ margin-top: 15px;
81
+ }
82
+
83
+ .column-item {
84
+ background: white;
85
+ padding: 10px;
86
+ border-radius: 5px;
87
+ border-left: 4px solid #3498db;
88
+ font-family: 'Courier New', monospace;
89
+ font-size: 0.9rem;
90
+ }
91
+
92
+ .upload-section {
93
+ margin-bottom: 30px;
94
+ }
95
+
96
+ .upload-section h3 {
97
+ color: #2c3e50;
98
+ margin-bottom: 15px;
99
+ font-size: 1.3rem;
100
+ }
101
+
102
+ .file-upload {
103
+ border: 2px dashed #3498db;
104
+ border-radius: 10px;
105
+ padding: 30px;
106
+ text-align: center;
107
+ background: #f8f9fa;
108
+ margin-bottom: 20px;
109
+ transition: all 0.3s ease;
110
+ }
111
+
112
+ .file-upload:hover {
113
+ border-color: #2980b9;
114
+ background: #e8f4fd;
115
+ }
116
+
117
+ .file-upload input[type="file"] {
118
+ display: none;
119
+ }
120
+
121
+ .file-upload label {
122
+ display: block;
123
+ cursor: pointer;
124
+ font-size: 1.1rem;
125
+ color: #2c3e50;
126
+ }
127
+
128
+ .file-upload .upload-icon {
129
+ font-size: 3rem;
130
+ margin-bottom: 10px;
131
+ color: #3498db;
132
+ }
133
+
134
+ .file-info {
135
+ margin-top: 10px;
136
+ padding: 10px;
137
+ background: #d4edda;
138
+ border-radius: 5px;
139
+ border: 1px solid #c3e6cb;
140
+ display: none;
141
+ }
142
+
143
+ .optional-uploads {
144
+ display: grid;
145
+ grid-template-columns: 1fr 1fr;
146
+ gap: 20px;
147
+ margin-top: 20px;
148
+ }
149
+
150
+ .optional-upload {
151
+ border: 1px solid #dee2e6;
152
+ border-radius: 8px;
153
+ padding: 20px;
154
+ background: #f8f9fa;
155
+ }
156
+
157
+ .optional-upload h4 {
158
+ color: #495057;
159
+ margin-bottom: 10px;
160
+ font-size: 1rem;
161
+ }
162
+
163
+ .optional-upload input[type="file"] {
164
+ width: 100%;
165
+ padding: 8px;
166
+ border: 1px solid #ced4da;
167
+ border-radius: 4px;
168
+ font-size: 0.9rem;
169
+ }
170
+
171
+ .btn {
172
+ background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
173
+ color: white;
174
+ border: none;
175
+ padding: 15px 30px;
176
+ font-size: 1.1rem;
177
+ border-radius: 8px;
178
+ cursor: pointer;
179
+ transition: all 0.3s ease;
180
+ width: 100%;
181
+ margin-top: 20px;
182
+ }
183
+
184
+ .btn:hover {
185
+ transform: translateY(-2px);
186
+ box-shadow: 0 5px 15px rgba(52, 152, 219, 0.4);
187
+ }
188
+
189
+ .btn:disabled {
190
+ background: #95a5a6;
191
+ cursor: not-allowed;
192
+ transform: none;
193
+ box-shadow: none;
194
+ }
195
+
196
+ .progress-container {
197
+ display: none;
198
+ margin-top: 30px;
199
+ padding: 20px;
200
+ background: #f8f9fa;
201
+ border-radius: 10px;
202
+ border: 1px solid #dee2e6;
203
+ }
204
+
205
+ .progress-bar {
206
+ width: 100%;
207
+ height: 25px;
208
+ background: #e9ecef;
209
+ border-radius: 15px;
210
+ overflow: hidden;
211
+ margin-bottom: 15px;
212
+ }
213
+
214
+ .progress-fill {
215
+ height: 100%;
216
+ background: linear-gradient(90deg, #3498db, #2ecc71);
217
+ width: 0%;
218
+ transition: width 0.3s ease;
219
+ border-radius: 15px;
220
+ }
221
+
222
+ .progress-text {
223
+ text-align: center;
224
+ font-weight: bold;
225
+ color: #2c3e50;
226
+ margin-bottom: 10px;
227
+ }
228
+
229
+ .progress-details {
230
+ text-align: center;
231
+ color: #7f8c8d;
232
+ font-size: 0.9rem;
233
+ }
234
+
235
+ .results-container {
236
+ display: none;
237
+ margin-top: 30px;
238
+ padding: 20px;
239
+ background: #d4edda;
240
+ border-radius: 10px;
241
+ border: 1px solid #c3e6cb;
242
+ }
243
+
244
+ .results-stats {
245
+ display: grid;
246
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
247
+ gap: 15px;
248
+ margin-bottom: 20px;
249
+ }
250
+
251
+ .stat-item {
252
+ background: white;
253
+ padding: 15px;
254
+ border-radius: 8px;
255
+ text-align: center;
256
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
257
+ }
258
+
259
+ .stat-value {
260
+ font-size: 1.5rem;
261
+ font-weight: bold;
262
+ color: #2c3e50;
263
+ }
264
+
265
+ .stat-label {
266
+ font-size: 0.9rem;
267
+ color: #7f8c8d;
268
+ margin-top: 5px;
269
+ }
270
+
271
+ .download-btn {
272
+ background: linear-gradient(135deg, #27ae60 0%, #229954 100%);
273
+ margin-top: 0;
274
+ }
275
+
276
+ .download-btn:hover {
277
+ box-shadow: 0 5px 15px rgba(39, 174, 96, 0.4);
278
+ }
279
+
280
+ .error-container {
281
+ display: none;
282
+ margin-top: 20px;
283
+ padding: 20px;
284
+ background: #f8d7da;
285
+ border-radius: 10px;
286
+ border: 1px solid #f5c6cb;
287
+ color: #721c24;
288
+ }
289
+
290
+ .model-info {
291
+ background: #fff3cd;
292
+ border: 1px solid #ffeaa7;
293
+ border-radius: 8px;
294
+ padding: 15px;
295
+ margin-bottom: 20px;
296
+ }
297
+
298
+ .model-info h4 {
299
+ color: #856404;
300
+ margin-bottom: 10px;
301
+ }
302
+
303
+ .model-info ul {
304
+ margin-left: 20px;
305
+ color: #856404;
306
+ }
307
+
308
+ @media (max-width: 768px) {
309
+ .container {
310
+ margin: 10px;
311
+ border-radius: 10px;
312
+ }
313
+
314
+ .header {
315
+ padding: 20px;
316
+ }
317
+
318
+ .header h1 {
319
+ font-size: 2rem;
320
+ }
321
+
322
+ .content {
323
+ padding: 20px;
324
+ }
325
+
326
+ .optional-uploads {
327
+ grid-template-columns: 1fr;
328
+ }
329
+
330
+ .results-stats {
331
+ grid-template-columns: 1fr;
332
+ }
333
+ }
334
+ </style>
335
+ </head>
336
+ <body>
337
+ <div class="container">
338
+ <div class="header">
339
+ <h1>🚢 Vessel Trajectory Inference</h1>
340
+ <p>Vessel trajectory prediction using our model logic</p>
341
+ <p class="subtitle">Upload your preprocessed dataset with segments and velocity features</p>
342
+ </div>
343
+
344
+ <div class="content">
345
+ <!-- Data Format Information -->
346
+ <div class="data-format-info">
347
+ <h3>📋 Required Data Format</h3>
348
+ <p>Your CSV file should contain the following columns (automatically detects ';' or ',' separator):</p>
349
+ <div class="required-columns">
350
+ <div class="column-item">segment</div>
351
+ <div class="column-item">latitude_velocity_km</div>
352
+ <div class="column-item">longitude_velocity_km</div>
353
+ <div class="column-item">latitude_degrees</div>
354
+ <div class="column-item">longitude_degrees</div>
355
+ <div class="column-item">time_difference_hours</div>
356
+ <div class="column-item">time_scalar (or datetime)</div>
357
+ </div>
358
+ <p style="margin-top: 15px; font-style: italic; color: #495057;">
359
+ ✅ Your data format is supported! The app will automatically create time_scalar from datetime/time_decimal if needed.
360
+ </p>
361
+ </div>
362
+
363
+ <!-- Model Information -->
364
+ <div class="model-info">
365
+ <h4>🤖 Default Model Configuration</h4>
366
+ <ul>
367
+ <li><strong>Model:</strong> LSTMWithAttentionWithResid (7 layers, 250 hidden units)</li>
368
+ <li><strong>Training:</strong> Atlantic Ocean vessel trajectories</li>
369
+ <li><strong>Normalization:</strong> Atlantic dataset parameters</li>
370
+ <li><strong>Sequence Length:</strong> 12 time steps</li>
371
+ <li><strong>Forecast Horizon:</strong> 1 time step</li>
372
+ </ul>
373
+ </div>
374
+
375
+ <!-- File Upload Section -->
376
+ <div class="upload-section">
377
+ <h3>📁 Upload Files</h3>
378
+
379
+ <!-- Main CSV Upload -->
380
+ <div class="file-upload" onclick="document.getElementById('csv_file').click()">
381
+ <div class="upload-icon">📊</div>
382
+ <label for="csv_file">
383
+ <strong>Select Your Inference Dataset (CSV)</strong><br>
384
+ Click here or drag and drop your CSV file
385
+ </label>
386
+ <input type="file" id="csv_file" accept=".csv" onchange="handleFileSelect(this, 'csv')">
387
+ <div id="csv_file_info" class="file-info"></div>
388
+ </div>
389
+
390
+ <!-- Optional Uploads -->
391
+ <div class="optional-uploads">
392
+ <div class="optional-upload">
393
+ <h4>🧠 Custom Model (Optional)</h4>
394
+ <input type="file" id="model_file" accept=".pth,.pt" onchange="handleFileSelect(this, 'model')">
395
+ <small>Default: best_model.pth</small>
396
+ </div>
397
+
398
+ <div class="optional-upload">
399
+ <h4>⚙️ Custom Normalization (Optional)</h4>
400
+ <input type="file" id="normalization_file" accept=".json" onchange="handleFileSelect(this, 'norm')">
401
+ <small>Default: Atlantic dataset parameters</small>
402
+ </div>
403
+ </div>
404
+ </div>
405
+
406
+ <!-- Submit Button -->
407
+ <button class="btn" id="submit_btn" onclick="startInference()" disabled>
408
+ 🚀 Start Inference
409
+ </button>
410
+
411
+ <!-- Progress Container -->
412
+ <div class="progress-container" id="progress_container">
413
+ <div class="progress-text" id="progress_text">Initializing...</div>
414
+ <div class="progress-bar">
415
+ <div class="progress-fill" id="progress_fill"></div>
416
+ </div>
417
+ <div class="progress-details" id="progress_details">Please wait...</div>
418
+ </div>
419
+
420
+ <!-- Results Container -->
421
+ <div class="results-container" id="results_container">
422
+ <h3>📈 Inference Results</h3>
423
+ <div class="results-stats" id="results_stats">
424
+ <!-- Stats will be populated dynamically -->
425
+ </div>
426
+ <button class="btn download-btn" onclick="downloadResults()">
427
+ 💾 Download Results CSV
428
+ </button>
429
+
430
+ <!-- Error Distribution Chart -->
431
+ <div style="margin-top:30px;">
432
+ <canvas id="errorChart" height="220"></canvas>
433
+ </div>
434
+ </div>
435
+
436
+ <!-- Error Container -->
437
+ <div class="error-container" id="error_container">
438
+ <h3>❌ Error</h3>
439
+ <p id="error_message"></p>
440
+ </div>
441
+ </div>
442
+ </div>
443
+
444
+ <script>
445
+ const socket = io();
446
+ let inferenceInProgress = false;
447
+
448
+ // File selection handlers
449
+ function handleFileSelect(input, type) {
450
+ const file = input.files[0];
451
+ if (file) {
452
+ const infoDiv = document.getElementById(input.id + '_info');
453
+ if (infoDiv) {
454
+ infoDiv.style.display = 'block';
455
+ infoDiv.innerHTML = `📁 Selected: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)`;
456
+ }
457
+
458
+ // Enable submit button if CSV is selected
459
+ if (type === 'csv') {
460
+ document.getElementById('submit_btn').disabled = false;
461
+ }
462
+ }
463
+ }
464
+
465
+ // Start inference
466
+ function startInference() {
467
+ if (inferenceInProgress) return;
468
+
469
+ const csvFile = document.getElementById('csv_file').files[0];
470
+ if (!csvFile) {
471
+ alert('Please select a CSV file first!');
472
+ return;
473
+ }
474
+
475
+ inferenceInProgress = true;
476
+ document.getElementById('submit_btn').disabled = true;
477
+ document.getElementById('progress_container').style.display = 'block';
478
+ document.getElementById('results_container').style.display = 'none';
479
+ document.getElementById('error_container').style.display = 'none';
480
+
481
+ // Prepare form data
482
+ const formData = new FormData();
483
+ formData.append('csv_file', csvFile);
484
+
485
+ const modelFile = document.getElementById('model_file').files[0];
486
+ if (modelFile) {
487
+ formData.append('model_file', modelFile);
488
+ }
489
+
490
+ const normFile = document.getElementById('normalization_file').files[0];
491
+ if (normFile) {
492
+ formData.append('normalization_file', normFile);
493
+ }
494
+
495
+ // Upload files and start inference
496
+ fetch('/upload', {
497
+ method: 'POST',
498
+ body: formData
499
+ })
500
+ .then(response => response.json())
501
+ .then(data => {
502
+ if (!data.success) {
503
+ showError(data.error);
504
+ resetUI();
505
+ }
506
+ })
507
+ .catch(error => {
508
+ showError('Upload failed: ' + error.message);
509
+ resetUI();
510
+ });
511
+ }
512
+
513
+ // Download results
514
+ function downloadResults() {
515
+ window.location.href = '/download_results';
516
+ }
517
+
518
+ // Show error
519
+ function showError(message) {
520
+ document.getElementById('error_container').style.display = 'block';
521
+ document.getElementById('error_message').textContent = message;
522
+ document.getElementById('progress_container').style.display = 'none';
523
+ }
524
+
525
+ // Reset UI
526
+ function resetUI() {
527
+ inferenceInProgress = false;
528
+ document.getElementById('submit_btn').disabled = !document.getElementById('csv_file').files[0];
529
+ document.getElementById('progress_container').style.display = 'none';
530
+ }
531
+
532
+ // Show results
533
+ function showResults(stats, histogram) {
534
+ const resultsStats = document.getElementById('results_stats');
535
+ resultsStats.innerHTML = `
536
+ <div class="stat-item">
537
+ <div class="stat-value">${stats.mean_error_km.toFixed(2)} km</div>
538
+ <div class="stat-label">Mean Error</div>
539
+ </div>
540
+ <div class="stat-item">
541
+ <div class="stat-value">${stats.median_error_km.toFixed(2)} km</div>
542
+ <div class="stat-label">Median Error</div>
543
+ </div>
544
+ <div class="stat-item">
545
+ <div class="stat-value">${stats.total_predictions.toLocaleString()}</div>
546
+ <div class="stat-label">Total Predictions</div>
547
+ </div>
548
+ <div class="stat-item">
549
+ <div class="stat-value">${stats.total_segments}</div>
550
+ <div class="stat-label">Segments Processed</div>
551
+ </div>
552
+ <div class="stat-item">
553
+ <div class="stat-value">${stats.min_error_km.toFixed(2)} km</div>
554
+ <div class="stat-label">Min Error</div>
555
+ </div>
556
+ <div class="stat-item">
557
+ <div class="stat-value">${stats.max_error_km.toFixed(2)} km</div>
558
+ <div class="stat-label">Max Error</div>
559
+ </div>
560
+ `;
561
+
562
+ // Render error distribution histogram if data provided
563
+ if (histogram && histogram.bins && histogram.counts) {
564
+ renderErrorChart(histogram);
565
+ }
566
+ document.getElementById('results_container').style.display = 'block';
567
+ document.getElementById('progress_container').style.display = 'none';
568
+ resetUI();
569
+ }
570
+
571
+ // Chart.js rendering function
572
+ let errorChartInstance = null;
573
+ function renderErrorChart(histogram) {
574
+ const ctx = document.getElementById('errorChart').getContext('2d');
575
+ // Prepare labels as mid-points of bins
576
+ const labels = [];
577
+ for (let i = 0; i < histogram.bins.length - 1; i++) {
578
+ const mid = (histogram.bins[i] + histogram.bins[i + 1]) / 2;
579
+ labels.push(mid.toFixed(1));
580
+ }
581
+ // Destroy previous chart if it exists (for multiple runs)
582
+ if (errorChartInstance) {
583
+ errorChartInstance.destroy();
584
+ }
585
+ errorChartInstance = new Chart(ctx, {
586
+ type: 'bar',
587
+ data: {
588
+ labels: labels,
589
+ datasets: [{
590
+ label: 'Error (km) distribution',
591
+ data: histogram.counts,
592
+ backgroundColor: 'rgba(52, 152, 219, 0.5)',
593
+ borderColor: 'rgba(41, 128, 185, 1)',
594
+ borderWidth: 1
595
+ }]
596
+ },
597
+ options: {
598
+ scales: {
599
+ x: {
600
+ title: {
601
+ display: true,
602
+ text: 'Error (km)'
603
+ }
604
+ },
605
+ y: {
606
+ beginAtZero: true,
607
+ title: {
608
+ display: true,
609
+ text: 'Count'
610
+ }
611
+ }
612
+ }
613
+ }
614
+ });
615
+ }
616
+
617
+ // Socket event handlers
618
+ socket.on('progress_update', function(data) {
619
+ document.getElementById('progress_text').textContent = data.step;
620
+ document.getElementById('progress_fill').style.width = data.progress + '%';
621
+ document.getElementById('progress_details').textContent = data.details;
622
+
623
+ if (data.step === 'Complete') {
624
+ // Results will be shown via the inference_complete event
625
+ } else if (data.step === 'Error') {
626
+ showError(data.details);
627
+ resetUI();
628
+ }
629
+ });
630
+
631
+ socket.on('inference_complete', function(data) {
632
+ if (data.success) {
633
+ showResults(data.stats, data.histogram);
634
+ } else {
635
+ showError(data.error);
636
+ resetUI();
637
+ }
638
+ });
639
+
640
+ // Drag and drop functionality
641
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
642
+ document.addEventListener(eventName, preventDefaults, false);
643
+ });
644
+
645
+ function preventDefaults(e) {
646
+ e.preventDefault();
647
+ e.stopPropagation();
648
+ }
649
+
650
+ ['dragenter', 'dragover'].forEach(eventName => {
651
+ document.querySelector('.file-upload').addEventListener(eventName, highlight, false);
652
+ });
653
+
654
+ ['dragleave', 'drop'].forEach(eventName => {
655
+ document.querySelector('.file-upload').addEventListener(eventName, unhighlight, false);
656
+ });
657
+
658
+ function highlight(e) {
659
+ e.currentTarget.style.borderColor = '#2980b9';
660
+ e.currentTarget.style.background = '#e8f4fd';
661
+ }
662
+
663
+ function unhighlight(e) {
664
+ e.currentTarget.style.borderColor = '#3498db';
665
+ e.currentTarget.style.background = '#f8f9fa';
666
+ }
667
+
668
+ document.querySelector('.file-upload').addEventListener('drop', handleDrop, false);
669
+
670
+ function handleDrop(e) {
671
+ const dt = e.dataTransfer;
672
+ const files = dt.files;
673
+
674
+ if (files.length > 0) {
675
+ document.getElementById('csv_file').files = files;
676
+ handleFileSelect(document.getElementById('csv_file'), 'csv');
677
+ }
678
+ }
679
+ </script>
680
+ </body>
681
+ </html>