lisichatbot 1.0.0 → 1.0.1
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 +151 -33
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({
|
|
@@ -349,12 +410,19 @@ function init(containerId, flowConfig) {
|
|
|
349
410
|
chatState.history = [];
|
|
350
411
|
chatState.currentSelection = null;
|
|
351
412
|
|
|
352
|
-
// Create UI
|
|
413
|
+
// Create UI with data-chat-element attributes
|
|
353
414
|
elements.container.innerHTML = `
|
|
354
|
-
<div class="cf-wrapper">
|
|
355
|
-
<div class="cf-messages"
|
|
356
|
-
|
|
357
|
-
|
|
415
|
+
<div class="cf-wrapper" data-chat-element="chat-wrapper">
|
|
416
|
+
<div class="cf-messages"
|
|
417
|
+
id="cfMessages"
|
|
418
|
+
data-chat-element="messages-container"></div>
|
|
419
|
+
<div class="cf-bottom"
|
|
420
|
+
data-chat-element="navigation-bottom">
|
|
421
|
+
<button id="cfNextBtn"
|
|
422
|
+
class="cf-next-btn"
|
|
423
|
+
data-chat-element="next-button"
|
|
424
|
+
style="opacity: 0.5; cursor: not-allowed;"
|
|
425
|
+
disabled>
|
|
358
426
|
Next →
|
|
359
427
|
</button>
|
|
360
428
|
</div>
|
|
@@ -370,10 +438,15 @@ function init(containerId, flowConfig) {
|
|
|
370
438
|
elements.nextBtn.onclick = handleNext;
|
|
371
439
|
}
|
|
372
440
|
|
|
441
|
+
// Expose editStep globally for onclick handlers
|
|
442
|
+
if (typeof window !== 'undefined') {
|
|
443
|
+
window.editStep = editStep;
|
|
444
|
+
}
|
|
445
|
+
|
|
373
446
|
// Start
|
|
374
447
|
showNextStep();
|
|
375
448
|
|
|
376
|
-
console.log('✅ Conversational Flow Builder initialized');
|
|
449
|
+
console.log('✅ Conversational Flow Builder initialized with custom attributes');
|
|
377
450
|
|
|
378
451
|
return {
|
|
379
452
|
getState,
|
|
@@ -425,6 +498,27 @@ function goToStep(stepNumber) {
|
|
|
425
498
|
showNextStep();
|
|
426
499
|
}
|
|
427
500
|
|
|
501
|
+
function editStep(stepNumber) {
|
|
502
|
+
if (stepNumber < 0 || stepNumber >= flowData.flow.length) {
|
|
503
|
+
console.error('Invalid step number:', stepNumber);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Clear history from this step forward
|
|
508
|
+
chatState.history = chatState.history.filter(h => h.step < stepNumber);
|
|
509
|
+
|
|
510
|
+
// Reset data for this field and beyond
|
|
511
|
+
const stepsToReset = flowData.flow.slice(stepNumber);
|
|
512
|
+
stepsToReset.forEach(step => {
|
|
513
|
+
if (step.input && step.input.field) {
|
|
514
|
+
delete chatState.data[step.input.field];
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// Go to that step
|
|
519
|
+
goToStep(stepNumber);
|
|
520
|
+
}
|
|
521
|
+
|
|
428
522
|
function injectStyles() {
|
|
429
523
|
if (typeof document === 'undefined') return;
|
|
430
524
|
|
|
@@ -454,6 +548,7 @@ function injectStyles() {
|
|
|
454
548
|
.cf-message {
|
|
455
549
|
margin-bottom: 15px;
|
|
456
550
|
animation: cfSlideIn 0.3s ease;
|
|
551
|
+
position: relative;
|
|
457
552
|
}
|
|
458
553
|
|
|
459
554
|
@keyframes cfSlideIn {
|
|
@@ -469,6 +564,28 @@ function injectStyles() {
|
|
|
469
564
|
text-align: right;
|
|
470
565
|
}
|
|
471
566
|
|
|
567
|
+
.cf-edit-icon {
|
|
568
|
+
position: absolute;
|
|
569
|
+
right: -30px;
|
|
570
|
+
top: 50%;
|
|
571
|
+
transform: translateY(-50%);
|
|
572
|
+
background: none;
|
|
573
|
+
border: none;
|
|
574
|
+
cursor: pointer;
|
|
575
|
+
font-size: 16px;
|
|
576
|
+
opacity: 0;
|
|
577
|
+
transition: opacity 0.2s ease;
|
|
578
|
+
padding: 4px 8px;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.cf-message:hover .cf-edit-icon {
|
|
582
|
+
opacity: 1;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.cf-edit-icon:hover {
|
|
586
|
+
transform: translateY(-50%) scale(1.2);
|
|
587
|
+
}
|
|
588
|
+
|
|
472
589
|
.cf-message-content {
|
|
473
590
|
display: inline-block;
|
|
474
591
|
padding: 12px 16px;
|
|
@@ -579,5 +696,6 @@ module.exports = {
|
|
|
579
696
|
init,
|
|
580
697
|
getState,
|
|
581
698
|
reset,
|
|
582
|
-
goToStep
|
|
699
|
+
goToStep,
|
|
700
|
+
editStep
|
|
583
701
|
};
|