lisichatbot 1.3.7 → 1.3.9

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 +319 -287
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisichatbot",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "exports": {
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 + MULTI-SELECT-DROPDOWN
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 = {
@@ -58,16 +58,18 @@ function scrollToBottom() {
58
58
  function enableNextButton() {
59
59
  if (elements.nextBtn) {
60
60
  elements.nextBtn.disabled = false;
61
- elements.nextBtn.style.opacity = '1';
62
- elements.nextBtn.style.cursor = 'pointer';
61
+ elements.nextBtn.style.setProperty('opacity', '1', 'important');
62
+ elements.nextBtn.style.setProperty('cursor', 'pointer', 'important');
63
+ console.log(' āœ… Next button ENABLED (opacity: 1, cursor: pointer)');
63
64
  }
64
65
  }
65
66
 
66
67
  function disableNextButton() {
67
68
  if (elements.nextBtn) {
68
69
  elements.nextBtn.disabled = true;
69
- elements.nextBtn.style.opacity = '0.5';
70
- elements.nextBtn.style.cursor = 'not-allowed';
70
+ elements.nextBtn.style.setProperty('opacity', '0.5', 'important');
71
+ elements.nextBtn.style.setProperty('cursor', 'not-allowed', 'important');
72
+ console.log(' šŸ”’ Next button DISABLED (opacity: 0.5, cursor: not-allowed)');
71
73
  }
72
74
  }
73
75
 
@@ -78,7 +80,6 @@ function disableNextButton() {
78
80
  function addMessage(content, type = 'bot', hasInput = false, stepNumber = null) {
79
81
  if (!elements.messages) return;
80
82
 
81
- // Find the wrapper element directly by data-chat-element
82
83
  const wrapperSelector = `[data-chat-element="${type}-message-wrapper"]`;
83
84
  const existingWrapper = document.querySelector(wrapperSelector);
84
85
 
@@ -87,20 +88,16 @@ function addMessage(content, type = 'bot', hasInput = false, stepNumber = null)
87
88
  return;
88
89
  }
89
90
 
90
- // Clone the existing wrapper
91
91
  const clone = existingWrapper.cloneNode(true);
92
92
 
93
- // Immediately hide any edit icons in the clone (regardless of template state)
94
93
  const allEditIcons = clone.querySelectorAll('[data-chat-element="bot-edit-icon"]');
95
94
  allEditIcons.forEach(icon => {
96
95
  icon.style.setProperty('display', 'none', 'important');
97
96
  });
98
97
 
99
- // Add step number if provided
100
98
  if (stepNumber !== null) {
101
99
  clone.setAttribute('data-chat-step', stepNumber);
102
100
 
103
- // Mark all previous instances of this step as NOT latest
104
101
  const previousInstances = elements.messages.querySelectorAll(
105
102
  `[data-chat-element="${type}-message-wrapper"][data-chat-step="${stepNumber}"]`
106
103
  );
@@ -108,11 +105,9 @@ function addMessage(content, type = 'bot', hasInput = false, stepNumber = null)
108
105
  prev.removeAttribute('data-chat-latest');
109
106
  });
110
107
 
111
- // Mark this as the latest instance of this step
112
108
  clone.setAttribute('data-chat-latest', 'true');
113
109
  }
114
110
 
115
- // Find text element in clone and set content
116
111
  const textElement = clone.querySelector(`[data-chat-element="${type}-message-text"]`);
117
112
  if (textElement) {
118
113
  textElement.textContent = content;
@@ -120,7 +115,6 @@ function addMessage(content, type = 'bot', hasInput = false, stepNumber = null)
120
115
  console.error(`Element with data-chat-element="${type}-message-text" not found in wrapper`);
121
116
  }
122
117
 
123
- // Handle edit icon for bot messages
124
118
  if (type === 'bot') {
125
119
  console.log(`\nšŸ” Processing bot message - Step ${stepNumber || 'N/A'}, hasInput: ${hasInput}`);
126
120
 
@@ -128,14 +122,11 @@ function addMessage(content, type = 'bot', hasInput = false, stepNumber = null)
128
122
  if (editIcon) {
129
123
  console.log(' āœ“ Edit icon element found');
130
124
 
131
- // ALWAYS hide first with !important to override any CSS
132
125
  editIcon.style.setProperty('display', 'none', 'important');
133
126
  console.log(' āœ“ Edit icon hidden (display: none !important)');
134
127
 
135
- // Check if this is the latest instance (it should be since we just marked it)
136
128
  const isLatest = clone.hasAttribute('data-chat-latest');
137
129
 
138
- // Only show if this step has input AND it's a PREVIOUS step AND it's the latest instance
139
130
  if (hasInput && stepNumber !== null && stepNumber < chatState.step && isLatest) {
140
131
  editIcon.setAttribute('data-chat-step', stepNumber);
141
132
  editIcon.onclick = (e) => {
@@ -163,13 +154,9 @@ function addMessage(content, type = 'bot', hasInput = false, stepNumber = null)
163
154
  }
164
155
  }
165
156
 
166
- // Make clone visible
167
157
  clone.style.display = '';
168
-
169
- // Append to messages container
170
158
  elements.messages.appendChild(clone);
171
159
 
172
- // IMPORTANT: Set edit icon display AFTER appending to DOM
173
160
  if (type === 'bot') {
174
161
  const editIconAfterAppend = clone.querySelector('[data-chat-element="bot-edit-icon"]');
175
162
  if (editIconAfterAppend) {
@@ -246,206 +233,7 @@ function updateEditIcons() {
246
233
  }
247
234
 
248
235
  // =============================================================================
249
- // OPTIONS RENDERING WITH CUSTOM ATTRIBUTES
250
- // =============================================================================
251
-
252
- function renderOptions(options, field, isSingleSelect = true) {
253
- if (!elements.messages) return;
254
-
255
- const inputTypeAttr = isSingleSelect ? 'single-select-input' : 'multi-select-input';
256
- const optionSelector = `[data-chat-element="${inputTypeAttr}"]`;
257
- const existingOption = document.querySelector(optionSelector);
258
-
259
- if (!existingOption) {
260
- console.error(`Element with ${optionSelector} not found in HTML. Please add it to your HTML.`);
261
- return;
262
- }
263
-
264
- const existingData = chatState.data[field];
265
- console.log(`šŸ“ Pre-filling ${field}:`, existingData);
266
-
267
- const optionsWrapper = document.createElement('div');
268
- optionsWrapper.setAttribute('data-chat-element', 'options-wrapper');
269
- optionsWrapper.style.display = 'flex';
270
- optionsWrapper.style.flexDirection = 'column';
271
- optionsWrapper.style.alignItems = 'flex-start';
272
- optionsWrapper.style.gap = '8px';
273
-
274
- options.forEach((option, index) => {
275
- const optionName = option.name || option;
276
- const optionValue = option.value !== undefined ? option.value : option;
277
- const valueStr = typeof optionValue === 'object' ?
278
- JSON.stringify(optionValue) : String(optionValue);
279
-
280
- const clone = existingOption.cloneNode(true);
281
- clone.style.display = '';
282
-
283
- let shouldBeChecked = false;
284
- if (isSingleSelect) {
285
- shouldBeChecked = existingData !== undefined &&
286
- JSON.stringify(existingData) === JSON.stringify(optionValue);
287
- } else {
288
- shouldBeChecked = Array.isArray(existingData) &&
289
- existingData.some(v => JSON.stringify(v) === JSON.stringify(optionValue));
290
- }
291
-
292
- if (shouldBeChecked) {
293
- clone.classList.add('cf-checked');
294
- clone.style.backgroundColor = config.selectedBackground;
295
- console.log(` āœ… Pre-selected: ${optionName}`);
296
- } else {
297
- clone.classList.remove('cf-checked');
298
- clone.style.backgroundColor = 'transparent';
299
- }
300
-
301
- clone.setAttribute('data-chat-element', inputTypeAttr);
302
- clone.setAttribute('data-field', field);
303
- clone.setAttribute('data-value', valueStr);
304
- clone.setAttribute('data-name', optionName);
305
- clone.setAttribute('data-index', index);
306
-
307
- const input = clone.querySelector('[data-chat-input-element="input"]');
308
- if (input) {
309
- input.setAttribute('data-chat-element', inputTypeAttr);
310
- input.name = field;
311
- input.value = valueStr;
312
- input.checked = shouldBeChecked;
313
- input.onclick = (e) => e.stopPropagation();
314
- }
315
-
316
- const tickIcon = clone.querySelector('[data-chat-input-element="tick-icon"]');
317
- if (tickIcon) {
318
- tickIcon.style.display = shouldBeChecked ? 'block' : 'none';
319
- }
320
-
321
- const textElement = clone.querySelector('[data-chat-input-element="text"]');
322
- if (textElement) {
323
- textElement.textContent = optionName;
324
- textElement.style.display = '';
325
- } else {
326
- console.error('Text element not found in option');
327
- }
328
-
329
- optionsWrapper.appendChild(clone);
330
- });
331
-
332
- elements.messages.appendChild(optionsWrapper);
333
- scrollToBottom();
334
-
335
- const optionElements = optionsWrapper.querySelectorAll('[data-chat-input-element="container"]');
336
- optionElements.forEach(el => {
337
- el.onclick = null;
338
- el.onclick = (e) => {
339
- e.stopPropagation();
340
- e.preventDefault();
341
- handleOptionClick(el, field, isSingleSelect);
342
- };
343
- });
344
- }
345
-
346
- // =============================================================================
347
- // MULTI-SELECT-COLOR OPTIONS RENDERING
348
- // =============================================================================
349
-
350
- function renderColorOptions(options, field) {
351
- if (!elements.messages) return;
352
-
353
- const optionSelector = '[data-chat-element="multi-select-color"]';
354
- const existingOption = document.querySelector(optionSelector);
355
-
356
- if (!existingOption) {
357
- console.error(`Element with ${optionSelector} not found in HTML. Please add it to your HTML.`);
358
- return;
359
- }
360
-
361
- const existingData = chatState.data[field];
362
- console.log(`šŸ“ Pre-filling ${field} (color):`, existingData);
363
-
364
- const optionsWrapper = document.createElement('div');
365
- optionsWrapper.setAttribute('data-chat-element', 'options-wrapper');
366
- optionsWrapper.style.display = 'flex';
367
- optionsWrapper.style.flexDirection = 'column';
368
- optionsWrapper.style.alignItems = 'flex-start';
369
- optionsWrapper.style.gap = '8px';
370
-
371
- options.forEach((option, index) => {
372
- const optionName = option.name || option;
373
- const optionValue = option.value !== undefined ? option.value : option;
374
- const optionColor = option.color || '#cccccc';
375
- const valueStr = typeof optionValue === 'object' ?
376
- JSON.stringify(optionValue) : String(optionValue);
377
-
378
- const clone = existingOption.cloneNode(true);
379
- clone.style.display = '';
380
-
381
- const shouldBeChecked = Array.isArray(existingData) &&
382
- existingData.some(v => JSON.stringify(v) === JSON.stringify(optionValue));
383
-
384
- if (shouldBeChecked) {
385
- clone.classList.add('cf-checked');
386
- clone.style.backgroundColor = config.selectedBackground;
387
- console.log(` āœ… Pre-selected color: ${optionName}`);
388
- } else {
389
- clone.classList.remove('cf-checked');
390
- clone.style.backgroundColor = 'transparent';
391
- }
392
-
393
- clone.setAttribute('data-chat-element', 'multi-select-color');
394
- clone.setAttribute('data-field', field);
395
- clone.setAttribute('data-value', valueStr);
396
- clone.setAttribute('data-name', optionName);
397
- clone.setAttribute('data-index', index);
398
- clone.setAttribute('data-color', optionColor);
399
-
400
- const input = clone.querySelector('[data-chat-input-element="input"]');
401
- if (input) {
402
- input.setAttribute('data-chat-element', 'multi-select-color');
403
- input.name = field;
404
- input.value = valueStr;
405
- input.checked = shouldBeChecked;
406
- input.onclick = (e) => e.stopPropagation();
407
- }
408
-
409
- const tickIcon = clone.querySelector('[data-chat-input-element="tick-icon"]');
410
- if (tickIcon) {
411
- tickIcon.style.display = shouldBeChecked ? 'block' : 'none';
412
- }
413
-
414
- const colorBlock = clone.querySelector('[data-chat-input-element="color-block"]');
415
- if (colorBlock) {
416
- colorBlock.style.backgroundColor = optionColor;
417
- colorBlock.style.display = '';
418
- } else {
419
- console.warn('Color block element not found in multi-select-color template');
420
- }
421
-
422
- const textElement = clone.querySelector('[data-chat-input-element="text"]');
423
- if (textElement) {
424
- textElement.textContent = optionName;
425
- textElement.style.display = '';
426
- } else {
427
- console.error('Text element not found in option');
428
- }
429
-
430
- optionsWrapper.appendChild(clone);
431
- });
432
-
433
- elements.messages.appendChild(optionsWrapper);
434
- scrollToBottom();
435
-
436
- const optionElements = optionsWrapper.querySelectorAll('[data-chat-input-element="container"]');
437
- optionElements.forEach(el => {
438
- el.onclick = null;
439
- el.onclick = (e) => {
440
- e.stopPropagation();
441
- e.preventDefault();
442
- handleOptionClick(el, field, false);
443
- };
444
- });
445
- }
446
-
447
- // =============================================================================
448
- // MULTI-SELECT DROPDOWN RENDERING
236
+ // MULTI-SELECT DROPDOWN RENDERING - COMPLETE FIXED VERSION
449
237
  // =============================================================================
450
238
 
451
239
  function renderMultiSelectDropdown(options, field) {
@@ -454,44 +242,48 @@ function renderMultiSelectDropdown(options, field) {
454
242
  console.log(`\nšŸŽØ renderMultiSelectDropdown for field: ${field}`);
455
243
  console.log(`Options:`, options);
456
244
 
457
- // Find template
458
245
  const template = document.querySelector('[data-chat-element="multi-select-dropdown"]');
459
246
  if (!template) {
460
247
  console.error('multi-select-dropdown template not found');
461
248
  return;
462
249
  }
463
250
 
464
- // Clone template
465
251
  const clone = template.cloneNode(true);
466
252
  clone.style.display = '';
467
253
  clone.setAttribute('data-field', field);
468
254
 
469
- // Get all elements
470
255
  const dropdown = clone.querySelector('[data-chat-input-element="dropdown"]');
471
256
  const optionsWrapper = clone.querySelector('[data-chat-input-element="dropdown-options-wrapper"]');
472
257
  const searchBar = clone.querySelector('[data-chat-input-element="search-bar"]');
473
- const optionsContainer = clone.querySelector('[data-chat-input-element="dropdown-options-container"]');
258
+ const optionsContainer = clone.querySelector('.lisi-app--dropdown_options-list');
474
259
  const optionTemplate = clone.querySelector('[data-chat-input-element="dropdown-option-wrapper"]');
475
260
  const tagsContainer = clone.querySelector('[data-chat-input-element="tags-container"]');
476
261
  const tagTemplate = clone.querySelector('[data-chat-input-element="tag-wrapper"]');
477
262
 
478
263
  if (!dropdown || !optionsWrapper || !searchBar || !optionsContainer || !optionTemplate || !tagsContainer || !tagTemplate) {
479
264
  console.error('Multi-select dropdown: Missing required elements');
265
+ console.error({
266
+ dropdown: !!dropdown,
267
+ optionsWrapper: !!optionsWrapper,
268
+ searchBar: !!searchBar,
269
+ optionsContainer: !!optionsContainer,
270
+ optionTemplate: !!optionTemplate,
271
+ tagsContainer: !!tagsContainer,
272
+ tagTemplate: !!tagTemplate
273
+ });
480
274
  return;
481
275
  }
482
276
 
483
- // Store original template option and tag, then remove from DOM
484
277
  const optionTemplateClone = optionTemplate.cloneNode(true);
485
278
  const tagTemplateClone = tagTemplate.cloneNode(true);
486
279
  optionTemplate.remove();
487
280
  tagTemplate.remove();
488
281
 
489
- // Get existing selections for pre-filling
282
+ // āœ… Get existing data for pre-filling (edit mode)
490
283
  const existingData = chatState.data[field] || [];
491
284
  const selectedValues = new Set(existingData);
492
- console.log(`šŸ“ Pre-filling selections:`, Array.from(selectedValues));
285
+ console.log(`šŸ“ Pre-filling selections for ${field}:`, Array.from(selectedValues));
493
286
 
494
- // Store all options for filtering
495
287
  const allOptions = [];
496
288
 
497
289
  // Render options
@@ -506,15 +298,21 @@ function renderMultiSelectDropdown(options, field) {
506
298
 
507
299
  if (nameEl) nameEl.textContent = option.name;
508
300
 
509
- // Check if pre-selected
301
+ // āœ… Check if pre-selected (for edit mode)
510
302
  const isSelected = selectedValues.has(option.value);
511
303
  if (isSelected) {
512
304
  optionEl.style.backgroundColor = config.selectedBackground;
513
- if (tickIcon) tickIcon.style.display = '';
305
+ if (tickIcon) {
306
+ // āœ… FIX: Use display: block instead of empty string
307
+ tickIcon.style.display = 'block';
308
+ }
514
309
  console.log(` āœ… Pre-selected: ${option.name}`);
310
+ } else {
311
+ if (tickIcon) {
312
+ tickIcon.style.display = 'none';
313
+ }
515
314
  }
516
315
 
517
- // Click handler - prevents dropdown from closing
518
316
  optionEl.onclick = (e) => {
519
317
  e.stopPropagation();
520
318
  toggleOption(option, optionEl, tickIcon, field);
@@ -524,7 +322,6 @@ function renderMultiSelectDropdown(options, field) {
524
322
  allOptions.push({ element: optionEl, option });
525
323
  });
526
324
 
527
- // Toggle dropdown visibility - click to open/close
528
325
  dropdown.onclick = (e) => {
529
326
  e.stopPropagation();
530
327
  const isVisible = optionsWrapper.style.display !== 'none';
@@ -547,7 +344,6 @@ function renderMultiSelectDropdown(options, field) {
547
344
  }
548
345
  };
549
346
 
550
- // Close dropdown when clicking outside
551
347
  const closeDropdownHandler = (e) => {
552
348
  if (!clone.contains(e.target)) {
553
349
  if (optionsWrapper.style.display !== 'none') {
@@ -560,7 +356,6 @@ function renderMultiSelectDropdown(options, field) {
560
356
  document.addEventListener('click', closeDropdownHandler);
561
357
  clone.dataset.closeHandler = 'attached';
562
358
 
563
- // Search functionality - filters options by name or value
564
359
  searchBar.oninput = (e) => {
565
360
  const searchTerm = e.target.value.toLowerCase();
566
361
  console.log(`šŸ” Searching for: "${searchTerm}"`);
@@ -574,12 +369,10 @@ function renderMultiSelectDropdown(options, field) {
574
369
  });
575
370
  };
576
371
 
577
- // Prevent search bar clicks from closing dropdown
578
372
  searchBar.onclick = (e) => {
579
373
  e.stopPropagation();
580
374
  };
581
375
 
582
- // Toggle option selection
583
376
  function toggleOption(option, optionEl, tickIcon, field) {
584
377
  const currentSelections = chatState.data[field] || [];
585
378
  const isCurrentlySelected = currentSelections.includes(option.value);
@@ -587,20 +380,23 @@ function renderMultiSelectDropdown(options, field) {
587
380
  console.log(`šŸŽÆ Toggle option: ${option.name}`, { isCurrentlySelected });
588
381
 
589
382
  if (isCurrentlySelected) {
590
- // Deselect
591
383
  chatState.data[field] = currentSelections.filter(v => v !== option.value);
592
384
  optionEl.style.backgroundColor = '';
593
- if (tickIcon) tickIcon.style.display = 'none';
385
+ if (tickIcon) {
386
+ // āœ… FIX: Explicitly set to 'none'
387
+ tickIcon.style.display = 'none';
388
+ }
594
389
  console.log(` āŒ Deselected: ${option.name}`);
595
390
  } else {
596
- // Select
597
391
  chatState.data[field] = [...currentSelections, option.value];
598
392
  optionEl.style.backgroundColor = config.selectedBackground;
599
- if (tickIcon) tickIcon.style.display = '';
393
+ if (tickIcon) {
394
+ // āœ… FIX: Explicitly set to 'block'
395
+ tickIcon.style.display = 'block';
396
+ }
600
397
  console.log(` āœ… Selected: ${option.name}`);
601
398
  }
602
399
 
603
- // Update current selection for handleNext
604
400
  chatState.currentSelection = {
605
401
  field,
606
402
  value: chatState.data[field],
@@ -612,20 +408,24 @@ function renderMultiSelectDropdown(options, field) {
612
408
 
613
409
  console.log(` šŸ“Š Current selections:`, chatState.data[field]);
614
410
 
615
- // Re-render tags
616
411
  renderTags(field, options, tagsContainer, tagTemplateClone, optionsContainer);
617
412
 
618
- // Enable Next button if at least one selected
619
413
  if (chatState.data[field].length > 0) {
620
414
  enableNextButton();
621
415
  } else {
622
- disableNextButton();
416
+ // āœ… FIX: Check if input is required before disabling
417
+ const currentStep = flowData.flow[chatState.step];
418
+ const inputRequired = currentStep.inputRequired === true;
419
+
420
+ if (inputRequired) {
421
+ disableNextButton();
422
+ } else {
423
+ enableNextButton();
424
+ }
623
425
  }
624
426
  }
625
427
 
626
- // Render tags - displays selected options as removable tags
627
428
  function renderTags(field, options, tagsContainer, tagTemplate, optionsContainer) {
628
- // Clear existing tags (except template)
629
429
  const existingTags = tagsContainer.querySelectorAll('[data-chat-input-element="tag-wrapper"]');
630
430
  existingTags.forEach(tag => {
631
431
  if (tag.style.display !== 'none') {
@@ -651,32 +451,28 @@ function renderMultiSelectDropdown(options, field) {
651
451
  textEl.textContent = option.name;
652
452
  }
653
453
 
654
- // āœ… CRITICAL: Set cross icon display to BLOCK and handle click
655
454
  if (crossIcon) {
656
- // Set display to block (as requested by user)
657
455
  crossIcon.style.display = 'block';
658
456
  console.log(` āœ… Cross icon visible for: ${option.name}`);
659
457
 
660
- // Handle click to remove tag
661
458
  crossIcon.onclick = (e) => {
662
459
  e.stopPropagation();
663
460
  console.log(`šŸ—‘ļø Removing tag: ${option.name}`);
664
461
 
665
- // Remove from selections array
666
462
  chatState.data[field] = chatState.data[field].filter(v => v !== value);
667
463
 
668
- // Update option UI in dropdown - remove background and tick
669
464
  const optionEl = optionsContainer.querySelector(`[data-value='${JSON.stringify(value)}']`);
670
465
  if (optionEl) {
671
466
  optionEl.style.backgroundColor = '';
672
467
  const tickIcon = optionEl.querySelector('[data-chat-input-element="dropdown-option-tick-icon"]');
673
- if (tickIcon) tickIcon.style.display = 'none';
468
+ if (tickIcon) {
469
+ // āœ… FIX: Explicitly set to 'none'
470
+ tickIcon.style.display = 'none';
471
+ }
674
472
  }
675
473
 
676
- // Remove tag from DOM
677
474
  tagEl.remove();
678
475
 
679
- // Update current selection
680
476
  chatState.currentSelection = {
681
477
  field,
682
478
  value: chatState.data[field],
@@ -688,9 +484,16 @@ function renderMultiSelectDropdown(options, field) {
688
484
 
689
485
  console.log(` šŸ“Š Remaining selections:`, chatState.data[field]);
690
486
 
691
- // Disable Next button if no selections remain
692
487
  if (chatState.data[field].length === 0) {
693
- disableNextButton();
488
+ // āœ… FIX: Check if input is required before disabling
489
+ const currentStep = flowData.flow[chatState.step];
490
+ const inputRequired = currentStep.inputRequired === true;
491
+
492
+ if (inputRequired) {
493
+ disableNextButton();
494
+ } else {
495
+ enableNextButton();
496
+ }
694
497
  }
695
498
  };
696
499
  }
@@ -699,19 +502,217 @@ function renderMultiSelectDropdown(options, field) {
699
502
  });
700
503
  }
701
504
 
702
- // Initial tag render if pre-filled
505
+ // āœ… Initial tag render if pre-filled (edit mode)
703
506
  if (selectedValues.size > 0) {
704
507
  renderTags(field, options, tagsContainer, tagTemplateClone, optionsContainer);
705
- enableNextButton();
508
+ console.log(` āœ… Pre-filled ${selectedValues.size} selections, Next button enabled`);
706
509
  }
707
510
 
708
- // Append to messages
709
511
  elements.messages.appendChild(clone);
710
512
  scrollToBottom();
711
513
 
712
514
  console.log(`āœ… Multi-select dropdown rendered for ${field}`);
713
515
  }
714
516
 
517
+ // =============================================================================
518
+ // OPTIONS RENDERING WITH CUSTOM ATTRIBUTES
519
+ // =============================================================================
520
+
521
+ function renderOptions(options, field, isSingleSelect = true) {
522
+ if (!elements.messages) return;
523
+
524
+ const inputTypeAttr = isSingleSelect ? 'single-select-input' : 'multi-select-input';
525
+ const optionSelector = `[data-chat-element="${inputTypeAttr}"]`;
526
+ const existingOption = document.querySelector(optionSelector);
527
+
528
+ if (!existingOption) {
529
+ console.error(`Element with ${optionSelector} not found in HTML. Please add it to your HTML.`);
530
+ return;
531
+ }
532
+
533
+ const existingData = chatState.data[field];
534
+ console.log(`šŸ“ Pre-filling ${field}:`, existingData);
535
+
536
+ const optionsWrapper = document.createElement('div');
537
+ optionsWrapper.setAttribute('data-chat-element', 'options-wrapper');
538
+ optionsWrapper.style.display = 'flex';
539
+ optionsWrapper.style.flexDirection = 'column';
540
+ optionsWrapper.style.alignItems = 'flex-start';
541
+ optionsWrapper.style.gap = '8px';
542
+
543
+ options.forEach((option, index) => {
544
+ const optionName = option.name || option;
545
+ const optionValue = option.value !== undefined ? option.value : option;
546
+ const valueStr = typeof optionValue === 'object' ?
547
+ JSON.stringify(optionValue) : String(optionValue);
548
+
549
+ const clone = existingOption.cloneNode(true);
550
+ clone.style.display = '';
551
+
552
+ let shouldBeChecked = false;
553
+ if (isSingleSelect) {
554
+ shouldBeChecked = existingData !== undefined &&
555
+ JSON.stringify(existingData) === JSON.stringify(optionValue);
556
+ } else {
557
+ shouldBeChecked = Array.isArray(existingData) &&
558
+ existingData.some(v => JSON.stringify(v) === JSON.stringify(optionValue));
559
+ }
560
+
561
+ if (shouldBeChecked) {
562
+ clone.classList.add('cf-checked');
563
+ clone.style.backgroundColor = config.selectedBackground;
564
+ console.log(` āœ… Pre-selected: ${optionName}`);
565
+ } else {
566
+ clone.classList.remove('cf-checked');
567
+ clone.style.backgroundColor = 'transparent';
568
+ }
569
+
570
+ clone.setAttribute('data-chat-element', inputTypeAttr);
571
+ clone.setAttribute('data-field', field);
572
+ clone.setAttribute('data-value', valueStr);
573
+ clone.setAttribute('data-name', optionName);
574
+ clone.setAttribute('data-index', index);
575
+
576
+ const input = clone.querySelector('[data-chat-input-element="input"]');
577
+ if (input) {
578
+ input.setAttribute('data-chat-element', inputTypeAttr);
579
+ input.name = field;
580
+ input.value = valueStr;
581
+ input.checked = shouldBeChecked;
582
+ input.onclick = (e) => e.stopPropagation();
583
+ }
584
+
585
+ const tickIcon = clone.querySelector('[data-chat-input-element="tick-icon"]');
586
+ if (tickIcon) {
587
+ tickIcon.style.display = shouldBeChecked ? 'block' : 'none';
588
+ }
589
+
590
+ const textElement = clone.querySelector('[data-chat-input-element="text"]');
591
+ if (textElement) {
592
+ textElement.textContent = optionName;
593
+ textElement.style.display = '';
594
+ } else {
595
+ console.error('Text element not found in option');
596
+ }
597
+
598
+ optionsWrapper.appendChild(clone);
599
+ });
600
+
601
+ elements.messages.appendChild(optionsWrapper);
602
+ scrollToBottom();
603
+
604
+ const optionElements = optionsWrapper.querySelectorAll('[data-chat-input-element="container"]');
605
+ optionElements.forEach(el => {
606
+ el.onclick = null;
607
+ el.onclick = (e) => {
608
+ e.stopPropagation();
609
+ e.preventDefault();
610
+ handleOptionClick(el, field, isSingleSelect);
611
+ };
612
+ });
613
+ }
614
+
615
+ // =============================================================================
616
+ // MULTI-SELECT-COLOR OPTIONS RENDERING
617
+ // =============================================================================
618
+
619
+ function renderColorOptions(options, field) {
620
+ if (!elements.messages) return;
621
+
622
+ const optionSelector = '[data-chat-element="multi-select-color"]';
623
+ const existingOption = document.querySelector(optionSelector);
624
+
625
+ if (!existingOption) {
626
+ console.error(`Element with ${optionSelector} not found in HTML. Please add it to your HTML.`);
627
+ return;
628
+ }
629
+
630
+ const existingData = chatState.data[field];
631
+ console.log(`šŸ“ Pre-filling ${field} (color):`, existingData);
632
+
633
+ const optionsWrapper = document.createElement('div');
634
+ optionsWrapper.setAttribute('data-chat-element', 'options-wrapper');
635
+ optionsWrapper.style.display = 'flex';
636
+ optionsWrapper.style.flexDirection = 'column';
637
+ optionsWrapper.style.alignItems = 'flex-start';
638
+ optionsWrapper.style.gap = '8px';
639
+
640
+ options.forEach((option, index) => {
641
+ const optionName = option.name || option;
642
+ const optionValue = option.value !== undefined ? option.value : option;
643
+ const optionColor = option.color || '#cccccc';
644
+ const valueStr = typeof optionValue === 'object' ?
645
+ JSON.stringify(optionValue) : String(optionValue);
646
+
647
+ const clone = existingOption.cloneNode(true);
648
+ clone.style.display = '';
649
+
650
+ const shouldBeChecked = Array.isArray(existingData) &&
651
+ existingData.some(v => JSON.stringify(v) === JSON.stringify(optionValue));
652
+
653
+ if (shouldBeChecked) {
654
+ clone.classList.add('cf-checked');
655
+ clone.style.backgroundColor = config.selectedBackground;
656
+ console.log(` āœ… Pre-selected color: ${optionName}`);
657
+ } else {
658
+ clone.classList.remove('cf-checked');
659
+ clone.style.backgroundColor = 'transparent';
660
+ }
661
+
662
+ clone.setAttribute('data-chat-element', 'multi-select-color');
663
+ clone.setAttribute('data-field', field);
664
+ clone.setAttribute('data-value', valueStr);
665
+ clone.setAttribute('data-name', optionName);
666
+ clone.setAttribute('data-index', index);
667
+ clone.setAttribute('data-color', optionColor);
668
+
669
+ const input = clone.querySelector('[data-chat-input-element="input"]');
670
+ if (input) {
671
+ input.setAttribute('data-chat-element', 'multi-select-color');
672
+ input.name = field;
673
+ input.value = valueStr;
674
+ input.checked = shouldBeChecked;
675
+ input.onclick = (e) => e.stopPropagation();
676
+ }
677
+
678
+ const tickIcon = clone.querySelector('[data-chat-input-element="tick-icon"]');
679
+ if (tickIcon) {
680
+ tickIcon.style.display = shouldBeChecked ? 'block' : 'none';
681
+ }
682
+
683
+ const colorBlock = clone.querySelector('[data-chat-input-element="color-block"]');
684
+ if (colorBlock) {
685
+ colorBlock.style.backgroundColor = optionColor;
686
+ colorBlock.style.display = '';
687
+ } else {
688
+ console.warn('Color block element not found in multi-select-color template');
689
+ }
690
+
691
+ const textElement = clone.querySelector('[data-chat-input-element="text"]');
692
+ if (textElement) {
693
+ textElement.textContent = optionName;
694
+ textElement.style.display = '';
695
+ } else {
696
+ console.error('Text element not found in option');
697
+ }
698
+
699
+ optionsWrapper.appendChild(clone);
700
+ });
701
+
702
+ elements.messages.appendChild(optionsWrapper);
703
+ scrollToBottom();
704
+
705
+ const optionElements = optionsWrapper.querySelectorAll('[data-chat-input-element="container"]');
706
+ optionElements.forEach(el => {
707
+ el.onclick = null;
708
+ el.onclick = (e) => {
709
+ e.stopPropagation();
710
+ e.preventDefault();
711
+ handleOptionClick(el, field, false);
712
+ };
713
+ });
714
+ }
715
+
715
716
  // =============================================================================
716
717
  // SINGLE-SELECT-CUSTOM (WITH MIN/MAX RANGE)
717
718
  // =============================================================================
@@ -737,7 +738,6 @@ function renderCustomSelectOptions(options, field, customConfig) {
737
738
  optionsWrapper.style.alignItems = 'flex-start';
738
739
  optionsWrapper.style.gap = '8px';
739
740
 
740
- // Render regular options
741
741
  options.forEach((option, index) => {
742
742
  const optionName = option.name || option;
743
743
  const optionValue = option.value !== undefined ? option.value : option;
@@ -787,7 +787,6 @@ function renderCustomSelectOptions(options, field, customConfig) {
787
787
  optionsWrapper.appendChild(clone);
788
788
  });
789
789
 
790
- // Render custom option
791
790
  if (customConfig) {
792
791
  const customClone = existingOption.cloneNode(true);
793
792
  customClone.style.display = '';
@@ -1278,9 +1277,19 @@ function renderTextInput(field, inputType = 'text', inputConfig = {}) {
1278
1277
  }
1279
1278
  }
1280
1279
 
1281
- if (existingValue !== undefined && existingValue !== null) {
1280
+ // āœ… FIX: Handle pre-filled values for both validation and non-validation cases
1281
+ const hasValue = existingValue !== undefined && existingValue !== null && existingValue !== '';
1282
+
1283
+ if (hasValue) {
1282
1284
  inputElement.value = existingValue;
1283
- console.log(` āœ… Pre-filled with: ${existingValue}`);
1285
+ console.log(` āœ… Pre-filled with: "${existingValue}"`);
1286
+
1287
+ // Set currentSelection for pre-filled value
1288
+ chatState.currentSelection = {
1289
+ field,
1290
+ value: existingValue,
1291
+ name: existingValue.toString()
1292
+ };
1284
1293
 
1285
1294
  if (hasValidation) {
1286
1295
  const value = parseFloat(existingValue);
@@ -1293,14 +1302,30 @@ function renderTextInput(field, inputType = 'text', inputConfig = {}) {
1293
1302
 
1294
1303
  if (isValid) {
1295
1304
  enableNextButton();
1296
- console.log(` āœ… Pre-filled value is valid: ${value}`);
1305
+ console.log(` āœ… Pre-filled number is valid - Next button enabled`);
1297
1306
  } else {
1298
1307
  disableNextButton();
1299
- console.log(` āŒ Pre-filled value is invalid: ${value}`);
1308
+ console.log(` āŒ Pre-filled number is invalid - Next button disabled`);
1300
1309
  }
1310
+ } else {
1311
+ // āœ… FIX: Enable button for pre-filled text input
1312
+ enableNextButton();
1313
+ console.log(` āœ… Pre-filled text - Next button enabled`);
1301
1314
  }
1302
1315
  } else {
1316
+ // āœ… No pre-filled value - set initial button state
1303
1317
  inputElement.value = '';
1318
+
1319
+ const currentStep = flowData.flow[chatState.step];
1320
+ const inputRequired = currentStep.inputRequired === true;
1321
+
1322
+ if (inputRequired || hasValidation) {
1323
+ disableNextButton();
1324
+ console.log(` šŸ”’ Empty input (required or has validation) - Next button disabled`);
1325
+ } else {
1326
+ enableNextButton();
1327
+ console.log(` šŸ”“ Empty input (optional, no validation) - Next button enabled`);
1328
+ }
1304
1329
  }
1305
1330
 
1306
1331
  inputElement.type = inputType === 'number' ? 'number' : 'text';
@@ -1309,7 +1334,7 @@ function renderTextInput(field, inputType = 'text', inputConfig = {}) {
1309
1334
  const value = inputType === 'number' ? parseFloat(e.target.value) : e.target.value;
1310
1335
 
1311
1336
  chatState.data[field] = value;
1312
- chatState.currentSelection = { field, value, name: value };
1337
+ chatState.currentSelection = { field, value, name: value.toString() };
1313
1338
 
1314
1339
  if (hasValidation) {
1315
1340
  const min = inputConfig.min;
@@ -1318,7 +1343,7 @@ function renderTextInput(field, inputType = 'text', inputConfig = {}) {
1318
1343
  let isValid = true;
1319
1344
  let errorMessage = '';
1320
1345
 
1321
- if (value === '' || isNaN(value)) {
1346
+ if (e.target.value === '' || (inputType === 'number' && isNaN(value))) {
1322
1347
  isValid = false;
1323
1348
  errorMessage = 'Please enter a valid number';
1324
1349
  } else if (min !== undefined && value < min) {
@@ -1331,13 +1356,29 @@ function renderTextInput(field, inputType = 'text', inputConfig = {}) {
1331
1356
 
1332
1357
  if (isValid) {
1333
1358
  enableNextButton();
1334
- console.log(` āœ… Number input valid: ${value}`);
1359
+ console.log(` āœ… Number input valid: ${value} - Next button enabled`);
1335
1360
  } else {
1336
1361
  disableNextButton();
1337
- console.log(` āŒ Number input invalid: ${errorMessage}`);
1362
+ console.log(` āŒ Number input invalid: ${errorMessage} - Next button disabled`);
1338
1363
  }
1339
1364
  } else {
1340
- console.log(` šŸ“ Input updated: ${value} (no validation)`);
1365
+ // āœ… FIX: For text inputs without validation, enable button as user types
1366
+ if (e.target.value && e.target.value.trim() !== '') {
1367
+ enableNextButton();
1368
+ console.log(` āœ… Text input has value - Next button enabled`);
1369
+ } else {
1370
+ // āœ… Check if input is required
1371
+ const currentStep = flowData.flow[chatState.step];
1372
+ const inputRequired = currentStep.inputRequired === true;
1373
+
1374
+ if (inputRequired) {
1375
+ disableNextButton();
1376
+ console.log(` šŸ”’ Text input empty and required - Next button disabled`);
1377
+ } else {
1378
+ enableNextButton();
1379
+ console.log(` šŸ”“ Text input empty but not required - Next button enabled`);
1380
+ }
1381
+ }
1341
1382
  }
1342
1383
  };
1343
1384
  }
@@ -1476,7 +1517,6 @@ async function handleNext() {
1476
1517
  return;
1477
1518
  }
1478
1519
 
1479
- // VALIDATION: Check for single-select-custom validation before proceeding
1480
1520
  if (currentStep.inputType === 'single-select-custom' && currentStep.input) {
1481
1521
  const field = currentStep.input.field;
1482
1522
  const customConfig = currentStep.input.custom;
@@ -1659,18 +1699,8 @@ async function showNextStep() {
1659
1699
  const inputType = nextStep.inputType || 'single-select';
1660
1700
 
1661
1701
  if (inputType === 'text' || inputType === 'number') {
1702
+ // āœ… renderTextInput handles ALL button state logic (pre-filled, typing, validation)
1662
1703
  renderTextInput(nextStep.input.field, inputType, nextStep.input);
1663
-
1664
- const hasValidation = inputType === 'number' &&
1665
- (nextStep.input.min !== undefined || nextStep.input.max !== undefined);
1666
-
1667
- if (inputRequired || hasValidation) {
1668
- disableNextButton();
1669
- console.log(' šŸ”’ Next button disabled initially (input required or validation enabled)');
1670
- } else {
1671
- enableNextButton();
1672
- console.log(' šŸ”“ Next button enabled (optional input, no validation)');
1673
- }
1674
1704
  } else if (inputType === 'multi-select-color') {
1675
1705
  renderColorOptions(nextStep.input.options, nextStep.input.field);
1676
1706
 
@@ -1690,14 +1720,16 @@ async function showNextStep() {
1690
1720
  enableNextButton();
1691
1721
  }
1692
1722
  } else if (inputType === 'multi-select-dropdown') {
1693
- // Render multi-select dropdown
1723
+ // āœ… Render multi-select dropdown
1694
1724
  renderMultiSelectDropdown(nextStep.input.options, nextStep.input.field);
1695
1725
 
1696
- // Disable Next button initially if required
1726
+ // āœ… FIX: Enable Next button initially if NOT required
1697
1727
  if (inputRequired) {
1698
1728
  disableNextButton();
1729
+ console.log(' šŸ”’ Next button disabled initially (input required)');
1699
1730
  } else {
1700
1731
  enableNextButton();
1732
+ console.log(' šŸ”“ Next button enabled (input NOT required)');
1701
1733
  }
1702
1734
  } else {
1703
1735
  const isSingleSelect = inputType === 'single-select';