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
@@ -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 %>
|