drf-to-mkdoc 0.2.3__py3-none-any.whl → 0.3.0__py3-none-any.whl

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.

Potentially problematic release.


This version of drf-to-mkdoc might be problematic. Click here for more details.

Files changed (40) hide show
  1. drf_to_mkdoc/conf/defaults.py +1 -0
  2. drf_to_mkdoc/conf/settings.py +1 -0
  3. drf_to_mkdoc/management/commands/build_model_docs.py +10 -1
  4. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/field-sections-loader.js +29 -0
  5. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/query-parameters-loader.js +16 -0
  6. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/field-extractor.js +200 -0
  7. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/form-manager.js +307 -14
  8. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/main.js +39 -11
  9. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/modal.js +298 -18
  10. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/query-parameters-extractor.js +94 -0
  11. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/request-executor.js +278 -62
  12. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/response-modal.js +173 -0
  13. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/suggestions.js +59 -152
  14. drf_to_mkdoc/static/drf-to-mkdoc/javascripts/try-out/tabs.js +52 -9
  15. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/badges.css +13 -5
  16. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/theme-toggle.css +297 -25
  17. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/try-out/fab.css +204 -0
  18. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/try-out/response.css +323 -0
  19. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/endpoints/try-out/variables.css +139 -0
  20. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/field-sections.css +136 -0
  21. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/form.css +539 -0
  22. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/modal.css +239 -17
  23. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/response.css +503 -43
  24. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/tabs.css +71 -19
  25. drf_to_mkdoc/static/drf-to-mkdoc/stylesheets/try-out/variables.css +71 -15
  26. drf_to_mkdoc/templates/endpoints/detail/request_body.html +2 -0
  27. drf_to_mkdoc/templates/er_diagrams/app.html +26 -0
  28. drf_to_mkdoc/templates/er_diagrams/index.html +14 -0
  29. drf_to_mkdoc/templates/er_diagrams/main.html +22 -0
  30. drf_to_mkdoc/templates/try-out/fab.html +67 -3
  31. drf_to_mkdoc/templates/try-out/form.html +221 -74
  32. drf_to_mkdoc/templates/try-out/modal.html +75 -7
  33. drf_to_mkdoc/templates/try-out/response-modal.html +138 -9
  34. drf_to_mkdoc/utils/endpoint_detail_generator.py +1 -0
  35. drf_to_mkdoc/utils/er_diagram_generator.py +230 -0
  36. {drf_to_mkdoc-0.2.3.dist-info → drf_to_mkdoc-0.3.0.dist-info}/METADATA +89 -10
  37. {drf_to_mkdoc-0.2.3.dist-info → drf_to_mkdoc-0.3.0.dist-info}/RECORD +40 -27
  38. {drf_to_mkdoc-0.2.3.dist-info → drf_to_mkdoc-0.3.0.dist-info}/WHEEL +0 -0
  39. {drf_to_mkdoc-0.2.3.dist-info → drf_to_mkdoc-0.3.0.dist-info}/licenses/LICENSE +0 -0
  40. {drf_to_mkdoc-0.2.3.dist-info → drf_to_mkdoc-0.3.0.dist-info}/top_level.txt +0 -0
@@ -1,61 +1,259 @@
1
1
  // Request execution functionality
2
2
  const RequestExecutor = {
3
- async executeRequest() {
4
- const executeBtn = document.getElementById('executeBtn');
5
- if (!executeBtn) return;
6
-
7
- // Validate required parameters
8
- const emptyParams = FormManager.validateRequiredParams();
9
- if (emptyParams.length > 0) {
10
- // Switch to parameters tab if we're on a different tab
11
- const activeTab = document.querySelector('.try-out-form .tab.active');
12
- if (activeTab && activeTab.getAttribute('data-tab') !== 'parameters') {
13
- const parametersTab = document.querySelector('.try-out-form .tab[data-tab="parameters"]');
3
+ // Show validation error in the appropriate section
4
+ showValidationError: function(message, section) {
5
+ let errorContainer;
6
+
7
+ if (section === 'json') {
8
+ // Show error in JSON validation status
9
+ errorContainer = document.querySelector('.validation-status');
10
+ if (errorContainer) {
11
+ errorContainer.textContent = ' ' + message;
12
+ errorContainer.className = 'validation-status invalid';
13
+ }
14
+ } else if (section === 'parameters') {
15
+ // Show error in parameters section
16
+ const parametersTab = document.querySelector('[data-tab="parameters"]');
14
17
  if (parametersTab) {
15
- TabManager.switchTab(parametersTab);
16
- }
18
+ window.TabManager?.switchTab(parametersTab);
17
19
  }
18
20
 
19
- this.showValidationError(`Please fill in the required parameters: ${emptyParams.join(', ')}`);
21
+ // Create or update error message
22
+ errorContainer = document.querySelector('#parametersError');
23
+ if (!errorContainer) {
24
+ errorContainer = document.createElement('div');
25
+ errorContainer.id = 'parametersError';
26
+ errorContainer.className = 'error-message';
27
+ const parametersSection = document.querySelector('#parametersTab .form-section');
28
+ if (parametersSection) {
29
+ parametersSection.insertBefore(errorContainer, parametersSection.firstChild);
30
+ }
31
+ }
32
+ errorContainer.textContent = message;
33
+ errorContainer.style.display = 'block';
34
+ }
35
+ },
36
+
37
+ // Clear validation errors
38
+ clearValidationErrors: function() {
39
+ // Clear JSON validation status
40
+ const jsonStatus = document.querySelector('.validation-status');
41
+ if (jsonStatus) {
42
+ jsonStatus.textContent = '';
43
+ jsonStatus.className = 'validation-status';
44
+ }
45
+
46
+ // Clear parameters error
47
+ const paramsError = document.querySelector('#parametersError');
48
+ if (paramsError) {
49
+ paramsError.style.display = 'none';
50
+ }
51
+
52
+ // Clear all input validation states
53
+ document.querySelectorAll('.error').forEach(el => {
54
+ el.classList.remove('error');
55
+ });
56
+ document.querySelectorAll('.validation-message').forEach(msg => {
57
+ msg.style.display = 'none';
58
+ });
59
+ },
60
+
61
+ // Form validation
62
+ validateInput: function(input) {
63
+ const isValid = input.value.trim() !== '';
64
+ const validationMessage = input.parentElement.querySelector('.validation-message');
65
+
66
+ if (!isValid) {
67
+ input.classList.add('error');
68
+ if (validationMessage) {
69
+ validationMessage.textContent = 'This field is required';
70
+ validationMessage.style.display = 'block';
71
+ }
72
+ } else {
73
+ input.classList.remove('error');
74
+ if (validationMessage) {
75
+ validationMessage.textContent = '';
76
+ validationMessage.style.display = 'none';
77
+ }
78
+ }
79
+
80
+ return isValid;
81
+ },
82
+
83
+ // Build complete request data
84
+ buildRequestData: function() {
85
+ const baseUrl = document.getElementById('baseUrl')?.value || '';
86
+ const pathDisplay = document.querySelector('.path-display')?.textContent || '';
87
+
88
+ // Build full URL
89
+ let fullUrl = baseUrl + pathDisplay;
90
+
91
+ // Replace path parameters
92
+ const pathParams = {};
93
+ document.querySelectorAll('#pathParams input').forEach(input => {
94
+ const param = input.dataset.param;
95
+ if (param && input.value) {
96
+ pathParams[param] = input.value;
97
+ fullUrl = fullUrl.replace(`{${param}}`, encodeURIComponent(input.value));
98
+ }
99
+ });
100
+
101
+ // Collect query parameters
102
+ const queryParams = {};
103
+ document.querySelectorAll('#queryParams .parameter-item').forEach(item => {
104
+ const nameInput = item.querySelector('.name-input');
105
+ const valueInput = item.querySelector('.value-input');
106
+ if (nameInput?.value && valueInput?.value) {
107
+ queryParams[nameInput.value] = valueInput.value;
108
+ }
109
+ });
110
+
111
+ // Add query parameters to URL
112
+ if (Object.keys(queryParams).length > 0) {
113
+ const queryString = new URLSearchParams(queryParams).toString();
114
+ fullUrl += (fullUrl.includes('?') ? '&' : '?') + queryString;
115
+ }
116
+
117
+ // Collect headers
118
+ const headers = {};
119
+ document.querySelectorAll('#requestHeaders .header-item').forEach(item => {
120
+ const nameInput = item.querySelector('.name-input');
121
+ const valueInput = item.querySelector('.value-input');
122
+ if (nameInput?.value && valueInput?.value) {
123
+ headers[nameInput.value] = valueInput.value;
124
+ }
125
+ });
126
+
127
+ // Add default headers if not already set
128
+ if (!headers['Accept']) {
129
+ headers['Accept'] = '*/*';
130
+ }
131
+ if (!headers['User-Agent']) {
132
+ headers['User-Agent'] = navigator.userAgent;
133
+ }
134
+ if (!headers['Accept-Language']) {
135
+ headers['Accept-Language'] = navigator.language || 'en-US,en;q=0.9';
136
+ }
137
+ if (!headers['Accept-Encoding']) {
138
+ headers['Accept-Encoding'] = 'gzip, deflate, br';
139
+ }
140
+ if (!headers['Connection']) {
141
+ headers['Connection'] = 'keep-alive';
142
+ }
143
+ if (!headers['Cache-Control']) {
144
+ headers['Cache-Control'] = 'no-cache';
145
+ }
146
+
147
+ // Add cookies if available
148
+ if (document.cookie) {
149
+ headers['Cookie'] = document.cookie;
150
+ }
151
+
152
+ // Get request body
153
+ const bodyEditor = document.getElementById('requestBody');
154
+ let body = null;
155
+ if (bodyEditor?.value.trim()) {
156
+ try {
157
+ body = JSON.parse(bodyEditor.value);
158
+ } catch (e) {
159
+ body = bodyEditor.value;
160
+ }
161
+ }
162
+
163
+ // Get method
164
+ const methodBadge = document.querySelector('.method-badge');
165
+ const method = methodBadge?.dataset.method || 'GET';
166
+
167
+ return {
168
+ url: fullUrl,
169
+ method,
170
+ headers,
171
+ body,
172
+ pathParams,
173
+ queryParams
174
+ };
175
+ },
176
+ async executeRequest() {
177
+ const executeBtn = document.querySelector('[data-action="send"], .primary-button, .primary-btn, #executeBtn');
178
+ if (!executeBtn) {
179
+ console.warn('Execute button not found');
180
+ return;
181
+ }
182
+
183
+ // Validate required fields
184
+ const requiredInputs = document.querySelectorAll('#pathParams input[required]');
185
+ let emptyFields = [];
186
+
187
+ requiredInputs.forEach(input => {
188
+ if (!input.value.trim()) {
189
+ const paramName = input.dataset.param || 'parameter';
190
+ emptyFields.push(paramName);
191
+ this.validateInput(input);
192
+ }
193
+ });
194
+
195
+ if (emptyFields.length > 0) {
196
+ this.showValidationError(`Please fill in required fields: ${emptyFields.join(', ')}`, 'parameters');
20
197
  return;
21
198
  }
22
199
 
23
200
  // Show loading state
24
201
  this.setLoadingState(executeBtn, true);
25
202
 
203
+ const startTime = Date.now();
204
+
26
205
  try {
27
- const startTime = Date.now();
28
- const url = FormManager.buildRequestUrl();
29
- const headers = FormManager.getRequestHeaders();
30
- const body = FormManager.getRequestBody();
31
- const method = document.querySelector('.try-out-form')?.getAttribute('data-method') || 'GET';
206
+ const requestData = this.buildRequestData();
32
207
 
33
208
  const requestOptions = {
34
- method: method.toUpperCase(),
35
- headers: headers
209
+ method: requestData.method.toUpperCase(),
210
+ headers: requestData.headers
36
211
  };
37
212
 
38
213
  // Add body for non-GET requests
39
- if (body && !['GET', 'HEAD'].includes(method.toUpperCase())) {
40
- if (typeof body === 'string') {
41
- requestOptions.body = body;
214
+ if (requestData.body && !['GET', 'HEAD'].includes(requestData.method.toUpperCase())) {
215
+ if (typeof requestData.body === 'string') {
216
+ requestOptions.body = requestData.body;
42
217
  } else {
43
- requestOptions.body = JSON.stringify(body);
44
- if (!headers['Content-Type']) {
218
+ requestOptions.body = JSON.stringify(requestData.body);
219
+ if (!requestData.headers['Content-Type']) {
45
220
  requestOptions.headers['Content-Type'] = 'application/json';
46
221
  }
47
222
  }
48
223
  }
49
224
 
50
- const response = await fetch(url, requestOptions);
225
+ // Show response section
226
+ const responseSection = document.querySelector('.response-section');
227
+ if (responseSection) {
228
+ responseSection.hidden = false;
229
+ }
230
+
231
+ const response = await fetch(requestData.url, requestOptions);
51
232
  const responseTime = Date.now() - startTime;
52
233
  const responseText = await response.text();
53
234
 
54
- ModalManager.showResponseModal(response.status, responseText, responseTime);
235
+ // Convert response headers to object
236
+ const responseHeaders = {};
237
+ response.headers.forEach((value, key) => {
238
+ // Handle multiple headers with the same name (like Set-Cookie)
239
+ if (responseHeaders[key]) {
240
+ if (Array.isArray(responseHeaders[key])) {
241
+ responseHeaders[key].push(value);
242
+ } else {
243
+ responseHeaders[key] = [responseHeaders[key], value];
244
+ }
245
+ } else {
246
+ responseHeaders[key] = value;
247
+ }
248
+ });
249
+
250
+ ModalManager.showResponseModal(response.status, responseText, responseTime, responseHeaders, requestData.headers);
55
251
 
56
252
  } catch (error) {
57
- let errorMessage = error.message || 'Unknown error occurred';
58
- ModalManager.showResponseModal('Error', errorMessage);
253
+ console.error('Request failed:', error);
254
+ const requestData = this.buildRequestData();
255
+ const errorTime = Date.now() - startTime;
256
+ ModalManager.showResponseModal('Error', error.message || 'Unknown error occurred', errorTime, null, requestData.headers);
59
257
  } finally {
60
258
  this.setLoadingState(executeBtn, false);
61
259
  }
@@ -63,49 +261,67 @@ const RequestExecutor = {
63
261
 
64
262
  setLoadingState(button, loading) {
65
263
  button.disabled = loading;
66
- button.innerHTML = '';
67
264
 
68
265
  if (loading) {
69
- const spinner = document.createElement('div');
70
- spinner.className = 'spinner';
71
- const text = document.createTextNode(' Sending...');
72
- button.appendChild(spinner);
73
- button.appendChild(text);
266
+ button.classList.add('loading');
267
+ const spinner = button.querySelector('.loading-spinner');
268
+ if (spinner) {
269
+ spinner.style.display = 'inline-block';
270
+ }
74
271
  } else {
75
- const playIcon = document.createElement('span');
76
- playIcon.textContent = '';
77
- const text = document.createTextNode(' Execute Request');
78
- button.appendChild(playIcon);
79
- button.appendChild(text);
272
+ button.classList.remove('loading');
273
+ const spinner = button.querySelector('.loading-spinner');
274
+ if (spinner) {
275
+ spinner.style.display = 'none';
276
+ }
80
277
  }
81
278
  },
82
279
 
83
- showValidationError(message) {
84
- // Create or update validation error display
85
- let errorDiv = document.getElementById('validation-error');
86
- if (!errorDiv) {
87
- errorDiv = document.createElement('div');
88
- errorDiv.id = 'validation-error';
89
- errorDiv.className = 'error-message show';
90
-
91
- const executeBtn = document.getElementById('executeBtn');
92
- if (executeBtn) {
93
- executeBtn.parentNode.insertBefore(errorDiv, executeBtn);
94
- }
280
+ // JSON formatting and validation
281
+ formatJson: function() {
282
+ const editor = document.getElementById('requestBody');
283
+ if (!editor) return;
284
+
285
+ try {
286
+ const formatted = JSON.stringify(JSON.parse(editor.value), null, 2);
287
+ editor.value = formatted;
288
+ this.validateJson();
289
+ } catch (e) {
290
+ this.showValidationError('Invalid JSON format', 'json');
95
291
  }
292
+ },
293
+
294
+ validateJson: function() {
295
+ const editor = document.getElementById('requestBody');
296
+ const status = document.querySelector('.validation-status');
96
297
 
97
- errorDiv.textContent = message;
98
- errorDiv.classList.add('show');
99
-
100
- // Auto-hide after 5 seconds
101
- setTimeout(() => {
102
- errorDiv.classList.remove('show');
103
- }, 5000);
104
- }
298
+ if (!editor || !status) return true;
299
+
300
+ if (!editor.value.trim()) {
301
+ status.textContent = '';
302
+ status.className = 'validation-status';
303
+ return true;
304
+ }
305
+
306
+ try {
307
+ JSON.parse(editor.value);
308
+ status.textContent = '✓ Valid JSON';
309
+ status.className = 'validation-status valid';
310
+ return true;
311
+ } catch (e) {
312
+ status.textContent = '✗ ' + e.message;
313
+ status.className = 'validation-status invalid';
314
+ return false;
315
+ }
316
+ },
317
+
318
+ // This method is now handled by the main showValidationError method above
105
319
  };
106
320
 
107
- // Global function for onclick handlers
321
+ // Global functions for onclick handlers and backward compatibility
108
322
  window.executeRequest = () => RequestExecutor.executeRequest();
323
+ window.formatJson = () => RequestExecutor.formatJson();
324
+ window.validateJson = () => RequestExecutor.validateJson();
109
325
 
110
326
  // Export for global access
111
327
  window.RequestExecutor = RequestExecutor;
@@ -0,0 +1,173 @@
1
+ // Response modal functionality
2
+ const ResponseModalManager = {
3
+ init: function() {
4
+ // Tab switching
5
+ const tabs = document.querySelectorAll('.response-tabs .tab');
6
+ tabs.forEach(tab => {
7
+ tab.addEventListener('click', () => this.switchTab(tab));
8
+ });
9
+
10
+ // Initialize syntax highlighting
11
+ this.initializePrism();
12
+ },
13
+
14
+ switchTab: function(tab) {
15
+ if (!tab) return;
16
+
17
+ // Remove active class from all tabs and content
18
+ document.querySelectorAll('.response-tabs .tab').forEach(t => {
19
+ if (t && t.classList) {
20
+ t.classList.remove('active');
21
+ t.setAttribute('aria-selected', 'false');
22
+ }
23
+ });
24
+ document.querySelectorAll('.tab-content').forEach(c => {
25
+ if (c && c.classList) {
26
+ c.classList.remove('active');
27
+ }
28
+ });
29
+
30
+ // Add active class to clicked tab and its content
31
+ if (tab.classList) {
32
+ tab.classList.add('active');
33
+ tab.setAttribute('aria-selected', 'true');
34
+ }
35
+ const contentId = tab.getAttribute('aria-controls');
36
+ const contentElement = document.getElementById(contentId);
37
+ if (contentElement && contentElement.classList) {
38
+ contentElement.classList.add('active');
39
+ }
40
+ },
41
+
42
+ copyResponse: function() {
43
+ const responseBody = document.getElementById('modalResponseBody');
44
+ if (!responseBody) return;
45
+
46
+ const responseText = responseBody.textContent;
47
+ if (navigator.clipboard) {
48
+ navigator.clipboard.writeText(responseText).then(() => {
49
+ this.showToast('Response copied to clipboard');
50
+ }).catch(() => {
51
+ this.showToast('Failed to copy response');
52
+ });
53
+ } else {
54
+ this.showToast('Clipboard not supported');
55
+ }
56
+ },
57
+
58
+ downloadResponse: function() {
59
+ const responseBody = document.getElementById('modalResponseBody');
60
+ if (!responseBody) return;
61
+
62
+ const responseText = responseBody.textContent;
63
+ const blob = new Blob([responseText], { type: 'application/json' });
64
+ const url = URL.createObjectURL(blob);
65
+ const a = document.createElement('a');
66
+ a.href = url;
67
+ a.download = 'response.json';
68
+ document.body.appendChild(a);
69
+ a.click();
70
+ document.body.removeChild(a);
71
+ URL.revokeObjectURL(url);
72
+ this.showToast('Response downloaded');
73
+ },
74
+
75
+ formatResponse: function() {
76
+ try {
77
+ const responseBody = document.getElementById('modalResponseBody');
78
+ if (!responseBody) return;
79
+
80
+ const content = responseBody.textContent;
81
+ if (!content.trim()) return;
82
+
83
+ try {
84
+ const parsed = JSON.parse(content);
85
+ responseBody.textContent = JSON.stringify(parsed, null, 2);
86
+ } catch (e) {
87
+ // If not JSON, just return as is
88
+ console.log('Response is not valid JSON, skipping formatting');
89
+ }
90
+ } catch (error) {
91
+ console.error('Error formatting response:', error);
92
+ }
93
+ },
94
+
95
+ collapseAll: function() {
96
+ const responseBody = document.getElementById('modalResponseBody');
97
+ if (!responseBody) return;
98
+
99
+ // Simple collapse - replace newlines with spaces for basic collapse
100
+ const content = responseBody.textContent;
101
+ if (content) {
102
+ responseBody.textContent = content.replace(/\n\s*/g, ' ').trim();
103
+ }
104
+ },
105
+
106
+ expandAll: function() {
107
+ const responseBody = document.getElementById('modalResponseBody');
108
+ if (!responseBody) return;
109
+
110
+ const content = responseBody.textContent;
111
+ if (!content.trim()) return;
112
+
113
+ try {
114
+ // Try to format as JSON first
115
+ const parsed = JSON.parse(content);
116
+ responseBody.textContent = JSON.stringify(parsed, null, 2);
117
+ } catch (e) {
118
+ // If not JSON, just restore original formatting
119
+ responseBody.textContent = content;
120
+ }
121
+ },
122
+
123
+ showToast: function(message) {
124
+ // Create toast element
125
+ const toast = document.createElement('div');
126
+ toast.className = 'toast';
127
+ toast.textContent = message;
128
+ toast.style.cssText = `
129
+ position: fixed;
130
+ top: 20px;
131
+ right: 20px;
132
+ background: #333;
133
+ color: white;
134
+ padding: 12px 20px;
135
+ border-radius: 4px;
136
+ z-index: 10000;
137
+ font-size: 14px;
138
+ opacity: 0;
139
+ transition: opacity 0.3s ease;
140
+ `;
141
+
142
+ document.body.appendChild(toast);
143
+
144
+ // Show toast
145
+ setTimeout(() => {
146
+ toast.style.opacity = '1';
147
+ }, 10);
148
+
149
+ // Hide toast after 3 seconds
150
+ setTimeout(() => {
151
+ toast.style.opacity = '0';
152
+ setTimeout(() => {
153
+ if (toast.parentNode) {
154
+ toast.parentNode.removeChild(toast);
155
+ }
156
+ }, 300);
157
+ }, 3000);
158
+ },
159
+
160
+ initializePrism: function() {
161
+ // Basic syntax highlighting placeholder
162
+ // This would integrate with Prism.js if available
163
+ console.log('Syntax highlighting initialized');
164
+ }
165
+ };
166
+
167
+ // Export for global access
168
+ window.ResponseModalManager = ResponseModalManager;
169
+
170
+ // Initialize when DOM is loaded
171
+ document.addEventListener('DOMContentLoaded', function() {
172
+ ResponseModalManager.init();
173
+ });