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,585 @@
1
+ /**
2
+ * Ask Junkie SDK - Main SDK Class
3
+ * Handles initialization, configuration, and orchestration
4
+ */
5
+
6
+ import { ChatWidget } from '../ui/ChatWidget.js';
7
+ import { AIProviderFactory } from '../ai/AIProviderFactory.js';
8
+ import { FirebaseLogger } from '../analytics/FirebaseLogger.js';
9
+ import { StorageManager } from '../utils/StorageManager.js';
10
+ import { EventEmitter } from './EventEmitter.js';
11
+
12
+ class AskJunkie {
13
+ static instance = null;
14
+ static VERSION = '1.1.1';
15
+
16
+ constructor() {
17
+ this.config = null;
18
+ this.widget = null;
19
+ this.aiProvider = null;
20
+ this.analytics = null;
21
+ this.storage = null;
22
+ this.events = new EventEmitter();
23
+ this.chatHistory = [];
24
+ this.isInitialized = false;
25
+ this.siteId = null;
26
+ }
27
+
28
+ /**
29
+ * Initialize the SDK with configuration
30
+ * @param {Object} config - Configuration options
31
+ */
32
+ static async init(config = {}) {
33
+ if (AskJunkie.instance && AskJunkie.instance.isInitialized) {
34
+ console.warn('[AskJunkie] Already initialized. Call destroy() first to reinitialize.');
35
+ return AskJunkie.instance;
36
+ }
37
+
38
+ AskJunkie.instance = new AskJunkie();
39
+
40
+ // Check if using sdkKey mode (fetches settings from dashboard)
41
+ if (config.sdkKey) {
42
+ await AskJunkie.instance._initializeWithSdkKey(config.sdkKey, config);
43
+ } else {
44
+ AskJunkie.instance._initialize(config);
45
+ }
46
+
47
+ return AskJunkie.instance;
48
+ }
49
+
50
+ /**
51
+ * Initialize using SDK key - fetches settings from Firebase dashboard
52
+ */
53
+ async _initializeWithSdkKey(sdkKey, overrides = {}) {
54
+ console.log('[AskJunkie] Initializing with SDK key...');
55
+
56
+ try {
57
+ // Fetch settings from Firebase
58
+ const settings = await this._fetchSettingsFromDashboard(sdkKey);
59
+
60
+ if (!settings) {
61
+ console.error('[AskJunkie] Invalid SDK key or site not found.');
62
+ return;
63
+ }
64
+
65
+ // Merge fetched settings with any overrides
66
+ const config = {
67
+ apiKey: settings.providerApiKey,
68
+ provider: settings.provider,
69
+ model: settings.model,
70
+ botName: settings.botName,
71
+ welcomeMessage: settings.welcomeMessage,
72
+ suggestions: settings.suggestions,
73
+ animatedSuggestions: settings.animatedSuggestions,
74
+ position: settings.position,
75
+ draggable: settings.draggable,
76
+ resizable: settings.resizable,
77
+ voiceInput: settings.speechToText, // Map speechToText to voiceInput
78
+ enabled: settings.enabled,
79
+ theme: settings.theme,
80
+ context: settings.context,
81
+ analytics: {
82
+ enabled: true,
83
+ siteId: this.siteId,
84
+ userId: this.userId // Pass userId for new nested path
85
+ },
86
+ ...overrides,
87
+ _sdkKey: sdkKey
88
+ };
89
+
90
+ this._initialize(config);
91
+
92
+ } catch (error) {
93
+ console.error('[AskJunkie] Error fetching settings:', error);
94
+ if (overrides.onError) {
95
+ overrides.onError(error);
96
+ }
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Fetch settings from Firebase dashboard
102
+ */
103
+ async _fetchSettingsFromDashboard(sdkKey) {
104
+ const FIREBASE_PROJECT = 'ai-chatbot-fe4a2';
105
+ const API_URL = `https://firestore.googleapis.com/v1/projects/${FIREBASE_PROJECT}/databases/(default)/documents`;
106
+
107
+ try {
108
+ // Step 1: Get the key lookup from sdkKeys collection
109
+ // Document ID is the SDK key itself
110
+ const lookupUrl = `${API_URL}/sdkKeys/${sdkKey}`;
111
+ const lookupResponse = await fetch(lookupUrl);
112
+
113
+ if (!lookupResponse.ok) {
114
+ console.error('[AskJunkie] SDK key not found in lookup');
115
+ return null;
116
+ }
117
+
118
+ const lookupData = await lookupResponse.json();
119
+
120
+ if (!lookupData.fields) {
121
+ console.error('[AskJunkie] SDK key not found');
122
+ return null;
123
+ }
124
+
125
+ const userId = lookupData.fields.userId.stringValue;
126
+ const keyId = lookupData.fields.keyId.stringValue;
127
+ const status = lookupData.fields.status?.stringValue;
128
+
129
+ this.siteId = keyId;
130
+ this.userId = userId;
131
+
132
+ // Check if key is active
133
+ if (status !== 'active') {
134
+ console.error('[AskJunkie] SDK key is not active');
135
+ return null;
136
+ }
137
+
138
+ // Step 2: Fetch the full API key document with settings
139
+ const keyUrl = `${API_URL}/users/${userId}/apiKeys/${keyId}`;
140
+ const keyResponse = await fetch(keyUrl);
141
+
142
+ if (!keyResponse.ok) {
143
+ console.error('[AskJunkie] Could not fetch API key details');
144
+ return null;
145
+ }
146
+
147
+ const keyData = await keyResponse.json();
148
+
149
+ if (!keyData.fields) {
150
+ console.error('[AskJunkie] API key document not found');
151
+ return null;
152
+ }
153
+
154
+ // Settings are embedded in the API key document
155
+ if (!keyData.fields.settings) {
156
+ console.warn('[AskJunkie] No settings configured in dashboard. Using defaults.');
157
+ return this._getDefaultSettings();
158
+ }
159
+
160
+ // Parse Firestore format to plain object
161
+ return this._parseFirestoreSettings(keyData.fields.settings.mapValue.fields);
162
+
163
+ } catch (error) {
164
+ console.error('[AskJunkie] Error fetching from Firebase:', error);
165
+ return null;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Get default settings for SDK
171
+ */
172
+ _getDefaultSettings() {
173
+ return {
174
+ enabled: true,
175
+ botName: 'AI Assistant',
176
+ welcomeMessage: "Hi! 👋 I'm your AI assistant. How can I help you today?",
177
+ suggestions: [],
178
+ animatedSuggestions: true,
179
+ speechToText: true,
180
+ provider: 'groq',
181
+ providerApiKey: '', // User must provide in overrides
182
+ model: 'llama-3.3-70b-versatile',
183
+ position: 'bottom-right',
184
+ draggable: true,
185
+ resizable: false,
186
+ theme: {
187
+ mode: 'gradient',
188
+ preset: 1,
189
+ primary: '#6366f1',
190
+ secondary: '#ec4899'
191
+ },
192
+ context: {
193
+ siteName: document.title || 'Website',
194
+ siteDescription: '',
195
+ customInfo: '',
196
+ restrictions: ''
197
+ }
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Parse Firestore document format to plain object
203
+ */
204
+ _parseFirestoreSettings(fields) {
205
+ const getString = (field) => field?.stringValue || '';
206
+ const getNumber = (field) => field?.integerValue || field?.doubleValue || 0;
207
+ const getBool = (field) => field?.booleanValue || false;
208
+ const getArray = (field) => {
209
+ if (!field?.arrayValue?.values) return [];
210
+ return field.arrayValue.values.map(v => v.stringValue || '');
211
+ };
212
+
213
+ const themeFields = fields.theme?.mapValue?.fields || {};
214
+ const contextFields = fields.context?.mapValue?.fields || {};
215
+
216
+ return {
217
+ enabled: fields.enabled?.booleanValue !== false, // Default true if not set
218
+ botName: getString(fields.botName) || 'AI Assistant',
219
+ welcomeMessage: getString(fields.welcomeMessage) || "Hi! 👋 I'm your AI assistant.",
220
+ suggestions: getArray(fields.suggestions),
221
+ animatedSuggestions: fields.animatedSuggestions?.booleanValue !== false, // Default true
222
+ speechToText: fields.speechToText?.booleanValue !== false, // Default true
223
+ provider: getString(fields.provider) || 'groq',
224
+ providerApiKey: getString(fields.providerApiKey),
225
+ model: getString(fields.model),
226
+ position: getString(fields.position) || 'bottom-right',
227
+ draggable: getBool(fields.draggable),
228
+ resizable: getBool(fields.resizable),
229
+ theme: {
230
+ mode: getString(themeFields.mode) || 'gradient',
231
+ preset: getNumber(themeFields.preset) || 1,
232
+ primary: getString(themeFields.primary) || '#6366f1',
233
+ secondary: getString(themeFields.secondary) || '#ec4899'
234
+ },
235
+ context: {
236
+ siteName: getString(contextFields.siteName),
237
+ siteDescription: getString(contextFields.siteDescription),
238
+ customInfo: getString(contextFields.customInfo),
239
+ restrictions: getString(contextFields.restrictions)
240
+ }
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Internal initialization logic
246
+ */
247
+ _initialize(config) {
248
+ // Merge with defaults
249
+ this.config = this._mergeConfig(config);
250
+
251
+ // Check if chatbot is enabled
252
+ if (this.config.enabled === false) {
253
+ console.log('[AskJunkie] Chatbot is disabled in settings. Widget will not render.');
254
+ this.isInitialized = true;
255
+ return;
256
+ }
257
+
258
+ // Validate required config
259
+ if (!this.config.apiKey && !this.config._sdkKey) {
260
+ console.error('[AskJunkie] API key or SDK key is required.');
261
+ return;
262
+ }
263
+
264
+ // Initialize storage manager
265
+ this.storage = new StorageManager(this.config.storagePrefix);
266
+
267
+ // Load chat history from storage
268
+ this.chatHistory = this.storage.get('chat_history') || [];
269
+
270
+ // Initialize AI provider
271
+ this.aiProvider = AIProviderFactory.create(
272
+ this.config.provider,
273
+ this.config.apiKey,
274
+ this.config.model,
275
+ this.config.proxyUrl
276
+ );
277
+
278
+ // Initialize Firebase analytics (if enabled)
279
+ if (this.config.analytics.enabled) {
280
+ this.analytics = new FirebaseLogger(this.config.analytics);
281
+ }
282
+
283
+ // Create and render the chat widget
284
+ this.widget = new ChatWidget({
285
+ ...this.config,
286
+ onSendMessage: (message) => this._handleUserMessage(message),
287
+ onClearHistory: () => this._clearHistory(),
288
+ chatHistory: this.chatHistory
289
+ });
290
+
291
+ this.widget.render();
292
+
293
+ this.isInitialized = true;
294
+ this.events.emit('ready');
295
+
296
+ if (this.config.onReady) {
297
+ this.config.onReady();
298
+ }
299
+
300
+ console.log(`[AskJunkie] SDK v${AskJunkie.VERSION} initialized successfully`);
301
+ }
302
+
303
+ /**
304
+ * Merge user config with defaults
305
+ */
306
+ _mergeConfig(userConfig) {
307
+ const defaults = {
308
+ // Required
309
+ apiKey: null,
310
+
311
+ // Master enable flag
312
+ enabled: true,
313
+
314
+ // AI Provider
315
+ provider: 'groq',
316
+ model: null, // Will use provider default
317
+
318
+ // Appearance
319
+ botName: 'AI Assistant',
320
+ botAvatar: null, // URL to custom avatar
321
+ welcomeMessage: "Hi! 👋 I'm your AI assistant. How can I help you today?",
322
+ position: 'bottom-right',
323
+ theme: {
324
+ mode: 'gradient',
325
+ preset: 1,
326
+ primary: '#6366f1',
327
+ secondary: '#ec4899'
328
+ },
329
+
330
+ // Behavior
331
+ draggable: false,
332
+ resizable: false,
333
+ persistChat: true,
334
+ voiceInput: true,
335
+ suggestions: [],
336
+ animatedSuggestions: true,
337
+ openOnLoad: false,
338
+
339
+ // Context for AI
340
+ context: {
341
+ siteName: document.title || 'Website',
342
+ siteDescription: '',
343
+ customInfo: '',
344
+ restrictions: ''
345
+ },
346
+
347
+ // Analytics
348
+ analytics: {
349
+ enabled: true,
350
+ siteId: window.location.hostname.replace(/[^a-zA-Z0-9.-]/g, '_')
351
+ },
352
+
353
+ // Proxy mode (optional)
354
+ proxyUrl: null,
355
+
356
+ // Storage prefix
357
+ storagePrefix: 'ask_junkie_',
358
+
359
+ // Event callbacks
360
+ onReady: null,
361
+ onMessage: null,
362
+ onOpen: null,
363
+ onClose: null,
364
+ onError: null
365
+ };
366
+
367
+ // Deep merge theme object
368
+ const theme = { ...defaults.theme, ...(userConfig.theme || {}) };
369
+ const context = { ...defaults.context, ...(userConfig.context || {}) };
370
+ const analytics = { ...defaults.analytics, ...(userConfig.analytics || {}) };
371
+
372
+ return {
373
+ ...defaults,
374
+ ...userConfig,
375
+ theme,
376
+ context,
377
+ analytics
378
+ };
379
+ }
380
+
381
+ /**
382
+ * Handle user message submission
383
+ */
384
+ async _handleUserMessage(message) {
385
+ if (!message.trim()) return;
386
+
387
+ // Add user message to history
388
+ this.chatHistory.push({
389
+ role: 'user',
390
+ content: message,
391
+ timestamp: new Date().toISOString()
392
+ });
393
+
394
+ // Show typing indicator
395
+ this.widget.showTyping();
396
+
397
+ try {
398
+ // Build context string
399
+ const contextString = this._buildContext();
400
+
401
+ // Get AI response
402
+ const response = await this.aiProvider.sendMessage(
403
+ message,
404
+ contextString,
405
+ this.chatHistory.slice(-10) // Send last 10 messages for context
406
+ );
407
+
408
+ // Add bot response to history
409
+ this.chatHistory.push({
410
+ role: 'assistant',
411
+ content: response,
412
+ timestamp: new Date().toISOString()
413
+ });
414
+
415
+ // Display response
416
+ this.widget.hideTyping();
417
+ this.widget.addMessage(response, 'bot');
418
+
419
+ // Save to storage
420
+ if (this.config.persistChat) {
421
+ this.storage.set('chat_history', this.chatHistory.slice(-50));
422
+ }
423
+
424
+ // Log to analytics
425
+ if (this.analytics) {
426
+ this.analytics.logChat({
427
+ userMessage: message,
428
+ aiResponse: response,
429
+ pageUrl: window.location.href,
430
+ sessionId: this.storage.getSessionId()
431
+ });
432
+ }
433
+
434
+ // Fire callback
435
+ if (this.config.onMessage) {
436
+ this.config.onMessage(response, true);
437
+ }
438
+
439
+ this.events.emit('message', { message: response, isBot: true });
440
+
441
+ } catch (error) {
442
+ this.widget.hideTyping();
443
+ const errorMessage = 'Sorry, I encountered an error. Please try again.';
444
+ this.widget.addMessage(errorMessage, 'bot');
445
+
446
+ console.error('[AskJunkie] Error:', error);
447
+
448
+ if (this.config.onError) {
449
+ this.config.onError(error);
450
+ }
451
+
452
+ this.events.emit('error', error);
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Build AI context from configuration
458
+ */
459
+ _buildContext() {
460
+ const ctx = this.config.context;
461
+ let context = '';
462
+
463
+ context += `You are "${this.config.botName}", a friendly and helpful AI assistant for "${ctx.siteName}". `;
464
+ context += `Your personality is warm, professional, and knowledgeable. `;
465
+
466
+ context += `IMPORTANT IDENTITY RULES: `;
467
+ context += `1. Your name is ${this.config.botName}. `;
468
+ context += `2. If asked who created you, politely say you're an AI assistant designed to help with website inquiries. `;
469
+ context += `3. Stay focused on helping users with questions about this website. `;
470
+
471
+ if (ctx.siteDescription) {
472
+ context += `\n\nABOUT THIS WEBSITE: ${ctx.siteDescription} `;
473
+ }
474
+
475
+ if (ctx.customInfo) {
476
+ context += `\n\nADDITIONAL INFORMATION: ${ctx.customInfo} `;
477
+ }
478
+
479
+ if (ctx.restrictions) {
480
+ context += `\n\n🚫 RESTRICTIONS (follow strictly): ${ctx.restrictions} `;
481
+ }
482
+
483
+ context += `\n\nRESPONSE FORMATTING: `;
484
+ context += `1. Be professional and courteous. `;
485
+ context += `2. Use clear paragraphs with line breaks. `;
486
+ context += `3. For lists, use numbered points or bullets. `;
487
+ context += `4. For links, use markdown format: [Link Text](URL). `;
488
+ context += `5. Use **bold** for important terms. `;
489
+ context += `6. Keep responses helpful but concise. `;
490
+
491
+ return context;
492
+ }
493
+
494
+ /**
495
+ * Clear chat history
496
+ */
497
+ _clearHistory() {
498
+ this.chatHistory = [];
499
+ this.storage.remove('chat_history');
500
+ this.events.emit('clear');
501
+ }
502
+
503
+ // ===== PUBLIC API =====
504
+
505
+ /**
506
+ * Open the chat widget
507
+ */
508
+ static open() {
509
+ if (AskJunkie.instance?.widget) {
510
+ AskJunkie.instance.widget.open();
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Close the chat widget
516
+ */
517
+ static close() {
518
+ if (AskJunkie.instance?.widget) {
519
+ AskJunkie.instance.widget.close();
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Toggle the chat widget
525
+ */
526
+ static toggle() {
527
+ if (AskJunkie.instance?.widget) {
528
+ AskJunkie.instance.widget.toggle();
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Send a message programmatically
534
+ */
535
+ static sendMessage(text) {
536
+ if (AskJunkie.instance) {
537
+ AskJunkie.instance.widget.addMessage(text, 'user');
538
+ AskJunkie.instance._handleUserMessage(text);
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Update context dynamically
544
+ */
545
+ static setContext(newContext) {
546
+ if (AskJunkie.instance) {
547
+ AskJunkie.instance.config.context = {
548
+ ...AskJunkie.instance.config.context,
549
+ ...newContext
550
+ };
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Subscribe to events
556
+ */
557
+ static on(event, callback) {
558
+ if (AskJunkie.instance) {
559
+ AskJunkie.instance.events.on(event, callback);
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Destroy the SDK instance
565
+ */
566
+ static destroy() {
567
+ if (AskJunkie.instance) {
568
+ if (AskJunkie.instance.widget) {
569
+ AskJunkie.instance.widget.destroy();
570
+ }
571
+ AskJunkie.instance.isInitialized = false;
572
+ AskJunkie.instance = null;
573
+ console.log('[AskJunkie] Destroyed');
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Get SDK version
579
+ */
580
+ static getVersion() {
581
+ return AskJunkie.VERSION;
582
+ }
583
+ }
584
+
585
+ export default AskJunkie;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Simple Event Emitter for SDK events
3
+ */
4
+
5
+ export class EventEmitter {
6
+ constructor() {
7
+ this.events = {};
8
+ }
9
+
10
+ /**
11
+ * Subscribe to an event
12
+ */
13
+ on(event, callback) {
14
+ if (!this.events[event]) {
15
+ this.events[event] = [];
16
+ }
17
+ this.events[event].push(callback);
18
+ return () => this.off(event, callback);
19
+ }
20
+
21
+ /**
22
+ * Unsubscribe from an event
23
+ */
24
+ off(event, callback) {
25
+ if (!this.events[event]) return;
26
+ this.events[event] = this.events[event].filter(cb => cb !== callback);
27
+ }
28
+
29
+ /**
30
+ * Emit an event
31
+ */
32
+ emit(event, data) {
33
+ if (!this.events[event]) return;
34
+ this.events[event].forEach(callback => {
35
+ try {
36
+ callback(data);
37
+ } catch (error) {
38
+ console.error(`[AskJunkie] Event handler error for "${event}":`, error);
39
+ }
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Subscribe to an event once
45
+ */
46
+ once(event, callback) {
47
+ const unsubscribe = this.on(event, (data) => {
48
+ unsubscribe();
49
+ callback(data);
50
+ });
51
+ }
52
+ }
package/src/index.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Ask Junkie SDK - Main Entry Point
3
+ * Universal AI Chatbot for any website
4
+ *
5
+ * @author Junkies Coder
6
+ * @version 1.0.0
7
+ */
8
+
9
+ import AskJunkie from './core/AskJunkie.js';
10
+ import './ui/styles.css';
11
+
12
+ // Export for ES modules
13
+ export default AskJunkie;
14
+ export { AskJunkie };
15
+
16
+ // Auto-attach to window for script tag usage
17
+ if (typeof window !== 'undefined') {
18
+ window.AskJunkie = AskJunkie;
19
+ }