@zantopia/zephyr-widget 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,719 @@
1
+ /**
2
+ * 🦊 Zephyr Widget — Embeddable AI Navigation Assistant
3
+ *
4
+ * Usage:
5
+ * <script src="https://your-zephyr-server/sdk/zephyr-widget.js"></script>
6
+ * <script>
7
+ * ZephyrWidget.init({
8
+ * server: 'https://your-zephyr-server',
9
+ * persona: 'minimal',
10
+ * theme: 'dark',
11
+ * position: 'bottom-right',
12
+ * language: 'fr',
13
+ * });
14
+ * </script>
15
+ *
16
+ * Or via npm:
17
+ * import { ZephyrWidget } from '@zephyr/widget';
18
+ * ZephyrWidget.init({ server: '...', persona: 'spirit' });
19
+ */
20
+
21
+ (function (root, factory) {
22
+ if (typeof module !== "undefined" && module.exports) {
23
+ module.exports = factory();
24
+ } else {
25
+ root.ZephyrWidget = factory();
26
+ }
27
+ })(typeof globalThis !== "undefined" ? globalThis : this, function () {
28
+ "use strict";
29
+
30
+ // ─── Defaults ───────────────────────────────────────────────
31
+ const DEFAULTS = {
32
+ server: "",
33
+ apiKey: "",
34
+ persona: "minimal", // "mascot" | "spirit" | "minimal" | "futuristic" | custom URL
35
+ theme: "dark", // "dark" | "light" | "auto"
36
+ position: "bottom-right", // "bottom-right" | "bottom-left" | "top-right" | "top-left"
37
+ size: "md", // "sm" | "md" | "lg"
38
+ language: "fr", // "fr" | "en"
39
+ greeting: null, // Custom greeting — null = use server default
40
+ placeholder: null, // Input placeholder
41
+ accentColor: "#ff6b35",
42
+ zIndex: 99999,
43
+ draggable: false,
44
+ open: false, // Start open
45
+ showBadge: true,
46
+ badgeCount: 0,
47
+ features: ["chat", "guide", "search"], // Enabled features
48
+ appContext: null, // App context object: { name, description, features, faq, terminology, workflows, custom }
49
+ onReady: null,
50
+ onMessage: null,
51
+ onError: null,
52
+ onToggle: null,
53
+ containerSelector: null, // Mount inside a specific element (null = body)
54
+ customCSS: "", // Additional CSS to inject
55
+ };
56
+
57
+ // ─── Zephyr Logo (base64) ──────────────────────────────
58
+ const ZEPHYR_LOGO = "data:image/png;base64,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";
59
+
60
+ // ─── Persona SVG templates ─────────────────────────────────
61
+ const PERSONAS = {
62
+ mascot: {
63
+ svg: (accent) => `
64
+ <svg viewBox="0 0 64 64" width="100%" height="100%">
65
+ <circle cx="32" cy="32" r="30" fill="${accent}" opacity="0.12"/>
66
+ <circle cx="32" cy="32" r="28" fill="none" stroke="${accent}" stroke-width="2" opacity="0.5"/>
67
+ <image href="${ZEPHYR_LOGO}" x="6" y="6" width="52" height="52" style="border-radius:50%"/>
68
+ </svg>`,
69
+ },
70
+ spirit: {
71
+ svg: (accent) => `
72
+ <svg viewBox="0 0 64 64" width="100%" height="100%">
73
+ <circle cx="32" cy="32" r="31" fill="${accent}" opacity="0.1"/>
74
+ <circle cx="32" cy="32" r="27" fill="${accent}" opacity="0.06"/>
75
+ <circle cx="32" cy="32" r="23" fill="${accent}" opacity="0.03"/>
76
+ <circle cx="32" cy="32" r="26" fill="none" stroke="${accent}" stroke-width="1" opacity="0.3"/>
77
+ <image href="${ZEPHYR_LOGO}" x="8" y="8" width="48" height="48" opacity="0.9"/>
78
+ </svg>`,
79
+ },
80
+ minimal: {
81
+ svg: (accent) => `
82
+ <svg viewBox="0 0 64 64" width="100%" height="100%">
83
+ <rect x="6" y="6" width="52" height="52" rx="14" fill="${accent}" opacity="0.08"/>
84
+ <rect x="6" y="6" width="52" height="52" rx="14" fill="none" stroke="${accent}" stroke-width="1.5" opacity="0.25"/>
85
+ <image href="${ZEPHYR_LOGO}" x="8" y="8" width="48" height="48"/>
86
+ </svg>`,
87
+ },
88
+ futuristic: {
89
+ svg: (accent) => `
90
+ <svg viewBox="0 0 64 64" width="100%" height="100%">
91
+ <rect x="4" y="4" width="56" height="56" rx="12" fill="none" stroke="${accent}" stroke-width="1.5" opacity="0.6"/>
92
+ <rect x="8" y="8" width="48" height="48" rx="10" fill="${accent}" opacity="0.06"/>
93
+ <circle cx="32" cy="6" r="2.5" fill="${accent}" opacity="0.8"/>
94
+ <circle cx="32" cy="58" r="2" fill="${accent}" opacity="0.4"/>
95
+ <circle cx="6" cy="32" r="2" fill="${accent}" opacity="0.4"/>
96
+ <circle cx="58" cy="32" r="2" fill="${accent}" opacity="0.4"/>
97
+ <image href="${ZEPHYR_LOGO}" x="10" y="10" width="44" height="44"/>
98
+ </svg>`,
99
+ },
100
+ };
101
+
102
+ // ─── Expression mutations ──────────────────────────────────
103
+ const EXPRESSIONS = {
104
+ neutral: { eyeRx: 3, eyeRy: 3.5, mouth: "M28,42 Q32,44 36,42" },
105
+ happy: { eyeRx: 0, eyeRy: 0, mouth: "M26,42 Q32,48 38,42", eyeArc: true },
106
+ surprised: { eyeRx: 4, eyeRy: 5, mouth: "M28,43 Q32,47 36,43" },
107
+ thinking: { eyeRx: 3, eyeRy: 2.5, mouth: "M28,42 L36,42", dots: true },
108
+ helping: { eyeRx: 3, eyeRy: 3.5, mouth: "M26,42 Q32,46 38,42" },
109
+ speaking: { eyeRx: 3, eyeRy: 3.5, mouth: "M28,41 Q32,46 36,41" },
110
+ wink: { eyeRx: 3, eyeRy: 3.5, mouth: "M26,42 Q32,47 38,42", winkLeft: true },
111
+ };
112
+
113
+ // ─── Theme definitions ─────────────────────────────────────
114
+ const THEMES = {
115
+ dark: {
116
+ "--kw-bg": "#0f0f1a",
117
+ "--kw-surface": "#1a1a2e",
118
+ "--kw-text": "#e8e8f0",
119
+ "--kw-muted": "#6b6b80",
120
+ "--kw-border": "#2a2a40",
121
+ "--kw-user-bubble": "var(--kw-accent, #ff6b35)",
122
+ "--kw-user-text": "#ffffff",
123
+ },
124
+ light: {
125
+ "--kw-bg": "#f5f5fa",
126
+ "--kw-surface": "#ffffff",
127
+ "--kw-text": "#1a1a2e",
128
+ "--kw-muted": "#8888a0",
129
+ "--kw-border": "#e0e0ee",
130
+ "--kw-user-bubble": "var(--kw-accent, #e85d26)",
131
+ "--kw-user-text": "#ffffff",
132
+ },
133
+ };
134
+
135
+ // ─── CSS ───────────────────────────────────────────────────
136
+ const CSS = `
137
+ .kw-root {
138
+ --kw-accent: #ff6b35;
139
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif;
140
+ font-size: 14px;
141
+ line-height: 1.5;
142
+ box-sizing: border-box;
143
+ }
144
+ .kw-root *, .kw-root *::before, .kw-root *::after {
145
+ box-sizing: inherit;
146
+ }
147
+
148
+ /* Trigger button */
149
+ .kw-trigger {
150
+ position: fixed;
151
+ width: 56px; height: 56px;
152
+ border-radius: 50%;
153
+ border: 2px solid var(--kw-accent);
154
+ background: var(--kw-surface);
155
+ cursor: pointer;
156
+ z-index: var(--kw-z);
157
+ display: flex; align-items: center; justify-content: center;
158
+ box-shadow: 0 4px 20px rgba(0,0,0,0.25);
159
+ transition: transform 0.25s ease, box-shadow 0.25s ease;
160
+ padding: 6px;
161
+ animation: kw-float 3s ease-in-out infinite;
162
+ }
163
+ .kw-trigger:hover {
164
+ transform: scale(1.08);
165
+ box-shadow: 0 6px 28px rgba(0,0,0,0.35);
166
+ }
167
+ .kw-trigger.kw-sm { width: 44px; height: 44px; }
168
+ .kw-trigger.kw-lg { width: 68px; height: 68px; }
169
+ .kw-trigger.kw-open { animation: none; }
170
+
171
+ /* Badge */
172
+ .kw-badge {
173
+ position: absolute; top: -4px; right: -4px;
174
+ background: #ff4757; color: #fff; font-size: 10px; font-weight: 700;
175
+ min-width: 18px; height: 18px; border-radius: 9px;
176
+ display: flex; align-items: center; justify-content: center;
177
+ padding: 0 4px;
178
+ }
179
+ .kw-badge:empty { display: none; }
180
+
181
+ /* Panel */
182
+ .kw-panel {
183
+ position: fixed;
184
+ width: 380px; max-width: calc(100vw - 24px);
185
+ height: 520px; max-height: calc(100vh - 100px);
186
+ border-radius: 16px;
187
+ background: var(--kw-bg);
188
+ border: 1px solid var(--kw-border);
189
+ box-shadow: 0 12px 40px rgba(0,0,0,0.3);
190
+ display: flex; flex-direction: column;
191
+ overflow: hidden;
192
+ z-index: var(--kw-z);
193
+ opacity: 0; pointer-events: none;
194
+ transform: translateY(12px) scale(0.95);
195
+ transition: opacity 0.25s ease, transform 0.25s ease;
196
+ }
197
+ .kw-panel.kw-visible {
198
+ opacity: 1; pointer-events: auto;
199
+ transform: translateY(0) scale(1);
200
+ }
201
+
202
+ /* Header */
203
+ .kw-header {
204
+ display: flex; align-items: center; gap: 10px;
205
+ padding: 12px 16px;
206
+ background: var(--kw-surface);
207
+ border-bottom: 1px solid var(--kw-border);
208
+ }
209
+ .kw-header-avatar { width: 36px; height: 36px; flex-shrink: 0; }
210
+ .kw-header-info { flex: 1; min-width: 0; }
211
+ .kw-header-title { font-weight: 600; color: var(--kw-accent); font-size: 15px; }
212
+ .kw-header-sub { font-size: 11px; color: var(--kw-muted); }
213
+ .kw-close {
214
+ background: none; border: none; color: var(--kw-muted);
215
+ font-size: 20px; cursor: pointer; padding: 4px 8px;
216
+ border-radius: 6px; transition: background 0.2s;
217
+ }
218
+ .kw-close:hover { background: var(--kw-border); }
219
+
220
+ /* Messages */
221
+ .kw-messages {
222
+ flex: 1; overflow-y: auto; padding: 12px;
223
+ display: flex; flex-direction: column; gap: 8px;
224
+ }
225
+ .kw-msg {
226
+ max-width: 82%; padding: 10px 14px;
227
+ border-radius: 14px; font-size: 13px;
228
+ animation: kw-slide-up 0.2s ease-out;
229
+ word-wrap: break-word;
230
+ }
231
+ .kw-msg.kw-user {
232
+ background: var(--kw-user-bubble); color: var(--kw-user-text);
233
+ align-self: flex-end; border-bottom-right-radius: 4px;
234
+ }
235
+ .kw-msg.kw-bot {
236
+ background: var(--kw-surface); color: var(--kw-text);
237
+ border: 1px solid var(--kw-border);
238
+ align-self: flex-start; border-bottom-left-radius: 4px;
239
+ }
240
+ .kw-msg.kw-system {
241
+ align-self: center; font-size: 11px; color: var(--kw-muted);
242
+ font-style: italic; max-width: 90%;
243
+ }
244
+ .kw-msg code {
245
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
246
+ font-size: 12px; background: rgba(0,0,0,0.15);
247
+ padding: 1px 4px; border-radius: 3px;
248
+ }
249
+ .kw-msg pre {
250
+ background: rgba(0,0,0,0.2); padding: 8px; border-radius: 6px;
251
+ overflow-x: auto; margin: 6px 0; font-size: 12px;
252
+ }
253
+
254
+ /* Typing indicator */
255
+ .kw-typing {
256
+ display: flex; align-items: center; gap: 4px; padding: 8px 14px;
257
+ align-self: flex-start;
258
+ }
259
+ .kw-typing span {
260
+ width: 6px; height: 6px; border-radius: 50%;
261
+ background: var(--kw-accent); animation: kw-bounce 1.2s infinite;
262
+ }
263
+ .kw-typing span:nth-child(2) { animation-delay: 0.15s; }
264
+ .kw-typing span:nth-child(3) { animation-delay: 0.3s; }
265
+
266
+ /* Suggestions */
267
+ .kw-suggestions {
268
+ display: flex; flex-wrap: wrap; gap: 6px;
269
+ padding: 6px 12px;
270
+ }
271
+ .kw-suggestion {
272
+ font-size: 11px; padding: 5px 10px; border-radius: 12px;
273
+ background: var(--kw-surface); border: 1px solid var(--kw-border);
274
+ color: var(--kw-text); cursor: pointer; transition: all 0.15s;
275
+ }
276
+ .kw-suggestion:hover {
277
+ border-color: var(--kw-accent); color: var(--kw-accent);
278
+ }
279
+
280
+ /* Input */
281
+ .kw-input-wrap {
282
+ display: flex; align-items: center; gap: 8px;
283
+ padding: 10px 12px; border-top: 1px solid var(--kw-border);
284
+ background: var(--kw-surface);
285
+ }
286
+ .kw-input {
287
+ flex: 1; border: 1px solid var(--kw-border); border-radius: 10px;
288
+ padding: 8px 12px; font-size: 13px; resize: none; outline: none;
289
+ background: var(--kw-bg); color: var(--kw-text);
290
+ max-height: 80px; min-height: 36px; font-family: inherit;
291
+ transition: border-color 0.2s;
292
+ }
293
+ .kw-input:focus { border-color: var(--kw-accent); }
294
+ .kw-send {
295
+ background: var(--kw-accent); color: #fff; border: none;
296
+ width: 36px; height: 36px; border-radius: 50%;
297
+ font-size: 16px; cursor: pointer; transition: opacity 0.2s;
298
+ display: flex; align-items: center; justify-content: center;
299
+ }
300
+ .kw-send:disabled { opacity: 0.4; cursor: not-allowed; }
301
+
302
+ /* Positions */
303
+ .kw-pos-br .kw-trigger { bottom: 20px; right: 20px; }
304
+ .kw-pos-br .kw-panel { bottom: 86px; right: 20px; }
305
+ .kw-pos-bl .kw-trigger { bottom: 20px; left: 20px; }
306
+ .kw-pos-bl .kw-panel { bottom: 86px; left: 20px; }
307
+ .kw-pos-tr .kw-trigger { top: 20px; right: 20px; }
308
+ .kw-pos-tr .kw-panel { top: 86px; right: 20px; }
309
+ .kw-pos-tl .kw-trigger { top: 20px; left: 20px; }
310
+ .kw-pos-tl .kw-panel { top: 86px; left: 20px; }
311
+
312
+ /* Inline (no trigger) */
313
+ .kw-inline .kw-trigger { display: none; }
314
+ .kw-inline .kw-panel {
315
+ position: relative; opacity: 1; pointer-events: auto;
316
+ transform: none; width: 100%; height: 100%;
317
+ max-width: none; max-height: none; border-radius: 12px;
318
+ }
319
+
320
+ /* Scrollbar */
321
+ .kw-messages::-webkit-scrollbar { width: 4px; }
322
+ .kw-messages::-webkit-scrollbar-thumb { background: var(--kw-border); border-radius: 2px; }
323
+
324
+ /* Animations */
325
+ @keyframes kw-float {
326
+ 0%, 100% { transform: translateY(0); }
327
+ 50% { transform: translateY(-6px); }
328
+ }
329
+ @keyframes kw-slide-up {
330
+ from { transform: translateY(8px); opacity: 0; }
331
+ to { transform: translateY(0); opacity: 1; }
332
+ }
333
+ @keyframes kw-bounce {
334
+ 0%, 80%, 100% { transform: scale(0); }
335
+ 40% { transform: scale(1); }
336
+ }
337
+
338
+ /* Mobile */
339
+ @media (max-width: 480px) {
340
+ .kw-panel {
341
+ width: calc(100vw - 16px);
342
+ height: calc(100vh - 80px);
343
+ bottom: 70px !important; right: 8px !important; left: 8px !important;
344
+ }
345
+ }
346
+ `;
347
+
348
+ // ─── Widget Class ──────────────────────────────────────────
349
+ class Widget {
350
+ constructor(options) {
351
+ this.config = { ...DEFAULTS, ...options };
352
+ this.ws = null;
353
+ this.sessionId = null;
354
+ this.messages = [];
355
+ this.expression = "neutral";
356
+ this.isOpen = this.config.open;
357
+ this.isLoading = false;
358
+ this.suggestions = [];
359
+ this.el = null;
360
+ this._mounted = false;
361
+ }
362
+
363
+ // ─── Public API ─────────────────────────────────────────
364
+
365
+ mount(container) {
366
+ if (this._mounted) return;
367
+ this._mounted = true;
368
+
369
+ const target = container
370
+ ? (typeof container === "string" ? document.querySelector(container) : container)
371
+ : document.body;
372
+
373
+ // Inject CSS
374
+ if (!document.getElementById("kw-styles")) {
375
+ const style = document.createElement("style");
376
+ style.id = "kw-styles";
377
+ style.textContent = CSS + (this.config.customCSS || "");
378
+ document.head.appendChild(style);
379
+ }
380
+
381
+ // Create root
382
+ this.el = document.createElement("div");
383
+ this.el.className = "kw-root " + this._posClass();
384
+ this._applyTheme();
385
+ this.el.style.setProperty("--kw-z", this.config.zIndex);
386
+ this.el.style.setProperty("--kw-accent", this.config.accentColor);
387
+
388
+ const isInline = this.config.containerSelector !== null;
389
+ if (isInline) this.el.classList.add("kw-inline");
390
+
391
+ this.el.innerHTML = this._renderHTML();
392
+ target.appendChild(this.el);
393
+
394
+ this._bindEvents();
395
+ this._connectWS();
396
+
397
+ if (this.config.onReady) this.config.onReady(this);
398
+ }
399
+
400
+ destroy() {
401
+ if (this.ws) this.ws.close();
402
+ if (this.el) this.el.remove();
403
+ this._mounted = false;
404
+ }
405
+
406
+ open() { this._toggle(true); }
407
+ close() { this._toggle(false); }
408
+ toggle() { this._toggle(!this.isOpen); }
409
+
410
+ send(text) {
411
+ if (!text.trim()) return;
412
+ this._addMessage("user", text);
413
+ const payload = { message: text, url: window.location.href };
414
+ // Include app context in first message as fallback if WS reconnected
415
+ if (this.config.appContext && this.messages.filter(m => m.role === "user").length <= 1) {
416
+ payload.app_context = this.config.appContext;
417
+ }
418
+ this._wsSend(payload);
419
+ }
420
+
421
+ /**
422
+ * Update the application context at runtime.
423
+ * Useful for SPAs that want to change context based on the current route.
424
+ */
425
+ setAppContext(ctx) {
426
+ this.config.appContext = ctx;
427
+ // Notify server of updated context
428
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
429
+ this.ws.send(JSON.stringify({ type: "app_context", app_context: ctx }));
430
+ }
431
+ }
432
+
433
+ setTheme(theme) {
434
+ this.config.theme = theme;
435
+ this._applyTheme();
436
+ }
437
+
438
+ setPersona(persona) {
439
+ this.config.persona = persona;
440
+ this._updateAvatar();
441
+ }
442
+
443
+ setAccentColor(color) {
444
+ this.config.accentColor = color;
445
+ this.el.style.setProperty("--kw-accent", color);
446
+ this._updateAvatar();
447
+ }
448
+
449
+ on(event, handler) {
450
+ this.config["on" + event.charAt(0).toUpperCase() + event.slice(1)] = handler;
451
+ }
452
+
453
+ // ─── Internal ───────────────────────────────────────────
454
+
455
+ _posClass() {
456
+ const map = {
457
+ "bottom-right": "kw-pos-br",
458
+ "bottom-left": "kw-pos-bl",
459
+ "top-right": "kw-pos-tr",
460
+ "top-left": "kw-pos-tl",
461
+ };
462
+ return map[this.config.position] || "kw-pos-br";
463
+ }
464
+
465
+ _applyTheme() {
466
+ let theme = this.config.theme;
467
+ if (theme === "auto") {
468
+ theme = window.matchMedia("(prefers-color-scheme: dark)").matches
469
+ ? "dark"
470
+ : "light";
471
+ }
472
+ const vars = THEMES[theme] || THEMES.dark;
473
+ for (const [key, val] of Object.entries(vars)) {
474
+ this.el.style.setProperty(key, val);
475
+ }
476
+ }
477
+
478
+ _avatarSVG() {
479
+ // Always use the actual logo image directly
480
+ return `<img src="${ZEPHYR_LOGO}" style="width:100%;height:100%;object-fit:contain;border-radius:50%;" alt="Zephyr"/>`;
481
+ }
482
+
483
+ _renderHTML() {
484
+ const sizeClass = "kw-" + (this.config.size || "md");
485
+ const ph = this.config.placeholder ||
486
+ (this.config.language === "fr" ? "Posez une question..." : "Ask a question...");
487
+
488
+ return `
489
+ <button class="kw-trigger ${sizeClass}" aria-label="Zephyr Assistant">
490
+ <div class="kw-trigger-avatar">${this._avatarSVG()}</div>
491
+ ${this.config.showBadge ? '<div class="kw-badge"></div>' : ""}
492
+ </button>
493
+ <div class="kw-panel ${this.isOpen ? "kw-visible" : ""}">
494
+ <div class="kw-header">
495
+ <div class="kw-header-avatar">${this._avatarSVG()}</div>
496
+ <div class="kw-header-info">
497
+ <div class="kw-header-title">Zephyr</div>
498
+ <div class="kw-header-sub">Assistant Navigation</div>
499
+ </div>
500
+ <button class="kw-close" aria-label="Fermer">×</button>
501
+ </div>
502
+ <div class="kw-messages"></div>
503
+ <div class="kw-suggestions"></div>
504
+ <div class="kw-input-wrap">
505
+ <textarea class="kw-input" rows="1" placeholder="${ph}"></textarea>
506
+ <button class="kw-send" aria-label="Envoyer">➤</button>
507
+ </div>
508
+ </div>
509
+ `;
510
+ }
511
+
512
+ _bindEvents() {
513
+ const trigger = this.el.querySelector(".kw-trigger");
514
+ const close = this.el.querySelector(".kw-close");
515
+ const input = this.el.querySelector(".kw-input");
516
+ const send = this.el.querySelector(".kw-send");
517
+
518
+ trigger.addEventListener("click", () => this.toggle());
519
+ close.addEventListener("click", () => this.close());
520
+
521
+ send.addEventListener("click", () => {
522
+ this.send(input.value);
523
+ input.value = "";
524
+ input.style.height = "auto";
525
+ });
526
+
527
+ input.addEventListener("keydown", (e) => {
528
+ if (e.key === "Enter" && !e.shiftKey) {
529
+ e.preventDefault();
530
+ this.send(input.value);
531
+ input.value = "";
532
+ input.style.height = "auto";
533
+ }
534
+ });
535
+
536
+ // Auto-resize textarea
537
+ input.addEventListener("input", () => {
538
+ input.style.height = "auto";
539
+ input.style.height = Math.min(input.scrollHeight, 80) + "px";
540
+ });
541
+ }
542
+
543
+ _toggle(open) {
544
+ this.isOpen = open;
545
+ const panel = this.el.querySelector(".kw-panel");
546
+ const trigger = this.el.querySelector(".kw-trigger");
547
+ panel.classList.toggle("kw-visible", open);
548
+ trigger.classList.toggle("kw-open", open);
549
+ if (this.config.onToggle) this.config.onToggle(open);
550
+
551
+ if (open) {
552
+ setTimeout(() => this.el.querySelector(".kw-input")?.focus(), 200);
553
+ }
554
+ }
555
+
556
+ _addMessage(role, text, expr) {
557
+ const cls = role === "user" ? "kw-user" : role === "system" ? "kw-system" : "kw-bot";
558
+ const container = this.el.querySelector(".kw-messages");
559
+ const msg = document.createElement("div");
560
+ msg.className = `kw-msg ${cls}`;
561
+ msg.innerHTML = this._renderMarkdown(text);
562
+ container.appendChild(msg);
563
+ container.scrollTop = container.scrollHeight;
564
+
565
+ this.messages.push({ role, text, expression: expr || this.expression });
566
+ if (this.config.onMessage) this.config.onMessage({ role, text, expression: expr });
567
+ }
568
+
569
+ _showTyping() {
570
+ this.isLoading = true;
571
+ const container = this.el.querySelector(".kw-messages");
572
+ let typing = container.querySelector(".kw-typing");
573
+ if (!typing) {
574
+ typing = document.createElement("div");
575
+ typing.className = "kw-typing";
576
+ typing.innerHTML = "<span></span><span></span><span></span>";
577
+ container.appendChild(typing);
578
+ container.scrollTop = container.scrollHeight;
579
+ }
580
+ }
581
+
582
+ _hideTyping() {
583
+ this.isLoading = false;
584
+ const typing = this.el.querySelector(".kw-typing");
585
+ if (typing) typing.remove();
586
+ }
587
+
588
+ _showSuggestions(items) {
589
+ this.suggestions = items || [];
590
+ const container = this.el.querySelector(".kw-suggestions");
591
+ container.innerHTML = items
592
+ .map((s) => `<button class="kw-suggestion">${this._esc(s)}</button>`)
593
+ .join("");
594
+ container.querySelectorAll(".kw-suggestion").forEach((btn) => {
595
+ btn.addEventListener("click", () => {
596
+ this.send(btn.textContent);
597
+ container.innerHTML = "";
598
+ });
599
+ });
600
+ }
601
+
602
+ _updateAvatar() {
603
+ const svg = this._avatarSVG();
604
+ const triggerAv = this.el.querySelector(".kw-trigger-avatar");
605
+ const headerAv = this.el.querySelector(".kw-header-avatar");
606
+ if (triggerAv) triggerAv.innerHTML = svg;
607
+ if (headerAv) headerAv.innerHTML = svg;
608
+ }
609
+
610
+ // ─── WebSocket ──────────────────────────────────────────
611
+
612
+ _connectWS() {
613
+ if (!this.config.server) {
614
+ console.warn("[ZephyrWidget] No server URL configured.");
615
+ return;
616
+ }
617
+
618
+ const base = this.config.server.replace(/\/$/, "");
619
+ const protocol = base.startsWith("https") ? "wss:" : "ws:";
620
+ const host = base.replace(/^https?:\/\//, "");
621
+ const sid = this.sessionId || "";
622
+ const url = `${protocol}//${host}/ws/chat?session_id=${sid}`;
623
+
624
+ this.ws = new WebSocket(url);
625
+
626
+ this.ws.onopen = () => {
627
+ // Send app context on connect so the server can cache it for the session
628
+ if (this.config.appContext) {
629
+ this.ws.send(JSON.stringify({
630
+ type: "app_context",
631
+ app_context: this.config.appContext,
632
+ }));
633
+ }
634
+ };
635
+
636
+ this.ws.onmessage = (event) => {
637
+ let data;
638
+ try { data = JSON.parse(event.data); } catch { return; }
639
+
640
+ if (data.type === "welcome") {
641
+ this.sessionId = data.session_id;
642
+ this.expression = data.expression || "happy";
643
+ const greeting = this.config.greeting || data.message;
644
+ this._addMessage("bot", greeting, data.expression);
645
+ } else if (data.type === "status") {
646
+ this.expression = data.expression || "thinking";
647
+ this._showTyping();
648
+ } else if (data.type === "response") {
649
+ this._hideTyping();
650
+ this.expression = data.expression || "neutral";
651
+ this._addMessage("bot", data.message, data.expression);
652
+ if (data.suggestions?.length) this._showSuggestions(data.suggestions);
653
+ } else if (data.type === "error") {
654
+ this._hideTyping();
655
+ this._addMessage("system", data.message, "surprised");
656
+ if (this.config.onError) this.config.onError(data);
657
+ }
658
+ };
659
+
660
+ this.ws.onclose = () => {
661
+ setTimeout(() => this._connectWS(), 3000);
662
+ };
663
+
664
+ this.ws.onerror = () => {
665
+ if (this.config.onError) this.config.onError({ message: "WebSocket error" });
666
+ };
667
+ }
668
+
669
+ _wsSend(data) {
670
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
671
+ this._showTyping();
672
+ this.ws.send(JSON.stringify(data));
673
+ }
674
+
675
+ // ─── Markdown (minimal) ─────────────────────────────────
676
+
677
+ _renderMarkdown(text) {
678
+ if (!text) return "";
679
+ let html = this._esc(text);
680
+ // Code blocks
681
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, "<pre><code>$2</code></pre>");
682
+ // Inline code
683
+ html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
684
+ // Bold
685
+ html = html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
686
+ // Italic
687
+ html = html.replace(/\*(.+?)\*/g, "<em>$1</em>");
688
+ // Lists
689
+ html = html.replace(/^[-•] (.+)$/gm, "<li>$1</li>");
690
+ html = html.replace(/(<li>.*<\/li>\n?)+/g, "<ul>$&</ul>");
691
+ // Line breaks
692
+ html = html.replace(/\n/g, "<br>");
693
+ return html;
694
+ }
695
+
696
+ _esc(text) {
697
+ const div = document.createElement("div");
698
+ div.textContent = text;
699
+ return div.innerHTML;
700
+ }
701
+ }
702
+
703
+ // ─── Static API ────────────────────────────────────────────
704
+ let instance = null;
705
+
706
+ return {
707
+ init(options) {
708
+ instance = new Widget(options);
709
+ instance.mount(options.containerSelector || null);
710
+ return instance;
711
+ },
712
+ getInstance() {
713
+ return instance;
714
+ },
715
+ Widget: Widget,
716
+ PERSONAS: PERSONAS,
717
+ THEMES: THEMES,
718
+ };
719
+ });