ragdoll-rails 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/ragdoll/application.js +129 -0
  3. data/app/assets/javascripts/ragdoll/bulk_upload_status.js +454 -0
  4. data/app/assets/stylesheets/ragdoll/application.css +84 -0
  5. data/app/assets/stylesheets/ragdoll/bulk_upload_status.css +379 -0
  6. data/app/channels/application_cable/channel.rb +6 -0
  7. data/app/channels/application_cable/connection.rb +6 -0
  8. data/app/channels/ragdoll/bulk_upload_status_channel.rb +27 -0
  9. data/app/channels/ragdoll/file_processing_channel.rb +26 -0
  10. data/app/components/ragdoll/alert_component.html.erb +4 -0
  11. data/app/components/ragdoll/alert_component.rb +32 -0
  12. data/app/components/ragdoll/application_component.rb +6 -0
  13. data/app/components/ragdoll/card_component.html.erb +15 -0
  14. data/app/components/ragdoll/card_component.rb +21 -0
  15. data/app/components/ragdoll/document_list_component.html.erb +41 -0
  16. data/app/components/ragdoll/document_list_component.rb +13 -0
  17. data/app/components/ragdoll/document_table_component.html.erb +76 -0
  18. data/app/components/ragdoll/document_table_component.rb +13 -0
  19. data/app/components/ragdoll/empty_state_component.html.erb +12 -0
  20. data/app/components/ragdoll/empty_state_component.rb +17 -0
  21. data/app/components/ragdoll/flash_messages_component.html.erb +3 -0
  22. data/app/components/ragdoll/flash_messages_component.rb +37 -0
  23. data/app/components/ragdoll/navbar_component.html.erb +24 -0
  24. data/app/components/ragdoll/navbar_component.rb +31 -0
  25. data/app/components/ragdoll/page_header_component.html.erb +13 -0
  26. data/app/components/ragdoll/page_header_component.rb +15 -0
  27. data/app/components/ragdoll/stats_card_component.html.erb +11 -0
  28. data/app/components/ragdoll/stats_card_component.rb +17 -0
  29. data/app/components/ragdoll/status_badge_component.html.erb +3 -0
  30. data/app/components/ragdoll/status_badge_component.rb +30 -0
  31. data/app/controllers/ragdoll/api/v1/analytics_controller.rb +72 -0
  32. data/app/controllers/ragdoll/api/v1/base_controller.rb +29 -0
  33. data/app/controllers/ragdoll/api/v1/documents_controller.rb +148 -0
  34. data/app/controllers/ragdoll/api/v1/search_controller.rb +87 -0
  35. data/app/controllers/ragdoll/api/v1/system_controller.rb +97 -0
  36. data/app/controllers/ragdoll/application_controller.rb +17 -0
  37. data/app/controllers/ragdoll/configuration_controller.rb +82 -0
  38. data/app/controllers/ragdoll/dashboard_controller.rb +98 -0
  39. data/app/controllers/ragdoll/documents_controller.rb +460 -0
  40. data/app/controllers/ragdoll/documents_controller_backup.rb +68 -0
  41. data/app/controllers/ragdoll/jobs_controller.rb +116 -0
  42. data/app/controllers/ragdoll/search_controller.rb +368 -0
  43. data/app/jobs/application_job.rb +9 -0
  44. data/app/jobs/ragdoll/bulk_document_processing_job.rb +280 -0
  45. data/app/jobs/ragdoll/process_file_job.rb +166 -0
  46. data/app/services/ragdoll/worker_health_service.rb +111 -0
  47. data/app/views/layouts/ragdoll/application.html.erb +162 -0
  48. data/app/views/ragdoll/dashboard/analytics.html.erb +333 -0
  49. data/app/views/ragdoll/dashboard/index.html.erb +208 -0
  50. data/app/views/ragdoll/documents/edit.html.erb +91 -0
  51. data/app/views/ragdoll/documents/index.html.erb +302 -0
  52. data/app/views/ragdoll/documents/new.html.erb +1518 -0
  53. data/app/views/ragdoll/documents/show.html.erb +188 -0
  54. data/app/views/ragdoll/documents/upload_results.html.erb +248 -0
  55. data/app/views/ragdoll/jobs/index.html.erb +669 -0
  56. data/app/views/ragdoll/jobs/show.html.erb +129 -0
  57. data/app/views/ragdoll/search/index.html.erb +324 -0
  58. data/config/cable.yml +12 -0
  59. data/config/routes.rb +56 -1
  60. data/lib/ragdoll/rails/engine.rb +32 -1
  61. data/lib/ragdoll/rails/version.rb +1 -1
  62. metadata +86 -1
@@ -0,0 +1,1518 @@
1
+ <% content_for :title, "Add Document - Ragdoll Engine" %>
2
+
3
+ <div class="row">
4
+ <div class="col-12">
5
+ <div class="d-flex justify-content-between align-items-center mb-4">
6
+ <h1><i class="fas fa-plus"></i> Add Document</h1>
7
+ <%= link_to "Back to Documents", ragdoll.documents_path, class: "btn btn-outline-secondary" %>
8
+ </div>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="row">
13
+ <div class="col-md-8">
14
+ <div class="card">
15
+ <div class="card-header">
16
+ <ul class="nav nav-tabs card-header-tabs" role="tablist">
17
+ <li class="nav-item">
18
+ <a class="nav-link active" id="file-tab" data-bs-toggle="tab" href="#file-upload" role="tab">
19
+ <i class="fas fa-upload"></i> Upload Files
20
+ </a>
21
+ </li>
22
+ <li class="nav-item">
23
+ <a class="nav-link" id="text-tab" data-bs-toggle="tab" href="#text-content" role="tab">
24
+ <i class="fas fa-keyboard"></i> Text Content
25
+ </a>
26
+ </li>
27
+ <li class="nav-item">
28
+ <a class="nav-link" id="directory-tab" data-bs-toggle="tab" href="#directory-upload" role="tab">
29
+ <i class="fas fa-folder"></i> Directory
30
+ </a>
31
+ </li>
32
+ </ul>
33
+ </div>
34
+ <div class="card-body">
35
+ <div class="tab-content">
36
+ <!-- File Upload Tab -->
37
+ <div class="tab-pane fade show active" id="file-upload" role="tabpanel">
38
+
39
+ <!-- Progress Upload Section -->
40
+ <div id="progress-upload-section">
41
+ <h6 class="text-primary mb-3">
42
+ <i class="fas fa-chart-line me-2"></i>Upload with Progress Tracking
43
+ </h6>
44
+
45
+ <!-- Status indicator -->
46
+ <div id="websocket-status" class="alert alert-info mb-3">
47
+ <i class="fas fa-spinner fa-spin me-2"></i>Checking real-time capabilities...
48
+ </div>
49
+
50
+ <!-- Progress Upload Form -->
51
+ <form id="progress-upload-form"
52
+ action="<%= ragdoll.upload_async_documents_path %>"
53
+ method="post"
54
+ enctype="multipart/form-data"
55
+ class="needs-validation mb-4"
56
+ novalidate>
57
+
58
+ <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
59
+
60
+ <div class="mb-3">
61
+ <label for="progress_files" class="form-label">Select Files</label>
62
+ <input type="file"
63
+ name="ragdoll_document[files][]"
64
+ id="progress_files"
65
+ multiple
66
+ accept=".pdf,.docx,.txt,.md,.html,.json,.xml,.csv"
67
+ class="form-control"
68
+ required>
69
+ <div class="form-text">
70
+ Supported formats: PDF, DOCX, TXT, MD, HTML, JSON, XML, CSV
71
+ </div>
72
+ <div class="invalid-feedback">
73
+ Please select at least one file.
74
+ </div>
75
+ </div>
76
+
77
+ <div class="mb-3">
78
+ <label for="progress_metadata" class="form-label">Metadata (Optional)</label>
79
+ <textarea name="ragdoll_document[metadata]"
80
+ id="progress_metadata"
81
+ placeholder='{"author": "Your Name", "category": "documentation"}'
82
+ class="form-control"
83
+ rows="3"></textarea>
84
+ <div class="form-text">
85
+ Additional metadata in JSON format
86
+ </div>
87
+ </div>
88
+
89
+ <div class="mb-3">
90
+ <div class="form-check">
91
+ <input type="checkbox"
92
+ name="ragdoll_document[force_duplicate]"
93
+ value="1"
94
+ id="progress_force_duplicate"
95
+ class="form-check-input">
96
+ <label for="progress_force_duplicate" class="form-check-label">
97
+ <i class="fas fa-copy me-1 text-warning"></i>
98
+ Allow duplicate documents
99
+ </label>
100
+ <div class="form-text">
101
+ Check this to add documents even if duplicates already exist in the system
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <button type="submit" class="btn btn-primary" id="progress-upload-btn">
107
+ <i class="fas fa-upload me-2"></i>
108
+ Upload Files with Progress
109
+ </button>
110
+ </form>
111
+
112
+ <!-- Progress Container -->
113
+ <div id="progress-container" class="mt-4" style="display: none;">
114
+ <div class="card">
115
+ <div class="card-header">
116
+ <h6 class="mb-0"><i class="fas fa-tasks me-2"></i>Upload Progress</h6>
117
+ </div>
118
+ <div class="card-body">
119
+ <div id="file-status-list"></div>
120
+ <div class="mt-3">
121
+ <div class="d-flex justify-content-between mb-2">
122
+ <span>Overall Progress:</span>
123
+ <span id="overall-percentage">0%</span>
124
+ </div>
125
+ <div class="progress">
126
+ <div id="overall-progress-bar"
127
+ class="progress-bar"
128
+ role="progressbar"
129
+ style="width: 0%"
130
+ aria-valuenow="0"
131
+ aria-valuemin="0"
132
+ aria-valuemax="100">
133
+ 0%
134
+ </div>
135
+ </div>
136
+ <small id="progress-status" class="text-muted mt-2 d-block">Ready to upload</small>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+
144
+ <!-- Text Content Tab -->
145
+ <div class="tab-pane fade" id="text-content" role="tabpanel">
146
+ <%= form_with model: @document,
147
+ url: ragdoll.documents_path,
148
+ local: true,
149
+ class: "needs-validation",
150
+ novalidate: true,
151
+ as: :ragdoll_document do |form| %>
152
+
153
+ <div class="mb-3">
154
+ <label for="document_title" class="form-label">Title</label>
155
+ <%= form.text_field :title,
156
+ placeholder: "Enter document title",
157
+ class: "form-control",
158
+ required: true %>
159
+ <div class="invalid-feedback">
160
+ Please provide a title.
161
+ </div>
162
+ </div>
163
+
164
+ <div class="mb-3">
165
+ <label for="document_text_content" class="form-label">Content</label>
166
+ <%= form.text_area :text_content,
167
+ placeholder: "Enter your text content here...",
168
+ class: "form-control",
169
+ rows: 10,
170
+ required: true %>
171
+ <div class="invalid-feedback">
172
+ Please provide some content.
173
+ </div>
174
+ </div>
175
+
176
+ <div class="mb-3">
177
+ <label for="text_metadata" class="form-label">Metadata (Optional)</label>
178
+ <%= form.text_area :metadata,
179
+ placeholder: '{"author": "Your Name", "category": "documentation"}',
180
+ class: "form-control",
181
+ rows: 3,
182
+ id: "text_metadata" %>
183
+ <div class="form-text">
184
+ Additional metadata in JSON format
185
+ </div>
186
+ </div>
187
+
188
+ <div class="mb-3">
189
+ <div class="form-check">
190
+ <%= form.check_box :force_duplicate,
191
+ { class: "form-check-input", id: "text_force_duplicate" },
192
+ "1", "0" %>
193
+ <label for="text_force_duplicate" class="form-check-label">
194
+ <i class="fas fa-copy me-1 text-warning"></i>
195
+ Allow duplicate documents
196
+ </label>
197
+ <div class="form-text">
198
+ Check this to add documents even if duplicates already exist in the system
199
+ </div>
200
+ </div>
201
+ </div>
202
+
203
+ <%= form.submit "Add Document", class: "btn btn-primary", id: "text-content-btn" %>
204
+ <% end %>
205
+ </div>
206
+
207
+ <!-- Directory Upload Tab -->
208
+ <div class="tab-pane fade" id="directory-upload" role="tabpanel">
209
+ <%= form_with url: ragdoll.bulk_upload_documents_path,
210
+ method: :post,
211
+ local: true,
212
+ multipart: true,
213
+ class: "needs-validation",
214
+ novalidate: true,
215
+ id: "directory-upload-form" do |form| %>
216
+
217
+ <div class="mb-3">
218
+ <label for="directory_files" class="form-label">Select Directory</label>
219
+ <%= form.file_field :directory_files,
220
+ multiple: true,
221
+ class: "form-control",
222
+ id: "directory_files",
223
+ required: true %>
224
+ <div class="form-text">
225
+ Click "Choose Files" and select a folder to upload all supported files within it
226
+ </div>
227
+ <div class="invalid-feedback">
228
+ Please select a directory.
229
+ </div>
230
+ </div>
231
+
232
+ <div class="mb-3">
233
+ <div class="form-check">
234
+ <input type="checkbox"
235
+ name="force_duplicate"
236
+ value="1"
237
+ id="directory_force_duplicate"
238
+ class="form-check-input">
239
+ <label for="directory_force_duplicate" class="form-check-label">
240
+ <i class="fas fa-copy me-1 text-warning"></i>
241
+ Allow duplicate documents
242
+ </label>
243
+ <div class="form-text">
244
+ Check this to add documents even if duplicates already exist in the system
245
+ </div>
246
+ </div>
247
+ </div>
248
+
249
+ <div class="alert alert-info">
250
+ <i class="fas fa-info-circle"></i>
251
+ <strong>Note:</strong> This will recursively process all supported files in the specified directory.
252
+ Large directories may take some time to process.
253
+ </div>
254
+
255
+ <button type="submit" class="btn btn-warning" id="directory-upload-btn">
256
+ <span class="spinner-border spinner-border-sm d-none me-2" role="status" aria-hidden="true"></span>
257
+ <span class="btn-text">Import Directory</span>
258
+ </button>
259
+
260
+ <div class="alert alert-warning mt-3 d-none" id="directory-processing-alert">
261
+ <i class="fas fa-spinner fa-spin"></i>
262
+ <strong>Processing...</strong> Your directory is being imported. This may take several minutes for large directories.
263
+ Please do not close this page.
264
+ </div>
265
+
266
+ <!-- Directory Upload Progress Container -->
267
+ <div id="directory-progress-container" class="mt-4" style="display: none;">
268
+ <div class="card">
269
+ <div class="card-header">
270
+ <h6 class="mb-0"><i class="fas fa-folder-open me-2"></i>Directory Import Progress</h6>
271
+ </div>
272
+ <div class="card-body">
273
+ <div class="mb-3">
274
+ <div class="d-flex justify-content-between mb-2">
275
+ <span>Upload Progress:</span>
276
+ <span id="directory-upload-percentage">0%</span>
277
+ </div>
278
+ <div class="progress mb-2">
279
+ <div id="directory-upload-progress-bar"
280
+ class="progress-bar"
281
+ role="progressbar"
282
+ style="width: 0%"
283
+ aria-valuenow="0"
284
+ aria-valuemin="0"
285
+ aria-valuemax="100">
286
+ 0%
287
+ </div>
288
+ </div>
289
+ </div>
290
+
291
+ <div class="mb-3">
292
+ <div class="d-flex justify-content-between mb-2">
293
+ <span>Processing Progress:</span>
294
+ <span id="directory-processing-percentage">0%</span>
295
+ </div>
296
+ <div class="progress">
297
+ <div id="directory-processing-progress-bar"
298
+ class="progress-bar bg-success"
299
+ role="progressbar"
300
+ style="width: 0%"
301
+ aria-valuenow="0"
302
+ aria-valuemin="0"
303
+ aria-valuemax="100">
304
+ 0%
305
+ </div>
306
+ </div>
307
+ </div>
308
+
309
+ <div class="row">
310
+ <div class="col-md-6">
311
+ <small class="text-muted">
312
+ <i class="fas fa-upload me-1"></i>
313
+ <span id="directory-files-uploaded">0</span> /
314
+ <span id="directory-total-files">0</span> files uploaded
315
+ </small>
316
+ </div>
317
+ <div class="col-md-6">
318
+ <small class="text-muted">
319
+ <i class="fas fa-check-circle me-1"></i>
320
+ <span id="directory-files-processed">0</span> files processed
321
+ </small>
322
+ </div>
323
+ </div>
324
+
325
+ <small id="directory-progress-status" class="text-muted mt-2 d-block">Preparing upload...</small>
326
+
327
+ <!-- Progress Details -->
328
+ <div id="directory-progress-details" class="mt-3" style="display: none;">
329
+ <div class="row">
330
+ <div class="col-12">
331
+ <div class="progress-details-container">
332
+ <h6 class="mb-2">Processing Details:</h6>
333
+ <div id="directory-current-file" class="text-muted mb-2">
334
+ <i class="fas fa-file-alt me-1"></i>
335
+ <span>Waiting to start...</span>
336
+ </div>
337
+ <div id="directory-progress-log" class="progress-log" style="max-height: 150px; overflow-y: auto; font-size: 0.85em; background: #f8f9fa; padding: 10px; border-radius: 5px;">
338
+ <!-- Progress messages will appear here -->
339
+ </div>
340
+ </div>
341
+ </div>
342
+ </div>
343
+ </div>
344
+ </div>
345
+ </div>
346
+ </div>
347
+ <% end %>
348
+ </div>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ </div>
353
+
354
+ <div class="col-md-4">
355
+ <div class="card">
356
+ <div class="card-header">
357
+ <h5><i class="fas fa-info-circle"></i> Upload Information</h5>
358
+ </div>
359
+ <div class="card-body">
360
+ <h6>Supported File Types:</h6>
361
+ <ul class="list-unstyled">
362
+ <li><i class="fas fa-file-pdf text-danger"></i> PDF Documents</li>
363
+ <li><i class="fas fa-file-word text-primary"></i> Word Documents (.docx)</li>
364
+ <li><i class="fas fa-file-alt text-secondary"></i> Text Files (.txt, .md)</li>
365
+ <li><i class="fas fa-file-code text-warning"></i> HTML Files</li>
366
+ <li><i class="fas fa-file-code text-info"></i> JSON/XML Files</li>
367
+ <li><i class="fas fa-file-csv text-success"></i> CSV Files</li>
368
+ </ul>
369
+
370
+ <hr>
371
+
372
+ <h6>Processing:</h6>
373
+ <ul class="list-unstyled">
374
+ <li><i class="fas fa-cogs text-primary"></i> Automatic content extraction</li>
375
+ <li><i class="fas fa-cut text-success"></i> Intelligent text chunking</li>
376
+ <li><i class="fas fa-vector-square text-info"></i> Vector embedding generation</li>
377
+ <li><i class="fas fa-search text-warning"></i> Search optimization</li>
378
+ </ul>
379
+
380
+ <hr>
381
+
382
+ <h6>Features:</h6>
383
+ <ul class="list-unstyled">
384
+ <li><i class="fas fa-tags text-primary"></i> Metadata extraction</li>
385
+ <li><i class="fas fa-language text-success"></i> Multi-language support</li>
386
+ <li><i class="fas fa-chart-line text-info"></i> Usage analytics</li>
387
+ <li><i class="fas fa-copy text-warning"></i> Duplicate detection</li>
388
+ <li><i class="fas fa-sync text-secondary"></i> Reprocessing support</li>
389
+ </ul>
390
+ </div>
391
+ </div>
392
+ </div>
393
+ </div>
394
+
395
+ <script>
396
+ document.addEventListener('DOMContentLoaded', function() {
397
+ // Check if ActionCable is available for progress tracking
398
+ let actionCableAvailable = false;
399
+ let progressTracker = null;
400
+
401
+ // Wait a bit for CDN resources to load, then check ActionCable availability
402
+ function checkActionCableAvailability() {
403
+ try {
404
+ actionCableAvailable = typeof ActionCable !== 'undefined';
405
+ console.log('🔍 ActionCable availability check:', actionCableAvailable);
406
+ console.log('🔍 window.App exists:', !!window.App);
407
+ console.log('🔍 window.App.cable exists:', !!(window.App && window.App.cable));
408
+ return actionCableAvailable;
409
+ } catch (error) {
410
+ console.log('❌ ActionCable check failed:', error);
411
+ return false;
412
+ }
413
+ }
414
+
415
+ // Initial check
416
+ actionCableAvailable = checkActionCableAvailability();
417
+
418
+ // Update status indicator with re-check
419
+ function updateStatusIndicator() {
420
+ const statusEl = document.getElementById('websocket-status');
421
+ actionCableAvailable = checkActionCableAvailability();
422
+
423
+ if (actionCableAvailable && statusEl) {
424
+ statusEl.className = 'alert alert-success mb-3';
425
+ statusEl.innerHTML = '<i class="fas fa-check-circle me-2"></i>Real-time progress tracking enabled';
426
+ } else if (statusEl) {
427
+ statusEl.className = 'alert alert-warning mb-3';
428
+ statusEl.innerHTML = '<i class="fas fa-exclamation-triangle me-2"></i>Limited functionality - progress tracking unavailable';
429
+ }
430
+ }
431
+
432
+ // Initial update
433
+ updateStatusIndicator();
434
+
435
+ // Re-check after a delay to account for CDN loading
436
+ setTimeout(function() {
437
+ updateStatusIndicator();
438
+ console.log('🔄 Re-checked ActionCable availability after delay');
439
+ }, 2000);
440
+
441
+ // Generate a temporary session ID for ActionCable subscription
442
+ function generateTemporarySessionId() {
443
+ // Use crypto.randomUUID if available, otherwise fallback to timestamp + random
444
+ if (crypto && crypto.randomUUID) {
445
+ return crypto.randomUUID();
446
+ } else {
447
+ // Fallback: timestamp + random string
448
+ const timestamp = Date.now().toString(36);
449
+ const randomStr = Math.random().toString(36).substr(2, 9);
450
+ return `temp_${timestamp}_${randomStr}`;
451
+ }
452
+ }
453
+
454
+ // Progress upload form handler
455
+ const progressForm = document.getElementById('progress-upload-form');
456
+ if (progressForm) {
457
+ console.log('📝 Progress form handler attached');
458
+ progressForm.addEventListener('submit', async function(e) {
459
+ console.log('🚀 Progress form submitted');
460
+ e.preventDefault();
461
+
462
+ const fileInput = document.getElementById('progress_files');
463
+ const submitBtn = document.getElementById('progress-upload-btn');
464
+ const progressContainer = document.getElementById('progress-container');
465
+
466
+ // Validate files and log detailed information
467
+ if (!fileInput.files.length) {
468
+ alert('Please select at least one file.');
469
+ return;
470
+ }
471
+
472
+ // Log detailed file selection information for debugging
473
+ console.log('📁 File selection analysis:');
474
+ console.log(` Files selected by user: ${fileInput.files.length}`);
475
+ console.log(' File details:');
476
+ Array.from(fileInput.files).forEach((file, index) => {
477
+ console.log(` ${index + 1}. ${file.name} (${file.size} bytes, ${file.type || 'unknown type'})`);
478
+ });
479
+
480
+ // Set upload in progress flag
481
+ uploadInProgress = true;
482
+
483
+ // Disable submit button
484
+ const originalBtnHtml = submitBtn.innerHTML;
485
+ submitBtn.disabled = true;
486
+ submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Uploading...';
487
+
488
+ // Show progress container
489
+ if (progressContainer) {
490
+ progressContainer.style.display = 'block';
491
+ setupProgressDisplay(Array.from(fileInput.files));
492
+ }
493
+
494
+ // Setup ActionCable connection BEFORE starting upload to ensure it's ready
495
+ let temporarySessionId = null;
496
+ if (checkActionCableAvailability()) {
497
+ // Generate a temporary session ID or use existing one
498
+ temporarySessionId = generateTemporarySessionId();
499
+ console.log('🎯 Setting up progress tracking BEFORE upload with temp session ID:', temporarySessionId);
500
+
501
+ // Setup the subscription synchronously - we can't wait for connection in this context
502
+ setupProgressTracking(temporarySessionId);
503
+ updateProgressStatus('Connecting to real-time updates. Starting upload...');
504
+
505
+ // Longer delay to ensure ActionCable subscription is fully established
506
+ console.log('⏰ Waiting for ActionCable subscription to establish...');
507
+ await new Promise(resolve => setTimeout(resolve, 1500));
508
+ console.log('✅ ActionCable delay complete, starting upload');
509
+ } else {
510
+ console.warn('⚠️ ActionCable not available, will use fallback mode');
511
+ updateProgressStatus('Starting upload (real-time updates unavailable)...');
512
+ }
513
+
514
+ try {
515
+ const formData = new FormData(progressForm);
516
+
517
+ // Add the temporary session ID to the form data so backend uses it
518
+ if (temporarySessionId) {
519
+ formData.append('temp_session_id', temporarySessionId);
520
+ }
521
+
522
+ // Add force duplicate setting if checked
523
+ const forceDuplicateCheckbox = document.getElementById('progress_force_duplicate');
524
+ if (forceDuplicateCheckbox && forceDuplicateCheckbox.checked) {
525
+ formData.append('ragdoll_document[force_duplicate]', '1');
526
+ }
527
+
528
+ const csrfToken = document.querySelector('meta[name="csrf-token"]');
529
+
530
+ console.log('🚀 Sending upload request...');
531
+ const response = await fetch(progressForm.action, {
532
+ method: 'POST',
533
+ body: formData,
534
+ headers: {
535
+ 'X-CSRF-Token': csrfToken ? csrfToken.content : ''
536
+ }
537
+ });
538
+
539
+ console.log('📨 Upload response received:', {
540
+ status: response.status,
541
+ statusText: response.statusText,
542
+ ok: response.ok,
543
+ headers: Object.fromEntries(response.headers.entries())
544
+ });
545
+
546
+ if (!response.ok) {
547
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
548
+ }
549
+
550
+ const result = await response.json();
551
+ console.log('📊 Upload result parsed:', result);
552
+ console.log('🎯 Processing upload success...');
553
+
554
+ if (result.success) {
555
+ console.log('✅ Upload successful, processing has started');
556
+ console.log('📡 Server session ID:', result.session_id);
557
+ console.log('📡 Temp session ID used:', temporarySessionId);
558
+ console.log('📊 Processing results:', result.results);
559
+
560
+ // Check if any files were processed synchronously
561
+ const syncResults = result.results?.filter(r => r.status === 'completed_sync') || [];
562
+ const queuedResults = result.results?.filter(r => r.status === 'queued') || [];
563
+ const failedResults = result.results?.filter(r => r.status === 'failed') || [];
564
+
565
+ if (syncResults.length > 0) {
566
+ console.log('📋 Files processed synchronously:', syncResults);
567
+ // Update progress display for sync results
568
+ syncResults.forEach((syncResult, index) => {
569
+ const fileIndex = fileMapping.get(syncResult.file);
570
+ if (fileIndex !== undefined) {
571
+ updateFileStatus(fileIndex, 'completed', 'Processed successfully ✓');
572
+ }
573
+ });
574
+
575
+ // Calculate overall progress
576
+ calculateOverallProgress();
577
+
578
+ if (queuedResults.length === 0) {
579
+ // All files processed synchronously
580
+ updateProgressStatus('🎉 All files processed successfully! Redirecting...');
581
+ setTimeout(() => {
582
+ window.location.href = '<%= ragdoll.documents_path %>';
583
+ }, 2000);
584
+ } else {
585
+ updateProgressStatus(`${syncResults.length} files processed immediately, ${queuedResults.length} queued for background processing...`);
586
+ }
587
+ }
588
+
589
+ if (queuedResults.length > 0 && actionCableAvailable) {
590
+ // If we have a different session ID from server, update our subscription
591
+ if (result.session_id && result.session_id !== temporarySessionId) {
592
+ console.log('🔄 Server provided different session ID, updating subscription...');
593
+ // Unsubscribe from old channel
594
+ if (progressTracker) {
595
+ progressTracker.unsubscribe();
596
+ }
597
+ // Subscribe to correct channel
598
+ setupProgressTracking(result.session_id);
599
+ }
600
+
601
+ updateProgressStatus('Upload successful! Background processing started with real-time tracking...');
602
+ } else if (queuedResults.length > 0) {
603
+ updateProgressStatus(`${queuedResults.length} files queued for background processing (real-time updates unavailable)...`);
604
+ // Redirect after a delay since we can't track progress
605
+ setTimeout(() => {
606
+ window.location.href = '<%= ragdoll.documents_path %>';
607
+ }, 3000);
608
+ }
609
+
610
+ if (failedResults.length > 0) {
611
+ console.error('❌ Some files failed:', failedResults);
612
+ failedResults.forEach((failedResult, index) => {
613
+ const fileIndex = fileMapping.get(failedResult.file);
614
+ if (fileIndex !== undefined) {
615
+ updateFileStatus(fileIndex, 'failed', `Error: ${failedResult.error}`);
616
+ }
617
+ });
618
+ }
619
+
620
+ // Clear upload flag since upload phase is complete
621
+ uploadInProgress = false;
622
+
623
+ // Clear form
624
+ fileInput.value = '';
625
+
626
+ } else {
627
+ throw new Error(result.error || 'Upload failed');
628
+ }
629
+
630
+ } catch (error) {
631
+ console.error('💥 Upload error:', error);
632
+ console.error('💥 Error details:', {
633
+ message: error.message,
634
+ stack: error.stack,
635
+ name: error.name
636
+ });
637
+ updateProgressStatus('Upload failed: ' + error.message, 'danger');
638
+ alert('Upload failed: ' + error.message);
639
+
640
+ // Clear upload flag on error
641
+ uploadInProgress = false;
642
+
643
+ // Re-enable submit button only on error
644
+ submitBtn.disabled = false;
645
+ submitBtn.innerHTML = originalBtnHtml;
646
+ }
647
+ });
648
+ }
649
+
650
+ // Store file mapping for progress tracking
651
+ let fileMapping = new Map();
652
+ let fileTimeouts = new Map(); // Track timeouts for each file
653
+ const FILE_TIMEOUT = 10 * 60 * 1000; // 10 minutes timeout
654
+
655
+ // Setup progress display for files
656
+ function setupProgressDisplay(files) {
657
+ const fileStatusList = document.getElementById('file-status-list');
658
+ const overallProgressBar = document.getElementById('overall-progress-bar');
659
+ const overallPercentage = document.getElementById('overall-percentage');
660
+
661
+ if (!fileStatusList) return;
662
+
663
+ // Create file status items and store mapping
664
+ fileStatusList.innerHTML = '';
665
+ fileMapping.clear();
666
+
667
+ files.forEach((file, index) => {
668
+ const fileItem = document.createElement('div');
669
+ fileItem.className = 'mb-2 p-2 border rounded bg-light';
670
+ fileItem.id = `file-${index}`;
671
+ fileItem.setAttribute('data-filename', file.name);
672
+ fileItem.innerHTML = `
673
+ <div class="d-flex align-items-center justify-content-between">
674
+ <span><i class="fas fa-file me-2"></i>${file.name}</span>
675
+ <div class="d-flex align-items-center">
676
+ <small class="text-muted me-2" id="file-status-${index}">Queued</small>
677
+ <i class="fas fa-clock text-muted" id="file-icon-${index}"></i>
678
+ </div>
679
+ </div>
680
+ `;
681
+ fileStatusList.appendChild(fileItem);
682
+
683
+ // Store mapping for easy lookup
684
+ fileMapping.set(file.name, index);
685
+ console.log(`📝 Mapped file "${file.name}" to index ${index}`);
686
+
687
+ // Set up timeout for this file
688
+ const timeoutId = setTimeout(() => {
689
+ handleFileTimeout(file.name, index);
690
+ }, FILE_TIMEOUT);
691
+ fileTimeouts.set(file.name, timeoutId);
692
+ });
693
+
694
+ console.log('🗺️ Final file mapping:', Array.from(fileMapping.entries()));
695
+
696
+ // Initialize progress
697
+ updateOverallProgress(0);
698
+ updateProgressStatus('Files uploaded, starting background processing...');
699
+ }
700
+
701
+ // Setup ActionCable progress tracking (sync version for immediate use)
702
+ function setupProgressTracking(sessionId) {
703
+ console.log('🚀 Setting up progress tracking for session (sync):', sessionId);
704
+ console.log('📊 Current ActionCable state:', {
705
+ ActionCable: typeof ActionCable,
706
+ windowApp: !!window.App,
707
+ appCable: !!(window.App && window.App.cable)
708
+ });
709
+
710
+ // Final check for ActionCable availability
711
+ if (!checkActionCableAvailability()) {
712
+ console.warn('❌ ActionCable not available, skipping progress tracking');
713
+ updateProgressStatus('Real-time updates not available, using basic mode', 'warning');
714
+ return;
715
+ }
716
+
717
+ try {
718
+ // Ensure App.cable is initialized
719
+ if (!window.App) {
720
+ window.App = {};
721
+ }
722
+
723
+ if (!window.App.cable && typeof ActionCable !== 'undefined') {
724
+ console.log('🔧 Initializing App.cable...');
725
+ window.App.cable = ActionCable.createConsumer('/cable');
726
+ }
727
+
728
+ const consumer = window.App.cable;
729
+ console.log('📡 Using consumer:', consumer);
730
+
731
+ if (!consumer) {
732
+ throw new Error('No ActionCable consumer available');
733
+ }
734
+
735
+ progressTracker = consumer.subscriptions.create(
736
+ {
737
+ channel: "Ragdoll::FileProcessingChannel",
738
+ session_id: sessionId
739
+ },
740
+ {
741
+ received: function(data) {
742
+ console.log('📡 ActionCable data received:', data);
743
+
744
+ if (data.type === 'ping') {
745
+ console.log('🏓 Ping response received:', data.message);
746
+ return;
747
+ }
748
+
749
+ handleProgressUpdate(data);
750
+ },
751
+ connected: function() {
752
+ console.log('✅ Connected to Ragdoll::FileProcessingChannel with session:', sessionId);
753
+ updateProgressStatus('Connected to real-time updates. Processing will begin shortly...');
754
+
755
+ // Test if the connection is really working by sending a ping
756
+ console.log('🏓 Testing ActionCable connection...');
757
+ this.perform('test_connection');
758
+ },
759
+ disconnected: function() {
760
+ console.log('❌ Disconnected from Ragdoll::FileProcessingChannel');
761
+ updateProgressStatus('Lost connection to real-time updates', 'warning');
762
+ },
763
+ rejected: function() {
764
+ console.log('❌ Connection rejected to Ragdoll::FileProcessingChannel');
765
+ updateProgressStatus('Connection rejected - channel may not exist', 'danger');
766
+ }
767
+ }
768
+ );
769
+ console.log('📡 Subscription created:', progressTracker);
770
+ } catch (error) {
771
+ console.error('💥 Failed to setup progress tracking:', error);
772
+ updateProgressStatus('Failed to setup real-time updates: ' + error.message, 'danger');
773
+ }
774
+ }
775
+
776
+ // Handle progress updates from ActionCable
777
+ function handleProgressUpdate(data) {
778
+ console.log('📡 Progress update received:', data);
779
+ console.log('📊 Data breakdown:', {
780
+ file_id: data.file_id,
781
+ filename: data.filename,
782
+ status: data.status,
783
+ progress: data.progress,
784
+ message: data.message
785
+ });
786
+
787
+ const { file_id, filename, status, progress, message } = data;
788
+
789
+ // Find corresponding file by name using our mapping
790
+ console.log('🔍 Looking for file in mapping:', filename);
791
+ console.log('🗺️ Current file mapping:', Array.from(fileMapping.entries()));
792
+
793
+ const fileIndex = fileMapping.get(filename);
794
+ if (fileIndex === undefined) {
795
+ console.warn('❌ File not found in mapping:', filename);
796
+ console.log('📋 Available files in mapping:', Array.from(fileMapping.keys()));
797
+ return;
798
+ }
799
+
800
+ console.log('✅ Found file at index:', fileIndex);
801
+
802
+ const fileItem = document.getElementById(`file-${fileIndex}`);
803
+ const statusSpan = document.getElementById(`file-status-${fileIndex}`);
804
+ const iconSpan = document.getElementById(`file-icon-${fileIndex}`);
805
+
806
+ if (!fileItem || !statusSpan) {
807
+ console.warn('File UI elements not found for index:', fileIndex);
808
+ return;
809
+ }
810
+
811
+ // Update status and visual indicators
812
+ if (status === 'started') {
813
+ statusSpan.textContent = 'Processing...';
814
+ iconSpan.className = 'fas fa-spinner fa-spin text-primary';
815
+ fileItem.className = 'mb-2 p-2 border rounded bg-primary text-white';
816
+ updateProgressStatus(`Processing ${filename}...`);
817
+ } else if (status === 'processing') {
818
+ statusSpan.textContent = `${progress}% - ${message}`;
819
+ iconSpan.className = 'fas fa-cog fa-spin text-primary';
820
+ } else if (status === 'completed') {
821
+ statusSpan.textContent = 'Complete ✓';
822
+ iconSpan.className = 'fas fa-check-circle text-success';
823
+ fileItem.className = 'mb-2 p-2 border rounded bg-success text-white';
824
+ clearFileTimeout(filename);
825
+ calculateOverallProgress();
826
+ } else if (status === 'error') {
827
+ statusSpan.textContent = 'Error: ' + message;
828
+ iconSpan.className = 'fas fa-exclamation-circle text-danger';
829
+ fileItem.className = 'mb-2 p-2 border rounded bg-danger text-white';
830
+ clearFileTimeout(filename);
831
+ calculateOverallProgress();
832
+ }
833
+ }
834
+
835
+ // Calculate overall progress based on completed files
836
+ function calculateOverallProgress() {
837
+ const fileStatusList = document.getElementById('file-status-list');
838
+ if (!fileStatusList) return;
839
+
840
+ const totalFiles = fileStatusList.children.length;
841
+ let completedFiles = 0;
842
+ let errorFiles = 0;
843
+
844
+ for (const fileItem of fileStatusList.children) {
845
+ if (fileItem.className.includes('bg-success')) {
846
+ completedFiles++;
847
+ } else if (fileItem.className.includes('bg-danger')) {
848
+ completedFiles++;
849
+ errorFiles++;
850
+ }
851
+ }
852
+
853
+ const percentage = Math.round((completedFiles / totalFiles) * 100);
854
+ updateOverallProgress(percentage);
855
+
856
+ if (completedFiles === totalFiles) {
857
+ const successFiles = completedFiles - errorFiles;
858
+ if (errorFiles > 0) {
859
+ updateProgressStatus(`Processing complete! ${successFiles} successful, ${errorFiles} failed. Check the Documents page for details.`);
860
+ } else {
861
+ updateProgressStatus('🎉 All files processed successfully! Redirecting...');
862
+ }
863
+
864
+ // Redirect after a short delay
865
+ setTimeout(() => {
866
+ window.location.href = '<%= ragdoll.documents_path %>';
867
+ }, 3000);
868
+ } else {
869
+ const processingFiles = totalFiles - completedFiles;
870
+ updateProgressStatus(`Processing ${completedFiles}/${totalFiles} files (${processingFiles} remaining)...`);
871
+ }
872
+ }
873
+
874
+ // Update overall progress bar
875
+ function updateOverallProgress(percentage) {
876
+ const progressBar = document.getElementById('overall-progress-bar');
877
+ const percentageSpan = document.getElementById('overall-percentage');
878
+
879
+ if (progressBar) {
880
+ progressBar.style.width = percentage + '%';
881
+ progressBar.setAttribute('aria-valuenow', percentage);
882
+ progressBar.textContent = percentage + '%';
883
+
884
+ if (percentage === 100) {
885
+ progressBar.className = 'progress-bar bg-success';
886
+ }
887
+ }
888
+
889
+ if (percentageSpan) {
890
+ percentageSpan.textContent = percentage + '%';
891
+ }
892
+ }
893
+
894
+ // Update progress status message
895
+ function updateProgressStatus(message, type = 'info') {
896
+ const statusEl = document.getElementById('progress-status');
897
+ if (statusEl) {
898
+ statusEl.textContent = message;
899
+ statusEl.className = `text-${type === 'danger' ? 'danger' : 'muted'} mt-2 d-block`;
900
+ }
901
+ }
902
+
903
+ // Handle file timeout
904
+ function handleFileTimeout(filename, fileIndex) {
905
+ console.warn(`⏰ File timeout detected for: ${filename}`);
906
+
907
+ const fileItem = document.getElementById(`file-${fileIndex}`);
908
+ const statusSpan = document.getElementById(`file-status-${fileIndex}`);
909
+ const iconSpan = document.getElementById(`file-icon-${fileIndex}`);
910
+
911
+ if (fileItem && statusSpan) {
912
+ // Check if file is still in progress (not completed or errored)
913
+ if (!fileItem.className.includes('bg-success') && !fileItem.className.includes('bg-danger')) {
914
+ statusSpan.textContent = 'Timeout - job may be stuck';
915
+ iconSpan.className = 'fas fa-clock text-warning';
916
+ fileItem.className = 'mb-2 p-2 border rounded bg-warning text-dark';
917
+
918
+ updateProgressStatus(`File processing timeout detected for ${filename}. This may indicate a stuck job.`);
919
+ calculateOverallProgress();
920
+ }
921
+ }
922
+
923
+ // Clean up the timeout
924
+ fileTimeouts.delete(filename);
925
+ }
926
+
927
+ // Clear file timeout
928
+ function clearFileTimeout(filename) {
929
+ const timeoutId = fileTimeouts.get(filename);
930
+ if (timeoutId) {
931
+ clearTimeout(timeoutId);
932
+ fileTimeouts.delete(filename);
933
+ console.log(`🧹 Cleared timeout for: ${filename}`);
934
+ }
935
+ }
936
+
937
+ // Update individual file status
938
+ function updateFileStatus(fileIndex, status, message) {
939
+ const fileItem = document.getElementById(`file-${fileIndex}`);
940
+ const statusSpan = document.getElementById(`file-status-${fileIndex}`);
941
+ const iconSpan = document.getElementById(`file-icon-${fileIndex}`);
942
+
943
+ if (!fileItem || !statusSpan) {
944
+ console.warn('File UI elements not found for index:', fileIndex);
945
+ return;
946
+ }
947
+
948
+ // Update status and visual indicators
949
+ if (status === 'completed') {
950
+ statusSpan.textContent = message || 'Complete ✓';
951
+ iconSpan.className = 'fas fa-check-circle text-success';
952
+ fileItem.className = 'mb-2 p-2 border rounded bg-success text-white';
953
+ } else if (status === 'failed') {
954
+ statusSpan.textContent = message || 'Failed ✗';
955
+ iconSpan.className = 'fas fa-exclamation-circle text-danger';
956
+ fileItem.className = 'mb-2 p-2 border rounded bg-danger text-white';
957
+ } else if (status === 'processing') {
958
+ statusSpan.textContent = message || 'Processing...';
959
+ iconSpan.className = 'fas fa-cog fa-spin text-primary';
960
+ fileItem.className = 'mb-2 p-2 border rounded bg-primary text-white';
961
+ }
962
+ }
963
+
964
+ // Track upload state to show navigation warnings
965
+ let uploadInProgress = false;
966
+
967
+ // Warn user before leaving during upload
968
+ function handleBeforeUnload(e) {
969
+ if (uploadInProgress) {
970
+ const confirmationMessage = 'Upload in progress! Leaving this page will cancel the upload. Are you sure you want to leave?';
971
+ e.preventDefault();
972
+ e.returnValue = confirmationMessage;
973
+ return confirmationMessage;
974
+ }
975
+ }
976
+
977
+ // Add beforeunload listener
978
+ window.addEventListener('beforeunload', handleBeforeUnload);
979
+
980
+ // Directory upload form handler
981
+ const directoryForm = document.getElementById('directory-upload-form');
982
+ const directoryInput = document.getElementById('directory_files');
983
+
984
+ if (directoryInput) {
985
+ // Set webkitdirectory attribute (Rails data helper doesn't work for this)
986
+ directoryInput.setAttribute('webkitdirectory', '');
987
+
988
+ // Show directory selection info
989
+ directoryInput.addEventListener('change', function() {
990
+ const files = this.files;
991
+ if (files.length > 0) {
992
+ // Get directory name from first file path
993
+ const firstFile = files[0];
994
+ const pathParts = firstFile.webkitRelativePath.split('/');
995
+ const directoryName = pathParts[0];
996
+
997
+ // Update form text with selection info
998
+ const formText = directoryInput.nextElementSibling;
999
+ if (formText && formText.classList.contains('form-text')) {
1000
+ formText.innerHTML = `Selected directory: <strong>${directoryName}</strong> (${files.length} files)`;
1001
+ }
1002
+ }
1003
+ });
1004
+ }
1005
+
1006
+ // Function to attach directory form listener
1007
+ function attachDirectoryFormListener() {
1008
+ const directoryForm = document.getElementById('directory-upload-form');
1009
+ if (directoryForm && !directoryForm.ragdollListenerAttached) {
1010
+ console.log('📋 Attaching directory form event listener...');
1011
+ directoryForm.ragdollListenerAttached = true; // Prevent duplicate listeners
1012
+
1013
+ directoryForm.addEventListener('submit', function(e) {
1014
+ e.preventDefault(); // Prevent default form submission
1015
+
1016
+ const submitBtn = document.getElementById('directory-upload-btn');
1017
+ const processingAlert = document.getElementById('directory-processing-alert');
1018
+ const progressContainer = document.getElementById('directory-progress-container');
1019
+ const fileInput = document.getElementById('directory_files');
1020
+
1021
+ // Validate files
1022
+ if (!fileInput.files.length) {
1023
+ alert('Please select a directory.');
1024
+ return;
1025
+ }
1026
+
1027
+ // Set upload in progress flag
1028
+ uploadInProgress = true;
1029
+
1030
+ if (submitBtn) {
1031
+ const spinner = submitBtn.querySelector('.spinner-border');
1032
+ const btnText = submitBtn.querySelector('.btn-text');
1033
+
1034
+ submitBtn.disabled = true;
1035
+ if (spinner) spinner.classList.remove('d-none');
1036
+ if (btnText) btnText.textContent = 'Importing...';
1037
+ }
1038
+
1039
+ if (processingAlert) {
1040
+ processingAlert.classList.remove('d-none');
1041
+ }
1042
+
1043
+ // Show progress container
1044
+ if (progressContainer) {
1045
+ progressContainer.style.display = 'block';
1046
+ setupDirectoryProgress(fileInput.files);
1047
+ }
1048
+
1049
+ // Initialize bulk upload status popup if available
1050
+ if (window.bulkUploadStatus) {
1051
+ const sessionId = generateTemporarySessionId();
1052
+ window.bulkUploadStatus.startUpload(sessionId, fileInput.files.length);
1053
+
1054
+ // Store session ID for later use
1055
+ window.currentUploadSessionId = sessionId;
1056
+ }
1057
+
1058
+ // Submit form using fetch for better progress control
1059
+ submitDirectoryUpload();
1060
+ });
1061
+ } else if (directoryForm) {
1062
+ console.log('📋 Directory form listener already attached');
1063
+ } else {
1064
+ console.log('📋 Directory form not found');
1065
+ }
1066
+ }
1067
+
1068
+ // Try to attach the listener immediately
1069
+ attachDirectoryFormListener();
1070
+
1071
+ // Also try when the directory tab becomes active
1072
+ const directoryTab = document.getElementById('directory-tab');
1073
+ if (directoryTab) {
1074
+ directoryTab.addEventListener('shown.bs.tab', function() {
1075
+ console.log('📋 Directory tab activated, attaching listener...');
1076
+ attachDirectoryFormListener();
1077
+ });
1078
+ }
1079
+
1080
+ // Setup directory progress display
1081
+ function setupDirectoryProgress(files) {
1082
+ const totalFiles = files.length;
1083
+
1084
+ // Initialize progress display
1085
+ document.getElementById('directory-total-files').textContent = totalFiles;
1086
+ document.getElementById('directory-files-uploaded').textContent = '0';
1087
+ document.getElementById('directory-files-processed').textContent = '0';
1088
+ updateDirectoryUploadProgress(0);
1089
+ updateDirectoryProcessingProgress(0);
1090
+ updateDirectoryProgressStatus('Starting upload...');
1091
+ }
1092
+
1093
+ // Submit directory upload with progress tracking
1094
+ async function submitDirectoryUpload() {
1095
+ const form = document.getElementById('directory-upload-form');
1096
+ const formData = new FormData(form);
1097
+
1098
+ // Use stored session ID or generate new one
1099
+ const sessionId = window.currentUploadSessionId || generateTemporarySessionId();
1100
+ formData.append('temp_session_id', sessionId);
1101
+
1102
+ // Add force duplicate setting if checked
1103
+ const directoryForceDuplicateCheckbox = document.getElementById('directory_force_duplicate');
1104
+ if (directoryForceDuplicateCheckbox && directoryForceDuplicateCheckbox.checked) {
1105
+ formData.append('force_duplicate', '1');
1106
+ }
1107
+
1108
+ // Setup ActionCable for directory progress if available
1109
+ if (checkActionCableAvailability()) {
1110
+ setupDirectoryProgressTracking(sessionId);
1111
+ }
1112
+
1113
+ try {
1114
+ updateDirectoryProgressStatus('Uploading files to server...');
1115
+
1116
+ const csrfToken = document.querySelector('meta[name="csrf-token"]');
1117
+ const response = await fetch(form.action, {
1118
+ method: 'POST',
1119
+ body: formData,
1120
+ headers: {
1121
+ 'X-CSRF-Token': csrfToken ? csrfToken.content : '',
1122
+ 'Accept': 'application/json',
1123
+ 'X-Requested-With': 'XMLHttpRequest'
1124
+ }
1125
+ });
1126
+
1127
+ if (!response.ok) {
1128
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1129
+ }
1130
+
1131
+ // Simulate upload completion (since we don't have real upload progress from server)
1132
+ updateDirectoryUploadProgress(100);
1133
+ updateDirectoryProgressStatus('Upload complete! Files queued for processing...');
1134
+
1135
+ const result = await response.json(); // Parse JSON response
1136
+
1137
+ // Clear upload flag since upload phase is complete
1138
+ uploadInProgress = false;
1139
+
1140
+ if (result.success) {
1141
+ console.log('✅ Directory upload successful:', result);
1142
+ console.log('📡 Session ID for tracking:', result.session_id);
1143
+ console.log('📁 Files queued:', result.file_count);
1144
+
1145
+ // Update the bulk upload status popup to track this session
1146
+ if (window.bulkUploadStatus && result.session_id) {
1147
+ // The popup is already started from form submission, just update status
1148
+ updateDirectoryProgressStatus(`${result.file_count} files queued for background processing. Tracking progress...`);
1149
+ }
1150
+
1151
+ // DON'T redirect immediately - let the popup track the background job
1152
+ // The popup will handle the redirect when processing is complete
1153
+
1154
+ // Hide the in-page progress indicators since the popup is handling it
1155
+ const progressContainer = document.getElementById('directory-progress-container');
1156
+ if (progressContainer) {
1157
+ setTimeout(() => {
1158
+ progressContainer.style.display = 'none';
1159
+ }, 3000);
1160
+ }
1161
+
1162
+ // Re-enable the form for another upload if needed
1163
+ const submitBtn = document.getElementById('directory-upload-btn');
1164
+ if (submitBtn) {
1165
+ const spinner = submitBtn.querySelector('.spinner-border');
1166
+ const btnText = submitBtn.querySelector('.btn-text');
1167
+
1168
+ submitBtn.disabled = false;
1169
+ if (spinner) spinner.classList.add('d-none');
1170
+ if (btnText) btnText.textContent = 'Import Directory';
1171
+ }
1172
+
1173
+ // Hide processing alert
1174
+ const processingAlert = document.getElementById('directory-processing-alert');
1175
+ if (processingAlert) {
1176
+ processingAlert.classList.add('d-none');
1177
+ }
1178
+ } else {
1179
+ // Handle error response
1180
+ updateDirectoryProgressStatus('Upload failed: ' + (result.error || 'Unknown error'));
1181
+
1182
+ // Re-enable form on error
1183
+ const submitBtn = document.getElementById('directory-upload-btn');
1184
+ if (submitBtn) {
1185
+ const spinner = submitBtn.querySelector('.spinner-border');
1186
+ const btnText = submitBtn.querySelector('.btn-text');
1187
+
1188
+ submitBtn.disabled = false;
1189
+ if (spinner) spinner.classList.add('d-none');
1190
+ if (btnText) btnText.textContent = 'Import Directory';
1191
+ }
1192
+ }
1193
+
1194
+ } catch (error) {
1195
+ console.error('Directory upload error:', error);
1196
+ updateDirectoryProgressStatus('Upload failed: ' + error.message);
1197
+
1198
+ // Clear upload flag on error
1199
+ uploadInProgress = false;
1200
+
1201
+ // Re-enable form
1202
+ const submitBtn = document.getElementById('directory-upload-btn');
1203
+ if (submitBtn) {
1204
+ const spinner = submitBtn.querySelector('.spinner-border');
1205
+ const btnText = submitBtn.querySelector('.btn-text');
1206
+
1207
+ submitBtn.disabled = false;
1208
+ if (spinner) spinner.classList.add('d-none');
1209
+ if (btnText) btnText.textContent = 'Import Directory';
1210
+ }
1211
+ }
1212
+ }
1213
+
1214
+ // Setup ActionCable progress tracking for directory uploads
1215
+ function setupDirectoryProgressTracking(sessionId) {
1216
+ console.log('Setting up directory progress tracking for session:', sessionId);
1217
+
1218
+ if (!checkActionCableAvailability()) {
1219
+ console.warn('ActionCable not available for directory progress tracking');
1220
+ return;
1221
+ }
1222
+
1223
+ try {
1224
+ if (!window.App) window.App = {};
1225
+ if (!window.App.cable && typeof ActionCable !== 'undefined') {
1226
+ window.App.cable = ActionCable.createConsumer('/cable');
1227
+ }
1228
+
1229
+ const consumer = window.App.cable;
1230
+ if (!consumer) {
1231
+ console.warn('No ActionCable consumer available for directory tracking');
1232
+ return;
1233
+ }
1234
+
1235
+ const directoryProgressTracker = consumer.subscriptions.create(
1236
+ {
1237
+ channel: "Ragdoll::FileProcessingChannel",
1238
+ session_id: sessionId
1239
+ },
1240
+ {
1241
+ received: function(data) {
1242
+ console.log('Directory progress update received:', data);
1243
+ handleDirectoryProgressUpdate(data);
1244
+ },
1245
+ connected: function() {
1246
+ console.log('Connected to directory progress tracking');
1247
+ updateDirectoryProgressStatus('Connected to real-time updates...');
1248
+ },
1249
+ disconnected: function() {
1250
+ console.log('Disconnected from directory progress tracking');
1251
+ }
1252
+ }
1253
+ );
1254
+
1255
+ } catch (error) {
1256
+ console.error('Failed to setup directory progress tracking:', error);
1257
+ }
1258
+ }
1259
+
1260
+ // Handle directory progress updates with enhanced tracking
1261
+ function handleDirectoryProgressUpdate(data) {
1262
+ console.log('📊 Directory progress update:', data);
1263
+
1264
+ const progressBar = document.getElementById('directory-processing-progress');
1265
+ const progressText = document.getElementById('directory-progress-percentage');
1266
+ const processedCount = document.getElementById('directory-files-processed');
1267
+ const totalCount = document.getElementById('directory-total-files');
1268
+ const currentFile = document.getElementById('directory-current-file');
1269
+ const progressLog = document.getElementById('directory-progress-log');
1270
+ const progressDetails = document.getElementById('directory-progress-details');
1271
+
1272
+ // Show progress details section
1273
+ if (progressDetails) {
1274
+ progressDetails.style.display = 'block';
1275
+ }
1276
+
1277
+ if (data.type === 'file_progress') {
1278
+ // Individual file being processed
1279
+ const percentage = data.percentage || 0;
1280
+
1281
+ if (progressBar) {
1282
+ progressBar.style.width = percentage + '%';
1283
+ progressBar.setAttribute('aria-valuenow', percentage);
1284
+ progressBar.textContent = percentage.toFixed(1) + '%';
1285
+ }
1286
+
1287
+ if (progressText) {
1288
+ progressText.textContent = percentage.toFixed(1) + '%';
1289
+ }
1290
+
1291
+ if (processedCount) {
1292
+ processedCount.textContent = data.processed || 0;
1293
+ }
1294
+
1295
+ if (totalCount) {
1296
+ totalCount.textContent = data.total || 0;
1297
+ }
1298
+
1299
+ if (currentFile) {
1300
+ currentFile.innerHTML = `<i class="fas fa-cogs text-primary me-1"></i><span>Processing: ${data.filename}</span>`;
1301
+ }
1302
+
1303
+ // Add to progress log
1304
+ if (progressLog) {
1305
+ const logEntry = document.createElement('div');
1306
+ logEntry.className = 'mb-1 text-muted';
1307
+ logEntry.innerHTML = `<i class="fas fa-spinner fa-spin text-primary me-1"></i>Processing: ${data.filename}`;
1308
+ progressLog.appendChild(logEntry);
1309
+ progressLog.scrollTop = progressLog.scrollHeight;
1310
+ }
1311
+
1312
+ updateDirectoryProgressStatus(`Processing ${data.filename}... (${data.processed}/${data.total} files)`);
1313
+
1314
+ } else if (data.type === 'file_complete') {
1315
+ // File completed successfully
1316
+ const percentage = data.percentage || 0;
1317
+
1318
+ if (progressBar) {
1319
+ progressBar.style.width = percentage + '%';
1320
+ progressBar.setAttribute('aria-valuenow', percentage);
1321
+ progressBar.textContent = percentage.toFixed(1) + '%';
1322
+ }
1323
+
1324
+ if (processedCount) {
1325
+ processedCount.textContent = data.processed || 0;
1326
+ }
1327
+
1328
+ // Update log with success
1329
+ if (progressLog) {
1330
+ const logEntry = document.createElement('div');
1331
+ logEntry.className = 'mb-1 text-success';
1332
+ logEntry.innerHTML = `<i class="fas fa-check-circle text-success me-1"></i>Completed: ${data.filename}`;
1333
+ progressLog.appendChild(logEntry);
1334
+ progressLog.scrollTop = progressLog.scrollHeight;
1335
+ }
1336
+
1337
+ updateDirectoryProgressStatus(`Completed ${data.filename} (${data.processed}/${data.total} files)`);
1338
+
1339
+ } else if (data.type === 'file_error') {
1340
+ // File failed
1341
+ const percentage = data.percentage || 0;
1342
+
1343
+ if (progressBar) {
1344
+ progressBar.style.width = percentage + '%';
1345
+ progressBar.setAttribute('aria-valuenow', percentage);
1346
+ progressBar.textContent = percentage.toFixed(1) + '%';
1347
+ }
1348
+
1349
+ // Update log with error
1350
+ if (progressLog) {
1351
+ const logEntry = document.createElement('div');
1352
+ logEntry.className = 'mb-1 text-danger';
1353
+ logEntry.innerHTML = `<i class="fas fa-exclamation-circle text-danger me-1"></i>Failed: ${data.filename} - ${data.error || 'Unknown error'}`;
1354
+ progressLog.appendChild(logEntry);
1355
+ progressLog.scrollTop = progressLog.scrollHeight;
1356
+ }
1357
+
1358
+ updateDirectoryProgressStatus(`Error processing ${data.filename}: ${data.error || 'Unknown error'}`);
1359
+
1360
+ } else if (data.type === 'bulk_complete') {
1361
+ // All files completed
1362
+ if (progressBar) {
1363
+ progressBar.style.width = '100%';
1364
+ progressBar.setAttribute('aria-valuenow', 100);
1365
+ progressBar.textContent = '100%';
1366
+ progressBar.className = 'progress-bar bg-success';
1367
+ }
1368
+
1369
+ if (progressText) {
1370
+ progressText.textContent = '100%';
1371
+ }
1372
+
1373
+ if (currentFile) {
1374
+ currentFile.innerHTML = `<i class="fas fa-check-circle text-success me-1"></i><span>All files processed!</span>`;
1375
+ }
1376
+
1377
+ // Add completion summary to log
1378
+ if (progressLog) {
1379
+ const logEntry = document.createElement('div');
1380
+ logEntry.className = 'mb-1 text-success fw-bold';
1381
+ logEntry.innerHTML = `<i class="fas fa-check-circle text-success me-1"></i>Bulk processing completed! ${data.processed}/${data.total} successful${data.failed > 0 ? `, ${data.failed} failed` : ''}`;
1382
+ progressLog.appendChild(logEntry);
1383
+ progressLog.scrollTop = progressLog.scrollHeight;
1384
+ }
1385
+
1386
+ const message = `Bulk processing completed! ${data.processed}/${data.total} files processed successfully`;
1387
+ const failedMessage = data.failed > 0 ? ` (${data.failed} failed: ${data.failed_files.join(', ')})` : '';
1388
+ updateDirectoryProgressStatus(message + failedMessage);
1389
+
1390
+ // Redirect after delay
1391
+ setTimeout(() => {
1392
+ window.location.href = '<%= ragdoll.documents_path %>';
1393
+ }, 3000);
1394
+
1395
+ } else if (data.type === 'job_error') {
1396
+ // Job completely failed
1397
+ if (progressBar) {
1398
+ progressBar.className = 'progress-bar bg-danger';
1399
+ }
1400
+
1401
+ if (currentFile) {
1402
+ currentFile.innerHTML = `<i class="fas fa-exclamation-circle text-danger me-1"></i><span>Processing failed</span>`;
1403
+ }
1404
+
1405
+ if (progressLog) {
1406
+ const logEntry = document.createElement('div');
1407
+ logEntry.className = 'mb-1 text-danger fw-bold';
1408
+ logEntry.innerHTML = `<i class="fas fa-exclamation-circle text-danger me-1"></i>Job failed: ${data.error}`;
1409
+ progressLog.appendChild(logEntry);
1410
+ progressLog.scrollTop = progressLog.scrollHeight;
1411
+ }
1412
+
1413
+ updateDirectoryProgressStatus(`Processing failed: ${data.error}`);
1414
+ }
1415
+
1416
+ // Legacy compatibility for old data format
1417
+ if (data.total_files && data.completed_files !== undefined) {
1418
+ const processingPercentage = Math.round((data.completed_files / data.total_files) * 100);
1419
+ updateDirectoryProcessingProgress(processingPercentage);
1420
+
1421
+ if (processedCount) {
1422
+ processedCount.textContent = data.completed_files;
1423
+ }
1424
+
1425
+ if (data.status === 'completed') {
1426
+ updateDirectoryProgressStatus(`Processing ${data.filename}... (${data.completed_files}/${data.total_files} complete)`);
1427
+ } else if (status === 'error') {
1428
+ updateDirectoryProgressStatus(`Error processing ${filename}: ${progress || 'Unknown error'}`);
1429
+ }
1430
+
1431
+ // Check if all files are complete
1432
+ if (completed_files >= total_files) {
1433
+ updateDirectoryProgressStatus('All files processed successfully! Redirecting...');
1434
+ setTimeout(() => {
1435
+ window.location.href = '<%= ragdoll.documents_path %>';
1436
+ }, 2000);
1437
+ }
1438
+ }
1439
+ }
1440
+
1441
+ // Update directory upload progress bar
1442
+ function updateDirectoryUploadProgress(percentage) {
1443
+ const progressBar = document.getElementById('directory-upload-progress-bar');
1444
+ const percentageSpan = document.getElementById('directory-upload-percentage');
1445
+
1446
+ if (progressBar) {
1447
+ progressBar.style.width = percentage + '%';
1448
+ progressBar.setAttribute('aria-valuenow', percentage);
1449
+ progressBar.textContent = percentage + '%';
1450
+ }
1451
+
1452
+ if (percentageSpan) {
1453
+ percentageSpan.textContent = percentage + '%';
1454
+ }
1455
+ }
1456
+
1457
+ // Update directory processing progress bar
1458
+ function updateDirectoryProcessingProgress(percentage) {
1459
+ const progressBar = document.getElementById('directory-processing-progress-bar');
1460
+ const percentageSpan = document.getElementById('directory-processing-percentage');
1461
+
1462
+ if (progressBar) {
1463
+ progressBar.style.width = percentage + '%';
1464
+ progressBar.setAttribute('aria-valuenow', percentage);
1465
+ progressBar.textContent = percentage + '%';
1466
+ }
1467
+
1468
+ if (percentageSpan) {
1469
+ percentageSpan.textContent = percentage + '%';
1470
+ }
1471
+ }
1472
+
1473
+ // Update directory progress status message
1474
+ function updateDirectoryProgressStatus(message) {
1475
+ const statusEl = document.getElementById('directory-progress-status');
1476
+ if (statusEl) {
1477
+ statusEl.textContent = message;
1478
+ }
1479
+ }
1480
+
1481
+ // Text content form handler
1482
+ const textForms = document.querySelectorAll('form');
1483
+ textForms.forEach(function(form) {
1484
+ if (form.querySelector('textarea[name*="text_content"]')) {
1485
+ form.addEventListener('submit', function() {
1486
+ const submitBtn = document.getElementById('text-content-btn');
1487
+ if (submitBtn) {
1488
+ submitBtn.disabled = true;
1489
+ submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Processing...';
1490
+ }
1491
+ });
1492
+ }
1493
+ });
1494
+
1495
+ // Form validation
1496
+ const forms = document.querySelectorAll('.needs-validation');
1497
+ forms.forEach(function(form) {
1498
+ form.addEventListener('submit', function(event) {
1499
+ if (!form.checkValidity()) {
1500
+ event.preventDefault();
1501
+ event.stopPropagation();
1502
+ }
1503
+ form.classList.add('was-validated');
1504
+ }, false);
1505
+ });
1506
+ });
1507
+ </script>
1508
+
1509
+ <% if @document&.errors&.any? %>
1510
+ <div class="alert alert-danger mt-3">
1511
+ <h5>Errors:</h5>
1512
+ <ul class="mb-0">
1513
+ <% @document.errors.full_messages.each do |message| %>
1514
+ <li><%= message %></li>
1515
+ <% end %>
1516
+ </ul>
1517
+ </div>
1518
+ <% end %>