lisichatbot 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +0 -0
  2. package/package.json +15 -0
  3. package/src/index.js +583 -0
package/README.md ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "lisichatbot",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./src/index.js",
6
+ "exports": {
7
+ ".": "./src/index.js"
8
+ },
9
+ "files": [
10
+ "src"
11
+ ],
12
+ "engines": {
13
+ "node": ">=14"
14
+ }
15
+ }
package/src/index.js ADDED
@@ -0,0 +1,583 @@
1
+
2
+ let chatState = {
3
+ step: 0,
4
+ data: {},
5
+ history: [],
6
+ currentSelection: null
7
+ };
8
+
9
+ let elements = {
10
+ container: null,
11
+ messages: null,
12
+ nextBtn: null
13
+ };
14
+
15
+ let config = {
16
+ selectedBackground: '#667eea',
17
+ autoAdvanceDelay: 2000,
18
+ enableAnimations: true
19
+ };
20
+
21
+ let flowData = null;
22
+
23
+ // =============================================================================
24
+ // UTILITY FUNCTIONS
25
+ // =============================================================================
26
+
27
+ function scrollToBottom() {
28
+ if (elements.messages) {
29
+ setTimeout(() => {
30
+ elements.messages.scrollTop = elements.messages.scrollHeight;
31
+ }, 100);
32
+ }
33
+ }
34
+
35
+ function enableNextButton() {
36
+ if (elements.nextBtn) {
37
+ elements.nextBtn.disabled = false;
38
+ elements.nextBtn.style.opacity = '1';
39
+ elements.nextBtn.style.cursor = 'pointer';
40
+ }
41
+ }
42
+
43
+ function disableNextButton() {
44
+ if (elements.nextBtn) {
45
+ elements.nextBtn.disabled = true;
46
+ elements.nextBtn.style.opacity = '0.5';
47
+ elements.nextBtn.style.cursor = 'not-allowed';
48
+ }
49
+ }
50
+
51
+ // =============================================================================
52
+ // MESSAGE FUNCTIONS
53
+ // =============================================================================
54
+
55
+ function addMessage(content, type = 'bot') {
56
+ if (!elements.messages) return;
57
+
58
+ const messageDiv = document.createElement('div');
59
+ messageDiv.className = `cf-message cf-${type}`;
60
+ messageDiv.innerHTML = `<div class="cf-message-content">${content}</div>`;
61
+
62
+ elements.messages.appendChild(messageDiv);
63
+ scrollToBottom();
64
+ }
65
+
66
+ // =============================================================================
67
+ // OPTIONS RENDERING
68
+ // =============================================================================
69
+
70
+ function renderOptions(options, field, isSingleSelect = true) {
71
+ if (!elements.messages) return;
72
+
73
+ const messageDiv = document.createElement('div');
74
+ messageDiv.className = 'cf-message cf-bot';
75
+
76
+ const optionsHtml = options.map((option, index) => {
77
+ const optionName = option.name || option;
78
+ const optionValue = option.value !== undefined ? option.value : option;
79
+ const valueStr = typeof optionValue === 'object' ?
80
+ JSON.stringify(optionValue) : String(optionValue);
81
+
82
+ return `
83
+ <label class="cf-option ${isSingleSelect ? 'cf-single' : 'cf-multi'}"
84
+ data-field="${field}"
85
+ data-value='${valueStr}'
86
+ data-name="${optionName}"
87
+ data-index="${index}">
88
+ <div class="cf-tick" style="display: none;">✓</div>
89
+ <input type="${isSingleSelect ? 'radio' : 'checkbox'}"
90
+ name="${field}"
91
+ value="${valueStr}"
92
+ style="display: none;">
93
+ <span class="cf-option-text">${optionName}</span>
94
+ </label>
95
+ `;
96
+ }).join('');
97
+
98
+ messageDiv.innerHTML = `
99
+ <div class="cf-message-content">
100
+ <div class="cf-options">${optionsHtml}</div>
101
+ </div>
102
+ `;
103
+
104
+ elements.messages.appendChild(messageDiv);
105
+ scrollToBottom();
106
+
107
+ // Add click handlers
108
+ const optionElements = messageDiv.querySelectorAll('.cf-option');
109
+ optionElements.forEach(el => {
110
+ el.onclick = () => handleOptionClick(el, field, isSingleSelect);
111
+ });
112
+ }
113
+
114
+ // =============================================================================
115
+ // OPTION CLICK HANDLER
116
+ // =============================================================================
117
+
118
+ function handleOptionClick(element, field, isSingleSelect) {
119
+ const tickIcon = element.querySelector('.cf-tick');
120
+ const input = element.querySelector('input');
121
+ const valueStr = element.getAttribute('data-value');
122
+ const optionName = element.getAttribute('data-name');
123
+
124
+ // Parse value
125
+ let value;
126
+ try {
127
+ value = JSON.parse(valueStr);
128
+ } catch (e) {
129
+ value = valueStr;
130
+ }
131
+
132
+ if (isSingleSelect) {
133
+ // Uncheck all others
134
+ const allOptions = document.querySelectorAll(`[data-field="${field}"]`);
135
+ allOptions.forEach(opt => {
136
+ opt.classList.remove('cf-checked');
137
+ opt.style.backgroundColor = 'transparent';
138
+ opt.querySelector('.cf-tick').style.display = 'none';
139
+ opt.querySelector('input').checked = false;
140
+ });
141
+
142
+ // Check this one
143
+ element.classList.add('cf-checked');
144
+ element.style.backgroundColor = config.selectedBackground;
145
+ tickIcon.style.display = 'block';
146
+ input.checked = true;
147
+
148
+ // Save value
149
+ chatState.data[field] = value;
150
+ chatState.currentSelection = { field, value, name: optionName };
151
+
152
+ } else {
153
+ // Multi-select
154
+ const isChecked = !element.classList.contains('cf-checked');
155
+
156
+ if (isChecked) {
157
+ element.classList.add('cf-checked');
158
+ element.style.backgroundColor = config.selectedBackground;
159
+ tickIcon.style.display = 'block';
160
+ input.checked = true;
161
+
162
+ // Add to array
163
+ if (!Array.isArray(chatState.data[field])) {
164
+ chatState.data[field] = [];
165
+ }
166
+
167
+ const exists = chatState.data[field].some(v =>
168
+ JSON.stringify(v) === JSON.stringify(value)
169
+ );
170
+ if (!exists) {
171
+ chatState.data[field].push(value);
172
+ }
173
+ } else {
174
+ element.classList.remove('cf-checked');
175
+ element.style.backgroundColor = 'transparent';
176
+ tickIcon.style.display = 'none';
177
+ input.checked = false;
178
+
179
+ // Remove from array
180
+ if (Array.isArray(chatState.data[field])) {
181
+ chatState.data[field] = chatState.data[field].filter(v =>
182
+ JSON.stringify(v) !== JSON.stringify(value)
183
+ );
184
+ }
185
+ }
186
+
187
+ // Get all selected names
188
+ const selectedOptions = document.querySelectorAll(`[data-field="${field}"].cf-checked`);
189
+ const selectedNames = Array.from(selectedOptions).map(opt =>
190
+ opt.getAttribute('data-name')
191
+ ).join(', ');
192
+
193
+ chatState.currentSelection = {
194
+ field,
195
+ value: chatState.data[field],
196
+ name: selectedNames || 'None selected'
197
+ };
198
+ }
199
+
200
+ // Enable Next button
201
+ enableNextButton();
202
+ }
203
+
204
+ // =============================================================================
205
+ // NAVIGATION
206
+ // =============================================================================
207
+
208
+ async function handleNext() {
209
+ if (!chatState.currentSelection) return;
210
+
211
+ const currentStep = flowData.flow[chatState.step];
212
+
213
+ // Call onNext validation if exists
214
+ if (currentStep.onNext) {
215
+ try {
216
+ disableNextButton();
217
+ const canProceed = await currentStep.onNext(
218
+ chatState.currentSelection.value,
219
+ chatState.data
220
+ );
221
+
222
+ if (!canProceed) {
223
+ enableNextButton();
224
+ return;
225
+ }
226
+ } catch (error) {
227
+ console.error('Validation error:', error);
228
+ alert('An error occurred. Please try again.');
229
+ enableNextButton();
230
+ return;
231
+ }
232
+ }
233
+
234
+ // Add user message
235
+ addMessage(chatState.currentSelection.name, 'user');
236
+
237
+ // Save to history
238
+ chatState.history.push({
239
+ step: chatState.step,
240
+ field: chatState.currentSelection.field,
241
+ value: chatState.currentSelection.value,
242
+ name: chatState.currentSelection.name
243
+ });
244
+
245
+ // Reset selection
246
+ chatState.currentSelection = null;
247
+ disableNextButton();
248
+
249
+ // Move to next step
250
+ chatState.step++;
251
+
252
+ // Check if finished
253
+ if (chatState.step >= flowData.flow.length) {
254
+ handleCompletion();
255
+ return;
256
+ }
257
+
258
+ // Show next step
259
+ await showNextStep();
260
+ }
261
+
262
+ async function showNextStep() {
263
+ const nextStep = flowData.flow[chatState.step];
264
+
265
+ // Call onStart if exists
266
+ if (nextStep.onStart) {
267
+ try {
268
+ const canStart = await nextStep.onStart(chatState.data);
269
+ if (!canStart) {
270
+ console.log('Step blocked');
271
+ return;
272
+ }
273
+ } catch (error) {
274
+ console.error('Error in onStart:', error);
275
+ return;
276
+ }
277
+ }
278
+
279
+ // Add message
280
+ addMessage(nextStep.message);
281
+
282
+ // Add options if input exists
283
+ if (nextStep.input) {
284
+ const isSingleSelect = nextStep.inputType !== 'multi';
285
+ renderOptions(nextStep.input.options, nextStep.input.field, isSingleSelect);
286
+ } else {
287
+ // Auto-advance for steps without input
288
+ setTimeout(() => {
289
+ chatState.step++;
290
+ if (chatState.step < flowData.flow.length) {
291
+ showNextStep();
292
+ } else {
293
+ handleCompletion();
294
+ }
295
+ }, config.autoAdvanceDelay);
296
+ }
297
+ }
298
+
299
+ function handleCompletion() {
300
+ addMessage('Thank you! All done.');
301
+
302
+ console.log('✅ Flow completed. Data:', chatState.data);
303
+
304
+ // Hide Next button
305
+ if (elements.nextBtn) {
306
+ elements.nextBtn.style.display = 'none';
307
+ }
308
+
309
+ // Trigger event
310
+ if (typeof window !== 'undefined') {
311
+ const event = new CustomEvent('conversationalFlowComplete', {
312
+ detail: {
313
+ data: chatState.data,
314
+ history: chatState.history
315
+ }
316
+ });
317
+ window.dispatchEvent(event);
318
+ }
319
+
320
+ // Call onComplete if provided
321
+ if (flowData.onComplete && typeof flowData.onComplete === 'function') {
322
+ flowData.onComplete(chatState.data);
323
+ }
324
+ }
325
+
326
+ // =============================================================================
327
+ // INITIALIZATION
328
+ // =============================================================================
329
+
330
+ function init(containerId, flowConfig) {
331
+ // Get container
332
+ elements.container = document.getElementById(containerId);
333
+ if (!elements.container) {
334
+ console.error('Container not found:', containerId);
335
+ return null;
336
+ }
337
+
338
+ // Merge config
339
+ if (flowConfig.config) {
340
+ Object.assign(config, flowConfig.config);
341
+ }
342
+
343
+ // Store flow data
344
+ flowData = flowConfig;
345
+
346
+ // Initialize state
347
+ chatState.step = 0;
348
+ chatState.data = flowConfig.initialData || {};
349
+ chatState.history = [];
350
+ chatState.currentSelection = null;
351
+
352
+ // Create UI
353
+ elements.container.innerHTML = `
354
+ <div class="cf-wrapper">
355
+ <div class="cf-messages" id="cfMessages"></div>
356
+ <div class="cf-bottom">
357
+ <button id="cfNextBtn" class="cf-next-btn" style="opacity: 0.5; cursor: not-allowed;" disabled>
358
+ Next →
359
+ </button>
360
+ </div>
361
+ </div>
362
+ `;
363
+
364
+ // Get elements
365
+ elements.messages = document.getElementById('cfMessages');
366
+ elements.nextBtn = document.getElementById('cfNextBtn');
367
+
368
+ // Add event listener
369
+ if (elements.nextBtn) {
370
+ elements.nextBtn.onclick = handleNext;
371
+ }
372
+
373
+ // Start
374
+ showNextStep();
375
+
376
+ console.log('✅ Conversational Flow Builder initialized');
377
+
378
+ return {
379
+ getState,
380
+ reset,
381
+ goToStep
382
+ };
383
+ }
384
+
385
+ function getState() {
386
+ return {
387
+ step: chatState.step,
388
+ data: { ...chatState.data },
389
+ history: [...chatState.history]
390
+ };
391
+ }
392
+
393
+ function reset() {
394
+ chatState.step = 0;
395
+ chatState.data = flowData.initialData || {};
396
+ chatState.history = [];
397
+ chatState.currentSelection = null;
398
+
399
+ if (elements.messages) {
400
+ elements.messages.innerHTML = '';
401
+ }
402
+
403
+ if (elements.nextBtn) {
404
+ elements.nextBtn.style.display = 'flex';
405
+ }
406
+
407
+ disableNextButton();
408
+ showNextStep();
409
+ }
410
+
411
+ function goToStep(stepNumber) {
412
+ if (stepNumber < 0 || stepNumber >= flowData.flow.length) {
413
+ console.error('Invalid step number:', stepNumber);
414
+ return;
415
+ }
416
+
417
+ chatState.step = stepNumber;
418
+ chatState.currentSelection = null;
419
+
420
+ if (elements.messages) {
421
+ elements.messages.innerHTML = '';
422
+ }
423
+
424
+ disableNextButton();
425
+ showNextStep();
426
+ }
427
+
428
+ function injectStyles() {
429
+ if (typeof document === 'undefined') return;
430
+
431
+ const styleId = 'cf-styles';
432
+ if (document.getElementById(styleId)) return;
433
+
434
+ const style = document.createElement('style');
435
+ style.id = styleId;
436
+ style.textContent = `
437
+ .cf-wrapper {
438
+ display: flex;
439
+ flex-direction: column;
440
+ height: 100%;
441
+ max-height: 600px;
442
+ border: 1px solid #e5e5e5;
443
+ border-radius: 8px;
444
+ overflow: hidden;
445
+ background: #f9f9f9;
446
+ }
447
+
448
+ .cf-messages {
449
+ flex: 1;
450
+ overflow-y: auto;
451
+ padding: 20px;
452
+ }
453
+
454
+ .cf-message {
455
+ margin-bottom: 15px;
456
+ animation: cfSlideIn 0.3s ease;
457
+ }
458
+
459
+ @keyframes cfSlideIn {
460
+ from { opacity: 0; transform: translateY(10px); }
461
+ to { opacity: 1; transform: translateY(0); }
462
+ }
463
+
464
+ .cf-bot {
465
+ text-align: left;
466
+ }
467
+
468
+ .cf-user {
469
+ text-align: right;
470
+ }
471
+
472
+ .cf-message-content {
473
+ display: inline-block;
474
+ padding: 12px 16px;
475
+ border-radius: 8px;
476
+ max-width: 80%;
477
+ }
478
+
479
+ .cf-bot .cf-message-content {
480
+ background: white;
481
+ border: 1px solid #e5e5e5;
482
+ }
483
+
484
+ .cf-user .cf-message-content {
485
+ background: #667eea;
486
+ color: white;
487
+ }
488
+
489
+ .cf-options {
490
+ display: flex;
491
+ flex-direction: column;
492
+ gap: 10px;
493
+ margin-top: 10px;
494
+ }
495
+
496
+ .cf-option {
497
+ display: flex;
498
+ align-items: center;
499
+ gap: 12px;
500
+ padding: 14px 16px;
501
+ background: white;
502
+ border: 2px solid #e5e5e5;
503
+ border-radius: 8px;
504
+ cursor: pointer;
505
+ transition: all 0.2s ease;
506
+ }
507
+
508
+ .cf-option:hover {
509
+ border-color: #667eea;
510
+ transform: translateX(4px);
511
+ }
512
+
513
+ .cf-option.cf-checked {
514
+ border-color: #667eea;
515
+ color: white;
516
+ }
517
+
518
+ .cf-tick {
519
+ font-weight: bold;
520
+ font-size: 18px;
521
+ min-width: 20px;
522
+ }
523
+
524
+ .cf-option-text {
525
+ flex: 1;
526
+ }
527
+
528
+ .cf-bottom {
529
+ padding: 16px;
530
+ background: white;
531
+ border-top: 1px solid #e5e5e5;
532
+ }
533
+
534
+ .cf-next-btn {
535
+ width: 100%;
536
+ padding: 14px 24px;
537
+ background: #667eea;
538
+ color: white;
539
+ border: none;
540
+ border-radius: 8px;
541
+ font-size: 16px;
542
+ font-weight: 600;
543
+ cursor: pointer;
544
+ transition: all 0.2s ease;
545
+ display: flex;
546
+ align-items: center;
547
+ justify-content: center;
548
+ gap: 8px;
549
+ }
550
+
551
+ .cf-next-btn:not(:disabled):hover {
552
+ background: #5568d3;
553
+ transform: translateY(-2px);
554
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
555
+ }
556
+
557
+ .cf-next-btn:disabled {
558
+ cursor: not-allowed;
559
+ }
560
+ `;
561
+
562
+ document.head.appendChild(style);
563
+ }
564
+
565
+ // Auto-inject styles when imported
566
+ if (typeof document !== 'undefined') {
567
+ if (document.readyState === 'loading') {
568
+ document.addEventListener('DOMContentLoaded', injectStyles);
569
+ } else {
570
+ injectStyles();
571
+ }
572
+ }
573
+
574
+ // =============================================================================
575
+ // EXPORTS
576
+ // =============================================================================
577
+
578
+ module.exports = {
579
+ init,
580
+ getState,
581
+ reset,
582
+ goToStep
583
+ };