lisichatbot 1.3.6 → 1.3.8

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +332 -303
package/src/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Conversational Flow Builder
3
3
  * A simple, flexible conversational flow builder for multi-step chat interfaces
4
4
  * @version 1.0.0
5
- * WITH CUSTOM DATA ATTRIBUTES
5
+ * WITH MULTI-SELECT-DROPDOWN - COMPLETE FIXED VERSION
6
6
  */
7
7
 
8
8
  'use strict';
@@ -16,15 +16,15 @@ let chatState = {
16
16
  data: {},
17
17
  history: [],
18
18
  currentSelection: null,
19
- returnToStep: null, // Track where to return after editing
20
- completed: false // Track if flow is completed
19
+ returnToStep: null,
20
+ completed: false
21
21
  };
22
22
 
23
23
  let elements = {
24
24
  container: null,
25
25
  messages: null,
26
26
  nextBtn: null,
27
- originalNextBtnText: null // Store original button text
27
+ originalNextBtnText: null
28
28
  };
29
29
 
30
30
  let config = {
@@ -78,7 +78,6 @@ function disableNextButton() {
78
78
  function addMessage(content, type = 'bot', hasInput = false, stepNumber = null) {
79
79
  if (!elements.messages) return;
80
80
 
81
- // Find the wrapper element directly by data-chat-element
82
81
  const wrapperSelector = `[data-chat-element="${type}-message-wrapper"]`;
83
82
  const existingWrapper = document.querySelector(wrapperSelector);
84
83
 
@@ -87,20 +86,16 @@ function addMessage(content, type = 'bot', hasInput = false, stepNumber = null)
87
86
  return;
88
87
  }
89
88
 
90
- // Clone the existing wrapper
91
89
  const clone = existingWrapper.cloneNode(true);
92
90
 
93
- // Immediately hide any edit icons in the clone (regardless of template state)
94
91
  const allEditIcons = clone.querySelectorAll('[data-chat-element="bot-edit-icon"]');
95
92
  allEditIcons.forEach(icon => {
96
93
  icon.style.setProperty('display', 'none', 'important');
97
94
  });
98
95
 
99
- // Add step number if provided
100
96
  if (stepNumber !== null) {
101
97
  clone.setAttribute('data-chat-step', stepNumber);
102
98
 
103
- // Mark all previous instances of this step as NOT latest
104
99
  const previousInstances = elements.messages.querySelectorAll(
105
100
  `[data-chat-element="${type}-message-wrapper"][data-chat-step="${stepNumber}"]`
106
101
  );
@@ -108,11 +103,9 @@ function addMessage(content, type = 'bot', hasInput = false, stepNumber = null)
108
103
  prev.removeAttribute('data-chat-latest');
109
104
  });
110
105
 
111
- // Mark this as the latest instance of this step
112
106
  clone.setAttribute('data-chat-latest', 'true');
113
107
  }
114
108
 
115
- // Find text element in clone and set content
116
109
  const textElement = clone.querySelector(`[data-chat-element="${type}-message-text"]`);
117
110
  if (textElement) {
118
111
  textElement.textContent = content;
@@ -120,7 +113,6 @@ function addMessage(content, type = 'bot', hasInput = false, stepNumber = null)
120
113
  console.error(`Element with data-chat-element="${type}-message-text" not found in wrapper`);
121
114
  }
122
115
 
123
- // Handle edit icon for bot messages
124
116
  if (type === 'bot') {
125
117
  console.log(`\nšŸ” Processing bot message - Step ${stepNumber || 'N/A'}, hasInput: ${hasInput}`);
126
118
 
@@ -128,14 +120,11 @@ function addMessage(content, type = 'bot', hasInput = false, stepNumber = null)
128
120
  if (editIcon) {
129
121
  console.log(' āœ“ Edit icon element found');
130
122
 
131
- // ALWAYS hide first with !important to override any CSS
132
123
  editIcon.style.setProperty('display', 'none', 'important');
133
124
  console.log(' āœ“ Edit icon hidden (display: none !important)');
134
125
 
135
- // Check if this is the latest instance (it should be since we just marked it)
136
126
  const isLatest = clone.hasAttribute('data-chat-latest');
137
127
 
138
- // Only show if this step has input AND it's a PREVIOUS step AND it's the latest instance
139
128
  if (hasInput && stepNumber !== null && stepNumber < chatState.step && isLatest) {
140
129
  editIcon.setAttribute('data-chat-step', stepNumber);
141
130
  editIcon.onclick = (e) => {
@@ -161,33 +150,22 @@ function addMessage(content, type = 'bot', hasInput = false, stepNumber = null)
161
150
  console.warn(' āš ļø Edit icon element not found in bot-message-wrapper!');
162
151
  console.warn(' → Make sure you have: <span data-chat-element="bot-edit-icon">āœļø</span>');
163
152
  }
164
-
165
- // REMOVED: No longer setting wrapper flex properties
166
- // User controls wrapper styling with their own CSS
167
153
  }
168
154
 
169
- // Make clone visible
170
155
  clone.style.display = '';
171
-
172
- // Append to messages container
173
156
  elements.messages.appendChild(clone);
174
157
 
175
- // IMPORTANT: Set edit icon display AFTER appending to DOM
176
- // This ensures our styles override any CSS rules
177
158
  if (type === 'bot') {
178
159
  const editIconAfterAppend = clone.querySelector('[data-chat-element="bot-edit-icon"]');
179
160
  if (editIconAfterAppend) {
180
- // Force hide with !important AFTER append
181
161
  editIconAfterAppend.style.setProperty('display', 'none', 'important');
182
162
  editIconAfterAppend.style.setProperty('margin-left', '8px', 'important');
183
163
 
184
164
  const isLatest = clone.hasAttribute('data-chat-latest');
185
165
 
186
- // Only show if has input AND it's a previous step AND it's latest instance
187
166
  if (hasInput && stepNumber !== null && stepNumber < chatState.step && isLatest) {
188
167
  editIconAfterAppend.style.setProperty('display', 'flex', 'important');
189
168
 
190
- // Debug: Check spacing
191
169
  setTimeout(() => {
192
170
  const computedMargin = window.getComputedStyle(editIconAfterAppend).marginLeft;
193
171
  const computedDisplay = window.getComputedStyle(editIconAfterAppend).display;
@@ -205,7 +183,6 @@ function addMessage(content, type = 'bot', hasInput = false, stepNumber = null)
205
183
  // =============================================================================
206
184
 
207
185
  function updateEditIcons() {
208
- // Find all rendered bot message wrappers
209
186
  const allBotMessages = elements.messages.querySelectorAll('[data-chat-element="bot-message-wrapper"]');
210
187
 
211
188
  console.log(`\nšŸ”„ Updating edit icons - Current step: ${chatState.step}, Completed: ${chatState.completed}`);
@@ -216,20 +193,16 @@ function updateEditIcons() {
216
193
  const isLatest = wrapper.hasAttribute('data-chat-latest');
217
194
 
218
195
  if (editIcon && !isNaN(stepNumber)) {
219
- // Check if this step has input by looking at the flow
220
196
  const stepData = flowData.flow[stepNumber];
221
197
  const hasInput = stepData && !!stepData.input;
222
198
 
223
- // Hide ALL icons if flow is completed
224
199
  if (chatState.completed) {
225
200
  editIcon.style.setProperty('display', 'none', 'important');
226
201
  console.log(` šŸ Step ${stepNumber}: Icon HIDDEN (flow completed)`);
227
202
  return;
228
203
  }
229
204
 
230
- // Only show icon if: has input AND is previous step AND is latest instance
231
205
  if (hasInput && stepNumber < chatState.step && isLatest) {
232
- // Set onclick handler
233
206
  editIcon.onclick = (e) => {
234
207
  e.stopPropagation();
235
208
  e.preventDefault();
@@ -257,6 +230,288 @@ function updateEditIcons() {
257
230
  });
258
231
  }
259
232
 
233
+ // =============================================================================
234
+ // MULTI-SELECT DROPDOWN RENDERING - COMPLETE FIXED VERSION
235
+ // =============================================================================
236
+
237
+ function renderMultiSelectDropdown(options, field) {
238
+ if (!elements.messages) return;
239
+
240
+ console.log(`\nšŸŽØ renderMultiSelectDropdown for field: ${field}`);
241
+ console.log(`Options:`, options);
242
+
243
+ const template = document.querySelector('[data-chat-element="multi-select-dropdown"]');
244
+ if (!template) {
245
+ console.error('multi-select-dropdown template not found');
246
+ return;
247
+ }
248
+
249
+ const clone = template.cloneNode(true);
250
+ clone.style.display = '';
251
+ clone.setAttribute('data-field', field);
252
+
253
+ const dropdown = clone.querySelector('[data-chat-input-element="dropdown"]');
254
+ const optionsWrapper = clone.querySelector('[data-chat-input-element="dropdown-options-wrapper"]');
255
+ const searchBar = clone.querySelector('[data-chat-input-element="search-bar"]');
256
+ const optionsContainer = clone.querySelector('.lisi-app--dropdown_options-list');
257
+ const optionTemplate = clone.querySelector('[data-chat-input-element="dropdown-option-wrapper"]');
258
+ const tagsContainer = clone.querySelector('[data-chat-input-element="tags-container"]');
259
+ const tagTemplate = clone.querySelector('[data-chat-input-element="tag-wrapper"]');
260
+
261
+ if (!dropdown || !optionsWrapper || !searchBar || !optionsContainer || !optionTemplate || !tagsContainer || !tagTemplate) {
262
+ console.error('Multi-select dropdown: Missing required elements');
263
+ console.error({
264
+ dropdown: !!dropdown,
265
+ optionsWrapper: !!optionsWrapper,
266
+ searchBar: !!searchBar,
267
+ optionsContainer: !!optionsContainer,
268
+ optionTemplate: !!optionTemplate,
269
+ tagsContainer: !!tagsContainer,
270
+ tagTemplate: !!tagTemplate
271
+ });
272
+ return;
273
+ }
274
+
275
+ const optionTemplateClone = optionTemplate.cloneNode(true);
276
+ const tagTemplateClone = tagTemplate.cloneNode(true);
277
+ optionTemplate.remove();
278
+ tagTemplate.remove();
279
+
280
+ // āœ… Get existing data for pre-filling (edit mode)
281
+ const existingData = chatState.data[field] || [];
282
+ const selectedValues = new Set(existingData);
283
+ console.log(`šŸ“ Pre-filling selections for ${field}:`, Array.from(selectedValues));
284
+
285
+ const allOptions = [];
286
+
287
+ // Render options
288
+ options.forEach(option => {
289
+ const optionEl = optionTemplateClone.cloneNode(true);
290
+ optionEl.style.display = '';
291
+ optionEl.setAttribute('data-value', JSON.stringify(option.value));
292
+ optionEl.setAttribute('data-name', option.name);
293
+
294
+ const nameEl = optionEl.querySelector('[data-chat-input-element="dropdown-option-name"]');
295
+ const tickIcon = optionEl.querySelector('[data-chat-input-element="dropdown-option-tick-icon"]');
296
+
297
+ if (nameEl) nameEl.textContent = option.name;
298
+
299
+ // āœ… Check if pre-selected (for edit mode)
300
+ const isSelected = selectedValues.has(option.value);
301
+ if (isSelected) {
302
+ optionEl.style.backgroundColor = config.selectedBackground;
303
+ if (tickIcon) {
304
+ // āœ… FIX: Use display: block instead of empty string
305
+ tickIcon.style.display = 'block';
306
+ }
307
+ console.log(` āœ… Pre-selected: ${option.name}`);
308
+ } else {
309
+ if (tickIcon) {
310
+ tickIcon.style.display = 'none';
311
+ }
312
+ }
313
+
314
+ optionEl.onclick = (e) => {
315
+ e.stopPropagation();
316
+ toggleOption(option, optionEl, tickIcon, field);
317
+ };
318
+
319
+ optionsContainer.appendChild(optionEl);
320
+ allOptions.push({ element: optionEl, option });
321
+ });
322
+
323
+ dropdown.onclick = (e) => {
324
+ e.stopPropagation();
325
+ const isVisible = optionsWrapper.style.display !== 'none';
326
+
327
+ if (isVisible) {
328
+ optionsWrapper.style.display = 'none';
329
+ console.log(' šŸ™ˆ Dropdown closed');
330
+ } else {
331
+ optionsWrapper.style.display = '';
332
+ console.log(' šŸ‘ļø Dropdown opened');
333
+
334
+ setTimeout(() => {
335
+ searchBar.focus();
336
+ }, 100);
337
+
338
+ searchBar.value = '';
339
+ allOptions.forEach(({ element }) => {
340
+ element.style.display = '';
341
+ });
342
+ }
343
+ };
344
+
345
+ const closeDropdownHandler = (e) => {
346
+ if (!clone.contains(e.target)) {
347
+ if (optionsWrapper.style.display !== 'none') {
348
+ optionsWrapper.style.display = 'none';
349
+ console.log(' šŸ™ˆ Dropdown closed (clicked outside)');
350
+ }
351
+ }
352
+ };
353
+
354
+ document.addEventListener('click', closeDropdownHandler);
355
+ clone.dataset.closeHandler = 'attached';
356
+
357
+ searchBar.oninput = (e) => {
358
+ const searchTerm = e.target.value.toLowerCase();
359
+ console.log(`šŸ” Searching for: "${searchTerm}"`);
360
+
361
+ allOptions.forEach(({ element, option }) => {
362
+ const matchesName = option.name.toLowerCase().includes(searchTerm);
363
+ const matchesValue = String(option.value).toLowerCase().includes(searchTerm);
364
+ const matches = matchesName || matchesValue;
365
+
366
+ element.style.display = matches ? '' : 'none';
367
+ });
368
+ };
369
+
370
+ searchBar.onclick = (e) => {
371
+ e.stopPropagation();
372
+ };
373
+
374
+ function toggleOption(option, optionEl, tickIcon, field) {
375
+ const currentSelections = chatState.data[field] || [];
376
+ const isCurrentlySelected = currentSelections.includes(option.value);
377
+
378
+ console.log(`šŸŽÆ Toggle option: ${option.name}`, { isCurrentlySelected });
379
+
380
+ if (isCurrentlySelected) {
381
+ chatState.data[field] = currentSelections.filter(v => v !== option.value);
382
+ optionEl.style.backgroundColor = '';
383
+ if (tickIcon) {
384
+ // āœ… FIX: Explicitly set to 'none'
385
+ tickIcon.style.display = 'none';
386
+ }
387
+ console.log(` āŒ Deselected: ${option.name}`);
388
+ } else {
389
+ chatState.data[field] = [...currentSelections, option.value];
390
+ optionEl.style.backgroundColor = config.selectedBackground;
391
+ if (tickIcon) {
392
+ // āœ… FIX: Explicitly set to 'block'
393
+ tickIcon.style.display = 'block';
394
+ }
395
+ console.log(` āœ… Selected: ${option.name}`);
396
+ }
397
+
398
+ chatState.currentSelection = {
399
+ field,
400
+ value: chatState.data[field],
401
+ name: chatState.data[field].map(v => {
402
+ const opt = options.find(o => o.value === v);
403
+ return opt ? opt.name : v;
404
+ }).join(', ')
405
+ };
406
+
407
+ console.log(` šŸ“Š Current selections:`, chatState.data[field]);
408
+
409
+ renderTags(field, options, tagsContainer, tagTemplateClone, optionsContainer);
410
+
411
+ if (chatState.data[field].length > 0) {
412
+ enableNextButton();
413
+ } else {
414
+ // āœ… FIX: Check if input is required before disabling
415
+ const currentStep = flowData.flow[chatState.step];
416
+ const inputRequired = currentStep.inputRequired === true;
417
+
418
+ if (inputRequired) {
419
+ disableNextButton();
420
+ } else {
421
+ enableNextButton();
422
+ }
423
+ }
424
+ }
425
+
426
+ function renderTags(field, options, tagsContainer, tagTemplate, optionsContainer) {
427
+ const existingTags = tagsContainer.querySelectorAll('[data-chat-input-element="tag-wrapper"]');
428
+ existingTags.forEach(tag => {
429
+ if (tag.style.display !== 'none') {
430
+ tag.remove();
431
+ }
432
+ });
433
+
434
+ const selections = chatState.data[field] || [];
435
+ console.log(`šŸ·ļø Rendering ${selections.length} tags`);
436
+
437
+ selections.forEach(value => {
438
+ const option = options.find(o => o.value === value);
439
+ if (!option) return;
440
+
441
+ const tagEl = tagTemplate.cloneNode(true);
442
+ tagEl.style.display = '';
443
+ tagEl.setAttribute('data-value', JSON.stringify(value));
444
+
445
+ const textEl = tagEl.querySelector('[data-chat-input-element="tag-text"]');
446
+ const crossIcon = tagEl.querySelector('[data-chat-input-element="tag-cross-icon"]');
447
+
448
+ if (textEl) {
449
+ textEl.textContent = option.name;
450
+ }
451
+
452
+ if (crossIcon) {
453
+ crossIcon.style.display = 'block';
454
+ console.log(` āœ… Cross icon visible for: ${option.name}`);
455
+
456
+ crossIcon.onclick = (e) => {
457
+ e.stopPropagation();
458
+ console.log(`šŸ—‘ļø Removing tag: ${option.name}`);
459
+
460
+ chatState.data[field] = chatState.data[field].filter(v => v !== value);
461
+
462
+ const optionEl = optionsContainer.querySelector(`[data-value='${JSON.stringify(value)}']`);
463
+ if (optionEl) {
464
+ optionEl.style.backgroundColor = '';
465
+ const tickIcon = optionEl.querySelector('[data-chat-input-element="dropdown-option-tick-icon"]');
466
+ if (tickIcon) {
467
+ // āœ… FIX: Explicitly set to 'none'
468
+ tickIcon.style.display = 'none';
469
+ }
470
+ }
471
+
472
+ tagEl.remove();
473
+
474
+ chatState.currentSelection = {
475
+ field,
476
+ value: chatState.data[field],
477
+ name: chatState.data[field].map(v => {
478
+ const opt = options.find(o => o.value === v);
479
+ return opt ? opt.name : v;
480
+ }).join(', ')
481
+ };
482
+
483
+ console.log(` šŸ“Š Remaining selections:`, chatState.data[field]);
484
+
485
+ if (chatState.data[field].length === 0) {
486
+ // āœ… FIX: Check if input is required before disabling
487
+ const currentStep = flowData.flow[chatState.step];
488
+ const inputRequired = currentStep.inputRequired === true;
489
+
490
+ if (inputRequired) {
491
+ disableNextButton();
492
+ } else {
493
+ enableNextButton();
494
+ }
495
+ }
496
+ };
497
+ }
498
+
499
+ tagsContainer.appendChild(tagEl);
500
+ });
501
+ }
502
+
503
+ // āœ… Initial tag render if pre-filled (edit mode)
504
+ if (selectedValues.size > 0) {
505
+ renderTags(field, options, tagsContainer, tagTemplateClone, optionsContainer);
506
+ console.log(` āœ… Pre-filled ${selectedValues.size} selections, Next button enabled`);
507
+ }
508
+
509
+ elements.messages.appendChild(clone);
510
+ scrollToBottom();
511
+
512
+ console.log(`āœ… Multi-select dropdown rendered for ${field}`);
513
+ }
514
+
260
515
  // =============================================================================
261
516
  // OPTIONS RENDERING WITH CUSTOM ATTRIBUTES
262
517
  // =============================================================================
@@ -264,10 +519,7 @@ function updateEditIcons() {
264
519
  function renderOptions(options, field, isSingleSelect = true) {
265
520
  if (!elements.messages) return;
266
521
 
267
- // Determine input type attribute value
268
522
  const inputTypeAttr = isSingleSelect ? 'single-select-input' : 'multi-select-input';
269
-
270
- // Find existing option element in HTML by data-chat-element
271
523
  const optionSelector = `[data-chat-element="${inputTypeAttr}"]`;
272
524
  const existingOption = document.querySelector(optionSelector);
273
525
 
@@ -276,44 +528,34 @@ function renderOptions(options, field, isSingleSelect = true) {
276
528
  return;
277
529
  }
278
530
 
279
- // Get existing data for this field (for pre-filling when editing)
280
531
  const existingData = chatState.data[field];
281
532
  console.log(`šŸ“ Pre-filling ${field}:`, existingData);
282
533
 
283
- // Create wrapper to hold all options
284
534
  const optionsWrapper = document.createElement('div');
285
535
  optionsWrapper.setAttribute('data-chat-element', 'options-wrapper');
286
536
  optionsWrapper.style.display = 'flex';
287
537
  optionsWrapper.style.flexDirection = 'column';
288
- optionsWrapper.style.alignItems = 'flex-start'; // Don't stretch options
538
+ optionsWrapper.style.alignItems = 'flex-start';
289
539
  optionsWrapper.style.gap = '8px';
290
540
 
291
- // Clone and fill option element for each option
292
541
  options.forEach((option, index) => {
293
542
  const optionName = option.name || option;
294
543
  const optionValue = option.value !== undefined ? option.value : option;
295
544
  const valueStr = typeof optionValue === 'object' ?
296
545
  JSON.stringify(optionValue) : String(optionValue);
297
546
 
298
- // Clone existing option element
299
547
  const clone = existingOption.cloneNode(true);
300
-
301
- // Make clone visible (remove display:none from template)
302
548
  clone.style.display = '';
303
549
 
304
- // Check if this option should be pre-selected
305
550
  let shouldBeChecked = false;
306
551
  if (isSingleSelect) {
307
- // For single-select, check if value matches existing data
308
552
  shouldBeChecked = existingData !== undefined &&
309
553
  JSON.stringify(existingData) === JSON.stringify(optionValue);
310
554
  } else {
311
- // For multi-select, check if value is in array
312
555
  shouldBeChecked = Array.isArray(existingData) &&
313
556
  existingData.some(v => JSON.stringify(v) === JSON.stringify(optionValue));
314
557
  }
315
558
 
316
- // Set checked state
317
559
  if (shouldBeChecked) {
318
560
  clone.classList.add('cf-checked');
319
561
  clone.style.backgroundColor = config.selectedBackground;
@@ -323,56 +565,46 @@ function renderOptions(options, field, isSingleSelect = true) {
323
565
  clone.style.backgroundColor = 'transparent';
324
566
  }
325
567
 
326
- // Set data attributes on container
327
568
  clone.setAttribute('data-chat-element', inputTypeAttr);
328
569
  clone.setAttribute('data-field', field);
329
570
  clone.setAttribute('data-value', valueStr);
330
571
  clone.setAttribute('data-name', optionName);
331
572
  clone.setAttribute('data-index', index);
332
573
 
333
- // Find and set input
334
574
  const input = clone.querySelector('[data-chat-input-element="input"]');
335
575
  if (input) {
336
576
  input.setAttribute('data-chat-element', inputTypeAttr);
337
577
  input.name = field;
338
578
  input.value = valueStr;
339
- input.checked = shouldBeChecked; // Pre-check if needed
340
- input.onclick = (e) => e.stopPropagation(); // Prevent click bubbling
579
+ input.checked = shouldBeChecked;
580
+ input.onclick = (e) => e.stopPropagation();
341
581
  }
342
582
 
343
- // Find and set tick icon visibility
344
583
  const tickIcon = clone.querySelector('[data-chat-input-element="tick-icon"]');
345
584
  if (tickIcon) {
346
585
  tickIcon.style.display = shouldBeChecked ? 'block' : 'none';
347
586
  }
348
587
 
349
- // Find and set text
350
588
  const textElement = clone.querySelector('[data-chat-input-element="text"]');
351
589
  if (textElement) {
352
590
  textElement.textContent = optionName;
353
- textElement.style.display = ''; // Make sure text is visible
591
+ textElement.style.display = '';
354
592
  } else {
355
593
  console.error('Text element not found in option');
356
594
  }
357
595
 
358
- // Append to wrapper
359
596
  optionsWrapper.appendChild(clone);
360
597
  });
361
598
 
362
- // Append wrapper to messages
363
599
  elements.messages.appendChild(optionsWrapper);
364
600
  scrollToBottom();
365
601
 
366
- // Add click handlers with proper event handling
367
602
  const optionElements = optionsWrapper.querySelectorAll('[data-chat-input-element="container"]');
368
603
  optionElements.forEach(el => {
369
- // Clear any existing onclick
370
604
  el.onclick = null;
371
-
372
- // Add new click handler with stopPropagation
373
605
  el.onclick = (e) => {
374
- e.stopPropagation(); // Prevent bubbling
375
- e.preventDefault(); // Prevent default behavior
606
+ e.stopPropagation();
607
+ e.preventDefault();
376
608
  handleOptionClick(el, field, isSingleSelect);
377
609
  };
378
610
  });
@@ -385,7 +617,6 @@ function renderOptions(options, field, isSingleSelect = true) {
385
617
  function renderColorOptions(options, field) {
386
618
  if (!elements.messages) return;
387
619
 
388
- // Find existing color option element in HTML by data-chat-element
389
620
  const optionSelector = '[data-chat-element="multi-select-color"]';
390
621
  const existingOption = document.querySelector(optionSelector);
391
622
 
@@ -394,37 +625,29 @@ function renderColorOptions(options, field) {
394
625
  return;
395
626
  }
396
627
 
397
- // Get existing data for this field (for pre-filling when editing)
398
628
  const existingData = chatState.data[field];
399
629
  console.log(`šŸ“ Pre-filling ${field} (color):`, existingData);
400
630
 
401
- // Create wrapper to hold all options
402
631
  const optionsWrapper = document.createElement('div');
403
632
  optionsWrapper.setAttribute('data-chat-element', 'options-wrapper');
404
633
  optionsWrapper.style.display = 'flex';
405
634
  optionsWrapper.style.flexDirection = 'column';
406
- optionsWrapper.style.alignItems = 'flex-start'; // Don't stretch options
635
+ optionsWrapper.style.alignItems = 'flex-start';
407
636
  optionsWrapper.style.gap = '8px';
408
637
 
409
- // Clone and fill option element for each option
410
638
  options.forEach((option, index) => {
411
639
  const optionName = option.name || option;
412
640
  const optionValue = option.value !== undefined ? option.value : option;
413
- const optionColor = option.color || '#cccccc'; // Default gray if no color
641
+ const optionColor = option.color || '#cccccc';
414
642
  const valueStr = typeof optionValue === 'object' ?
415
643
  JSON.stringify(optionValue) : String(optionValue);
416
644
 
417
- // Clone existing option element
418
645
  const clone = existingOption.cloneNode(true);
419
-
420
- // Make clone visible (remove display:none from template)
421
646
  clone.style.display = '';
422
647
 
423
- // Check if this option should be pre-selected (multi-select behavior)
424
648
  const shouldBeChecked = Array.isArray(existingData) &&
425
649
  existingData.some(v => JSON.stringify(v) === JSON.stringify(optionValue));
426
650
 
427
- // Set checked state
428
651
  if (shouldBeChecked) {
429
652
  clone.classList.add('cf-checked');
430
653
  clone.style.backgroundColor = config.selectedBackground;
@@ -434,7 +657,6 @@ function renderColorOptions(options, field) {
434
657
  clone.style.backgroundColor = 'transparent';
435
658
  }
436
659
 
437
- // Set data attributes on container
438
660
  clone.setAttribute('data-chat-element', 'multi-select-color');
439
661
  clone.setAttribute('data-field', field);
440
662
  clone.setAttribute('data-value', valueStr);
@@ -442,59 +664,48 @@ function renderColorOptions(options, field) {
442
664
  clone.setAttribute('data-index', index);
443
665
  clone.setAttribute('data-color', optionColor);
444
666
 
445
- // Find and set input
446
667
  const input = clone.querySelector('[data-chat-input-element="input"]');
447
668
  if (input) {
448
669
  input.setAttribute('data-chat-element', 'multi-select-color');
449
670
  input.name = field;
450
671
  input.value = valueStr;
451
- input.checked = shouldBeChecked; // Pre-check if needed
452
- input.onclick = (e) => e.stopPropagation(); // Prevent click bubbling
672
+ input.checked = shouldBeChecked;
673
+ input.onclick = (e) => e.stopPropagation();
453
674
  }
454
675
 
455
- // Find and set tick icon visibility
456
676
  const tickIcon = clone.querySelector('[data-chat-input-element="tick-icon"]');
457
677
  if (tickIcon) {
458
678
  tickIcon.style.display = shouldBeChecked ? 'block' : 'none';
459
679
  }
460
680
 
461
- // Find and set color block
462
681
  const colorBlock = clone.querySelector('[data-chat-input-element="color-block"]');
463
682
  if (colorBlock) {
464
683
  colorBlock.style.backgroundColor = optionColor;
465
- colorBlock.style.display = ''; // Make sure it's visible
684
+ colorBlock.style.display = '';
466
685
  } else {
467
686
  console.warn('Color block element not found in multi-select-color template');
468
687
  }
469
688
 
470
- // Find and set text
471
689
  const textElement = clone.querySelector('[data-chat-input-element="text"]');
472
690
  if (textElement) {
473
691
  textElement.textContent = optionName;
474
- textElement.style.display = ''; // Make sure text is visible
692
+ textElement.style.display = '';
475
693
  } else {
476
694
  console.error('Text element not found in option');
477
695
  }
478
696
 
479
- // Append to wrapper
480
697
  optionsWrapper.appendChild(clone);
481
698
  });
482
699
 
483
- // Append wrapper to messages
484
700
  elements.messages.appendChild(optionsWrapper);
485
701
  scrollToBottom();
486
702
 
487
- // Add click handlers with proper event handling
488
703
  const optionElements = optionsWrapper.querySelectorAll('[data-chat-input-element="container"]');
489
704
  optionElements.forEach(el => {
490
- // Clear any existing onclick
491
705
  el.onclick = null;
492
-
493
- // Add new click handler with stopPropagation
494
706
  el.onclick = (e) => {
495
- e.stopPropagation(); // Prevent bubbling
496
- e.preventDefault(); // Prevent default behavior
497
- // Use multi-select behavior (isSingleSelect = false)
707
+ e.stopPropagation();
708
+ e.preventDefault();
498
709
  handleOptionClick(el, field, false);
499
710
  };
500
711
  });
@@ -507,7 +718,6 @@ function renderColorOptions(options, field) {
507
718
  function renderCustomSelectOptions(options, field, customConfig) {
508
719
  if (!elements.messages) return;
509
720
 
510
- // Find existing single-select option element
511
721
  const optionSelector = '[data-chat-element="single-select-input"]';
512
722
  const existingOption = document.querySelector(optionSelector);
513
723
 
@@ -516,19 +726,16 @@ function renderCustomSelectOptions(options, field, customConfig) {
516
726
  return;
517
727
  }
518
728
 
519
- // Get existing data for pre-filling
520
729
  const existingData = chatState.data[field];
521
730
  console.log(`šŸ“ Pre-filling ${field} (custom):`, existingData);
522
731
 
523
- // Create wrapper to hold all options
524
732
  const optionsWrapper = document.createElement('div');
525
733
  optionsWrapper.setAttribute('data-chat-element', 'options-wrapper');
526
734
  optionsWrapper.style.display = 'flex';
527
735
  optionsWrapper.style.flexDirection = 'column';
528
- optionsWrapper.style.alignItems = 'flex-start'; // Don't stretch options
736
+ optionsWrapper.style.alignItems = 'flex-start';
529
737
  optionsWrapper.style.gap = '8px';
530
738
 
531
- // Render regular options
532
739
  options.forEach((option, index) => {
533
740
  const optionName = option.name || option;
534
741
  const optionValue = option.value !== undefined ? option.value : option;
@@ -538,9 +745,8 @@ function renderCustomSelectOptions(options, field, customConfig) {
538
745
  const clone = existingOption.cloneNode(true);
539
746
  clone.style.display = '';
540
747
 
541
- // Check if this option should be pre-selected
542
748
  const shouldBeChecked = existingData !== undefined &&
543
- !Array.isArray(existingData) && // Exclude array (custom range)
749
+ !Array.isArray(existingData) &&
544
750
  JSON.stringify(existingData) === JSON.stringify(optionValue);
545
751
 
546
752
  if (shouldBeChecked) {
@@ -579,12 +785,10 @@ function renderCustomSelectOptions(options, field, customConfig) {
579
785
  optionsWrapper.appendChild(clone);
580
786
  });
581
787
 
582
- // Render custom option
583
788
  if (customConfig) {
584
789
  const customClone = existingOption.cloneNode(true);
585
790
  customClone.style.display = '';
586
791
 
587
- // Check if custom was selected before (data is array [min, max])
588
792
  const isCustomSelected = existingData !== undefined &&
589
793
  Array.isArray(existingData) &&
590
794
  existingData.length === 2;
@@ -624,15 +828,10 @@ function renderCustomSelectOptions(options, field, customConfig) {
624
828
  optionsWrapper.appendChild(customClone);
625
829
  }
626
830
 
627
- // Append wrapper to messages
628
831
  elements.messages.appendChild(optionsWrapper);
629
-
630
- // Render min/max inputs
631
832
  renderMinMaxInputs(field, customConfig, existingData);
632
-
633
833
  scrollToBottom();
634
834
 
635
- // Add click handlers for regular and custom options
636
835
  const optionElements = optionsWrapper.querySelectorAll('[data-chat-input-element="container"]');
637
836
  optionElements.forEach(el => {
638
837
  el.onclick = (e) => {
@@ -646,7 +845,6 @@ function renderCustomSelectOptions(options, field, customConfig) {
646
845
  function renderMinMaxInputs(field, customConfig, existingData) {
647
846
  if (!elements.messages || !customConfig) return;
648
847
 
649
- // Find min/max input templates
650
848
  const minSelector = '[data-chat-element="single-select-custom-min"]';
651
849
  const maxSelector = '[data-chat-element="single-select-custom-max"]';
652
850
  const minTemplate = document.querySelector(minSelector);
@@ -657,37 +855,32 @@ function renderMinMaxInputs(field, customConfig, existingData) {
657
855
  return;
658
856
  }
659
857
 
660
- // Remove existing rangeWrapper if it exists (for edit flow)
661
858
  const existingRangeWrapper = elements.messages.querySelector(`[data-chat-element="range-wrapper"][data-field="${field}"]`);
662
859
  if (existingRangeWrapper) {
663
860
  console.log(' šŸ—‘ļø Removing existing rangeWrapper for re-render');
664
861
  existingRangeWrapper.remove();
665
862
  }
666
863
 
667
- // Create wrapper for min/max
668
864
  const rangeWrapper = document.createElement('div');
669
865
  rangeWrapper.setAttribute('data-chat-element', 'range-wrapper');
670
866
  rangeWrapper.setAttribute('data-field', field);
671
- rangeWrapper.style.marginBottom = '16px'; // Add space below wrapper
867
+ rangeWrapper.style.marginBottom = '16px';
672
868
  rangeWrapper.style.display = 'flex';
673
- rangeWrapper.style.gap = '16px'; // Gap between min and max
869
+ rangeWrapper.style.gap = '16px';
674
870
  rangeWrapper.style.flexWrap = 'wrap';
675
871
 
676
- // Check if should show min/max (custom option selected)
677
- // Data is stored as array [min, max]
678
872
  const showMinMax = existingData !== undefined &&
679
873
  Array.isArray(existingData) &&
680
874
  existingData.length === 2;
681
875
 
682
- rangeWrapper.style.display = showMinMax ? 'flex' : 'none'; // Keep flex when visible!
876
+ rangeWrapper.style.display = showMinMax ? 'flex' : 'none';
683
877
 
684
- // Clone and setup min input
685
878
  const minClone = minTemplate.cloneNode(true);
686
879
  minClone.style.display = '';
687
- minClone.style.marginRight = '16px'; // Horizontal gap
688
- minClone.style.marginBottom = '12px'; // Bottom margin if wraps
689
- minClone.style.width = 'auto'; // Don't stretch
690
- minClone.style.flex = 'none'; // Don't grow or shrink
880
+ minClone.style.marginRight = '16px';
881
+ minClone.style.marginBottom = '12px';
882
+ minClone.style.width = 'auto';
883
+ minClone.style.flex = 'none';
691
884
  minClone.setAttribute('data-field', field);
692
885
 
693
886
  const minInput = minClone.querySelector('[data-chat-input-element="input"]');
@@ -698,19 +891,17 @@ function renderMinMaxInputs(field, customConfig, existingData) {
698
891
  minInput.min = customConfig.min !== undefined ? customConfig.min : 0;
699
892
  minInput.max = customConfig.max !== undefined ? customConfig.max : 100;
700
893
  minInput.value = showMinMax && existingData[0] !== undefined ? existingData[0] : '';
701
- // Removed placeholder
702
894
 
703
895
  if (showMinMax) {
704
896
  console.log(` āœ… Pre-filled min: ${existingData[0]}`);
705
897
  }
706
898
  }
707
899
 
708
- // Clone and setup max input
709
900
  const maxClone = maxTemplate.cloneNode(true);
710
901
  maxClone.style.display = '';
711
- maxClone.style.marginBottom = '12px'; // Bottom margin if wraps
712
- maxClone.style.width = 'auto'; // Don't stretch
713
- maxClone.style.flex = 'none'; // Don't grow or shrink
902
+ maxClone.style.marginBottom = '12px';
903
+ maxClone.style.width = 'auto';
904
+ maxClone.style.flex = 'none';
714
905
  maxClone.setAttribute('data-field', field);
715
906
 
716
907
  const maxInput = maxClone.querySelector('[data-chat-input-element="input"]');
@@ -721,7 +912,6 @@ function renderMinMaxInputs(field, customConfig, existingData) {
721
912
  maxInput.min = customConfig.min !== undefined ? customConfig.min : 0;
722
913
  maxInput.max = customConfig.max !== undefined ? customConfig.max : 100;
723
914
  maxInput.value = showMinMax && existingData[1] !== undefined ? existingData[1] : '';
724
- // Removed placeholder
725
915
 
726
916
  if (showMinMax) {
727
917
  console.log(` āœ… Pre-filled max: ${existingData[1]}`);
@@ -731,7 +921,6 @@ function renderMinMaxInputs(field, customConfig, existingData) {
731
921
  rangeWrapper.appendChild(minClone);
732
922
  rangeWrapper.appendChild(maxClone);
733
923
 
734
- // Add error message display
735
924
  const errorDiv = document.createElement('div');
736
925
  errorDiv.setAttribute('data-chat-element', 'range-error');
737
926
  errorDiv.setAttribute('data-field', field);
@@ -743,18 +932,15 @@ function renderMinMaxInputs(field, customConfig, existingData) {
743
932
 
744
933
  elements.messages.appendChild(rangeWrapper);
745
934
 
746
- // Add handlers for both inputs
747
935
  if (minInput && maxInput) {
748
936
  const updateVisualFeedback = () => {
749
937
  const minFilled = minInput.value.trim() !== '';
750
938
  const maxFilled = maxInput.value.trim() !== '';
751
939
  const anyFilled = minFilled || maxFilled;
752
940
 
753
- // Get tick icons
754
941
  const minTick = minClone.querySelector('[data-chat-input-element="tick-icon"]');
755
942
  const maxTick = maxClone.querySelector('[data-chat-input-element="tick-icon"]');
756
943
 
757
- // Update background and tick for BOTH inputs
758
944
  if (anyFilled) {
759
945
  minClone.style.backgroundColor = config.selectedBackground;
760
946
  maxClone.style.backgroundColor = config.selectedBackground;
@@ -768,10 +954,8 @@ function renderMinMaxInputs(field, customConfig, existingData) {
768
954
  }
769
955
  };
770
956
 
771
- // Apply visual feedback immediately if pre-filled
772
957
  if (showMinMax) {
773
958
  updateVisualFeedback();
774
- // Also run validation to set currentSelection
775
959
  validateMinMax(field, customConfig);
776
960
  console.log(' āœ… Applied visual feedback for pre-filled values');
777
961
  }
@@ -797,18 +981,15 @@ function renderMinMaxInputs(field, customConfig, existingData) {
797
981
  }
798
982
 
799
983
  function selectCustomOption(field) {
800
- // Deselect all regular options
801
984
  const allOptions = document.querySelectorAll(`[data-field="${field}"][data-chat-element="single-select-input"]`);
802
985
  allOptions.forEach(opt => {
803
986
  const isCustom = opt.getAttribute('data-is-custom') === 'true';
804
987
  if (isCustom) {
805
- // Select custom option
806
988
  opt.classList.add('cf-checked');
807
989
  opt.style.setProperty('background-color', config.selectedBackground, 'important');
808
990
  const tickIcon = opt.querySelector('[data-chat-input-element="tick-icon"]');
809
991
  if (tickIcon) tickIcon.style.setProperty('display', 'block', 'important');
810
992
  } else {
811
- // Deselect regular options
812
993
  opt.classList.remove('cf-checked');
813
994
  opt.style.setProperty('background-color', 'transparent', 'important');
814
995
  const tickIcon = opt.querySelector('[data-chat-input-element="tick-icon"]');
@@ -816,10 +997,9 @@ function selectCustomOption(field) {
816
997
  }
817
998
  });
818
999
 
819
- // Show min/max inputs
820
1000
  const rangeWrapper = document.querySelector(`[data-chat-element="range-wrapper"][data-field="${field}"]`);
821
1001
  if (rangeWrapper) {
822
- rangeWrapper.style.display = 'flex'; // Keep flex!
1002
+ rangeWrapper.style.display = 'flex';
823
1003
  }
824
1004
  }
825
1005
 
@@ -830,7 +1010,6 @@ function handleCustomSelectClick(element, field, customConfig) {
830
1010
 
831
1011
  console.log(`Custom-select clicked:`, { field, name: optionName, isCustom });
832
1012
 
833
- // Deselect all options first
834
1013
  const allOptions = document.querySelectorAll(`[data-field="${field}"][data-chat-element="single-select-input"]`);
835
1014
  allOptions.forEach(opt => {
836
1015
  opt.classList.remove('cf-checked');
@@ -839,37 +1018,25 @@ function handleCustomSelectClick(element, field, customConfig) {
839
1018
  if (tickIcon) tickIcon.style.setProperty('display', 'none', 'important');
840
1019
  });
841
1020
 
842
- // Select this option
843
1021
  element.classList.add('cf-checked');
844
1022
  element.style.setProperty('background-color', config.selectedBackground, 'important');
845
1023
  const tickIcon = element.querySelector('[data-chat-input-element="tick-icon"]');
846
1024
  if (tickIcon) tickIcon.style.setProperty('display', 'block', 'important');
847
1025
 
848
1026
  if (isCustom) {
849
- // Show min/max inputs
850
1027
  const rangeWrapper = document.querySelector(`[data-chat-element="range-wrapper"][data-field="${field}"]`);
851
- console.log(' šŸ“¦ Looking for rangeWrapper:', `[data-chat-element="range-wrapper"][data-field="${field}"]`);
852
- console.log(' šŸ“¦ Found rangeWrapper:', rangeWrapper);
853
1028
 
854
1029
  if (rangeWrapper) {
855
- console.log(' šŸ“¦ Current display:', rangeWrapper.style.display);
856
- rangeWrapper.style.display = 'flex'; // Keep flex!
857
- console.log(' āœ… Set rangeWrapper display to flex');
858
- console.log(' šŸ“¦ New display:', rangeWrapper.style.display);
859
- } else {
860
- console.log(' āŒ rangeWrapper not found!');
1030
+ rangeWrapper.style.display = 'flex';
861
1031
  }
862
1032
 
863
- // Don't save data yet - wait for validation
864
1033
  chatState.currentSelection = null;
865
1034
  console.log(' ā„¹ļø Custom selected - waiting for min/max input');
866
1035
  } else {
867
- // Hide min/max inputs and clear them
868
1036
  const rangeWrapper = document.querySelector(`[data-chat-element="range-wrapper"][data-field="${field}"]`);
869
1037
  if (rangeWrapper) {
870
1038
  rangeWrapper.style.display = 'none';
871
1039
 
872
- // Clear input values - fixed selectors
873
1040
  const minInput = document.querySelector(`[data-field="${field}"][data-input-type="min"][data-chat-input-element="input"]`);
874
1041
  const maxInput = document.querySelector(`[data-field="${field}"][data-input-type="max"][data-chat-input-element="input"]`);
875
1042
 
@@ -882,7 +1049,6 @@ function handleCustomSelectClick(element, field, customConfig) {
882
1049
  console.log(' 🧹 Cleared max input');
883
1050
  }
884
1051
 
885
- // Reset visual feedback (background and ticks)
886
1052
  const minContainer = rangeWrapper.querySelector('[data-chat-element="single-select-custom-min"]');
887
1053
  const maxContainer = rangeWrapper.querySelector('[data-chat-element="single-select-custom-max"]');
888
1054
 
@@ -898,14 +1064,12 @@ function handleCustomSelectClick(element, field, customConfig) {
898
1064
  if (maxTick) maxTick.style.display = 'none';
899
1065
  }
900
1066
 
901
- // Hide error message
902
1067
  const errorDiv = rangeWrapper.querySelector('[data-chat-element="range-error"]');
903
1068
  if (errorDiv) errorDiv.style.display = 'none';
904
1069
 
905
1070
  console.log(' 🧹 Min/max inputs cleared and hidden');
906
1071
  }
907
1072
 
908
- // Parse and save regular value
909
1073
  let value;
910
1074
  try {
911
1075
  value = JSON.parse(valueStr);
@@ -923,21 +1087,14 @@ function handleCustomSelectClick(element, field, customConfig) {
923
1087
  function validateMinMax(field, customConfig) {
924
1088
  console.log(`\nšŸ” === VALIDATING MIN/MAX for field: ${field} ===`);
925
1089
 
926
- // Always use global config errors
927
1090
  const errors = config.customRangeErrors;
928
1091
 
929
- // Fixed selectors - input has all attributes on it directly
930
1092
  const minInput = document.querySelector(`[data-field="${field}"][data-input-type="min"][data-chat-input-element="input"]`);
931
1093
  const maxInput = document.querySelector(`[data-field="${field}"][data-input-type="max"][data-chat-input-element="input"]`);
932
1094
  const errorDiv = document.querySelector(`[data-chat-element="range-error"][data-field="${field}"]`);
933
1095
 
934
1096
  if (!minInput || !maxInput) {
935
1097
  console.log('āŒ Min or Max input not found');
936
- console.log(' Tried selectors:');
937
- console.log(` Min: [data-field="${field}"][data-input-type="min"][data-chat-input-element="input"]`);
938
- console.log(` Max: [data-field="${field}"][data-input-type="max"][data-chat-input-element="input"]`);
939
- console.log(' Found min:', minInput);
940
- console.log(' Found max:', maxInput);
941
1098
  return { valid: false, error: null };
942
1099
  }
943
1100
 
@@ -957,28 +1114,22 @@ function validateMinMax(field, customConfig) {
957
1114
  Min constraint: >= ${minConstraint}
958
1115
  Max constraint: <= ${maxConstraint}`);
959
1116
 
960
- // Helper to show error in div and clear data
961
1117
  const showErrorDiv = (message) => {
962
1118
  if (errorDiv) {
963
1119
  errorDiv.textContent = message;
964
1120
  errorDiv.style.display = 'block';
965
1121
  }
966
- // Clear the data when validation fails
967
1122
  delete chatState.data[field];
968
1123
  chatState.currentSelection = null;
969
1124
  console.log(`šŸ—‘ļø Data cleared for field: ${field}`);
970
1125
  };
971
1126
 
972
- // Helper to hide error div
973
1127
  const hideErrorDiv = () => {
974
1128
  if (errorDiv) {
975
1129
  errorDiv.style.display = 'none';
976
1130
  }
977
1131
  };
978
-
979
- // Validation rules - return error message if invalid
980
1132
 
981
- // 1. Check if only one is filled
982
1133
  if (minFilled && !maxFilled) {
983
1134
  const error = errors.maxRequired;
984
1135
  showErrorDiv(error);
@@ -995,7 +1146,6 @@ function validateMinMax(field, customConfig) {
995
1146
  return { valid: false, error };
996
1147
  }
997
1148
 
998
- // 2. Check if both are empty
999
1149
  if (!minFilled && !maxFilled) {
1000
1150
  hideErrorDiv();
1001
1151
  delete chatState.data[field];
@@ -1005,7 +1155,6 @@ function validateMinMax(field, customConfig) {
1005
1155
  return { valid: false, error: null };
1006
1156
  }
1007
1157
 
1008
- // 3. Check if values are valid numbers
1009
1158
  if (isNaN(minValue) || isNaN(maxValue)) {
1010
1159
  const error = errors.bothRequired;
1011
1160
  showErrorDiv(error);
@@ -1014,7 +1163,6 @@ function validateMinMax(field, customConfig) {
1014
1163
  return { valid: false, error };
1015
1164
  }
1016
1165
 
1017
- // 4. Check min constraint
1018
1166
  console.log(`šŸ” Checking: ${minValue} < ${minConstraint}?`);
1019
1167
  if (minValue < minConstraint) {
1020
1168
  const error = errors.minBelowConstraint.replace('{min}', minConstraint);
@@ -1025,7 +1173,6 @@ function validateMinMax(field, customConfig) {
1025
1173
  }
1026
1174
  console.log(`āœ… PASS: Min (${minValue}) >= constraint (${minConstraint})`);
1027
1175
 
1028
- // 5. Check max constraint
1029
1176
  console.log(`šŸ” Checking: ${maxValue} > ${maxConstraint}?`);
1030
1177
  if (maxValue > maxConstraint) {
1031
1178
  const error = errors.maxAboveConstraint.replace('{max}', maxConstraint);
@@ -1036,7 +1183,6 @@ function validateMinMax(field, customConfig) {
1036
1183
  }
1037
1184
  console.log(`āœ… PASS: Max (${maxValue}) <= constraint (${maxConstraint})`);
1038
1185
 
1039
- // 6. Check min < max
1040
1186
  console.log(`šŸ” Checking: ${minValue} >= ${maxValue}?`);
1041
1187
  if (minValue >= maxValue) {
1042
1188
  const error = errors.minGreaterThanMax;
@@ -1047,20 +1193,16 @@ function validateMinMax(field, customConfig) {
1047
1193
  }
1048
1194
  console.log(`āœ… PASS: Min (${minValue}) < Max (${maxValue})`);
1049
1195
 
1050
- // All valid! Hide error and save data as array
1051
1196
  hideErrorDiv();
1052
1197
 
1053
- // Store as array [min, max]
1054
1198
  chatState.data[field] = [minValue, maxValue];
1055
1199
 
1056
- // Get the current step's input config for formatting
1057
1200
  const currentStep = flowData.flow[chatState.step];
1058
1201
  const inputConfig = currentStep?.input || {};
1059
1202
  const valueType = inputConfig.selectedInputValueType;
1060
1203
  const prefix = inputConfig.selectedInputPrefix || '';
1061
1204
  const suffix = inputConfig.selectedInputSuffix || '';
1062
1205
 
1063
- // Format display name
1064
1206
  let displayName = `${minValue}-${maxValue}`;
1065
1207
  if (valueType === 'arrayRange') {
1066
1208
  const formattedMin = minValue.toLocaleString();
@@ -1068,7 +1210,6 @@ function validateMinMax(field, customConfig) {
1068
1210
  displayName = `${formattedMin} - ${formattedMax}`;
1069
1211
  }
1070
1212
 
1071
- // Add prefix and suffix with spaces
1072
1213
  if (prefix) displayName = `${prefix} ${displayName}`;
1073
1214
  if (suffix) displayName = `${displayName} ${suffix}`;
1074
1215
 
@@ -1094,10 +1235,7 @@ function validateMinMax(field, customConfig) {
1094
1235
  function renderTextInput(field, inputType = 'text', inputConfig = {}) {
1095
1236
  if (!elements.messages) return;
1096
1237
 
1097
- // Determine input type attribute value
1098
1238
  const inputTypeAttr = inputType === 'number' ? 'number-input' : 'text-input';
1099
-
1100
- // Find existing input element in HTML by data-chat-element
1101
1239
  const inputSelector = `[data-chat-element="${inputTypeAttr}"]`;
1102
1240
  const existingInput = document.querySelector(inputSelector);
1103
1241
 
@@ -1106,11 +1244,9 @@ function renderTextInput(field, inputType = 'text', inputConfig = {}) {
1106
1244
  return;
1107
1245
  }
1108
1246
 
1109
- // Get existing data for this field (for pre-filling when editing)
1110
1247
  const existingValue = chatState.data[field];
1111
1248
  console.log(`šŸ“ Pre-filling ${field}:`, existingValue);
1112
1249
 
1113
- // Check if validation should be applied (only if input has min/max defined)
1114
1250
  const hasValidation = inputType === 'number' &&
1115
1251
  (inputConfig.min !== undefined || inputConfig.max !== undefined);
1116
1252
 
@@ -1121,22 +1257,15 @@ function renderTextInput(field, inputType = 'text', inputConfig = {}) {
1121
1257
  });
1122
1258
  }
1123
1259
 
1124
- // Clone existing input element
1125
1260
  const clone = existingInput.cloneNode(true);
1126
-
1127
- // Make clone visible
1128
1261
  clone.style.display = '';
1129
-
1130
- // Set data attributes
1131
1262
  clone.setAttribute('data-field', field);
1132
1263
 
1133
- // Find the actual input element
1134
1264
  const inputElement = clone.querySelector('[data-chat-input-element="input"]');
1135
1265
  if (inputElement) {
1136
1266
  inputElement.setAttribute('data-field', field);
1137
1267
  inputElement.name = field;
1138
1268
 
1139
- // Set min/max attributes if validation enabled
1140
1269
  if (hasValidation) {
1141
1270
  if (inputConfig.min !== undefined) {
1142
1271
  inputElement.min = inputConfig.min;
@@ -1146,12 +1275,10 @@ function renderTextInput(field, inputType = 'text', inputConfig = {}) {
1146
1275
  }
1147
1276
  }
1148
1277
 
1149
- // Pre-fill value if it exists
1150
1278
  if (existingValue !== undefined && existingValue !== null) {
1151
1279
  inputElement.value = existingValue;
1152
1280
  console.log(` āœ… Pre-filled with: ${existingValue}`);
1153
1281
 
1154
- // If validation enabled, validate the pre-filled value
1155
1282
  if (hasValidation) {
1156
1283
  const value = parseFloat(existingValue);
1157
1284
  const min = inputConfig.min;
@@ -1175,20 +1302,16 @@ function renderTextInput(field, inputType = 'text', inputConfig = {}) {
1175
1302
 
1176
1303
  inputElement.type = inputType === 'number' ? 'number' : 'text';
1177
1304
 
1178
- // Add input event handler
1179
1305
  inputElement.oninput = (e) => {
1180
1306
  const value = inputType === 'number' ? parseFloat(e.target.value) : e.target.value;
1181
1307
 
1182
- // Save value
1183
1308
  chatState.data[field] = value;
1184
1309
  chatState.currentSelection = { field, value, name: value };
1185
1310
 
1186
- // Apply validation ONLY if min/max defined at input level
1187
1311
  if (hasValidation) {
1188
1312
  const min = inputConfig.min;
1189
1313
  const max = inputConfig.max;
1190
1314
 
1191
- // Check if value is valid
1192
1315
  let isValid = true;
1193
1316
  let errorMessage = '';
1194
1317
 
@@ -1211,17 +1334,14 @@ function renderTextInput(field, inputType = 'text', inputConfig = {}) {
1211
1334
  console.log(` āŒ Number input invalid: ${errorMessage}`);
1212
1335
  }
1213
1336
  } else {
1214
- // No validation - just log
1215
1337
  console.log(` šŸ“ Input updated: ${value} (no validation)`);
1216
1338
  }
1217
1339
  };
1218
1340
  }
1219
1341
 
1220
- // Append to messages
1221
1342
  elements.messages.appendChild(clone);
1222
1343
  scrollToBottom();
1223
1344
 
1224
- // Focus the input
1225
1345
  if (inputElement) {
1226
1346
  setTimeout(() => inputElement.focus(), 100);
1227
1347
  }
@@ -1232,7 +1352,6 @@ function renderTextInput(field, inputType = 'text', inputConfig = {}) {
1232
1352
  // =============================================================================
1233
1353
 
1234
1354
  function handleOptionClick(element, field, isSingleSelect) {
1235
- // Prevent rapid double-clicks
1236
1355
  const now = Date.now();
1237
1356
  const lastClick = element.getAttribute('data-last-click');
1238
1357
  if (lastClick && (now - parseInt(lastClick)) < 300) {
@@ -1241,7 +1360,6 @@ function handleOptionClick(element, field, isSingleSelect) {
1241
1360
  }
1242
1361
  element.setAttribute('data-last-click', now.toString());
1243
1362
 
1244
- // Find elements using custom attributes
1245
1363
  const tickIcon = element.querySelector('[data-chat-input-element="tick-icon"]');
1246
1364
  const input = element.querySelector('[data-chat-input-element="input"]');
1247
1365
  const textElement = element.querySelector('[data-chat-input-element="text"]');
@@ -1257,7 +1375,6 @@ function handleOptionClick(element, field, isSingleSelect) {
1257
1375
  name: optionName
1258
1376
  });
1259
1377
 
1260
- // Parse value
1261
1378
  let value;
1262
1379
  try {
1263
1380
  value = JSON.parse(valueStr);
@@ -1266,7 +1383,6 @@ function handleOptionClick(element, field, isSingleSelect) {
1266
1383
  }
1267
1384
 
1268
1385
  if (isSingleSelect) {
1269
- // Uncheck all others with same field and input type
1270
1386
  const selector = `[data-field="${field}"][data-chat-element="${inputType}"]`;
1271
1387
  const allOptions = document.querySelectorAll(selector);
1272
1388
 
@@ -1281,18 +1397,15 @@ function handleOptionClick(element, field, isSingleSelect) {
1281
1397
  if (otherInput) otherInput.checked = false;
1282
1398
  });
1283
1399
 
1284
- // Check this one
1285
1400
  element.classList.add('cf-checked');
1286
1401
  element.style.setProperty('background-color', config.selectedBackground, 'important');
1287
1402
  if (tickIcon) tickIcon.style.setProperty('display', 'block', 'important');
1288
1403
  if (input) input.checked = true;
1289
1404
 
1290
- // Save value
1291
1405
  chatState.data[field] = value;
1292
1406
  chatState.currentSelection = { field, value, name: optionName };
1293
1407
 
1294
1408
  } else {
1295
- // Multi-select
1296
1409
  const isChecked = !element.classList.contains('cf-checked');
1297
1410
 
1298
1411
  if (isChecked) {
@@ -1303,7 +1416,6 @@ function handleOptionClick(element, field, isSingleSelect) {
1303
1416
  }
1304
1417
  if (input) input.checked = true;
1305
1418
 
1306
- // Add to array
1307
1419
  if (!Array.isArray(chatState.data[field])) {
1308
1420
  chatState.data[field] = [];
1309
1421
  }
@@ -1322,7 +1434,6 @@ function handleOptionClick(element, field, isSingleSelect) {
1322
1434
  if (tickIcon) tickIcon.style.setProperty('display', 'none', 'important');
1323
1435
  if (input) input.checked = false;
1324
1436
 
1325
- // Remove from array
1326
1437
  if (Array.isArray(chatState.data[field])) {
1327
1438
  chatState.data[field] = chatState.data[field].filter(v =>
1328
1439
  JSON.stringify(v) !== JSON.stringify(value)
@@ -1332,7 +1443,6 @@ function handleOptionClick(element, field, isSingleSelect) {
1332
1443
  console.log(`āŒ Removed "${optionName}" - Total selected:`, chatState.data[field]);
1333
1444
  }
1334
1445
 
1335
- // Get all selected names using custom attribute
1336
1446
  const selector = `[data-field="${field}"][data-chat-element="${inputType}"].cf-checked`;
1337
1447
  const selectedOptions = document.querySelectorAll(selector);
1338
1448
  const selectedNames = Array.from(selectedOptions).map(opt =>
@@ -1346,7 +1456,6 @@ function handleOptionClick(element, field, isSingleSelect) {
1346
1456
  };
1347
1457
  }
1348
1458
 
1349
- // Enable Next button
1350
1459
  enableNextButton();
1351
1460
  }
1352
1461
 
@@ -1357,7 +1466,6 @@ function handleOptionClick(element, field, isSingleSelect) {
1357
1466
  async function handleNext() {
1358
1467
  const currentStep = flowData.flow[chatState.step];
1359
1468
 
1360
- // Check if input is required (default false)
1361
1469
  const inputRequired = currentStep.inputRequired === true;
1362
1470
 
1363
1471
  if (inputRequired && !chatState.currentSelection) {
@@ -1365,7 +1473,6 @@ async function handleNext() {
1365
1473
  return;
1366
1474
  }
1367
1475
 
1368
- // VALIDATION: Check for single-select-custom validation before proceeding
1369
1476
  if (currentStep.inputType === 'single-select-custom' && currentStep.input) {
1370
1477
  const field = currentStep.input.field;
1371
1478
  const customConfig = currentStep.input.custom;
@@ -1374,8 +1481,6 @@ async function handleNext() {
1374
1481
  console.log(`Field: ${field}`);
1375
1482
  console.log(`Custom config:`, customConfig);
1376
1483
 
1377
- // Check if the CUSTOM OPTION is actually selected (not just if value is array)
1378
- // Regular options can also have array values!
1379
1484
  const customOptionElement = document.querySelector(
1380
1485
  `[data-field="${field}"][data-is-custom="true"][data-chat-element="single-select-input"]`
1381
1486
  );
@@ -1387,7 +1492,6 @@ async function handleNext() {
1387
1492
  console.log(`Is custom option selected? ${isCustomOptionSelected}`);
1388
1493
 
1389
1494
  if (isCustomOptionSelected && customConfig) {
1390
- // Validate the custom range one more time before proceeding
1391
1495
  console.log(`šŸ”„ Running final validation before proceeding...`);
1392
1496
 
1393
1497
  const validation = validateMinMax(field, customConfig);
@@ -1395,11 +1499,9 @@ async function handleNext() {
1395
1499
  console.log(`Validation result:`, validation);
1396
1500
 
1397
1501
  if (!validation.valid) {
1398
- // If there's an error message, show it
1399
1502
  if (validation.error) {
1400
1503
  console.log(' āš ļø Validation failed in handleNext, showing error message');
1401
1504
 
1402
- // Hide current inputs (options and range wrapper)
1403
1505
  const optionsWrapper = document.querySelector('[data-chat-element="options-wrapper"]');
1404
1506
  if (optionsWrapper) {
1405
1507
  optionsWrapper.style.display = 'none';
@@ -1410,28 +1512,22 @@ async function handleNext() {
1410
1512
  rangeWrapper.style.display = 'none';
1411
1513
  }
1412
1514
 
1413
- // Add error message as bot message
1414
1515
  addMessage(validation.error, 'bot', false, null);
1415
-
1416
- // Re-display the same step with fresh inputs
1417
1516
  await showNextStep();
1418
1517
  } else {
1419
- // No error message, just incomplete data - do nothing
1420
1518
  console.log(' āš ļø Validation incomplete, waiting for user input');
1421
1519
  }
1422
1520
 
1423
- return; // Stop here, don't proceed
1521
+ return;
1424
1522
  }
1425
1523
 
1426
1524
  console.log('āœ… Validation passed, proceeding to next step');
1427
1525
  } else if (customConfig && chatState.currentSelection && chatState.currentSelection.value === customConfig.value) {
1428
- // User selected the custom option but hasn't entered valid min/max yet
1429
1526
  console.log(' āš ļø Custom option selected but no valid range entered');
1430
- return; // Stop here
1527
+ return;
1431
1528
  }
1432
1529
  }
1433
1530
 
1434
- // Call onNext validation if exists
1435
1531
  if (currentStep.onNext) {
1436
1532
  try {
1437
1533
  disableNextButton();
@@ -1452,9 +1548,7 @@ async function handleNext() {
1452
1548
  }
1453
1549
  }
1454
1550
 
1455
- // Add user message (only if selection was made)
1456
1551
  if (chatState.currentSelection) {
1457
- // Get the step's input config
1458
1552
  const inputConfig = currentStep.input || {};
1459
1553
  const valueType = inputConfig.selectedInputValueType;
1460
1554
  const prefix = inputConfig.selectedInputPrefix || '';
@@ -1462,7 +1556,6 @@ async function handleNext() {
1462
1556
 
1463
1557
  let displayName = chatState.currentSelection.name;
1464
1558
 
1465
- // Format based on valueType
1466
1559
  if (valueType === 'arrayRange' && Array.isArray(chatState.currentSelection.value)) {
1467
1560
  const [min, max] = chatState.currentSelection.value;
1468
1561
  const formattedMin = min.toLocaleString();
@@ -1470,13 +1563,11 @@ async function handleNext() {
1470
1563
  displayName = `${formattedMin} - ${formattedMax}`;
1471
1564
  }
1472
1565
 
1473
- // Add prefix and suffix with spaces
1474
1566
  if (prefix) displayName = `${prefix} ${displayName}`;
1475
1567
  if (suffix) displayName = `${displayName} ${suffix}`;
1476
1568
 
1477
1569
  addMessage(displayName, 'user', false, chatState.step);
1478
1570
 
1479
- // Save to history
1480
1571
  chatState.history.push({
1481
1572
  step: chatState.step,
1482
1573
  field: chatState.currentSelection.field,
@@ -1485,32 +1576,25 @@ async function handleNext() {
1485
1576
  });
1486
1577
  }
1487
1578
 
1488
- // Reset selection
1489
1579
  chatState.currentSelection = null;
1490
1580
  disableNextButton();
1491
1581
 
1492
- // Check if we should return to a saved step (after editing)
1493
1582
  if (chatState.returnToStep !== null) {
1494
1583
  const targetStep = chatState.returnToStep;
1495
1584
  console.log(`\nšŸ”™ Returning to saved step ${targetStep} (edited step complete)`);
1496
- chatState.returnToStep = null; // Clear the return step
1585
+ chatState.returnToStep = null;
1497
1586
 
1498
- // Check if target step is valid
1499
1587
  if (targetStep >= 0 && targetStep < flowData.flow.length) {
1500
1588
  chatState.step = targetStep;
1501
1589
  console.log(` āœ… Jumped to step ${targetStep}`);
1502
1590
 
1503
- // Hide all range-wrappers
1504
1591
  const allRangeWrappers = document.querySelectorAll('[data-chat-element="range-wrapper"]');
1505
1592
  allRangeWrappers.forEach(wrapper => {
1506
1593
  wrapper.style.display = 'none';
1507
1594
  });
1508
1595
  console.log(' šŸ™ˆ Hidden all range-wrappers');
1509
1596
 
1510
- // Update edit icons for the new position
1511
1597
  updateEditIcons();
1512
-
1513
- // Show the step we returned to
1514
1598
  await showNextStep();
1515
1599
  return;
1516
1600
  } else {
@@ -1518,33 +1602,27 @@ async function handleNext() {
1518
1602
  }
1519
1603
  }
1520
1604
 
1521
- // Normal flow: Move to next step
1522
1605
  chatState.step++;
1523
1606
 
1524
- // Hide all range-wrappers (custom min/max inputs)
1525
1607
  const allRangeWrappers = document.querySelectorAll('[data-chat-element="range-wrapper"]');
1526
1608
  allRangeWrappers.forEach(wrapper => {
1527
1609
  wrapper.style.display = 'none';
1528
1610
  });
1529
1611
  console.log(' šŸ™ˆ Hidden all range-wrappers');
1530
1612
 
1531
- // Update edit icons for all previous steps
1532
1613
  updateEditIcons();
1533
1614
 
1534
- // Check if finished
1535
1615
  if (chatState.step >= flowData.flow.length) {
1536
1616
  handleCompletion();
1537
1617
  return;
1538
1618
  }
1539
1619
 
1540
- // Show next step
1541
1620
  await showNextStep();
1542
1621
  }
1543
1622
 
1544
1623
  async function showNextStep() {
1545
1624
  const nextStep = flowData.flow[chatState.step];
1546
1625
 
1547
- // Call onStart if exists
1548
1626
  if (nextStep.onStart) {
1549
1627
  try {
1550
1628
  const canStart = await nextStep.onStart(chatState.data);
@@ -1558,87 +1636,81 @@ async function showNextStep() {
1558
1636
  }
1559
1637
  }
1560
1638
 
1561
- // Hide previous inputs (options, text inputs, number inputs)
1562
1639
  const previousInputs = elements.messages.querySelectorAll(
1563
1640
  '[data-chat-element="options-wrapper"], ' +
1564
1641
  '[data-chat-element="text-input"]:not([style*="display: none"]), ' +
1565
- '[data-chat-element="number-input"]:not([style*="display: none"])'
1642
+ '[data-chat-element="number-input"]:not([style*="display: none"]), ' +
1643
+ '[data-chat-element="multi-select-dropdown"]:not([style*="display: none"])'
1566
1644
  );
1567
1645
  previousInputs.forEach(input => {
1568
1646
  input.style.display = 'none';
1569
1647
  });
1570
1648
 
1571
- // Add bot message with edit icon if has input
1572
1649
  const hasInput = !!nextStep.input;
1573
1650
  addMessage(nextStep.message, 'bot', hasInput, chatState.step);
1574
1651
 
1575
- // Check if input is required (default false)
1576
1652
  const inputRequired = nextStep.inputRequired === true;
1577
1653
 
1578
- // Add options if input exists
1579
1654
  if (nextStep.input) {
1580
- const inputType = nextStep.inputType || 'single-select'; // Default to single-select
1655
+ const inputType = nextStep.inputType || 'single-select';
1581
1656
 
1582
1657
  if (inputType === 'text' || inputType === 'number') {
1583
- // Render text or number input
1584
1658
  renderTextInput(nextStep.input.field, inputType, nextStep.input);
1585
1659
 
1586
- // Check if validation is enabled (number input with min/max defined)
1587
1660
  const hasValidation = inputType === 'number' &&
1588
1661
  (nextStep.input.min !== undefined || nextStep.input.max !== undefined);
1589
1662
 
1590
- // Initial Next button state
1591
1663
  if (inputRequired || hasValidation) {
1592
- // Disable if input is required OR has validation rules
1593
1664
  disableNextButton();
1594
1665
  console.log(' šŸ”’ Next button disabled initially (input required or validation enabled)');
1595
1666
  } else {
1596
- // Enable if optional and no validation
1597
1667
  enableNextButton();
1598
1668
  console.log(' šŸ”“ Next button enabled (optional input, no validation)');
1599
1669
  }
1600
1670
  } else if (inputType === 'multi-select-color') {
1601
- // Render color options with color blocks
1602
1671
  renderColorOptions(nextStep.input.options, nextStep.input.field);
1603
1672
 
1604
- // Enable Next button if input not required (default behavior)
1605
1673
  if (!inputRequired) {
1606
1674
  enableNextButton();
1607
1675
  }
1608
1676
  } else if (inputType === 'single-select-custom') {
1609
- // Render single-select with custom min/max option
1610
1677
  renderCustomSelectOptions(
1611
1678
  nextStep.input.options,
1612
1679
  nextStep.input.field,
1613
1680
  nextStep.input.custom
1614
1681
  );
1615
1682
 
1616
- // Disable Next button initially (wait for selection/validation)
1617
1683
  if (inputRequired) {
1618
1684
  disableNextButton();
1619
1685
  } else {
1620
1686
  enableNextButton();
1621
1687
  }
1688
+ } else if (inputType === 'multi-select-dropdown') {
1689
+ // āœ… Render multi-select dropdown
1690
+ renderMultiSelectDropdown(nextStep.input.options, nextStep.input.field);
1691
+
1692
+ // āœ… FIX: Enable Next button initially if NOT required
1693
+ if (inputRequired) {
1694
+ disableNextButton();
1695
+ console.log(' šŸ”’ Next button disabled initially (input required)');
1696
+ } else {
1697
+ enableNextButton();
1698
+ console.log(' šŸ”“ Next button enabled (input NOT required)');
1699
+ }
1622
1700
  } else {
1623
- // Render options (single-select or multi-select)
1624
1701
  const isSingleSelect = inputType === 'single-select';
1625
1702
  renderOptions(nextStep.input.options, nextStep.input.field, isSingleSelect);
1626
1703
 
1627
- // Enable Next button if input not required (default behavior)
1628
1704
  if (!inputRequired) {
1629
1705
  enableNextButton();
1630
1706
  }
1631
1707
  }
1632
1708
  } else {
1633
- // No input - always auto-advance (nextButtonDisplay only controls button visibility)
1634
- // Use step-level autoAdvanceDelay if provided, otherwise use global config
1635
1709
  const delay = nextStep.autoAdvanceDelay !== undefined ? nextStep.autoAdvanceDelay : config.autoAdvanceDelay;
1636
1710
  console.log(` ā±ļø Auto-advance delay: ${delay}ms ${nextStep.autoAdvanceDelay !== undefined ? '(step-level)' : '(global)'}`);
1637
1711
 
1638
1712
  setTimeout(() => {
1639
1713
  chatState.step++;
1640
-
1641
- // Update edit icons after auto-advance
1642
1714
  updateEditIcons();
1643
1715
 
1644
1716
  if (chatState.step < flowData.flow.length) {
@@ -1649,7 +1721,6 @@ async function showNextStep() {
1649
1721
  }, delay);
1650
1722
  }
1651
1723
 
1652
- // Handle nextButtonDisplay - default is true
1653
1724
  const showNextButton = nextStep.nextButtonDisplay !== false;
1654
1725
  if (elements.nextBtn) {
1655
1726
  if (showNextButton) {
@@ -1661,7 +1732,6 @@ async function showNextStep() {
1661
1732
  }
1662
1733
  }
1663
1734
 
1664
- // Handle nextButtonText - update button text if specified at step level
1665
1735
  if (elements.nextBtn) {
1666
1736
  const nextBtnTextElement = elements.nextBtn.querySelector('[data-chat-element="next-button-text"]');
1667
1737
 
@@ -1670,19 +1740,15 @@ async function showNextStep() {
1670
1740
  nextBtnTextElement.textContent = nextStep.nextButtonText;
1671
1741
  console.log(` šŸ“ Next button text: "${nextStep.nextButtonText}" (step-level)`);
1672
1742
  } else {
1673
- // Restore original text
1674
1743
  nextBtnTextElement.textContent = elements.originalNextBtnText;
1675
1744
  console.log(` šŸ“ Next button text: "${elements.originalNextBtnText}" (original)`);
1676
1745
  }
1677
1746
  } else if (nextStep.nextButtonText) {
1678
- // Fallback: warn user if they're trying to set custom text but element doesn't exist
1679
1747
  console.warn(` āš ļø nextButtonText specified ("${nextStep.nextButtonText}") but next-button-text element not found`);
1680
1748
  console.warn(' Add <span data-chat-element="next-button-text">Next</span> inside your next-button');
1681
1749
  }
1682
1750
  }
1683
1751
 
1684
- // Always update edit icons at the end to ensure correct state
1685
- // Use setTimeout to ensure it runs after any DOM updates
1686
1752
  setTimeout(() => {
1687
1753
  updateEditIcons();
1688
1754
  }, 10);
@@ -1691,19 +1757,15 @@ async function showNextStep() {
1691
1757
  function handleCompletion() {
1692
1758
  console.log('āœ… Flow completed. Data:', chatState.data);
1693
1759
 
1694
- // Mark flow as completed
1695
1760
  chatState.completed = true;
1696
1761
  console.log(' šŸ Flow marked as completed');
1697
1762
 
1698
- // Hide all edit icons
1699
1763
  updateEditIcons();
1700
1764
 
1701
- // Hide Next button
1702
1765
  if (elements.nextBtn) {
1703
1766
  elements.nextBtn.style.display = 'none';
1704
1767
  }
1705
1768
 
1706
- // Trigger event
1707
1769
  if (typeof window !== 'undefined') {
1708
1770
  const event = new CustomEvent('conversationalFlowComplete', {
1709
1771
  detail: {
@@ -1714,7 +1776,6 @@ function handleCompletion() {
1714
1776
  window.dispatchEvent(event);
1715
1777
  }
1716
1778
 
1717
- // Call onComplete if provided
1718
1779
  if (flowData.onComplete && typeof flowData.onComplete === 'function') {
1719
1780
  flowData.onComplete(chatState.data);
1720
1781
  }
@@ -1725,7 +1786,6 @@ function handleCompletion() {
1725
1786
  // =============================================================================
1726
1787
 
1727
1788
  function init(flowName, flowConfig, options = {}) {
1728
- // Find container by data-chat-element="chat-wrapper"
1729
1789
  elements.container = document.querySelector('[data-chat-element="chat-wrapper"]');
1730
1790
 
1731
1791
  if (!elements.container) {
@@ -1734,19 +1794,15 @@ function init(flowName, flowConfig, options = {}) {
1734
1794
  return null;
1735
1795
  }
1736
1796
 
1737
- // Merge config
1738
1797
  if (flowConfig.config) {
1739
1798
  Object.assign(config, flowConfig.config);
1740
1799
  }
1741
1800
 
1742
- // Merge customRangeErrors - priority: options param > flow object > defaults
1743
- // 1. First, try flow object customRangeErrors
1744
1801
  if (flowConfig.customRangeErrors) {
1745
1802
  console.log('āœ… Using custom range errors from flow object');
1746
1803
  Object.assign(config.customRangeErrors, flowConfig.customRangeErrors);
1747
1804
  }
1748
1805
 
1749
- // 2. Then, options parameter (overrides flow object if both provided)
1750
1806
  if (options.customRangeErrors) {
1751
1807
  console.log('āœ… Using custom range errors from options parameter');
1752
1808
  Object.assign(config.customRangeErrors, options.customRangeErrors);
@@ -1759,20 +1815,16 @@ function init(flowName, flowConfig, options = {}) {
1759
1815
  customRangeErrors: config.customRangeErrors
1760
1816
  });
1761
1817
 
1762
- // Store flow data
1763
1818
  flowData = flowConfig;
1764
1819
 
1765
- // Initialize state
1766
1820
  chatState.step = 0;
1767
1821
  chatState.data = flowConfig.initialData || {};
1768
1822
  chatState.history = [];
1769
1823
  chatState.currentSelection = null;
1770
1824
 
1771
- // Find existing elements (don't create new ones)
1772
1825
  elements.messages = elements.container.querySelector('[data-chat-element="messages-container"]');
1773
1826
  elements.nextBtn = elements.container.querySelector('[data-chat-element="next-button"]');
1774
1827
 
1775
- // Validate required elements exist
1776
1828
  if (!elements.messages) {
1777
1829
  console.error('messages-container not found. Please add <div data-chat-element="messages-container"></div>');
1778
1830
  return null;
@@ -1783,10 +1835,8 @@ function init(flowName, flowConfig, options = {}) {
1783
1835
  return null;
1784
1836
  }
1785
1837
 
1786
- // Find the text element inside the button
1787
1838
  const nextBtnTextElement = elements.nextBtn.querySelector('[data-chat-element="next-button-text"]');
1788
1839
  if (nextBtnTextElement) {
1789
- // Store original button text from the text element
1790
1840
  elements.originalNextBtnText = nextBtnTextElement.textContent || nextBtnTextElement.innerText || 'Next';
1791
1841
  console.log(`šŸ’¾ Stored original button text: "${elements.originalNextBtnText}" (from next-button-text element)`);
1792
1842
  } else {
@@ -1795,23 +1845,18 @@ function init(flowName, flowConfig, options = {}) {
1795
1845
  elements.originalNextBtnText = 'Next';
1796
1846
  }
1797
1847
 
1798
- // Clear ONLY messages container (not entire chat-wrapper)
1799
1848
  elements.messages.innerHTML = '';
1800
1849
 
1801
- // Reset button state
1802
1850
  elements.nextBtn.disabled = true;
1803
1851
  elements.nextBtn.style.opacity = '0.5';
1804
1852
  elements.nextBtn.style.cursor = 'not-allowed';
1805
1853
 
1806
- // Add event listener
1807
1854
  elements.nextBtn.onclick = handleNext;
1808
1855
 
1809
- // Expose editStep globally for onclick handlers
1810
1856
  if (typeof window !== 'undefined') {
1811
1857
  window.editStep = editStep;
1812
1858
  }
1813
1859
 
1814
- // Start
1815
1860
  showNextStep();
1816
1861
 
1817
1862
  console.log(`āœ… Flow "${flowName}" initialized`);
@@ -1863,8 +1908,6 @@ function goToStep(stepNumber) {
1863
1908
  chatState.step = stepNumber;
1864
1909
  chatState.currentSelection = null;
1865
1910
 
1866
- // DON'T clear messages - keep chat history visible!
1867
- // User can see their previous answers
1868
1911
  console.log(` Keeping messages visible (not clearing)...`);
1869
1912
 
1870
1913
  console.log(` Disabling next button and calling showNextStep()...`);
@@ -1881,27 +1924,21 @@ function editStep(stepNumber) {
1881
1924
  return;
1882
1925
  }
1883
1926
 
1884
- // Save where we are now so we can return after editing
1885
1927
  chatState.returnToStep = chatState.step;
1886
1928
  console.log(` šŸ’¾ Saved return step: ${chatState.returnToStep}`);
1887
1929
 
1888
- // Reset completed flag since we're editing
1889
1930
  if (chatState.completed) {
1890
1931
  chatState.completed = false;
1891
1932
  console.log(` šŸ”“ Flow unmarked as completed (editing)`);
1892
1933
  }
1893
1934
 
1894
1935
  console.log(` Clearing history from step ${stepNumber} forward...`);
1895
- // Clear history from this step forward
1896
1936
  chatState.history = chatState.history.filter(h => h.step < stepNumber);
1897
1937
 
1898
1938
  console.log(` āœ… Keeping ALL data intact for editing (no data deleted)`);
1899
1939
  console.log(` Current data:`, chatState.data);
1900
- // Don't delete any data - keep everything for pre-filling
1901
- // Data will be replaced when user submits the edited step
1902
1940
 
1903
1941
  console.log(` Calling goToStep(${stepNumber})...`);
1904
- // Go to that step
1905
1942
  goToStep(stepNumber);
1906
1943
  }
1907
1944
 
@@ -1914,7 +1951,6 @@ function injectStyles() {
1914
1951
  const style = document.createElement('style');
1915
1952
  style.id = styleId;
1916
1953
  style.textContent = `
1917
- /* User message spacing and alignment */
1918
1954
  [data-chat-element="user-message-wrapper"] {
1919
1955
  margin-bottom: 16px;
1920
1956
  display: inline-flex;
@@ -1922,21 +1958,18 @@ function injectStyles() {
1922
1958
  align-self: flex-end;
1923
1959
  }
1924
1960
 
1925
- /* User message natural width from right */
1926
1961
  [data-chat-element="user-message-text"] {
1927
1962
  width: auto;
1928
1963
  display: inline-block;
1929
1964
  text-align: left;
1930
1965
  }
1931
1966
 
1932
- /* Messages container as flex column */
1933
1967
  [data-chat-element="messages-container"] {
1934
1968
  padding: 20px;
1935
1969
  display: flex;
1936
1970
  flex-direction: column;
1937
1971
  }
1938
1972
 
1939
- /* Options wrapper spacing and gap */
1940
1973
  [data-chat-element="options-wrapper"] {
1941
1974
  margin-bottom: 20px;
1942
1975
  display: flex;
@@ -1944,17 +1977,14 @@ function injectStyles() {
1944
1977
  gap: 12px;
1945
1978
  }
1946
1979
 
1947
- /* Text input spacing */
1948
1980
  [data-chat-element="text-input"] {
1949
1981
  margin-bottom: 20px;
1950
1982
  }
1951
1983
 
1952
- /* Number input spacing */
1953
1984
  [data-chat-element="number-input"] {
1954
1985
  margin-bottom: 20px;
1955
1986
  }
1956
1987
 
1957
- /* Bot edit icon styling */
1958
1988
  [data-chat-element="bot-edit-icon"] {
1959
1989
  display: none;
1960
1990
  margin-left: 8px !important;
@@ -1971,7 +2001,6 @@ function injectStyles() {
1971
2001
  document.head.appendChild(style);
1972
2002
  }
1973
2003
 
1974
- // Auto-inject styles when imported
1975
2004
  if (typeof document !== 'undefined') {
1976
2005
  if (document.readyState === 'loading') {
1977
2006
  document.addEventListener('DOMContentLoaded', injectStyles);