@cognitiondesk/widget 1.2.1 → 1.2.2

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();
@@ -346,16 +388,55 @@ export default class CognitionDeskWidget {
346
388
  if (this._cfg.widgetId) {
347
389
  return this._fetchWidgetConfig()
348
390
  .then(() => _mount())
349
- .catch(() => _mount()); // fallback to inline config on network error
391
+ .catch(() => _mount());
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
+ await this._fetchWidgetConfig();
423
+ this._applyConfig();
424
+ }
425
+
426
+ // ── Config internals ────────────────────────────────────────────────────────
427
+
428
+ /**
429
+ * Returns true if `key` is explicitly controlled by `overrideSettings`.
430
+ * Those keys are immune to server config overwrites.
431
+ */
432
+ _userSet(key) {
433
+ return this._overrides.has(key);
434
+ }
435
+
356
436
  /**
357
437
  * Fetch public widget config from the platform and merge into this._cfg.
358
- * Only fields not already overridden by the constructor are applied.
438
+ * Server values apply to any key NOT present in `overrideSettings`.
439
+ * After merging, overrides are re-applied so they always win.
359
440
  */
360
441
  async _fetchWidgetConfig() {
361
442
  const url = `${this._cfg.backendUrl}/platforms/web-widget/public/${this._cfg.widgetId}`;
@@ -364,27 +445,97 @@ export default class CognitionDeskWidget {
364
445
  if (!res.ok) return;
365
446
  const { config } = await res.json();
366
447
  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)
448
+
449
+ // Apply each server field unless the developer has overridden it
450
+ const apply = (key, value) => {
451
+ if (value != null && !this._userSet(key)) this._cfg[key] = value;
452
+ };
453
+
454
+ apply('botName', config.botName);
455
+ apply('welcomeMessage', config.welcomeMessage);
456
+ apply('primaryColor', config.primaryColor);
457
+ apply('secondaryColor', config.secondaryColor);
458
+ apply('placeholder', config.placeholder);
459
+ apply('theme', config.theme);
460
+ apply('position', config.position);
461
+ apply('style', config.style);
462
+ apply('size', config.size);
463
+
464
+ // assistantId: use server value only if not set inline
465
+ if (config.assistantId && !this._cfg.assistantId) {
466
+ this._cfg.assistantId = config.assistantId;
467
+ }
468
+
469
+ // Avatar emoji
470
+ if (config.avatar?.value && !this._userSet('botEmoji')) {
471
+ this._cfg.botEmoji = config.avatar.value;
472
+ }
473
+
474
+ // Rate limiting — server is always authoritative (security-sensitive)
375
475
  if (config.rateLimiting) {
376
476
  this._cfg.rateLimiting = { ...this._cfg.rateLimiting, ...config.rateLimiting };
377
477
  }
478
+
479
+ // Re-apply overrides so they always take precedence over server values
480
+ if (this._cfg.overrideSettings) {
481
+ this._applyOverrides(this._cfg.overrideSettings);
482
+ }
378
483
  } catch {
379
- // Silently ignorewidget falls back to inline config
484
+ // Network errorkeep current config
380
485
  }
381
486
  }
382
487
 
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;
488
+ /**
489
+ * Write override values into this._cfg (flat fields only; rateLimiting is merged).
490
+ */
491
+ _applyOverrides(overrides) {
492
+ for (const [key, value] of Object.entries(overrides)) {
493
+ if (key === 'rateLimiting' && typeof value === 'object') {
494
+ this._cfg.rateLimiting = { ...this._cfg.rateLimiting, ...value };
495
+ } else {
496
+ this._cfg[key] = value;
497
+ }
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Update live DOM elements to reflect the current this._cfg.
503
+ * Safe to call before or after mounting.
504
+ */
505
+ _applyConfig() {
506
+ // ── Theme & colors ──
507
+ const isDark = this._cfg.theme === 'dark'
508
+ || (this._cfg.theme === 'auto' && window.matchMedia?.('(prefers-color-scheme: dark)').matches);
509
+
510
+ this._root?.classList.toggle('cd-dark', isDark);
511
+ this._root?.style.setProperty('--cd-primary', this._cfg.primaryColor);
512
+ this._panel?.style.setProperty('--cd-primary', this._cfg.primaryColor);
513
+ this._toggleBtn?.style.setProperty('--cd-primary', this._cfg.primaryColor);
514
+
515
+ // ── Header: avatar + bot name ──
516
+ const avatarEl = this._shadow?.querySelector('.cd-header-avatar');
517
+ if (avatarEl) avatarEl.textContent = this._cfg.botEmoji;
518
+
519
+ const nameEl = this._shadow?.querySelector('.cd-header-name');
520
+ if (nameEl) nameEl.textContent = this._cfg.botName;
521
+
522
+ // ── Input placeholder ──
523
+ if (this._textarea) this._textarea.placeholder = this._cfg.placeholder;
524
+ }
525
+
526
+ /**
527
+ * Silently refresh config from server in the background.
528
+ * Called each time the panel opens so dashboard changes
529
+ * are reflected without reloading the page.
530
+ * Uses a debounce flag to avoid concurrent fetches.
531
+ */
532
+ _silentRefresh() {
533
+ if (!this._cfg.widgetId || this._refreshPending) return;
534
+ this._refreshPending = true;
535
+ this._fetchWidgetConfig()
536
+ .then(() => this._applyConfig())
537
+ .catch(() => {})
538
+ .finally(() => { this._refreshPending = false; });
388
539
  }
389
540
 
390
541
  unmount() {
@@ -400,6 +551,8 @@ export default class CognitionDeskWidget {
400
551
  this._syncViewportMetrics();
401
552
  this._panel?.classList.add('open');
402
553
  this._textarea?.focus();
554
+ // Background refresh so config changes from dashboard appear on next open
555
+ this._silentRefresh();
403
556
  }
404
557
 
405
558
  close() {