@myned-ai/avatar-chat-widget 0.0.3 → 0.1.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.
@@ -3,7 +3,7 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  var _a;
5
5
  const __vite_import_meta_env__ = {};
6
- const DEFAULT_CONFIG$1 = {
6
+ const DEFAULT_CONFIG$2 = {
7
7
  auth: {
8
8
  enabled: false
9
9
  // Dev mode: auth disabled for local testing
@@ -61,14 +61,39 @@ const DEFAULT_CONFIG$1 = {
61
61
  ui: {
62
62
  avatarBackgroundColor: "0xffffff",
63
63
  useIrisOcclusion: true
64
+ },
65
+ assets: {
66
+ // Default to local paths (works in dev mode)
67
+ // CDN usage will auto-detect and override this in widget.ts init()
68
+ baseUrl: "",
69
+ // Empty = use root path (works with Vite's public folder)
70
+ defaultAvatarPath: "/asset/nyx.zip"
64
71
  }
65
72
  };
66
- let runtimeConfig = JSON.parse(JSON.stringify(DEFAULT_CONFIG$1));
73
+ function deepClone(obj) {
74
+ if (obj === null || typeof obj !== "object") {
75
+ return obj;
76
+ }
77
+ if (obj instanceof Date) {
78
+ return new Date(obj.getTime());
79
+ }
80
+ if (Array.isArray(obj)) {
81
+ return obj.map((item) => deepClone(item));
82
+ }
83
+ const clonedObj = {};
84
+ for (const key in obj) {
85
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
86
+ clonedObj[key] = deepClone(obj[key]);
87
+ }
88
+ }
89
+ return clonedObj;
90
+ }
91
+ let runtimeConfig = deepClone(DEFAULT_CONFIG$2);
67
92
  function deepMerge(target, source) {
68
93
  const result = { ...target };
69
94
  for (const key in source) {
70
95
  if (source[key] !== void 0) {
71
- if (typeof source[key] === "object" && source[key] !== null && !Array.isArray(source[key]) && typeof target[key] === "object") {
96
+ if (typeof source[key] === "object" && source[key] !== null && !Array.isArray(source[key]) && typeof target[key] === "object" && typeof source[key] !== "function") {
72
97
  result[key] = deepMerge(target[key], source[key]);
73
98
  } else {
74
99
  result[key] = source[key];
@@ -80,9 +105,21 @@ function deepMerge(target, source) {
80
105
  function setConfig(config) {
81
106
  runtimeConfig = deepMerge(runtimeConfig, config);
82
107
  }
83
- const CONFIG = new Proxy({}, {
84
- get(_target2, prop) {
85
- return runtimeConfig[prop];
108
+ const CONFIG = new Proxy(runtimeConfig, {
109
+ get(target, prop) {
110
+ if (typeof prop === "string" && prop in target) {
111
+ return target[prop];
112
+ }
113
+ if (typeof prop === "symbol") {
114
+ return target[prop];
115
+ }
116
+ throw new Error(`Invalid config property: ${String(prop)}`);
117
+ },
118
+ set(_target2, prop) {
119
+ throw new Error(`CONFIG is read-only. Use setConfig() to update. Attempted to set: ${String(prop)}`);
120
+ },
121
+ deleteProperty(_target2, prop) {
122
+ throw new Error(`CONFIG is read-only. Cannot delete property: ${String(prop)}`);
86
123
  }
87
124
  });
88
125
  const LogLevel = {
@@ -258,7 +295,9 @@ class LazyAvatar {
258
295
  const { GaussianAvatar: GaussianAvatar2 } = await Promise.resolve().then(() => GaussianAvatar$1);
259
296
  this._removePlaceholder();
260
297
  this._avatar = new GaussianAvatar2(this._container, this._assetsPath);
261
- await this._avatar.start?.();
298
+ if ("start" in this._avatar && typeof this._avatar.start === "function") {
299
+ await this._avatar.start();
300
+ }
262
301
  if (this._pendingState !== "Idle") {
263
302
  this._avatar.setChatState(this._pendingState);
264
303
  }
@@ -284,7 +323,9 @@ class LazyAvatar {
284
323
  */
285
324
  start() {
286
325
  if (this._avatar) {
287
- this._avatar.start?.();
326
+ if ("start" in this._avatar && typeof this._avatar.start === "function") {
327
+ this._avatar.start();
328
+ }
288
329
  } else {
289
330
  this.load();
290
331
  }
@@ -323,12 +364,12 @@ class LazyAvatar {
323
364
  }
324
365
  }
325
366
  pause() {
326
- if (this._avatar && "pause" in this._avatar) {
367
+ if (this._avatar && "pause" in this._avatar && typeof this._avatar.pause === "function") {
327
368
  this._avatar.pause();
328
369
  }
329
370
  }
330
371
  resume() {
331
- if (this._avatar && "resume" in this._avatar) {
372
+ if (this._avatar && "resume" in this._avatar && typeof this._avatar.resume === "function") {
332
373
  this._avatar.resume();
333
374
  }
334
375
  }
@@ -391,20 +432,45 @@ class EventEmitter {
391
432
  class ErrorBoundary {
392
433
  constructor() {
393
434
  this.errorHandlers = /* @__PURE__ */ new Map();
394
- this.errorCounts = /* @__PURE__ */ new Map();
395
- this.maxErrorsPerContext = 10;
435
+ this.errorWindows = /* @__PURE__ */ new Map();
436
+ this.maxErrorsPerWindow = 10;
437
+ this.errorWindowMs = 6e4;
438
+ this.circuitResetMs = 3e4;
396
439
  }
440
+ // 30 seconds to try recovery
397
441
  registerHandler(context, handler) {
398
442
  this.errorHandlers.set(context, handler);
399
443
  }
400
444
  handleError(error2, context) {
401
445
  console.error(`[${context}]`, error2);
402
- const count = (this.errorCounts.get(context) || 0) + 1;
403
- this.errorCounts.set(context, count);
404
- if (count > this.maxErrorsPerContext) {
405
- console.error(`Too many errors in context: ${context}. Circuit breaker triggered.`);
406
- this.notifyUser(`Service temporarily unavailable: ${context}`);
407
- return;
446
+ const now = Date.now();
447
+ const errorWindow = this.errorWindows.get(context);
448
+ if (errorWindow?.circuitOpen) {
449
+ if (errorWindow.circuitOpenedAt && now - errorWindow.circuitOpenedAt > this.circuitResetMs) {
450
+ console.info(`[${context}] Circuit breaker reset attempt - clearing error history`);
451
+ this.errorWindows.delete(context);
452
+ } else {
453
+ console.warn(`[${context}] Circuit breaker open - error suppressed`);
454
+ return;
455
+ }
456
+ }
457
+ if (!errorWindow || now - errorWindow.firstError > this.errorWindowMs) {
458
+ this.errorWindows.set(context, {
459
+ count: 1,
460
+ firstError: now,
461
+ circuitOpen: false
462
+ });
463
+ } else {
464
+ errorWindow.count++;
465
+ if (errorWindow.count > this.maxErrorsPerWindow) {
466
+ errorWindow.circuitOpen = true;
467
+ errorWindow.circuitOpenedAt = now;
468
+ console.error(
469
+ `[${context}] Too many errors (${errorWindow.count} in ${this.errorWindowMs}ms). Circuit breaker triggered for ${this.circuitResetMs}ms.`
470
+ );
471
+ this.notifyUser(`Service temporarily unavailable: ${context}. Retrying in ${this.circuitResetMs / 1e3}s...`);
472
+ return;
473
+ }
408
474
  }
409
475
  const handler = this.errorHandlers.get(context);
410
476
  if (handler) {
@@ -432,15 +498,33 @@ class ErrorBoundary {
432
498
  });
433
499
  window.dispatchEvent(event);
434
500
  }
501
+ /**
502
+ * Reset error tracking for a context (or all contexts)
503
+ */
435
504
  reset(context) {
436
505
  if (context) {
437
- this.errorCounts.delete(context);
506
+ this.errorWindows.delete(context);
438
507
  } else {
439
- this.errorCounts.clear();
508
+ this.errorWindows.clear();
440
509
  }
441
510
  }
511
+ /**
512
+ * Get current error count for a context
513
+ */
442
514
  getErrorCount(context) {
443
- return this.errorCounts.get(context) || 0;
515
+ return this.errorWindows.get(context)?.count || 0;
516
+ }
517
+ /**
518
+ * Check if circuit breaker is open for a context
519
+ */
520
+ isCircuitOpen(context) {
521
+ return this.errorWindows.get(context)?.circuitOpen || false;
522
+ }
523
+ /**
524
+ * Get error window stats for monitoring/debugging
525
+ */
526
+ getStats(context) {
527
+ return this.errorWindows.get(context) || null;
444
528
  }
445
529
  }
446
530
  const errorBoundary = new ErrorBoundary();
@@ -584,9 +668,11 @@ class BinaryProtocol {
584
668
  const headerSize = 5;
585
669
  const audioData = buffer.slice(headerSize);
586
670
  return {
587
- type,
588
- audio: audioData,
589
- timestamp
671
+ type: "audio_chunk",
672
+ data: audioData,
673
+ timestamp,
674
+ sessionId: ""
675
+ // SessionId not included in binary protocol, set by handler
590
676
  };
591
677
  }
592
678
  /**
@@ -598,10 +684,16 @@ class BinaryProtocol {
598
684
  const weightsData = new Float32Array(buffer, headerSize, 52);
599
685
  const weights = new Float32Array(52);
600
686
  weights.set(weightsData);
687
+ const weightsRecord = {};
688
+ weights.forEach((value, index) => {
689
+ weightsRecord[`blend_${index}`] = value;
690
+ });
601
691
  return {
602
- type,
603
- weights: Array.from(weights),
604
- timestamp
692
+ type: "blendshape",
693
+ weights: weightsRecord,
694
+ timestamp,
695
+ sessionId: ""
696
+ // SessionId not included in binary protocol, set by handler
605
697
  };
606
698
  }
607
699
  /**
@@ -618,11 +710,22 @@ class BinaryProtocol {
618
710
  const weightsData = new Float32Array(buffer, weightsStart, 52);
619
711
  const weights = new Float32Array(52);
620
712
  weights.set(weightsData);
713
+ const weightsRecord = {};
714
+ weights.forEach((value, index) => {
715
+ weightsRecord[`blend_${index}`] = value;
716
+ });
717
+ const bytes = new Uint8Array(audioData);
718
+ const binary = String.fromCharCode.apply(null, Array.from(bytes));
719
+ const base64Audio = btoa(binary);
621
720
  return {
622
- type,
623
- audio: audioData,
624
- weights: Array.from(weights),
625
- timestamp
721
+ type: "sync_frame",
722
+ audio: base64Audio,
723
+ weights: weightsRecord,
724
+ timestamp,
725
+ sessionId: "",
726
+ // SessionId not included in binary protocol, set by handler
727
+ frameIndex: 0
728
+ // FrameIndex not included in binary protocol, set by handler
626
729
  };
627
730
  }
628
731
  /**
@@ -747,6 +850,7 @@ class SocketService extends EventEmitter {
747
850
  this.heartbeatInterval = null;
748
851
  this.messageQueue = [];
749
852
  this.maxQueueSize = 100;
853
+ this.maxReconnectAttempts = 10;
750
854
  this.isIntentionallyClosed = false;
751
855
  this.useBinaryProtocol = false;
752
856
  this.authService = new AuthService();
@@ -846,8 +950,22 @@ class SocketService extends EventEmitter {
846
950
  log$c.info("Auth failed, clearing token");
847
951
  this.authService.clearToken();
848
952
  }
849
- if (!this.isIntentionallyClosed && this.reconnectAttempts < CONFIG.websocket.reconnectAttempts) {
850
- this.scheduleReconnect();
953
+ if (!this.isIntentionallyClosed) {
954
+ const maxAttempts = Math.min(CONFIG.websocket.reconnectAttempts, this.maxReconnectAttempts);
955
+ if (this.reconnectAttempts < maxAttempts) {
956
+ this.scheduleReconnect();
957
+ } else {
958
+ log$c.error(`Max reconnection attempts (${maxAttempts}) reached. Giving up.`);
959
+ this.setConnectionState("error");
960
+ this.emit("connection-failed", {
961
+ reason: "max-retries",
962
+ attempts: this.reconnectAttempts
963
+ });
964
+ errorBoundary.handleError(
965
+ new Error(`Failed to connect after ${maxAttempts} attempts`),
966
+ "websocket"
967
+ );
968
+ }
851
969
  }
852
970
  }
853
971
  scheduleReconnect() {
@@ -856,7 +974,8 @@ class SocketService extends EventEmitter {
856
974
  }
857
975
  this.setConnectionState("reconnecting");
858
976
  const delay = this.calculateReconnectDelay();
859
- log$c.info(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/${CONFIG.websocket.reconnectAttempts})`);
977
+ const maxAttempts = Math.min(CONFIG.websocket.reconnectAttempts, this.maxReconnectAttempts);
978
+ log$c.info(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1}/${maxAttempts})`);
860
979
  this.reconnectTimeout = window.setTimeout(() => {
861
980
  this.reconnectTimeout = null;
862
981
  this.reconnectAttempts++;
@@ -941,6 +1060,17 @@ class SocketService extends EventEmitter {
941
1060
  this.emit("connectionStateChanged", state);
942
1061
  }
943
1062
  }
1063
+ /**
1064
+ * Manually reconnect to the server
1065
+ * Resets reconnection counter and attempts immediate connection
1066
+ */
1067
+ async reconnect() {
1068
+ log$c.info("Manual reconnect requested");
1069
+ this.disconnect();
1070
+ this.reconnectAttempts = 0;
1071
+ this.isIntentionallyClosed = false;
1072
+ return this.connect();
1073
+ }
944
1074
  disconnect() {
945
1075
  this.isIntentionallyClosed = true;
946
1076
  if (this.reconnectTimeout !== null) {
@@ -1071,7 +1201,9 @@ class AudioInput {
1071
1201
  if (!this.audioContext || !this.sourceNode) {
1072
1202
  throw new Error("AudioContext not initialized");
1073
1203
  }
1074
- await this.audioContext.audioWorklet.addModule("/pcm16-processor.worklet.js");
1204
+ const baseUrl = CONFIG.assets.baseUrl || "";
1205
+ const workletUrl = baseUrl ? `${baseUrl}/pcm16-processor.worklet.js` : "/pcm16-processor.worklet.js";
1206
+ await this.audioContext.audioWorklet.addModule(workletUrl);
1075
1207
  this.workletNode = new AudioWorkletNode(this.audioContext, "pcm16-processor", {
1076
1208
  numberOfInputs: 1,
1077
1209
  numberOfOutputs: 1,
@@ -1188,7 +1320,7 @@ class AudioInput {
1188
1320
  };
1189
1321
  this.mediaRecorder.onerror = (event) => {
1190
1322
  errorBoundary.handleError(
1191
- new Error(`MediaRecorder error: ${event.error}`),
1323
+ new Error(`MediaRecorder error: ${event.error || "Unknown error"}`),
1192
1324
  "audio-input"
1193
1325
  );
1194
1326
  };
@@ -1538,6 +1670,7 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1538
1670
  this._context = null;
1539
1671
  this._isResumeListenerAdded = false;
1540
1672
  this._resumePromise = null;
1673
+ this._suspendPromise = null;
1541
1674
  this._sampleRate = 24e3;
1542
1675
  }
1543
1676
  static getInstance() {
@@ -1559,6 +1692,9 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1559
1692
  }
1560
1693
  try {
1561
1694
  const AudioContextClass = window.AudioContext || window.webkitAudioContext;
1695
+ if (!AudioContextClass) {
1696
+ throw new Error("AudioContext not supported in this browser");
1697
+ }
1562
1698
  this._context = new AudioContextClass({
1563
1699
  sampleRate: this._sampleRate,
1564
1700
  latencyHint: "interactive"
@@ -1604,6 +1740,7 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1604
1740
  }
1605
1741
  /**
1606
1742
  * Resume the AudioContext (call after user interaction)
1743
+ * Race-condition safe: multiple calls will share the same promise
1607
1744
  */
1608
1745
  async resume() {
1609
1746
  if (!this._context) {
@@ -1615,28 +1752,45 @@ const _AudioContextManagerImpl = class _AudioContextManagerImpl {
1615
1752
  if (this._resumePromise) {
1616
1753
  return this._resumePromise;
1617
1754
  }
1618
- this._resumePromise = this._context.resume().then(() => {
1619
- log$9.info("AudioContext resumed successfully");
1620
- }).catch((error2) => {
1621
- log$9.error("Failed to resume AudioContext:", error2);
1622
- errorBoundary.handleError(error2, "audio-context-manager");
1623
- }).finally(() => {
1624
- this._resumePromise = null;
1625
- });
1755
+ this._resumePromise = (async () => {
1756
+ try {
1757
+ await this._context.resume();
1758
+ log$9.info("AudioContext resumed successfully");
1759
+ } catch (error2) {
1760
+ log$9.error("Failed to resume AudioContext:", error2);
1761
+ errorBoundary.handleError(error2, "audio-context-manager");
1762
+ throw error2;
1763
+ } finally {
1764
+ this._resumePromise = null;
1765
+ }
1766
+ })();
1626
1767
  return this._resumePromise;
1627
1768
  }
1628
1769
  /**
1629
1770
  * Suspend the AudioContext (save resources when not needed)
1771
+ * Race-condition safe: multiple calls will share the same promise
1630
1772
  */
1631
1773
  async suspend() {
1632
- if (this._context && this._context.state === "running") {
1774
+ if (!this._context) {
1775
+ return;
1776
+ }
1777
+ if (this._context.state === "suspended") {
1778
+ return;
1779
+ }
1780
+ if (this._suspendPromise) {
1781
+ return this._suspendPromise;
1782
+ }
1783
+ this._suspendPromise = (async () => {
1633
1784
  try {
1634
1785
  await this._context.suspend();
1635
1786
  log$9.debug("AudioContext suspended");
1636
1787
  } catch (error2) {
1637
1788
  log$9.error("Failed to suspend AudioContext:", error2);
1789
+ } finally {
1790
+ this._suspendPromise = null;
1638
1791
  }
1639
- }
1792
+ })();
1793
+ return this._suspendPromise;
1640
1794
  }
1641
1795
  /**
1642
1796
  * Get current AudioContext state
@@ -2596,6 +2750,7 @@ class ChatManager {
2596
2750
  this.isRecording = false;
2597
2751
  this.animationFrameId = null;
2598
2752
  this.useSyncPlayback = false;
2753
+ this.currentStreamingMessage = null;
2599
2754
  this.avatar = avatar;
2600
2755
  this.options = options;
2601
2756
  this.userId = this.generateUserId();
@@ -2754,21 +2909,21 @@ class ChatManager {
2754
2909
  this.syncPlayback.stop();
2755
2910
  this.audioOutput.stop();
2756
2911
  this.blendshapeBuffer.clear();
2912
+ this.finalizeStreamingMessage();
2757
2913
  this.avatar.disableLiveBlendshapes();
2758
2914
  this.avatar.setChatState("Hello");
2759
2915
  }
2760
2916
  });
2761
2917
  this.socketService.on("transcript_delta", (message) => {
2762
- if (message.type === "transcript_delta") {
2763
- log$3.debug(`Transcript delta [${message.role}]: ${message.text}`);
2918
+ if (message.type === "transcript_delta" && message.text) {
2919
+ const role = message.role === "assistant" ? "assistant" : "user";
2920
+ this.streamTranscript(message.text, role);
2764
2921
  }
2765
2922
  });
2766
2923
  this.socketService.on("transcript_done", (message) => {
2767
2924
  if (message.type === "transcript_done") {
2768
2925
  log$3.debug(`Transcript complete [${message.role}]: ${message.text}`);
2769
- if (message.text) {
2770
- this.addMessage(message.text, message.role === "assistant" ? "assistant" : "user");
2771
- }
2926
+ this.finalizeStreamingMessage();
2772
2927
  }
2773
2928
  });
2774
2929
  this.socketService.on("error", (error2) => {
@@ -2891,6 +3046,60 @@ class ChatManager {
2891
3046
  this.scrollToBottom();
2892
3047
  this.options.onMessage?.({ role: sender, text });
2893
3048
  }
3049
+ /**
3050
+ * Stream transcript text in real-time (word by word)
3051
+ */
3052
+ streamTranscript(text, role) {
3053
+ if (!this.currentStreamingMessage || this.currentStreamingMessage.role !== role) {
3054
+ const messageId = Date.now().toString();
3055
+ const messageEl = document.createElement("div");
3056
+ messageEl.className = `message ${role}`;
3057
+ messageEl.dataset.id = messageId;
3058
+ const bubbleEl = document.createElement("div");
3059
+ bubbleEl.className = "message-bubble";
3060
+ bubbleEl.textContent = text;
3061
+ const timeEl = document.createElement("div");
3062
+ timeEl.className = "message-time";
3063
+ timeEl.textContent = (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
3064
+ hour: "2-digit",
3065
+ minute: "2-digit"
3066
+ });
3067
+ messageEl.appendChild(bubbleEl);
3068
+ messageEl.appendChild(timeEl);
3069
+ this.chatMessages.appendChild(messageEl);
3070
+ this.currentStreamingMessage = {
3071
+ id: messageId,
3072
+ element: messageEl,
3073
+ role
3074
+ };
3075
+ this.scrollToBottom();
3076
+ } else {
3077
+ const bubbleEl = this.currentStreamingMessage.element.querySelector(".message-bubble");
3078
+ if (bubbleEl) {
3079
+ bubbleEl.textContent += text;
3080
+ this.scrollToBottom();
3081
+ }
3082
+ }
3083
+ }
3084
+ /**
3085
+ * Finalize the streaming message and add it to messages array
3086
+ */
3087
+ finalizeStreamingMessage() {
3088
+ if (!this.currentStreamingMessage) return;
3089
+ const bubbleEl = this.currentStreamingMessage.element.querySelector(".message-bubble");
3090
+ const text = bubbleEl?.textContent || "";
3091
+ if (text) {
3092
+ const message = {
3093
+ id: this.currentStreamingMessage.id,
3094
+ text,
3095
+ sender: this.currentStreamingMessage.role,
3096
+ timestamp: Date.now()
3097
+ };
3098
+ this.messages.push(message);
3099
+ this.options.onMessage?.({ role: this.currentStreamingMessage.role, text });
3100
+ }
3101
+ this.currentStreamingMessage = null;
3102
+ }
2894
3103
  renderMessage(message) {
2895
3104
  const messageEl = document.createElement("div");
2896
3105
  messageEl.className = `message ${message.sender}`;
@@ -2919,30 +3128,51 @@ class ChatManager {
2919
3128
  this.resetOnMinimize();
2920
3129
  } else {
2921
3130
  bubble?.classList.add("hidden");
3131
+ this.reconnectOnExpand();
2922
3132
  }
2923
3133
  }
3134
+ /**
3135
+ * Reset chat state when minimizing (public API for widget)
3136
+ */
2924
3137
  resetOnMinimize() {
3138
+ log$3.info("Minimizing widget - stopping all activity");
2925
3139
  if (this.isRecording) {
3140
+ log$3.debug("Stopping recording...");
2926
3141
  this.stopRecording();
2927
3142
  }
3143
+ log$3.debug("Stopping audio playback and clearing buffers...");
2928
3144
  this.syncPlayback.stop();
2929
3145
  this.audioOutput.stop();
2930
3146
  this.blendshapeBuffer.clear();
2931
3147
  this.avatar.disableLiveBlendshapes();
3148
+ log$3.debug("Disconnecting from server...");
3149
+ this.socketService.disconnect();
2932
3150
  this.avatar.setChatState("Idle");
2933
- if ("pause" in this.avatar) {
3151
+ if ("pause" in this.avatar && typeof this.avatar.pause === "function") {
2934
3152
  this.avatar.pause();
2935
3153
  }
2936
- log$3.debug("Chat minimized - reset audio, animation, and avatar state");
3154
+ log$3.info("Widget minimized - all activity stopped");
3155
+ }
3156
+ /**
3157
+ * Reconnect to server when expanding (public API for widget)
3158
+ */
3159
+ async reconnectOnExpand() {
3160
+ try {
3161
+ await this.socketService.connect();
3162
+ log$3.debug("Chat expanded - reconnected to server");
3163
+ } catch (error2) {
3164
+ log$3.error("Failed to reconnect on expand:", error2);
3165
+ }
3166
+ if ("resume" in this.avatar && typeof this.avatar.resume === "function") {
3167
+ this.avatar.resume();
3168
+ }
2937
3169
  }
2938
3170
  openChat() {
2939
3171
  const widget = document.getElementById("chatWidget");
2940
3172
  const bubble = document.getElementById("chatBubble");
2941
3173
  widget?.classList.remove("minimized");
2942
3174
  bubble?.classList.add("hidden");
2943
- if ("resume" in this.avatar) {
2944
- this.avatar.resume();
2945
- }
3175
+ this.reconnectOnExpand();
2946
3176
  this.chatInput?.focus();
2947
3177
  }
2948
3178
  generateUserId() {
@@ -2961,6 +3191,12 @@ class ChatManager {
2961
3191
  }
2962
3192
  return bytes.buffer;
2963
3193
  }
3194
+ /**
3195
+ * Manually reconnect to the server
3196
+ */
3197
+ async reconnect() {
3198
+ return this.socketService.reconnect();
3199
+ }
2964
3200
  dispose() {
2965
3201
  if (this.animationFrameId !== null) {
2966
3202
  cancelAnimationFrame(this.animationFrameId);
@@ -2972,19 +3208,6 @@ class ChatManager {
2972
3208
  this.syncPlayback.dispose();
2973
3209
  }
2974
3210
  }
2975
- const log$2 = logger.scope("Widget");
2976
- const DEFAULT_CONFIG = {
2977
- position: "bottom-right",
2978
- theme: "light",
2979
- startCollapsed: true,
2980
- width: 380,
2981
- height: 550,
2982
- enableVoice: true,
2983
- enableText: true,
2984
- avatarUrl: "./asset/nyx.zip",
2985
- authEnabled: true,
2986
- logLevel: "error"
2987
- };
2988
3211
  const WIDGET_STYLES = `
2989
3212
  /* Reset all inherited styles */
2990
3213
  :host {
@@ -3015,11 +3238,18 @@ const WIDGET_STYLES = `
3015
3238
  height: 100%;
3016
3239
  display: flex;
3017
3240
  flex-direction: column;
3018
- background: #ffffff;
3019
- border-radius: 16px;
3020
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
3021
- overflow: hidden;
3022
- transition: all 0.3s ease;
3241
+ background: white;
3242
+ border-radius: 12px;
3243
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
3244
+ overflow: visible;
3245
+ transition: transform 0.3s ease;
3246
+ position: relative;
3247
+ }
3248
+
3249
+ .widget-root.minimized {
3250
+ transform: scale(0);
3251
+ opacity: 0;
3252
+ pointer-events: none;
3023
3253
  }
3024
3254
 
3025
3255
  .widget-root.theme-dark {
@@ -3029,285 +3259,305 @@ const WIDGET_STYLES = `
3029
3259
 
3030
3260
  /* Collapsed bubble state */
3031
3261
  :host(.collapsed) {
3032
- width: 64px !important;
3033
- height: 64px !important;
3262
+ width: 60px !important;
3263
+ height: 60px !important;
3034
3264
  }
3035
3265
 
3036
3266
  .chat-bubble {
3037
- width: 64px;
3038
- height: 64px;
3267
+ width: 60px;
3268
+ height: 60px;
3039
3269
  border-radius: 50%;
3040
3270
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
3271
+ box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);
3272
+ cursor: pointer;
3041
3273
  display: flex;
3042
3274
  align-items: center;
3043
3275
  justify-content: center;
3044
- cursor: pointer;
3045
- box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
3046
- transition: transform 0.2s, box-shadow 0.2s;
3276
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
3047
3277
  }
3048
3278
 
3049
3279
  .chat-bubble:hover {
3050
- transform: scale(1.08);
3051
- box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
3280
+ transform: scale(1.1);
3281
+ box-shadow: 0 6px 25px rgba(102, 126, 234, 0.5);
3282
+ }
3283
+
3284
+ .chat-bubble.hidden {
3285
+ transform: scale(0);
3286
+ opacity: 0;
3287
+ pointer-events: none;
3052
3288
  }
3053
3289
 
3054
3290
  .chat-bubble svg {
3055
3291
  width: 28px;
3056
3292
  height: 28px;
3057
- fill: white;
3293
+ color: white;
3058
3294
  }
3059
3295
 
3060
- /* Avatar section */
3061
- .avatar-section {
3062
- position: relative;
3063
- flex: 0 0 auto;
3064
- height: 180px;
3065
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
3296
+ /* Avatar Circle - Breaking out at top */
3297
+ .avatar-circle {
3298
+ width: 120px;
3299
+ height: 120px;
3300
+ border-radius: 50%;
3301
+ background: #ffffff;
3302
+ border: 3px solid #667eea;
3066
3303
  overflow: hidden;
3304
+ position: absolute;
3305
+ left: -35px;
3306
+ top: -40px;
3307
+ z-index: 1001;
3308
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
3309
+ transition: opacity 0.3s ease, transform 0.3s ease;
3067
3310
  }
3068
3311
 
3069
- .theme-dark .avatar-section {
3070
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
3312
+ .widget-root.minimized .avatar-circle {
3313
+ opacity: 0;
3314
+ pointer-events: none;
3315
+ transform: scale(0.5);
3071
3316
  }
3072
3317
 
3073
- .avatar-container {
3074
- width: 100%;
3075
- height: 100%;
3076
- position: relative;
3318
+ .theme-dark .avatar-circle {
3319
+ background: #1a1a2e;
3320
+ border-color: #764ba2;
3077
3321
  }
3078
3322
 
3323
+ /* Avatar render container - high-res rendering scaled down */
3079
3324
  .avatar-render-container {
3080
- width: 400px;
3081
- height: 400px;
3325
+ width: 800px;
3326
+ height: 800px;
3082
3327
  position: absolute;
3083
- top: 50%;
3328
+ top: -80px;
3084
3329
  left: 50%;
3085
- transform: translate(-50%, -50%) scale(0.45);
3086
- transform-origin: center;
3087
- /* OPTIMIZATION: Hint browser to optimize for transform animations */
3330
+ transform: translateX(-50%) scale(0.38);
3331
+ transform-origin: center top;
3088
3332
  will-change: transform;
3089
3333
  }
3090
3334
 
3091
- /* Header */
3092
- .chat-header {
3093
- display: flex;
3094
- align-items: center;
3095
- justify-content: space-between;
3096
- padding: 10px 16px;
3097
- background: rgba(255, 255, 255, 0.95);
3098
- border-bottom: 1px solid #eee;
3335
+ .avatar-render-container canvas {
3336
+ width: 800px !important;
3337
+ height: 800px !important;
3338
+ object-fit: contain;
3099
3339
  }
3100
3340
 
3101
- .theme-dark .chat-header {
3102
- background: rgba(26, 26, 46, 0.95);
3103
- border-bottom-color: #333;
3341
+ .avatar-circle > canvas {
3342
+ width: 800px !important;
3343
+ height: 800px !important;
3344
+ position: absolute;
3345
+ top: -80px;
3346
+ left: 50%;
3347
+ transform: translateX(-50%) scale(0.38);
3348
+ transform-origin: center top;
3349
+ object-fit: contain;
3104
3350
  }
3105
3351
 
3106
- .status-indicator {
3107
- display: flex;
3108
- align-items: center;
3109
- gap: 8px;
3110
- font-size: 12px;
3111
- color: #666;
3352
+ /* Speaking indicator */
3353
+ .avatar-circle.speaking {
3354
+ border-color: #27ae60;
3355
+ box-shadow: 0 6px 30px rgba(0, 0, 0, 0.3), 0 0 0 5px rgba(39, 174, 96, 0.3);
3356
+ animation: speakPulse 1s infinite;
3112
3357
  }
3113
3358
 
3114
- .theme-dark .status-indicator { color: #aaa; }
3359
+ @keyframes speakPulse {
3360
+ 0%, 100% { box-shadow: 0 6px 30px rgba(0, 0, 0, 0.3), 0 0 0 5px rgba(39, 174, 96, 0.3); }
3361
+ 50% { box-shadow: 0 6px 30px rgba(0, 0, 0, 0.3), 0 0 0 10px rgba(39, 174, 96, 0.1); }
3362
+ }
3115
3363
 
3116
- .status-dot {
3117
- width: 8px;
3118
- height: 8px;
3119
- border-radius: 50%;
3120
- background: #ccc;
3121
- transition: background 0.3s;
3364
+ /* Chat Header */
3365
+ .chat-header {
3366
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
3367
+ color: white;
3368
+ padding: 14px;
3369
+ padding-left: 95px;
3370
+ border-radius: 12px 12px 0 0;
3371
+ display: flex;
3372
+ justify-content: space-between;
3373
+ align-items: center;
3374
+ cursor: pointer;
3375
+ position: relative;
3122
3376
  }
3123
3377
 
3124
- .status-dot.connected { background: #4caf50; }
3125
- .status-dot.connecting { background: #ff9800; animation: pulse 1s infinite; }
3126
- .status-dot.error { background: #f44336; }
3378
+ .theme-dark .chat-header {
3379
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
3380
+ }
3127
3381
 
3128
- @keyframes pulse {
3129
- 0%, 100% { opacity: 1; }
3130
- 50% { opacity: 0.5; }
3382
+ .chat-header h3 {
3383
+ font-size: 15px;
3384
+ font-weight: 600;
3385
+ margin: 0;
3131
3386
  }
3132
3387
 
3133
- .header-actions {
3388
+ .chat-header-buttons {
3134
3389
  display: flex;
3135
- gap: 4px;
3390
+ gap: 8px;
3136
3391
  }
3137
3392
 
3138
- .icon-btn {
3139
- background: none;
3393
+ .chat-header-buttons button {
3394
+ background: rgba(255, 255, 255, 0.2);
3140
3395
  border: none;
3396
+ color: white;
3397
+ width: 28px;
3398
+ height: 28px;
3399
+ border-radius: 50%;
3141
3400
  cursor: pointer;
3142
- padding: 6px;
3143
- border-radius: 6px;
3144
- color: #666;
3145
- transition: background 0.2s;
3401
+ font-size: 14px;
3146
3402
  display: flex;
3147
3403
  align-items: center;
3148
3404
  justify-content: center;
3405
+ transition: background 0.2s;
3149
3406
  }
3150
3407
 
3151
- .icon-btn:hover { background: rgba(0, 0, 0, 0.05); }
3152
- .theme-dark .icon-btn { color: #aaa; }
3153
- .theme-dark .icon-btn:hover { background: rgba(255, 255, 255, 0.1); }
3408
+ .chat-header-buttons button:hover {
3409
+ background: rgba(255, 255, 255, 0.3);
3410
+ }
3154
3411
 
3155
- /* Messages */
3156
- .messages-section {
3412
+ /* Messages Container */
3413
+ .chat-messages {
3157
3414
  flex: 1;
3158
3415
  overflow-y: auto;
3159
- padding: 16px;
3416
+ padding: 14px;
3417
+ min-height: 250px;
3418
+ max-height: 320px;
3419
+ background: #fafafa;
3420
+ }
3421
+
3422
+ .theme-dark .chat-messages {
3423
+ background: #16213e;
3424
+ }
3425
+
3426
+ .message {
3427
+ margin-bottom: 16px;
3160
3428
  display: flex;
3161
3429
  flex-direction: column;
3162
- gap: 10px;
3163
- min-height: 0;
3430
+ animation: fadeIn 0.3s ease;
3164
3431
  }
3165
3432
 
3166
- .message {
3167
- max-width: 85%;
3433
+ @keyframes fadeIn {
3434
+ from { opacity: 0; transform: translateY(10px); }
3435
+ to { opacity: 1; transform: translateY(0); }
3436
+ }
3437
+
3438
+ .message.user {
3439
+ align-items: flex-end;
3440
+ }
3441
+
3442
+ .message.assistant {
3443
+ align-items: flex-start;
3444
+ }
3445
+
3446
+ .message-bubble {
3447
+ max-width: 80%;
3168
3448
  padding: 10px 14px;
3169
3449
  border-radius: 16px;
3170
3450
  word-wrap: break-word;
3171
3451
  font-size: 14px;
3172
- line-height: 1.4;
3452
+ line-height: 1.45;
3173
3453
  }
3174
3454
 
3175
- .message.user {
3176
- align-self: flex-end;
3177
- background: #007bff;
3455
+ .message.user .message-bubble {
3456
+ background: #667eea;
3178
3457
  color: white;
3179
3458
  border-bottom-right-radius: 4px;
3180
3459
  }
3181
3460
 
3182
- .message.assistant {
3183
- align-self: flex-start;
3184
- background: #f0f0f0;
3461
+ .message.assistant .message-bubble {
3462
+ background: white;
3185
3463
  color: #333;
3464
+ border: 1px solid #e0e0e0;
3186
3465
  border-bottom-left-radius: 4px;
3187
3466
  }
3188
3467
 
3189
- .theme-dark .message.assistant {
3190
- background: #2d2d44;
3468
+ .theme-dark .message.assistant .message-bubble {
3469
+ background: #1a1a2e;
3191
3470
  color: #e0e0e0;
3471
+ border-color: #333;
3192
3472
  }
3193
3473
 
3194
3474
  .message-time {
3195
- font-size: 10px;
3196
- opacity: 0.7;
3197
- margin-top: 4px;
3475
+ font-size: 11px;
3476
+ color: #999;
3477
+ margin-top: 6px;
3478
+ padding: 0 4px;
3198
3479
  }
3199
3480
 
3200
- /* Input area */
3201
- .input-section {
3202
- display: flex;
3203
- gap: 8px;
3204
- padding: 12px 16px;
3205
- border-top: 1px solid #eee;
3206
- background: #fafafa;
3481
+ /* Input Area */
3482
+ .chat-input-area {
3483
+ padding: 12px;
3484
+ background: white;
3485
+ border-top: 1px solid #e0e0e0;
3486
+ border-radius: 0 0 12px 12px;
3207
3487
  }
3208
3488
 
3209
- .theme-dark .input-section {
3210
- background: #16213e;
3489
+ .theme-dark .chat-input-area {
3490
+ background: #1a1a2e;
3211
3491
  border-top-color: #333;
3212
3492
  }
3213
3493
 
3214
- .chat-input {
3494
+ .chat-input-wrapper {
3495
+ display: flex;
3496
+ gap: 10px;
3497
+ align-items: flex-end;
3498
+ }
3499
+
3500
+ .chat-input-controls {
3501
+ display: flex;
3502
+ gap: 6px;
3503
+ align-items: center;
3504
+ flex: 1;
3505
+ }
3506
+
3507
+ #chatInput {
3215
3508
  flex: 1;
3216
3509
  padding: 10px 14px;
3217
- border: 1px solid #ddd;
3218
- border-radius: 24px;
3219
- outline: none;
3510
+ border: 1px solid #e0e0e0;
3511
+ border-radius: 20px;
3220
3512
  font-size: 14px;
3513
+ outline: none;
3514
+ transition: border-color 0.2s;
3221
3515
  font-family: inherit;
3222
- transition: border-color 0.2s, box-shadow 0.2s;
3223
3516
  }
3224
3517
 
3225
- .chat-input:focus {
3226
- border-color: #007bff;
3227
- box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
3518
+ #chatInput:focus {
3519
+ border-color: #667eea;
3228
3520
  }
3229
3521
 
3230
- .theme-dark .chat-input {
3231
- background: #1a1a2e;
3232
- border-color: #444;
3522
+ .theme-dark #chatInput {
3523
+ background: #16213e;
3524
+ border-color: #333;
3233
3525
  color: #e0e0e0;
3234
3526
  }
3235
3527
 
3236
- .theme-dark .chat-input:focus {
3237
- border-color: #667eea;
3238
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
3239
- }
3240
-
3241
- .action-btn {
3242
- width: 40px;
3243
- height: 40px;
3244
- border: none;
3528
+ .input-button {
3529
+ width: 38px;
3530
+ height: 38px;
3245
3531
  border-radius: 50%;
3532
+ border: none;
3533
+ background: #667eea;
3534
+ color: white;
3246
3535
  cursor: pointer;
3247
3536
  display: flex;
3248
3537
  align-items: center;
3249
3538
  justify-content: center;
3250
- transition: background 0.2s, transform 0.1s;
3539
+ transition: all 0.2s;
3251
3540
  flex-shrink: 0;
3252
3541
  }
3253
3542
 
3254
- .action-btn:active { transform: scale(0.95); }
3255
-
3256
- .voice-btn {
3257
- background: #f0f0f0;
3258
- color: #666;
3259
- }
3260
-
3261
- .voice-btn:hover { background: #e0e0e0; }
3262
-
3263
- .voice-btn.recording {
3264
- background: #ff4444;
3265
- color: white;
3266
- animation: pulse 1s infinite;
3267
- }
3268
-
3269
- .theme-dark .voice-btn {
3270
- background: #2d2d44;
3271
- color: #aaa;
3272
- }
3273
-
3274
- .theme-dark .voice-btn:hover { background: #3d3d54; }
3275
-
3276
- .send-btn {
3277
- background: #007bff;
3278
- color: white;
3279
- }
3280
-
3281
- .send-btn:hover { background: #0056b3; }
3282
- .send-btn:disabled { background: #ccc; cursor: not-allowed; }
3283
-
3284
- /* Loading states */
3285
- .loading-overlay {
3286
- position: absolute;
3287
- inset: 0;
3288
- background: rgba(255, 255, 255, 0.9);
3289
- display: flex;
3290
- align-items: center;
3291
- justify-content: center;
3292
- flex-direction: column;
3293
- gap: 12px;
3543
+ .input-button:hover:not(:disabled) {
3544
+ background: #5568d3;
3545
+ transform: scale(1.05);
3294
3546
  }
3295
3547
 
3296
- .theme-dark .loading-overlay {
3297
- background: rgba(26, 26, 46, 0.9);
3548
+ .input-button:disabled {
3549
+ background: #ccc;
3550
+ cursor: not-allowed;
3298
3551
  }
3299
3552
 
3300
- .spinner {
3301
- width: 32px;
3302
- height: 32px;
3303
- border: 3px solid #eee;
3304
- border-top-color: #007bff;
3305
- border-radius: 50%;
3306
- animation: spin 0.8s linear infinite;
3553
+ .input-button.recording {
3554
+ background: #e74c3c;
3555
+ animation: recordPulse 1.5s infinite;
3307
3556
  }
3308
3557
 
3309
- @keyframes spin {
3310
- to { transform: rotate(360deg); }
3558
+ @keyframes recordPulse {
3559
+ 0%, 100% { transform: scale(1); }
3560
+ 50% { transform: scale(1.1); }
3311
3561
  }
3312
3562
 
3313
3563
  /* Accessibility */
@@ -3320,61 +3570,89 @@ const WIDGET_STYLES = `
3320
3570
  overflow: hidden;
3321
3571
  clip: rect(0, 0, 0, 0);
3322
3572
  white-space: nowrap;
3323
- border: 0;
3573
+ border-width: 0;
3324
3574
  }
3325
3575
  `;
3326
3576
  const WIDGET_TEMPLATE = `
3327
3577
  <div class="widget-root">
3328
- <div class="avatar-section">
3329
- <div class="avatar-container">
3330
- <div id="avatarCircle" class="avatar-render-container"></div>
3331
- </div>
3578
+ <!-- Avatar Circle - Breaking out at top -->
3579
+ <div class="avatar-circle" id="avatarCircle" aria-label="AI Avatar">
3580
+ <div class="avatar-placeholder">👤</div>
3332
3581
  </div>
3333
-
3334
- <div class="chat-header">
3335
- <div class="status-indicator">
3336
- <span class="status-dot connecting"></span>
3337
- <span class="status-text">Connecting...</span>
3338
- </div>
3339
- <div class="header-actions">
3340
- <button class="icon-btn minimize-btn" aria-label="Minimize">
3341
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
3342
- <line x1="5" y1="12" x2="19" y2="12"/>
3343
- </svg>
3344
- </button>
3582
+
3583
+ <div class="chat-header" role="button" tabindex="0" aria-label="Toggle chat window">
3584
+ <h3>Nyx Assistant</h3>
3585
+ <div class="chat-header-buttons">
3586
+ <button id="minimizeBtn" aria-label="Minimize chat" title="Minimize">−</button>
3345
3587
  </div>
3346
3588
  </div>
3347
-
3348
- <div id="chatMessages" class="messages-section" role="log" aria-live="polite" aria-label="Chat messages"></div>
3349
-
3350
- <div class="input-section">
3351
- <input
3352
- type="text"
3353
- id="chatInput"
3354
- class="chat-input"
3355
- placeholder="Type a message..."
3356
- aria-label="Chat message input"
3357
- />
3358
- <button id="micBtn" class="action-btn voice-btn" aria-label="Voice input" aria-pressed="false">
3359
- <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
3360
- <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 1.93c-3.94-.49-7-3.85-7-7.93h2c0 3.31 2.69 6 6 6s6-2.69 6-6h2c0 4.08-3.06 7.44-7 7.93V20h4v2H8v-2h4v-4.07z"/>
3361
- </svg>
3362
- </button>
3363
- <button id="sendBtn" class="action-btn send-btn" aria-label="Send message">
3364
- <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
3365
- <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
3366
- </svg>
3367
- </button>
3589
+
3590
+ <div class="chat-messages" id="chatMessages" role="log" aria-live="polite" aria-atomic="false">
3591
+ <!-- Messages will be added here dynamically -->
3592
+ </div>
3593
+
3594
+ <div class="chat-input-area">
3595
+ <div class="chat-input-wrapper">
3596
+ <div class="chat-input-controls">
3597
+ <input
3598
+ type="text"
3599
+ id="chatInput"
3600
+ placeholder="Type a message..."
3601
+ aria-label="Chat message input"
3602
+ autocomplete="off"
3603
+ />
3604
+ <button
3605
+ id="micBtn"
3606
+ class="input-button"
3607
+ aria-label="Voice input"
3608
+ title="Voice input"
3609
+ >
3610
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
3611
+ <path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/>
3612
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
3613
+ <line x1="12" x2="12" y1="19" y2="22"/>
3614
+ </svg>
3615
+ </button>
3616
+ <button
3617
+ id="sendBtn"
3618
+ class="input-button"
3619
+ aria-label="Send message"
3620
+ title="Send"
3621
+ >
3622
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
3623
+ <path d="M22 2 11 13"/>
3624
+ <path d="M22 2 15 22 11 13 2 9 22 2z"/>
3625
+ </svg>
3626
+ </button>
3627
+ </div>
3628
+ </div>
3368
3629
  </div>
3369
3630
  </div>
3370
3631
  `;
3371
3632
  const BUBBLE_TEMPLATE = `
3372
3633
  <div class="chat-bubble" role="button" aria-label="Open chat" tabindex="0">
3373
- <svg viewBox="0 0 24 24">
3374
- <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"/>
3634
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
3635
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
3375
3636
  </svg>
3376
3637
  </div>
3377
3638
  `;
3639
+ const DEFAULT_CONFIG$1 = {
3640
+ position: "bottom-right",
3641
+ theme: "light",
3642
+ startCollapsed: true,
3643
+ width: 380,
3644
+ height: 550,
3645
+ enableVoice: true,
3646
+ enableText: true,
3647
+ authEnabled: false,
3648
+ logLevel: "error"
3649
+ };
3650
+ const log$2 = logger.scope("Widget");
3651
+ const DEFAULT_CONFIG = {
3652
+ ...DEFAULT_CONFIG$1,
3653
+ avatarUrl: "./asset/nyx.zip"
3654
+ // Override for local dev compatibility
3655
+ };
3378
3656
  class AvatarChatElement extends HTMLElement {
3379
3657
  constructor() {
3380
3658
  super();
@@ -3383,6 +3661,8 @@ class AvatarChatElement extends HTMLElement {
3383
3661
  this._isMounted = false;
3384
3662
  this._isConnected = false;
3385
3663
  this._isCollapsed = false;
3664
+ this.themeMediaQuery = null;
3665
+ this.themeChangeHandler = null;
3386
3666
  this.shadow = this.attachShadow({ mode: "open" });
3387
3667
  }
3388
3668
  /**
@@ -3418,7 +3698,7 @@ class AvatarChatElement extends HTMLElement {
3418
3698
  this.classList.add(`position-${this.config.position}`);
3419
3699
  }
3420
3700
  this.style.width = `${this.config.width}px`;
3421
- this.style.height = `${this.config.height}px`;
3701
+ this.style.maxHeight = `${this.config.height}px`;
3422
3702
  if (this.config.startCollapsed) {
3423
3703
  this._isCollapsed = true;
3424
3704
  this.classList.add("collapsed");
@@ -3453,7 +3733,7 @@ class AvatarChatElement extends HTMLElement {
3453
3733
  if (voiceBtn) voiceBtn.style.display = "none";
3454
3734
  }
3455
3735
  if (!this.config.enableText) {
3456
- const inputSection = this.shadow.querySelector(".input-section");
3736
+ const inputSection = this.shadow.querySelector(".chat-input-area");
3457
3737
  if (inputSection) inputSection.style.display = "none";
3458
3738
  }
3459
3739
  await this.initializeAvatar();
@@ -3485,8 +3765,7 @@ class AvatarChatElement extends HTMLElement {
3485
3765
  return;
3486
3766
  }
3487
3767
  const renderContainer = document.createElement("div");
3488
- renderContainer.style.width = "400px";
3489
- renderContainer.style.height = "400px";
3768
+ renderContainer.className = "avatar-render-container";
3490
3769
  avatarContainer.appendChild(renderContainer);
3491
3770
  try {
3492
3771
  this.avatar = new LazyAvatar(
@@ -3553,44 +3832,36 @@ class AvatarChatElement extends HTMLElement {
3553
3832
  * Setup UI event listeners
3554
3833
  */
3555
3834
  setupUIEvents() {
3556
- const minimizeBtn = this.shadow.querySelector(".minimize-btn");
3835
+ const minimizeBtn = this.shadow.getElementById("minimizeBtn");
3557
3836
  minimizeBtn?.addEventListener("click", () => this.collapse());
3558
3837
  if (this.config.theme === "auto") {
3559
- window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
3838
+ this.themeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
3839
+ this.themeChangeHandler = (e) => {
3560
3840
  const root = this.shadow.querySelector(".widget-root");
3561
3841
  root?.classList.toggle("theme-dark", e.matches);
3562
- });
3842
+ };
3843
+ this.themeMediaQuery.addEventListener("change", this.themeChangeHandler);
3563
3844
  }
3564
3845
  }
3565
3846
  /**
3566
- * Update connection status UI
3847
+ * Update connection status (stub - connection indicator removed from UI)
3567
3848
  */
3568
- updateConnectionStatus(connected, state) {
3569
- const dot = this.shadow.querySelector(".status-dot");
3570
- const text = this.shadow.querySelector(".status-text");
3571
- if (dot) {
3572
- dot.classList.remove("connected", "connecting", "error");
3573
- if (state === "error") {
3574
- dot.classList.add("error");
3575
- } else {
3576
- dot.classList.add(connected ? "connected" : "connecting");
3577
- }
3578
- }
3579
- if (text) {
3580
- if (state === "error") {
3581
- text.textContent = "Connection failed";
3582
- } else {
3583
- text.textContent = connected ? "Connected" : "Connecting...";
3584
- }
3585
- }
3849
+ updateConnectionStatus(_connected, _state) {
3586
3850
  }
3587
3851
  /**
3588
3852
  * Collapse to bubble
3589
3853
  */
3590
3854
  collapse() {
3591
3855
  if (this._isCollapsed) return;
3856
+ if (this.chatManager) {
3857
+ this.chatManager.resetOnMinimize();
3858
+ }
3592
3859
  this._isCollapsed = true;
3593
3860
  this.classList.add("collapsed");
3861
+ const widgetRoot = this.shadow.querySelector(".widget-root");
3862
+ if (widgetRoot) {
3863
+ widgetRoot.style.display = "none";
3864
+ }
3594
3865
  this.renderBubble();
3595
3866
  }
3596
3867
  /**
@@ -3601,8 +3872,18 @@ class AvatarChatElement extends HTMLElement {
3601
3872
  this._isCollapsed = false;
3602
3873
  this.classList.remove("collapsed");
3603
3874
  this.style.width = `${this.config.width}px`;
3604
- this.style.height = `${this.config.height}px`;
3605
- await this.renderWidget();
3875
+ this.style.maxHeight = `${this.config.height}px`;
3876
+ const widgetRoot = this.shadow.querySelector(".widget-root");
3877
+ if (widgetRoot) {
3878
+ const bubble = this.shadow.querySelector(".chat-bubble");
3879
+ if (bubble) bubble.remove();
3880
+ widgetRoot.style.display = "flex";
3881
+ if (this.chatManager) {
3882
+ await this.chatManager.reconnectOnExpand();
3883
+ }
3884
+ } else {
3885
+ await this.renderWidget();
3886
+ }
3606
3887
  }
3607
3888
  /**
3608
3889
  * Show widget
@@ -3636,11 +3917,26 @@ class AvatarChatElement extends HTMLElement {
3636
3917
  isServerConnected() {
3637
3918
  return this._isConnected;
3638
3919
  }
3920
+ /**
3921
+ * Manually reconnect to the server
3922
+ * Useful after network changes or connection failures
3923
+ */
3924
+ async reconnect() {
3925
+ if (!this.chatManager) {
3926
+ throw new Error("Widget not initialized");
3927
+ }
3928
+ return this.chatManager.reconnect();
3929
+ }
3639
3930
  /**
3640
3931
  * Cleanup
3641
3932
  */
3642
3933
  destroy() {
3643
3934
  log$2.info("Destroying widget");
3935
+ if (this.themeMediaQuery && this.themeChangeHandler) {
3936
+ this.themeMediaQuery.removeEventListener("change", this.themeChangeHandler);
3937
+ this.themeMediaQuery = null;
3938
+ this.themeChangeHandler = null;
3939
+ }
3644
3940
  if (this.chatManager) {
3645
3941
  this.chatManager.dispose();
3646
3942
  this.chatManager = null;
@@ -3663,6 +3959,25 @@ const AvatarChat = {
3663
3959
  version: "__VERSION__",
3664
3960
  /** Active instance */
3665
3961
  _instance: null,
3962
+ /**
3963
+ * Get the URL for the default included avatar
3964
+ * Auto-detects CDN usage and returns the appropriate URL
3965
+ */
3966
+ getDefaultAvatarUrl() {
3967
+ const scripts = document.getElementsByTagName("script");
3968
+ for (let i = 0; i < scripts.length; i++) {
3969
+ const src = scripts[i].src;
3970
+ if (src.includes("jsdelivr.net") && src.includes("avatar-chat-widget")) {
3971
+ const baseUrl = src.substring(0, src.lastIndexOf("/"));
3972
+ return `${baseUrl}/public/asset/nyx.zip`;
3973
+ }
3974
+ if (src.includes("unpkg.com") && src.includes("avatar-chat-widget")) {
3975
+ const baseUrl = src.substring(0, src.lastIndexOf("/"));
3976
+ return `${baseUrl}/public/asset/nyx.zip`;
3977
+ }
3978
+ }
3979
+ return "/asset/nyx.zip";
3980
+ },
3666
3981
  /**
3667
3982
  * Initialize and mount the widget
3668
3983
  */
@@ -3670,9 +3985,74 @@ const AvatarChat = {
3670
3985
  if (!config.serverUrl) {
3671
3986
  throw new Error("AvatarChat.init(): serverUrl is required");
3672
3987
  }
3988
+ if (!config.serverUrl.match(/^wss?:\/\/.+/)) {
3989
+ throw new Error("AvatarChat.init(): serverUrl must be a valid WebSocket URL (ws:// or wss://)");
3990
+ }
3673
3991
  if (!config.container) {
3674
3992
  throw new Error("AvatarChat.init(): container is required");
3675
3993
  }
3994
+ const containerElement = typeof config.container === "string" ? document.querySelector(config.container) : config.container;
3995
+ if (!containerElement) {
3996
+ throw new Error(`AvatarChat.init(): container not found: ${config.container}`);
3997
+ }
3998
+ if (config.width !== void 0) {
3999
+ if (typeof config.width !== "number" || config.width < 200 || config.width > 2e3) {
4000
+ throw new Error("AvatarChat.init(): width must be a number between 200 and 2000 pixels");
4001
+ }
4002
+ }
4003
+ if (config.height !== void 0) {
4004
+ if (typeof config.height !== "number" || config.height < 300 || config.height > 2e3) {
4005
+ throw new Error("AvatarChat.init(): height must be a number between 300 and 2000 pixels");
4006
+ }
4007
+ }
4008
+ if (config.onReady !== void 0 && typeof config.onReady !== "function") {
4009
+ throw new Error("AvatarChat.init(): onReady must be a function");
4010
+ }
4011
+ if (config.onMessage !== void 0 && typeof config.onMessage !== "function") {
4012
+ throw new Error("AvatarChat.init(): onMessage must be a function");
4013
+ }
4014
+ if (config.onError !== void 0 && typeof config.onError !== "function") {
4015
+ throw new Error("AvatarChat.init(): onError must be a function");
4016
+ }
4017
+ if (config.logLevel !== void 0) {
4018
+ const validLogLevels = ["none", "error", "warn", "info", "debug"];
4019
+ if (!validLogLevels.includes(config.logLevel)) {
4020
+ throw new Error(`AvatarChat.init(): logLevel must be one of: ${validLogLevels.join(", ")}`);
4021
+ }
4022
+ }
4023
+ if (config.theme !== void 0) {
4024
+ const validThemes = ["light", "dark", "auto"];
4025
+ if (!validThemes.includes(config.theme)) {
4026
+ throw new Error(`AvatarChat.init(): theme must be one of: ${validThemes.join(", ")}`);
4027
+ }
4028
+ }
4029
+ if (config.position !== void 0) {
4030
+ const validPositions = ["inline", "bottom-right", "bottom-left", "top-right", "top-left"];
4031
+ if (!validPositions.includes(config.position)) {
4032
+ throw new Error(`AvatarChat.init(): position must be one of: ${validPositions.join(", ")}`);
4033
+ }
4034
+ }
4035
+ if (config.assetsBaseUrl) {
4036
+ setConfig({ assets: { baseUrl: config.assetsBaseUrl, defaultAvatarPath: "/asset/nyx.zip" } });
4037
+ } else {
4038
+ const scripts = document.getElementsByTagName("script");
4039
+ for (let i = 0; i < scripts.length; i++) {
4040
+ const src = scripts[i].src;
4041
+ if (src.includes("jsdelivr.net") && src.includes("avatar-chat-widget")) {
4042
+ const baseUrl = src.substring(0, src.lastIndexOf("/"));
4043
+ setConfig({ assets: { baseUrl: `${baseUrl}/public`, defaultAvatarPath: "/asset/nyx.zip" } });
4044
+ break;
4045
+ }
4046
+ if (src.includes("unpkg.com") && src.includes("avatar-chat-widget")) {
4047
+ const baseUrl = src.substring(0, src.lastIndexOf("/"));
4048
+ setConfig({ assets: { baseUrl: `${baseUrl}/public`, defaultAvatarPath: "/asset/nyx.zip" } });
4049
+ break;
4050
+ }
4051
+ }
4052
+ }
4053
+ if (!config.avatarUrl) {
4054
+ config.avatarUrl = this.getDefaultAvatarUrl();
4055
+ }
3676
4056
  const containerEl = typeof config.container === "string" ? document.querySelector(config.container) : config.container;
3677
4057
  if (!containerEl) {
3678
4058
  throw new Error(`AvatarChat.init(): container not found: ${config.container}`);
@@ -3698,7 +4078,8 @@ const AvatarChat = {
3698
4078
  expand: () => widget.expand(),
3699
4079
  collapse: () => widget.collapse(),
3700
4080
  isMounted: () => widget.isMounted(),
3701
- isConnected: () => widget.isServerConnected()
4081
+ isConnected: () => widget.isServerConnected(),
4082
+ reconnect: () => widget.reconnect()
3702
4083
  };
3703
4084
  },
3704
4085
  /**
@@ -3728,7 +4109,8 @@ const AvatarChat = {
3728
4109
  expand: () => widget.expand(),
3729
4110
  collapse: () => widget.collapse(),
3730
4111
  isMounted: () => widget.isMounted(),
3731
- isConnected: () => widget.isServerConnected()
4112
+ isConnected: () => widget.isServerConnected(),
4113
+ reconnect: () => widget.reconnect()
3732
4114
  };
3733
4115
  }
3734
4116
  };
@@ -40123,7 +40505,7 @@ function interceptControlUp(event) {
40123
40505
  document2.removeEventListener("keyup", this._interceptControlUp, { passive: true, capture: true });
40124
40506
  }
40125
40507
  }
40126
- const j = { None: 0, Info: 3 }, Y = { Always: 0, Never: 2 }, J = { Ply: 0 }, X = (e) => e.endsWith(".ply") ? J.Ply : null, $ = { Default: 0, Instant: 2 }, Z = { ThreeD: 0, TwoD: 1 };
40508
+ const j = { None: 0, Info: 3 }, Y = { Always: 0, Never: 2 }, J = { Ply: 0 }, X = (e) => e.endsWith(".ply") ? J.Ply : null, $ = { Default: 0, Gradual: 1, Instant: 2 }, Z = { ThreeD: 0, TwoD: 1 };
40127
40509
  let ee = (_a = class {
40128
40510
  }, __publicField(_a, "DefaultSplatSortDistanceMapPrecision", 16), __publicField(_a, "MemoryPageSize", 65536), __publicField(_a, "BytesPerFloat", 4), __publicField(_a, "BytesPerInt", 4), __publicField(_a, "MaxScenes", 32), __publicField(_a, "ProgressiveLoadSectionSize", 262144), __publicField(_a, "ProgressiveLoadSectionDelayDuration", 15), __publicField(_a, "SphericalHarmonics8BitCompressionRange", 3), _a);
40129
40511
  const te = ee.SphericalHarmonics8BitCompressionRange, Ae = 15e5, Ce = { DirectToSplatBuffer: 0, DirectToSplatArray: 1, DownloadBeforeProcessing: 2 }, ye = { Downloading: 0, Processing: 1, Done: 2 };
@@ -41121,7 +41503,7 @@ class As {
41121
41503
  ` : t += "\n bool isRightIris = false;\n ", e.left_iris && e.left_iris.length > 0 ? t += `
41122
41504
  // Check if this splat is part of left iris
41123
41505
  bool isLeftIris = ${e.left_iris.map(([e2, t2]) => `(idx >= ${e2}.0 && idx <= ${t2}.0)`).join(" ||\n ")};
41124
- ` : t += "\n bool isLeftIris = false;\n ", t += "\n float finalOpacity = opacity;\n\n // Smooth fade: iris fades out as eye closes (blink increases)\n // smoothstep(0.1, 0.5, blink) = 0 when blink<0.1, 1 when blink>0.5\n if (isRightIris) {\n float fadeFactor = 1.0 - smoothstep(0.1, 0.5, eyeBlinkRight);\n finalOpacity = opacity * fadeFactor;\n } else if (isLeftIris) {\n float fadeFactor = 1.0 - smoothstep(0.1, 0.5, eyeBlinkLeft);\n finalOpacity = opacity * fadeFactor;\n }\n\n if (finalOpacity < 1.0 / 255.0)\n discard;\n\n gl_FragColor = vec4(vColor.rgb, finalOpacity);\n }\n ") : t += "\n gl_FragColor = vec4(vColor.rgb, opacity);\n }\n ", t;
41506
+ ` : t += "\n bool isLeftIris = false;\n ", t += "\n float finalOpacity = opacity;\n\n // Very narrow fade window at high blink values only\n // Iris stays visible until eye is almost completely closed\n if (isRightIris) {\n float fadeFactor = 1.0 - smoothstep(0.5, 0.7, eyeBlinkRight);\n finalOpacity = opacity * fadeFactor;\n } else if (isLeftIris) {\n float fadeFactor = 1.0 - smoothstep(0.5, 0.7, eyeBlinkLeft);\n finalOpacity = opacity * fadeFactor;\n }\n\n if (finalOpacity < 1.0 / 255.0)\n discard;\n\n gl_FragColor = vec4(vColor.rgb, finalOpacity);\n }\n ") : t += "\n gl_FragColor = vec4(vColor.rgb, opacity);\n }\n ", t;
41125
41507
  }
41126
41508
  }
41127
41509
  class fs {
@@ -41249,7 +41631,7 @@ function Is(e, t, s) {
41249
41631
  }
41250
41632
  const bs = 16777216;
41251
41633
  class ws extends Mesh {
41252
- constructor(e = Z.ThreeD, t = false, i = false, n = false, r = 1, a = true, o = false, l = false, c = 1024, h = j.None, d = 0, u = 1, p = 0.3) {
41634
+ constructor(e = Z.ThreeD, t = false, i = false, n = false, r = 1, a = true, o = false, l = false, c = 1024, h = j.None, d = 0, u = 1, p = 0.3, m = null) {
41253
41635
  super(Cs, ys);
41254
41636
  __publicField(this, "buildSplatTree", (e = [], t, s) => new Promise((i) => {
41255
41637
  this.disposeSplatTree(), this.baseSplatTree = new ms(8, 1e3);
@@ -41405,7 +41787,7 @@ class ws extends Mesh {
41405
41787
  this.getLocalSplatParameters(t, e), e.splatBuffer.getSplatColor(e.localIndex, s);
41406
41788
  };
41407
41789
  }());
41408
- this.renderer = void 0, this.splatRenderMode = e, this.dynamicMode = t, this.enableOptionalEffects = i, this.halfPrecisionCovariancesOnGPU = n, this.devicePixelRatio = r, this.enableDistancesComputationOnGPU = a, this.integerBasedDistancesComputation = o, this.antialiased = l, this.kernel2DSize = p, this.maxScreenSpaceSplatSize = c, this.logLevel = h, this.sphericalHarmonicsDegree = d, this.minSphericalHarmonicsDegree = 0, this.sceneFadeInRateMultiplier = u, this.scenes = [], this.splatTree = null, this.baseSplatTree = null, this.splatDataTextures = {}, this.flameModel = null, this.expressionBSNum = 0, this.bsWeight = [], this.bonesMatrix = null, this.bonesNum = null, this.bonesWeight = null, this.gaussianSplatCount = null, this.morphTargetDictionary = null, this.distancesTransformFeedback = { id: null, vertexShader: null, fragmentShader: null, program: null, centersBuffer: null, sceneIndexesBuffer: null, outDistancesBuffer: null, centersLoc: -1, modelViewProjLoc: -1, sceneIndexesLoc: -1, transformsLocs: [] }, this.globalSplatIndexToLocalSplatIndexMap = [], this.globalSplatIndexToSceneIndexMap = [], this.irisOcclusionConfig = null, this.lastBuildSplatCount = 0, this.lastBuildScenes = [], this.lastBuildMaxSplatCount = 0, this.lastBuildSceneCount = 0, this.firstRenderTime = -1, this.finalBuild = false, this.webGLUtils = null, this.boundingBox = new Box3(), this.calculatedSceneCenter = new Vector3(), this.maxSplatDistanceFromSceneCenter = 0, this.visibleRegionBufferRadius = 0, this.visibleRegionRadius = 0, this.visibleRegionFadeStartRadius = 0, this.visibleRegionChanging = false, this.splatScale = 1, this.pointCloudModeEnabled = false, this.disposed = false, this.lastRenderer = null, this.visible = false;
41790
+ this.renderer = void 0, this.splatRenderMode = e, this.dynamicMode = t, this.enableOptionalEffects = i, this.halfPrecisionCovariancesOnGPU = n, this.devicePixelRatio = r, this.enableDistancesComputationOnGPU = a, this.integerBasedDistancesComputation = o, this.antialiased = l, this.kernel2DSize = p, this.maxScreenSpaceSplatSize = c, this.logLevel = h, this.sphericalHarmonicsDegree = d, this.minSphericalHarmonicsDegree = 0, this.sceneFadeInRateMultiplier = u, this.scenes = [], this.splatTree = null, this.baseSplatTree = null, this.splatDataTextures = {}, this.flameModel = null, this.expressionBSNum = 0, this.bsWeight = [], this.bonesMatrix = null, this.bonesNum = null, this.bonesWeight = null, this.gaussianSplatCount = null, this.morphTargetDictionary = null, this.distancesTransformFeedback = { id: null, vertexShader: null, fragmentShader: null, program: null, centersBuffer: null, sceneIndexesBuffer: null, outDistancesBuffer: null, centersLoc: -1, modelViewProjLoc: -1, sceneIndexesLoc: -1, transformsLocs: [] }, this.globalSplatIndexToLocalSplatIndexMap = [], this.globalSplatIndexToSceneIndexMap = [], this.irisOcclusionConfig = m, this.lastBuildSplatCount = 0, this.lastBuildScenes = [], this.lastBuildMaxSplatCount = 0, this.lastBuildSceneCount = 0, this.firstRenderTime = -1, this.finalBuild = false, this.webGLUtils = null, this.boundingBox = new Box3(), this.calculatedSceneCenter = new Vector3(), this.maxSplatDistanceFromSceneCenter = 0, this.visibleRegionBufferRadius = 0, this.visibleRegionRadius = 0, this.visibleRegionFadeStartRadius = 0, this.visibleRegionChanging = false, this.splatScale = 1, this.pointCloudModeEnabled = false, this.disposed = false, this.lastRenderer = null, this.visible = false;
41409
41791
  }
41410
41792
  static buildScenes(e, t, i) {
41411
41793
  const r = [];
@@ -43256,7 +43638,7 @@ const _Viewer = class _Viewer {
43256
43638
  }
43257
43639
  };
43258
43640
  }());
43259
- if (e.cameraUp || (e.cameraUp = [0, 1, 0]), this.cameraUp = new Vector3().fromArray(e.cameraUp), e.initialCameraPosition || (e.initialCameraPosition = [0, 10, 15]), this.initialCameraPosition = new Vector3().fromArray(e.initialCameraPosition), e.initialCameraRotation || (e.initialCameraRotation = [0, 0, 0]), this.initialCameraRotation = new Vector3().fromArray(e.initialCameraRotation), this.backgroundColor = e.backgroundColor, e.initialCameraLookAt || (e.initialCameraLookAt = [0, 0, 0]), this.initialCameraLookAt = new Vector3().fromArray(e.initialCameraLookAt), this.dropInMode = e.dropInMode || false, void 0 !== e.selfDrivenMode && null !== e.selfDrivenMode || (e.selfDrivenMode = true), this.selfDrivenMode = e.selfDrivenMode && !this.dropInMode, this.selfDrivenUpdateFunc = this.selfDrivenUpdate.bind(this), void 0 === e.useBuiltInControls && (e.useBuiltInControls = true), this.useBuiltInControls = e.useBuiltInControls, this.rootElement = e.rootElement, this.canvas = e.threejsCanvas, this.ignoreDevicePixelRatio = e.ignoreDevicePixelRatio || false, this.devicePixelRatio = this.ignoreDevicePixelRatio ? 1 : window.devicePixelRatio || 1, this.halfPrecisionCovariancesOnGPU = e.halfPrecisionCovariancesOnGPU || false, this.threeScene = e.threeScene, this.renderer = e.renderer, this.camera = e.camera, this.gpuAcceleratedSort = e.gpuAcceleratedSort || false, void 0 !== e.integerBasedSort && null !== e.integerBasedSort || (e.integerBasedSort = true), this.integerBasedSort = e.integerBasedSort, void 0 !== e.sharedMemoryForWorkers && null !== e.sharedMemoryForWorkers || (e.sharedMemoryForWorkers = true), this.sharedMemoryForWorkers = false, this.dynamicScene = !!e.dynamicScene, this.antialiased = e.antialiased || false, this.kernel2DSize = void 0 === e.kernel2DSize ? 0.3 : e.kernel2DSize, this.renderMode = e.renderMode || Y.Always, this.sceneRevealMode = e.sceneRevealMode || $.Default, this.focalAdjustment = e.focalAdjustment || 1, this.maxScreenSpaceSplatSize = e.maxScreenSpaceSplatSize || 1024, this.logLevel = e.logLevel || j.None, this.sphericalHarmonicsDegree = e.sphericalHarmonicsDegree || 0, this.enableOptionalEffects = e.enableOptionalEffects || false, void 0 !== e.enableSIMDInSort && null !== e.enableSIMDInSort || (e.enableSIMDInSort = true), this.enableSIMDInSort = e.enableSIMDInSort, void 0 !== e.inMemoryCompressionLevel && null !== e.inMemoryCompressionLevel || (e.inMemoryCompressionLevel = 0), this.inMemoryCompressionLevel = e.inMemoryCompressionLevel, void 0 !== e.optimizeSplatData && null !== e.optimizeSplatData || (e.optimizeSplatData = true), this.optimizeSplatData = e.optimizeSplatData, void 0 !== e.freeIntermediateSplatData && null !== e.freeIntermediateSplatData || (e.freeIntermediateSplatData = false), this.freeIntermediateSplatData = e.freeIntermediateSplatData, je()) {
43641
+ if (e.cameraUp || (e.cameraUp = [0, 1, 0]), this.cameraUp = new Vector3().fromArray(e.cameraUp), e.initialCameraPosition || (e.initialCameraPosition = [0, 10, 15]), this.initialCameraPosition = new Vector3().fromArray(e.initialCameraPosition), e.initialCameraRotation || (e.initialCameraRotation = [0, 0, 0]), this.initialCameraRotation = new Vector3().fromArray(e.initialCameraRotation), this.backgroundColor = e.backgroundColor, this.irisOcclusionConfig = e.irisOcclusionConfig || null, e.initialCameraLookAt || (e.initialCameraLookAt = [0, 0, 0]), this.initialCameraLookAt = new Vector3().fromArray(e.initialCameraLookAt), this.dropInMode = e.dropInMode || false, void 0 !== e.selfDrivenMode && null !== e.selfDrivenMode || (e.selfDrivenMode = true), this.selfDrivenMode = e.selfDrivenMode && !this.dropInMode, this.selfDrivenUpdateFunc = this.selfDrivenUpdate.bind(this), void 0 === e.useBuiltInControls && (e.useBuiltInControls = true), this.useBuiltInControls = e.useBuiltInControls, this.rootElement = e.rootElement, this.canvas = e.threejsCanvas, this.ignoreDevicePixelRatio = e.ignoreDevicePixelRatio || false, this.devicePixelRatio = this.ignoreDevicePixelRatio ? 1 : window.devicePixelRatio || 1, this.halfPrecisionCovariancesOnGPU = e.halfPrecisionCovariancesOnGPU || false, this.threeScene = e.threeScene, this.renderer = e.renderer, this.camera = e.camera, this.gpuAcceleratedSort = e.gpuAcceleratedSort || false, void 0 !== e.integerBasedSort && null !== e.integerBasedSort || (e.integerBasedSort = true), this.integerBasedSort = e.integerBasedSort, void 0 !== e.sharedMemoryForWorkers && null !== e.sharedMemoryForWorkers || (e.sharedMemoryForWorkers = true), this.sharedMemoryForWorkers = false, this.dynamicScene = !!e.dynamicScene, this.antialiased = e.antialiased || false, this.kernel2DSize = void 0 === e.kernel2DSize ? 0.3 : e.kernel2DSize, this.renderMode = e.renderMode || Y.Always, this.sceneRevealMode = e.sceneRevealMode || $.Default, this.focalAdjustment = e.focalAdjustment || 1, this.maxScreenSpaceSplatSize = e.maxScreenSpaceSplatSize || 1024, this.logLevel = e.logLevel || j.None, this.sphericalHarmonicsDegree = e.sphericalHarmonicsDegree || 0, this.enableOptionalEffects = e.enableOptionalEffects || false, void 0 !== e.enableSIMDInSort && null !== e.enableSIMDInSort || (e.enableSIMDInSort = true), this.enableSIMDInSort = e.enableSIMDInSort, void 0 !== e.inMemoryCompressionLevel && null !== e.inMemoryCompressionLevel || (e.inMemoryCompressionLevel = 0), this.inMemoryCompressionLevel = e.inMemoryCompressionLevel, void 0 !== e.optimizeSplatData && null !== e.optimizeSplatData || (e.optimizeSplatData = true), this.optimizeSplatData = e.optimizeSplatData, void 0 !== e.freeIntermediateSplatData && null !== e.freeIntermediateSplatData || (e.freeIntermediateSplatData = false), this.freeIntermediateSplatData = e.freeIntermediateSplatData, je()) {
43260
43642
  const e2 = Ye();
43261
43643
  e2.major < 17 && (this.enableSIMDInSort = false), e2.major < 16 && (this.sharedMemoryForWorkers = false);
43262
43644
  }
@@ -43265,7 +43647,7 @@ const _Viewer = class _Viewer {
43265
43647
  this.splatSortDistanceMapPrecision = ze(this.splatSortDistanceMapPrecision, 10, t), this.onSplatMeshChangedCallback = null, this.createSplatMesh(), this.controls = null, this.perspectiveControls = null, this.orthographicControls = null, this.orthographicCamera = null, this.perspectiveCamera = null, this.showMeshCursor = false, this.showControlPlane = false, this.showInfo = false, this.sceneHelper = null, this.sortWorker = null, this.sortRunning = false, this.splatRenderCount = 0, this.splatSortCount = 0, this.lastSplatSortCount = 0, this.sortWorkerIndexesToSort = null, this.sortWorkerSortedIndexes = null, this.sortWorkerPrecomputedDistances = null, this.sortWorkerTransforms = null, this.preSortMessages = [], this.runAfterNextSort = [], this.selfDrivenModeRunning = false, this.splatRenderReady = false, this.raycaster = new wi(), this.infoPanel = null, this.startInOrthographicMode = false, this.currentFPS = 0, this.lastSortTime = 0, this.consecutiveRenderFrames = 0, this.previousCameraTarget = new Vector3(), this.nextCameraTarget = new Vector3(), this.mousePosition = new Vector2(), this.mouseDownPosition = new Vector2(), this.mouseDownTime = null, this.resizeObserver = null, this.mouseMoveListener = null, this.mouseDownListener = null, this.mouseUpListener = null, this.keyDownListener = null, this.sortPromise = null, this.sortPromiseResolver = null, this.splatSceneDownloadControllers = [], this.splatSceneDownloadPromises = {}, this.splatSceneDownloadAndBuildPromise = null, this.splatSceneRemovalPromise = null, this.loadingSpinner = new Ei(null, this.rootElement || document.body), this.loadingSpinner.hide(), this.loadingProgressBar = new Mi(this.rootElement || document.body), this.loadingProgressBar.hide(), this.usingExternalCamera = !(!this.dropInMode && !this.camera), this.usingExternalRenderer = !(!this.dropInMode && !this.renderer), this.initialized = false, this.disposing = false, this.disposed = false, this.disposePromise = null, this.lastTime = 0, this.gaussianSplatCount = 0, this.totalFrames = 0, this.frame = 0, this.avatarMesh = null, this.skinModel = null, this.boneRoot = null, this.baseMesh = null, this.setSkinAttibutes = false, this.dropInMode || this.init();
43266
43648
  }
43267
43649
  createSplatMesh() {
43268
- this.splatMesh = new ws(this.splatRenderMode, this.dynamicScene, this.enableOptionalEffects, this.halfPrecisionCovariancesOnGPU, this.devicePixelRatio, this.gpuAcceleratedSort, this.integerBasedSort, this.antialiased, this.maxScreenSpaceSplatSize, this.logLevel, this.sphericalHarmonicsDegree, this.sceneFadeInRateMultiplier, this.kernel2DSize), this.splatMesh.irisOcclusionConfig = this.irisOcclusionConfig, this.splatMesh.frustumCulled = false, this.onSplatMeshChangedCallback && this.onSplatMeshChangedCallback();
43650
+ this.splatMesh = new ws(this.splatRenderMode, this.dynamicScene, this.enableOptionalEffects, this.halfPrecisionCovariancesOnGPU, this.devicePixelRatio, this.gpuAcceleratedSort, this.integerBasedSort, this.antialiased, this.maxScreenSpaceSplatSize, this.logLevel, this.sphericalHarmonicsDegree, this.sceneFadeInRateMultiplier, this.kernel2DSize, this.irisOcclusionConfig), this.splatMesh.frustumCulled = false, this.onSplatMeshChangedCallback && this.onSplatMeshChangedCallback();
43269
43651
  }
43270
43652
  init() {
43271
43653
  this.initialized || (this.rootElement || (this.usingExternalRenderer ? this.rootElement = this.renderer.domElement || document.body : (this.rootElement = document.createElement("div"), this.rootElement.style.width = "100%", this.rootElement.style.height = "100%", this.rootElement.style.position = "absolute", document.body.appendChild(this.rootElement))), this.setupCamera(), this.setupRenderer(), this.setupEventHandlers(), this.threeScene = this.threeScene || new Scene(), this.sceneHelper = new Ri(this.threeScene), this.sceneHelper.setupMeshCursor(), this.sceneHelper.setupFocusMarker(), this.sceneHelper.setupControlPlane(), this.loadingProgressBar.setContainer(this.rootElement), this.loadingSpinner.setContainer(this.rootElement), this.initialized = true);
@@ -43667,19 +44049,26 @@ class GaussianSplatRenderer {
43667
44049
  Fi.debug("Found model folder in ZIP", { fileName: c }), Fi.debug("Creating GaussianSplatRenderer instance");
43668
44050
  const h = new GaussianSplatRenderer(e, l), d = Rt.set(Pi ?? 0, ki ?? 0, _i ?? 1), u = new Vector3(Li ?? 0, Ui ?? 0, Hi ?? 0);
43669
44051
  Fi.debug("Camera setup", { position: { x: d.x, y: d.y, z: d.z }, rotation: { x: u.x, y: u.y, z: u.z } });
43670
- let p, m = 16777215;
44052
+ let p = 16777215;
43671
44053
  try {
43672
44054
  if (Oi) {
43673
44055
  const e2 = parseInt(Oi, 16);
43674
- isNaN(e2) ? Fi.warn("Invalid backgroundColor in config, using default", { value: Oi }) : m = e2;
44056
+ isNaN(e2) ? Fi.warn("Invalid backgroundColor in config, using default", { value: Oi }) : p = e2;
43675
44057
  }
43676
- i?.backgroundColor && (h.isHexColorStrict(i.backgroundColor) ? m = parseInt(i.backgroundColor, 16) : Fi.warn("Invalid backgroundColor option, using config value", { value: i.backgroundColor }));
44058
+ i?.backgroundColor && (h.isHexColorStrict(i.backgroundColor) ? p = parseInt(i.backgroundColor, 16) : Fi.warn("Invalid backgroundColor option, using config value", { value: i.backgroundColor }));
43677
44059
  } catch (e2) {
43678
44060
  Fi.warn("Error parsing backgroundColor, using default", e2);
43679
44061
  }
43680
- Fi.debug("Background color set", { backgroundColor: m.toString(16) }), h.getChatState = i?.getChatState, h.getExpressionData = i?.getExpressionData, Fi.debug("Creating Viewer instance");
44062
+ Fi.debug("Background color set", { backgroundColor: p.toString(16) }), h.getChatState = i?.getChatState, h.getExpressionData = i?.getExpressionData, Fi.debug("Checking for iris_occlusion.json");
44063
+ let m, g = null;
44064
+ try {
44065
+ g = await h._loadJsonFromZip(c + "/iris_occlusion.json"), g ? (Fi.info("Iris occlusion configuration loaded", { rightIrisRanges: g.right_iris?.length ?? 0, leftIrisRanges: g.left_iris?.length ?? 0 }), h.irisOcclusionConfig = g) : Fi.debug("No iris_occlusion.json found, iris occlusion will be disabled");
44066
+ } catch (e2) {
44067
+ Fi.warn("Failed to load iris_occlusion.json, continuing without it", { error: e2.message }), h.irisOcclusionConfig = null;
44068
+ }
44069
+ Fi.debug("Creating Viewer instance");
43681
44070
  try {
43682
- h.viewer = new Viewer({ rootElement: e, threejsCanvas: h._canvas, cameraUp: [0, 1, 0], initialCameraPosition: [d.x, d.y, d.z], initialCameraRotation: [u.x, u.y, u.z], sphericalHarmonicsDegree: 0, backgroundColor: m });
44071
+ h.viewer = new Viewer({ rootElement: e, threejsCanvas: h._canvas, cameraUp: [0, 1, 0], initialCameraPosition: [d.x, d.y, d.z], initialCameraRotation: [u.x, u.y, u.z], sphericalHarmonicsDegree: 0, backgroundColor: p, sceneRevealMode: $.Default, sceneFadeInRateMultiplier: 3, irisOcclusionConfig: g });
43683
44072
  } catch (e2) {
43684
44073
  throw new Te(`Failed to create Viewer instance: ${e2.message}`, e2);
43685
44074
  }
@@ -43696,17 +44085,10 @@ class GaussianSplatRenderer {
43696
44085
  }
43697
44086
  Fi.debug("Loading offset PLY file");
43698
44087
  try {
43699
- p = await h.unpackFileAsBlob(c + "/offset.ply");
44088
+ m = await h.unpackFileAsBlob(c + "/offset.ply");
43700
44089
  } catch (e2) {
43701
44090
  throw new we(`Failed to load offset.ply: ${e2.message}`, c + "/offset.ply", e2);
43702
44091
  }
43703
- Fi.debug("Checking for iris_occlusion.json");
43704
- let g = null;
43705
- try {
43706
- g = await h._loadJsonFromZip(c + "/iris_occlusion.json"), g ? (Fi.info("Iris occlusion configuration loaded", { rightIrisRanges: g.right_iris?.length ?? 0, leftIrisRanges: g.left_iris?.length ?? 0 }), h.irisOcclusionConfig = g, h.viewer.irisOcclusionConfig = g) : Fi.debug("No iris_occlusion.json found, iris occlusion will be disabled");
43707
- } catch (e2) {
43708
- Fi.warn("Failed to load iris_occlusion.json, continuing without it", { error: e2.message }), h.irisOcclusionConfig = null;
43709
- }
43710
44092
  if (i.loadProgress) try {
43711
44093
  i.loadProgress(0.3);
43712
44094
  } catch (e2) {
@@ -43714,7 +44096,7 @@ class GaussianSplatRenderer {
43714
44096
  }
43715
44097
  Fi.debug("Adding splat scene");
43716
44098
  try {
43717
- await h.viewer.addSplatScene(p, { progressiveLoad: true, sharedMemoryForWorkers: false, showLoadingUI: false, format: J.Ply });
44099
+ await h.viewer.addSplatScene(m, { progressiveLoad: true, sharedMemoryForWorkers: false, showLoadingUI: false, format: J.Ply });
43718
44100
  } catch (e2) {
43719
44101
  throw new Te(`Failed to add splat scene: ${e2.message}`, e2);
43720
44102
  }