@embeddables/cli 0.1.0

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 (173) hide show
  1. package/README.md +116 -0
  2. package/bin/embeddables.mjs +2 -0
  3. package/dist/auth/index.d.ts +43 -0
  4. package/dist/auth/index.d.ts.map +1 -0
  5. package/dist/auth/index.js +100 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +75 -0
  9. package/dist/commands/build-workbench.d.ts +5 -0
  10. package/dist/commands/build-workbench.d.ts.map +1 -0
  11. package/dist/commands/build-workbench.js +122 -0
  12. package/dist/commands/build.d.ts +7 -0
  13. package/dist/commands/build.d.ts.map +1 -0
  14. package/dist/commands/build.js +22 -0
  15. package/dist/commands/dev.d.ts +11 -0
  16. package/dist/commands/dev.d.ts.map +1 -0
  17. package/dist/commands/dev.js +153 -0
  18. package/dist/commands/login.d.ts +2 -0
  19. package/dist/commands/login.d.ts.map +1 -0
  20. package/dist/commands/login.js +112 -0
  21. package/dist/commands/logout.d.ts +2 -0
  22. package/dist/commands/logout.d.ts.map +1 -0
  23. package/dist/commands/logout.js +18 -0
  24. package/dist/commands/pull.d.ts +7 -0
  25. package/dist/commands/pull.d.ts.map +1 -0
  26. package/dist/commands/pull.js +97 -0
  27. package/dist/compiler/errors.d.ts +20 -0
  28. package/dist/compiler/errors.d.ts.map +1 -0
  29. package/dist/compiler/errors.js +35 -0
  30. package/dist/compiler/evalStatic.d.ts +3 -0
  31. package/dist/compiler/evalStatic.d.ts.map +1 -0
  32. package/dist/compiler/evalStatic.js +57 -0
  33. package/dist/compiler/flatten.js +1 -0
  34. package/dist/compiler/helpers/duplicateIds.d.ts +9 -0
  35. package/dist/compiler/helpers/duplicateIds.d.ts.map +1 -0
  36. package/dist/compiler/helpers/duplicateIds.js +71 -0
  37. package/dist/compiler/index.d.ts +16 -0
  38. package/dist/compiler/index.d.ts.map +1 -0
  39. package/dist/compiler/index.js +934 -0
  40. package/dist/compiler/parsePage.d.ts +15 -0
  41. package/dist/compiler/parsePage.d.ts.map +1 -0
  42. package/dist/compiler/parsePage.js +562 -0
  43. package/dist/compiler/registry.d.ts +4 -0
  44. package/dist/compiler/registry.d.ts.map +1 -0
  45. package/dist/compiler/registry.js +44 -0
  46. package/dist/compiler/reverse.d.ts +17 -0
  47. package/dist/compiler/reverse.d.ts.map +1 -0
  48. package/dist/compiler/reverse.js +1632 -0
  49. package/dist/compiler/types.d.ts +21 -0
  50. package/dist/compiler/types.d.ts.map +1 -0
  51. package/dist/compiler/types.js +1 -0
  52. package/dist/components/index.d.ts +21 -0
  53. package/dist/components/index.d.ts.map +1 -0
  54. package/dist/components/index.js +21 -0
  55. package/dist/components/primitives/BaseComponent.d.ts +32 -0
  56. package/dist/components/primitives/BaseComponent.d.ts.map +1 -0
  57. package/dist/components/primitives/BaseComponent.js +26 -0
  58. package/dist/components/primitives/BookMeeting.d.ts +18 -0
  59. package/dist/components/primitives/BookMeeting.d.ts.map +1 -0
  60. package/dist/components/primitives/BookMeeting.js +5 -0
  61. package/dist/components/primitives/Chart.d.ts +41 -0
  62. package/dist/components/primitives/Chart.d.ts.map +1 -0
  63. package/dist/components/primitives/Chart.js +5 -0
  64. package/dist/components/primitives/Container.d.ts +8 -0
  65. package/dist/components/primitives/Container.d.ts.map +1 -0
  66. package/dist/components/primitives/Container.js +5 -0
  67. package/dist/components/primitives/CustomButton.d.ts +37 -0
  68. package/dist/components/primitives/CustomButton.d.ts.map +1 -0
  69. package/dist/components/primitives/CustomButton.js +10 -0
  70. package/dist/components/primitives/CustomHTML.d.ts +8 -0
  71. package/dist/components/primitives/CustomHTML.d.ts.map +1 -0
  72. package/dist/components/primitives/CustomHTML.js +5 -0
  73. package/dist/components/primitives/FileUpload.d.ts +18 -0
  74. package/dist/components/primitives/FileUpload.d.ts.map +1 -0
  75. package/dist/components/primitives/FileUpload.js +16 -0
  76. package/dist/components/primitives/InputBox.d.ts +34 -0
  77. package/dist/components/primitives/InputBox.d.ts.map +1 -0
  78. package/dist/components/primitives/InputBox.js +25 -0
  79. package/dist/components/primitives/Lottie.d.ts +11 -0
  80. package/dist/components/primitives/Lottie.d.ts.map +1 -0
  81. package/dist/components/primitives/Lottie.js +5 -0
  82. package/dist/components/primitives/MediaEmbed.d.ts +13 -0
  83. package/dist/components/primitives/MediaEmbed.d.ts.map +1 -0
  84. package/dist/components/primitives/MediaEmbed.js +6 -0
  85. package/dist/components/primitives/MediaImage.d.ts +8 -0
  86. package/dist/components/primitives/MediaImage.d.ts.map +1 -0
  87. package/dist/components/primitives/MediaImage.js +5 -0
  88. package/dist/components/primitives/OptionSelector.d.ts +35 -0
  89. package/dist/components/primitives/OptionSelector.d.ts.map +1 -0
  90. package/dist/components/primitives/OptionSelector.js +8 -0
  91. package/dist/components/primitives/PaypalCheckout.d.ts +25 -0
  92. package/dist/components/primitives/PaypalCheckout.d.ts.map +1 -0
  93. package/dist/components/primitives/PaypalCheckout.js +5 -0
  94. package/dist/components/primitives/PlainText.d.ts +6 -0
  95. package/dist/components/primitives/PlainText.d.ts.map +1 -0
  96. package/dist/components/primitives/PlainText.js +5 -0
  97. package/dist/components/primitives/ProgressBar.d.ts +15 -0
  98. package/dist/components/primitives/ProgressBar.d.ts.map +1 -0
  99. package/dist/components/primitives/ProgressBar.js +5 -0
  100. package/dist/components/primitives/RichText.d.ts +6 -0
  101. package/dist/components/primitives/RichText.d.ts.map +1 -0
  102. package/dist/components/primitives/RichText.js +5 -0
  103. package/dist/components/primitives/RichTextMarkdown.d.ts +6 -0
  104. package/dist/components/primitives/RichTextMarkdown.d.ts.map +1 -0
  105. package/dist/components/primitives/RichTextMarkdown.js +5 -0
  106. package/dist/components/primitives/Rive.d.ts +16 -0
  107. package/dist/components/primitives/Rive.d.ts.map +1 -0
  108. package/dist/components/primitives/Rive.js +8 -0
  109. package/dist/components/primitives/StripeCheckout.d.ts +52 -0
  110. package/dist/components/primitives/StripeCheckout.d.ts.map +1 -0
  111. package/dist/components/primitives/StripeCheckout.js +5 -0
  112. package/dist/components/primitives/StripeCheckout2.d.ts +30 -0
  113. package/dist/components/primitives/StripeCheckout2.d.ts.map +1 -0
  114. package/dist/components/primitives/StripeCheckout2.js +7 -0
  115. package/dist/proxy/injectApiInterceptor.d.ts +6 -0
  116. package/dist/proxy/injectApiInterceptor.d.ts.map +1 -0
  117. package/dist/proxy/injectApiInterceptor.js +66 -0
  118. package/dist/proxy/injectReload.d.ts +2 -0
  119. package/dist/proxy/injectReload.d.ts.map +1 -0
  120. package/dist/proxy/injectReload.js +14 -0
  121. package/dist/proxy/injectWorkbench.d.ts +4 -0
  122. package/dist/proxy/injectWorkbench.d.ts.map +1 -0
  123. package/dist/proxy/injectWorkbench.js +16 -0
  124. package/dist/proxy/server.d.ts +11 -0
  125. package/dist/proxy/server.d.ts.map +1 -0
  126. package/dist/proxy/server.js +246 -0
  127. package/dist/proxy/sse.d.ts +5 -0
  128. package/dist/proxy/sse.d.ts.map +1 -0
  129. package/dist/proxy/sse.js +17 -0
  130. package/dist/types-builder.d.ts +800 -0
  131. package/dist/types-builder.d.ts.map +1 -0
  132. package/dist/types-builder.js +20 -0
  133. package/dist/workbench/ActionsPanel.d.ts +6 -0
  134. package/dist/workbench/ActionsPanel.d.ts.map +1 -0
  135. package/dist/workbench/ActionsPanel.js +47 -0
  136. package/dist/workbench/AutofillPanel.d.ts +6 -0
  137. package/dist/workbench/AutofillPanel.d.ts.map +1 -0
  138. package/dist/workbench/AutofillPanel.js +543 -0
  139. package/dist/workbench/ComputedFieldsPanel.d.ts +6 -0
  140. package/dist/workbench/ComputedFieldsPanel.d.ts.map +1 -0
  141. package/dist/workbench/ComputedFieldsPanel.js +31 -0
  142. package/dist/workbench/ExperimentsPanel.d.ts +6 -0
  143. package/dist/workbench/ExperimentsPanel.d.ts.map +1 -0
  144. package/dist/workbench/ExperimentsPanel.js +182 -0
  145. package/dist/workbench/FieldEditorPanel.d.ts +9 -0
  146. package/dist/workbench/FieldEditorPanel.d.ts.map +1 -0
  147. package/dist/workbench/FieldEditorPanel.js +650 -0
  148. package/dist/workbench/InspectorPanel.d.ts +6 -0
  149. package/dist/workbench/InspectorPanel.d.ts.map +1 -0
  150. package/dist/workbench/InspectorPanel.js +341 -0
  151. package/dist/workbench/PageNavigator.d.ts +6 -0
  152. package/dist/workbench/PageNavigator.d.ts.map +1 -0
  153. package/dist/workbench/PageNavigator.js +123 -0
  154. package/dist/workbench/SchemaPanel.d.ts +6 -0
  155. package/dist/workbench/SchemaPanel.d.ts.map +1 -0
  156. package/dist/workbench/SchemaPanel.js +222 -0
  157. package/dist/workbench/UserDataPanel.d.ts +6 -0
  158. package/dist/workbench/UserDataPanel.d.ts.map +1 -0
  159. package/dist/workbench/UserDataPanel.js +350 -0
  160. package/dist/workbench/WorkbenchApp.d.ts +6 -0
  161. package/dist/workbench/WorkbenchApp.d.ts.map +1 -0
  162. package/dist/workbench/WorkbenchApp.js +193 -0
  163. package/dist/workbench/cloudflare-worker/README.md +31 -0
  164. package/dist/workbench/cloudflare-worker/public/workbench.css +1614 -0
  165. package/dist/workbench/cloudflare-worker/public/workbench.js +77 -0
  166. package/dist/workbench/cloudflare-worker/worker.js +40 -0
  167. package/dist/workbench/cloudflare-worker/wrangler.toml +10 -0
  168. package/dist/workbench/index.d.ts +9 -0
  169. package/dist/workbench/index.d.ts.map +1 -0
  170. package/dist/workbench/index.js +44 -0
  171. package/dist/workbench/workbench.css +1614 -0
  172. package/dist/workbench/workbench.js +77 -0
  173. package/package.json +79 -0
@@ -0,0 +1,543 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useState } from 'react';
3
+ // Generate realistic test data based on input type
4
+ function generateTestValue(comp) {
5
+ const inputComp = comp;
6
+ const inputType = inputComp.input_type || 'text';
7
+ switch (inputType) {
8
+ case 'email':
9
+ return 'test@example.com';
10
+ case 'phone':
11
+ return '+1 (555) 123-4567';
12
+ case 'number':
13
+ const min = inputComp.range_min !== undefined ? Number(inputComp.range_min) : 0;
14
+ const max = inputComp.range_max !== undefined ? Number(inputComp.range_max) : 100;
15
+ return String(Math.floor((min + max) / 2));
16
+ case 'date':
17
+ return new Date().toISOString().split('T')[0];
18
+ case 'time':
19
+ return '12:00';
20
+ case 'month':
21
+ const now = new Date();
22
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
23
+ case 'password':
24
+ case 'confirm':
25
+ return 'TestPassword123!';
26
+ case 'range':
27
+ const rangeMin = inputComp.range_min !== undefined ? Number(inputComp.range_min) : 0;
28
+ const rangeMax = inputComp.range_max !== undefined ? Number(inputComp.range_max) : 100;
29
+ return String(Math.floor((rangeMin + rangeMax) / 2));
30
+ case 'switch':
31
+ case 'checkbox':
32
+ return 'checked';
33
+ case 'text':
34
+ default:
35
+ // Generate contextual text based on key or label
36
+ const key = comp.key.toLowerCase();
37
+ const label = (comp.label || '').toLowerCase();
38
+ const context = `${key} ${label}`;
39
+ if (context.includes('name') && context.includes('first'))
40
+ return 'John';
41
+ if (context.includes('name') && context.includes('last'))
42
+ return 'Doe';
43
+ if (context.includes('name') && context.includes('full'))
44
+ return 'John Doe';
45
+ if (context.includes('name'))
46
+ return 'John Doe';
47
+ if (context.includes('address') || context.includes('street'))
48
+ return '123 Main Street';
49
+ if (context.includes('city'))
50
+ return 'San Francisco';
51
+ if (context.includes('state'))
52
+ return 'California';
53
+ if (context.includes('zip') || context.includes('postal'))
54
+ return '94102';
55
+ if (context.includes('country'))
56
+ return 'United States';
57
+ if (context.includes('company') || context.includes('business'))
58
+ return 'Acme Inc.';
59
+ if (context.includes('title') || context.includes('job'))
60
+ return 'Software Engineer';
61
+ if (context.includes('website') || context.includes('url'))
62
+ return 'https://example.com';
63
+ if (context.includes('age'))
64
+ return '30';
65
+ if (context.includes('comment') || context.includes('message') || context.includes('note'))
66
+ return 'This is a test message for autofill purposes.';
67
+ return 'Test input';
68
+ }
69
+ }
70
+ // Fill an InputBox component
71
+ function fillInputBox(componentId, value, inputType) {
72
+ const element = document.querySelector(`.cid-${componentId}`);
73
+ if (!element)
74
+ return false;
75
+ if (inputType === 'switch' || inputType === 'checkbox') {
76
+ const checkbox = element.querySelector('input[type="checkbox"]');
77
+ if (checkbox && !checkbox.checked) {
78
+ checkbox.click();
79
+ return true;
80
+ }
81
+ return false;
82
+ }
83
+ const input = element.querySelector('input, textarea');
84
+ if (!input)
85
+ return false;
86
+ // Focus and set value
87
+ input.focus();
88
+ // Use native value setter to trigger React's onChange
89
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
90
+ const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
91
+ if (input instanceof HTMLTextAreaElement && nativeTextAreaValueSetter) {
92
+ nativeTextAreaValueSetter.call(input, value);
93
+ }
94
+ else if (nativeInputValueSetter) {
95
+ nativeInputValueSetter.call(input, value);
96
+ }
97
+ else {
98
+ input.value = value;
99
+ }
100
+ // Dispatch input event
101
+ input.dispatchEvent(new Event('input', { bubbles: true }));
102
+ input.dispatchEvent(new Event('change', { bubbles: true }));
103
+ // Blur to trigger validation
104
+ input.blur();
105
+ return true;
106
+ }
107
+ // Fill an OptionSelector component by clicking the first (or a random) option
108
+ function fillOptionSelector(componentId, buttons, random = false) {
109
+ const element = document.querySelector(`.cid-${componentId}`);
110
+ if (!element)
111
+ return false;
112
+ // Check if it's a dropdown
113
+ const select = element.querySelector('select');
114
+ if (select) {
115
+ const options = Array.from(select.options).filter((opt) => opt.value);
116
+ if (options.length === 0)
117
+ return false;
118
+ const optionIndex = random ? Math.floor(Math.random() * options.length) : 0;
119
+ select.value = options[optionIndex].value;
120
+ select.dispatchEvent(new Event('change', { bubbles: true }));
121
+ return true;
122
+ }
123
+ // Look for ButtonCard elements (the main clickable elements in OptionSelector)
124
+ // These are divs with data-action that handle clicks via Stimulus controller
125
+ const buttonCards = element.querySelectorAll('.ButtonCard:not(.selected), [data-components--option-selector-target="option"]:not(.selected)');
126
+ if (buttonCards.length > 0) {
127
+ const cardIndex = random ? Math.floor(Math.random() * buttonCards.length) : 0;
128
+ const card = buttonCards[cardIndex];
129
+ if (card) {
130
+ card.click();
131
+ return true;
132
+ }
133
+ }
134
+ // Fallback: try clicking any button element
135
+ const buttonElements = element.querySelectorAll('button:not([disabled])');
136
+ if (buttonElements.length > 0) {
137
+ const buttonIndex = random ? Math.floor(Math.random() * buttonElements.length) : 0;
138
+ const button = buttonElements[buttonIndex];
139
+ if (button) {
140
+ button.click();
141
+ return true;
142
+ }
143
+ }
144
+ return false;
145
+ }
146
+ export function AutofillPanel({ embeddableId }) {
147
+ const [inputComponents, setInputComponents] = useState([]);
148
+ const [currentPageKey, setCurrentPageKey] = useState('');
149
+ const [error, setError] = useState(null);
150
+ const [isAutofilling, setIsAutofilling] = useState(false);
151
+ const [lastAutofillTime, setLastAutofillTime] = useState(null);
152
+ const [useRandomOptions, setUseRandomOptions] = useState(false);
153
+ const savvy = window.Savvy;
154
+ // Scan current page for input components
155
+ const scanPage = useCallback(() => {
156
+ setError(null);
157
+ if (!savvy?.getFlowJson) {
158
+ setError('window.Savvy.getFlowJson is not available.');
159
+ return;
160
+ }
161
+ if (!savvy?.getUserData) {
162
+ setError('window.Savvy.getUserData is not available.');
163
+ return;
164
+ }
165
+ const flowJson = savvy.getFlowJson(embeddableId);
166
+ if (!flowJson) {
167
+ setError('Could not retrieve flow JSON.');
168
+ return;
169
+ }
170
+ const userData = savvy.getUserData(embeddableId);
171
+ if (!userData) {
172
+ setError('Could not retrieve user data.');
173
+ return;
174
+ }
175
+ const currentPageId = userData.current_page_id;
176
+ setCurrentPageKey(userData.current_page_key || '');
177
+ // Find the current page
178
+ const currentPage = flowJson.pages?.find((p) => p.id === currentPageId);
179
+ if (!currentPage) {
180
+ setError(`Current page "${currentPageId}" not found in flow.`);
181
+ return;
182
+ }
183
+ // Collect all input components from the current page
184
+ const inputs = [];
185
+ const processComponent = (comp) => {
186
+ if (comp.type === 'InputBox') {
187
+ const inputComp = comp;
188
+ const element = document.querySelector(`.cid-${comp.id}`);
189
+ const input = element?.querySelector('input, textarea');
190
+ const inputType = inputComp.input_type || 'text';
191
+ // Get current value
192
+ let currentValue;
193
+ if (inputType === 'switch' || inputType === 'checkbox') {
194
+ const checkbox = element?.querySelector('input[type="checkbox"]');
195
+ currentValue = checkbox?.checked ? 'checked' : undefined;
196
+ }
197
+ else if (input?.value) {
198
+ currentValue = input.value;
199
+ }
200
+ inputs.push({
201
+ id: comp.id,
202
+ key: comp.key,
203
+ type: comp.type,
204
+ label: comp.label,
205
+ inputType,
206
+ filled: !!input?.value ||
207
+ (inputType === 'checkbox' || inputType === 'switch'
208
+ ? !!element?.querySelector('input[type="checkbox"]')?.checked
209
+ : false),
210
+ currentValue,
211
+ });
212
+ }
213
+ else if (comp.type === 'OptionSelector') {
214
+ const optComp = comp;
215
+ const element = document.querySelector(`.cid-${comp.id}`);
216
+ // Check if an option is selected by looking for the 'selected' class on button elements
217
+ // This is the most reliable indicator of the visual selection state
218
+ let hasSelection = false;
219
+ let currentValue;
220
+ if (element) {
221
+ // Check for button-style OptionSelector (ButtonCard with 'selected' class)
222
+ const selectedButton = element.querySelector('.ButtonCard.selected, [data-components--option-selector-target="option"].selected');
223
+ if (selectedButton) {
224
+ hasSelection = true;
225
+ // Try to get the text content of the selected button
226
+ currentValue = selectedButton.textContent?.trim() || undefined;
227
+ }
228
+ // Check for dropdown-style OptionSelector
229
+ if (!hasSelection) {
230
+ const select = element.querySelector('select');
231
+ if (select && select.value) {
232
+ hasSelection = true;
233
+ currentValue = select.options[select.selectedIndex]?.text || select.value;
234
+ }
235
+ }
236
+ // Check for checkbox/radio style
237
+ if (!hasSelection) {
238
+ const checkedInput = element.querySelector('input[type="checkbox"]:checked, input[type="radio"]:checked');
239
+ if (checkedInput) {
240
+ hasSelection = true;
241
+ // Try to get label text
242
+ const label = checkedInput.closest('label')?.textContent?.trim();
243
+ currentValue = label || checkedInput.value || 'selected';
244
+ }
245
+ }
246
+ }
247
+ inputs.push({
248
+ id: comp.id,
249
+ key: comp.key,
250
+ type: comp.type,
251
+ label: comp.label,
252
+ buttons: optComp.buttons?.map((b) => ({ key: b.key, text: b.text })) || [],
253
+ filled: hasSelection,
254
+ currentValue,
255
+ });
256
+ }
257
+ else if (comp.type === 'FileUpload') {
258
+ const element = document.querySelector(`.cid-${comp.id}`);
259
+ const input = element?.querySelector('input[type="file"]');
260
+ const hasFiles = !!(input?.files && input.files.length > 0);
261
+ // Get filename(s) if any
262
+ let currentValue;
263
+ if (hasFiles && input?.files) {
264
+ const fileNames = Array.from(input.files).map((f) => f.name);
265
+ currentValue = fileNames.join(', ');
266
+ }
267
+ inputs.push({
268
+ id: comp.id,
269
+ key: comp.key,
270
+ type: comp.type,
271
+ label: comp.label,
272
+ filled: hasFiles,
273
+ currentValue,
274
+ });
275
+ }
276
+ // Process nested components (e.g., in Container)
277
+ if (comp.components) {
278
+ comp.components.forEach(processComponent);
279
+ }
280
+ };
281
+ currentPage.components?.forEach(processComponent);
282
+ // Also check global components that might be visible
283
+ flowJson.components?.forEach(processComponent);
284
+ setInputComponents(inputs);
285
+ }, [embeddableId, savvy]);
286
+ useEffect(() => {
287
+ // Initial scan with a small delay to ensure DOM is ready
288
+ const timer = setTimeout(scanPage, 300);
289
+ return () => clearTimeout(timer);
290
+ }, [scanPage]);
291
+ // Re-scan when page changes
292
+ useEffect(() => {
293
+ const handleUserDataUpdate = () => {
294
+ setTimeout(scanPage, 100);
295
+ };
296
+ window.addEventListener('embeddables:userdata_updated', handleUserDataUpdate);
297
+ return () => window.removeEventListener('embeddables:userdata_updated', handleUserDataUpdate);
298
+ }, [scanPage]);
299
+ const handleAutofill = async () => {
300
+ setIsAutofilling(true);
301
+ setError(null);
302
+ let filledCount = 0;
303
+ for (const comp of inputComponents) {
304
+ if (comp.filled)
305
+ continue; // Skip already filled
306
+ try {
307
+ if (comp.type === 'InputBox') {
308
+ const testValue = generateTestValue({
309
+ key: comp.key,
310
+ label: comp.label,
311
+ input_type: comp.inputType,
312
+ });
313
+ if (fillInputBox(comp.id, testValue, comp.inputType || 'text')) {
314
+ filledCount++;
315
+ }
316
+ }
317
+ else if (comp.type === 'OptionSelector' && comp.buttons && comp.buttons.length > 0) {
318
+ if (fillOptionSelector(comp.id, comp.buttons, useRandomOptions)) {
319
+ filledCount++;
320
+ }
321
+ }
322
+ // FileUpload is skipped as it requires actual file selection
323
+ // Small delay between fills
324
+ await new Promise((resolve) => setTimeout(resolve, 50));
325
+ }
326
+ catch (err) {
327
+ console.error(`Error filling component ${comp.id}:`, err);
328
+ }
329
+ }
330
+ setLastAutofillTime(Date.now());
331
+ setIsAutofilling(false);
332
+ // Re-scan to update filled status
333
+ setTimeout(scanPage, 200);
334
+ return filledCount;
335
+ };
336
+ const handleClearPage = () => {
337
+ setError(null);
338
+ // Collect OptionSelector keys to clear via setUserData
339
+ const optionSelectorKeys = [];
340
+ for (const comp of inputComponents) {
341
+ try {
342
+ const element = document.querySelector(`.cid-${comp.id}`);
343
+ if (!element)
344
+ continue;
345
+ if (comp.type === 'InputBox') {
346
+ const inputType = comp.inputType || 'text';
347
+ if (inputType === 'switch' || inputType === 'checkbox') {
348
+ const checkbox = element.querySelector('input[type="checkbox"]');
349
+ if (checkbox && checkbox.checked) {
350
+ checkbox.click();
351
+ }
352
+ }
353
+ else {
354
+ const input = element.querySelector('input, textarea');
355
+ if (input) {
356
+ input.focus();
357
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
358
+ const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
359
+ if (input instanceof HTMLTextAreaElement && nativeTextAreaValueSetter) {
360
+ nativeTextAreaValueSetter.call(input, '');
361
+ }
362
+ else if (nativeInputValueSetter) {
363
+ nativeInputValueSetter.call(input, '');
364
+ }
365
+ else {
366
+ input.value = '';
367
+ }
368
+ input.dispatchEvent(new Event('input', { bubbles: true }));
369
+ input.dispatchEvent(new Event('change', { bubbles: true }));
370
+ input.blur();
371
+ }
372
+ }
373
+ }
374
+ else if (comp.type === 'OptionSelector') {
375
+ // For dropdowns, reset to first (empty) option
376
+ const select = element.querySelector('select');
377
+ if (select) {
378
+ select.value = '';
379
+ select.dispatchEvent(new Event('change', { bubbles: true }));
380
+ }
381
+ // For button cards, we need to clear via setUserData since clicking
382
+ // a selected option doesn't deselect it (no toggle behavior)
383
+ if (comp.filled) {
384
+ optionSelectorKeys.push(comp.key);
385
+ }
386
+ // For checkbox/radio inputs
387
+ const checkedInputs = element.querySelectorAll('input[type="checkbox"]:checked, input[type="radio"]:checked');
388
+ checkedInputs.forEach((input) => {
389
+ input.click();
390
+ });
391
+ }
392
+ }
393
+ catch (err) {
394
+ console.error(`Error clearing component ${comp.id}:`, err);
395
+ }
396
+ }
397
+ // Clear OptionSelector values via setUserData
398
+ if (optionSelectorKeys.length > 0 && savvy?.getUserData && savvy?.setUserData) {
399
+ const userData = savvy.getUserData(embeddableId);
400
+ if (userData) {
401
+ const clearedFields = {};
402
+ for (const key of optionSelectorKeys) {
403
+ clearedFields[key] = null;
404
+ }
405
+ savvy.setUserData(embeddableId, clearedFields);
406
+ }
407
+ }
408
+ // Re-scan to update filled status
409
+ setTimeout(scanPage, 200);
410
+ };
411
+ const handleClearSingle = (comp) => {
412
+ setError(null);
413
+ try {
414
+ const element = document.querySelector(`.cid-${comp.id}`);
415
+ if (!element)
416
+ return;
417
+ if (comp.type === 'InputBox') {
418
+ const inputType = comp.inputType || 'text';
419
+ if (inputType === 'switch' || inputType === 'checkbox') {
420
+ const checkbox = element.querySelector('input[type="checkbox"]');
421
+ if (checkbox && checkbox.checked) {
422
+ checkbox.click();
423
+ }
424
+ }
425
+ else {
426
+ const input = element.querySelector('input, textarea');
427
+ if (input) {
428
+ input.focus();
429
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
430
+ const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
431
+ if (input instanceof HTMLTextAreaElement && nativeTextAreaValueSetter) {
432
+ nativeTextAreaValueSetter.call(input, '');
433
+ }
434
+ else if (nativeInputValueSetter) {
435
+ nativeInputValueSetter.call(input, '');
436
+ }
437
+ else {
438
+ input.value = '';
439
+ }
440
+ input.dispatchEvent(new Event('input', { bubbles: true }));
441
+ input.dispatchEvent(new Event('change', { bubbles: true }));
442
+ input.blur();
443
+ }
444
+ }
445
+ }
446
+ else if (comp.type === 'OptionSelector') {
447
+ // For dropdowns, reset to first (empty) option
448
+ const select = element.querySelector('select');
449
+ if (select) {
450
+ select.value = '';
451
+ select.dispatchEvent(new Event('change', { bubbles: true }));
452
+ }
453
+ // For button cards, we need to clear via setUserData since clicking
454
+ // a selected option doesn't deselect it (no toggle behavior)
455
+ if (comp.filled && savvy?.getUserData && savvy?.setUserData) {
456
+ const userData = savvy.getUserData(embeddableId);
457
+ if (userData) {
458
+ savvy.setUserData(embeddableId, { [comp.key]: null });
459
+ }
460
+ }
461
+ // For checkbox/radio inputs
462
+ const checkedInputs = element.querySelectorAll('input[type="checkbox"]:checked, input[type="radio"]:checked');
463
+ checkedInputs.forEach((input) => {
464
+ input.click();
465
+ });
466
+ }
467
+ // Re-scan to update filled status
468
+ setTimeout(scanPage, 200);
469
+ }
470
+ catch (err) {
471
+ setError(err?.message || String(err));
472
+ }
473
+ };
474
+ const handleAutofillAndNext = async () => {
475
+ await handleAutofill();
476
+ // Wait a bit for the DOM to update after autofill
477
+ await new Promise((resolve) => setTimeout(resolve, 150));
478
+ // Find the first CustomButton with next-page or page-* action
479
+ const nextButton = document.querySelector('[data-components--custom-button-action-value="next-page"], ' +
480
+ '[data-components--custom-button-action-value^="page-"]');
481
+ if (nextButton) {
482
+ nextButton.click();
483
+ }
484
+ else {
485
+ setError('No next page button found on this page.');
486
+ }
487
+ };
488
+ const handleFillSingle = (comp) => {
489
+ setError(null);
490
+ try {
491
+ if (comp.type === 'InputBox') {
492
+ const testValue = generateTestValue({
493
+ key: comp.key,
494
+ label: comp.label,
495
+ input_type: comp.inputType,
496
+ });
497
+ fillInputBox(comp.id, testValue, comp.inputType || 'text');
498
+ }
499
+ else if (comp.type === 'OptionSelector' && comp.buttons && comp.buttons.length > 0) {
500
+ fillOptionSelector(comp.id, comp.buttons, useRandomOptions);
501
+ }
502
+ // Re-scan to update filled status
503
+ setTimeout(scanPage, 200);
504
+ }
505
+ catch (err) {
506
+ setError(err?.message || String(err));
507
+ }
508
+ };
509
+ const getInputTypeIcon = (type, inputType) => {
510
+ if (type === 'OptionSelector')
511
+ return '☰';
512
+ if (type === 'FileUpload')
513
+ return '📎';
514
+ switch (inputType) {
515
+ case 'email':
516
+ return '✉';
517
+ case 'phone':
518
+ return '☎';
519
+ case 'number':
520
+ return '#';
521
+ case 'date':
522
+ return '📅';
523
+ case 'time':
524
+ return '⏰';
525
+ case 'month':
526
+ return '📆';
527
+ case 'password':
528
+ case 'confirm':
529
+ return '🔒';
530
+ case 'checkbox':
531
+ case 'switch':
532
+ return '☑';
533
+ case 'range':
534
+ return '↔';
535
+ default:
536
+ return '𝐓';
537
+ }
538
+ };
539
+ const unfilledCount = inputComponents.filter((c) => !c.filled && c.type !== 'FileUpload').length;
540
+ return (_jsxs("div", { className: "mx-auto flex h-full w-full max-w-6xl flex-col", children: [_jsxs("div", { className: "flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-xs font-semibold tracking-wide text-slate-100", children: "Autofill Components" }), _jsx("div", { className: "mt-0.5 text-[11px] text-slate-400", children: "Automatically fill input components on the current page with test data." })] }), _jsxs("div", { className: "flex flex-wrap items-center justify-start gap-2 flex-2 sm:justify-end", children: [_jsxs("label", { className: "flex cursor-pointer items-center gap-1.5 text-[11px] text-slate-400", children: [_jsx("input", { type: "checkbox", checked: useRandomOptions, onChange: (e) => setUseRandomOptions(e.target.checked), className: "h-3 w-3 rounded border-slate-600 bg-slate-800 text-sky-500 focus:ring-sky-500/50" }), "Random options"] }), _jsx("button", { type: "button", onClick: scanPage, className: "inline-flex cursor-pointer items-center gap-1.5 rounded-lg bg-slate-700 px-2.5 py-1.5 text-xs font-semibold text-slate-200 ring-1 ring-inset ring-slate-600 hover:bg-slate-600 hover:text-white", children: "Rescan" }), _jsxs("button", { type: "button", onClick: handleClearPage, disabled: isAutofilling || inputComponents.length === 0, className: "inline-flex cursor-pointer items-center gap-1.5 rounded-lg bg-slate-700 px-2.5 py-1.5 text-xs font-semibold text-slate-200 ring-1 ring-inset ring-slate-600 hover:bg-slate-600 hover:text-white disabled:cursor-not-allowed disabled:opacity-60", children: [_jsx("span", { className: "text-sm", children: "\uD83D\uDDD1" }), "Clear Page"] }), _jsx("button", { type: "button", onClick: handleAutofill, disabled: isAutofilling || unfilledCount === 0, className: "inline-flex cursor-pointer items-center gap-1.5 rounded-lg bg-emerald-600 px-2.5 py-1.5 text-xs font-semibold text-white ring-1 ring-inset ring-emerald-500 hover:bg-emerald-500 disabled:cursor-not-allowed disabled:opacity-60", children: isAutofilling ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "inline-block h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent" }), "Filling..."] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-sm", children: "\u26A1" }), "Autofill All (", unfilledCount, ")"] })) }), _jsxs("button", { type: "button", onClick: handleAutofillAndNext, disabled: isAutofilling, className: "inline-flex cursor-pointer items-center gap-1.5 rounded-lg bg-sky-600 px-2.5 py-1.5 text-xs font-semibold text-white ring-1 ring-inset ring-sky-500 hover:bg-sky-500 disabled:cursor-not-allowed disabled:opacity-60", children: [_jsx("span", { className: "text-sm", children: "\u26A1\u2192" }), "Autofill All + Next"] })] })] }), error && (_jsx("div", { className: "mt-3 rounded-xl bg-rose-500/10 px-3 py-2 text-xs text-rose-200 ring-1 ring-inset ring-rose-500/20", children: error })), lastAutofillTime && (_jsx("div", { className: "mt-3 rounded-xl bg-emerald-500/10 px-3 py-2 text-xs text-emerald-200 ring-1 ring-inset ring-emerald-500/20", children: "Autofill completed. Components have been filled with test data." })), _jsxs("div", { className: "mt-4 flex min-h-0 flex-1 flex-col overflow-hidden", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wider text-slate-400", children: "Input Components" }), _jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-emerald-500/20 px-2 py-0.5 text-[11px] font-medium text-emerald-300 ring-1 ring-inset ring-emerald-500/30", children: [_jsx("span", { className: "text-emerald-400", children: inputComponents.filter((c) => c.filled).length }), _jsx("span", { className: "text-emerald-300/60", children: "filled" })] }), _jsx("span", { className: "text-slate-300 text-[12px]", children: "of" }), _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-slate-700/50 px-2 py-0.5 text-[11px] font-medium text-slate-300 ring-1 ring-inset ring-slate-600/50", children: [_jsx("span", { children: inputComponents.length }), _jsx("span", { className: "text-slate-400", children: "total" })] })] })] }), inputComponents.length === 0 ? (_jsx("div", { className: "flex flex-1 items-center justify-center text-[11px] text-slate-500", children: "No input components found on the current page." })) : (_jsx("div", { className: "min-h-0 flex-1 overflow-auto rounded-xl bg-slate-950/60 p-2 ring-1 ring-inset ring-white/10", children: _jsx("div", { className: "space-y-1", children: inputComponents.map((comp) => (_jsxs("div", { className: `flex items-center justify-between gap-2 rounded-lg px-2.5 py-2 ${comp.filled
541
+ ? 'bg-emerald-500/10 ring-1 ring-inset ring-emerald-500/20'
542
+ : 'bg-white/5 ring-1 ring-inset ring-white/10'}`, children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [_jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded bg-slate-800 text-xs", title: comp.inputType || comp.type, children: getInputTypeIcon(comp.type, comp.inputType) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "truncate text-xs font-medium text-slate-100", children: comp.label || comp.key }), _jsxs("div", { className: "flex items-center gap-2 text-[10px] text-slate-500", children: [_jsx("span", { children: comp.type }), comp.inputType && (_jsxs(_Fragment, { children: [_jsx("span", { children: "\u2022" }), _jsx("span", { className: "font-mono", children: comp.inputType })] })), comp.buttons && (_jsxs(_Fragment, { children: [_jsx("span", { children: "\u2022" }), _jsxs("span", { children: [comp.buttons.length, " options"] })] }))] })] }), comp.currentValue && (_jsx("div", { className: "min-w-0 max-w-[200px] shrink-0", children: _jsx("div", { className: "truncate rounded bg-slate-800/80 px-2 py-1 text-[10px] font-mono text-sky-300", title: comp.currentValue, children: comp.currentValue }) }))] }), _jsx("div", { className: "flex shrink-0 items-center gap-2", children: comp.type === 'FileUpload' ? (_jsx("span", { className: "text-[10px] text-slate-500", children: "Manual only" })) : comp.filled ? (_jsx("button", { type: "button", onClick: () => handleClearSingle(comp), className: "cursor-pointer rounded-lg bg-slate-700 px-2 py-1 text-[10px] font-semibold text-rose-300 ring-1 ring-inset ring-slate-600 hover:bg-slate-600 hover:text-rose-200", children: "Clear" })) : (_jsx("button", { type: "button", onClick: () => handleFillSingle(comp), className: "cursor-pointer rounded-lg bg-slate-700 px-2 py-1 text-[10px] font-semibold text-teal-300 ring-1 ring-inset ring-slate-600 hover:bg-slate-600 hover:text-teal-200", children: "Fill" })) })] }, comp.id))) }) }))] })] }));
543
+ }
@@ -0,0 +1,6 @@
1
+ type ComputedFieldsPanelProps = {
2
+ embeddableId: string;
3
+ };
4
+ export declare function ComputedFieldsPanel({ embeddableId }: ComputedFieldsPanelProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
6
+ //# sourceMappingURL=ComputedFieldsPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ComputedFieldsPanel.d.ts","sourceRoot":"","sources":["../../src/workbench/ComputedFieldsPanel.tsx"],"names":[],"mappings":"AAGA,KAAK,wBAAwB,GAAG;IAC9B,YAAY,EAAE,MAAM,CAAA;CACrB,CAAA;AASD,wBAAgB,mBAAmB,CAAC,EAAE,YAAY,EAAE,EAAE,wBAAwB,2CA4I7E"}
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useState } from 'react';
3
+ export function ComputedFieldsPanel({ embeddableId }) {
4
+ const [computedFields, setComputedFields] = useState([]);
5
+ const [selectedField, setSelectedField] = useState(null);
6
+ const [error, setError] = useState(null);
7
+ const savvy = window.Savvy;
8
+ const refresh = useCallback(() => {
9
+ if (!savvy?.getFlowJson)
10
+ return;
11
+ try {
12
+ const flowJson = savvy.getFlowJson(embeddableId);
13
+ const list = flowJson?.computedFields ?? [];
14
+ setComputedFields(list);
15
+ setSelectedField((prev) => {
16
+ const stillExists = prev && list.some((cf) => cf.id === prev.id);
17
+ return stillExists ? prev : (list[0] ?? null);
18
+ });
19
+ setError(null);
20
+ }
21
+ catch (err) {
22
+ setError(err instanceof Error ? err.message : String(err));
23
+ }
24
+ }, [embeddableId, savvy]);
25
+ useEffect(() => {
26
+ refresh();
27
+ }, [refresh]);
28
+ return (_jsxs("div", { className: "mx-auto flex h-full w-full max-w-6xl flex-col", children: [_jsxs("div", { className: "mb-3", children: [_jsx("div", { className: "text-xs font-semibold tracking-wide text-slate-100", children: "Computed Fields" }), _jsx("div", { className: "mt-0.5 text-[11px] text-slate-400", children: "View computed field metadata and code. Computed values are derived from user data." })] }), error && (_jsx("div", { className: "mb-3 rounded-xl bg-rose-500/10 px-3 py-2 text-xs text-rose-200 ring-1 ring-inset ring-rose-500/20", children: error })), _jsxs("div", { className: "flex min-h-0 flex-1 gap-3 overflow-hidden", children: [_jsxs("div", { className: "flex w-1/3 min-w-0 shrink-0 flex-col", children: [_jsx("div", { className: "mb-2 text-[11px] font-semibold uppercase tracking-wider text-slate-400", children: "Computed Fields" }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto rounded-xl bg-slate-950/60 ring-1 ring-inset ring-white/10", children: computedFields.length === 0 ? (_jsx("div", { className: "p-3 text-[11px] italic text-slate-500", children: "No computed fields defined." })) : (_jsx("div", { className: "p-1", children: computedFields.map((field) => (_jsxs("button", { type: "button", onClick: () => setSelectedField(field), className: `w-full rounded-lg px-2.5 py-2 text-left text-xs transition-colors ${selectedField?.id === field.id
29
+ ? 'bg-white/15 text-slate-100 ring-1 ring-inset ring-white/20'
30
+ : 'text-slate-300 hover:bg-white/5 hover:text-slate-100'}`, children: [_jsx("span", { className: "block truncate font-medium", children: field.key || field.id }), _jsx("span", { className: "mt-0.5 block truncate font-mono text-[10px] text-slate-500", children: field.id })] }, field.id))) })) })] }), _jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [_jsx("div", { className: "mb-2 text-[11px] font-semibold uppercase tracking-wider text-slate-400", children: selectedField ? selectedField.key || selectedField.id : 'Select a computed field' }), selectedField ? (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col gap-3 overflow-hidden", children: [_jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsxs("span", { className: "inline-flex items-center rounded-md bg-slate-700/80 px-2 py-1 font-mono text-[10px] text-slate-200 ring-1 ring-inset ring-slate-600", children: ["id: ", selectedField.id] }), _jsxs("span", { className: "inline-flex items-center rounded-md bg-slate-700/80 px-2 py-1 font-mono text-[10px] text-slate-200 ring-1 ring-inset ring-slate-600", children: ["key: ", selectedField.key] }), _jsx("span", { className: "inline-flex items-center rounded-md bg-amber-500/20 px-2 py-1 text-[10px] font-medium text-amber-300 ring-1 ring-inset ring-amber-500/30", children: selectedField.formula }), selectedField.inputs && selectedField.inputs.length > 0 && (_jsxs("span", { className: "inline-flex items-center rounded-md bg-teal-500/20 px-2 py-1 text-[10px] text-teal-300 ring-1 ring-inset ring-teal-500/30", children: ["inputs: [", selectedField.inputs.join(', '), "]"] })), selectedField.async && (_jsx("span", { className: "inline-flex items-center rounded-md bg-sky-500/20 px-2 py-1 text-[10px] font-medium text-sky-300 ring-1 ring-inset ring-sky-500/30", children: "async" })), selectedField.doNotSave && (_jsxs("span", { className: "inline-flex items-center rounded-md bg-rose-500/20 px-2 py-1 text-[10px] font-medium text-rose-300 ring-1 ring-inset ring-rose-500/30", children: ["doNotSave: ", String(selectedField.doNotSave)] })), selectedField.use_full_user_data && (_jsx("span", { className: "inline-flex items-center rounded-md bg-purple-500/20 px-2 py-1 text-[10px] font-medium text-purple-300 ring-1 ring-inset ring-purple-500/30", children: "use_full_user_data" })), selectedField.cache_data && (_jsx("span", { className: "inline-flex items-center rounded-md bg-emerald-500/20 px-2 py-1 text-[10px] font-medium text-emerald-300 ring-1 ring-inset ring-emerald-500/30", children: "cache_data" }))] }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col", children: _jsx("pre", { className: "min-h-0 flex-1 overflow-auto rounded-xl bg-slate-950/60 p-3 font-mono text-xs leading-5 text-slate-100 ring-1 ring-inset ring-white/10", children: selectedField.code?.trim() || '(no code)' }) })] })) : (_jsx("div", { className: "flex flex-1 items-center justify-center rounded-xl bg-slate-950/60 ring-1 ring-inset ring-white/10", children: _jsx("span", { className: "text-[11px] text-slate-500", children: "Select a computed field from the list" }) }))] })] })] }));
31
+ }
@@ -0,0 +1,6 @@
1
+ type ExperimentsPanelProps = {
2
+ embeddableId: string;
3
+ };
4
+ export declare function ExperimentsPanel({ embeddableId }: ExperimentsPanelProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
6
+ //# sourceMappingURL=ExperimentsPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExperimentsPanel.d.ts","sourceRoot":"","sources":["../../src/workbench/ExperimentsPanel.tsx"],"names":[],"mappings":"AAGA,KAAK,qBAAqB,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAA;CACrB,CAAA;AAwED,wBAAgB,gBAAgB,CAAC,EAAE,YAAY,EAAE,EAAE,qBAAqB,2CAgQvE"}