lisichatbot 1.0.0 → 1.0.2
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.
- package/package.json +1 -1
- package/src/index.js +158 -44
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversational Flow Builder
|
|
3
|
+
* A simple, flexible conversational flow builder for multi-step chat interfaces
|
|
4
|
+
* @version 1.0.0
|
|
5
|
+
* WITH CUSTOM DATA ATTRIBUTES
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// STATE MANAGEMENT
|
|
12
|
+
// =============================================================================
|
|
1
13
|
|
|
2
14
|
let chatState = {
|
|
3
15
|
step: 0,
|
|
@@ -52,19 +64,43 @@ function disableNextButton() {
|
|
|
52
64
|
// MESSAGE FUNCTIONS
|
|
53
65
|
// =============================================================================
|
|
54
66
|
|
|
55
|
-
function addMessage(content, type = 'bot') {
|
|
67
|
+
function addMessage(content, type = 'bot', isEditable = false, stepNumber = null) {
|
|
56
68
|
if (!elements.messages) return;
|
|
57
69
|
|
|
58
70
|
const messageDiv = document.createElement('div');
|
|
59
71
|
messageDiv.className = `cf-message cf-${type}`;
|
|
60
|
-
messageDiv.
|
|
72
|
+
messageDiv.setAttribute('data-chat-element', `${type}-message-wrapper`);
|
|
73
|
+
|
|
74
|
+
if (stepNumber !== null) {
|
|
75
|
+
messageDiv.setAttribute('data-chat-step', stepNumber);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let editIcon = '';
|
|
79
|
+
if (isEditable && type === 'user') {
|
|
80
|
+
editIcon = `
|
|
81
|
+
<button class="cf-edit-icon"
|
|
82
|
+
data-chat-element="edit-button"
|
|
83
|
+
data-chat-step="${stepNumber}"
|
|
84
|
+
onclick="editStep(${stepNumber})"
|
|
85
|
+
title="Edit this response">
|
|
86
|
+
✏️
|
|
87
|
+
</button>
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
messageDiv.innerHTML = `
|
|
92
|
+
<div class="cf-message-content" data-chat-element="${type}-message-text">
|
|
93
|
+
${content}
|
|
94
|
+
</div>
|
|
95
|
+
${editIcon}
|
|
96
|
+
`;
|
|
61
97
|
|
|
62
98
|
elements.messages.appendChild(messageDiv);
|
|
63
99
|
scrollToBottom();
|
|
64
100
|
}
|
|
65
101
|
|
|
66
102
|
// =============================================================================
|
|
67
|
-
// OPTIONS RENDERING
|
|
103
|
+
// OPTIONS RENDERING WITH CUSTOM ATTRIBUTES
|
|
68
104
|
// =============================================================================
|
|
69
105
|
|
|
70
106
|
function renderOptions(options, field, isSingleSelect = true) {
|
|
@@ -72,6 +108,10 @@ function renderOptions(options, field, isSingleSelect = true) {
|
|
|
72
108
|
|
|
73
109
|
const messageDiv = document.createElement('div');
|
|
74
110
|
messageDiv.className = 'cf-message cf-bot';
|
|
111
|
+
messageDiv.setAttribute('data-chat-message', 'bot');
|
|
112
|
+
|
|
113
|
+
// Determine input type attribute value
|
|
114
|
+
const inputTypeAttr = isSingleSelect ? 'single-select-input' : 'multi-select-input';
|
|
75
115
|
|
|
76
116
|
const optionsHtml = options.map((option, index) => {
|
|
77
117
|
const optionName = option.name || option;
|
|
@@ -81,45 +121,59 @@ function renderOptions(options, field, isSingleSelect = true) {
|
|
|
81
121
|
|
|
82
122
|
return `
|
|
83
123
|
<label class="cf-option ${isSingleSelect ? 'cf-single' : 'cf-multi'}"
|
|
124
|
+
data-chat-input-element="container"
|
|
125
|
+
data-chat-element="${inputTypeAttr}"
|
|
84
126
|
data-field="${field}"
|
|
85
127
|
data-value='${valueStr}'
|
|
86
128
|
data-name="${optionName}"
|
|
87
129
|
data-index="${index}">
|
|
88
|
-
|
|
130
|
+
|
|
131
|
+
<div class="cf-tick"
|
|
132
|
+
data-chat-input-element="tick-icon"
|
|
133
|
+
style="display: none;">✓</div>
|
|
134
|
+
|
|
89
135
|
<input type="${isSingleSelect ? 'radio' : 'checkbox'}"
|
|
90
136
|
name="${field}"
|
|
91
137
|
value="${valueStr}"
|
|
138
|
+
data-chat-input-element="input"
|
|
139
|
+
data-chat-element="${inputTypeAttr}"
|
|
92
140
|
style="display: none;">
|
|
93
|
-
|
|
141
|
+
|
|
142
|
+
<span class="cf-option-text"
|
|
143
|
+
data-chat-input-element="text">${optionName}</span>
|
|
94
144
|
</label>
|
|
95
145
|
`;
|
|
96
146
|
}).join('');
|
|
97
147
|
|
|
98
148
|
messageDiv.innerHTML = `
|
|
99
|
-
<div class="cf-message-content">
|
|
100
|
-
<div class="cf-options">${optionsHtml}</div>
|
|
149
|
+
<div class="cf-message-content" data-chat-element="bot-message-text">
|
|
150
|
+
<div class="cf-options" data-chat-element="options-wrapper">${optionsHtml}</div>
|
|
101
151
|
</div>
|
|
102
152
|
`;
|
|
103
153
|
|
|
104
154
|
elements.messages.appendChild(messageDiv);
|
|
105
155
|
scrollToBottom();
|
|
106
156
|
|
|
107
|
-
// Add click handlers
|
|
108
|
-
const optionElements = messageDiv.querySelectorAll('
|
|
157
|
+
// Add click handlers using custom attribute selector
|
|
158
|
+
const optionElements = messageDiv.querySelectorAll('[data-chat-input-element="container"]');
|
|
109
159
|
optionElements.forEach(el => {
|
|
110
160
|
el.onclick = () => handleOptionClick(el, field, isSingleSelect);
|
|
111
161
|
});
|
|
112
162
|
}
|
|
113
163
|
|
|
114
164
|
// =============================================================================
|
|
115
|
-
// OPTION CLICK HANDLER
|
|
165
|
+
// OPTION CLICK HANDLER USING CUSTOM ATTRIBUTES
|
|
116
166
|
// =============================================================================
|
|
117
167
|
|
|
118
168
|
function handleOptionClick(element, field, isSingleSelect) {
|
|
119
|
-
|
|
120
|
-
const
|
|
169
|
+
// Find elements using custom attributes
|
|
170
|
+
const tickIcon = element.querySelector('[data-chat-input-element="tick-icon"]');
|
|
171
|
+
const input = element.querySelector('[data-chat-input-element="input"]');
|
|
172
|
+
const textElement = element.querySelector('[data-chat-input-element="text"]');
|
|
173
|
+
|
|
121
174
|
const valueStr = element.getAttribute('data-value');
|
|
122
175
|
const optionName = element.getAttribute('data-name');
|
|
176
|
+
const inputType = element.getAttribute('data-chat-element');
|
|
123
177
|
|
|
124
178
|
// Parse value
|
|
125
179
|
let value;
|
|
@@ -130,20 +184,26 @@ function handleOptionClick(element, field, isSingleSelect) {
|
|
|
130
184
|
}
|
|
131
185
|
|
|
132
186
|
if (isSingleSelect) {
|
|
133
|
-
// Uncheck all others
|
|
134
|
-
const
|
|
187
|
+
// Uncheck all others with same field and input type
|
|
188
|
+
const selector = `[data-field="${field}"][data-chat-element="${inputType}"]`;
|
|
189
|
+
const allOptions = document.querySelectorAll(selector);
|
|
190
|
+
|
|
135
191
|
allOptions.forEach(opt => {
|
|
136
192
|
opt.classList.remove('cf-checked');
|
|
137
193
|
opt.style.backgroundColor = 'transparent';
|
|
138
|
-
|
|
139
|
-
opt.querySelector('input')
|
|
194
|
+
|
|
195
|
+
const otherTick = opt.querySelector('[data-chat-input-element="tick-icon"]');
|
|
196
|
+
const otherInput = opt.querySelector('[data-chat-input-element="input"]');
|
|
197
|
+
|
|
198
|
+
if (otherTick) otherTick.style.display = 'none';
|
|
199
|
+
if (otherInput) otherInput.checked = false;
|
|
140
200
|
});
|
|
141
201
|
|
|
142
202
|
// Check this one
|
|
143
203
|
element.classList.add('cf-checked');
|
|
144
204
|
element.style.backgroundColor = config.selectedBackground;
|
|
145
|
-
tickIcon.style.display = 'block';
|
|
146
|
-
input.checked = true;
|
|
205
|
+
if (tickIcon) tickIcon.style.display = 'block';
|
|
206
|
+
if (input) input.checked = true;
|
|
147
207
|
|
|
148
208
|
// Save value
|
|
149
209
|
chatState.data[field] = value;
|
|
@@ -156,8 +216,8 @@ function handleOptionClick(element, field, isSingleSelect) {
|
|
|
156
216
|
if (isChecked) {
|
|
157
217
|
element.classList.add('cf-checked');
|
|
158
218
|
element.style.backgroundColor = config.selectedBackground;
|
|
159
|
-
tickIcon.style.display = 'block';
|
|
160
|
-
input.checked = true;
|
|
219
|
+
if (tickIcon) tickIcon.style.display = 'block';
|
|
220
|
+
if (input) input.checked = true;
|
|
161
221
|
|
|
162
222
|
// Add to array
|
|
163
223
|
if (!Array.isArray(chatState.data[field])) {
|
|
@@ -173,8 +233,8 @@ function handleOptionClick(element, field, isSingleSelect) {
|
|
|
173
233
|
} else {
|
|
174
234
|
element.classList.remove('cf-checked');
|
|
175
235
|
element.style.backgroundColor = 'transparent';
|
|
176
|
-
tickIcon.style.display = 'none';
|
|
177
|
-
input.checked = false;
|
|
236
|
+
if (tickIcon) tickIcon.style.display = 'none';
|
|
237
|
+
if (input) input.checked = false;
|
|
178
238
|
|
|
179
239
|
// Remove from array
|
|
180
240
|
if (Array.isArray(chatState.data[field])) {
|
|
@@ -184,8 +244,9 @@ function handleOptionClick(element, field, isSingleSelect) {
|
|
|
184
244
|
}
|
|
185
245
|
}
|
|
186
246
|
|
|
187
|
-
// Get all selected names
|
|
188
|
-
const
|
|
247
|
+
// Get all selected names using custom attribute
|
|
248
|
+
const selector = `[data-field="${field}"][data-chat-element="${inputType}"].cf-checked`;
|
|
249
|
+
const selectedOptions = document.querySelectorAll(selector);
|
|
189
250
|
const selectedNames = Array.from(selectedOptions).map(opt =>
|
|
190
251
|
opt.getAttribute('data-name')
|
|
191
252
|
).join(', ');
|
|
@@ -231,8 +292,8 @@ async function handleNext() {
|
|
|
231
292
|
}
|
|
232
293
|
}
|
|
233
294
|
|
|
234
|
-
// Add user message
|
|
235
|
-
addMessage(chatState.currentSelection.name, 'user');
|
|
295
|
+
// Add user message with edit capability
|
|
296
|
+
addMessage(chatState.currentSelection.name, 'user', true, chatState.step);
|
|
236
297
|
|
|
237
298
|
// Save to history
|
|
238
299
|
chatState.history.push({
|
|
@@ -327,11 +388,13 @@ function handleCompletion() {
|
|
|
327
388
|
// INITIALIZATION
|
|
328
389
|
// =============================================================================
|
|
329
390
|
|
|
330
|
-
function init(
|
|
331
|
-
//
|
|
332
|
-
elements.container = document.
|
|
391
|
+
function init(flowName, flowConfig) {
|
|
392
|
+
// Find container by data-chat-element="chat-wrapper"
|
|
393
|
+
elements.container = document.querySelector('[data-chat-element="chat-wrapper"]');
|
|
394
|
+
|
|
333
395
|
if (!elements.container) {
|
|
334
|
-
console.error('Container not found
|
|
396
|
+
console.error('Container with data-chat-element="chat-wrapper" not found');
|
|
397
|
+
console.error('Please add <div data-chat-element="chat-wrapper"></div> to your HTML');
|
|
335
398
|
return null;
|
|
336
399
|
}
|
|
337
400
|
|
|
@@ -349,36 +412,42 @@ function init(containerId, flowConfig) {
|
|
|
349
412
|
chatState.history = [];
|
|
350
413
|
chatState.currentSelection = null;
|
|
351
414
|
|
|
352
|
-
//
|
|
415
|
+
// Clear existing content and create structure
|
|
353
416
|
elements.container.innerHTML = `
|
|
354
|
-
<div
|
|
355
|
-
|
|
356
|
-
<
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
</
|
|
417
|
+
<div data-chat-element="messages-container"></div>
|
|
418
|
+
<div data-chat-element="navigation-bottom">
|
|
419
|
+
<button data-chat-element="next-button"
|
|
420
|
+
style="opacity: 0.5; cursor: not-allowed;"
|
|
421
|
+
disabled>
|
|
422
|
+
Next →
|
|
423
|
+
</button>
|
|
361
424
|
</div>
|
|
362
425
|
`;
|
|
363
426
|
|
|
364
|
-
// Get elements
|
|
365
|
-
elements.messages =
|
|
366
|
-
elements.nextBtn =
|
|
427
|
+
// Get elements by data-chat-element
|
|
428
|
+
elements.messages = elements.container.querySelector('[data-chat-element="messages-container"]');
|
|
429
|
+
elements.nextBtn = elements.container.querySelector('[data-chat-element="next-button"]');
|
|
367
430
|
|
|
368
431
|
// Add event listener
|
|
369
432
|
if (elements.nextBtn) {
|
|
370
433
|
elements.nextBtn.onclick = handleNext;
|
|
371
434
|
}
|
|
372
435
|
|
|
436
|
+
// Expose editStep globally for onclick handlers
|
|
437
|
+
if (typeof window !== 'undefined') {
|
|
438
|
+
window.editStep = editStep;
|
|
439
|
+
}
|
|
440
|
+
|
|
373
441
|
// Start
|
|
374
442
|
showNextStep();
|
|
375
443
|
|
|
376
|
-
console.log(
|
|
444
|
+
console.log(`✅ Flow "${flowName}" initialized`);
|
|
377
445
|
|
|
378
446
|
return {
|
|
379
447
|
getState,
|
|
380
448
|
reset,
|
|
381
|
-
goToStep
|
|
449
|
+
goToStep,
|
|
450
|
+
editStep
|
|
382
451
|
};
|
|
383
452
|
}
|
|
384
453
|
|
|
@@ -425,6 +494,27 @@ function goToStep(stepNumber) {
|
|
|
425
494
|
showNextStep();
|
|
426
495
|
}
|
|
427
496
|
|
|
497
|
+
function editStep(stepNumber) {
|
|
498
|
+
if (stepNumber < 0 || stepNumber >= flowData.flow.length) {
|
|
499
|
+
console.error('Invalid step number:', stepNumber);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Clear history from this step forward
|
|
504
|
+
chatState.history = chatState.history.filter(h => h.step < stepNumber);
|
|
505
|
+
|
|
506
|
+
// Reset data for this field and beyond
|
|
507
|
+
const stepsToReset = flowData.flow.slice(stepNumber);
|
|
508
|
+
stepsToReset.forEach(step => {
|
|
509
|
+
if (step.input && step.input.field) {
|
|
510
|
+
delete chatState.data[step.input.field];
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// Go to that step
|
|
515
|
+
goToStep(stepNumber);
|
|
516
|
+
}
|
|
517
|
+
|
|
428
518
|
function injectStyles() {
|
|
429
519
|
if (typeof document === 'undefined') return;
|
|
430
520
|
|
|
@@ -454,6 +544,7 @@ function injectStyles() {
|
|
|
454
544
|
.cf-message {
|
|
455
545
|
margin-bottom: 15px;
|
|
456
546
|
animation: cfSlideIn 0.3s ease;
|
|
547
|
+
position: relative;
|
|
457
548
|
}
|
|
458
549
|
|
|
459
550
|
@keyframes cfSlideIn {
|
|
@@ -469,6 +560,28 @@ function injectStyles() {
|
|
|
469
560
|
text-align: right;
|
|
470
561
|
}
|
|
471
562
|
|
|
563
|
+
.cf-edit-icon {
|
|
564
|
+
position: absolute;
|
|
565
|
+
right: -30px;
|
|
566
|
+
top: 50%;
|
|
567
|
+
transform: translateY(-50%);
|
|
568
|
+
background: none;
|
|
569
|
+
border: none;
|
|
570
|
+
cursor: pointer;
|
|
571
|
+
font-size: 16px;
|
|
572
|
+
opacity: 0;
|
|
573
|
+
transition: opacity 0.2s ease;
|
|
574
|
+
padding: 4px 8px;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.cf-message:hover .cf-edit-icon {
|
|
578
|
+
opacity: 1;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.cf-edit-icon:hover {
|
|
582
|
+
transform: translateY(-50%) scale(1.2);
|
|
583
|
+
}
|
|
584
|
+
|
|
472
585
|
.cf-message-content {
|
|
473
586
|
display: inline-block;
|
|
474
587
|
padding: 12px 16px;
|
|
@@ -579,5 +692,6 @@ module.exports = {
|
|
|
579
692
|
init,
|
|
580
693
|
getState,
|
|
581
694
|
reset,
|
|
582
|
-
goToStep
|
|
695
|
+
goToStep,
|
|
696
|
+
editStep
|
|
583
697
|
};
|