test-chat-component-per 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.
@@ -0,0 +1,683 @@
1
+ /**
2
+ * DelaChat Web Component
3
+ * A standards-based custom element for chat functionality
4
+ *
5
+ * Usage:
6
+ * <dela-chat
7
+ * api-url="https://localhost:5001"
8
+ * session-uid="your-session"
9
+ * datasource="EPMSCA"
10
+ * product-id="200"
11
+ * token="your-token">
12
+ * </dela-chat>
13
+ */
14
+
15
+ class DelaChatComponent extends HTMLElement {
16
+ constructor() {
17
+ super();
18
+
19
+ // Attach shadow DOM for style encapsulation
20
+ this.attachShadow({ mode: 'open' });
21
+
22
+ // State
23
+ this.state = {
24
+ messages: [],
25
+ isLoading: false,
26
+ isOpen: true,
27
+ error: null
28
+ };
29
+ }
30
+
31
+ // Observed attributes (auto-updates when changed)
32
+ static get observedAttributes() {
33
+ return ['api-url', 'session-uid', 'datasource', 'product-id', 'token', 'theme', 'height', 'width'];
34
+ }
35
+
36
+ // Called when element is added to DOM
37
+ connectedCallback() {
38
+ this.render();
39
+ this.attachEventListeners();
40
+ this.loadHistory();
41
+
42
+ // Emit ready event
43
+ this.dispatchEvent(new CustomEvent('ready', {
44
+ bubbles: true,
45
+ composed: true
46
+ }));
47
+ }
48
+
49
+ // Called when attributes change
50
+ attributeChangedCallback(name, oldValue, newValue) {
51
+ if (oldValue !== newValue && this.shadowRoot.innerHTML) {
52
+ this.render();
53
+ }
54
+ }
55
+
56
+ // Get configuration from attributes
57
+ get config() {
58
+ return {
59
+ apiUrl: this.getAttribute('api-url'),
60
+ sessionUid: this.getAttribute('session-uid'),
61
+ datasource: this.getAttribute('datasource'),
62
+ productId: parseInt(this.getAttribute('product-id')),
63
+ token: this.getAttribute('token'),
64
+ theme: this.getAttribute('theme') || 'light',
65
+ height: this.getAttribute('height') || '600px',
66
+ width: this.getAttribute('width') || '100%'
67
+ };
68
+ }
69
+
70
+ // Render component
71
+ render() {
72
+ const config = this.config;
73
+
74
+ this.shadowRoot.innerHTML = `
75
+ <style>
76
+ ${this.getStyles()}
77
+ </style>
78
+
79
+ <div class="chat-window" data-theme="${config.theme}" style="height: ${config.height}; width: ${config.width};">
80
+ <!-- Header -->
81
+ <div class="chat-header">
82
+ <h2 class="chat-title">Ask Dela</h2>
83
+ <div class="chat-header-buttons">
84
+ <button class="btn-icon" data-action="disclaimer" title="Disclaimer">ℹ</button>
85
+ <button class="btn-icon" data-action="new-chat" title="New Chat">+</button>
86
+ <button class="btn-icon" data-action="help" title="Help">?</button>
87
+ <button class="btn-icon" data-action="close" title="Close">✕</button>
88
+ </div>
89
+ </div>
90
+
91
+ <!-- Disclaimer -->
92
+ <div class="disclaimer" style="display: none;">
93
+ <p>Dela is an AI-powered assistant. While I strive to provide accurate information, please verify important details independently.</p>
94
+ </div>
95
+
96
+ <!-- Error message -->
97
+ <div class="error" style="display: none;"></div>
98
+
99
+ <!-- Messages area -->
100
+ <div class="messages">
101
+ <div class="welcome">
102
+ <div class="ai-avatar-large">🤖</div>
103
+ <p>Hello! I'm Dela, your AI assistant. How can I help you today?</p>
104
+ </div>
105
+ </div>
106
+
107
+ <!-- Loading indicator -->
108
+ <div class="loading" style="display: none;">
109
+ <div class="loading-dots">
110
+ <span></span>
111
+ <span></span>
112
+ <span></span>
113
+ </div>
114
+ <span>Dela is thinking...</span>
115
+ </div>
116
+
117
+ <!-- Input area -->
118
+ <div class="input-container">
119
+ <div class="input-wrapper">
120
+ <textarea class="input" placeholder="Type your message..." rows="1"></textarea>
121
+ <button class="btn-send" data-action="send" title="Send message">➤</button>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ `;
126
+ }
127
+
128
+ // Attach event listeners
129
+ attachEventListeners() {
130
+ const shadow = this.shadowRoot;
131
+ const input = shadow.querySelector('.input');
132
+ const btnSend = shadow.querySelector('[data-action="send"]');
133
+ const btnClose = shadow.querySelector('[data-action="close"]');
134
+ const btnDisclaimer = shadow.querySelector('[data-action="disclaimer"]');
135
+ const btnNewChat = shadow.querySelector('[data-action="new-chat"]');
136
+ const btnHelp = shadow.querySelector('[data-action="help"]');
137
+
138
+ // Send button
139
+ btnSend.addEventListener('click', () => this.handleSend());
140
+
141
+ // Enter key
142
+ input.addEventListener('keydown', (e) => {
143
+ if (e.key === 'Enter' && !e.shiftKey) {
144
+ e.preventDefault();
145
+ this.handleSend();
146
+ }
147
+ });
148
+
149
+ // Auto-resize textarea
150
+ input.addEventListener('input', function() {
151
+ this.style.height = 'auto';
152
+ this.style.height = (this.scrollHeight) + 'px';
153
+ });
154
+
155
+ // Close button
156
+ btnClose.addEventListener('click', () => {
157
+ this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true }));
158
+ this.style.display = 'none';
159
+ });
160
+
161
+ // Disclaimer toggle
162
+ btnDisclaimer.addEventListener('click', () => {
163
+ const disclaimer = shadow.querySelector('.disclaimer');
164
+ disclaimer.style.display = disclaimer.style.display === 'none' ? 'block' : 'none';
165
+ });
166
+
167
+ // New chat
168
+ btnNewChat.addEventListener('click', () => {
169
+ if (confirm('Start a new conversation? This will clear the current chat.')) {
170
+ this.clearHistory();
171
+ }
172
+ });
173
+
174
+ // Help
175
+ btnHelp.addEventListener('click', () => {
176
+ alert('DelaChat Help\n\nType your question and press Enter or click Send.');
177
+ });
178
+ }
179
+
180
+ // Handle send message
181
+ handleSend() {
182
+ const input = this.shadowRoot.querySelector('.input');
183
+ const message = input.value.trim();
184
+
185
+ if (!message || this.state.isLoading) return;
186
+
187
+ this.sendMessage(message);
188
+ input.value = '';
189
+ input.style.height = 'auto';
190
+ }
191
+
192
+ // Load chat history
193
+ async loadHistory() {
194
+ this.setLoading(true);
195
+
196
+ try {
197
+ const data = await this.apiRequest('GET', '/delachat/history?limit=50&offset=0');
198
+ this.state.messages = data.records || [];
199
+ this.renderMessages();
200
+ } catch (error) {
201
+ this.handleError(error);
202
+ } finally {
203
+ this.setLoading(false);
204
+ }
205
+ }
206
+
207
+ // Send message
208
+ async sendMessage(message) {
209
+ this.setLoading(true);
210
+ this.hideWelcome();
211
+
212
+ // Add to UI immediately
213
+ const tempMsg = {
214
+ historyId: 'temp-' + Date.now(),
215
+ message: message,
216
+ source: 0,
217
+ created: new Date().toISOString(),
218
+ userId: this.config.userName || 'User',
219
+ isUserMessage: true
220
+ };
221
+
222
+ this.state.messages.unshift(tempMsg);
223
+ this.renderMessages();
224
+
225
+ try {
226
+ // Send to API
227
+ const data = await this.apiRequest('POST', '/delachat/history', {
228
+ message: message,
229
+ source: 0,
230
+ productUid: this.config.productId
231
+ });
232
+
233
+ // Replace temp with real message
234
+ this.state.messages[0] = data.record;
235
+ this.renderMessages();
236
+
237
+ // Emit event
238
+ this.dispatchEvent(new CustomEvent('message-sent', {
239
+ bubbles: true,
240
+ composed: true,
241
+ detail: { message }
242
+ }));
243
+
244
+ // Simulate AI response
245
+ this.simulateAIResponse(message);
246
+
247
+ } catch (error) {
248
+ this.handleError(error);
249
+ this.setLoading(false);
250
+ }
251
+ }
252
+
253
+ // Simulate AI response (replace with real AI integration)
254
+ async simulateAIResponse(userMessage) {
255
+ setTimeout(async () => {
256
+ const aiResponse = `I received your message: "${userMessage}". This is a simulated response.`;
257
+
258
+ try {
259
+ const data = await this.apiRequest('POST', '/delachat/history', {
260
+ message: aiResponse,
261
+ source: 1,
262
+ productUid: this.config.productId
263
+ });
264
+
265
+ this.state.messages.unshift(data.record);
266
+ this.renderMessages();
267
+ this.setLoading(false);
268
+
269
+ // Emit event
270
+ this.dispatchEvent(new CustomEvent('message-received', {
271
+ bubbles: true,
272
+ composed: true,
273
+ detail: { message: aiResponse }
274
+ }));
275
+
276
+ } catch (error) {
277
+ this.handleError(error);
278
+ this.setLoading(false);
279
+ }
280
+ }, 1500);
281
+ }
282
+
283
+ // Render messages
284
+ renderMessages() {
285
+ const messagesContainer = this.shadowRoot.querySelector('.messages');
286
+ const messagesHtml = this.state.messages.map(msg => this.renderMessage(msg)).join('');
287
+ messagesContainer.innerHTML = messagesHtml;
288
+ this.scrollToBottom();
289
+ }
290
+
291
+ // Render single message
292
+ renderMessage(msg) {
293
+ const isUser = msg.source === 0;
294
+ const avatarIcon = isUser ? '👤' : '🤖';
295
+ const messageClass = isUser ? 'message-user' : 'message-ai';
296
+
297
+ const actions = !isUser ? `
298
+ <div class="message-actions">
299
+ <button onclick="this.getRootNode().host.copyMessage('${msg.historyId}')" class="btn-action">
300
+ 📋 Copy
301
+ </button>
302
+ </div>
303
+ ` : '';
304
+
305
+ return `
306
+ <div class="message ${messageClass}" data-id="${msg.historyId}">
307
+ <div class="avatar">${avatarIcon}</div>
308
+ <div class="message-content">
309
+ <div class="message-bubble">${this.escapeHtml(msg.message)}</div>
310
+ ${actions}
311
+ </div>
312
+ </div>
313
+ `;
314
+ }
315
+
316
+ // Public methods
317
+ copyMessage(messageId) {
318
+ const msg = this.state.messages.find(m => m.historyId === messageId);
319
+ if (msg && navigator.clipboard) {
320
+ navigator.clipboard.writeText(msg.message);
321
+ alert('Message copied!');
322
+ }
323
+ }
324
+
325
+ clearHistory() {
326
+ this.state.messages = [];
327
+ this.renderMessages();
328
+ this.shadowRoot.querySelector('.welcome').style.display = 'block';
329
+ }
330
+
331
+ open() {
332
+ this.style.display = 'block';
333
+ }
334
+
335
+ close() {
336
+ this.style.display = 'none';
337
+ }
338
+
339
+ // Utility methods
340
+ async apiRequest(method, endpoint, data = null) {
341
+ const config = this.config;
342
+ const url = config.apiUrl + endpoint;
343
+
344
+ const response = await fetch(url, {
345
+ method,
346
+ headers: {
347
+ 'Content-Type': 'application/json',
348
+ 'X-SessionUid': config.sessionUid,
349
+ 'X-Datasource': config.datasource,
350
+ 'X-ProductId': config.productId.toString(),
351
+ 'X-RequestVerificationToken': config.token
352
+ },
353
+ body: data ? JSON.stringify(data) : null
354
+ });
355
+
356
+ if (!response.ok) {
357
+ if (response.status === 401) {
358
+ throw new Error('Unauthorized: Session expired or invalid token');
359
+ }
360
+ throw new Error(`Request failed: ${response.status}`);
361
+ }
362
+
363
+ return response.json();
364
+ }
365
+
366
+ setLoading(isLoading) {
367
+ this.state.isLoading = isLoading;
368
+ const loading = this.shadowRoot.querySelector('.loading');
369
+ const input = this.shadowRoot.querySelector('.input');
370
+ const btnSend = this.shadowRoot.querySelector('[data-action="send"]');
371
+
372
+ loading.style.display = isLoading ? 'flex' : 'none';
373
+ input.disabled = isLoading;
374
+ btnSend.disabled = isLoading;
375
+ }
376
+
377
+ handleError(error) {
378
+ this.state.error = error.message;
379
+ const errorEl = this.shadowRoot.querySelector('.error');
380
+ errorEl.textContent = error.message;
381
+ errorEl.style.display = 'block';
382
+
383
+ // Emit error event
384
+ this.dispatchEvent(new CustomEvent('error', {
385
+ bubbles: true,
386
+ composed: true,
387
+ detail: { error }
388
+ }));
389
+
390
+ // Auto-hide after 5 seconds
391
+ setTimeout(() => {
392
+ errorEl.style.display = 'none';
393
+ }, 5000);
394
+ }
395
+
396
+ hideWelcome() {
397
+ const welcome = this.shadowRoot.querySelector('.welcome');
398
+ if (welcome) welcome.style.display = 'none';
399
+ }
400
+
401
+ scrollToBottom() {
402
+ const messages = this.shadowRoot.querySelector('.messages');
403
+ messages.scrollTop = messages.scrollHeight;
404
+ }
405
+
406
+ escapeHtml(text) {
407
+ const div = document.createElement('div');
408
+ div.textContent = text;
409
+ return div.innerHTML.replace(/\n/g, '<br>');
410
+ }
411
+
412
+ // Styles (inline for encapsulation)
413
+ getStyles() {
414
+ return `
415
+ :host {
416
+ display: block;
417
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
418
+ font-size: 14px;
419
+ color: #1F2937;
420
+ }
421
+
422
+ .chat-window {
423
+ display: flex;
424
+ flex-direction: column;
425
+ background: #FFFFFF;
426
+ border-radius: 12px;
427
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
428
+ overflow: hidden;
429
+ }
430
+
431
+ .chat-header {
432
+ background: #7C3AED;
433
+ color: white;
434
+ padding: 16px;
435
+ display: flex;
436
+ align-items: center;
437
+ justify-content: space-between;
438
+ }
439
+
440
+ .chat-title {
441
+ font-size: 18px;
442
+ font-weight: 600;
443
+ margin: 0;
444
+ }
445
+
446
+ .chat-header-buttons {
447
+ display: flex;
448
+ gap: 8px;
449
+ }
450
+
451
+ .btn-icon {
452
+ background: transparent;
453
+ border: none;
454
+ color: white;
455
+ cursor: pointer;
456
+ padding: 6px;
457
+ border-radius: 6px;
458
+ width: 32px;
459
+ height: 32px;
460
+ font-size: 16px;
461
+ }
462
+
463
+ .btn-icon:hover {
464
+ background: #6D28D9;
465
+ }
466
+
467
+ .disclaimer {
468
+ background: #FEF3C7;
469
+ border-bottom: 1px solid #F59E0B;
470
+ padding: 12px 16px;
471
+ font-size: 13px;
472
+ color: #92400E;
473
+ }
474
+
475
+ .error {
476
+ background: #FEE2E2;
477
+ border-bottom: 1px solid #EF4444;
478
+ padding: 12px 16px;
479
+ font-size: 13px;
480
+ color: #991B1B;
481
+ }
482
+
483
+ .messages {
484
+ flex: 1;
485
+ overflow-y: auto;
486
+ padding: 16px;
487
+ display: flex;
488
+ flex-direction: column-reverse;
489
+ gap: 16px;
490
+ }
491
+
492
+ .welcome {
493
+ display: flex;
494
+ flex-direction: column;
495
+ align-items: center;
496
+ padding: 40px 20px;
497
+ text-align: center;
498
+ color: #6B7280;
499
+ }
500
+
501
+ .ai-avatar-large {
502
+ width: 64px;
503
+ height: 64px;
504
+ background: #7C3AED;
505
+ border-radius: 50%;
506
+ display: flex;
507
+ align-items: center;
508
+ justify-content: center;
509
+ font-size: 32px;
510
+ margin-bottom: 16px;
511
+ }
512
+
513
+ .message {
514
+ display: flex;
515
+ gap: 12px;
516
+ align-items: flex-start;
517
+ }
518
+
519
+ .message-user {
520
+ flex-direction: row-reverse;
521
+ }
522
+
523
+ .avatar {
524
+ width: 32px;
525
+ height: 32px;
526
+ background: #6B7280;
527
+ border-radius: 50%;
528
+ display: flex;
529
+ align-items: center;
530
+ justify-content: center;
531
+ font-size: 16px;
532
+ flex-shrink: 0;
533
+ }
534
+
535
+ .message-user .avatar {
536
+ background: #7C3AED;
537
+ }
538
+
539
+ .message-content {
540
+ flex: 1;
541
+ display: flex;
542
+ flex-direction: column;
543
+ gap: 6px;
544
+ max-width: 75%;
545
+ }
546
+
547
+ .message-user .message-content {
548
+ align-items: flex-end;
549
+ }
550
+
551
+ .message-bubble {
552
+ padding: 12px 16px;
553
+ border-radius: 12px;
554
+ line-height: 1.5;
555
+ word-wrap: break-word;
556
+ }
557
+
558
+ .message-ai .message-bubble {
559
+ background: #F3F4F6;
560
+ border-bottom-left-radius: 4px;
561
+ }
562
+
563
+ .message-user .message-bubble {
564
+ background: #7C3AED;
565
+ color: white;
566
+ border-bottom-right-radius: 4px;
567
+ }
568
+
569
+ .message-actions {
570
+ display: flex;
571
+ gap: 8px;
572
+ }
573
+
574
+ .btn-action {
575
+ background: transparent;
576
+ border: none;
577
+ color: #6B7280;
578
+ cursor: pointer;
579
+ padding: 4px 8px;
580
+ border-radius: 4px;
581
+ font-size: 12px;
582
+ }
583
+
584
+ .btn-action:hover {
585
+ background: #F9FAFB;
586
+ color: #7C3AED;
587
+ }
588
+
589
+ .loading {
590
+ display: flex;
591
+ align-items: center;
592
+ justify-content: center;
593
+ gap: 8px;
594
+ padding: 12px;
595
+ color: #6B7280;
596
+ font-size: 13px;
597
+ }
598
+
599
+ .loading-dots {
600
+ display: flex;
601
+ gap: 4px;
602
+ }
603
+
604
+ .loading-dots span {
605
+ width: 6px;
606
+ height: 6px;
607
+ background: #7C3AED;
608
+ border-radius: 50%;
609
+ animation: bounce 1.4s infinite ease-in-out both;
610
+ }
611
+
612
+ .loading-dots span:nth-child(2) {
613
+ animation-delay: -0.16s;
614
+ }
615
+
616
+ .loading-dots span:nth-child(3) {
617
+ animation-delay: -0.32s;
618
+ }
619
+
620
+ @keyframes bounce {
621
+ 0%, 80%, 100% { transform: scale(0); }
622
+ 40% { transform: scale(1); }
623
+ }
624
+
625
+ .input-container {
626
+ padding: 16px;
627
+ background: #F9FAFB;
628
+ border-top: 1px solid #E5E7EB;
629
+ }
630
+
631
+ .input-wrapper {
632
+ display: flex;
633
+ gap: 8px;
634
+ align-items: flex-end;
635
+ }
636
+
637
+ .input {
638
+ flex: 1;
639
+ padding: 10px 12px;
640
+ border: 1px solid #E5E7EB;
641
+ border-radius: 8px;
642
+ font-family: inherit;
643
+ font-size: 14px;
644
+ resize: none;
645
+ max-height: 120px;
646
+ }
647
+
648
+ .input:focus {
649
+ outline: none;
650
+ border-color: #7C3AED;
651
+ box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
652
+ }
653
+
654
+ .input:disabled {
655
+ background: #F9FAFB;
656
+ cursor: not-allowed;
657
+ opacity: 0.6;
658
+ }
659
+
660
+ .btn-send {
661
+ background: #7C3AED;
662
+ color: white;
663
+ border: none;
664
+ border-radius: 8px;
665
+ padding: 10px 16px;
666
+ cursor: pointer;
667
+ font-size: 16px;
668
+ }
669
+
670
+ .btn-send:hover:not(:disabled) {
671
+ background: #6D28D9;
672
+ }
673
+
674
+ .btn-send:disabled {
675
+ opacity: 0.5;
676
+ cursor: not-allowed;
677
+ }
678
+ `;
679
+ }
680
+ }
681
+
682
+ // Define the custom element
683
+ customElements.define('dela-chat', DelaChatComponent);