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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b281102216de9a922f017373bc1510946bbb530c229d8e2362aaf09257125450
4
- data.tar.gz: 7af9bf6f7a3024445e70088d9698d1f5baafa1adb3ecd03f8efb6aee1799d707
3
+ metadata.gz: 1672f9baacb258a7c8842d8f3632011b5e4f064b0fbab5b58ac803cbac12d196
4
+ data.tar.gz: 4ae58b4aa072fd99e1a48593812b6dcdde99b5877b9e9cdcce04e3614ff09d76
5
5
  SHA512:
6
- metadata.gz: 0c9457bed61b4c8c26dbffb8b448d777c0f4df3244320eddb020c4b9386328dca3249c5f2af7fbdab622f6727b0e86354ef6d5e2b216e483b7946a6bfa2f81ec
7
- data.tar.gz: 2aa4a94c63eb0bd651f865801ec36624326281b62f6b26043d18def963a7905155a9c25746f17272811c162cceb322656672f7385b0f62a50972083293b9264d
6
+ metadata.gz: 9233cdc22a07420f550c992c6ba7b0ec53eb36d72f6b2aafa7ca8771802fdffe9a067364842ad40e318321260d7478853663ec39978c2926fc796152996b447d
7
+ data.tar.gz: fcff07ebf80eba9f1e6c4cbbd48e995e2b29593b775185e15b179589cf21de935ef524c88fb823f985bdf04cf00a3dbfa806b5042d85c8bcae0b3a7d2fd93ce7
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Ragdoll Rails Engine JavaScript
3
+ * Core functionality for the Ragdoll Engine interface
4
+ */
5
+
6
+ window.Ragdoll = window.Ragdoll || {};
7
+
8
+ // Initialize Ragdoll namespace
9
+ Ragdoll.init = function() {
10
+ console.log('🤖 Ragdoll Engine JavaScript initialized');
11
+
12
+ // Initialize components
13
+ Ragdoll.initTooltips();
14
+ Ragdoll.initFormValidation();
15
+ Ragdoll.initSearchEnhancements();
16
+ Ragdoll.initActionCable();
17
+ };
18
+
19
+ // Initialize Bootstrap tooltips
20
+ Ragdoll.initTooltips = function() {
21
+ var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
22
+ var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
23
+ return new bootstrap.Tooltip(tooltipTriggerEl, {
24
+ delay: { "show": 500, "hide": 100 },
25
+ placement: 'top',
26
+ boundary: 'viewport',
27
+ fallbackPlacements: ['top', 'bottom']
28
+ });
29
+ });
30
+ };
31
+
32
+ // Form validation helpers
33
+ Ragdoll.initFormValidation = function() {
34
+ // Add basic form validation
35
+ var forms = document.querySelectorAll('.needs-validation');
36
+ forms.forEach(function(form) {
37
+ form.addEventListener('submit', function(event) {
38
+ if (!form.checkValidity()) {
39
+ event.preventDefault();
40
+ event.stopPropagation();
41
+ }
42
+ form.classList.add('was-validated');
43
+ });
44
+ });
45
+ };
46
+
47
+ // Search enhancements
48
+ Ragdoll.initSearchEnhancements = function() {
49
+ // Auto-submit search form on Enter
50
+ var searchInputs = document.querySelectorAll('input[type="search"], input[name="query"]');
51
+ searchInputs.forEach(function(input) {
52
+ input.addEventListener('keypress', function(e) {
53
+ if (e.key === 'Enter') {
54
+ var form = input.closest('form');
55
+ if (form) {
56
+ form.submit();
57
+ }
58
+ }
59
+ });
60
+ });
61
+ };
62
+
63
+ // ActionCable initialization
64
+ Ragdoll.initActionCable = function() {
65
+ // Wait for ActionCable to be available (might be loaded via CDN)
66
+ function waitForActionCable() {
67
+ if (typeof ActionCable !== 'undefined') {
68
+ console.log('🔗 Initializing ActionCable for Ragdoll Engine');
69
+
70
+ // Initialize App namespace if not exists
71
+ if (!window.App) {
72
+ window.App = {};
73
+ }
74
+
75
+ // Create ActionCable consumer if not exists
76
+ if (!window.App.cable) {
77
+ try {
78
+ window.App.cable = ActionCable.createConsumer('/cable');
79
+ console.log('📡 ActionCable consumer created for Ragdoll');
80
+ console.log('✅ App.cable ready:', window.App.cable);
81
+ } catch (e) {
82
+ console.error('❌ Failed to create ActionCable consumer:', e);
83
+ }
84
+ } else {
85
+ console.log('✅ App.cable already exists');
86
+ }
87
+ } else {
88
+ console.warn('⚠️ ActionCable not available - real-time features will be limited');
89
+ // Retry after a short delay in case ActionCable is still loading
90
+ setTimeout(waitForActionCable, 100);
91
+ }
92
+ }
93
+
94
+ // Start waiting for ActionCable
95
+ waitForActionCable();
96
+ };
97
+
98
+ // Utility functions
99
+ Ragdoll.showLoading = function(element) {
100
+ element.classList.add('ragdoll-loading');
101
+ };
102
+
103
+ Ragdoll.hideLoading = function(element) {
104
+ element.classList.remove('ragdoll-loading');
105
+ };
106
+
107
+ Ragdoll.showAlert = function(message, type = 'info') {
108
+ var alertDiv = document.createElement('div');
109
+ alertDiv.className = `alert alert-${type} alert-dismissible fade show ragdoll-alert`;
110
+ alertDiv.innerHTML = `
111
+ ${message}
112
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
113
+ `;
114
+
115
+ var container = document.querySelector('.container');
116
+ if (container) {
117
+ container.insertBefore(alertDiv, container.firstChild);
118
+ }
119
+ };
120
+
121
+ // Initialize when DOM is ready
122
+ document.addEventListener('DOMContentLoaded', function() {
123
+ Ragdoll.init();
124
+ });
125
+
126
+ // Re-initialize on Turbo navigation (if using Turbo)
127
+ document.addEventListener('turbo:load', function() {
128
+ Ragdoll.init();
129
+ });
@@ -0,0 +1,454 @@
1
+ class BulkUploadStatus {
2
+ constructor() {
3
+ this.activeUploads = new Map();
4
+ this.container = null;
5
+ this.isMinimized = false;
6
+ this.cable = null;
7
+ this.subscriptions = new Map();
8
+ this.init();
9
+ this.restoreActiveUploads(); // Restore uploads from previous page
10
+ }
11
+
12
+ init() {
13
+ this.createContainer();
14
+ this.setupEventListeners();
15
+ this.cable = ActionCable.createConsumer();
16
+ }
17
+
18
+ // Restore active uploads from sessionStorage when page loads
19
+ restoreActiveUploads() {
20
+ const storedUploads = sessionStorage.getItem('ragdoll_active_uploads');
21
+ if (storedUploads) {
22
+ try {
23
+ const uploads = JSON.parse(storedUploads);
24
+ console.log('🔄 Restoring active uploads:', uploads);
25
+
26
+ uploads.forEach(upload => {
27
+ // Only restore if upload is not completed or failed
28
+ if (upload.status === 'processing' || upload.status === 'starting') {
29
+ // Convert date strings back to Date objects
30
+ upload.startTime = new Date(upload.startTime);
31
+ if (upload.completedAt) upload.completedAt = new Date(upload.completedAt);
32
+ if (upload.failedAt) upload.failedAt = new Date(upload.failedAt);
33
+
34
+ this.activeUploads.set(upload.sessionId, upload);
35
+ this.subscribeToSession(upload.sessionId);
36
+ }
37
+ });
38
+
39
+ if (this.activeUploads.size > 0) {
40
+ this.updateDisplay();
41
+ this.show();
42
+
43
+ // Restore minimized state
44
+ const minimizedState = sessionStorage.getItem('ragdoll_popup_minimized');
45
+ if (minimizedState === 'true') {
46
+ this.isMinimized = true;
47
+ this.container.classList.add('minimized');
48
+ const icon = this.container.querySelector('.minimize-btn i');
49
+ if (icon) icon.className = 'fas fa-plus';
50
+ }
51
+ }
52
+ } catch (e) {
53
+ console.error('Failed to restore uploads:', e);
54
+ }
55
+ }
56
+ }
57
+
58
+ // Save active uploads to sessionStorage whenever they change
59
+ saveActiveUploads() {
60
+ const uploadsToSave = Array.from(this.activeUploads.values()).filter(upload =>
61
+ upload.status === 'processing' || upload.status === 'starting'
62
+ );
63
+ sessionStorage.setItem('ragdoll_active_uploads', JSON.stringify(uploadsToSave));
64
+ }
65
+
66
+ createContainer() {
67
+ this.container = document.createElement('div');
68
+ this.container.id = 'bulk-upload-status-container';
69
+ this.container.className = 'bulk-upload-status-container';
70
+ this.container.innerHTML = `
71
+ <div class="bulk-upload-status-header">
72
+ <span class="bulk-upload-status-title">
73
+ <i class="fas fa-cloud-upload-alt"></i>
74
+ <span class="title-text">Upload Progress</span>
75
+ </span>
76
+ <div class="bulk-upload-status-controls">
77
+ <button class="minimize-btn" title="Minimize">
78
+ <i class="fas fa-minus"></i>
79
+ </button>
80
+ <button class="close-btn" title="Close completed uploads">
81
+ <i class="fas fa-times"></i>
82
+ </button>
83
+ </div>
84
+ </div>
85
+ <div class="bulk-upload-status-content">
86
+ <div class="no-uploads-message">
87
+ No active uploads
88
+ </div>
89
+ </div>
90
+ `;
91
+
92
+ document.body.appendChild(this.container);
93
+ this.hide();
94
+ }
95
+
96
+ setupEventListeners() {
97
+ // Minimize/maximize toggle
98
+ this.container.querySelector('.minimize-btn').addEventListener('click', () => {
99
+ this.toggle();
100
+ });
101
+
102
+ // Close completed uploads
103
+ this.container.querySelector('.close-btn').addEventListener('click', () => {
104
+ this.closeCompletedUploads();
105
+ });
106
+
107
+ // Make draggable
108
+ this.makeDraggable();
109
+ }
110
+
111
+ makeDraggable() {
112
+ const header = this.container.querySelector('.bulk-upload-status-header');
113
+ let isDragging = false;
114
+ let currentX;
115
+ let currentY;
116
+ let initialX;
117
+ let initialY;
118
+ let xOffset = 0;
119
+ let yOffset = 0;
120
+
121
+ header.addEventListener('mousedown', (e) => {
122
+ if (e.target.closest('button')) return;
123
+
124
+ initialX = e.clientX - xOffset;
125
+ initialY = e.clientY - yOffset;
126
+ isDragging = true;
127
+ header.style.cursor = 'grabbing';
128
+ });
129
+
130
+ document.addEventListener('mousemove', (e) => {
131
+ if (isDragging) {
132
+ e.preventDefault();
133
+ currentX = e.clientX - initialX;
134
+ currentY = e.clientY - initialY;
135
+ xOffset = currentX;
136
+ yOffset = currentY;
137
+
138
+ this.container.style.transform = `translate(${currentX}px, ${currentY}px)`;
139
+ }
140
+ });
141
+
142
+ document.addEventListener('mouseup', () => {
143
+ isDragging = false;
144
+ header.style.cursor = 'grab';
145
+ });
146
+ }
147
+
148
+ startUpload(sessionId, totalFiles) {
149
+ const upload = {
150
+ sessionId,
151
+ totalFiles,
152
+ processed: 0,
153
+ failed: 0,
154
+ currentFile: null,
155
+ status: 'starting',
156
+ startTime: new Date(),
157
+ errors: []
158
+ };
159
+
160
+ this.activeUploads.set(sessionId, upload);
161
+ this.subscribeToSession(sessionId);
162
+ this.saveActiveUploads(); // Save to sessionStorage
163
+ this.updateDisplay();
164
+ this.show();
165
+ }
166
+
167
+ subscribeToSession(sessionId) {
168
+ if (this.subscriptions.has(sessionId)) {
169
+ return; // Already subscribed
170
+ }
171
+
172
+ const subscription = this.cable.subscriptions.create(
173
+ {
174
+ channel: "Ragdoll::BulkUploadStatusChannel",
175
+ session_id: sessionId
176
+ },
177
+ {
178
+ received: (data) => {
179
+ this.handleStatusUpdate(sessionId, data);
180
+ },
181
+
182
+ connected: () => {
183
+ console.log(`Connected to bulk upload status for session ${sessionId}`);
184
+ },
185
+
186
+ disconnected: () => {
187
+ console.log(`Disconnected from bulk upload status for session ${sessionId}`);
188
+ }
189
+ }
190
+ );
191
+
192
+ this.subscriptions.set(sessionId, subscription);
193
+ }
194
+
195
+ handleStatusUpdate(sessionId, data) {
196
+ const upload = this.activeUploads.get(sessionId);
197
+ if (!upload) return;
198
+
199
+ switch (data.type) {
200
+ case 'upload_start':
201
+ upload.status = 'processing';
202
+ upload.totalFiles = data.total_files;
203
+ break;
204
+
205
+ case 'file_start':
206
+ upload.currentFile = data.filename;
207
+ upload.processed = data.processed;
208
+ break;
209
+
210
+ case 'file_complete':
211
+ upload.processed = data.processed;
212
+ upload.currentFile = null;
213
+ break;
214
+
215
+ case 'file_error':
216
+ upload.processed = data.processed;
217
+ upload.failed++;
218
+ upload.errors.push({
219
+ filename: data.filename,
220
+ error: data.error
221
+ });
222
+ break;
223
+
224
+ case 'upload_complete':
225
+ upload.status = 'completed';
226
+ upload.processed = data.processed;
227
+ upload.failed = data.failed;
228
+ upload.currentFile = null;
229
+ upload.completedAt = new Date();
230
+ this.scheduleAutoClose(sessionId);
231
+
232
+ // If all files processed successfully, optionally redirect to documents page
233
+ if (data.failed === 0 && window.location.pathname.includes('/documents/new')) {
234
+ setTimeout(() => {
235
+ window.location.href = '/ragdoll/documents';
236
+ }, 5000); // Redirect after 5 seconds to show completion message
237
+ }
238
+ break;
239
+
240
+ case 'upload_error':
241
+ upload.status = 'failed';
242
+ upload.error = data.error;
243
+ upload.failedAt = new Date();
244
+ break;
245
+ }
246
+
247
+ this.saveActiveUploads(); // Save state after each update
248
+ this.updateDisplay();
249
+ }
250
+
251
+ updateDisplay() {
252
+ const content = this.container.querySelector('.bulk-upload-status-content');
253
+
254
+ if (this.activeUploads.size === 0) {
255
+ content.innerHTML = '<div class="no-uploads-message">No active uploads</div>';
256
+ return;
257
+ }
258
+
259
+ const uploadsHTML = Array.from(this.activeUploads.entries()).map(([sessionId, upload]) => {
260
+ return this.renderUpload(sessionId, upload);
261
+ }).join('');
262
+
263
+ content.innerHTML = uploadsHTML;
264
+ }
265
+
266
+ renderUpload(sessionId, upload) {
267
+ const percentage = upload.totalFiles > 0 ? (upload.processed / upload.totalFiles * 100) : 0;
268
+ const statusClass = this.getStatusClass(upload.status);
269
+ const eta = this.calculateETA(upload);
270
+
271
+ return `
272
+ <div class="upload-item ${statusClass}" data-session-id="${sessionId}">
273
+ <div class="upload-header">
274
+ <span class="upload-title">
275
+ <i class="${this.getStatusIcon(upload.status)}"></i>
276
+ Bulk Upload (${upload.totalFiles} files)
277
+ </span>
278
+ <span class="upload-percentage">${percentage.toFixed(1)}%</span>
279
+ </div>
280
+
281
+ <div class="progress-bar">
282
+ <div class="progress-fill" style="width: ${percentage}%"></div>
283
+ </div>
284
+
285
+ <div class="upload-details">
286
+ <div class="upload-stats">
287
+ <span class="processed-count">${upload.processed}/${upload.totalFiles} processed</span>
288
+ ${upload.failed > 0 ? `<span class="failed-count">${upload.failed} failed</span>` : ''}
289
+ ${eta ? `<span class="eta">ETA: ${eta}</span>` : ''}
290
+ </div>
291
+
292
+ ${upload.currentFile ? `
293
+ <div class="current-file">
294
+ <i class="fas fa-file-alt"></i>
295
+ Processing: ${upload.currentFile}
296
+ </div>
297
+ ` : ''}
298
+
299
+ ${upload.status === 'completed' ? `
300
+ <div class="completion-message">
301
+ <i class="fas fa-check-circle"></i>
302
+ Upload completed successfully
303
+ </div>
304
+ ` : ''}
305
+
306
+ ${upload.status === 'failed' ? `
307
+ <div class="error-message">
308
+ <i class="fas fa-exclamation-triangle"></i>
309
+ Upload failed: ${upload.error}
310
+ </div>
311
+ ` : ''}
312
+
313
+ ${upload.errors.length > 0 ? `
314
+ <div class="error-list">
315
+ <details>
316
+ <summary>${upload.errors.length} file(s) failed</summary>
317
+ <ul>
318
+ ${upload.errors.map(err => `<li>${err.filename}: ${err.error}</li>`).join('')}
319
+ </ul>
320
+ </details>
321
+ </div>
322
+ ` : ''}
323
+ </div>
324
+ </div>
325
+ `;
326
+ }
327
+
328
+ getStatusClass(status) {
329
+ const classes = {
330
+ 'starting': 'status-starting',
331
+ 'processing': 'status-processing',
332
+ 'completed': 'status-completed',
333
+ 'failed': 'status-failed'
334
+ };
335
+ return classes[status] || '';
336
+ }
337
+
338
+ getStatusIcon(status) {
339
+ const icons = {
340
+ 'starting': 'fas fa-clock',
341
+ 'processing': 'fas fa-spinner fa-spin',
342
+ 'completed': 'fas fa-check-circle',
343
+ 'failed': 'fas fa-exclamation-triangle'
344
+ };
345
+ return icons[status] || 'fas fa-circle';
346
+ }
347
+
348
+ calculateETA(upload) {
349
+ if (upload.status !== 'processing' || upload.processed === 0) {
350
+ return null;
351
+ }
352
+
353
+ const elapsed = (new Date() - upload.startTime) / 1000; // seconds
354
+ const rate = upload.processed / elapsed; // files per second
355
+ const remaining = upload.totalFiles - upload.processed;
356
+ const etaSeconds = remaining / rate;
357
+
358
+ if (etaSeconds > 60) {
359
+ const minutes = Math.ceil(etaSeconds / 60);
360
+ return `${minutes}m`;
361
+ } else {
362
+ return `${Math.ceil(etaSeconds)}s`;
363
+ }
364
+ }
365
+
366
+ scheduleAutoClose(sessionId) {
367
+ setTimeout(() => {
368
+ this.removeUpload(sessionId);
369
+ }, 30000); // Auto-close after 30 seconds
370
+ }
371
+
372
+ removeUpload(sessionId) {
373
+ const subscription = this.subscriptions.get(sessionId);
374
+ if (subscription) {
375
+ subscription.unsubscribe();
376
+ this.subscriptions.delete(sessionId);
377
+ }
378
+
379
+ this.activeUploads.delete(sessionId);
380
+ this.saveActiveUploads(); // Update sessionStorage
381
+ this.updateDisplay();
382
+
383
+ if (this.activeUploads.size === 0) {
384
+ this.hide();
385
+ sessionStorage.removeItem('ragdoll_active_uploads'); // Clear storage when no uploads
386
+ }
387
+ }
388
+
389
+ closeCompletedUploads() {
390
+ const completedSessions = Array.from(this.activeUploads.entries())
391
+ .filter(([_, upload]) => upload.status === 'completed' || upload.status === 'failed')
392
+ .map(([sessionId, _]) => sessionId);
393
+
394
+ completedSessions.forEach(sessionId => {
395
+ this.removeUpload(sessionId);
396
+ });
397
+ }
398
+
399
+ show() {
400
+ this.container.classList.add('visible');
401
+ }
402
+
403
+ hide() {
404
+ this.container.classList.remove('visible');
405
+ }
406
+
407
+ toggle() {
408
+ this.isMinimized = !this.isMinimized;
409
+ this.container.classList.toggle('minimized', this.isMinimized);
410
+
411
+ const icon = this.container.querySelector('.minimize-btn i');
412
+ icon.className = this.isMinimized ? 'fas fa-plus' : 'fas fa-minus';
413
+
414
+ // Save minimized state
415
+ sessionStorage.setItem('ragdoll_popup_minimized', this.isMinimized ? 'true' : 'false');
416
+ }
417
+ }
418
+
419
+ // Global instance
420
+ window.BulkUploadStatus = BulkUploadStatus;
421
+
422
+ // Auto-initialize when DOM is ready
423
+ document.addEventListener('DOMContentLoaded', () => {
424
+ if (!window.bulkUploadStatus) {
425
+ window.bulkUploadStatus = new BulkUploadStatus();
426
+ }
427
+ });
428
+
429
+ // Handle Turbo page changes
430
+ document.addEventListener('turbo:load', () => {
431
+ // Check if we already have an instance
432
+ if (window.bulkUploadStatus) {
433
+ // Destroy old instance properly
434
+ if (window.bulkUploadStatus.container && window.bulkUploadStatus.container.parentNode) {
435
+ window.bulkUploadStatus.container.parentNode.removeChild(window.bulkUploadStatus.container);
436
+ }
437
+ }
438
+ // Always create a new instance on Turbo navigation to restore state
439
+ window.bulkUploadStatus = new BulkUploadStatus();
440
+ });
441
+
442
+ // Save state before navigating away
443
+ document.addEventListener('turbo:before-cache', () => {
444
+ if (window.bulkUploadStatus) {
445
+ window.bulkUploadStatus.saveActiveUploads();
446
+ }
447
+ });
448
+
449
+ // Also handle regular page unload for non-Turbo navigation
450
+ window.addEventListener('beforeunload', () => {
451
+ if (window.bulkUploadStatus) {
452
+ window.bulkUploadStatus.saveActiveUploads();
453
+ }
454
+ });
@@ -0,0 +1,84 @@
1
+ /*
2
+ * Ragdoll Rails Engine Stylesheet
3
+ * This file provides custom styling for the Ragdoll Engine interface
4
+ */
5
+
6
+ /* Custom styling for Ragdoll components */
7
+ .ragdoll-brand {
8
+ font-weight: bold;
9
+ }
10
+
11
+ .ragdoll-stats-card {
12
+ transition: transform 0.2s ease-in-out;
13
+ }
14
+
15
+ .ragdoll-stats-card:hover {
16
+ transform: translateY(-2px);
17
+ }
18
+
19
+ .ragdoll-search-form {
20
+ background-color: #f8f9fa;
21
+ border-radius: 8px;
22
+ padding: 1rem;
23
+ }
24
+
25
+ .ragdoll-document-list {
26
+ max-height: 500px;
27
+ overflow-y: auto;
28
+ }
29
+
30
+ .ragdoll-similarity-score {
31
+ font-family: 'Courier New', monospace;
32
+ font-weight: bold;
33
+ }
34
+
35
+ /* Loading states */
36
+ .ragdoll-loading {
37
+ opacity: 0.6;
38
+ pointer-events: none;
39
+ }
40
+
41
+ .ragdoll-loading::after {
42
+ content: '';
43
+ position: absolute;
44
+ top: 50%;
45
+ left: 50%;
46
+ width: 20px;
47
+ height: 20px;
48
+ margin: -10px 0 0 -10px;
49
+ border: 2px solid #007bff;
50
+ border-radius: 50%;
51
+ border-top-color: transparent;
52
+ animation: ragdoll-spin 1s linear infinite;
53
+ }
54
+
55
+ @keyframes ragdoll-spin {
56
+ to {
57
+ transform: rotate(360deg);
58
+ }
59
+ }
60
+
61
+ /* Component specific styles */
62
+ .navbar.ragdoll-navbar {
63
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
64
+ }
65
+
66
+ .card.ragdoll-card {
67
+ border: 1px solid #e9ecef;
68
+ box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
69
+ }
70
+
71
+ .alert.ragdoll-alert {
72
+ border-radius: 6px;
73
+ }
74
+
75
+ /* Responsive adjustments */
76
+ @media (max-width: 768px) {
77
+ .ragdoll-stats-card {
78
+ margin-bottom: 1rem;
79
+ }
80
+
81
+ .ragdoll-search-form {
82
+ padding: 0.75rem;
83
+ }
84
+ }