@oyehello/chat 0.2.0 → 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.
package/dist/index.js CHANGED
@@ -37,6 +37,15 @@ async function openSession(opts) {
37
37
  if (!res.ok) throw await asError(res);
38
38
  return await res.json();
39
39
  }
40
+ async function getWidgetConfig(opts) {
41
+ const res = await opts.fetchImpl(`${opts.baseUrl}/chat/widget-config?agentId=${encodeURIComponent(opts.agentId)}`, {
42
+ method: "GET",
43
+ credentials: "omit",
44
+ headers: { Authorization: `Bearer ${opts.apiKey}` }
45
+ });
46
+ if (!res.ok) throw await asError(res);
47
+ return await res.json();
48
+ }
40
49
  async function postMessage(opts) {
41
50
  const body = { text: opts.text };
42
51
  if (opts.attachments && opts.attachments.length > 0) body.attachments = opts.attachments;
@@ -160,7 +169,7 @@ function createChatClient(options) {
160
169
  seenTurnIds.add(ev.turnId);
161
170
  const m = streamingId ? messages.find((x) => x.id === streamingId) : void 0;
162
171
  if (m) {
163
- m.text = ev.text;
172
+ m.text = ev.text || m.text;
164
173
  m.streaming = false;
165
174
  m.turnId = ev.turnId;
166
175
  } else {
@@ -214,6 +223,8 @@ function createChatClient(options) {
214
223
  }
215
224
  } else if (ev.type === "agent_typing") {
216
225
  emit({ type: "agentTyping", active: !!ev.active });
226
+ } else if (ev.type === "suggestions") {
227
+ emit({ type: "suggestions", items: Array.isArray(ev.items) ? ev.items : [] });
217
228
  }
218
229
  };
219
230
  const openStream = async () => {
@@ -281,6 +292,9 @@ function createChatClient(options) {
281
292
  if (!session) throw new Error("call start() before getAttachmentUrl");
282
293
  return getAttachmentUrl({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, storageRef, fetchImpl });
283
294
  },
295
+ getWidgetConfig() {
296
+ return getWidgetConfig({ baseUrl: options.baseUrl, apiKey: options.apiKey, agentId: options.agentId, fetchImpl });
297
+ },
284
298
  async start() {
285
299
  if (state === "opening" || state === "ready") return;
286
300
  closed = false;
@@ -405,132 +419,372 @@ function renderMarkdown(text) {
405
419
  // src/styles.ts
406
420
  var STYLES = `
407
421
  :host {
408
- --oc-accent: #2563eb;
409
- --oc-fg: #111827;
410
- --oc-muted: #6b7280;
411
- --oc-surface: #ffffff;
412
- --oc-surface-2: #f3f4f6;
413
- --oc-border: #e5e7eb;
414
- --oc-radius: 16px;
415
- position: fixed; right: 20px; bottom: 20px; z-index: 2147483000;
416
- font-family: system-ui, -apple-system, sans-serif;
422
+ /* accent (consumer-overridable via --oc-accent) + its derivations */
423
+ --oc-ac: var(--oc-accent, #096DD9);
424
+ --oc-btn-rim: color-mix(in oklab, var(--oc-ac) 70%, black);
425
+ --oc-btn-edge: 0 6px 16px -10px color-mix(in oklab, var(--oc-ac) 55%, transparent);
426
+ --oc-danger: #D92D20;
427
+ --oc-ease: cubic-bezier(0.16,1,0.3,1);
428
+ --oc-bounce: cubic-bezier(0.18,0.89,0.32,1.13);
429
+ --oc-mono: 'Geist Mono', ui-monospace, monospace;
430
+ --oc-r-full: 999px;
431
+
432
+ /* light palette (base) */
433
+ --oc-bg: #FFFFFF; --oc-bg-sunk: #F6F8FB;
434
+ --oc-fg: #0A0D12; --oc-fg-muted: rgba(10,13,18,0.66); --oc-fg-dim: rgba(10,13,18,0.44);
435
+ --oc-border: rgba(9,13,20,0.10);
436
+ --oc-accent-text: color-mix(in oklab, var(--oc-ac) 88%, black);
437
+ --oc-tint-1: rgba(9,13,20,0.025); --oc-tint-2: rgba(9,13,20,0.04); --oc-tint-3: rgba(9,13,20,0.055);
438
+ --oc-field-bg: #FFFFFF; --oc-field-border: rgba(9,13,20,0.22);
439
+ --oc-glass-border: rgba(9,13,20,0.12);
440
+
441
+ position: fixed; right: 24px; bottom: 24px; z-index: 2147483000;
442
+ font-family: 'Geist', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
443
+ -webkit-font-smoothing: antialiased;
444
+ }
445
+ @media (prefers-color-scheme: dark) {
446
+ :host(:not([theme="light"])) {
447
+ --oc-bg: #000000; --oc-bg-sunk: #000000;
448
+ --oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
449
+ --oc-border: rgba(255,255,255,0.09);
450
+ --oc-accent-text: color-mix(in oklab, var(--oc-ac) 62%, white);
451
+ --oc-tint-1: rgba(255,255,255,0.03); --oc-tint-2: rgba(255,255,255,0.05); --oc-tint-3: rgba(255,255,255,0.07);
452
+ --oc-field-bg: rgba(255,255,255,0.045); --oc-field-border: rgba(255,255,255,0.20);
453
+ --oc-glass-border: rgba(255,255,255,0.12);
454
+ }
455
+ }
456
+ :host([theme="dark"]) {
457
+ --oc-bg: #000000; --oc-bg-sunk: #000000;
458
+ --oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
459
+ --oc-border: rgba(255,255,255,0.09);
460
+ --oc-accent-text: color-mix(in oklab, var(--oc-ac) 62%, white);
461
+ --oc-tint-1: rgba(255,255,255,0.03); --oc-tint-2: rgba(255,255,255,0.05); --oc-tint-3: rgba(255,255,255,0.07);
462
+ --oc-field-bg: rgba(255,255,255,0.045); --oc-field-border: rgba(255,255,255,0.20);
463
+ --oc-glass-border: rgba(255,255,255,0.12);
417
464
  }
418
465
  * { box-sizing: border-box; }
466
+ button { font-family: inherit; }
467
+
468
+ .oc { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; }
419
469
 
420
- /* Launcher */
470
+ /* \u2500\u2500 Launcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
421
471
  .launcher {
422
- width: 60px; height: 60px; border-radius: 50%; border: none; cursor: pointer;
423
- background: var(--oc-accent); color: #fff; box-shadow: 0 6px 24px rgba(0,0,0,.25);
424
- display: grid; place-items: center; transition: transform .15s ease, opacity .15s ease;
472
+ position: relative; flex: none; width: 46px; height: 46px; border-radius: var(--oc-r-full);
473
+ border: 1px solid var(--oc-btn-rim); background: var(--oc-ac);
474
+ box-shadow: 0 8px 22px -8px color-mix(in oklab, var(--oc-ac) 70%, transparent), 0 18px 48px -18px rgba(0,0,0,0.6);
475
+ color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer;
476
+ transition: transform .26s var(--oc-bounce), filter .2s var(--oc-ease); align-self: flex-end;
425
477
  }
426
478
  .launcher:hover { transform: scale(1.06); }
427
- .launcher svg { width: 28px; height: 28px; }
428
- .launcher .badge {
429
- position: absolute; top: -2px; right: -2px; min-width: 20px; height: 20px; padding: 0 5px;
430
- border-radius: 10px; background: #ef4444; color: #fff; font-size: 11px; font-weight: 700;
431
- display: grid; place-items: center; border: 2px solid var(--oc-surface);
479
+ .launcher:active { transform: scale(0.93); }
480
+ .ring { pointer-events: none; position: absolute; inset: -1px; border-radius: var(--oc-r-full); border: 2px solid var(--oc-ac); opacity: 0; }
481
+ .oc.ring:not(.open) .ring { animation: w-ring 2.4s ease-out 1.1s 3; }
482
+ .ic-chat, .ic-close { position: absolute; display: flex; align-items: center; justify-content: center; transition: all .3s var(--oc-bounce); }
483
+ .ic-chat { width: 28px; height: 28px; opacity: 1; transform: rotate(0) scale(1); }
484
+ .ic-close { opacity: 0; transform: rotate(90deg) scale(0.6); }
485
+ .oc.open .ic-chat { opacity: 0; transform: rotate(-90deg) scale(0.6); }
486
+ .oc.open .ic-close { opacity: 1; transform: rotate(0) scale(1); }
487
+ .badge {
488
+ position: absolute; top: -2px; right: -2px; min-width: 17px; height: 17px; padding: 0 5px;
489
+ border-radius: var(--oc-r-full); border: 2px solid var(--oc-bg-sunk); background: var(--oc-danger);
490
+ color: #fff; font-family: var(--oc-mono); font-size: 10px; font-weight: 700;
491
+ display: flex; align-items: center; justify-content: center; opacity: 0; transform: scale(0.4);
492
+ transition: all .26s var(--oc-bounce);
432
493
  }
494
+ .oc.unread:not(.open) .badge { opacity: 1; transform: scale(1); }
433
495
 
434
- /* Panel */
496
+ /* \u2500\u2500 Teaser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
497
+ .teaser {
498
+ position: absolute; right: 0; bottom: 60px; display: none; align-items: center; gap: 11px;
499
+ opacity: 0; transform: translateY(8px) scale(0.94); pointer-events: none;
500
+ transition: opacity .3s var(--oc-ease), transform .38s var(--oc-bounce);
501
+ }
502
+ .oc.has-teaser .teaser { display: flex; }
503
+ .oc.teaser:not(.open) .teaser { opacity: 1; transform: none; pointer-events: auto; }
504
+ .teaser-btn {
505
+ max-width: 250px; display: flex; align-items: center; gap: 10px; padding: 11px 15px;
506
+ border-radius: 16px 16px 5px 16px; border: 1px solid var(--oc-glass-border); background: var(--oc-bg);
507
+ box-shadow: 0 10px 30px -14px rgba(0,0,0,0.5); color: var(--oc-fg); font-size: 13.5px; line-height: 1.45;
508
+ text-align: left; cursor: pointer; transition: transform .2s var(--oc-ease);
509
+ }
510
+ .teaser-btn:hover { transform: translateY(-1px); }
511
+ .teaser-btn .logo { flex: none; width: 24px; height: 24px; border-radius: 6px; overflow: hidden; color: var(--oc-ac); }
512
+ .teaser-x {
513
+ flex: none; width: 22px; height: 22px; border-radius: var(--oc-r-full); border: 1px solid var(--oc-border);
514
+ background: var(--oc-bg); color: var(--oc-fg-dim); display: flex; align-items: center; justify-content: center; cursor: pointer;
515
+ }
516
+ .teaser-x:hover { color: var(--oc-fg); }
517
+
518
+ /* \u2500\u2500 Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
435
519
  .panel {
436
- position: absolute; right: 0; bottom: 74px; width: 384px; height: min(600px, calc(100vh - 120px));
437
- background: var(--oc-surface); color: var(--oc-fg); border: 1px solid var(--oc-border);
438
- border-radius: var(--oc-radius); box-shadow: 0 12px 48px rgba(0,0,0,.22);
439
- display: flex; flex-direction: column; overflow: hidden;
440
- transform-origin: bottom right; transition: transform .16s ease, opacity .16s ease;
441
- }
442
- .panel[hidden] { display: none; }
443
- .panel.enter { opacity: 0; transform: translateY(8px) scale(.98); }
520
+ position: relative; width: min(384px, calc(100vw - 40px)); height: min(560px, calc(100vh - 108px));
521
+ display: flex; flex-direction: column; border-radius: 20px; border: 1px solid var(--oc-glass-border);
522
+ background: var(--oc-bg); box-shadow: 0 12px 28px -12px rgba(0,0,0,0.28), 0 40px 90px -30px rgba(0,0,0,0.55);
523
+ overflow: hidden; transform-origin: bottom right;
524
+ opacity: 0; transform: translateY(16px) scale(0.94); pointer-events: none; visibility: hidden;
525
+ transition: opacity .24s var(--oc-ease), transform .34s var(--oc-bounce), visibility .34s, width .3s var(--oc-ease), height .3s var(--oc-ease);
526
+ }
527
+ .oc.open .panel { opacity: 1; transform: none; pointer-events: auto; visibility: visible; }
528
+ .oc.max .panel { width: min(720px, calc(100vw - 40px)); height: calc(100vh - 108px); }
444
529
 
445
- .hd { display: flex; align-items: center; gap: 8px; padding: 14px 16px; border-bottom: 1px solid var(--oc-border); }
446
- .hd .title { font-weight: 700; font-size: 15px; flex: 1; }
447
- .hd .close { background: none; border: none; cursor: pointer; color: var(--oc-muted); padding: 4px; border-radius: 6px; display: grid; place-items: center; }
448
- .hd .close:hover { background: var(--oc-surface-2); }
449
- .hd .close svg { width: 18px; height: 18px; }
530
+ .hd { position: relative; flex: none; display: flex; align-items: center; gap: 12px; padding: 13px 14px 12px; background: var(--oc-ac); color: #fff; overflow: hidden; }
531
+ .hd-grid { pointer-events: none; position: absolute; inset: 0; background-image: linear-gradient(90deg, rgba(255,255,255,0.11) 1px, transparent 1px), linear-gradient(180deg, rgba(255,255,255,0.11) 1px, transparent 1px); background-size: 26px 26px; }
532
+ .hd .logo { position: relative; flex: none; width: 32px; height: 32px; border-radius: 7px; overflow: hidden; color: rgba(255,255,255,0.22); }
533
+ .hd-txt { position: relative; flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
534
+ .hd-title { font-size: 14.5px; font-weight: 600; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
535
+ .hd-status { display: flex; align-items: center; gap: 6px; font-size: 11.5px; opacity: 0.9; }
536
+ .hd-status .dot { width: 6px; height: 6px; border-radius: var(--oc-r-full); background: #7BE3A6; flex: none; }
537
+ .hd-status .t { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
538
+ .hd-btn { position: relative; flex: none; width: 28px; height: 28px; border-radius: 8px; border: none; background: rgba(255,255,255,0.16); color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background .18s var(--oc-ease); }
539
+ .hd-btn:hover { background: rgba(255,255,255,0.28); }
540
+ .hd-btn .i { position: absolute; display: flex; transition: opacity .2s var(--oc-ease); }
541
+ .i-expand { opacity: 1; } .i-shrink { opacity: 0; }
542
+ .oc.max .i-expand { opacity: 0; } .oc.max .i-shrink { opacity: 1; }
450
543
 
451
- .status { padding: 6px 16px; font-size: 12px; color: var(--oc-muted); background: #f9fafb; border-bottom: 1px solid var(--oc-border); }
452
- .err { padding: 8px 16px; font-size: 12px; color: #b91c1c; background: #fef2f2; border-bottom: 1px solid #fecaca; display: flex; align-items: center; gap: 8px; }
453
- .err .retry { padding: 3px 10px; font-size: 11px; background: #b91c1c; color: #fff; border: none; border-radius: 6px; cursor: pointer; }
544
+ /* Inline connection error, shown inside the chat window (no banner) */
545
+ .oc-err { margin: auto; display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 24px 16px; text-align: center; }
546
+ .oc-err-t { font-size: 13px; line-height: 1.5; color: var(--oc-fg-muted); text-wrap: pretty; }
547
+ .oc-err-retry { padding: 7px 15px; font-size: 12.5px; font-weight: 500; border-radius: var(--oc-r-full); border: 1px solid var(--oc-border); background: var(--oc-tint-2); color: var(--oc-fg); cursor: pointer; transition: background .18s var(--oc-ease); }
548
+ .oc-err-retry:hover { background: var(--oc-tint-3); }
454
549
 
455
- /* Message list */
456
- .body { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 10px; scroll-behavior: smooth; }
457
- .b { max-width: 85%; padding: 9px 13px; border-radius: 14px; font-size: 14px; line-height: 1.5; word-break: break-word; }
458
- .b.user { align-self: flex-end; background: var(--oc-accent); color: #fff; border-bottom-right-radius: 4px; }
459
- .b.assistant { align-self: flex-start; background: var(--oc-surface-2); color: var(--oc-fg); border-bottom-left-radius: 4px; }
460
- .b.human { align-self: flex-start; background: #dbeafe; color: var(--oc-fg); border-bottom-left-radius: 4px; }
461
- .b.typing { font-style: italic; color: var(--oc-muted); }
550
+ /* \u2500\u2500 Message list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
551
+ .body { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; padding: 16px 16px 26px; background: var(--oc-bg); scroll-behavior: smooth; }
552
+ .turn { display: flex; flex-direction: column; gap: 6px; animation: w-in .42s var(--oc-ease) both; }
553
+ .turn.bot { align-items: flex-start; } .turn.user { align-items: flex-end; }
554
+ .b { font-size: 13.5px; line-height: 1.55; text-wrap: pretty; word-break: break-word; }
555
+ .turn.bot .b { max-width: 86%; padding: 13px 15px; border-radius: 20px; background: var(--oc-tint-2); color: var(--oc-fg); }
556
+ .turn.user .b { max-width: 82%; padding: 11px 15px; border-radius: var(--oc-r-full); background: var(--oc-ac); color: #fff; }
557
+ .turn.human .b { max-width: 86%; padding: 13px 15px; border-radius: 20px; background: color-mix(in oklab, var(--oc-ac) 16%, transparent); color: var(--oc-fg); }
558
+ .src { display: inline-flex; align-items: center; gap: 7px; padding: 5px 10px 5px 8px; border-radius: var(--oc-r-full); border: 1px solid var(--oc-border); background: var(--oc-tint-1); max-width: 100%; }
559
+ .src .t { min-width: 0; font-size: 11.5px; color: var(--oc-fg-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
560
+ .byline { display: flex; align-items: center; gap: 6px; padding: 0 4px; font-size: 11.5px; color: var(--oc-fg-dim); }
462
561
  .human-label { font-size: 11px; font-weight: 600; opacity: .7; margin-bottom: 2px; }
463
562
 
464
- /* Markdown inside a bubble */
465
- .md > :first-child { margin-top: 0; }
466
- .md > :last-child { margin-bottom: 0; }
467
- .md p { margin: 0 0 8px; }
468
- .md ul, .md ol { margin: 0 0 8px; padding-left: 20px; }
469
- .md li { margin: 2px 0; }
470
- .md a { color: inherit; text-decoration: underline; }
471
- .b.assistant .md a, .b.human .md a { color: var(--oc-accent); }
472
- .md code { font-family: ui-monospace, monospace; font-size: 12.5px; background: rgba(0,0,0,.07); padding: 1px 5px; border-radius: 5px; }
473
- .md pre { background: #0f172a; color: #e2e8f0; padding: 10px 12px; border-radius: 8px; overflow-x: auto; margin: 0 0 8px; }
474
- .md pre code { background: none; padding: 0; color: inherit; }
475
- .md blockquote { margin: 0 0 8px; padding-left: 10px; border-left: 3px solid var(--oc-border); color: var(--oc-muted); }
563
+ /* markdown inside a bot bubble */
564
+ .md > :first-child { margin-top: 0; } .md > :last-child { margin-bottom: 0; }
565
+ .md p { margin: 0 0 8px; } .md ul, .md ol { margin: 0 0 8px; padding-left: 20px; } .md li { margin: 2px 0; }
566
+ .md a { color: var(--oc-accent-text); text-decoration: underline; text-underline-offset: 2px; }
567
+ .md code { font-family: var(--oc-mono); font-size: 12.5px; background: var(--oc-tint-3); padding: 1px 5px; border-radius: 5px; }
568
+ .md pre { background: color-mix(in oklab, var(--oc-fg) 10%, transparent); padding: 10px 12px; border-radius: 8px; overflow-x: auto; margin: 0 0 8px; }
569
+ .md pre code { background: none; padding: 0; }
570
+ .md blockquote { margin: 0 0 8px; padding-left: 10px; border-left: 3px solid var(--oc-border); color: var(--oc-fg-muted); }
476
571
  .md h1, .md h2, .md h3, .md h4 { margin: 8px 0 6px; font-size: 15px; }
477
572
  .md table { border-collapse: collapse; font-size: 13px; margin: 0 0 8px; display: block; overflow-x: auto; }
478
573
  .md th, .md td { border: 1px solid var(--oc-border); padding: 4px 8px; }
479
- .md hr { border: none; border-top: 1px solid var(--oc-border); margin: 8px 0; }
480
-
481
- .src-row { align-self: flex-start; display: flex; flex-wrap: wrap; gap: 6px; max-width: 85%; }
482
- .src-chip { font-size: 11px; background: rgba(0,0,0,.06); color: #374151; border-radius: 6px; padding: 2px 8px; cursor: default; }
483
574
  .att-thumb { display: block; max-width: 180px; max-height: 180px; border-radius: 8px; margin-bottom: 6px; }
484
- .att-chip { display: inline-block; font-size: 12px; background: rgba(0,0,0,.08); border-radius: 6px; padding: 2px 8px; margin: 0 0 6px; }
485
- .b.user .att-chip { background: rgba(255,255,255,.25); }
575
+ .att-chip { display: inline-block; font-size: 12px; background: var(--oc-tint-3); border-radius: 6px; padding: 2px 8px; margin: 0 0 6px; }
576
+ .turn.user .att-chip { background: rgba(255,255,255,0.25); }
577
+
578
+ .typing { align-self: flex-start; display: flex; align-items: center; gap: 5px; padding: 13px 16px; border-radius: 20px; background: var(--oc-tint-2); animation: w-in .3s var(--oc-ease) both; }
579
+ .typing span { width: 6px; height: 6px; border-radius: var(--oc-r-full); background: var(--oc-fg-muted); animation: w-dot 1.2s ease-in-out infinite; }
580
+ .typing span:nth-child(2) { animation-delay: .15s; } .typing span:nth-child(3) { animation-delay: .3s; }
486
581
 
487
- /* New-messages pill */
488
- .pill {
489
- position: absolute; left: 50%; transform: translateX(-50%); bottom: 78px;
490
- background: var(--oc-accent); color: #fff; border: none; cursor: pointer;
491
- font-size: 12px; font-weight: 600; padding: 6px 12px; border-radius: 999px;
492
- box-shadow: 0 4px 14px rgba(0,0,0,.25); display: flex; align-items: center; gap: 6px;
493
- }
494
- .pill[hidden] { display: none; }
495
- .pill svg { width: 13px; height: 13px; }
582
+ .chips { display: flex; flex-wrap: wrap; gap: 7px; padding-top: 2px; animation: w-in .42s var(--oc-ease) both; animation-delay: .3s; }
583
+ .chip { padding: 8px 13px; border-radius: var(--oc-r-full); border: 1px solid color-mix(in oklab, var(--oc-ac) 34%, transparent); background: color-mix(in oklab, var(--oc-ac) 10%, transparent); color: var(--oc-accent-text); font-size: 12px; font-weight: 500; cursor: pointer; white-space: nowrap; transition: all .18s var(--oc-ease); }
584
+ .chip:hover { background: color-mix(in oklab, var(--oc-ac) 18%, transparent); }
496
585
 
497
- /* Composer */
498
- form { display: flex; gap: 8px; padding: 10px; border-top: 1px solid var(--oc-border); align-items: flex-end; }
499
- textarea {
500
- flex: 1; resize: none; max-height: 120px; padding: 9px 11px; border-radius: 10px;
501
- border: 1px solid #d1d5db; font: inherit; font-size: 14px; line-height: 1.4; outline: none;
502
- }
503
- textarea:focus { border-color: var(--oc-accent); }
504
- .send { padding: 0 14px; height: 38px; border-radius: 10px; border: none; background: var(--oc-accent); color: #fff; font-weight: 600; cursor: pointer; flex: 0 0 auto; }
505
- .send:disabled, textarea:disabled { opacity: .55; cursor: not-allowed; }
586
+ /* \u2500\u2500 Jump to latest \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
587
+ .jump-wrap { position: relative; flex: none; height: 0; z-index: 2; display: flex; justify-content: center; }
588
+ .jump { position: absolute; bottom: 8px; width: 34px; height: 34px; border-radius: var(--oc-r-full); border: 1px solid var(--oc-border); background: var(--oc-bg); box-shadow: 0 8px 20px -10px rgba(0,0,0,0.5); color: var(--oc-fg-muted); display: flex; align-items: center; justify-content: center; cursor: pointer; opacity: 0; transform: translateY(6px) scale(0.8); pointer-events: none; transition: all .22s var(--oc-ease); }
589
+ .oc.away .jump { opacity: 1; transform: none; pointer-events: auto; }
590
+ .jump:hover { color: var(--oc-fg); border-color: var(--oc-fg-dim); }
591
+
592
+ /* \u2500\u2500 Composer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
593
+ .composer { flex: none; display: flex; flex-direction: column; gap: 9px; padding: 12px 14px 13px; background: var(--oc-bg); }
594
+ .field { display: flex; flex-direction: column; gap: 11px; padding: 13px 14px 11px; border-radius: 18px; border: 1px solid var(--oc-field-border); background: var(--oc-field-bg); transition: border-color .18s var(--oc-ease); }
595
+ .oc.focus .field { border-color: var(--oc-ac); }
596
+ .input { width: 100%; border: none; background: transparent; color: var(--oc-fg); font-size: 13.5px; line-height: 1.4; outline: none; padding: 0; resize: none; max-height: 108px; font-family: inherit; }
597
+ .input::placeholder { color: var(--oc-fg-dim); }
598
+ .acts { display: flex; align-items: center; gap: 16px; }
599
+ .act { flex: none; display: flex; border: none; background: transparent; padding: 4px; margin: -4px; color: var(--oc-fg-dim); cursor: pointer; transition: color .18s var(--oc-ease); }
600
+ .act:hover { color: var(--oc-fg-muted); }
601
+ .oc.emoji .act.emoji-btn { color: var(--oc-accent-text); }
602
+ .send { flex: none; margin-left: auto; width: 34px; height: 34px; border-radius: var(--oc-r-full); border: 1px solid var(--oc-border); background: var(--oc-tint-3); color: var(--oc-fg-dim); box-shadow: none; display: flex; align-items: center; justify-content: center; cursor: default; transition: all .22s var(--oc-ease); }
603
+ .oc.ready .send { border-color: var(--oc-btn-rim); background: var(--oc-ac); color: #fff; box-shadow: var(--oc-btn-edge); cursor: pointer; }
604
+ .oc.ready .send:hover { filter: brightness(1.1); }
605
+ .send:active { transform: scale(0.92); }
606
+
607
+ .foot { position: relative; display: none; text-align: center; font-size: 11.5px; line-height: 1.5; color: var(--oc-fg-dim); }
608
+ .oc.has-privacy .foot { display: block; }
609
+ .foot .priv { color: var(--oc-fg-muted); text-decoration: underline; text-underline-offset: 2px; cursor: default; }
610
+ .foot .tip { position: absolute; left: 0; right: 0; bottom: 16px; padding-bottom: 8px; z-index: 7; display: flex; justify-content: flex-end; pointer-events: none; opacity: 0; transition: opacity .18s var(--oc-ease); }
611
+ .oc.max .foot .tip { right: 188px; }
612
+ .foot:hover .tip { opacity: 1; }
613
+ .foot .tip-card { max-width: 100%; width: 262px; padding: 13px 15px; border-radius: 14px; border: 1px solid var(--oc-border); background: var(--oc-bg); box-shadow: 0 8px 22px -12px rgba(0,0,0,0.32); text-align: left; font-size: 11.5px; line-height: 1.55; color: var(--oc-fg-muted); text-wrap: pretty; transform: translateY(6px); transition: transform .24s var(--oc-bounce); }
614
+ .foot:hover .tip-card { transform: none; }
615
+ .foot .tip-card a { color: var(--oc-fg); text-decoration: underline; text-underline-offset: 2px; }
616
+
617
+ /* \u2500\u2500 Emoji picker \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
618
+ .emoji-scrim { position: absolute; inset: 0; z-index: 3; display: none; }
619
+ .oc.emoji .emoji-scrim { display: block; }
620
+ .emoji-panel { position: absolute; left: 10px; right: 10px; bottom: 96px; z-index: 4; max-height: min(340px, 58%); display: flex; flex-direction: column; border-radius: 18px; border: 1px solid var(--oc-border); background: var(--oc-bg); box-shadow: 0 18px 46px -18px rgba(0,0,0,0.6); overflow: hidden; opacity: 0; transform: translateY(10px) scale(0.96); transform-origin: bottom center; pointer-events: none; visibility: hidden; transition: opacity .18s var(--oc-ease), transform .26s var(--oc-bounce), visibility .26s; }
621
+ .oc.emoji .emoji-panel { opacity: 1; transform: none; pointer-events: auto; visibility: visible; }
622
+ .emoji-search { flex: none; display: flex; align-items: center; gap: 11px; padding: 13px 16px; border-bottom: 1px solid var(--oc-border); color: var(--oc-fg-dim); }
623
+ .emoji-search input { flex: 1 1 auto; min-width: 0; border: none; background: transparent; color: var(--oc-fg); font-size: 14.5px; outline: none; font-family: inherit; }
624
+ .emoji-search input::placeholder { color: var(--oc-fg-dim); }
625
+ .emoji-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding: 12px 14px 16px; }
626
+ .emoji-grp { display: flex; flex-direction: column; gap: 6px; }
627
+ .emoji-grp + .emoji-grp { padding-top: 8px; }
628
+ .emoji-grp-name { font-family: var(--oc-mono); font-size: 9.5px; font-weight: 600; letter-spacing: 0.15em; text-transform: uppercase; color: var(--oc-fg-dim); }
629
+ .emoji-grid { display: grid; grid-template-columns: repeat(7, minmax(0,1fr)); gap: 2px; }
630
+ .emoji-grid button { display: flex; align-items: center; justify-content: center; height: 34px; border-radius: 8px; border: none; background: transparent; font-size: 20px; line-height: 1; cursor: pointer; transition: background .14s var(--oc-ease); }
631
+ .emoji-grid button:hover { background: var(--oc-tint-3); }
632
+ .emoji-empty { display: none; flex-direction: column; align-items: center; gap: 5px; padding: 26px 10px; text-align: center; }
633
+ .emoji-empty .h { font-size: 14px; color: var(--oc-fg); } .emoji-empty .s { font-size: 12.5px; color: var(--oc-fg-dim); }
506
634
 
507
635
  @media (max-width: 480px) {
508
636
  :host { right: 0; bottom: 0; }
509
- .panel { position: fixed; inset: 0; width: 100vw; height: 100dvh; border-radius: 0; bottom: 0; }
510
- .launcher { position: fixed; right: 16px; bottom: 16px; }
511
- .pill { bottom: 82px; }
637
+ .oc { padding: 0 16px 16px; }
638
+ .panel { position: fixed; inset: 0; width: 100vw; height: 100dvh; border-radius: 0; }
639
+ .oc.max .panel { width: 100vw; height: 100dvh; }
512
640
  }
513
641
  @media (prefers-reduced-motion: reduce) {
514
- .launcher, .panel { transition: none; }
515
- .body { scroll-behavior: auto; }
642
+ .launcher, .panel, .teaser, .turn, .jump, .emoji-panel { transition: none; animation: none; }
643
+ .ring { animation: none; } .body { scroll-behavior: auto; }
516
644
  }
645
+
646
+ @keyframes w-in { from { opacity: 0; transform: translateY(10px) scale(0.97); } to { opacity: 1; transform: none; } }
647
+ @keyframes w-dot { 0%,60%,100% { transform: translateY(0); opacity: .45; } 30% { transform: translateY(-4px); opacity: 1; } }
648
+ @keyframes w-ring { 0% { transform: scale(1); opacity: .5; } 70%,100% { transform: scale(1.7); opacity: 0; } }
517
649
  `;
518
650
 
651
+ // src/icons.ts
652
+ var logoMark = (rectFill) => `<svg viewBox="0 0 1032 1024" fill="none" style="display:block;width:100%;height:100%"><rect x="4" width="1024" height="1024" rx="150" fill="${rectFill}"></rect><path d="M430.924 831.644C270.203 859.983 138.14 760.606 111.329 608.551C84.5174 456.497 174.627 317.945 335.348 289.605C496.068 261.266 628.131 360.643 654.943 512.698C681.754 664.752 591.644 803.304 430.924 831.644ZM406.613 693.77C488.549 679.323 518.727 615.513 505.252 539.092C491.777 462.671 441.594 413.031 359.658 427.478C277.722 441.926 247.544 505.736 261.02 582.157C274.495 658.578 324.677 708.218 406.613 693.77ZM870.337 558.389L744.281 580.616C727.055 482.923 674.565 369.515 652.06 241.884L841.143 208.543C863.509 335.387 853.111 460.696 870.337 558.389ZM910.583 740.568L768.771 765.573L745.016 630.852L886.828 605.846L910.583 740.568Z" fill="#fff"></path></svg>`;
653
+ var stroke = (w, path, size = 24) => `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${w}" stroke-linecap="round" stroke-linejoin="round" style="display:block;flex:none">${path}</svg>`;
654
+ var ICON = {
655
+ close: (size = 19) => stroke(2.4, '<path d="M18 6 6 18M6 6l12 12"/>', size),
656
+ minus: stroke(2, '<path d="M6 12h12"/>', 15),
657
+ expand: stroke(2, '<path d="M15 4h5v5M20 4l-6.5 6.5M9 20H4v-5M4 20l6.5-6.5"/>', 15),
658
+ shrink: stroke(2, '<path d="M20 9h-5V4M15 9l5.5-5.5M4 15h5v5M9 15l-5.5 5.5"/>', 15),
659
+ chevronDown: stroke(2.1, '<path d="m6 9 6 6 6-6"/>', 16),
660
+ send: stroke(2.2, '<path d="M12 19V5M6 11l6-6 6 6"/>', 16),
661
+ paperclip: stroke(1.7, '<path d="M21 11.5 12.5 20a5.5 5.5 0 0 1-7.8-7.8l8.5-8.5a3.7 3.7 0 0 1 5.2 5.2l-8.5 8.5a1.8 1.8 0 0 1-2.6-2.6l7.9-7.8"/>', 17),
662
+ emoji: stroke(1.7, '<circle cx="12" cy="12" r="9"/><path d="M8.5 14.5a4.5 4.5 0 0 0 7 0M9 9.5h.01M15 9.5h.01"/>', 17),
663
+ mic: stroke(1.7, '<rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/>', 17),
664
+ search: stroke(1.9, '<circle cx="11" cy="11" r="7.5"/><path d="m21 21-4.3-4.3"/>', 17),
665
+ clear: stroke(2.2, '<path d="M18 6 6 18M6 6l12 12"/>', 15),
666
+ // Two-page "source" glyph for citation chips.
667
+ book: `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" style="display:block;flex:none;color:var(--oc-accent-text)"><path d="M3 5.5A2.5 2.5 0 0 1 5.5 3H11v18H5.5A2.5 2.5 0 0 1 3 18.5Z"/><path d="M21 5.5A2.5 2.5 0 0 0 18.5 3H13v18h5.5a2.5 2.5 0 0 0 2.5-2.5Z"/></svg>`,
668
+ teaserDismiss: stroke(2.4, '<path d="M18 6 6 18M6 6l12 12"/>', 12)
669
+ };
670
+
671
+ // src/emoji.ts
672
+ var CATS = [
673
+ { name: "Frequently used", items: [
674
+ ["\u{1F44D}", "thumbs up"],
675
+ ["\u{1F64F}", "thanks"],
676
+ ["\u{1F44B}", "wave"],
677
+ ["\u2705", "done"],
678
+ ["\u{1F4DE}", "call"],
679
+ ["\u{1F60A}", "smile"],
680
+ ["\u2764\uFE0F", "heart"]
681
+ ] },
682
+ { name: "Smileys and emotion", items: [
683
+ ["\u{1F600}", "grinning"],
684
+ ["\u{1F603}", "smiley"],
685
+ ["\u{1F604}", "grin"],
686
+ ["\u{1F601}", "beaming"],
687
+ ["\u{1F606}", "squinting"],
688
+ ["\u{1F605}", "relief"],
689
+ ["\u{1F923}", "rolling"],
690
+ ["\u{1F602}", "tears of joy"],
691
+ ["\u{1F642}", "slightly smiling"],
692
+ ["\u{1F643}", "upside down"],
693
+ ["\u{1F609}", "wink"],
694
+ ["\u{1F607}", "halo"],
695
+ ["\u{1F970}", "smiling hearts"],
696
+ ["\u{1F60D}", "heart eyes"],
697
+ ["\u{1F618}", "kiss"],
698
+ ["\u{1F60B}", "yum"],
699
+ ["\u{1F914}", "thinking"],
700
+ ["\u{1F928}", "raised brow"],
701
+ ["\u{1F610}", "neutral"],
702
+ ["\u{1F644}", "eye roll"],
703
+ ["\u{1F62C}", "grimace"],
704
+ ["\u{1F625}", "sad"],
705
+ ["\u{1F622}", "crying"],
706
+ ["\u{1F62D}", "sobbing"],
707
+ ["\u{1F620}", "angry"],
708
+ ["\u{1F633}", "flushed"],
709
+ ["\u{1F929}", "star struck"],
710
+ ["\u{1F634}", "sleeping"]
711
+ ] },
712
+ { name: "Gestures", items: [
713
+ ["\u{1F44E}", "thumbs down"],
714
+ ["\u{1F44F}", "clap"],
715
+ ["\u{1F64C}", "raised hands"],
716
+ ["\u{1F91D}", "handshake"],
717
+ ["\u{1F44C}", "ok hand"],
718
+ ["\u270C\uFE0F", "victory"],
719
+ ["\u{1F4AA}", "strong"],
720
+ ["\u{1F440}", "eyes"],
721
+ ["\u{1F9E0}", "brain"],
722
+ ["\u{1F91F}", "love you"],
723
+ ["\u261D\uFE0F", "point up"],
724
+ ["\u{1F447}", "point down"],
725
+ ["\u{1F91A}", "raised back"],
726
+ ["\u{1F91E}", "fingers crossed"]
727
+ ] },
728
+ { name: "Work and travel", items: [
729
+ ["\u{1F4C5}", "calendar"],
730
+ ["\u{1F4C4}", "document"],
731
+ ["\u{1F4B0}", "money"],
732
+ ["\u{1F4CA}", "chart"],
733
+ ["\u{1F4E6}", "package"],
734
+ ["\u{1F3E5}", "hospital"],
735
+ ["\u{1F3E6}", "bank"],
736
+ ["\u{1F69A}", "truck"],
737
+ ["\u2708\uFE0F", "plane"],
738
+ ["\u{1F4F1}", "phone"],
739
+ ["\u{1F4BB}", "laptop"],
740
+ ["\u{1F50D}", "search"],
741
+ ["\u{1F4CC}", "pin"],
742
+ ["\u{1F514}", "bell"]
743
+ ] },
744
+ { name: "Symbols", items: [
745
+ ["\u{1F525}", "fire"],
746
+ ["\u2728", "sparkles"],
747
+ ["\u{1F389}", "celebrate"],
748
+ ["\u{1F4AF}", "hundred"],
749
+ ["\u26A0\uFE0F", "warning"],
750
+ ["\u274C", "cross"],
751
+ ["\u2B50", "star"],
752
+ ["\u{1F680}", "launch"],
753
+ ["\u{1F49A}", "green heart"],
754
+ ["\u{1F499}", "blue heart"],
755
+ ["\u{1F552}", "clock"],
756
+ ["\u{1F4A1}", "idea"],
757
+ ["\u{1F512}", "lock"],
758
+ ["\u{1F3AF}", "target"]
759
+ ] }
760
+ ];
761
+ var toGroup = (name, items) => ({
762
+ name,
763
+ items: items.map(([ch, kw]) => ({ ch, name: kw }))
764
+ });
765
+ function emojiGroups(query) {
766
+ const q = query.trim().toLowerCase();
767
+ if (!q) return CATS.map((c) => toGroup(c.name, c.items));
768
+ const hits = [];
769
+ for (const c of CATS) for (const e of c.items) {
770
+ if (e[1].indexOf(q) >= 0 && !hits.some((h) => h[0] === e[0])) hits.push(e);
771
+ }
772
+ if (!hits.length) return [];
773
+ return [toGroup(`${hits.length} result${hits.length === 1 ? "" : "s"}`, hits)];
774
+ }
775
+
519
776
  // src/element.ts
520
- var HANDOFF_STATUS_TEXT = {
777
+ var DEFAULT_STATUS = "Replies in a few seconds";
778
+ var ATTACHMENT_ACCEPT = "image/*,application/pdf,text/plain,text/csv";
779
+ var AWAY_THRESHOLD = 60;
780
+ var HANDOFF_STATUS = {
521
781
  ai: null,
522
782
  awaiting_human: "Connecting you to a person\u2026",
523
783
  human: "You're chatting with a person"
524
784
  };
525
- var ATTACHMENT_ACCEPT = "image/*,application/pdf,text/plain,text/csv";
526
- var AGENT_TYPING_TEXT = "The agent is typing\u2026";
527
- var PIN_THRESHOLD = 48;
528
- var ICON_CHAT = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>';
529
- var ICON_CLOSE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
530
- var ICON_DOWN = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>';
531
- var ICON_CLIP = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>';
532
785
  var OyehelloChatElement = class extends HTMLElement {
533
- static observedAttributes = ["api-key", "agent-id", "base-url", "title"];
786
+ static observedAttributes = ["api-key", "agent-id", "base-url", "title", "theme"];
787
+ // ── live client state ──
534
788
  client = null;
535
789
  off = null;
536
790
  messages = [];
@@ -539,35 +793,42 @@ var OyehelloChatElement = class extends HTMLElement {
539
793
  handoffState = "ai";
540
794
  agentTyping = false;
541
795
  lastError = null;
542
- // registerTools() may run before connectedCallback creates the client — buffer until it exists.
543
796
  pendingTools = {};
544
797
  pendingFiles = [];
545
798
  imagePreviewCache = /* @__PURE__ */ new Map();
546
799
  resolvedUrlCache = /* @__PURE__ */ new Map();
547
- // Rendered-markdown cache for FINALIZED assistant/human messages (keyed by message id) so the whole
548
- // history isn't re-parsed on every streamed token — only the live streaming message is re-rendered.
549
800
  mdCache = /* @__PURE__ */ new Map();
550
- // Widget UI state.
551
- open = false;
801
+ receivedAt = /* @__PURE__ */ new Map();
802
+ // ── widget UI state ──
803
+ uiOpen = false;
552
804
  started = false;
553
- // lazy: the session opens on first panel open, not on page load
554
- pinned = true;
555
- // view is at/near the bottom
805
+ uiMax = false;
806
+ emojiOpen = false;
807
+ emojiQuery = "";
808
+ away = false;
809
+ // reader scrolled up
556
810
  unseen = 0;
557
- // messages that arrived while scrolled-up or while closed
811
+ focused = false;
812
+ teaserActive = false;
558
813
  renderedCount = 0;
559
- // message count at the last renderMessages()
560
- // Shadow-DOM refs (assigned once in renderShell).
561
- panelEl;
814
+ renderedBotFinal = 0;
815
+ timers = [];
816
+ // Server (flow) suggestions for the latest turn; null until the first `suggestions` event, then the
817
+ // widget prefers these over the static `suggestions` attribute.
818
+ serverSuggestions = null;
819
+ teaserText = "Need a hand? Ask me anything.";
820
+ teaserShownOnce = false;
821
+ idleTimer = null;
822
+ idleReset = null;
823
+ // ── shadow refs ──
824
+ oc;
562
825
  bodyEl;
563
826
  textareaEl;
564
- sendEl;
565
827
  fileEl;
566
- pillEl;
567
- badgeEl;
568
- statusEl;
569
- errEl;
570
828
  titleEl;
829
+ statusEl;
830
+ emojiListEl;
831
+ emojiSearchEl;
571
832
  connectedCallback() {
572
833
  if (!this.shadowRoot) this.attachShadow({ mode: "open" });
573
834
  const apiKey = this.getAttribute("api-key");
@@ -577,6 +838,7 @@ var OyehelloChatElement = class extends HTMLElement {
577
838
  this.renderError("oyehello-chat: api-key, agent-id and base-url are required");
578
839
  return;
579
840
  }
841
+ this.teaserText = this.getAttribute("teaser") || this.teaserText;
580
842
  this.client = createChatClient({ apiKey, agentId, baseUrl });
581
843
  if (Object.keys(this.pendingTools).length > 0) {
582
844
  this.client.registerTools(this.pendingTools);
@@ -584,18 +846,100 @@ var OyehelloChatElement = class extends HTMLElement {
584
846
  }
585
847
  this.off = this.client.on((e) => this.onClientEvent(e));
586
848
  this.renderShell();
587
- this.updateTitle();
849
+ this.renderMessages();
850
+ this.updateHeader();
588
851
  this.updateStatus();
852
+ this.updateClasses();
853
+ void this.loadWidgetConfig();
589
854
  let wasOpen = false;
590
855
  try {
591
- wasOpen = sessionStorage.getItem(this.openKey()) === "1";
856
+ wasOpen = sessionStorage.getItem(this.key("open")) === "1";
857
+ } catch {
858
+ }
859
+ if (wasOpen) {
860
+ this.openPanel();
861
+ return;
862
+ }
863
+ this.updateClasses(true);
864
+ this.timers.push(setTimeout(() => {
865
+ this.oc?.classList.remove("ring");
866
+ }, 6e3));
867
+ this.armIdleTeaser();
868
+ }
869
+ // Show the teaser once the visitor has been idle (no page interaction) for 10s — unless the panel
870
+ // is already open or the teaser was dismissed this session. Fires at most once.
871
+ armIdleTeaser() {
872
+ let dismissed = false;
873
+ try {
874
+ dismissed = sessionStorage.getItem(this.key("teaser")) === "0";
875
+ } catch {
876
+ }
877
+ if (dismissed) return;
878
+ const reset = () => {
879
+ if (this.idleTimer) clearTimeout(this.idleTimer);
880
+ this.idleTimer = setTimeout(() => {
881
+ if (!this.uiOpen && !this.teaserShownOnce) {
882
+ this.teaserActive = true;
883
+ this.teaserShownOnce = true;
884
+ this.updateClasses();
885
+ this.teardownIdle();
886
+ }
887
+ }, 1e4);
888
+ };
889
+ this.idleReset = reset;
890
+ for (const ev of ["mousemove", "keydown", "scroll", "touchstart", "click"]) document.addEventListener(ev, reset, { passive: true });
891
+ reset();
892
+ }
893
+ teardownIdle() {
894
+ if (this.idleTimer) {
895
+ clearTimeout(this.idleTimer);
896
+ this.idleTimer = null;
897
+ }
898
+ if (this.idleReset) {
899
+ for (const ev of ["mousemove", "keydown", "scroll", "touchstart", "click"]) document.removeEventListener(ev, this.idleReset);
900
+ this.idleReset = null;
901
+ }
902
+ }
903
+ async loadWidgetConfig() {
904
+ try {
905
+ const cfg = await this.client.getWidgetConfig();
906
+ if (!this.getAttribute("teaser") && cfg.teaser) {
907
+ this.teaserText = cfg.teaser;
908
+ this.applyTeaserText();
909
+ }
910
+ if (cfg.brand) {
911
+ if (cfg.brand.accentColor) this.style.setProperty("--oc-accent", cfg.brand.accentColor);
912
+ if (cfg.brand.name && !this.getAttribute("title")) {
913
+ this.agentName = cfg.brand.name;
914
+ this.updateHeader();
915
+ }
916
+ if (cfg.brand.logoUrl) this.applyBrandLogo(cfg.brand.logoUrl);
917
+ }
592
918
  } catch {
593
919
  }
594
- if (wasOpen) this.openPanel();
920
+ }
921
+ applyTeaserText() {
922
+ const el = this.shadowRoot?.querySelector(".teaser-btn span:last-child");
923
+ if (el) el.textContent = this.teaserText;
924
+ }
925
+ applyBrandLogo(url) {
926
+ const set = (host) => {
927
+ if (!host) return;
928
+ host.textContent = "";
929
+ const img = document.createElement("img");
930
+ img.src = url;
931
+ img.alt = "";
932
+ img.style.cssText = "width:100%;height:100%;object-fit:contain";
933
+ host.appendChild(img);
934
+ };
935
+ set(this.shadowRoot?.querySelector(".hd .logo"));
936
+ set(this.shadowRoot?.querySelector(".teaser-btn .logo"));
595
937
  }
596
938
  disconnectedCallback() {
597
939
  this.off?.();
598
940
  void this.client?.end();
941
+ this.teardownIdle();
942
+ for (const t of this.timers) clearTimeout(t);
599
943
  for (const url of this.imagePreviewCache.values()) {
600
944
  if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
601
945
  }
@@ -605,61 +949,123 @@ var OyehelloChatElement = class extends HTMLElement {
605
949
  if (this.client) this.client.registerTools(handlers);
606
950
  else Object.assign(this.pendingTools, handlers);
607
951
  }
608
- openKey() {
609
- return `oyehello-chat:open:${this.getAttribute("agent-id") ?? ""}`;
952
+ key(k) {
953
+ return `oyehello-chat:${k}:${this.getAttribute("agent-id") ?? ""}`;
954
+ }
955
+ // Static fallback chips from the `suggestions` attribute (for embedders without flow-driven ones).
956
+ get attrSuggestions() {
957
+ return (this.getAttribute("suggestions") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
958
+ }
959
+ // Effective chips: server (flow) suggestions once received, else the static attribute fallback.
960
+ get chipLabels() {
961
+ return this.serverSuggestions ?? this.attrSuggestions;
610
962
  }
611
963
  // ── Shell (built once) ─────────────────────────────────────────────────────────────────────────
612
964
  renderShell() {
613
965
  const root = this.shadowRoot;
966
+ const hasPrivacy = !!this.getAttribute("privacy-url");
967
+ const teaserText = this.teaserText;
614
968
  root.innerHTML = `
615
969
  <style>${STYLES}</style>
616
- <button class="launcher" part="launcher" aria-label="Open chat" aria-haspopup="dialog">
617
- ${ICON_CHAT}<span class="badge" hidden></span>
618
- </button>
619
- <div class="panel" part="panel" role="dialog" aria-label="Chat" hidden>
620
- <div class="hd"><span class="title">Chat</span><button class="close" aria-label="Close chat">${ICON_CLOSE}</button></div>
621
- <div class="status" hidden></div>
622
- <div class="err" hidden></div>
623
- <div class="body"></div>
624
- <button class="pill" type="button" hidden>${ICON_DOWN}<span class="pill-text">New messages</span></button>
625
- <form>
626
- <button type="button" class="attach" aria-label="Attach a file">${ICON_CLIP}</button>
627
- <input type="file" accept="${ATTACHMENT_ACCEPT}" multiple hidden />
628
- <textarea aria-label="message" rows="1" placeholder="Type a message\u2026"></textarea>
629
- <button type="submit" class="send">Send</button>
630
- </form>
970
+ <div class="oc has-teaser${hasPrivacy ? " has-privacy" : ""}">
971
+ <div class="panel" role="dialog" aria-label="Chat">
972
+ <div class="hd">
973
+ <span class="hd-grid"></span>
974
+ <span class="logo">${logoMark("currentColor")}</span>
975
+ <span class="hd-txt"><span class="hd-title">Chat</span><span class="hd-status"><span class="dot"></span><span class="t"></span></span></span>
976
+ <button type="button" class="hd-btn max" aria-label="Expand"><span class="i i-expand">${ICON.expand}</span><span class="i i-shrink">${ICON.shrink}</span></button>
977
+ <button type="button" class="hd-btn min" aria-label="Minimise">${ICON.minus}</button>
978
+ </div>
979
+ <div class="body"></div>
980
+ <span class="emoji-scrim"></span>
981
+ <div class="emoji-panel">
982
+ <div class="emoji-search">${ICON.search}<input type="text" placeholder="Search emoji\u2026" aria-label="Search emoji" /></div>
983
+ <div class="emoji-list"></div>
984
+ </div>
985
+ <div class="jump-wrap"><button type="button" class="jump" aria-label="Jump to latest">${ICON.chevronDown}</button></div>
986
+ <div class="composer">
987
+ <form class="field">
988
+ <textarea class="input" rows="1" aria-label="message" placeholder="Ask a question\u2026"></textarea>
989
+ <div class="acts">
990
+ <button type="button" class="act attach" aria-label="Attach a file">${ICON.paperclip}</button>
991
+ <input type="file" accept="${ATTACHMENT_ACCEPT}" multiple hidden />
992
+ <button type="button" class="act emoji-btn" aria-label="Insert emoji">${ICON.emoji}</button>
993
+ <button type="button" class="act mic" aria-label="Send a voice note">${ICON.mic}</button>
994
+ <button type="submit" class="send" aria-label="Send">${ICON.send}</button>
995
+ </div>
996
+ </form>
997
+ <div class="foot">
998
+ <span>By chatting with us, you agree to our </span><span class="priv">Privacy Policy</span>
999
+ <span class="tip"><span class="tip-card">By chatting with us, you agree to the monitoring and recording of this chat to deliver our services and processing of your personal data in accordance with our Privacy Policy. <a href="${this.escapeAttr(this.getAttribute("privacy-url") ?? "#")}" target="_blank" rel="noopener noreferrer">See our Privacy Policy</a>.</span></span>
1000
+ </div>
1001
+ </div>
1002
+ </div>
1003
+
1004
+ <div class="teaser">
1005
+ <button type="button" class="teaser-btn"><span class="logo">${logoMark("currentColor")}</span><span>${escapeHtml(teaserText)}</span></button>
1006
+ <button type="button" class="teaser-x" aria-label="Dismiss">${ICON.teaserDismiss}</button>
1007
+ </div>
1008
+
1009
+ <button type="button" class="launcher" aria-label="Open chat">
1010
+ <span class="ring"></span>
1011
+ <span class="ic-chat">${logoMark("var(--oc-ac)")}</span>
1012
+ <span class="ic-close">${ICON.close(19)}</span>
1013
+ <span class="badge"></span>
1014
+ </button>
631
1015
  </div>`;
632
- this.panelEl = root.querySelector(".panel");
1016
+ this.oc = root.querySelector(".oc");
633
1017
  this.bodyEl = root.querySelector(".body");
634
- this.textareaEl = root.querySelector("textarea");
635
- this.sendEl = root.querySelector(".send");
1018
+ this.textareaEl = root.querySelector("textarea.input");
636
1019
  this.fileEl = root.querySelector('input[type="file"]');
637
- this.pillEl = root.querySelector(".pill");
638
- this.badgeEl = root.querySelector(".badge");
639
- this.statusEl = root.querySelector(".status");
640
- this.errEl = root.querySelector(".err");
641
- this.titleEl = root.querySelector(".title");
642
- root.querySelector(".launcher").addEventListener("click", () => this.togglePanel());
643
- root.querySelector(".close").addEventListener("click", () => this.closePanel());
1020
+ this.titleEl = root.querySelector(".hd-title");
1021
+ this.statusEl = root.querySelector(".hd-status .t");
1022
+ this.emojiListEl = root.querySelector(".emoji-list");
1023
+ this.emojiSearchEl = root.querySelector(".emoji-search input");
1024
+ root.querySelector(".launcher").addEventListener("click", () => this.toggle());
1025
+ root.querySelector(".hd-btn.min").addEventListener("click", () => this.closePanel());
1026
+ root.querySelector(".hd-btn.max").addEventListener("click", () => {
1027
+ this.uiMax = !this.uiMax;
1028
+ this.updateClasses();
1029
+ });
1030
+ root.querySelector(".teaser-btn").addEventListener("click", () => this.openPanel());
1031
+ root.querySelector(".teaser-x").addEventListener("click", () => this.dismissTeaser());
1032
+ root.querySelector(".jump").addEventListener("click", () => this.scrollToBottom());
644
1033
  root.querySelector(".attach").addEventListener("click", () => this.fileEl.click());
645
1034
  this.fileEl.addEventListener("change", () => {
646
1035
  this.pendingFiles = this.fileEl.files ? Array.from(this.fileEl.files) : [];
647
1036
  });
648
- this.pillEl.addEventListener("click", () => this.scrollToBottom());
649
- this.bodyEl.addEventListener("scroll", () => this.onBodyScroll());
650
- root.querySelector("form").addEventListener("submit", (e) => {
1037
+ root.querySelector(".emoji-btn").addEventListener("click", () => this.toggleEmoji());
1038
+ root.querySelector(".emoji-scrim").addEventListener("click", () => this.closeEmoji());
1039
+ this.emojiSearchEl.addEventListener("input", () => {
1040
+ this.emojiQuery = this.emojiSearchEl.value;
1041
+ this.renderEmoji();
1042
+ });
1043
+ this.bodyEl.addEventListener("scroll", () => this.onScroll());
1044
+ root.querySelector("form.field").addEventListener("submit", (e) => {
651
1045
  e.preventDefault();
652
1046
  this.doSend();
653
1047
  });
654
- this.textareaEl.addEventListener("input", () => this.autoGrow());
1048
+ this.textareaEl.addEventListener("input", () => {
1049
+ this.autoGrow();
1050
+ this.updateClasses();
1051
+ });
1052
+ this.textareaEl.addEventListener("focus", () => {
1053
+ this.focused = true;
1054
+ this.updateClasses();
1055
+ });
1056
+ this.textareaEl.addEventListener("blur", () => {
1057
+ this.focused = false;
1058
+ this.updateClasses();
1059
+ });
655
1060
  this.textareaEl.addEventListener("keydown", (e) => {
656
1061
  if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
657
1062
  e.preventDefault();
658
1063
  this.doSend();
659
1064
  }
660
- });
661
- this.panelEl.addEventListener("keydown", (e) => {
662
- if (e.key === "Escape") this.closePanel();
1065
+ if (e.key === "Escape") {
1066
+ if (this.emojiOpen) this.closeEmoji();
1067
+ else this.closePanel();
1068
+ }
663
1069
  });
664
1070
  }
665
1071
  renderError(msg) {
@@ -667,37 +1073,63 @@ var OyehelloChatElement = class extends HTMLElement {
667
1073
  console.error(msg);
668
1074
  this.shadowRoot.innerHTML = `<div style="position:fixed;right:16px;bottom:16px;color:#b91c1c;font:12px system-ui;background:#fef2f2;border:1px solid #fecaca;padding:8px 12px;border-radius:8px">${escapeHtml(msg)}</div>`;
669
1075
  }
1076
+ // ── State-class sync ──────────────────────────────────────────────────────────────────────────
1077
+ updateClasses(initialRing = false) {
1078
+ if (!this.oc) return;
1079
+ const c = this.oc.classList;
1080
+ c.toggle("open", this.uiOpen);
1081
+ c.toggle("max", this.uiMax);
1082
+ c.toggle("emoji", this.emojiOpen);
1083
+ c.toggle("teaser", this.teaserActive);
1084
+ c.toggle("away", this.away);
1085
+ c.toggle("focus", this.focused);
1086
+ c.toggle("ready", (this.textareaEl?.value.trim().length ?? 0) > 0);
1087
+ c.toggle("unread", this.unseen > 0);
1088
+ if (initialRing) c.add("ring");
1089
+ const badge = this.oc.querySelector(".badge");
1090
+ if (badge) badge.textContent = this.unseen > 9 ? "9+" : String(this.unseen);
1091
+ const launcher = this.oc.querySelector(".launcher");
1092
+ if (launcher) launcher.setAttribute("aria-label", this.uiOpen ? "Close chat" : "Open chat");
1093
+ }
670
1094
  // ── Open / close ───────────────────────────────────────────────────────────────────────────────
671
- togglePanel() {
672
- this.open ? this.closePanel() : this.openPanel();
1095
+ toggle() {
1096
+ this.uiOpen ? this.closePanel() : this.openPanel();
673
1097
  }
674
1098
  openPanel() {
675
- this.open = true;
1099
+ this.uiOpen = true;
1100
+ this.teaserActive = false;
1101
+ this.unseen = 0;
1102
+ this.teardownIdle();
676
1103
  try {
677
- sessionStorage.setItem(this.openKey(), "1");
1104
+ sessionStorage.setItem(this.key("open"), "1");
678
1105
  } catch {
679
1106
  }
680
- this.panelEl.hidden = false;
681
- this.panelEl.classList.add("enter");
682
- requestAnimationFrame(() => this.panelEl.classList.remove("enter"));
683
1107
  if (!this.started && this.client) {
684
1108
  this.started = true;
685
1109
  void this.client.start();
686
1110
  }
1111
+ this.updateClasses();
687
1112
  this.renderMessages();
688
- this.pinned = true;
689
- this.unseen = 0;
690
- this.updateBadge();
691
1113
  this.scrollToBottom();
692
- if (!this.textareaEl.disabled) this.textareaEl.focus();
1114
+ if (!this.textareaEl.disabled) setTimeout(() => this.textareaEl.focus(), 60);
693
1115
  }
694
1116
  closePanel() {
695
- this.open = false;
1117
+ this.uiOpen = false;
1118
+ this.uiMax = false;
1119
+ this.emojiOpen = false;
696
1120
  try {
697
- sessionStorage.setItem(this.openKey(), "0");
1121
+ sessionStorage.setItem(this.key("open"), "0");
698
1122
  } catch {
699
1123
  }
700
- this.panelEl.hidden = true;
1124
+ this.updateClasses();
1125
+ }
1126
+ dismissTeaser() {
1127
+ this.teaserActive = false;
1128
+ try {
1129
+ sessionStorage.setItem(this.key("teaser"), "0");
1130
+ } catch {
1131
+ }
1132
+ this.updateClasses();
701
1133
  }
702
1134
  // ── Client events ─────────────────────────────────────────────────────────────────────────────
703
1135
  onClientEvent(e) {
@@ -707,91 +1139,99 @@ var OyehelloChatElement = class extends HTMLElement {
707
1139
  if (e.state === "ready" || e.state === "opening") this.lastError = null;
708
1140
  } else if (e.type === "agent") {
709
1141
  this.agentName = e.agent.name;
710
- this.updateTitle();
1142
+ this.updateHeader();
711
1143
  } else if (e.type === "handoff") this.handoffState = e.state;
712
1144
  else if (e.type === "agentTyping") this.agentTyping = e.active;
1145
+ else if (e.type === "suggestions") this.serverSuggestions = e.items;
713
1146
  else if (e.type === "error") this.lastError = e.error.message;
714
1147
  this.updateStatus();
715
1148
  this.renderMessages();
716
1149
  }
717
- updateTitle() {
1150
+ updateHeader() {
718
1151
  if (this.titleEl) this.titleEl.textContent = this.getAttribute("title") ?? this.agentName ?? "Chat";
719
1152
  }
720
1153
  updateStatus() {
721
1154
  if (!this.textareaEl) return;
722
1155
  const disabled = this.state !== "ready";
723
1156
  this.textareaEl.disabled = disabled;
724
- this.sendEl.disabled = disabled;
725
- const st = this.state === "opening" ? "Connecting\u2026" : HANDOFF_STATUS_TEXT[this.handoffState];
726
- this.statusEl.hidden = !st;
727
- this.statusEl.textContent = st ?? "";
728
- if (this.state === "error" && this.lastError) {
729
- this.errEl.hidden = false;
730
- this.errEl.innerHTML = `<span>Couldn't start chat: ${escapeHtml(this.lastError)}</span><button class="retry" type="button">Retry</button>`;
731
- this.errEl.querySelector(".retry").addEventListener("click", () => {
732
- void this.client?.start();
733
- });
734
- } else {
735
- this.errEl.hidden = true;
736
- this.errEl.innerHTML = "";
737
- }
1157
+ const handoff = HANDOFF_STATUS[this.handoffState];
1158
+ const text = this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
1159
+ if (this.statusEl) this.statusEl.textContent = text;
738
1160
  }
739
1161
  // ── Message list + scroll ─────────────────────────────────────────────────────────────────────
740
1162
  renderMessages() {
741
1163
  if (!this.bodyEl) return;
742
1164
  const body = this.bodyEl;
743
- const atBottom = this.open && this.pinned;
744
1165
  const prevTop = body.scrollTop;
745
1166
  const added = this.messages.length - this.renderedCount;
746
- const showTyping = this.agentTyping && (this.handoffState === "human" || this.handoffState === "awaiting_human");
747
- let html = this.messages.map((m) => this.bubbleHtml(m)).join("");
748
- if (showTyping) html += `<div class="b assistant typing">${escapeHtml(AGENT_TYPING_TEXT)}</div>`;
1167
+ const botFinal = this.messages.filter((m) => m.role === "assistant" && !m.streaming).length;
1168
+ const newBotFinal = botFinal > this.renderedBotFinal;
1169
+ const now = Date.now();
1170
+ for (const m of this.messages) if (!this.receivedAt.has(m.id)) this.receivedAt.set(m.id, now);
1171
+ const lastBot = (() => {
1172
+ for (let i = this.messages.length - 1; i >= 0; i--) if (this.messages[i]?.role === "assistant") return i;
1173
+ return -1;
1174
+ })();
1175
+ let html = this.messages.map((m, i) => this.turnHtml(m, i === lastBot && !this.agentTyping)).join("");
1176
+ if (this.agentTyping) html += '<span class="typing"><span></span><span></span><span></span></span>';
1177
+ const sugg = this.chipLabels;
1178
+ if (sugg.length > 0 && !this.agentTyping && this.state === "ready") {
1179
+ html += `<span class="chips">${sugg.map((s, i) => `<button type="button" class="chip" data-chip="${i}">${escapeHtml(s)}</button>`).join("")}</span>`;
1180
+ }
1181
+ if (this.state === "error" && this.lastError) {
1182
+ html += `<div class="oc-err"><span class="oc-err-t">${escapeHtml(this.lastError)}</span><button type="button" class="oc-err-retry">Try again</button></div>`;
1183
+ }
749
1184
  body.innerHTML = html;
750
1185
  this.hydrateAttachmentImages();
1186
+ this.bindChips();
751
1187
  this.renderedCount = this.messages.length;
752
- if (atBottom) this.scrollToBottom();
1188
+ this.renderedBotFinal = botFinal;
1189
+ if (!this.uiOpen) {
1190
+ if (added > 0) this.unseen += added;
1191
+ this.updateClasses();
1192
+ return;
1193
+ }
1194
+ if (newBotFinal && !this.away) this.parkLastQuestion();
1195
+ else if (!this.away) this.scrollToBottom();
753
1196
  else {
754
- if (this.open) body.scrollTop = prevTop;
1197
+ body.scrollTop = prevTop;
755
1198
  if (added > 0) this.unseen += added;
756
1199
  }
757
- this.updatePill();
758
- this.updateBadge();
1200
+ this.updateClasses();
1201
+ }
1202
+ // After a reply lands, float the question that prompted it to the top so the answer reads from its
1203
+ // first line rather than its last (the design's behaviour).
1204
+ parkLastQuestion() {
1205
+ const users = this.bodyEl.querySelectorAll(".turn.user");
1206
+ const last = users[users.length - 1];
1207
+ if (!last) {
1208
+ this.scrollToBottom();
1209
+ return;
1210
+ }
1211
+ const top = Math.max(0, Math.min(last.offsetTop - 22, this.bodyEl.scrollHeight - this.bodyEl.clientHeight));
1212
+ this.bodyEl.scrollTo({ top, behavior: "smooth" });
759
1213
  }
760
- onBodyScroll() {
1214
+ onScroll() {
761
1215
  const b = this.bodyEl;
762
- const nearBottom = b.scrollHeight - b.scrollTop - b.clientHeight < PIN_THRESHOLD;
763
- this.pinned = nearBottom;
764
- if (nearBottom) {
765
- this.unseen = 0;
766
- this.updateBadge();
1216
+ const away = b.scrollHeight - b.scrollTop - b.clientHeight > AWAY_THRESHOLD;
1217
+ if (away !== this.away) {
1218
+ this.away = away;
1219
+ if (!away) this.unseen = 0;
1220
+ this.updateClasses();
767
1221
  }
768
- this.updatePill();
769
1222
  }
770
1223
  scrollToBottom() {
771
1224
  if (!this.bodyEl) return;
772
1225
  this.bodyEl.scrollTop = this.bodyEl.scrollHeight;
773
- this.pinned = true;
1226
+ this.away = false;
774
1227
  this.unseen = 0;
775
- this.updatePill();
776
- this.updateBadge();
777
- }
778
- updatePill() {
779
- if (!this.pillEl) return;
780
- this.pillEl.hidden = !(this.open && !this.pinned && this.unseen > 0);
781
- const t = this.pillEl.querySelector(".pill-text");
782
- if (t) t.textContent = this.unseen === 1 ? "1 new message" : `${this.unseen} new messages`;
783
- }
784
- updateBadge() {
785
- if (!this.badgeEl) return;
786
- const show = !this.open && this.unseen > 0;
787
- this.badgeEl.hidden = !show;
788
- this.badgeEl.textContent = this.unseen > 9 ? "9+" : String(this.unseen);
1228
+ this.updateClasses();
789
1229
  }
790
1230
  // ── Composer ──────────────────────────────────────────────────────────────────────────────────
791
1231
  autoGrow() {
792
1232
  const ta = this.textareaEl;
793
1233
  ta.style.height = "auto";
794
- ta.style.height = `${Math.min(ta.scrollHeight, 120)}px`;
1234
+ ta.style.height = `${Math.min(ta.scrollHeight, 108)}px`;
795
1235
  }
796
1236
  doSend() {
797
1237
  const text = this.textareaEl.value;
@@ -804,15 +1244,66 @@ var OyehelloChatElement = class extends HTMLElement {
804
1244
  this.imagePreviewCache.set(f.name, URL.createObjectURL(f));
805
1245
  }
806
1246
  }
807
- this.pinned = true;
1247
+ this.away = false;
1248
+ this.emojiOpen = false;
808
1249
  void this.client?.sendMessage(text, files.length > 0 ? files : void 0);
809
1250
  this.textareaEl.value = "";
810
1251
  this.textareaEl.style.height = "auto";
1252
+ this.updateClasses();
1253
+ }
1254
+ sendChip(text) {
1255
+ this.away = false;
1256
+ this.emojiOpen = false;
1257
+ void this.client?.sendMessage(text);
1258
+ this.updateClasses();
1259
+ }
1260
+ bindChips() {
1261
+ const sugg = this.chipLabels;
1262
+ this.bodyEl.querySelectorAll(".chip[data-chip]").forEach((btn) => {
1263
+ const i = Number(btn.getAttribute("data-chip"));
1264
+ const text = sugg[i];
1265
+ if (text) btn.addEventListener("click", () => this.sendChip(text));
1266
+ });
1267
+ this.bodyEl.querySelector(".oc-err-retry")?.addEventListener("click", () => {
1268
+ void this.client?.start();
1269
+ });
1270
+ }
1271
+ // ── Emoji ─────────────────────────────────────────────────────────────────────────────────────
1272
+ toggleEmoji() {
1273
+ this.emojiOpen = !this.emojiOpen;
1274
+ this.emojiQuery = "";
1275
+ this.emojiSearchEl.value = "";
1276
+ this.renderEmoji();
1277
+ this.updateClasses();
1278
+ if (this.emojiOpen) setTimeout(() => this.emojiSearchEl.focus(), 60);
1279
+ }
1280
+ closeEmoji() {
1281
+ this.emojiOpen = false;
1282
+ this.emojiQuery = "";
1283
+ this.updateClasses();
1284
+ }
1285
+ renderEmoji() {
1286
+ const groups = emojiGroups(this.emojiQuery);
1287
+ let html = groups.map((g) => `
1288
+ <div class="emoji-grp"><span class="emoji-grp-name">${escapeHtml(g.name)}</span>
1289
+ <div class="emoji-grid">${g.items.map((e) => `<button type="button" title="${this.escapeAttr(e.name)}" aria-label="${this.escapeAttr(e.name)}" data-ch="${this.escapeAttr(e.ch)}">${e.ch}</button>`).join("")}</div>
1290
+ </div>`).join("");
1291
+ if (groups.length === 0) html = '<span class="emoji-empty" style="display:flex"><span class="h">No emoji match that</span><span class="s">Try a shorter word, like smile or call.</span></span>';
1292
+ this.emojiListEl.innerHTML = html;
1293
+ this.emojiListEl.querySelectorAll("button[data-ch]").forEach((btn) => {
1294
+ btn.addEventListener("click", () => {
1295
+ this.textareaEl.value += btn.getAttribute("data-ch") ?? "";
1296
+ this.emojiOpen = false;
1297
+ this.autoGrow();
1298
+ this.updateClasses();
1299
+ this.textareaEl.focus();
1300
+ });
1301
+ });
811
1302
  }
812
1303
  // ── Bubble rendering ──────────────────────────────────────────────────────────────────────────
813
- bubbleHtml(m) {
1304
+ turnHtml(m, isLastBot) {
1305
+ const role = m.role === "user" ? "user" : m.role === "human" ? "human" : "bot";
814
1306
  const atts = (m.attachments ?? []).map((a) => this.renderAttachment(a)).join("");
815
- const label = m.role === "human" ? '<div class="human-label">Support</div>' : "";
816
1307
  let content;
817
1308
  if (m.role === "assistant") {
818
1309
  let rendered = m.streaming ? void 0 : this.mdCache.get(m.id);
@@ -820,30 +1311,32 @@ var OyehelloChatElement = class extends HTMLElement {
820
1311
  rendered = renderMarkdown(m.text || "");
821
1312
  if (!m.streaming) this.mdCache.set(m.id, rendered);
822
1313
  }
823
- content = `<div class="md">${rendered}</div>`;
1314
+ content = `<span class="b">${atts}<span class="md">${rendered}</span></span>`;
824
1315
  } else {
825
- content = escapeHtml(m.text);
1316
+ const label = m.role === "human" ? '<div class="human-label">Support</div>' : "";
1317
+ content = `<span class="b">${label}${atts}${escapeHtml(m.text)}</span>`;
826
1318
  }
827
- const streamingDots = m.streaming ? '<span aria-hidden="true">\u2026</span>' : "";
828
- const bubble = `<div class="b ${m.role}">${label}${atts}${content}${streamingDots}</div>`;
829
- const sources = m.sources && m.sources.length > 0 ? `<div class="src-row">${m.sources.map((s) => this.renderSource(s)).join("")}</div>` : "";
830
- return bubble + sources;
1319
+ const src = m.sources && m.sources.length > 0 ? `<span class="src">${ICON.book}<span class="t">${escapeHtml("From " + (m.sources[0]?.title ?? ""))}</span></span>` : "";
1320
+ const byline = role === "bot" && isLastBot ? `<span class="byline">${escapeHtml((this.agentName ?? "OyeHello") + " \xB7 AI agent \xB7 " + this.relTime(m.id))}</span>` : "";
1321
+ return `<span class="turn ${role}">${content}${src}${byline}</span>`;
1322
+ }
1323
+ relTime(id) {
1324
+ const t = this.receivedAt.get(id);
1325
+ if (!t) return "just now";
1326
+ const mins = Math.floor((Date.now() - t) / 6e4);
1327
+ return mins <= 0 ? "just now" : `${mins} min ago`;
831
1328
  }
832
1329
  renderAttachment(a) {
833
1330
  if (a.contentType.startsWith("image/")) {
834
1331
  const cached = (a.name ? this.imagePreviewCache.get(a.name) : void 0) ?? this.resolvedUrlCache.get(a.storageRef);
835
1332
  const srcAttr = cached ? ` src="${cached}"` : "";
836
- return `<img class="att-thumb" data-oc-ref="${escapeHtml(a.storageRef)}"${srcAttr} alt="${escapeHtml(a.name ?? "image")}"/>`;
1333
+ return `<img class="att-thumb" data-oc-ref="${this.escapeAttr(a.storageRef)}"${srcAttr} alt="${this.escapeAttr(a.name ?? "image")}"/>`;
837
1334
  }
838
1335
  return `<div class="att-chip">\u{1F4CE} ${escapeHtml(a.name ?? a.contentType)}</div>`;
839
1336
  }
840
- renderSource(s) {
841
- return `<div class="src-chip" title="${escapeHtml(s.snippet)}">${escapeHtml(s.title)}</div>`;
842
- }
843
1337
  hydrateAttachmentImages() {
844
1338
  if (!this.client) return;
845
- const imgs = this.shadowRoot.querySelectorAll("img.att-thumb[data-oc-ref]");
846
- imgs.forEach((img) => {
1339
+ this.shadowRoot.querySelectorAll("img.att-thumb[data-oc-ref]").forEach((img) => {
847
1340
  if (img.hasAttribute("src")) return;
848
1341
  const ref = img.getAttribute("data-oc-ref");
849
1342
  if (!ref) return;
@@ -854,6 +1347,9 @@ var OyehelloChatElement = class extends HTMLElement {
854
1347
  });
855
1348
  });
856
1349
  }
1350
+ escapeAttr(s) {
1351
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
1352
+ }
857
1353
  };
858
1354
  function escapeHtml(s) {
859
1355
  return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);