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