@cognitiondesk/widget 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/widget.js CHANGED
@@ -271,23 +271,57 @@ function generateSessionId() {
271
271
  }
272
272
 
273
273
  export default class CognitionDeskWidget {
274
+ /**
275
+ * @param {Object} config
276
+ * @param {string} config.apiKey - Required. Your CognitionDesk API key.
277
+ * @param {string} [config.widgetId] - Widget ID from the dashboard. Enables remote config.
278
+ * @param {string} [config.assistantId] - Assistant ID (overrides server value if set).
279
+ * @param {string} [config.backendUrl] - Custom backend URL.
280
+ * @param {string} [config.theme] - 'light' | 'dark' | 'auto'
281
+ * @param {string} [config.primaryColor] - Hex color for buttons / header.
282
+ * @param {string} [config.botName] - Display name shown in the header.
283
+ * @param {string} [config.botEmoji] - Emoji shown as avatar.
284
+ * @param {string} [config.welcomeMessage] - First message shown to the user.
285
+ * @param {string} [config.placeholder] - Textarea placeholder text.
286
+ * @param {string} [config.position] - 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
287
+ * @param {boolean} [config.streaming] - Enable streaming responses (default: true).
288
+ * @param {Object} [config.overrideSettings]
289
+ * Explicit code-level overrides. Any key listed here takes precedence over
290
+ * the server-side dashboard config. Use this when you want to manage
291
+ * specific settings from code rather than the CognitionDesk dashboard.
292
+ *
293
+ * Example — let the dashboard control everything except color:
294
+ * overrideSettings: { primaryColor: '#e11d48' }
295
+ *
296
+ * Example — full code control (dashboard settings ignored):
297
+ * overrideSettings: {
298
+ * primaryColor: '#e11d48', botName: 'Aria', welcomeMessage: 'Hi!'
299
+ * }
300
+ *
301
+ * When omitted (default), ALL settings come from the dashboard.
302
+ */
274
303
  constructor(config = {}) {
275
304
  if (!config.apiKey) throw new Error('[CognitionDesk] apiKey is required');
276
305
 
306
+ // Keys the developer explicitly wants to control from code.
307
+ // These override whatever the dashboard says.
308
+ this._overrides = new Set(
309
+ config.overrideSettings ? Object.keys(config.overrideSettings) : []
310
+ );
311
+
277
312
  this._cfg = {
278
313
  apiKey: config.apiKey,
279
314
  widgetId: config.widgetId || null,
280
315
  assistantId: config.assistantId || null,
281
316
  backendUrl: config.backendUrl || DEFAULT_BACKEND,
282
317
  primaryColor: config.primaryColor || '#2563eb',
283
- theme: config.theme || 'light', // 'light' | 'dark' | 'auto'
318
+ theme: config.theme || 'light',
284
319
  botName: config.botName || 'AI Assistant',
285
320
  botEmoji: config.botEmoji || '🤖',
286
321
  welcomeMessage: config.welcomeMessage || 'Hello! How can I help you today?',
287
322
  placeholder: config.placeholder || 'Type a message…',
288
323
  position: config.position || 'bottom-right',
289
- streaming: config.streaming !== false, // default: true
290
- // Rate limiting — can be overridden by inline config or merged from server config
324
+ streaming: config.streaming !== false,
291
325
  rateLimiting: {
292
326
  enabled: config.rateLimiting?.enabled !== false,
293
327
  maxMessagesPerSession: config.rateLimiting?.maxMessagesPerSession ?? 0,
@@ -295,30 +329,38 @@ export default class CognitionDeskWidget {
295
329
  limitReachedMessage: config.rateLimiting?.limitReachedMessage || "You've reached the message limit for this session.",
296
330
  rateLimitMessage: config.rateLimiting?.rateLimitMessage || "You're sending messages too quickly. Please wait a moment.",
297
331
  },
332
+ // Store overrides for re-application after each config refresh
333
+ overrideSettings: config.overrideSettings || null,
298
334
  };
299
335
 
336
+ // Apply overrides immediately on top of defaults
337
+ if (config.overrideSettings) {
338
+ this._applyOverrides(config.overrideSettings);
339
+ }
340
+
300
341
  this._sessionId = generateSessionId();
301
- this._messageCount = 0; // total sent this session
302
- this._minuteCount = 0; // sent in current minute window
303
- this._minuteStart = Date.now(); // start of current minute window
304
- this._messages = []; // { role: 'user'|'assistant', content: string }[]
305
- this._open = false;
306
- this._loading = false;
307
- this._container = null;
308
- this._shadow = null;
309
- this._panel = null;
310
- this._messagesEl = null;
311
- this._textarea = null;
312
- this._sendBtn = null;
342
+ this._messageCount = 0;
343
+ this._minuteCount = 0;
344
+ this._minuteStart = Date.now();
345
+ this._messages = [];
346
+ this._open = false;
347
+ this._loading = false;
348
+ this._container = null;
349
+ this._shadow = null;
350
+ this._panel = null;
351
+ this._messagesEl = null;
352
+ this._textarea = null;
353
+ this._sendBtn = null;
313
354
  this._viewportHandler = null;
355
+ this._refreshPending = false;
314
356
  }
315
357
 
316
358
  // ── Public API ──────────────────────────────────────────────────────────────
317
359
 
318
360
  /**
319
361
  * Mount the widget.
320
- * If `widgetId` was provided, fetches the server-side config first (async).
321
- * Returns a Promise so callers can await full initialisation.
362
+ * If `widgetId` is set, fetches server config first (async).
363
+ * Returns a Promise so callers can `await widget.mount()`.
322
364
  */
323
365
  mount(target) {
324
366
  const _mount = () => {
@@ -333,7 +375,7 @@ export default class CognitionDeskWidget {
333
375
  this._shadow.appendChild(style);
334
376
 
335
377
  this._buildDOM();
336
- this._applyTheme();
378
+ this._applyConfig();
337
379
  this._syncViewportMetrics();
338
380
  this._bindViewportMetrics();
339
381
  this._bindEvents();
@@ -345,46 +387,170 @@ export default class CognitionDeskWidget {
345
387
 
346
388
  if (this._cfg.widgetId) {
347
389
  return this._fetchWidgetConfig()
348
- .then(() => _mount())
349
- .catch(() => _mount()); // fallback to inline config on network error
390
+ .then(active => { if (active !== false) _mount(); })
391
+ .catch(() => _mount()); // network error mount with local config
350
392
  }
351
393
 
352
394
  _mount();
353
395
  return Promise.resolve(this);
354
396
  }
355
397
 
398
+ /**
399
+ * Programmatically update settings at runtime.
400
+ * Merges `newSettings` into `overrideSettings` and re-applies to the live DOM.
401
+ * Use this to change colors, bot name, etc. without remounting.
402
+ *
403
+ * widget.updateSettings({ primaryColor: '#dc2626', botName: 'Support Bot' });
404
+ */
405
+ updateSettings(newSettings) {
406
+ if (!newSettings || typeof newSettings !== 'object') return;
407
+ // Merge into overrides
408
+ const merged = { ...(this._cfg.overrideSettings || {}), ...newSettings };
409
+ this._cfg.overrideSettings = merged;
410
+ this._overrides = new Set(Object.keys(merged));
411
+ this._applyOverrides(merged);
412
+ this._applyConfig();
413
+ }
414
+
415
+ /**
416
+ * Fetch fresh config from the dashboard right now and apply it.
417
+ * Useful after the user changes settings in the CognitionDesk dashboard
418
+ * and wants them reflected immediately without reloading the page.
419
+ */
420
+ async refreshConfig() {
421
+ if (!this._cfg.widgetId) return;
422
+ const active = await this._fetchWidgetConfig();
423
+ if (active === false) {
424
+ this.unmount();
425
+ } else {
426
+ this._applyConfig();
427
+ }
428
+ }
429
+
430
+ // ── Config internals ────────────────────────────────────────────────────────
431
+
432
+ /**
433
+ * Returns true if `key` is explicitly controlled by `overrideSettings`.
434
+ * Those keys are immune to server config overwrites.
435
+ */
436
+ _userSet(key) {
437
+ return this._overrides.has(key);
438
+ }
439
+
356
440
  /**
357
441
  * Fetch public widget config from the platform and merge into this._cfg.
358
- * Only fields not already overridden by the constructor are applied.
442
+ * Server values apply to any key NOT present in `overrideSettings`.
443
+ * After merging, overrides are re-applied so they always win.
359
444
  */
360
445
  async _fetchWidgetConfig() {
361
446
  const url = `${this._cfg.backendUrl}/platforms/web-widget/public/${this._cfg.widgetId}`;
362
447
  try {
363
448
  const res = await fetch(url);
364
- if (!res.ok) return;
449
+ // Widget deleted or disabled — signal caller to unmount
450
+ if (res.status === 404 || res.status === 403) return false;
451
+ if (!res.ok) return true; // other HTTP error — keep existing config
365
452
  const { config } = await res.json();
366
453
  if (!config) return;
367
- // Merge — explicit constructor overrides take precedence
368
- if (config.botName && !this._userSet('botName')) this._cfg.botName = config.botName;
369
- if (config.welcomeMessage && !this._userSet('welcomeMessage')) this._cfg.welcomeMessage = config.welcomeMessage;
370
- if (config.primaryColor && !this._userSet('primaryColor')) this._cfg.primaryColor = config.primaryColor;
371
- if (config.placeholder && !this._userSet('placeholder')) this._cfg.placeholder = config.placeholder;
372
- if (config.assistantId && !this._cfg.assistantId) this._cfg.assistantId = config.assistantId;
373
- if (config.avatar?.value && !this._userSet('botEmoji')) this._cfg.botEmoji = config.avatar.value;
374
- // Merge rate limiting from server config (server is authoritative)
454
+
455
+ // Apply each server field unless the developer has overridden it
456
+ const apply = (key, value) => {
457
+ if (value != null && !this._userSet(key)) this._cfg[key] = value;
458
+ };
459
+
460
+ apply('botName', config.botName);
461
+ apply('welcomeMessage', config.welcomeMessage);
462
+ apply('primaryColor', config.primaryColor);
463
+ apply('secondaryColor', config.secondaryColor);
464
+ apply('placeholder', config.placeholder);
465
+ apply('theme', config.theme);
466
+ apply('position', config.position);
467
+ apply('style', config.style);
468
+ apply('size', config.size);
469
+
470
+ // assistantId: use server value only if not set inline
471
+ if (config.assistantId && !this._cfg.assistantId) {
472
+ this._cfg.assistantId = config.assistantId;
473
+ }
474
+
475
+ // Avatar emoji
476
+ if (config.avatar?.value && !this._userSet('botEmoji')) {
477
+ this._cfg.botEmoji = config.avatar.value;
478
+ }
479
+
480
+ // Rate limiting — server is always authoritative (security-sensitive)
375
481
  if (config.rateLimiting) {
376
482
  this._cfg.rateLimiting = { ...this._cfg.rateLimiting, ...config.rateLimiting };
377
483
  }
484
+
485
+ // Re-apply overrides so they always take precedence over server values
486
+ if (this._cfg.overrideSettings) {
487
+ this._applyOverrides(this._cfg.overrideSettings);
488
+ }
489
+
490
+ return true; // config applied successfully
378
491
  } catch {
379
- // Silently ignorewidget falls back to inline config
492
+ return true; // network errorkeep current config, stay mounted
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Write override values into this._cfg (flat fields only; rateLimiting is merged).
498
+ */
499
+ _applyOverrides(overrides) {
500
+ for (const [key, value] of Object.entries(overrides)) {
501
+ if (key === 'rateLimiting' && typeof value === 'object') {
502
+ this._cfg.rateLimiting = { ...this._cfg.rateLimiting, ...value };
503
+ } else {
504
+ this._cfg[key] = value;
505
+ }
380
506
  }
381
507
  }
382
508
 
383
- // eslint-disable-next-line no-unused-vars
384
- _userSet(_key) {
385
- // In production use, explicit constructor props shadow defaults.
386
- // This is a simple passthrough — override in subclasses if needed.
387
- return false;
509
+ /**
510
+ * Update live DOM elements to reflect the current this._cfg.
511
+ * Safe to call before or after mounting.
512
+ */
513
+ _applyConfig() {
514
+ // ── Theme & colors ──
515
+ const isDark = this._cfg.theme === 'dark'
516
+ || (this._cfg.theme === 'auto' && window.matchMedia?.('(prefers-color-scheme: dark)').matches);
517
+
518
+ this._root?.classList.toggle('cd-dark', isDark);
519
+ this._root?.style.setProperty('--cd-primary', this._cfg.primaryColor);
520
+ this._panel?.style.setProperty('--cd-primary', this._cfg.primaryColor);
521
+ this._toggleBtn?.style.setProperty('--cd-primary', this._cfg.primaryColor);
522
+
523
+ // ── Header: avatar + bot name ──
524
+ const avatarEl = this._shadow?.querySelector('.cd-header-avatar');
525
+ if (avatarEl) avatarEl.textContent = this._cfg.botEmoji;
526
+
527
+ const nameEl = this._shadow?.querySelector('.cd-header-name');
528
+ if (nameEl) nameEl.textContent = this._cfg.botName;
529
+
530
+ // ── Input placeholder ──
531
+ if (this._textarea) this._textarea.placeholder = this._cfg.placeholder;
532
+ }
533
+
534
+ /**
535
+ * Silently refresh config from server in the background.
536
+ * Called each time the panel opens so dashboard changes
537
+ * are reflected without reloading the page.
538
+ * Uses a debounce flag to avoid concurrent fetches.
539
+ */
540
+ _silentRefresh() {
541
+ if (!this._cfg.widgetId || this._refreshPending) return;
542
+ this._refreshPending = true;
543
+ this._fetchWidgetConfig()
544
+ .then(active => {
545
+ if (active === false) {
546
+ // Widget was deleted or disabled — remove it from the page silently
547
+ this.unmount();
548
+ } else {
549
+ this._applyConfig();
550
+ }
551
+ })
552
+ .catch(() => {})
553
+ .finally(() => { this._refreshPending = false; });
388
554
  }
389
555
 
390
556
  unmount() {
@@ -400,6 +566,8 @@ export default class CognitionDeskWidget {
400
566
  this._syncViewportMetrics();
401
567
  this._panel?.classList.add('open');
402
568
  this._textarea?.focus();
569
+ // Background refresh so config changes from dashboard appear on next open
570
+ this._silentRefresh();
403
571
  }
404
572
 
405
573
  close() {