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