ask-junkie 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.
@@ -0,0 +1,1858 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.AskJunkie = {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ /**
8
+ * DOM Utility Functions
9
+ */
10
+
11
+ /**
12
+ * Escape HTML to prevent XSS
13
+ */
14
+ function escapeHtml(text) {
15
+ const div = document.createElement('div');
16
+ div.textContent = text;
17
+ return div.innerHTML;
18
+ }
19
+
20
+ /**
21
+ * Create an element with attributes and children
22
+ */
23
+ function createElement(tag, attributes = {}, children = []) {
24
+ const element = document.createElement(tag);
25
+
26
+ Object.entries(attributes).forEach(([key, value]) => {
27
+ if (key === 'className') {
28
+ element.className = value;
29
+ } else if (key === 'style' && typeof value === 'object') {
30
+ Object.assign(element.style, value);
31
+ } else if (key.startsWith('on') && typeof value === 'function') {
32
+ element.addEventListener(key.substring(2).toLowerCase(), value);
33
+ } else {
34
+ element.setAttribute(key, value);
35
+ }
36
+ });
37
+
38
+ children.forEach(child => {
39
+ if (typeof child === 'string') {
40
+ element.appendChild(document.createTextNode(child));
41
+ } else if (child instanceof Node) {
42
+ element.appendChild(child);
43
+ }
44
+ });
45
+
46
+ return element;
47
+ }
48
+
49
+ /**
50
+ * Parse markdown-style links [text](url) to HTML
51
+ */
52
+ function parseLinks(text) {
53
+ return text.replace(
54
+ /\[([^\]]+)\]\(([^)]+)\)/g,
55
+ '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Parse markdown bold **text** to HTML
61
+ */
62
+ function parseBold(text) {
63
+ return text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
64
+ }
65
+
66
+ /**
67
+ * Format bot response with basic markdown
68
+ */
69
+ function formatBotResponse(text) {
70
+ let formatted = escapeHtml(text);
71
+ formatted = parseLinks(formatted);
72
+ formatted = parseBold(formatted);
73
+ formatted = formatted.replace(/\n/g, '<br>');
74
+ return formatted;
75
+ }
76
+
77
+ /**
78
+ * Chat Widget UI Component
79
+ * Renders and manages the chatbot interface
80
+ */
81
+
82
+
83
+ class ChatWidget {
84
+ constructor(config) {
85
+ this.config = config;
86
+ this.container = null;
87
+ this.messagesContainer = null;
88
+ this.input = null;
89
+ this.isOpen = false;
90
+ this.isDragging = false;
91
+ this.dragOffset = { x: 0, y: 0 };
92
+ }
93
+
94
+ /**
95
+ * Render the chat widget to the DOM
96
+ */
97
+ render() {
98
+ // Create container
99
+ this.container = createElement('div', {
100
+ id: 'ask-junkie-widget',
101
+ className: 'ask-junkie-container'
102
+ });
103
+
104
+ // Apply position
105
+ this._applyPosition();
106
+
107
+ // Create toggle button
108
+ const toggleBtn = this._createToggleButton();
109
+ this.container.appendChild(toggleBtn);
110
+
111
+ // Create chat window
112
+ const chatWindow = this._createChatWindow();
113
+ this.container.appendChild(chatWindow);
114
+
115
+ // Add to DOM
116
+ document.body.appendChild(this.container);
117
+
118
+ // Apply theme colors
119
+ this._applyTheme();
120
+
121
+ // Load existing chat history
122
+ if (this.config.chatHistory?.length > 0) {
123
+ this._loadHistory(this.config.chatHistory);
124
+ }
125
+
126
+ // Set up event listeners
127
+ this._setupEventListeners();
128
+
129
+ // Enable dragging if configured
130
+ if (this.config.draggable) {
131
+ this._enableDragging(toggleBtn);
132
+ }
133
+
134
+ // Enable resizing if configured
135
+ if (this.config.resizable) {
136
+ this._enableResizing();
137
+ }
138
+
139
+ // Open on load if configured
140
+ if (this.config.openOnLoad) {
141
+ setTimeout(() => this.open(), 500);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Create the toggle button
147
+ */
148
+ _createToggleButton() {
149
+ const btn = createElement('button', {
150
+ id: 'ask-junkie-toggle',
151
+ className: 'ask-junkie-toggle',
152
+ 'aria-label': 'Toggle Chat'
153
+ });
154
+
155
+ // Default icon URL (hosted on jsDelivr CDN from npm package)
156
+ const defaultIconUrl = 'https://cdn.jsdelivr.net/npm/ask-junkie-sdk@latest/dist/junkie-icon.png';
157
+ const iconUrl = this.config.toggleIcon || defaultIconUrl;
158
+
159
+ btn.innerHTML = `
160
+ <img class="icon-chat" src="${iconUrl}" alt="Chat" onerror="this.style.display='none';this.nextElementSibling.style.display='block';">
161
+ <svg class="icon-chat-fallback" viewBox="0 0 24 24" width="28" height="28" fill="white" style="display:none">
162
+ <path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/>
163
+ </svg>
164
+ <svg class="icon-close" viewBox="0 0 24 24" width="28" height="28" fill="white" style="display:none">
165
+ <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
166
+ </svg>
167
+ `;
168
+
169
+ return btn;
170
+ }
171
+
172
+ /**
173
+ * Create the chat window
174
+ */
175
+ _createChatWindow() {
176
+ const window = createElement('div', {
177
+ id: 'ask-junkie-window',
178
+ className: 'ask-junkie-window'
179
+ });
180
+
181
+ window.style.display = 'none';
182
+
183
+ // Header
184
+ const header = createElement('div', { className: 'ask-junkie-header' });
185
+ const defaultAvatarUrl = 'https://cdn.jsdelivr.net/npm/ask-junkie-sdk@latest/dist/junkie-icon.png';
186
+ const avatarUrl = this.config.botAvatar || defaultAvatarUrl;
187
+ header.innerHTML = `
188
+ <div class="ask-junkie-avatar">
189
+ <img src="${escapeHtml(avatarUrl)}" alt="Bot">
190
+ </div>
191
+ <div class="ask-junkie-info">
192
+ <span class="ask-junkie-name">${escapeHtml(this.config.botName)}</span>
193
+ <span class="ask-junkie-status">Online • Ready to help</span>
194
+ </div>
195
+ <button class="ask-junkie-clear" title="Clear chat" style="display:none">
196
+ <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
197
+ <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
198
+ </svg>
199
+ </button>
200
+ `;
201
+ window.appendChild(header);
202
+
203
+ // Messages container
204
+ this.messagesContainer = createElement('div', {
205
+ id: 'ask-junkie-messages',
206
+ className: 'ask-junkie-messages'
207
+ });
208
+
209
+ // Add welcome message
210
+ const welcomeMsg = createElement('div', {
211
+ className: 'ask-junkie-message bot welcome'
212
+ });
213
+ welcomeMsg.innerHTML = `<div class="message-content">${escapeHtml(this.config.welcomeMessage)}</div>`;
214
+ this.messagesContainer.appendChild(welcomeMsg);
215
+
216
+ window.appendChild(this.messagesContainer);
217
+
218
+ // Input area
219
+ const inputArea = createElement('div', { className: 'ask-junkie-input-area' });
220
+
221
+ // Suggestions (animated or static based on config)
222
+ if (this.config.suggestions?.length > 0) {
223
+ if (this.config.animatedSuggestions) {
224
+ // Animated marquee suggestions
225
+ const suggestionsContainer = createElement('div', { className: 'ask-junkie-suggestions animated' });
226
+ const marqueeWrapper = createElement('div', { className: 'ask-junkie-marquee-wrapper' });
227
+ const marqueeContent = createElement('div', { className: 'ask-junkie-marquee-content' });
228
+
229
+ // Double the suggestions for seamless loop
230
+ const allSuggestions = [...this.config.suggestions, ...this.config.suggestions];
231
+
232
+ allSuggestions.forEach(text => {
233
+ const chip = createElement('button', {
234
+ className: 'ask-junkie-chip',
235
+ onClick: () => {
236
+ this.input.value = text;
237
+ this.input.focus();
238
+ }
239
+ }, [text]);
240
+ marqueeContent.appendChild(chip);
241
+ });
242
+
243
+ marqueeWrapper.appendChild(marqueeContent);
244
+ suggestionsContainer.appendChild(marqueeWrapper);
245
+
246
+ // Pause animation on hover
247
+ marqueeWrapper.addEventListener('mouseenter', () => {
248
+ marqueeContent.style.animationPlayState = 'paused';
249
+ });
250
+ marqueeWrapper.addEventListener('mouseleave', () => {
251
+ marqueeContent.style.animationPlayState = 'running';
252
+ });
253
+
254
+ inputArea.appendChild(suggestionsContainer);
255
+ } else {
256
+ // Static suggestions
257
+ const suggestionsContainer = createElement('div', { className: 'ask-junkie-suggestions' });
258
+ this.config.suggestions.forEach(text => {
259
+ const chip = createElement('button', {
260
+ className: 'ask-junkie-chip',
261
+ onClick: () => {
262
+ this.input.value = text;
263
+ this.input.focus();
264
+ }
265
+ }, [text]);
266
+ suggestionsContainer.appendChild(chip);
267
+ });
268
+ inputArea.appendChild(suggestionsContainer);
269
+ }
270
+ }
271
+
272
+ // Input row
273
+ const inputRow = createElement('div', { className: 'ask-junkie-input-row' });
274
+
275
+ this.input = createElement('input', {
276
+ type: 'text',
277
+ id: 'ask-junkie-input',
278
+ placeholder: 'Type your message...',
279
+ autocomplete: 'off'
280
+ });
281
+ inputRow.appendChild(this.input);
282
+
283
+ // Voice input button (if enabled)
284
+ if (this.config.voiceInput && 'webkitSpeechRecognition' in window) {
285
+ const micBtn = createElement('button', {
286
+ className: 'ask-junkie-mic',
287
+ title: 'Voice input',
288
+ onClick: () => this._startVoiceInput()
289
+ });
290
+ micBtn.innerHTML = `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>`;
291
+ inputRow.appendChild(micBtn);
292
+ }
293
+
294
+ // Send button
295
+ const sendBtn = createElement('button', {
296
+ className: 'ask-junkie-send',
297
+ onClick: () => this._sendMessage()
298
+ });
299
+ sendBtn.innerHTML = `<svg viewBox="0 0 24 24" width="20" height="20" fill="white"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>`;
300
+ inputRow.appendChild(sendBtn);
301
+
302
+ inputArea.appendChild(inputRow);
303
+ window.appendChild(inputArea);
304
+
305
+ return window;
306
+ }
307
+
308
+ /**
309
+ * Set up event listeners
310
+ */
311
+ _setupEventListeners() {
312
+ const toggleBtn = this.container.querySelector('#ask-junkie-toggle');
313
+ const clearBtn = this.container.querySelector('.ask-junkie-clear');
314
+
315
+ // Toggle button
316
+ toggleBtn.addEventListener('click', () => {
317
+ if (!this.isDragging) {
318
+ this.toggle();
319
+ }
320
+ });
321
+
322
+ // Input enter key
323
+ this.input.addEventListener('keypress', (e) => {
324
+ if (e.key === 'Enter') {
325
+ this._sendMessage();
326
+ }
327
+ });
328
+
329
+ // Clear button
330
+ if (clearBtn) {
331
+ clearBtn.addEventListener('click', (e) => {
332
+ e.stopPropagation();
333
+ if (confirm('Clear all chat history?')) {
334
+ this._clearMessages();
335
+ if (this.config.onClearHistory) {
336
+ this.config.onClearHistory();
337
+ }
338
+ }
339
+ });
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Apply position from config
345
+ */
346
+ _applyPosition() {
347
+ const pos = this.config.position || 'bottom-right';
348
+ const [vertical, horizontal] = pos.split('-');
349
+
350
+ this.container.style[vertical] = '24px';
351
+ this.container.style[horizontal] = '24px';
352
+ this.container.setAttribute('data-position', pos);
353
+ }
354
+
355
+ /**
356
+ * Apply theme colors
357
+ */
358
+ _applyTheme() {
359
+ const theme = this.config.theme;
360
+ let gradient;
361
+
362
+ if (theme.mode === 'solid') {
363
+ gradient = theme.primary;
364
+ } else {
365
+ gradient = `linear-gradient(135deg, ${theme.primary}, ${theme.secondary})`;
366
+ }
367
+
368
+ // Apply to toggle button and header
369
+ const toggleBtn = this.container.querySelector('.ask-junkie-toggle');
370
+ const header = this.container.querySelector('.ask-junkie-header');
371
+ const sendBtn = this.container.querySelector('.ask-junkie-send');
372
+
373
+ if (toggleBtn) toggleBtn.style.background = gradient;
374
+ if (header) header.style.background = gradient;
375
+ if (sendBtn) sendBtn.style.background = gradient;
376
+
377
+ // Set CSS variable for consistency
378
+ this.container.style.setProperty('--ask-junkie-primary', theme.primary);
379
+ this.container.style.setProperty('--ask-junkie-secondary', theme.secondary);
380
+ }
381
+
382
+ /**
383
+ * Send a message
384
+ */
385
+ _sendMessage() {
386
+ const message = this.input.value.trim();
387
+ if (!message) return;
388
+
389
+ // Add user message to UI
390
+ this.addMessage(message, 'user');
391
+ this.input.value = '';
392
+
393
+ // Update clear button visibility
394
+ this._updateClearButton();
395
+
396
+ // Trigger callback
397
+ if (this.config.onSendMessage) {
398
+ this.config.onSendMessage(message);
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Add a message to the chat
404
+ */
405
+ addMessage(text, sender = 'bot') {
406
+ const msgDiv = createElement('div', {
407
+ className: `ask-junkie-message ${sender}`
408
+ });
409
+
410
+ const content = createElement('div', { className: 'message-content' });
411
+
412
+ if (sender === 'bot') {
413
+ content.innerHTML = formatBotResponse(text);
414
+ } else {
415
+ content.textContent = text;
416
+ }
417
+
418
+ msgDiv.appendChild(content);
419
+ this.messagesContainer.appendChild(msgDiv);
420
+ this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
421
+
422
+ this._updateClearButton();
423
+ }
424
+
425
+ /**
426
+ * Show typing indicator
427
+ */
428
+ showTyping() {
429
+ const typing = createElement('div', {
430
+ className: 'ask-junkie-message bot typing',
431
+ id: 'ask-junkie-typing'
432
+ });
433
+ typing.innerHTML = `
434
+ <div class="message-content">
435
+ <div class="typing-dots">
436
+ <span></span><span></span><span></span>
437
+ </div>
438
+ </div>
439
+ `;
440
+ this.messagesContainer.appendChild(typing);
441
+ this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
442
+ }
443
+
444
+ /**
445
+ * Hide typing indicator
446
+ */
447
+ hideTyping() {
448
+ const typing = document.getElementById('ask-junkie-typing');
449
+ if (typing) typing.remove();
450
+ }
451
+
452
+ /**
453
+ * Load chat history
454
+ */
455
+ _loadHistory(history) {
456
+ history.forEach(msg => {
457
+ const sender = msg.role === 'assistant' ? 'bot' : 'user';
458
+ this.addMessage(msg.content, sender);
459
+ });
460
+ this._updateClearButton();
461
+ }
462
+
463
+ /**
464
+ * Clear messages
465
+ */
466
+ _clearMessages() {
467
+ const messages = this.messagesContainer.querySelectorAll('.ask-junkie-message:not(.welcome)');
468
+ messages.forEach(m => m.remove());
469
+ this._updateClearButton();
470
+ }
471
+
472
+ /**
473
+ * Update clear button visibility
474
+ */
475
+ _updateClearButton() {
476
+ const clearBtn = this.container.querySelector('.ask-junkie-clear');
477
+ const hasMessages = this.messagesContainer.querySelectorAll('.ask-junkie-message:not(.welcome)').length > 0;
478
+ if (clearBtn) {
479
+ clearBtn.style.display = hasMessages ? 'flex' : 'none';
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Start voice input
485
+ */
486
+ _startVoiceInput() {
487
+ if (!('webkitSpeechRecognition' in window)) return;
488
+
489
+ const recognition = new webkitSpeechRecognition();
490
+ recognition.continuous = false;
491
+ recognition.interimResults = false;
492
+
493
+ const micBtn = this.container.querySelector('.ask-junkie-mic');
494
+ if (micBtn) micBtn.classList.add('listening');
495
+
496
+ recognition.onresult = (event) => {
497
+ const transcript = event.results[0][0].transcript;
498
+ this.input.value = transcript;
499
+ this.input.focus();
500
+ };
501
+
502
+ recognition.onend = () => {
503
+ if (micBtn) micBtn.classList.remove('listening');
504
+ };
505
+
506
+ recognition.start();
507
+ }
508
+
509
+ /**
510
+ * Enable dragging
511
+ */
512
+ _enableDragging(toggleBtn) {
513
+ let startX, startY, startLeft, startTop;
514
+
515
+ const onMouseDown = (e) => {
516
+ if (this.isOpen) return;
517
+
518
+ startX = e.clientX || e.touches?.[0]?.clientX;
519
+ startY = e.clientY || e.touches?.[0]?.clientY;
520
+
521
+ const rect = this.container.getBoundingClientRect();
522
+ startLeft = rect.left;
523
+ startTop = rect.top;
524
+
525
+ document.addEventListener('mousemove', onMouseMove);
526
+ document.addEventListener('mouseup', onMouseUp);
527
+ document.addEventListener('touchmove', onMouseMove);
528
+ document.addEventListener('touchend', onMouseUp);
529
+ };
530
+
531
+ const onMouseMove = (e) => {
532
+ const clientX = e.clientX || e.touches?.[0]?.clientX;
533
+ const clientY = e.clientY || e.touches?.[0]?.clientY;
534
+
535
+ const deltaX = clientX - startX;
536
+ const deltaY = clientY - startY;
537
+
538
+ if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
539
+ this.isDragging = true;
540
+ }
541
+
542
+ const newLeft = Math.max(0, Math.min(startLeft + deltaX, window.innerWidth - 70));
543
+ const newTop = Math.max(0, Math.min(startTop + deltaY, window.innerHeight - 70));
544
+
545
+ this.container.style.left = newLeft + 'px';
546
+ this.container.style.top = newTop + 'px';
547
+ this.container.style.right = 'auto';
548
+ this.container.style.bottom = 'auto';
549
+ };
550
+
551
+ const onMouseUp = () => {
552
+ document.removeEventListener('mousemove', onMouseMove);
553
+ document.removeEventListener('mouseup', onMouseUp);
554
+ document.removeEventListener('touchmove', onMouseMove);
555
+ document.removeEventListener('touchend', onMouseUp);
556
+
557
+ setTimeout(() => { this.isDragging = false; }, 100);
558
+ };
559
+
560
+ toggleBtn.addEventListener('mousedown', onMouseDown);
561
+ toggleBtn.addEventListener('touchstart', onMouseDown);
562
+ }
563
+
564
+ /**
565
+ * Enable resizing of chat window
566
+ */
567
+ _enableResizing() {
568
+ const chatWindow = this.container.querySelector('.ask-junkie-window');
569
+ if (!chatWindow) return;
570
+
571
+ // Create resize handle
572
+ const resizeHandle = createElement('div', { className: 'ask-junkie-resize-handle' });
573
+ resizeHandle.innerHTML = `<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor"><path d="M22,22H20V20H22V22M22,18H20V16H22V18M18,22H16V20H18V22M18,18H16V16H18V18M14,22H12V20H14V22M22,14H20V12H22V14Z"/></svg>`;
574
+ chatWindow.appendChild(resizeHandle);
575
+
576
+ // Load saved size from localStorage
577
+ const savedSize = localStorage.getItem('ask_junkie_chat_size');
578
+ if (savedSize) {
579
+ try {
580
+ const { width, height } = JSON.parse(savedSize);
581
+ chatWindow.style.width = width + 'px';
582
+ chatWindow.style.height = height + 'px';
583
+ } catch (e) {
584
+ // Ignore parse errors
585
+ }
586
+ }
587
+
588
+ let isResizing = false;
589
+ let startX, startY, startWidth, startHeight;
590
+
591
+ const onMouseDown = (e) => {
592
+ e.preventDefault();
593
+ e.stopPropagation();
594
+ isResizing = true;
595
+
596
+ startX = e.clientX || e.touches?.[0]?.clientX;
597
+ startY = e.clientY || e.touches?.[0]?.clientY;
598
+ startWidth = chatWindow.offsetWidth;
599
+ startHeight = chatWindow.offsetHeight;
600
+
601
+ document.addEventListener('mousemove', onMouseMove);
602
+ document.addEventListener('mouseup', onMouseUp);
603
+ document.addEventListener('touchmove', onMouseMove);
604
+ document.addEventListener('touchend', onMouseUp);
605
+ };
606
+
607
+ const onMouseMove = (e) => {
608
+ if (!isResizing) return;
609
+
610
+ const clientX = e.clientX || e.touches?.[0]?.clientX;
611
+ const clientY = e.clientY || e.touches?.[0]?.clientY;
612
+
613
+ // Calculate new dimensions (resizing from top-left corner)
614
+ const deltaX = startX - clientX;
615
+ const deltaY = startY - clientY;
616
+
617
+ const newWidth = Math.max(280, Math.min(startWidth + deltaX, window.innerWidth - 40));
618
+ const newHeight = Math.max(350, Math.min(startHeight + deltaY, window.innerHeight - 100));
619
+
620
+ chatWindow.style.width = newWidth + 'px';
621
+ chatWindow.style.height = newHeight + 'px';
622
+ };
623
+
624
+ const onMouseUp = () => {
625
+ if (isResizing) {
626
+ // Save size to localStorage
627
+ localStorage.setItem('ask_junkie_chat_size', JSON.stringify({
628
+ width: chatWindow.offsetWidth,
629
+ height: chatWindow.offsetHeight
630
+ }));
631
+ }
632
+ isResizing = false;
633
+ document.removeEventListener('mousemove', onMouseMove);
634
+ document.removeEventListener('mouseup', onMouseUp);
635
+ document.removeEventListener('touchmove', onMouseMove);
636
+ document.removeEventListener('touchend', onMouseUp);
637
+ };
638
+
639
+ resizeHandle.addEventListener('mousedown', onMouseDown);
640
+ resizeHandle.addEventListener('touchstart', onMouseDown);
641
+ }
642
+
643
+ // ===== PUBLIC METHODS =====
644
+
645
+ open() {
646
+ this.isOpen = true;
647
+ const window = this.container.querySelector('.ask-junkie-window');
648
+ const chatIcon = this.container.querySelector('.icon-chat');
649
+ const closeIcon = this.container.querySelector('.icon-close');
650
+
651
+ window.style.display = 'flex';
652
+ chatIcon.style.display = 'none';
653
+ closeIcon.style.display = 'block';
654
+ this.input.focus();
655
+
656
+ if (this.config.onOpen) this.config.onOpen();
657
+ }
658
+
659
+ close() {
660
+ this.isOpen = false;
661
+ const window = this.container.querySelector('.ask-junkie-window');
662
+ const chatIcon = this.container.querySelector('.icon-chat');
663
+ const closeIcon = this.container.querySelector('.icon-close');
664
+
665
+ window.style.display = 'none';
666
+ chatIcon.style.display = 'block';
667
+ closeIcon.style.display = 'none';
668
+
669
+ if (this.config.onClose) this.config.onClose();
670
+ }
671
+
672
+ toggle() {
673
+ if (this.isOpen) {
674
+ this.close();
675
+ } else {
676
+ this.open();
677
+ }
678
+ }
679
+
680
+ destroy() {
681
+ if (this.container) {
682
+ this.container.remove();
683
+ this.container = null;
684
+ }
685
+ }
686
+ }
687
+
688
+ /**
689
+ * Base AI Provider Interface
690
+ * All AI providers extend this class
691
+ */
692
+
693
+ class BaseProvider {
694
+ constructor(apiKey, model = null, proxyUrl = null) {
695
+ this.apiKey = apiKey;
696
+ this.model = model || this.getDefaultModel();
697
+ this.proxyUrl = proxyUrl;
698
+ }
699
+
700
+ /**
701
+ * Get the default model for this provider
702
+ */
703
+ getDefaultModel() {
704
+ throw new Error('getDefaultModel must be implemented by subclass');
705
+ }
706
+
707
+ /**
708
+ * Get the API endpoint URL
709
+ */
710
+ getEndpoint() {
711
+ throw new Error('getEndpoint must be implemented by subclass');
712
+ }
713
+
714
+ /**
715
+ * Send a message to the AI provider
716
+ * @param {string} message - User message
717
+ * @param {string} context - System context/prompt
718
+ * @param {Array} history - Conversation history
719
+ * @returns {Promise<string>} AI response
720
+ */
721
+ async sendMessage(message, context, history = []) {
722
+ throw new Error('sendMessage must be implemented by subclass');
723
+ }
724
+
725
+ /**
726
+ * Make HTTP request (supports proxy mode)
727
+ */
728
+ async _fetch(url, options) {
729
+ this.proxyUrl || url;
730
+
731
+ if (this.proxyUrl) {
732
+ // Proxy mode - send the original URL and options to proxy
733
+ return fetch(this.proxyUrl, {
734
+ method: 'POST',
735
+ headers: { 'Content-Type': 'application/json' },
736
+ body: JSON.stringify({
737
+ targetUrl: url,
738
+ ...options
739
+ })
740
+ });
741
+ }
742
+
743
+ return fetch(url, options);
744
+ }
745
+
746
+ /**
747
+ * Format conversation history for chat API
748
+ */
749
+ _formatHistory(history) {
750
+ return history.map(msg => ({
751
+ role: msg.role === 'assistant' ? 'assistant' : 'user',
752
+ content: msg.content
753
+ }));
754
+ }
755
+ }
756
+
757
+ /**
758
+ * Groq AI Provider
759
+ * Fast LLM inference using Groq Cloud
760
+ */
761
+
762
+
763
+ class GroqProvider extends BaseProvider {
764
+ getDefaultModel() {
765
+ return 'openai/gpt-oss-120b';
766
+ }
767
+
768
+ getEndpoint() {
769
+ return 'https://api.groq.com/openai/v1/chat/completions';
770
+ }
771
+
772
+ async sendMessage(message, context, history = []) {
773
+ const messages = [
774
+ { role: 'system', content: context },
775
+ ...this._formatHistory(history),
776
+ { role: 'user', content: message }
777
+ ];
778
+
779
+ const response = await this._fetch(this.getEndpoint(), {
780
+ method: 'POST',
781
+ headers: {
782
+ 'Content-Type': 'application/json',
783
+ 'Authorization': `Bearer ${this.apiKey}`
784
+ },
785
+ body: JSON.stringify({
786
+ model: this.model,
787
+ messages: messages,
788
+ temperature: 0.7,
789
+ max_tokens: 1000
790
+ })
791
+ });
792
+
793
+ if (!response.ok) {
794
+ const error = await response.json().catch(() => ({}));
795
+ throw new Error(error.error?.message || `Groq API error: ${response.status}`);
796
+ }
797
+
798
+ const data = await response.json();
799
+
800
+ if (data.choices?.[0]?.message?.content) {
801
+ return data.choices[0].message.content;
802
+ }
803
+
804
+ throw new Error('Invalid response from Groq API');
805
+ }
806
+ }
807
+
808
+ /**
809
+ * Google Gemini AI Provider
810
+ */
811
+
812
+
813
+ class GeminiProvider extends BaseProvider {
814
+ getDefaultModel() {
815
+ return 'gemini-1.5-flash';
816
+ }
817
+
818
+ getEndpoint() {
819
+ return `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent`;
820
+ }
821
+
822
+ async sendMessage(message, context, history = []) {
823
+ // Build conversation text
824
+ let fullPrompt = context + '\n\n';
825
+
826
+ history.forEach(msg => {
827
+ const role = msg.role === 'user' ? 'User' : 'Assistant';
828
+ fullPrompt += `${role}: ${msg.content}\n`;
829
+ });
830
+
831
+ fullPrompt += `User: ${message}`;
832
+
833
+ const url = `${this.getEndpoint()}?key=${this.apiKey}`;
834
+
835
+ const response = await this._fetch(url, {
836
+ method: 'POST',
837
+ headers: {
838
+ 'Content-Type': 'application/json'
839
+ },
840
+ body: JSON.stringify({
841
+ contents: [{
842
+ parts: [{ text: fullPrompt }]
843
+ }],
844
+ generationConfig: {
845
+ temperature: 0.7,
846
+ maxOutputTokens: 1000
847
+ }
848
+ })
849
+ });
850
+
851
+ if (!response.ok) {
852
+ const error = await response.json().catch(() => ({}));
853
+ throw new Error(error.error?.message || `Gemini API error: ${response.status}`);
854
+ }
855
+
856
+ const data = await response.json();
857
+
858
+ if (data.candidates?.[0]?.content?.parts?.[0]?.text) {
859
+ return data.candidates[0].content.parts[0].text;
860
+ }
861
+
862
+ throw new Error('Invalid response from Gemini API');
863
+ }
864
+ }
865
+
866
+ /**
867
+ * OpenAI Provider
868
+ * GPT-3.5 and GPT-4 models
869
+ */
870
+
871
+
872
+ class OpenAIProvider extends BaseProvider {
873
+ getDefaultModel() {
874
+ return 'gpt-3.5-turbo';
875
+ }
876
+
877
+ getEndpoint() {
878
+ return 'https://api.openai.com/v1/chat/completions';
879
+ }
880
+
881
+ async sendMessage(message, context, history = []) {
882
+ const messages = [
883
+ { role: 'system', content: context },
884
+ ...this._formatHistory(history),
885
+ { role: 'user', content: message }
886
+ ];
887
+
888
+ const response = await this._fetch(this.getEndpoint(), {
889
+ method: 'POST',
890
+ headers: {
891
+ 'Content-Type': 'application/json',
892
+ 'Authorization': `Bearer ${this.apiKey}`
893
+ },
894
+ body: JSON.stringify({
895
+ model: this.model,
896
+ messages: messages,
897
+ temperature: 0.7,
898
+ max_tokens: 1000
899
+ })
900
+ });
901
+
902
+ if (!response.ok) {
903
+ const error = await response.json().catch(() => ({}));
904
+ throw new Error(error.error?.message || `OpenAI API error: ${response.status}`);
905
+ }
906
+
907
+ const data = await response.json();
908
+
909
+ if (data.choices?.[0]?.message?.content) {
910
+ return data.choices[0].message.content;
911
+ }
912
+
913
+ throw new Error('Invalid response from OpenAI API');
914
+ }
915
+ }
916
+
917
+ /**
918
+ * OpenRouter Provider
919
+ * Access multiple AI models through one API
920
+ */
921
+
922
+
923
+ class OpenRouterProvider extends BaseProvider {
924
+ getDefaultModel() {
925
+ return 'meta-llama/llama-3-8b-instruct';
926
+ }
927
+
928
+ getEndpoint() {
929
+ return 'https://openrouter.ai/api/v1/chat/completions';
930
+ }
931
+
932
+ async sendMessage(message, context, history = []) {
933
+ const messages = [
934
+ { role: 'system', content: context },
935
+ ...this._formatHistory(history),
936
+ { role: 'user', content: message }
937
+ ];
938
+
939
+ const response = await this._fetch(this.getEndpoint(), {
940
+ method: 'POST',
941
+ headers: {
942
+ 'Content-Type': 'application/json',
943
+ 'Authorization': `Bearer ${this.apiKey}`,
944
+ 'HTTP-Referer': window.location.origin,
945
+ 'X-Title': document.title || 'Ask Junkie Chatbot'
946
+ },
947
+ body: JSON.stringify({
948
+ model: this.model,
949
+ messages: messages,
950
+ temperature: 0.7,
951
+ max_tokens: 1000
952
+ })
953
+ });
954
+
955
+ if (!response.ok) {
956
+ const error = await response.json().catch(() => ({}));
957
+ throw new Error(error.error?.message || `OpenRouter API error: ${response.status}`);
958
+ }
959
+
960
+ const data = await response.json();
961
+
962
+ if (data.choices?.[0]?.message?.content) {
963
+ return data.choices[0].message.content;
964
+ }
965
+
966
+ throw new Error('Invalid response from OpenRouter API');
967
+ }
968
+ }
969
+
970
+ /**
971
+ * AI Provider Factory
972
+ * Creates the appropriate AI provider based on configuration
973
+ */
974
+
975
+
976
+ class AIProviderFactory {
977
+ static providers = {
978
+ groq: GroqProvider,
979
+ gemini: GeminiProvider,
980
+ openai: OpenAIProvider,
981
+ openrouter: OpenRouterProvider
982
+ };
983
+
984
+ /**
985
+ * Create an AI provider instance
986
+ * @param {string} providerName - Provider name (groq, gemini, openai, openrouter)
987
+ * @param {string} apiKey - API key for the provider
988
+ * @param {string|null} model - Optional model override
989
+ * @param {string|null} proxyUrl - Optional proxy URL
990
+ */
991
+ static create(providerName, apiKey, model = null, proxyUrl = null) {
992
+ const Provider = AIProviderFactory.providers[providerName.toLowerCase()];
993
+
994
+ if (!Provider) {
995
+ console.warn(`[AskJunkie] Unknown provider "${providerName}", falling back to Groq`);
996
+ return new GroqProvider(apiKey, model, proxyUrl);
997
+ }
998
+
999
+ return new Provider(apiKey, model, proxyUrl);
1000
+ }
1001
+
1002
+ /**
1003
+ * Get list of supported providers
1004
+ */
1005
+ static getSupportedProviders() {
1006
+ return Object.keys(AIProviderFactory.providers);
1007
+ }
1008
+ }
1009
+
1010
+ /**
1011
+ * Firebase Analytics Logger
1012
+ * Logs chat interactions to Firebase Firestore for centralized analytics
1013
+ */
1014
+
1015
+ class FirebaseLogger {
1016
+ static FIREBASE_API_KEY = 'AIzaSyDPDWA7s6dauaPup9DFz0hMrQvDYPtEWwo';
1017
+ static FIREBASE_PROJECT_ID = 'ai-chatbot-fe4a2';
1018
+
1019
+ constructor(config = {}) {
1020
+ this.enabled = config.enabled !== false;
1021
+ this.siteId = config.siteId || this._getSiteId();
1022
+ this.userId = config.userId || null; // User ID from sdkKeys lookup
1023
+ this.registered = false;
1024
+ }
1025
+
1026
+ /**
1027
+ * Get a sanitized site ID from hostname
1028
+ */
1029
+ _getSiteId() {
1030
+ return window.location.hostname.replace(/[^a-zA-Z0-9.-]/g, '_');
1031
+ }
1032
+
1033
+ /**
1034
+ * Get Firestore REST API endpoint
1035
+ */
1036
+ _getEndpoint(path) {
1037
+ return `https://firestore.googleapis.com/v1/projects/${FirebaseLogger.FIREBASE_PROJECT_ID}/databases/(default)/documents/${path}?key=${FirebaseLogger.FIREBASE_API_KEY}`;
1038
+ }
1039
+
1040
+ /**
1041
+ * Get the base path for this API key's data
1042
+ * Uses new nested structure: users/{userId}/apiKeys/{keyId}
1043
+ */
1044
+ _getBasePath() {
1045
+ if (this.userId && this.siteId) {
1046
+ return `users/${this.userId}/apiKeys/${this.siteId}`;
1047
+ }
1048
+ // Fallback to old structure for backwards compatibility
1049
+ return `sites/${this.siteId}`;
1050
+ }
1051
+
1052
+ /**
1053
+ * Register or update site analytics info
1054
+ * For new user-based path, we SKIP this to avoid overwriting dashboard data
1055
+ * Only used for legacy sites/ path
1056
+ */
1057
+ async registerSite() {
1058
+ if (this.registered) return;
1059
+
1060
+ // Skip registration for new user-based path - dashboard already created the document
1061
+ // Only the chats subcollection should be written to
1062
+ if (this.userId && this.siteId) {
1063
+ console.log('[AskJunkie] Skipping site registration for SDK-managed key');
1064
+ this.registered = true;
1065
+ return;
1066
+ }
1067
+
1068
+ // Legacy path: sites/{siteId} - still needs registration
1069
+ try {
1070
+ const url = this._getEndpoint(this._getBasePath());
1071
+
1072
+ await fetch(url, {
1073
+ method: 'PATCH',
1074
+ headers: { 'Content-Type': 'application/json' },
1075
+ body: JSON.stringify({
1076
+ fields: {
1077
+ domain: { stringValue: window.location.hostname },
1078
+ site_name: { stringValue: document.title || 'Unknown' },
1079
+ site_url: { stringValue: window.location.origin },
1080
+ sdk_version: { stringValue: '1.1.6' },
1081
+ platform: { stringValue: 'JavaScript SDK' },
1082
+ last_active: { stringValue: new Date().toISOString() }
1083
+ }
1084
+ })
1085
+ });
1086
+
1087
+ this.registered = true;
1088
+ } catch (error) {
1089
+ console.warn('[AskJunkie] Failed to register site:', error);
1090
+ }
1091
+ }
1092
+
1093
+ /**
1094
+ * Log a chat interaction
1095
+ */
1096
+ async logChat(data) {
1097
+ if (!this.enabled) return;
1098
+
1099
+ // Ensure site is registered first
1100
+ await this.registerSite();
1101
+
1102
+ try {
1103
+ const url = this._getEndpoint(`${this._getBasePath()}/chats`);
1104
+
1105
+ await fetch(url, {
1106
+ method: 'POST',
1107
+ headers: { 'Content-Type': 'application/json' },
1108
+ body: JSON.stringify({
1109
+ fields: {
1110
+ session_id: { stringValue: data.sessionId || 'unknown' },
1111
+ user_message: { stringValue: data.userMessage || '' },
1112
+ ai_response: { stringValue: data.aiResponse || '' },
1113
+ page_url: { stringValue: data.pageUrl || window.location.href },
1114
+ timestamp: { stringValue: new Date().toISOString() },
1115
+ user_agent: { stringValue: navigator.userAgent || '' }
1116
+ }
1117
+ })
1118
+ });
1119
+ } catch (error) {
1120
+ console.warn('[AskJunkie] Failed to log chat:', error);
1121
+ }
1122
+ }
1123
+ }
1124
+
1125
+ /**
1126
+ * Storage Manager
1127
+ * Handles localStorage operations with prefix and session management
1128
+ */
1129
+
1130
+ class StorageManager {
1131
+ constructor(prefix = 'ask_junkie_') {
1132
+ this.prefix = prefix;
1133
+ this.sessionId = null;
1134
+ }
1135
+
1136
+ /**
1137
+ * Get a value from storage
1138
+ */
1139
+ get(key) {
1140
+ try {
1141
+ const item = localStorage.getItem(this.prefix + key);
1142
+ return item ? JSON.parse(item) : null;
1143
+ } catch (e) {
1144
+ console.warn('[AskJunkie] Storage get error:', e);
1145
+ return null;
1146
+ }
1147
+ }
1148
+
1149
+ /**
1150
+ * Set a value in storage
1151
+ */
1152
+ set(key, value) {
1153
+ try {
1154
+ localStorage.setItem(this.prefix + key, JSON.stringify(value));
1155
+ return true;
1156
+ } catch (e) {
1157
+ console.warn('[AskJunkie] Storage set error:', e);
1158
+ return false;
1159
+ }
1160
+ }
1161
+
1162
+ /**
1163
+ * Remove a value from storage
1164
+ */
1165
+ remove(key) {
1166
+ try {
1167
+ localStorage.removeItem(this.prefix + key);
1168
+ } catch (e) {
1169
+ console.warn('[AskJunkie] Storage remove error:', e);
1170
+ }
1171
+ }
1172
+
1173
+ /**
1174
+ * Get or create a unique session ID
1175
+ */
1176
+ getSessionId() {
1177
+ if (this.sessionId) return this.sessionId;
1178
+
1179
+ this.sessionId = this.get('session_id');
1180
+
1181
+ if (!this.sessionId) {
1182
+ this.sessionId = 'sess_' +
1183
+ Math.random().toString(36).substring(2, 10) +
1184
+ '_' + Date.now().toString(36);
1185
+ this.set('session_id', this.sessionId);
1186
+ }
1187
+
1188
+ return this.sessionId;
1189
+ }
1190
+
1191
+ /**
1192
+ * Check if storage is available
1193
+ */
1194
+ isAvailable() {
1195
+ try {
1196
+ const test = '__storage_test__';
1197
+ localStorage.setItem(test, test);
1198
+ localStorage.removeItem(test);
1199
+ return true;
1200
+ } catch (e) {
1201
+ return false;
1202
+ }
1203
+ }
1204
+ }
1205
+
1206
+ /**
1207
+ * Simple Event Emitter for SDK events
1208
+ */
1209
+
1210
+ class EventEmitter {
1211
+ constructor() {
1212
+ this.events = {};
1213
+ }
1214
+
1215
+ /**
1216
+ * Subscribe to an event
1217
+ */
1218
+ on(event, callback) {
1219
+ if (!this.events[event]) {
1220
+ this.events[event] = [];
1221
+ }
1222
+ this.events[event].push(callback);
1223
+ return () => this.off(event, callback);
1224
+ }
1225
+
1226
+ /**
1227
+ * Unsubscribe from an event
1228
+ */
1229
+ off(event, callback) {
1230
+ if (!this.events[event]) return;
1231
+ this.events[event] = this.events[event].filter(cb => cb !== callback);
1232
+ }
1233
+
1234
+ /**
1235
+ * Emit an event
1236
+ */
1237
+ emit(event, data) {
1238
+ if (!this.events[event]) return;
1239
+ this.events[event].forEach(callback => {
1240
+ try {
1241
+ callback(data);
1242
+ } catch (error) {
1243
+ console.error(`[AskJunkie] Event handler error for "${event}":`, error);
1244
+ }
1245
+ });
1246
+ }
1247
+
1248
+ /**
1249
+ * Subscribe to an event once
1250
+ */
1251
+ once(event, callback) {
1252
+ const unsubscribe = this.on(event, (data) => {
1253
+ unsubscribe();
1254
+ callback(data);
1255
+ });
1256
+ }
1257
+ }
1258
+
1259
+ /**
1260
+ * Ask Junkie SDK - Main SDK Class
1261
+ * Handles initialization, configuration, and orchestration
1262
+ */
1263
+
1264
+
1265
+ class AskJunkie {
1266
+ static instance = null;
1267
+ static VERSION = '1.1.1';
1268
+
1269
+ constructor() {
1270
+ this.config = null;
1271
+ this.widget = null;
1272
+ this.aiProvider = null;
1273
+ this.analytics = null;
1274
+ this.storage = null;
1275
+ this.events = new EventEmitter();
1276
+ this.chatHistory = [];
1277
+ this.isInitialized = false;
1278
+ this.siteId = null;
1279
+ }
1280
+
1281
+ /**
1282
+ * Initialize the SDK with configuration
1283
+ * @param {Object} config - Configuration options
1284
+ */
1285
+ static async init(config = {}) {
1286
+ if (AskJunkie.instance && AskJunkie.instance.isInitialized) {
1287
+ console.warn('[AskJunkie] Already initialized. Call destroy() first to reinitialize.');
1288
+ return AskJunkie.instance;
1289
+ }
1290
+
1291
+ AskJunkie.instance = new AskJunkie();
1292
+
1293
+ // Check if using sdkKey mode (fetches settings from dashboard)
1294
+ if (config.sdkKey) {
1295
+ await AskJunkie.instance._initializeWithSdkKey(config.sdkKey, config);
1296
+ } else {
1297
+ AskJunkie.instance._initialize(config);
1298
+ }
1299
+
1300
+ return AskJunkie.instance;
1301
+ }
1302
+
1303
+ /**
1304
+ * Initialize using SDK key - fetches settings from Firebase dashboard
1305
+ */
1306
+ async _initializeWithSdkKey(sdkKey, overrides = {}) {
1307
+ console.log('[AskJunkie] Initializing with SDK key...');
1308
+
1309
+ try {
1310
+ // Fetch settings from Firebase
1311
+ const settings = await this._fetchSettingsFromDashboard(sdkKey);
1312
+
1313
+ if (!settings) {
1314
+ console.error('[AskJunkie] Invalid SDK key or site not found.');
1315
+ return;
1316
+ }
1317
+
1318
+ // Merge fetched settings with any overrides
1319
+ const config = {
1320
+ apiKey: settings.providerApiKey,
1321
+ provider: settings.provider,
1322
+ model: settings.model,
1323
+ botName: settings.botName,
1324
+ welcomeMessage: settings.welcomeMessage,
1325
+ suggestions: settings.suggestions,
1326
+ animatedSuggestions: settings.animatedSuggestions,
1327
+ position: settings.position,
1328
+ draggable: settings.draggable,
1329
+ resizable: settings.resizable,
1330
+ voiceInput: settings.speechToText, // Map speechToText to voiceInput
1331
+ enabled: settings.enabled,
1332
+ theme: settings.theme,
1333
+ context: settings.context,
1334
+ analytics: {
1335
+ enabled: true,
1336
+ siteId: this.siteId,
1337
+ userId: this.userId // Pass userId for new nested path
1338
+ },
1339
+ ...overrides,
1340
+ _sdkKey: sdkKey
1341
+ };
1342
+
1343
+ this._initialize(config);
1344
+
1345
+ } catch (error) {
1346
+ console.error('[AskJunkie] Error fetching settings:', error);
1347
+ if (overrides.onError) {
1348
+ overrides.onError(error);
1349
+ }
1350
+ }
1351
+ }
1352
+
1353
+ /**
1354
+ * Fetch settings from Firebase dashboard
1355
+ */
1356
+ async _fetchSettingsFromDashboard(sdkKey) {
1357
+ const FIREBASE_PROJECT = 'ai-chatbot-fe4a2';
1358
+ const API_URL = `https://firestore.googleapis.com/v1/projects/${FIREBASE_PROJECT}/databases/(default)/documents`;
1359
+
1360
+ try {
1361
+ // Step 1: Get the key lookup from sdkKeys collection
1362
+ // Document ID is the SDK key itself
1363
+ const lookupUrl = `${API_URL}/sdkKeys/${sdkKey}`;
1364
+ const lookupResponse = await fetch(lookupUrl);
1365
+
1366
+ if (!lookupResponse.ok) {
1367
+ console.error('[AskJunkie] SDK key not found in lookup');
1368
+ return null;
1369
+ }
1370
+
1371
+ const lookupData = await lookupResponse.json();
1372
+
1373
+ if (!lookupData.fields) {
1374
+ console.error('[AskJunkie] SDK key not found');
1375
+ return null;
1376
+ }
1377
+
1378
+ const userId = lookupData.fields.userId.stringValue;
1379
+ const keyId = lookupData.fields.keyId.stringValue;
1380
+ const status = lookupData.fields.status?.stringValue;
1381
+
1382
+ this.siteId = keyId;
1383
+ this.userId = userId;
1384
+
1385
+ // Check if key is active
1386
+ if (status !== 'active') {
1387
+ console.error('[AskJunkie] SDK key is not active');
1388
+ return null;
1389
+ }
1390
+
1391
+ // Step 2: Fetch the full API key document with settings
1392
+ const keyUrl = `${API_URL}/users/${userId}/apiKeys/${keyId}`;
1393
+ const keyResponse = await fetch(keyUrl);
1394
+
1395
+ if (!keyResponse.ok) {
1396
+ console.error('[AskJunkie] Could not fetch API key details');
1397
+ return null;
1398
+ }
1399
+
1400
+ const keyData = await keyResponse.json();
1401
+
1402
+ if (!keyData.fields) {
1403
+ console.error('[AskJunkie] API key document not found');
1404
+ return null;
1405
+ }
1406
+
1407
+ // Settings are embedded in the API key document
1408
+ if (!keyData.fields.settings) {
1409
+ console.warn('[AskJunkie] No settings configured in dashboard. Using defaults.');
1410
+ return this._getDefaultSettings();
1411
+ }
1412
+
1413
+ // Parse Firestore format to plain object
1414
+ return this._parseFirestoreSettings(keyData.fields.settings.mapValue.fields);
1415
+
1416
+ } catch (error) {
1417
+ console.error('[AskJunkie] Error fetching from Firebase:', error);
1418
+ return null;
1419
+ }
1420
+ }
1421
+
1422
+ /**
1423
+ * Get default settings for SDK
1424
+ */
1425
+ _getDefaultSettings() {
1426
+ return {
1427
+ enabled: true,
1428
+ botName: 'AI Assistant',
1429
+ welcomeMessage: "Hi! 👋 I'm your AI assistant. How can I help you today?",
1430
+ suggestions: [],
1431
+ animatedSuggestions: true,
1432
+ speechToText: true,
1433
+ provider: 'groq',
1434
+ providerApiKey: '', // User must provide in overrides
1435
+ model: 'llama-3.3-70b-versatile',
1436
+ position: 'bottom-right',
1437
+ draggable: true,
1438
+ resizable: false,
1439
+ theme: {
1440
+ mode: 'gradient',
1441
+ preset: 1,
1442
+ primary: '#6366f1',
1443
+ secondary: '#ec4899'
1444
+ },
1445
+ context: {
1446
+ siteName: document.title || 'Website',
1447
+ siteDescription: '',
1448
+ customInfo: '',
1449
+ restrictions: ''
1450
+ }
1451
+ };
1452
+ }
1453
+
1454
+ /**
1455
+ * Parse Firestore document format to plain object
1456
+ */
1457
+ _parseFirestoreSettings(fields) {
1458
+ const getString = (field) => field?.stringValue || '';
1459
+ const getNumber = (field) => field?.integerValue || field?.doubleValue || 0;
1460
+ const getBool = (field) => field?.booleanValue || false;
1461
+ const getArray = (field) => {
1462
+ if (!field?.arrayValue?.values) return [];
1463
+ return field.arrayValue.values.map(v => v.stringValue || '');
1464
+ };
1465
+
1466
+ const themeFields = fields.theme?.mapValue?.fields || {};
1467
+ const contextFields = fields.context?.mapValue?.fields || {};
1468
+
1469
+ return {
1470
+ enabled: fields.enabled?.booleanValue !== false, // Default true if not set
1471
+ botName: getString(fields.botName) || 'AI Assistant',
1472
+ welcomeMessage: getString(fields.welcomeMessage) || "Hi! 👋 I'm your AI assistant.",
1473
+ suggestions: getArray(fields.suggestions),
1474
+ animatedSuggestions: fields.animatedSuggestions?.booleanValue !== false, // Default true
1475
+ speechToText: fields.speechToText?.booleanValue !== false, // Default true
1476
+ provider: getString(fields.provider) || 'groq',
1477
+ providerApiKey: getString(fields.providerApiKey),
1478
+ model: getString(fields.model),
1479
+ position: getString(fields.position) || 'bottom-right',
1480
+ draggable: getBool(fields.draggable),
1481
+ resizable: getBool(fields.resizable),
1482
+ theme: {
1483
+ mode: getString(themeFields.mode) || 'gradient',
1484
+ preset: getNumber(themeFields.preset) || 1,
1485
+ primary: getString(themeFields.primary) || '#6366f1',
1486
+ secondary: getString(themeFields.secondary) || '#ec4899'
1487
+ },
1488
+ context: {
1489
+ siteName: getString(contextFields.siteName),
1490
+ siteDescription: getString(contextFields.siteDescription),
1491
+ customInfo: getString(contextFields.customInfo),
1492
+ restrictions: getString(contextFields.restrictions)
1493
+ }
1494
+ };
1495
+ }
1496
+
1497
+ /**
1498
+ * Internal initialization logic
1499
+ */
1500
+ _initialize(config) {
1501
+ // Merge with defaults
1502
+ this.config = this._mergeConfig(config);
1503
+
1504
+ // Check if chatbot is enabled
1505
+ if (this.config.enabled === false) {
1506
+ console.log('[AskJunkie] Chatbot is disabled in settings. Widget will not render.');
1507
+ this.isInitialized = true;
1508
+ return;
1509
+ }
1510
+
1511
+ // Validate required config
1512
+ if (!this.config.apiKey && !this.config._sdkKey) {
1513
+ console.error('[AskJunkie] API key or SDK key is required.');
1514
+ return;
1515
+ }
1516
+
1517
+ // Initialize storage manager
1518
+ this.storage = new StorageManager(this.config.storagePrefix);
1519
+
1520
+ // Load chat history from storage
1521
+ this.chatHistory = this.storage.get('chat_history') || [];
1522
+
1523
+ // Initialize AI provider
1524
+ this.aiProvider = AIProviderFactory.create(
1525
+ this.config.provider,
1526
+ this.config.apiKey,
1527
+ this.config.model,
1528
+ this.config.proxyUrl
1529
+ );
1530
+
1531
+ // Initialize Firebase analytics (if enabled)
1532
+ if (this.config.analytics.enabled) {
1533
+ this.analytics = new FirebaseLogger(this.config.analytics);
1534
+ }
1535
+
1536
+ // Create and render the chat widget
1537
+ this.widget = new ChatWidget({
1538
+ ...this.config,
1539
+ onSendMessage: (message) => this._handleUserMessage(message),
1540
+ onClearHistory: () => this._clearHistory(),
1541
+ chatHistory: this.chatHistory
1542
+ });
1543
+
1544
+ this.widget.render();
1545
+
1546
+ this.isInitialized = true;
1547
+ this.events.emit('ready');
1548
+
1549
+ if (this.config.onReady) {
1550
+ this.config.onReady();
1551
+ }
1552
+
1553
+ console.log(`[AskJunkie] SDK v${AskJunkie.VERSION} initialized successfully`);
1554
+ }
1555
+
1556
+ /**
1557
+ * Merge user config with defaults
1558
+ */
1559
+ _mergeConfig(userConfig) {
1560
+ const defaults = {
1561
+ // Required
1562
+ apiKey: null,
1563
+
1564
+ // Master enable flag
1565
+ enabled: true,
1566
+
1567
+ // AI Provider
1568
+ provider: 'groq',
1569
+ model: null, // Will use provider default
1570
+
1571
+ // Appearance
1572
+ botName: 'AI Assistant',
1573
+ botAvatar: null, // URL to custom avatar
1574
+ welcomeMessage: "Hi! 👋 I'm your AI assistant. How can I help you today?",
1575
+ position: 'bottom-right',
1576
+ theme: {
1577
+ mode: 'gradient',
1578
+ preset: 1,
1579
+ primary: '#6366f1',
1580
+ secondary: '#ec4899'
1581
+ },
1582
+
1583
+ // Behavior
1584
+ draggable: false,
1585
+ resizable: false,
1586
+ persistChat: true,
1587
+ voiceInput: true,
1588
+ suggestions: [],
1589
+ animatedSuggestions: true,
1590
+ openOnLoad: false,
1591
+
1592
+ // Context for AI
1593
+ context: {
1594
+ siteName: document.title || 'Website',
1595
+ siteDescription: '',
1596
+ customInfo: '',
1597
+ restrictions: ''
1598
+ },
1599
+
1600
+ // Analytics
1601
+ analytics: {
1602
+ enabled: true,
1603
+ siteId: window.location.hostname.replace(/[^a-zA-Z0-9.-]/g, '_')
1604
+ },
1605
+
1606
+ // Proxy mode (optional)
1607
+ proxyUrl: null,
1608
+
1609
+ // Storage prefix
1610
+ storagePrefix: 'ask_junkie_',
1611
+
1612
+ // Event callbacks
1613
+ onReady: null,
1614
+ onMessage: null,
1615
+ onOpen: null,
1616
+ onClose: null,
1617
+ onError: null
1618
+ };
1619
+
1620
+ // Deep merge theme object
1621
+ const theme = { ...defaults.theme, ...(userConfig.theme || {}) };
1622
+ const context = { ...defaults.context, ...(userConfig.context || {}) };
1623
+ const analytics = { ...defaults.analytics, ...(userConfig.analytics || {}) };
1624
+
1625
+ return {
1626
+ ...defaults,
1627
+ ...userConfig,
1628
+ theme,
1629
+ context,
1630
+ analytics
1631
+ };
1632
+ }
1633
+
1634
+ /**
1635
+ * Handle user message submission
1636
+ */
1637
+ async _handleUserMessage(message) {
1638
+ if (!message.trim()) return;
1639
+
1640
+ // Add user message to history
1641
+ this.chatHistory.push({
1642
+ role: 'user',
1643
+ content: message,
1644
+ timestamp: new Date().toISOString()
1645
+ });
1646
+
1647
+ // Show typing indicator
1648
+ this.widget.showTyping();
1649
+
1650
+ try {
1651
+ // Build context string
1652
+ const contextString = this._buildContext();
1653
+
1654
+ // Get AI response
1655
+ const response = await this.aiProvider.sendMessage(
1656
+ message,
1657
+ contextString,
1658
+ this.chatHistory.slice(-10) // Send last 10 messages for context
1659
+ );
1660
+
1661
+ // Add bot response to history
1662
+ this.chatHistory.push({
1663
+ role: 'assistant',
1664
+ content: response,
1665
+ timestamp: new Date().toISOString()
1666
+ });
1667
+
1668
+ // Display response
1669
+ this.widget.hideTyping();
1670
+ this.widget.addMessage(response, 'bot');
1671
+
1672
+ // Save to storage
1673
+ if (this.config.persistChat) {
1674
+ this.storage.set('chat_history', this.chatHistory.slice(-50));
1675
+ }
1676
+
1677
+ // Log to analytics
1678
+ if (this.analytics) {
1679
+ this.analytics.logChat({
1680
+ userMessage: message,
1681
+ aiResponse: response,
1682
+ pageUrl: window.location.href,
1683
+ sessionId: this.storage.getSessionId()
1684
+ });
1685
+ }
1686
+
1687
+ // Fire callback
1688
+ if (this.config.onMessage) {
1689
+ this.config.onMessage(response, true);
1690
+ }
1691
+
1692
+ this.events.emit('message', { message: response, isBot: true });
1693
+
1694
+ } catch (error) {
1695
+ this.widget.hideTyping();
1696
+ const errorMessage = 'Sorry, I encountered an error. Please try again.';
1697
+ this.widget.addMessage(errorMessage, 'bot');
1698
+
1699
+ console.error('[AskJunkie] Error:', error);
1700
+
1701
+ if (this.config.onError) {
1702
+ this.config.onError(error);
1703
+ }
1704
+
1705
+ this.events.emit('error', error);
1706
+ }
1707
+ }
1708
+
1709
+ /**
1710
+ * Build AI context from configuration
1711
+ */
1712
+ _buildContext() {
1713
+ const ctx = this.config.context;
1714
+ let context = '';
1715
+
1716
+ context += `You are "${this.config.botName}", a friendly and helpful AI assistant for "${ctx.siteName}". `;
1717
+ context += `Your personality is warm, professional, and knowledgeable. `;
1718
+
1719
+ context += `IMPORTANT IDENTITY RULES: `;
1720
+ context += `1. Your name is ${this.config.botName}. `;
1721
+ context += `2. If asked who created you, politely say you're an AI assistant designed to help with website inquiries. `;
1722
+ context += `3. Stay focused on helping users with questions about this website. `;
1723
+
1724
+ if (ctx.siteDescription) {
1725
+ context += `\n\nABOUT THIS WEBSITE: ${ctx.siteDescription} `;
1726
+ }
1727
+
1728
+ if (ctx.customInfo) {
1729
+ context += `\n\nADDITIONAL INFORMATION: ${ctx.customInfo} `;
1730
+ }
1731
+
1732
+ if (ctx.restrictions) {
1733
+ context += `\n\n🚫 RESTRICTIONS (follow strictly): ${ctx.restrictions} `;
1734
+ }
1735
+
1736
+ context += `\n\nRESPONSE FORMATTING: `;
1737
+ context += `1. Be professional and courteous. `;
1738
+ context += `2. Use clear paragraphs with line breaks. `;
1739
+ context += `3. For lists, use numbered points or bullets. `;
1740
+ context += `4. For links, use markdown format: [Link Text](URL). `;
1741
+ context += `5. Use **bold** for important terms. `;
1742
+ context += `6. Keep responses helpful but concise. `;
1743
+
1744
+ return context;
1745
+ }
1746
+
1747
+ /**
1748
+ * Clear chat history
1749
+ */
1750
+ _clearHistory() {
1751
+ this.chatHistory = [];
1752
+ this.storage.remove('chat_history');
1753
+ this.events.emit('clear');
1754
+ }
1755
+
1756
+ // ===== PUBLIC API =====
1757
+
1758
+ /**
1759
+ * Open the chat widget
1760
+ */
1761
+ static open() {
1762
+ if (AskJunkie.instance?.widget) {
1763
+ AskJunkie.instance.widget.open();
1764
+ }
1765
+ }
1766
+
1767
+ /**
1768
+ * Close the chat widget
1769
+ */
1770
+ static close() {
1771
+ if (AskJunkie.instance?.widget) {
1772
+ AskJunkie.instance.widget.close();
1773
+ }
1774
+ }
1775
+
1776
+ /**
1777
+ * Toggle the chat widget
1778
+ */
1779
+ static toggle() {
1780
+ if (AskJunkie.instance?.widget) {
1781
+ AskJunkie.instance.widget.toggle();
1782
+ }
1783
+ }
1784
+
1785
+ /**
1786
+ * Send a message programmatically
1787
+ */
1788
+ static sendMessage(text) {
1789
+ if (AskJunkie.instance) {
1790
+ AskJunkie.instance.widget.addMessage(text, 'user');
1791
+ AskJunkie.instance._handleUserMessage(text);
1792
+ }
1793
+ }
1794
+
1795
+ /**
1796
+ * Update context dynamically
1797
+ */
1798
+ static setContext(newContext) {
1799
+ if (AskJunkie.instance) {
1800
+ AskJunkie.instance.config.context = {
1801
+ ...AskJunkie.instance.config.context,
1802
+ ...newContext
1803
+ };
1804
+ }
1805
+ }
1806
+
1807
+ /**
1808
+ * Subscribe to events
1809
+ */
1810
+ static on(event, callback) {
1811
+ if (AskJunkie.instance) {
1812
+ AskJunkie.instance.events.on(event, callback);
1813
+ }
1814
+ }
1815
+
1816
+ /**
1817
+ * Destroy the SDK instance
1818
+ */
1819
+ static destroy() {
1820
+ if (AskJunkie.instance) {
1821
+ if (AskJunkie.instance.widget) {
1822
+ AskJunkie.instance.widget.destroy();
1823
+ }
1824
+ AskJunkie.instance.isInitialized = false;
1825
+ AskJunkie.instance = null;
1826
+ console.log('[AskJunkie] Destroyed');
1827
+ }
1828
+ }
1829
+
1830
+ /**
1831
+ * Get SDK version
1832
+ */
1833
+ static getVersion() {
1834
+ return AskJunkie.VERSION;
1835
+ }
1836
+ }
1837
+
1838
+ /**
1839
+ * Ask Junkie SDK - Main Entry Point
1840
+ * Universal AI Chatbot for any website
1841
+ *
1842
+ * @author Junkies Coder
1843
+ * @version 1.0.0
1844
+ */
1845
+
1846
+
1847
+ // Auto-attach to window for script tag usage
1848
+ if (typeof window !== 'undefined') {
1849
+ window.AskJunkie = AskJunkie;
1850
+ }
1851
+
1852
+ exports.AskJunkie = AskJunkie;
1853
+ exports.default = AskJunkie;
1854
+
1855
+ Object.defineProperty(exports, '__esModule', { value: true });
1856
+
1857
+ }));
1858
+ //# sourceMappingURL=ask-junkie.min.js.map