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