ragdoll-rails 0.1.9 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -2
  3. data/app/assets/javascripts/ragdoll/application.js +129 -0
  4. data/app/assets/javascripts/ragdoll/bulk_upload_status.js +454 -0
  5. data/app/assets/stylesheets/ragdoll/application.css +84 -0
  6. data/app/assets/stylesheets/ragdoll/bulk_upload_status.css +379 -0
  7. data/app/channels/application_cable/channel.rb +6 -0
  8. data/app/channels/application_cable/connection.rb +6 -0
  9. data/app/channels/ragdoll/bulk_upload_status_channel.rb +27 -0
  10. data/app/channels/ragdoll/file_processing_channel.rb +26 -0
  11. data/app/components/ragdoll/alert_component.html.erb +4 -0
  12. data/app/components/ragdoll/alert_component.rb +32 -0
  13. data/app/components/ragdoll/application_component.rb +6 -0
  14. data/app/components/ragdoll/card_component.html.erb +15 -0
  15. data/app/components/ragdoll/card_component.rb +21 -0
  16. data/app/components/ragdoll/document_list_component.html.erb +41 -0
  17. data/app/components/ragdoll/document_list_component.rb +13 -0
  18. data/app/components/ragdoll/document_table_component.html.erb +76 -0
  19. data/app/components/ragdoll/document_table_component.rb +13 -0
  20. data/app/components/ragdoll/empty_state_component.html.erb +12 -0
  21. data/app/components/ragdoll/empty_state_component.rb +17 -0
  22. data/app/components/ragdoll/flash_messages_component.html.erb +3 -0
  23. data/app/components/ragdoll/flash_messages_component.rb +37 -0
  24. data/app/components/ragdoll/navbar_component.html.erb +24 -0
  25. data/app/components/ragdoll/navbar_component.rb +31 -0
  26. data/app/components/ragdoll/page_header_component.html.erb +13 -0
  27. data/app/components/ragdoll/page_header_component.rb +15 -0
  28. data/app/components/ragdoll/stats_card_component.html.erb +11 -0
  29. data/app/components/ragdoll/stats_card_component.rb +17 -0
  30. data/app/components/ragdoll/status_badge_component.html.erb +3 -0
  31. data/app/components/ragdoll/status_badge_component.rb +30 -0
  32. data/app/controllers/ragdoll/api/v1/analytics_controller.rb +72 -0
  33. data/app/controllers/ragdoll/api/v1/base_controller.rb +29 -0
  34. data/app/controllers/ragdoll/api/v1/documents_controller.rb +148 -0
  35. data/app/controllers/ragdoll/api/v1/search_controller.rb +87 -0
  36. data/app/controllers/ragdoll/api/v1/system_controller.rb +97 -0
  37. data/app/controllers/ragdoll/application_controller.rb +17 -0
  38. data/app/controllers/ragdoll/configuration_controller.rb +82 -0
  39. data/app/controllers/ragdoll/dashboard_controller.rb +98 -0
  40. data/app/controllers/ragdoll/documents_controller.rb +460 -0
  41. data/app/controllers/ragdoll/documents_controller_backup.rb +68 -0
  42. data/app/controllers/ragdoll/jobs_controller.rb +116 -0
  43. data/app/controllers/ragdoll/search_controller.rb +374 -0
  44. data/app/jobs/application_job.rb +9 -0
  45. data/app/jobs/ragdoll/bulk_document_processing_job.rb +280 -0
  46. data/app/jobs/ragdoll/process_file_job.rb +166 -0
  47. data/app/services/ragdoll/worker_health_service.rb +111 -0
  48. data/app/views/layouts/ragdoll/application.html.erb +162 -0
  49. data/app/views/ragdoll/dashboard/analytics.html.erb +333 -0
  50. data/app/views/ragdoll/dashboard/index.html.erb +208 -0
  51. data/app/views/ragdoll/documents/edit.html.erb +91 -0
  52. data/app/views/ragdoll/documents/index.html.erb +305 -0
  53. data/app/views/ragdoll/documents/new.html.erb +1518 -0
  54. data/app/views/ragdoll/documents/show.html.erb +188 -0
  55. data/app/views/ragdoll/documents/upload_results.html.erb +248 -0
  56. data/app/views/ragdoll/jobs/index.html.erb +669 -0
  57. data/app/views/ragdoll/jobs/show.html.erb +129 -0
  58. data/app/views/ragdoll/search/index.html.erb +327 -0
  59. data/config/cable.yml +12 -0
  60. data/config/routes.rb +56 -1
  61. data/lib/generators/ragdoll/init/templates/ragdoll_config.rb +17 -4
  62. data/lib/ragdoll/rails/configuration.rb +2 -1
  63. data/lib/ragdoll/rails/engine.rb +32 -1
  64. data/lib/ragdoll/rails/version.rb +1 -1
  65. metadata +90 -4
@@ -0,0 +1,208 @@
1
+ <% content_for :title, "Dashboard - Ragdoll Engine" %>
2
+
3
+ <%= render Ragdoll::PageHeaderComponent.new(
4
+ title: "Ragdoll Engine Dashboard",
5
+ icon: "fas fa-tachometer-alt",
6
+ subtitle: "Overview of your document processing and search system"
7
+ ) %>
8
+
9
+ <div class="row mb-4">
10
+ <div class="col-md-3">
11
+ <%= render Ragdoll::StatsCardComponent.new(
12
+ title: "Documents",
13
+ value: @stats[:total_documents],
14
+ icon: "fas fa-file-alt",
15
+ color: "primary",
16
+ description: "Total documents"
17
+ ) %>
18
+ </div>
19
+ <div class="col-md-3">
20
+ <%= render Ragdoll::StatsCardComponent.new(
21
+ title: "Processed",
22
+ value: @stats[:processed_documents],
23
+ icon: "fas fa-check-circle",
24
+ color: "success",
25
+ description: "Successfully processed"
26
+ ) %>
27
+ </div>
28
+ <div class="col-md-3">
29
+ <%= render Ragdoll::StatsCardComponent.new(
30
+ title: "Embeddings",
31
+ value: @stats[:total_embeddings],
32
+ icon: "fas fa-vector-square",
33
+ color: "info",
34
+ description: "Vector embeddings"
35
+ ) %>
36
+ </div>
37
+ <div class="col-md-3">
38
+ <%= render Ragdoll::StatsCardComponent.new(
39
+ title: "Searches",
40
+ value: @stats[:total_searches],
41
+ icon: "fas fa-search",
42
+ color: "warning",
43
+ description: "Total searches"
44
+ ) %>
45
+ </div>
46
+ </div>
47
+
48
+ <div class="row">
49
+ <div class="col-md-6">
50
+ <%= render Ragdoll::CardComponent.new(title: "Document Types", icon: "fas fa-chart-pie") do %>
51
+ <% if @document_types.any? %>
52
+ <canvas id="documentTypesChart" width="400" height="200"></canvas>
53
+ <% else %>
54
+ <p class="text-muted">No documents yet. <%= link_to "Add your first document", ragdoll.new_document_path, class: "btn btn-primary btn-sm" %></p>
55
+ <% end %>
56
+ <% end %>
57
+ </div>
58
+
59
+ <div class="col-md-6">
60
+ <%= render Ragdoll::CardComponent.new(title: "Most Searched Documents", icon: "fas fa-fire") do %>
61
+ <% if @top_searched_documents.any? %>
62
+ <% @top_searched_documents.each do |title, count| %>
63
+ <div class="d-flex justify-content-between align-items-center mb-2">
64
+ <span class="text-truncate"><%= title %></span>
65
+ <span class="badge bg-primary"><%= count %></span>
66
+ </div>
67
+ <% end %>
68
+ <% else %>
69
+ <p class="text-muted">No search data yet. <%= link_to "Try searching", ragdoll.search_index_path, class: "btn btn-primary btn-sm" %></p>
70
+ <% end %>
71
+ <% end %>
72
+ </div>
73
+ </div>
74
+
75
+ <div class="row mt-4">
76
+ <div class="col-md-6">
77
+ <div class="card">
78
+ <div class="card-header">
79
+ <h5><i class="fas fa-clock"></i> Recent Documents</h5>
80
+ </div>
81
+ <div class="card-body">
82
+ <% if @recent_documents.any? %>
83
+ <% @recent_documents.each do |document| %>
84
+ <div class="d-flex justify-content-between align-items-center mb-2">
85
+ <div>
86
+ <%= link_to document.title, ragdoll.document_path(document), class: "text-decoration-none" %>
87
+ <small class="text-muted d-block">
88
+ <%= document.document_type&.upcase %> •
89
+ <%= render Ragdoll::StatusBadgeComponent.new(status: document.status) %>
90
+ </small>
91
+ </div>
92
+ <small class="text-muted"><%= time_ago_in_words(document.created_at) %> ago</small>
93
+ </div>
94
+ <% end %>
95
+ <div class="mt-3">
96
+ <%= link_to "View all documents", ragdoll.documents_path, class: "btn btn-outline-primary btn-sm" %>
97
+ </div>
98
+ <% else %>
99
+ <p class="text-muted">No documents yet. <%= link_to "Add your first document", ragdoll.new_document_path, class: "btn btn-primary btn-sm" %></p>
100
+ <% end %>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <div class="col-md-6">
106
+ <div class="card">
107
+ <div class="card-header">
108
+ <h5><i class="fas fa-history"></i> Recent Searches</h5>
109
+ </div>
110
+ <div class="card-body">
111
+ <% if @stats[:recent_searches].any? %>
112
+ <% @stats[:recent_searches].each do |search| %>
113
+ <div class="d-flex justify-content-between align-items-center mb-2">
114
+ <div>
115
+ <a href="<%= ragdoll.search_index_path %>?search_id=<%= search.id %>" class="text-decoration-none">
116
+ <i class="fas fa-search me-1"></i><strong><%= search.query %></strong>
117
+ </a>
118
+ <small class="text-muted d-block">
119
+ <%= search.search_type.titleize %>
120
+ • <%= search.results_count %> results
121
+ <% if search.execution_time_ms %>
122
+ • <%= number_with_precision(search.execution_time_ms / 1000.0, precision: 3) %>s
123
+ <% end %>
124
+ </small>
125
+ </div>
126
+ <small class="text-muted"><%= time_ago_in_words(search.created_at) %> ago</small>
127
+ </div>
128
+ <% end %>
129
+ <div class="mt-3">
130
+ <%= link_to "View search analytics", ragdoll.analytics_path, class: "btn btn-outline-primary btn-sm" %>
131
+ </div>
132
+ <% else %>
133
+ <p class="text-muted">No searches yet. <%= link_to "Try searching", ragdoll.search_index_path, class: "btn btn-primary btn-sm" %></p>
134
+ <% end %>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ <div class="row mt-4">
141
+ <div class="col-12">
142
+ <div class="card">
143
+ <div class="card-header">
144
+ <h5><i class="fas fa-tools"></i> Quick Actions</h5>
145
+ </div>
146
+ <div class="card-body">
147
+ <div class="row">
148
+ <div class="col-md-3 mb-2">
149
+ <%= link_to ragdoll.new_document_path, class: "btn btn-primary w-100" do %>
150
+ <i class="fas fa-plus"></i> Add Document
151
+ <% end %>
152
+ </div>
153
+ <div class="col-md-3 mb-2">
154
+ <%= link_to ragdoll.search_index_path, class: "btn btn-success w-100" do %>
155
+ <i class="fas fa-search"></i> Search Documents
156
+ <% end %>
157
+ </div>
158
+ <div class="col-md-3 mb-2">
159
+ <%= link_to ragdoll.analytics_path, class: "btn btn-info w-100" do %>
160
+ <i class="fas fa-chart-line"></i> View Analytics
161
+ <% end %>
162
+ </div>
163
+ <div class="col-md-3 mb-2">
164
+ <%= link_to ragdoll.configuration_path, class: "btn btn-warning w-100" do %>
165
+ <i class="fas fa-cog"></i> Configuration
166
+ <% end %>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </div>
173
+
174
+ <% if @document_types.any? %>
175
+ <%= content_for :javascript do %>
176
+ <script>
177
+ document.addEventListener('DOMContentLoaded', function() {
178
+ const ctx = document.getElementById('documentTypesChart').getContext('2d');
179
+ const documentTypesChart = new Chart(ctx, {
180
+ type: 'doughnut',
181
+ data: {
182
+ labels: <%= @document_types.keys.to_json.html_safe %>,
183
+ datasets: [{
184
+ data: <%= @document_types.values.to_json.html_safe %>,
185
+ backgroundColor: [
186
+ '#FF6384',
187
+ '#36A2EB',
188
+ '#FFCE56',
189
+ '#4BC0C0',
190
+ '#9966FF',
191
+ '#FF9F40'
192
+ ]
193
+ }]
194
+ },
195
+ options: {
196
+ responsive: true,
197
+ maintainAspectRatio: false,
198
+ plugins: {
199
+ legend: {
200
+ position: 'bottom'
201
+ }
202
+ }
203
+ }
204
+ });
205
+ });
206
+ </script>
207
+ <% end %>
208
+ <% end %>
@@ -0,0 +1,91 @@
1
+ <% content_for(:title, "Edit Document: #{@document.title}") %>
2
+
3
+ <div class="container-fluid mt-4">
4
+ <div class="row">
5
+ <div class="col-md-8 mx-auto">
6
+ <div class="card">
7
+ <div class="card-header">
8
+ <h4 class="card-title mb-0">
9
+ <i class="fas fa-edit"></i> Edit Document
10
+ </h4>
11
+ </div>
12
+ <div class="card-body">
13
+ <%= form_with model: @document, url: ragdoll.document_path(@document), local: true do |form| %>
14
+ <% if @document.errors.any? %>
15
+ <div class="alert alert-danger" role="alert">
16
+ <h6 class="alert-heading">Please fix the following errors:</h6>
17
+ <ul class="mb-0">
18
+ <% @document.errors.full_messages.each do |message| %>
19
+ <li><%= message %></li>
20
+ <% end %>
21
+ </ul>
22
+ </div>
23
+ <% end %>
24
+
25
+ <div class="mb-3">
26
+ <%= form.label :title, class: "form-label" %>
27
+ <%= form.text_field :title, class: "form-control", placeholder: "Enter document title" %>
28
+ </div>
29
+
30
+ <div class="mb-3">
31
+ <%= form.label :summary, class: "form-label" %>
32
+ <%= form.text_area :summary, class: "form-control", rows: 3, placeholder: "Enter document summary (optional)" %>
33
+ </div>
34
+
35
+ <div class="mb-3">
36
+ <%= form.label :keywords, class: "form-label" %>
37
+ <%= form.text_field :keywords, class: "form-control", placeholder: "Enter keywords separated by commas" %>
38
+ <small class="form-text text-muted">Separate keywords with commas</small>
39
+ </div>
40
+
41
+ <div class="mb-3">
42
+ <%= form.label :status, class: "form-label" %>
43
+ <%= form.select :status, options_for_select([
44
+ ['Pending', 'pending'],
45
+ ['Processing', 'processing'],
46
+ ['Processed', 'processed'],
47
+ ['Failed', 'failed']
48
+ ], @document.status), {}, class: "form-select" %>
49
+ </div>
50
+
51
+ <% if @document.location.present? %>
52
+ <div class="mb-3">
53
+ <label class="form-label">File Location</label>
54
+ <div class="input-group">
55
+ <input type="text" class="form-control" value="<%= @document.location %>" readonly>
56
+ <span class="input-group-text"><i class="fas fa-lock"></i></span>
57
+ </div>
58
+ <small class="form-text text-muted">File location cannot be changed</small>
59
+ </div>
60
+ <% end %>
61
+
62
+ <% if @document.metadata.present? %>
63
+ <div class="mb-3">
64
+ <label class="form-label">Metadata</label>
65
+ <div class="card bg-light">
66
+ <div class="card-body">
67
+ <% @document.metadata.each do |key, value| %>
68
+ <div class="row mb-2">
69
+ <div class="col-sm-4">
70
+ <small class="text-muted"><%= key.humanize %>:</small>
71
+ </div>
72
+ <div class="col-sm-8">
73
+ <small><%= value %></small>
74
+ </div>
75
+ </div>
76
+ <% end %>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ <% end %>
81
+
82
+ <div class="d-flex justify-content-between">
83
+ <%= link_to "Cancel", ragdoll.document_path(@document), class: "btn btn-secondary" %>
84
+ <%= form.submit "Update Document", class: "btn btn-primary" %>
85
+ </div>
86
+ <% end %>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </div>
91
+ </div>
@@ -0,0 +1,305 @@
1
+ <% content_for :title, "Documents - 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-file-alt"></i> Documents</h1>
7
+ <%= link_to "Add Document", ragdoll.new_document_path, class: "btn btn-primary" %>
8
+ </div>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="row mb-4">
13
+ <div class="col-12">
14
+ <div class="card">
15
+ <div class="card-header">
16
+ <h5><i class="fas fa-filter"></i> Filters</h5>
17
+ </div>
18
+ <div class="card-body">
19
+ <%= form_with url: ragdoll.documents_path, method: :get, local: true, class: "row g-3" do |form| %>
20
+ <div class="col-md-3">
21
+ <%= form.text_field :search, placeholder: "Search documents...", value: params[:search], class: "form-control" %>
22
+ </div>
23
+ <div class="col-md-3">
24
+ <%= form.select :document_type, options_for_select([["All Types", ""]] + @document_types.map { |type| [type.titleize, type] }, params[:document_type]), {}, { class: "form-select", title: "Document Type filtering is deprecated in unified text-based architecture" } %>
25
+ <small class="form-text text-warning">
26
+ <i class="fas fa-exclamation-triangle"></i> Note: All media types are now converted to text for unified search
27
+ </small>
28
+ </div>
29
+ <div class="col-md-3">
30
+ <%= form.select :status, options_for_select([["All Statuses", ""]] + @statuses.map { |status| [status.titleize, status] }, params[:status]), {}, { class: "form-select" } %>
31
+ </div>
32
+ <div class="col-md-3">
33
+ <%= form.submit "Filter", class: "btn btn-outline-primary" %>
34
+ <%= link_to "Clear", ragdoll.documents_path, class: "btn btn-outline-secondary" %>
35
+ </div>
36
+ <% end %>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ <% if @documents.any? %>
43
+ <div class="row">
44
+ <div class="col-12">
45
+ <div class="card">
46
+ <div class="card-header">
47
+ <div class="d-flex justify-content-between align-items-center">
48
+ <h5 class="mb-0"><i class="fas fa-list"></i> Document List</h5>
49
+ <div class="d-flex align-items-center gap-2">
50
+ <!-- View Toggle Buttons -->
51
+ <div class="btn-group btn-group-sm me-3" role="group" aria-label="View toggle">
52
+ <button type="button" class="btn btn-outline-secondary" id="card-view-btn" onclick="switchToCardView()">
53
+ <i class="fas fa-th-large"></i> Cards
54
+ </button>
55
+ <button type="button" class="btn btn-outline-secondary" id="table-view-btn" onclick="switchToTableView()">
56
+ <i class="fas fa-table"></i> Table
57
+ </button>
58
+ </div>
59
+
60
+ <!-- Bulk Action Buttons -->
61
+ <button class="btn btn-outline-danger btn-sm" onclick="bulkDelete()">
62
+ <i class="fas fa-trash"></i> Delete Selected
63
+ </button>
64
+ <button class="btn btn-outline-warning btn-sm" onclick="bulkReprocess()">
65
+ <i class="fas fa-sync"></i> Reprocess Selected
66
+ </button>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ <div class="card-body">
71
+ <!-- Card View -->
72
+ <div id="card-view" style="display: block;">
73
+ <%= render Ragdoll::DocumentListComponent.new(documents: @documents) %>
74
+ </div>
75
+
76
+ <!-- Table View -->
77
+ <div id="table-view" style="display: none;">
78
+ <%= render Ragdoll::DocumentTableComponent.new(documents: @documents) %>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ <% else %>
85
+ <div class="row">
86
+ <div class="col-12">
87
+ <%= render Ragdoll::EmptyStateComponent.new(
88
+ title: "No documents found",
89
+ message: "Get started by adding your first document to the system.",
90
+ icon: "fas fa-file-alt",
91
+ action_path: ragdoll.new_document_path,
92
+ action_text: "Add Document"
93
+ ) %>
94
+ </div>
95
+ </div>
96
+ <% end %>
97
+
98
+ <%= content_for :javascript do %>
99
+ <script>
100
+ // View switching functions
101
+ function switchToCardView() {
102
+ document.getElementById('card-view').style.display = 'block';
103
+ document.getElementById('table-view').style.display = 'none';
104
+ document.getElementById('card-view-btn').classList.add('active');
105
+ document.getElementById('table-view-btn').classList.remove('active');
106
+ localStorage.setItem('ragdoll-document-view', 'card');
107
+ }
108
+
109
+ function switchToTableView() {
110
+ document.getElementById('card-view').style.display = 'none';
111
+ document.getElementById('table-view').style.display = 'block';
112
+ document.getElementById('table-view-btn').classList.add('active');
113
+ document.getElementById('card-view-btn').classList.remove('active');
114
+ localStorage.setItem('ragdoll-document-view', 'table');
115
+ }
116
+
117
+ // Attach event listeners to document checkboxes
118
+ function attachCheckboxListeners() {
119
+ document.querySelectorAll('.document-checkbox').forEach(checkbox => {
120
+ checkbox.removeEventListener('change', updateSelectAllState); // Remove first to avoid duplicates
121
+ checkbox.addEventListener('change', updateSelectAllState);
122
+ });
123
+ updateSelectAllState(); // Initialize state
124
+ }
125
+
126
+ // Initialize view based on saved preference
127
+ document.addEventListener('DOMContentLoaded', function() {
128
+ const savedView = localStorage.getItem('ragdoll-document-view') || 'card';
129
+ if (savedView === 'table') {
130
+ switchToTableView();
131
+ } else {
132
+ switchToCardView();
133
+ }
134
+
135
+ // Attach listeners after view is set
136
+ setTimeout(attachCheckboxListeners, 100);
137
+ });
138
+
139
+ function toggleAll(source) {
140
+ // Get all document checkboxes
141
+ const checkboxes = document.querySelectorAll('.document-checkbox');
142
+ const selectAllCard = document.getElementById('select-all');
143
+ const selectAllTable = document.getElementById('select-all-table');
144
+
145
+ // Set all checkboxes to match the source checkbox state
146
+ checkboxes.forEach(checkbox => {
147
+ checkbox.checked = source.checked;
148
+ });
149
+
150
+ // Sync both select-all checkboxes
151
+ if (selectAllCard) {
152
+ selectAllCard.checked = source.checked;
153
+ selectAllCard.indeterminate = false;
154
+ }
155
+ if (selectAllTable) {
156
+ selectAllTable.checked = source.checked;
157
+ selectAllTable.indeterminate = false;
158
+ }
159
+ }
160
+
161
+ function updateSelectAllState() {
162
+ // Get all document checkboxes
163
+ const checkboxes = document.querySelectorAll('.document-checkbox');
164
+ const selectAllCard = document.getElementById('select-all');
165
+ const selectAllTable = document.getElementById('select-all-table');
166
+
167
+ if (checkboxes.length === 0) return;
168
+
169
+ // Count checked boxes
170
+ const checkedCount = Array.from(checkboxes).filter(cb => cb.checked).length;
171
+
172
+ // Determine state
173
+ let isChecked = false;
174
+ let isIndeterminate = false;
175
+
176
+ if (checkedCount === 0) {
177
+ // None checked
178
+ isChecked = false;
179
+ isIndeterminate = false;
180
+ } else if (checkedCount === checkboxes.length) {
181
+ // All checked
182
+ isChecked = true;
183
+ isIndeterminate = false;
184
+ } else {
185
+ // Some checked
186
+ isChecked = false;
187
+ isIndeterminate = true;
188
+ }
189
+
190
+ // Update both select-all checkboxes
191
+ if (selectAllCard) {
192
+ selectAllCard.checked = isChecked;
193
+ selectAllCard.indeterminate = isIndeterminate;
194
+ }
195
+ if (selectAllTable) {
196
+ selectAllTable.checked = isChecked;
197
+ selectAllTable.indeterminate = isIndeterminate;
198
+ }
199
+ }
200
+
201
+
202
+ function reprocessDocument(documentId) {
203
+ if (confirm('Are you sure you want to reprocess this document?')) {
204
+ // Create a form with proper CSRF token for individual reprocess action
205
+ const form = document.createElement('form');
206
+ form.method = 'POST';
207
+ form.action = `<%= ragdoll.documents_path %>/${documentId}/reprocess`;
208
+
209
+ // Add CSRF token
210
+ const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
211
+ const csrfInput = document.createElement('input');
212
+ csrfInput.type = 'hidden';
213
+ csrfInput.name = 'authenticity_token';
214
+ csrfInput.value = csrfToken;
215
+ form.appendChild(csrfInput);
216
+
217
+ document.body.appendChild(form);
218
+ form.submit();
219
+ }
220
+ }
221
+
222
+ function bulkDelete() {
223
+ const checkedBoxes = document.querySelectorAll('.document-checkbox:checked');
224
+ if (checkedBoxes.length === 0) {
225
+ alert('Please select at least one document to delete.');
226
+ return;
227
+ }
228
+
229
+ if (confirm(`Are you sure you want to delete ${checkedBoxes.length} selected documents?`)) {
230
+ // Create a new form with proper CSRF token for delete action
231
+ const form = document.createElement('form');
232
+ form.method = 'POST';
233
+ form.action = '<%= ragdoll.bulk_delete_documents_path %>';
234
+
235
+ // Add CSRF token
236
+ const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
237
+ const csrfInput = document.createElement('input');
238
+ csrfInput.type = 'hidden';
239
+ csrfInput.name = 'authenticity_token';
240
+ csrfInput.value = csrfToken;
241
+ form.appendChild(csrfInput);
242
+
243
+ // Add selected document IDs
244
+ checkedBoxes.forEach(checkbox => {
245
+ const input = document.createElement('input');
246
+ input.type = 'hidden';
247
+ input.name = 'document_ids[]';
248
+ input.value = checkbox.value;
249
+ form.appendChild(input);
250
+ });
251
+
252
+ document.body.appendChild(form);
253
+ form.submit();
254
+ }
255
+ }
256
+
257
+ function bulkReprocess() {
258
+ const checkedBoxes = document.querySelectorAll('.document-checkbox:checked');
259
+ if (checkedBoxes.length === 0) {
260
+ alert('Please select at least one document to reprocess.');
261
+ return;
262
+ }
263
+
264
+ if (confirm(`Are you sure you want to reprocess ${checkedBoxes.length} selected documents?`)) {
265
+ // Create a new form with proper CSRF token for reprocess action
266
+ const form = document.createElement('form');
267
+ form.method = 'POST';
268
+ form.action = '<%= ragdoll.bulk_reprocess_documents_path %>';
269
+
270
+ // Add CSRF token
271
+ const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
272
+ const csrfInput = document.createElement('input');
273
+ csrfInput.type = 'hidden';
274
+ csrfInput.name = 'authenticity_token';
275
+ csrfInput.value = csrfToken;
276
+ form.appendChild(csrfInput);
277
+
278
+ // Add selected document IDs
279
+ checkedBoxes.forEach(checkbox => {
280
+ const input = document.createElement('input');
281
+ input.type = 'hidden';
282
+ input.name = 'document_ids[]';
283
+ input.value = checkbox.value;
284
+ form.appendChild(input);
285
+ });
286
+
287
+ document.body.appendChild(form);
288
+ form.submit();
289
+ }
290
+ }
291
+
292
+ // Initialize Bootstrap tooltips
293
+ document.addEventListener('DOMContentLoaded', function () {
294
+ var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
295
+ var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
296
+ return new bootstrap.Tooltip(tooltipTriggerEl, {
297
+ delay: { "show": 500, "hide": 100 },
298
+ placement: 'top',
299
+ boundary: 'viewport',
300
+ fallbackPlacements: ['top', 'bottom']
301
+ });
302
+ });
303
+ });
304
+ </script>
305
+ <% end %>