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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/ragdoll/application.js +129 -0
- data/app/assets/javascripts/ragdoll/bulk_upload_status.js +454 -0
- data/app/assets/stylesheets/ragdoll/application.css +84 -0
- data/app/assets/stylesheets/ragdoll/bulk_upload_status.css +379 -0
- data/app/channels/application_cable/channel.rb +6 -0
- data/app/channels/application_cable/connection.rb +6 -0
- data/app/channels/ragdoll/bulk_upload_status_channel.rb +27 -0
- data/app/channels/ragdoll/file_processing_channel.rb +26 -0
- data/app/components/ragdoll/alert_component.html.erb +4 -0
- data/app/components/ragdoll/alert_component.rb +32 -0
- data/app/components/ragdoll/application_component.rb +6 -0
- data/app/components/ragdoll/card_component.html.erb +15 -0
- data/app/components/ragdoll/card_component.rb +21 -0
- data/app/components/ragdoll/document_list_component.html.erb +41 -0
- data/app/components/ragdoll/document_list_component.rb +13 -0
- data/app/components/ragdoll/document_table_component.html.erb +76 -0
- data/app/components/ragdoll/document_table_component.rb +13 -0
- data/app/components/ragdoll/empty_state_component.html.erb +12 -0
- data/app/components/ragdoll/empty_state_component.rb +17 -0
- data/app/components/ragdoll/flash_messages_component.html.erb +3 -0
- data/app/components/ragdoll/flash_messages_component.rb +37 -0
- data/app/components/ragdoll/navbar_component.html.erb +24 -0
- data/app/components/ragdoll/navbar_component.rb +31 -0
- data/app/components/ragdoll/page_header_component.html.erb +13 -0
- data/app/components/ragdoll/page_header_component.rb +15 -0
- data/app/components/ragdoll/stats_card_component.html.erb +11 -0
- data/app/components/ragdoll/stats_card_component.rb +17 -0
- data/app/components/ragdoll/status_badge_component.html.erb +3 -0
- data/app/components/ragdoll/status_badge_component.rb +30 -0
- data/app/controllers/ragdoll/api/v1/analytics_controller.rb +72 -0
- data/app/controllers/ragdoll/api/v1/base_controller.rb +29 -0
- data/app/controllers/ragdoll/api/v1/documents_controller.rb +148 -0
- data/app/controllers/ragdoll/api/v1/search_controller.rb +87 -0
- data/app/controllers/ragdoll/api/v1/system_controller.rb +97 -0
- data/app/controllers/ragdoll/application_controller.rb +17 -0
- data/app/controllers/ragdoll/configuration_controller.rb +82 -0
- data/app/controllers/ragdoll/dashboard_controller.rb +98 -0
- data/app/controllers/ragdoll/documents_controller.rb +460 -0
- data/app/controllers/ragdoll/documents_controller_backup.rb +68 -0
- data/app/controllers/ragdoll/jobs_controller.rb +116 -0
- data/app/controllers/ragdoll/search_controller.rb +368 -0
- data/app/jobs/application_job.rb +9 -0
- data/app/jobs/ragdoll/bulk_document_processing_job.rb +280 -0
- data/app/jobs/ragdoll/process_file_job.rb +166 -0
- data/app/services/ragdoll/worker_health_service.rb +111 -0
- data/app/views/layouts/ragdoll/application.html.erb +162 -0
- data/app/views/ragdoll/dashboard/analytics.html.erb +333 -0
- data/app/views/ragdoll/dashboard/index.html.erb +208 -0
- data/app/views/ragdoll/documents/edit.html.erb +91 -0
- data/app/views/ragdoll/documents/index.html.erb +302 -0
- data/app/views/ragdoll/documents/new.html.erb +1518 -0
- data/app/views/ragdoll/documents/show.html.erb +188 -0
- data/app/views/ragdoll/documents/upload_results.html.erb +248 -0
- data/app/views/ragdoll/jobs/index.html.erb +669 -0
- data/app/views/ragdoll/jobs/show.html.erb +129 -0
- data/app/views/ragdoll/search/index.html.erb +324 -0
- data/config/cable.yml +12 -0
- data/config/routes.rb +56 -1
- data/lib/ragdoll/rails/engine.rb +32 -1
- data/lib/ragdoll/rails/version.rb +1 -1
- metadata +86 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1672f9baacb258a7c8842d8f3632011b5e4f064b0fbab5b58ac803cbac12d196
|
4
|
+
data.tar.gz: 4ae58b4aa072fd99e1a48593812b6dcdde99b5877b9e9cdcce04e3614ff09d76
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
+
}
|