@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.
@@ -160,7 +160,7 @@ var OyehelloChat = (function (exports) {
160
160
  seenTurnIds.add(ev.turnId);
161
161
  const m2 = streamingId ? messages.find((x2) => x2.id === streamingId) : void 0;
162
162
  if (m2) {
163
- m2.text = ev.text;
163
+ m2.text = ev.text || m2.text;
164
164
  m2.streaming = false;
165
165
  m2.turnId = ev.turnId;
166
166
  } else {
@@ -3285,132 +3285,369 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3285
3285
  // src/styles.ts
3286
3286
  var STYLES = `
3287
3287
  :host {
3288
- --oc-accent: #2563eb;
3289
- --oc-fg: #111827;
3290
- --oc-muted: #6b7280;
3291
- --oc-surface: #ffffff;
3292
- --oc-surface-2: #f3f4f6;
3293
- --oc-border: #e5e7eb;
3294
- --oc-radius: 16px;
3295
- position: fixed; right: 20px; bottom: 20px; z-index: 2147483000;
3296
- font-family: system-ui, -apple-system, sans-serif;
3288
+ /* accent (consumer-overridable via --oc-accent) + its derivations */
3289
+ --oc-ac: var(--oc-accent, #096DD9);
3290
+ --oc-btn-rim: color-mix(in oklab, var(--oc-ac) 70%, black);
3291
+ --oc-btn-edge: 0 6px 16px -10px color-mix(in oklab, var(--oc-ac) 55%, transparent);
3292
+ --oc-danger: #D92D20;
3293
+ --oc-ease: cubic-bezier(0.16,1,0.3,1);
3294
+ --oc-bounce: cubic-bezier(0.18,0.89,0.32,1.13);
3295
+ --oc-mono: 'Geist Mono', ui-monospace, monospace;
3296
+ --oc-r-full: 999px;
3297
+
3298
+ /* light palette (base) */
3299
+ --oc-bg: #FFFFFF; --oc-bg-sunk: #F6F8FB;
3300
+ --oc-fg: #0A0D12; --oc-fg-muted: rgba(10,13,18,0.66); --oc-fg-dim: rgba(10,13,18,0.44);
3301
+ --oc-border: rgba(9,13,20,0.10);
3302
+ --oc-accent-text: color-mix(in oklab, var(--oc-ac) 88%, black);
3303
+ --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);
3304
+ --oc-field-bg: #FFFFFF; --oc-field-border: rgba(9,13,20,0.22);
3305
+ --oc-glass-border: rgba(9,13,20,0.12);
3306
+
3307
+ position: fixed; right: 24px; bottom: 24px; z-index: 2147483000;
3308
+ font-family: 'Geist', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
3309
+ -webkit-font-smoothing: antialiased;
3310
+ }
3311
+ @media (prefers-color-scheme: dark) {
3312
+ :host(:not([theme="light"])) {
3313
+ --oc-bg: #000000; --oc-bg-sunk: #000000;
3314
+ --oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
3315
+ --oc-border: rgba(255,255,255,0.09);
3316
+ --oc-accent-text: color-mix(in oklab, var(--oc-ac) 62%, white);
3317
+ --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);
3318
+ --oc-field-bg: rgba(255,255,255,0.045); --oc-field-border: rgba(255,255,255,0.20);
3319
+ --oc-glass-border: rgba(255,255,255,0.12);
3320
+ }
3321
+ }
3322
+ :host([theme="dark"]) {
3323
+ --oc-bg: #000000; --oc-bg-sunk: #000000;
3324
+ --oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
3325
+ --oc-border: rgba(255,255,255,0.09);
3326
+ --oc-accent-text: color-mix(in oklab, var(--oc-ac) 62%, white);
3327
+ --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);
3328
+ --oc-field-bg: rgba(255,255,255,0.045); --oc-field-border: rgba(255,255,255,0.20);
3329
+ --oc-glass-border: rgba(255,255,255,0.12);
3297
3330
  }
3298
3331
  * { box-sizing: border-box; }
3332
+ button { font-family: inherit; }
3299
3333
 
3300
- /* Launcher */
3334
+ .oc { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; }
3335
+
3336
+ /* \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 */
3301
3337
  .launcher {
3302
- width: 60px; height: 60px; border-radius: 50%; border: none; cursor: pointer;
3303
- background: var(--oc-accent); color: #fff; box-shadow: 0 6px 24px rgba(0,0,0,.25);
3304
- display: grid; place-items: center; transition: transform .15s ease, opacity .15s ease;
3338
+ position: relative; flex: none; width: 46px; height: 46px; border-radius: var(--oc-r-full);
3339
+ border: 1px solid var(--oc-btn-rim); background: var(--oc-ac);
3340
+ 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);
3341
+ color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer;
3342
+ transition: transform .26s var(--oc-bounce), filter .2s var(--oc-ease); align-self: flex-end;
3305
3343
  }
3306
3344
  .launcher:hover { transform: scale(1.06); }
3307
- .launcher svg { width: 28px; height: 28px; }
3308
- .launcher .badge {
3309
- position: absolute; top: -2px; right: -2px; min-width: 20px; height: 20px; padding: 0 5px;
3310
- border-radius: 10px; background: #ef4444; color: #fff; font-size: 11px; font-weight: 700;
3311
- display: grid; place-items: center; border: 2px solid var(--oc-surface);
3345
+ .launcher:active { transform: scale(0.93); }
3346
+ .ring { pointer-events: none; position: absolute; inset: -1px; border-radius: var(--oc-r-full); border: 2px solid var(--oc-ac); opacity: 0; }
3347
+ .oc.ring:not(.open) .ring { animation: w-ring 2.4s ease-out 1.1s 3; }
3348
+ .ic-chat, .ic-close { position: absolute; display: flex; align-items: center; justify-content: center; transition: all .3s var(--oc-bounce); }
3349
+ .ic-chat { width: 28px; height: 28px; opacity: 1; transform: rotate(0) scale(1); }
3350
+ .ic-close { opacity: 0; transform: rotate(90deg) scale(0.6); }
3351
+ .oc.open .ic-chat { opacity: 0; transform: rotate(-90deg) scale(0.6); }
3352
+ .oc.open .ic-close { opacity: 1; transform: rotate(0) scale(1); }
3353
+ .badge {
3354
+ position: absolute; top: -2px; right: -2px; min-width: 17px; height: 17px; padding: 0 5px;
3355
+ border-radius: var(--oc-r-full); border: 2px solid var(--oc-bg-sunk); background: var(--oc-danger);
3356
+ color: #fff; font-family: var(--oc-mono); font-size: 10px; font-weight: 700;
3357
+ display: flex; align-items: center; justify-content: center; opacity: 0; transform: scale(0.4);
3358
+ transition: all .26s var(--oc-bounce);
3312
3359
  }
3360
+ .oc.unread:not(.open) .badge { opacity: 1; transform: scale(1); }
3313
3361
 
3314
- /* Panel */
3362
+ /* \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 */
3363
+ .teaser {
3364
+ position: absolute; right: 0; bottom: 60px; display: none; align-items: center; gap: 11px;
3365
+ opacity: 0; transform: translateY(8px) scale(0.94); pointer-events: none;
3366
+ transition: opacity .3s var(--oc-ease), transform .38s var(--oc-bounce);
3367
+ }
3368
+ .oc.has-teaser .teaser { display: flex; }
3369
+ .oc.teaser:not(.open) .teaser { opacity: 1; transform: none; pointer-events: auto; }
3370
+ .teaser-btn {
3371
+ max-width: 250px; display: flex; align-items: center; gap: 10px; padding: 11px 15px;
3372
+ border-radius: 16px 16px 5px 16px; border: 1px solid var(--oc-glass-border); background: var(--oc-bg);
3373
+ box-shadow: 0 10px 30px -14px rgba(0,0,0,0.5); color: var(--oc-fg); font-size: 13.5px; line-height: 1.45;
3374
+ text-align: left; cursor: pointer; transition: transform .2s var(--oc-ease);
3375
+ }
3376
+ .teaser-btn:hover { transform: translateY(-1px); }
3377
+ .teaser-btn .logo { flex: none; width: 24px; height: 24px; border-radius: 6px; overflow: hidden; color: var(--oc-ac); }
3378
+ .teaser-x {
3379
+ flex: none; width: 22px; height: 22px; border-radius: var(--oc-r-full); border: 1px solid var(--oc-border);
3380
+ background: var(--oc-bg); color: var(--oc-fg-dim); display: flex; align-items: center; justify-content: center; cursor: pointer;
3381
+ }
3382
+ .teaser-x:hover { color: var(--oc-fg); }
3383
+
3384
+ /* \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 */
3315
3385
  .panel {
3316
- position: absolute; right: 0; bottom: 74px; width: 384px; height: min(600px, calc(100vh - 120px));
3317
- background: var(--oc-surface); color: var(--oc-fg); border: 1px solid var(--oc-border);
3318
- border-radius: var(--oc-radius); box-shadow: 0 12px 48px rgba(0,0,0,.22);
3319
- display: flex; flex-direction: column; overflow: hidden;
3320
- transform-origin: bottom right; transition: transform .16s ease, opacity .16s ease;
3386
+ position: relative; width: min(384px, calc(100vw - 40px)); height: min(560px, calc(100vh - 108px));
3387
+ display: flex; flex-direction: column; border-radius: 20px; border: 1px solid var(--oc-glass-border);
3388
+ 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);
3389
+ overflow: hidden; transform-origin: bottom right;
3390
+ opacity: 0; transform: translateY(16px) scale(0.94); pointer-events: none; visibility: hidden;
3391
+ transition: opacity .24s var(--oc-ease), transform .34s var(--oc-bounce), visibility .34s, width .3s var(--oc-ease), height .3s var(--oc-ease);
3321
3392
  }
3322
- .panel[hidden] { display: none; }
3323
- .panel.enter { opacity: 0; transform: translateY(8px) scale(.98); }
3393
+ .oc.open .panel { opacity: 1; transform: none; pointer-events: auto; visibility: visible; }
3394
+ .oc.max .panel { width: min(720px, calc(100vw - 40px)); height: calc(100vh - 108px); }
3324
3395
 
3325
- .hd { display: flex; align-items: center; gap: 8px; padding: 14px 16px; border-bottom: 1px solid var(--oc-border); }
3326
- .hd .title { font-weight: 700; font-size: 15px; flex: 1; }
3327
- .hd .close { background: none; border: none; cursor: pointer; color: var(--oc-muted); padding: 4px; border-radius: 6px; display: grid; place-items: center; }
3328
- .hd .close:hover { background: var(--oc-surface-2); }
3329
- .hd .close svg { width: 18px; height: 18px; }
3396
+ .hd { position: relative; flex: none; display: flex; align-items: center; gap: 12px; padding: 13px 14px 12px; background: var(--oc-ac); color: #fff; overflow: hidden; }
3397
+ .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; }
3398
+ .hd .logo { position: relative; flex: none; width: 32px; height: 32px; border-radius: 7px; overflow: hidden; color: rgba(255,255,255,0.22); }
3399
+ .hd-txt { position: relative; flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
3400
+ .hd-title { font-size: 14.5px; font-weight: 600; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
3401
+ .hd-status { display: flex; align-items: center; gap: 6px; font-size: 11.5px; opacity: 0.9; }
3402
+ .hd-status .dot { width: 6px; height: 6px; border-radius: var(--oc-r-full); background: #7BE3A6; flex: none; }
3403
+ .hd-status .t { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
3404
+ .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); }
3405
+ .hd-btn:hover { background: rgba(255,255,255,0.28); }
3406
+ .hd-btn .i { position: absolute; display: flex; transition: opacity .2s var(--oc-ease); }
3407
+ .i-expand { opacity: 1; } .i-shrink { opacity: 0; }
3408
+ .oc.max .i-expand { opacity: 0; } .oc.max .i-shrink { opacity: 1; }
3330
3409
 
3331
- .status { padding: 6px 16px; font-size: 12px; color: var(--oc-muted); background: #f9fafb; border-bottom: 1px solid var(--oc-border); }
3332
- .err { padding: 8px 16px; font-size: 12px; color: #b91c1c; background: #fef2f2; border-bottom: 1px solid #fecaca; display: flex; align-items: center; gap: 8px; }
3333
- .err .retry { padding: 3px 10px; font-size: 11px; background: #b91c1c; color: #fff; border: none; border-radius: 6px; cursor: pointer; }
3410
+ .err { flex: none; display: flex; align-items: center; gap: 8px; padding: 8px 16px; font-size: 12px; color: #fff; background: var(--oc-danger); }
3411
+ .err .retry { padding: 3px 10px; font-size: 11px; background: rgba(255,255,255,0.2); color: #fff; border: none; border-radius: 6px; cursor: pointer; }
3334
3412
 
3335
- /* Message list */
3336
- .body { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 10px; scroll-behavior: smooth; }
3337
- .b { max-width: 85%; padding: 9px 13px; border-radius: 14px; font-size: 14px; line-height: 1.5; word-break: break-word; }
3338
- .b.user { align-self: flex-end; background: var(--oc-accent); color: #fff; border-bottom-right-radius: 4px; }
3339
- .b.assistant { align-self: flex-start; background: var(--oc-surface-2); color: var(--oc-fg); border-bottom-left-radius: 4px; }
3340
- .b.human { align-self: flex-start; background: #dbeafe; color: var(--oc-fg); border-bottom-left-radius: 4px; }
3341
- .b.typing { font-style: italic; color: var(--oc-muted); }
3413
+ /* \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 */
3414
+ .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; }
3415
+ .turn { display: flex; flex-direction: column; gap: 6px; animation: w-in .42s var(--oc-ease) both; }
3416
+ .turn.bot { align-items: flex-start; } .turn.user { align-items: flex-end; }
3417
+ .b { font-size: 13.5px; line-height: 1.55; text-wrap: pretty; word-break: break-word; }
3418
+ .turn.bot .b { max-width: 86%; padding: 13px 15px; border-radius: 20px; background: var(--oc-tint-2); color: var(--oc-fg); }
3419
+ .turn.user .b { max-width: 82%; padding: 11px 15px; border-radius: var(--oc-r-full); background: var(--oc-ac); color: #fff; }
3420
+ .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); }
3421
+ .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%; }
3422
+ .src .t { min-width: 0; font-size: 11.5px; color: var(--oc-fg-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3423
+ .byline { display: flex; align-items: center; gap: 6px; padding: 0 4px; font-size: 11.5px; color: var(--oc-fg-dim); }
3342
3424
  .human-label { font-size: 11px; font-weight: 600; opacity: .7; margin-bottom: 2px; }
3343
3425
 
3344
- /* Markdown inside a bubble */
3345
- .md > :first-child { margin-top: 0; }
3346
- .md > :last-child { margin-bottom: 0; }
3347
- .md p { margin: 0 0 8px; }
3348
- .md ul, .md ol { margin: 0 0 8px; padding-left: 20px; }
3349
- .md li { margin: 2px 0; }
3350
- .md a { color: inherit; text-decoration: underline; }
3351
- .b.assistant .md a, .b.human .md a { color: var(--oc-accent); }
3352
- .md code { font-family: ui-monospace, monospace; font-size: 12.5px; background: rgba(0,0,0,.07); padding: 1px 5px; border-radius: 5px; }
3353
- .md pre { background: #0f172a; color: #e2e8f0; padding: 10px 12px; border-radius: 8px; overflow-x: auto; margin: 0 0 8px; }
3354
- .md pre code { background: none; padding: 0; color: inherit; }
3355
- .md blockquote { margin: 0 0 8px; padding-left: 10px; border-left: 3px solid var(--oc-border); color: var(--oc-muted); }
3426
+ /* markdown inside a bot bubble */
3427
+ .md > :first-child { margin-top: 0; } .md > :last-child { margin-bottom: 0; }
3428
+ .md p { margin: 0 0 8px; } .md ul, .md ol { margin: 0 0 8px; padding-left: 20px; } .md li { margin: 2px 0; }
3429
+ .md a { color: var(--oc-accent-text); text-decoration: underline; text-underline-offset: 2px; }
3430
+ .md code { font-family: var(--oc-mono); font-size: 12.5px; background: var(--oc-tint-3); padding: 1px 5px; border-radius: 5px; }
3431
+ .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; }
3432
+ .md pre code { background: none; padding: 0; }
3433
+ .md blockquote { margin: 0 0 8px; padding-left: 10px; border-left: 3px solid var(--oc-border); color: var(--oc-fg-muted); }
3356
3434
  .md h1, .md h2, .md h3, .md h4 { margin: 8px 0 6px; font-size: 15px; }
3357
3435
  .md table { border-collapse: collapse; font-size: 13px; margin: 0 0 8px; display: block; overflow-x: auto; }
3358
3436
  .md th, .md td { border: 1px solid var(--oc-border); padding: 4px 8px; }
3359
- .md hr { border: none; border-top: 1px solid var(--oc-border); margin: 8px 0; }
3360
-
3361
- .src-row { align-self: flex-start; display: flex; flex-wrap: wrap; gap: 6px; max-width: 85%; }
3362
- .src-chip { font-size: 11px; background: rgba(0,0,0,.06); color: #374151; border-radius: 6px; padding: 2px 8px; cursor: default; }
3363
3437
  .att-thumb { display: block; max-width: 180px; max-height: 180px; border-radius: 8px; margin-bottom: 6px; }
3364
- .att-chip { display: inline-block; font-size: 12px; background: rgba(0,0,0,.08); border-radius: 6px; padding: 2px 8px; margin: 0 0 6px; }
3365
- .b.user .att-chip { background: rgba(255,255,255,.25); }
3438
+ .att-chip { display: inline-block; font-size: 12px; background: var(--oc-tint-3); border-radius: 6px; padding: 2px 8px; margin: 0 0 6px; }
3439
+ .turn.user .att-chip { background: rgba(255,255,255,0.25); }
3366
3440
 
3367
- /* New-messages pill */
3368
- .pill {
3369
- position: absolute; left: 50%; transform: translateX(-50%); bottom: 78px;
3370
- background: var(--oc-accent); color: #fff; border: none; cursor: pointer;
3371
- font-size: 12px; font-weight: 600; padding: 6px 12px; border-radius: 999px;
3372
- box-shadow: 0 4px 14px rgba(0,0,0,.25); display: flex; align-items: center; gap: 6px;
3373
- }
3374
- .pill[hidden] { display: none; }
3375
- .pill svg { width: 13px; height: 13px; }
3441
+ .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; }
3442
+ .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; }
3443
+ .typing span:nth-child(2) { animation-delay: .15s; } .typing span:nth-child(3) { animation-delay: .3s; }
3376
3444
 
3377
- /* Composer */
3378
- form { display: flex; gap: 8px; padding: 10px; border-top: 1px solid var(--oc-border); align-items: flex-end; }
3379
- textarea {
3380
- flex: 1; resize: none; max-height: 120px; padding: 9px 11px; border-radius: 10px;
3381
- border: 1px solid #d1d5db; font: inherit; font-size: 14px; line-height: 1.4; outline: none;
3382
- }
3383
- textarea:focus { border-color: var(--oc-accent); }
3384
- .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; }
3385
- .send:disabled, textarea:disabled { opacity: .55; cursor: not-allowed; }
3445
+ .chips { display: flex; flex-wrap: wrap; gap: 7px; padding-top: 2px; animation: w-in .42s var(--oc-ease) both; animation-delay: .3s; }
3446
+ .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); }
3447
+ .chip:hover { background: color-mix(in oklab, var(--oc-ac) 18%, transparent); }
3448
+
3449
+ /* \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 */
3450
+ .jump-wrap { position: relative; flex: none; height: 0; z-index: 2; display: flex; justify-content: center; }
3451
+ .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); }
3452
+ .oc.away .jump { opacity: 1; transform: none; pointer-events: auto; }
3453
+ .jump:hover { color: var(--oc-fg); border-color: var(--oc-fg-dim); }
3454
+
3455
+ /* \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 */
3456
+ .composer { flex: none; display: flex; flex-direction: column; gap: 9px; padding: 12px 14px 13px; background: var(--oc-bg); }
3457
+ .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); }
3458
+ .oc.focus .field { border-color: var(--oc-ac); }
3459
+ .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; }
3460
+ .input::placeholder { color: var(--oc-fg-dim); }
3461
+ .acts { display: flex; align-items: center; gap: 16px; }
3462
+ .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); }
3463
+ .act:hover { color: var(--oc-fg-muted); }
3464
+ .oc.emoji .act.emoji-btn { color: var(--oc-accent-text); }
3465
+ .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); }
3466
+ .oc.ready .send { border-color: var(--oc-btn-rim); background: var(--oc-ac); color: #fff; box-shadow: var(--oc-btn-edge); cursor: pointer; }
3467
+ .oc.ready .send:hover { filter: brightness(1.1); }
3468
+ .send:active { transform: scale(0.92); }
3469
+
3470
+ .foot { position: relative; display: none; text-align: center; font-size: 11.5px; line-height: 1.5; color: var(--oc-fg-dim); }
3471
+ .oc.has-privacy .foot { display: block; }
3472
+ .foot .priv { color: var(--oc-fg-muted); text-decoration: underline; text-underline-offset: 2px; cursor: default; }
3473
+ .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); }
3474
+ .oc.max .foot .tip { right: 188px; }
3475
+ .foot:hover .tip { opacity: 1; }
3476
+ .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); }
3477
+ .foot:hover .tip-card { transform: none; }
3478
+ .foot .tip-card a { color: var(--oc-fg); text-decoration: underline; text-underline-offset: 2px; }
3479
+
3480
+ /* \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 */
3481
+ .emoji-scrim { position: absolute; inset: 0; z-index: 3; display: none; }
3482
+ .oc.emoji .emoji-scrim { display: block; }
3483
+ .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; }
3484
+ .oc.emoji .emoji-panel { opacity: 1; transform: none; pointer-events: auto; visibility: visible; }
3485
+ .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); }
3486
+ .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; }
3487
+ .emoji-search input::placeholder { color: var(--oc-fg-dim); }
3488
+ .emoji-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding: 12px 14px 16px; }
3489
+ .emoji-grp { display: flex; flex-direction: column; gap: 6px; }
3490
+ .emoji-grp + .emoji-grp { padding-top: 8px; }
3491
+ .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); }
3492
+ .emoji-grid { display: grid; grid-template-columns: repeat(7, minmax(0,1fr)); gap: 2px; }
3493
+ .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); }
3494
+ .emoji-grid button:hover { background: var(--oc-tint-3); }
3495
+ .emoji-empty { display: none; flex-direction: column; align-items: center; gap: 5px; padding: 26px 10px; text-align: center; }
3496
+ .emoji-empty .h { font-size: 14px; color: var(--oc-fg); } .emoji-empty .s { font-size: 12.5px; color: var(--oc-fg-dim); }
3386
3497
 
3387
3498
  @media (max-width: 480px) {
3388
3499
  :host { right: 0; bottom: 0; }
3389
- .panel { position: fixed; inset: 0; width: 100vw; height: 100dvh; border-radius: 0; bottom: 0; }
3390
- .launcher { position: fixed; right: 16px; bottom: 16px; }
3391
- .pill { bottom: 82px; }
3500
+ .oc { padding: 0 16px 16px; }
3501
+ .panel { position: fixed; inset: 0; width: 100vw; height: 100dvh; border-radius: 0; }
3502
+ .oc.max .panel { width: 100vw; height: 100dvh; }
3392
3503
  }
3393
3504
  @media (prefers-reduced-motion: reduce) {
3394
- .launcher, .panel { transition: none; }
3395
- .body { scroll-behavior: auto; }
3505
+ .launcher, .panel, .teaser, .turn, .jump, .emoji-panel { transition: none; animation: none; }
3506
+ .ring { animation: none; } .body { scroll-behavior: auto; }
3396
3507
  }
3508
+
3509
+ @keyframes w-in { from { opacity: 0; transform: translateY(10px) scale(0.97); } to { opacity: 1; transform: none; } }
3510
+ @keyframes w-dot { 0%,60%,100% { transform: translateY(0); opacity: .45; } 30% { transform: translateY(-4px); opacity: 1; } }
3511
+ @keyframes w-ring { 0% { transform: scale(1); opacity: .5; } 70%,100% { transform: scale(1.7); opacity: 0; } }
3397
3512
  `;
3398
3513
 
3514
+ // src/icons.ts
3515
+ 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>`;
3516
+ 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>`;
3517
+ var ICON = {
3518
+ close: (size = 19) => stroke(2.4, '<path d="M18 6 6 18M6 6l12 12"/>', size),
3519
+ minus: stroke(2, '<path d="M6 12h12"/>', 15),
3520
+ expand: stroke(2, '<path d="M15 4h5v5M20 4l-6.5 6.5M9 20H4v-5M4 20l6.5-6.5"/>', 15),
3521
+ shrink: stroke(2, '<path d="M20 9h-5V4M15 9l5.5-5.5M4 15h5v5M9 15l-5.5 5.5"/>', 15),
3522
+ chevronDown: stroke(2.1, '<path d="m6 9 6 6 6-6"/>', 16),
3523
+ send: stroke(2.2, '<path d="M12 19V5M6 11l6-6 6 6"/>', 16),
3524
+ 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),
3525
+ 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),
3526
+ 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),
3527
+ search: stroke(1.9, '<circle cx="11" cy="11" r="7.5"/><path d="m21 21-4.3-4.3"/>', 17),
3528
+ clear: stroke(2.2, '<path d="M18 6 6 18M6 6l12 12"/>', 15),
3529
+ // Two-page "source" glyph for citation chips.
3530
+ 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>`,
3531
+ teaserDismiss: stroke(2.4, '<path d="M18 6 6 18M6 6l12 12"/>', 12)
3532
+ };
3533
+
3534
+ // src/emoji.ts
3535
+ var CATS = [
3536
+ { name: "Frequently used", items: [
3537
+ ["\u{1F44D}", "thumbs up"],
3538
+ ["\u{1F64F}", "thanks"],
3539
+ ["\u{1F44B}", "wave"],
3540
+ ["\u2705", "done"],
3541
+ ["\u{1F4DE}", "call"],
3542
+ ["\u{1F60A}", "smile"],
3543
+ ["\u2764\uFE0F", "heart"]
3544
+ ] },
3545
+ { name: "Smileys and emotion", items: [
3546
+ ["\u{1F600}", "grinning"],
3547
+ ["\u{1F603}", "smiley"],
3548
+ ["\u{1F604}", "grin"],
3549
+ ["\u{1F601}", "beaming"],
3550
+ ["\u{1F606}", "squinting"],
3551
+ ["\u{1F605}", "relief"],
3552
+ ["\u{1F923}", "rolling"],
3553
+ ["\u{1F602}", "tears of joy"],
3554
+ ["\u{1F642}", "slightly smiling"],
3555
+ ["\u{1F643}", "upside down"],
3556
+ ["\u{1F609}", "wink"],
3557
+ ["\u{1F607}", "halo"],
3558
+ ["\u{1F970}", "smiling hearts"],
3559
+ ["\u{1F60D}", "heart eyes"],
3560
+ ["\u{1F618}", "kiss"],
3561
+ ["\u{1F60B}", "yum"],
3562
+ ["\u{1F914}", "thinking"],
3563
+ ["\u{1F928}", "raised brow"],
3564
+ ["\u{1F610}", "neutral"],
3565
+ ["\u{1F644}", "eye roll"],
3566
+ ["\u{1F62C}", "grimace"],
3567
+ ["\u{1F625}", "sad"],
3568
+ ["\u{1F622}", "crying"],
3569
+ ["\u{1F62D}", "sobbing"],
3570
+ ["\u{1F620}", "angry"],
3571
+ ["\u{1F633}", "flushed"],
3572
+ ["\u{1F929}", "star struck"],
3573
+ ["\u{1F634}", "sleeping"]
3574
+ ] },
3575
+ { name: "Gestures", items: [
3576
+ ["\u{1F44E}", "thumbs down"],
3577
+ ["\u{1F44F}", "clap"],
3578
+ ["\u{1F64C}", "raised hands"],
3579
+ ["\u{1F91D}", "handshake"],
3580
+ ["\u{1F44C}", "ok hand"],
3581
+ ["\u270C\uFE0F", "victory"],
3582
+ ["\u{1F4AA}", "strong"],
3583
+ ["\u{1F440}", "eyes"],
3584
+ ["\u{1F9E0}", "brain"],
3585
+ ["\u{1F91F}", "love you"],
3586
+ ["\u261D\uFE0F", "point up"],
3587
+ ["\u{1F447}", "point down"],
3588
+ ["\u{1F91A}", "raised back"],
3589
+ ["\u{1F91E}", "fingers crossed"]
3590
+ ] },
3591
+ { name: "Work and travel", items: [
3592
+ ["\u{1F4C5}", "calendar"],
3593
+ ["\u{1F4C4}", "document"],
3594
+ ["\u{1F4B0}", "money"],
3595
+ ["\u{1F4CA}", "chart"],
3596
+ ["\u{1F4E6}", "package"],
3597
+ ["\u{1F3E5}", "hospital"],
3598
+ ["\u{1F3E6}", "bank"],
3599
+ ["\u{1F69A}", "truck"],
3600
+ ["\u2708\uFE0F", "plane"],
3601
+ ["\u{1F4F1}", "phone"],
3602
+ ["\u{1F4BB}", "laptop"],
3603
+ ["\u{1F50D}", "search"],
3604
+ ["\u{1F4CC}", "pin"],
3605
+ ["\u{1F514}", "bell"]
3606
+ ] },
3607
+ { name: "Symbols", items: [
3608
+ ["\u{1F525}", "fire"],
3609
+ ["\u2728", "sparkles"],
3610
+ ["\u{1F389}", "celebrate"],
3611
+ ["\u{1F4AF}", "hundred"],
3612
+ ["\u26A0\uFE0F", "warning"],
3613
+ ["\u274C", "cross"],
3614
+ ["\u2B50", "star"],
3615
+ ["\u{1F680}", "launch"],
3616
+ ["\u{1F49A}", "green heart"],
3617
+ ["\u{1F499}", "blue heart"],
3618
+ ["\u{1F552}", "clock"],
3619
+ ["\u{1F4A1}", "idea"],
3620
+ ["\u{1F512}", "lock"],
3621
+ ["\u{1F3AF}", "target"]
3622
+ ] }
3623
+ ];
3624
+ var toGroup = (name, items) => ({
3625
+ name,
3626
+ items: items.map(([ch, kw]) => ({ ch, name: kw }))
3627
+ });
3628
+ function emojiGroups(query) {
3629
+ const q2 = query.trim().toLowerCase();
3630
+ if (!q2) return CATS.map((c) => toGroup(c.name, c.items));
3631
+ const hits = [];
3632
+ for (const c of CATS) for (const e of c.items) {
3633
+ if (e[1].indexOf(q2) >= 0 && !hits.some((h) => h[0] === e[0])) hits.push(e);
3634
+ }
3635
+ if (!hits.length) return [];
3636
+ return [toGroup(`${hits.length} result${hits.length === 1 ? "" : "s"}`, hits)];
3637
+ }
3638
+
3399
3639
  // src/element.ts
3400
- var HANDOFF_STATUS_TEXT = {
3640
+ var DEFAULT_STATUS = "Replies in a few seconds";
3641
+ var ATTACHMENT_ACCEPT = "image/*,application/pdf,text/plain,text/csv";
3642
+ var AWAY_THRESHOLD = 60;
3643
+ var HANDOFF_STATUS = {
3401
3644
  ai: null,
3402
3645
  awaiting_human: "Connecting you to a person\u2026",
3403
3646
  human: "You're chatting with a person"
3404
3647
  };
3405
- var ATTACHMENT_ACCEPT = "image/*,application/pdf,text/plain,text/csv";
3406
- var AGENT_TYPING_TEXT = "The agent is typing\u2026";
3407
- var PIN_THRESHOLD = 48;
3408
- 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>';
3409
- 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>';
3410
- 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>';
3411
- 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>';
3412
3648
  var OyehelloChatElement = class extends HTMLElement {
3413
- static observedAttributes = ["api-key", "agent-id", "base-url", "title"];
3649
+ static observedAttributes = ["api-key", "agent-id", "base-url", "title", "theme"];
3650
+ // ── live client state ──
3414
3651
  client = null;
3415
3652
  off = null;
3416
3653
  messages = [];
@@ -3419,35 +3656,36 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3419
3656
  handoffState = "ai";
3420
3657
  agentTyping = false;
3421
3658
  lastError = null;
3422
- // registerTools() may run before connectedCallback creates the client — buffer until it exists.
3423
3659
  pendingTools = {};
3424
3660
  pendingFiles = [];
3425
3661
  imagePreviewCache = /* @__PURE__ */ new Map();
3426
3662
  resolvedUrlCache = /* @__PURE__ */ new Map();
3427
- // Rendered-markdown cache for FINALIZED assistant/human messages (keyed by message id) so the whole
3428
- // history isn't re-parsed on every streamed token — only the live streaming message is re-rendered.
3429
3663
  mdCache = /* @__PURE__ */ new Map();
3430
- // Widget UI state.
3431
- open = false;
3664
+ receivedAt = /* @__PURE__ */ new Map();
3665
+ // ── widget UI state ──
3666
+ uiOpen = false;
3432
3667
  started = false;
3433
- // lazy: the session opens on first panel open, not on page load
3434
- pinned = true;
3435
- // view is at/near the bottom
3668
+ uiMax = false;
3669
+ emojiOpen = false;
3670
+ emojiQuery = "";
3671
+ away = false;
3672
+ // reader scrolled up
3436
3673
  unseen = 0;
3437
- // messages that arrived while scrolled-up or while closed
3674
+ focused = false;
3675
+ teaserActive = false;
3438
3676
  renderedCount = 0;
3439
- // message count at the last renderMessages()
3440
- // Shadow-DOM refs (assigned once in renderShell).
3441
- panelEl;
3677
+ renderedBotFinal = 0;
3678
+ timers = [];
3679
+ // ── shadow refs ──
3680
+ oc;
3442
3681
  bodyEl;
3443
3682
  textareaEl;
3444
- sendEl;
3445
3683
  fileEl;
3446
- pillEl;
3447
- badgeEl;
3684
+ titleEl;
3448
3685
  statusEl;
3449
3686
  errEl;
3450
- titleEl;
3687
+ emojiListEl;
3688
+ emojiSearchEl;
3451
3689
  connectedCallback() {
3452
3690
  if (!this.shadowRoot) this.attachShadow({ mode: "open" });
3453
3691
  const apiKey = this.getAttribute("api-key");
@@ -3464,18 +3702,42 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3464
3702
  }
3465
3703
  this.off = this.client.on((e) => this.onClientEvent(e));
3466
3704
  this.renderShell();
3467
- this.updateTitle();
3705
+ this.renderMessages();
3706
+ this.updateHeader();
3468
3707
  this.updateStatus();
3708
+ this.updateClasses();
3469
3709
  let wasOpen = false;
3470
3710
  try {
3471
- wasOpen = sessionStorage.getItem(this.openKey()) === "1";
3711
+ wasOpen = sessionStorage.getItem(this.key("open")) === "1";
3712
+ } catch {
3713
+ }
3714
+ if (wasOpen) {
3715
+ this.openPanel();
3716
+ return;
3717
+ }
3718
+ this.updateClasses(true);
3719
+ this.timers.push(setTimeout(() => {
3720
+ this.oc?.classList.remove("ring");
3721
+ }, 6e3));
3722
+ const teaserText = this.getAttribute("teaser");
3723
+ let dismissed = false;
3724
+ try {
3725
+ dismissed = sessionStorage.getItem(this.key("teaser")) === "0";
3472
3726
  } catch {
3473
3727
  }
3474
- if (wasOpen) this.openPanel();
3728
+ if (teaserText && !dismissed) {
3729
+ this.timers.push(setTimeout(() => {
3730
+ if (!this.uiOpen) {
3731
+ this.teaserActive = true;
3732
+ this.updateClasses();
3733
+ }
3734
+ }, 1400));
3735
+ }
3475
3736
  }
3476
3737
  disconnectedCallback() {
3477
3738
  this.off?.();
3478
3739
  void this.client?.end();
3740
+ for (const t of this.timers) clearTimeout(t);
3479
3741
  for (const url of this.imagePreviewCache.values()) {
3480
3742
  if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
3481
3743
  }
@@ -3485,61 +3747,121 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3485
3747
  if (this.client) this.client.registerTools(handlers);
3486
3748
  else Object.assign(this.pendingTools, handlers);
3487
3749
  }
3488
- openKey() {
3489
- return `oyehello-chat:open:${this.getAttribute("agent-id") ?? ""}`;
3750
+ key(k2) {
3751
+ return `oyehello-chat:${k2}:${this.getAttribute("agent-id") ?? ""}`;
3752
+ }
3753
+ get suggestions() {
3754
+ return (this.getAttribute("suggestions") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
3490
3755
  }
3491
3756
  // ── Shell (built once) ─────────────────────────────────────────────────────────────────────────
3492
3757
  renderShell() {
3493
3758
  const root = this.shadowRoot;
3759
+ const hasTeaser = !!this.getAttribute("teaser");
3760
+ const hasPrivacy = !!this.getAttribute("privacy-url");
3761
+ const teaserText = this.getAttribute("teaser") ?? "";
3494
3762
  root.innerHTML = `
3495
3763
  <style>${STYLES}</style>
3496
- <button class="launcher" part="launcher" aria-label="Open chat" aria-haspopup="dialog">
3497
- ${ICON_CHAT}<span class="badge" hidden></span>
3498
- </button>
3499
- <div class="panel" part="panel" role="dialog" aria-label="Chat" hidden>
3500
- <div class="hd"><span class="title">Chat</span><button class="close" aria-label="Close chat">${ICON_CLOSE}</button></div>
3501
- <div class="status" hidden></div>
3502
- <div class="err" hidden></div>
3503
- <div class="body"></div>
3504
- <button class="pill" type="button" hidden>${ICON_DOWN}<span class="pill-text">New messages</span></button>
3505
- <form>
3506
- <button type="button" class="attach" aria-label="Attach a file">${ICON_CLIP}</button>
3507
- <input type="file" accept="${ATTACHMENT_ACCEPT}" multiple hidden />
3508
- <textarea aria-label="message" rows="1" placeholder="Type a message\u2026"></textarea>
3509
- <button type="submit" class="send">Send</button>
3510
- </form>
3764
+ <div class="oc${hasTeaser ? " has-teaser" : ""}${hasPrivacy ? " has-privacy" : ""}">
3765
+ <div class="panel" role="dialog" aria-label="Chat">
3766
+ <div class="hd">
3767
+ <span class="hd-grid"></span>
3768
+ <span class="logo">${logoMark("currentColor")}</span>
3769
+ <span class="hd-txt"><span class="hd-title">Chat</span><span class="hd-status"><span class="dot"></span><span class="t"></span></span></span>
3770
+ <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>
3771
+ <button type="button" class="hd-btn min" aria-label="Minimise">${ICON.minus}</button>
3772
+ </div>
3773
+ <div class="err" hidden></div>
3774
+ <div class="body"></div>
3775
+ <span class="emoji-scrim"></span>
3776
+ <div class="emoji-panel">
3777
+ <div class="emoji-search">${ICON.search}<input type="text" placeholder="Search emoji\u2026" aria-label="Search emoji" /></div>
3778
+ <div class="emoji-list"></div>
3779
+ </div>
3780
+ <div class="jump-wrap"><button type="button" class="jump" aria-label="Jump to latest">${ICON.chevronDown}</button></div>
3781
+ <div class="composer">
3782
+ <form class="field">
3783
+ <textarea class="input" rows="1" aria-label="message" placeholder="Ask a question\u2026"></textarea>
3784
+ <div class="acts">
3785
+ <button type="button" class="act attach" aria-label="Attach a file">${ICON.paperclip}</button>
3786
+ <input type="file" accept="${ATTACHMENT_ACCEPT}" multiple hidden />
3787
+ <button type="button" class="act emoji-btn" aria-label="Insert emoji">${ICON.emoji}</button>
3788
+ <button type="button" class="act mic" aria-label="Send a voice note">${ICON.mic}</button>
3789
+ <button type="submit" class="send" aria-label="Send">${ICON.send}</button>
3790
+ </div>
3791
+ </form>
3792
+ <div class="foot">
3793
+ <span>By chatting with us, you agree to our </span><span class="priv">Privacy Policy</span>
3794
+ <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>
3795
+ </div>
3796
+ </div>
3797
+ </div>
3798
+
3799
+ <div class="teaser">
3800
+ <button type="button" class="teaser-btn"><span class="logo">${logoMark("currentColor")}</span><span>${escapeHtml(teaserText)}</span></button>
3801
+ <button type="button" class="teaser-x" aria-label="Dismiss">${ICON.teaserDismiss}</button>
3802
+ </div>
3803
+
3804
+ <button type="button" class="launcher" aria-label="Open chat">
3805
+ <span class="ring"></span>
3806
+ <span class="ic-chat">${logoMark("var(--oc-ac)")}</span>
3807
+ <span class="ic-close">${ICON.close(19)}</span>
3808
+ <span class="badge"></span>
3809
+ </button>
3511
3810
  </div>`;
3512
- this.panelEl = root.querySelector(".panel");
3811
+ this.oc = root.querySelector(".oc");
3513
3812
  this.bodyEl = root.querySelector(".body");
3514
- this.textareaEl = root.querySelector("textarea");
3515
- this.sendEl = root.querySelector(".send");
3813
+ this.textareaEl = root.querySelector("textarea.input");
3516
3814
  this.fileEl = root.querySelector('input[type="file"]');
3517
- this.pillEl = root.querySelector(".pill");
3518
- this.badgeEl = root.querySelector(".badge");
3519
- this.statusEl = root.querySelector(".status");
3815
+ this.titleEl = root.querySelector(".hd-title");
3816
+ this.statusEl = root.querySelector(".hd-status .t");
3520
3817
  this.errEl = root.querySelector(".err");
3521
- this.titleEl = root.querySelector(".title");
3522
- root.querySelector(".launcher").addEventListener("click", () => this.togglePanel());
3523
- root.querySelector(".close").addEventListener("click", () => this.closePanel());
3818
+ this.emojiListEl = root.querySelector(".emoji-list");
3819
+ this.emojiSearchEl = root.querySelector(".emoji-search input");
3820
+ root.querySelector(".launcher").addEventListener("click", () => this.toggle());
3821
+ root.querySelector(".hd-btn.min").addEventListener("click", () => this.closePanel());
3822
+ root.querySelector(".hd-btn.max").addEventListener("click", () => {
3823
+ this.uiMax = !this.uiMax;
3824
+ this.updateClasses();
3825
+ });
3826
+ root.querySelector(".teaser-btn").addEventListener("click", () => this.openPanel());
3827
+ root.querySelector(".teaser-x").addEventListener("click", () => this.dismissTeaser());
3828
+ root.querySelector(".jump").addEventListener("click", () => this.scrollToBottom());
3524
3829
  root.querySelector(".attach").addEventListener("click", () => this.fileEl.click());
3525
3830
  this.fileEl.addEventListener("change", () => {
3526
3831
  this.pendingFiles = this.fileEl.files ? Array.from(this.fileEl.files) : [];
3527
3832
  });
3528
- this.pillEl.addEventListener("click", () => this.scrollToBottom());
3529
- this.bodyEl.addEventListener("scroll", () => this.onBodyScroll());
3530
- root.querySelector("form").addEventListener("submit", (e) => {
3833
+ root.querySelector(".emoji-btn").addEventListener("click", () => this.toggleEmoji());
3834
+ root.querySelector(".emoji-scrim").addEventListener("click", () => this.closeEmoji());
3835
+ this.emojiSearchEl.addEventListener("input", () => {
3836
+ this.emojiQuery = this.emojiSearchEl.value;
3837
+ this.renderEmoji();
3838
+ });
3839
+ this.bodyEl.addEventListener("scroll", () => this.onScroll());
3840
+ root.querySelector("form.field").addEventListener("submit", (e) => {
3531
3841
  e.preventDefault();
3532
3842
  this.doSend();
3533
3843
  });
3534
- this.textareaEl.addEventListener("input", () => this.autoGrow());
3844
+ this.textareaEl.addEventListener("input", () => {
3845
+ this.autoGrow();
3846
+ this.updateClasses();
3847
+ });
3848
+ this.textareaEl.addEventListener("focus", () => {
3849
+ this.focused = true;
3850
+ this.updateClasses();
3851
+ });
3852
+ this.textareaEl.addEventListener("blur", () => {
3853
+ this.focused = false;
3854
+ this.updateClasses();
3855
+ });
3535
3856
  this.textareaEl.addEventListener("keydown", (e) => {
3536
3857
  if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
3537
3858
  e.preventDefault();
3538
3859
  this.doSend();
3539
3860
  }
3540
- });
3541
- this.panelEl.addEventListener("keydown", (e) => {
3542
- if (e.key === "Escape") this.closePanel();
3861
+ if (e.key === "Escape") {
3862
+ if (this.emojiOpen) this.closeEmoji();
3863
+ else this.closePanel();
3864
+ }
3543
3865
  });
3544
3866
  }
3545
3867
  renderError(msg) {
@@ -3547,37 +3869,62 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3547
3869
  console.error(msg);
3548
3870
  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>`;
3549
3871
  }
3872
+ // ── State-class sync ──────────────────────────────────────────────────────────────────────────
3873
+ updateClasses(initialRing = false) {
3874
+ if (!this.oc) return;
3875
+ const c = this.oc.classList;
3876
+ c.toggle("open", this.uiOpen);
3877
+ c.toggle("max", this.uiMax);
3878
+ c.toggle("emoji", this.emojiOpen);
3879
+ c.toggle("teaser", this.teaserActive);
3880
+ c.toggle("away", this.away);
3881
+ c.toggle("focus", this.focused);
3882
+ c.toggle("ready", (this.textareaEl?.value.trim().length ?? 0) > 0);
3883
+ c.toggle("unread", this.unseen > 0);
3884
+ if (initialRing) c.add("ring");
3885
+ const badge = this.oc.querySelector(".badge");
3886
+ if (badge) badge.textContent = this.unseen > 9 ? "9+" : String(this.unseen);
3887
+ const launcher = this.oc.querySelector(".launcher");
3888
+ if (launcher) launcher.setAttribute("aria-label", this.uiOpen ? "Close chat" : "Open chat");
3889
+ }
3550
3890
  // ── Open / close ───────────────────────────────────────────────────────────────────────────────
3551
- togglePanel() {
3552
- this.open ? this.closePanel() : this.openPanel();
3891
+ toggle() {
3892
+ this.uiOpen ? this.closePanel() : this.openPanel();
3553
3893
  }
3554
3894
  openPanel() {
3555
- this.open = true;
3895
+ this.uiOpen = true;
3896
+ this.teaserActive = false;
3897
+ this.unseen = 0;
3556
3898
  try {
3557
- sessionStorage.setItem(this.openKey(), "1");
3899
+ sessionStorage.setItem(this.key("open"), "1");
3558
3900
  } catch {
3559
3901
  }
3560
- this.panelEl.hidden = false;
3561
- this.panelEl.classList.add("enter");
3562
- requestAnimationFrame(() => this.panelEl.classList.remove("enter"));
3563
3902
  if (!this.started && this.client) {
3564
3903
  this.started = true;
3565
3904
  void this.client.start();
3566
3905
  }
3906
+ this.updateClasses();
3567
3907
  this.renderMessages();
3568
- this.pinned = true;
3569
- this.unseen = 0;
3570
- this.updateBadge();
3571
3908
  this.scrollToBottom();
3572
- if (!this.textareaEl.disabled) this.textareaEl.focus();
3909
+ if (!this.textareaEl.disabled) setTimeout(() => this.textareaEl.focus(), 60);
3573
3910
  }
3574
3911
  closePanel() {
3575
- this.open = false;
3912
+ this.uiOpen = false;
3913
+ this.uiMax = false;
3914
+ this.emojiOpen = false;
3915
+ try {
3916
+ sessionStorage.setItem(this.key("open"), "0");
3917
+ } catch {
3918
+ }
3919
+ this.updateClasses();
3920
+ }
3921
+ dismissTeaser() {
3922
+ this.teaserActive = false;
3576
3923
  try {
3577
- sessionStorage.setItem(this.openKey(), "0");
3924
+ sessionStorage.setItem(this.key("teaser"), "0");
3578
3925
  } catch {
3579
3926
  }
3580
- this.panelEl.hidden = true;
3927
+ this.updateClasses();
3581
3928
  }
3582
3929
  // ── Client events ─────────────────────────────────────────────────────────────────────────────
3583
3930
  onClientEvent(e) {
@@ -3587,24 +3934,23 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3587
3934
  if (e.state === "ready" || e.state === "opening") this.lastError = null;
3588
3935
  } else if (e.type === "agent") {
3589
3936
  this.agentName = e.agent.name;
3590
- this.updateTitle();
3937
+ this.updateHeader();
3591
3938
  } else if (e.type === "handoff") this.handoffState = e.state;
3592
3939
  else if (e.type === "agentTyping") this.agentTyping = e.active;
3593
3940
  else if (e.type === "error") this.lastError = e.error.message;
3594
3941
  this.updateStatus();
3595
3942
  this.renderMessages();
3596
3943
  }
3597
- updateTitle() {
3944
+ updateHeader() {
3598
3945
  if (this.titleEl) this.titleEl.textContent = this.getAttribute("title") ?? this.agentName ?? "Chat";
3599
3946
  }
3600
3947
  updateStatus() {
3601
3948
  if (!this.textareaEl) return;
3602
3949
  const disabled = this.state !== "ready";
3603
3950
  this.textareaEl.disabled = disabled;
3604
- this.sendEl.disabled = disabled;
3605
- const st2 = this.state === "opening" ? "Connecting\u2026" : HANDOFF_STATUS_TEXT[this.handoffState];
3606
- this.statusEl.hidden = !st2;
3607
- this.statusEl.textContent = st2 ?? "";
3951
+ const handoff = HANDOFF_STATUS[this.handoffState];
3952
+ const text2 = this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
3953
+ if (this.statusEl) this.statusEl.textContent = text2;
3608
3954
  if (this.state === "error" && this.lastError) {
3609
3955
  this.errEl.hidden = false;
3610
3956
  this.errEl.innerHTML = `<span>Couldn't start chat: ${escapeHtml(this.lastError)}</span><button class="retry" type="button">Retry</button>`;
@@ -3620,58 +3966,73 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3620
3966
  renderMessages() {
3621
3967
  if (!this.bodyEl) return;
3622
3968
  const body = this.bodyEl;
3623
- const atBottom = this.open && this.pinned;
3624
3969
  const prevTop = body.scrollTop;
3625
3970
  const added = this.messages.length - this.renderedCount;
3626
- const showTyping = this.agentTyping && (this.handoffState === "human" || this.handoffState === "awaiting_human");
3627
- let html2 = this.messages.map((m2) => this.bubbleHtml(m2)).join("");
3628
- if (showTyping) html2 += `<div class="b assistant typing">${escapeHtml(AGENT_TYPING_TEXT)}</div>`;
3971
+ const botFinal = this.messages.filter((m2) => m2.role === "assistant" && !m2.streaming).length;
3972
+ const newBotFinal = botFinal > this.renderedBotFinal;
3973
+ const now = Date.now();
3974
+ for (const m2 of this.messages) if (!this.receivedAt.has(m2.id)) this.receivedAt.set(m2.id, now);
3975
+ const lastBot = (() => {
3976
+ for (let i = this.messages.length - 1; i >= 0; i--) if (this.messages[i]?.role === "assistant") return i;
3977
+ return -1;
3978
+ })();
3979
+ let html2 = this.messages.map((m2, i) => this.turnHtml(m2, i === lastBot && !this.agentTyping)).join("");
3980
+ if (this.agentTyping) html2 += '<span class="typing"><span></span><span></span><span></span></span>';
3981
+ const sugg = this.suggestions;
3982
+ if (sugg.length > 0 && !this.agentTyping && this.state === "ready") {
3983
+ html2 += `<span class="chips">${sugg.map((s, i) => `<button type="button" class="chip" data-chip="${i}">${escapeHtml(s)}</button>`).join("")}</span>`;
3984
+ }
3629
3985
  body.innerHTML = html2;
3630
3986
  this.hydrateAttachmentImages();
3987
+ this.bindChips();
3631
3988
  this.renderedCount = this.messages.length;
3632
- if (atBottom) this.scrollToBottom();
3989
+ this.renderedBotFinal = botFinal;
3990
+ if (!this.uiOpen) {
3991
+ if (added > 0) this.unseen += added;
3992
+ this.updateClasses();
3993
+ return;
3994
+ }
3995
+ if (newBotFinal && !this.away) this.parkLastQuestion();
3996
+ else if (!this.away) this.scrollToBottom();
3633
3997
  else {
3634
- if (this.open) body.scrollTop = prevTop;
3998
+ body.scrollTop = prevTop;
3635
3999
  if (added > 0) this.unseen += added;
3636
4000
  }
3637
- this.updatePill();
3638
- this.updateBadge();
4001
+ this.updateClasses();
3639
4002
  }
3640
- onBodyScroll() {
4003
+ // After a reply lands, float the question that prompted it to the top so the answer reads from its
4004
+ // first line rather than its last (the design's behaviour).
4005
+ parkLastQuestion() {
4006
+ const users = this.bodyEl.querySelectorAll(".turn.user");
4007
+ const last = users[users.length - 1];
4008
+ if (!last) {
4009
+ this.scrollToBottom();
4010
+ return;
4011
+ }
4012
+ const top = Math.max(0, Math.min(last.offsetTop - 22, this.bodyEl.scrollHeight - this.bodyEl.clientHeight));
4013
+ this.bodyEl.scrollTo({ top, behavior: "smooth" });
4014
+ }
4015
+ onScroll() {
3641
4016
  const b2 = this.bodyEl;
3642
- const nearBottom = b2.scrollHeight - b2.scrollTop - b2.clientHeight < PIN_THRESHOLD;
3643
- this.pinned = nearBottom;
3644
- if (nearBottom) {
3645
- this.unseen = 0;
3646
- this.updateBadge();
4017
+ const away = b2.scrollHeight - b2.scrollTop - b2.clientHeight > AWAY_THRESHOLD;
4018
+ if (away !== this.away) {
4019
+ this.away = away;
4020
+ if (!away) this.unseen = 0;
4021
+ this.updateClasses();
3647
4022
  }
3648
- this.updatePill();
3649
4023
  }
3650
4024
  scrollToBottom() {
3651
4025
  if (!this.bodyEl) return;
3652
4026
  this.bodyEl.scrollTop = this.bodyEl.scrollHeight;
3653
- this.pinned = true;
4027
+ this.away = false;
3654
4028
  this.unseen = 0;
3655
- this.updatePill();
3656
- this.updateBadge();
3657
- }
3658
- updatePill() {
3659
- if (!this.pillEl) return;
3660
- this.pillEl.hidden = !(this.open && !this.pinned && this.unseen > 0);
3661
- const t = this.pillEl.querySelector(".pill-text");
3662
- if (t) t.textContent = this.unseen === 1 ? "1 new message" : `${this.unseen} new messages`;
3663
- }
3664
- updateBadge() {
3665
- if (!this.badgeEl) return;
3666
- const show = !this.open && this.unseen > 0;
3667
- this.badgeEl.hidden = !show;
3668
- this.badgeEl.textContent = this.unseen > 9 ? "9+" : String(this.unseen);
4029
+ this.updateClasses();
3669
4030
  }
3670
4031
  // ── Composer ──────────────────────────────────────────────────────────────────────────────────
3671
4032
  autoGrow() {
3672
4033
  const ta = this.textareaEl;
3673
4034
  ta.style.height = "auto";
3674
- ta.style.height = `${Math.min(ta.scrollHeight, 120)}px`;
4035
+ ta.style.height = `${Math.min(ta.scrollHeight, 108)}px`;
3675
4036
  }
3676
4037
  doSend() {
3677
4038
  const text2 = this.textareaEl.value;
@@ -3684,15 +4045,63 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3684
4045
  this.imagePreviewCache.set(f2.name, URL.createObjectURL(f2));
3685
4046
  }
3686
4047
  }
3687
- this.pinned = true;
4048
+ this.away = false;
4049
+ this.emojiOpen = false;
3688
4050
  void this.client?.sendMessage(text2, files.length > 0 ? files : void 0);
3689
4051
  this.textareaEl.value = "";
3690
4052
  this.textareaEl.style.height = "auto";
4053
+ this.updateClasses();
4054
+ }
4055
+ sendChip(text2) {
4056
+ this.away = false;
4057
+ this.emojiOpen = false;
4058
+ void this.client?.sendMessage(text2);
4059
+ this.updateClasses();
4060
+ }
4061
+ bindChips() {
4062
+ const sugg = this.suggestions;
4063
+ this.bodyEl.querySelectorAll(".chip[data-chip]").forEach((btn) => {
4064
+ const i = Number(btn.getAttribute("data-chip"));
4065
+ const text2 = sugg[i];
4066
+ if (text2) btn.addEventListener("click", () => this.sendChip(text2));
4067
+ });
4068
+ }
4069
+ // ── Emoji ─────────────────────────────────────────────────────────────────────────────────────
4070
+ toggleEmoji() {
4071
+ this.emojiOpen = !this.emojiOpen;
4072
+ this.emojiQuery = "";
4073
+ this.emojiSearchEl.value = "";
4074
+ this.renderEmoji();
4075
+ this.updateClasses();
4076
+ if (this.emojiOpen) setTimeout(() => this.emojiSearchEl.focus(), 60);
4077
+ }
4078
+ closeEmoji() {
4079
+ this.emojiOpen = false;
4080
+ this.emojiQuery = "";
4081
+ this.updateClasses();
4082
+ }
4083
+ renderEmoji() {
4084
+ const groups = emojiGroups(this.emojiQuery);
4085
+ let html2 = groups.map((g) => `
4086
+ <div class="emoji-grp"><span class="emoji-grp-name">${escapeHtml(g.name)}</span>
4087
+ <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>
4088
+ </div>`).join("");
4089
+ if (groups.length === 0) html2 = '<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>';
4090
+ this.emojiListEl.innerHTML = html2;
4091
+ this.emojiListEl.querySelectorAll("button[data-ch]").forEach((btn) => {
4092
+ btn.addEventListener("click", () => {
4093
+ this.textareaEl.value += btn.getAttribute("data-ch") ?? "";
4094
+ this.emojiOpen = false;
4095
+ this.autoGrow();
4096
+ this.updateClasses();
4097
+ this.textareaEl.focus();
4098
+ });
4099
+ });
3691
4100
  }
3692
4101
  // ── Bubble rendering ──────────────────────────────────────────────────────────────────────────
3693
- bubbleHtml(m2) {
4102
+ turnHtml(m2, isLastBot) {
4103
+ const role = m2.role === "user" ? "user" : m2.role === "human" ? "human" : "bot";
3694
4104
  const atts = (m2.attachments ?? []).map((a) => this.renderAttachment(a)).join("");
3695
- const label = m2.role === "human" ? '<div class="human-label">Support</div>' : "";
3696
4105
  let content;
3697
4106
  if (m2.role === "assistant") {
3698
4107
  let rendered = m2.streaming ? void 0 : this.mdCache.get(m2.id);
@@ -3700,30 +4109,32 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3700
4109
  rendered = renderMarkdown(m2.text || "");
3701
4110
  if (!m2.streaming) this.mdCache.set(m2.id, rendered);
3702
4111
  }
3703
- content = `<div class="md">${rendered}</div>`;
4112
+ content = `<span class="b">${atts}<span class="md">${rendered}</span></span>`;
3704
4113
  } else {
3705
- content = escapeHtml(m2.text);
4114
+ const label = m2.role === "human" ? '<div class="human-label">Support</div>' : "";
4115
+ content = `<span class="b">${label}${atts}${escapeHtml(m2.text)}</span>`;
3706
4116
  }
3707
- const streamingDots = m2.streaming ? '<span aria-hidden="true">\u2026</span>' : "";
3708
- const bubble = `<div class="b ${m2.role}">${label}${atts}${content}${streamingDots}</div>`;
3709
- const sources = m2.sources && m2.sources.length > 0 ? `<div class="src-row">${m2.sources.map((s) => this.renderSource(s)).join("")}</div>` : "";
3710
- return bubble + sources;
4117
+ const src = m2.sources && m2.sources.length > 0 ? `<span class="src">${ICON.book}<span class="t">${escapeHtml("From " + (m2.sources[0]?.title ?? ""))}</span></span>` : "";
4118
+ const byline = role === "bot" && isLastBot ? `<span class="byline">${escapeHtml((this.agentName ?? "OyeHello") + " \xB7 AI agent \xB7 " + this.relTime(m2.id))}</span>` : "";
4119
+ return `<span class="turn ${role}">${content}${src}${byline}</span>`;
4120
+ }
4121
+ relTime(id) {
4122
+ const t = this.receivedAt.get(id);
4123
+ if (!t) return "just now";
4124
+ const mins = Math.floor((Date.now() - t) / 6e4);
4125
+ return mins <= 0 ? "just now" : `${mins} min ago`;
3711
4126
  }
3712
4127
  renderAttachment(a) {
3713
4128
  if (a.contentType.startsWith("image/")) {
3714
4129
  const cached = (a.name ? this.imagePreviewCache.get(a.name) : void 0) ?? this.resolvedUrlCache.get(a.storageRef);
3715
4130
  const srcAttr = cached ? ` src="${cached}"` : "";
3716
- return `<img class="att-thumb" data-oc-ref="${escapeHtml(a.storageRef)}"${srcAttr} alt="${escapeHtml(a.name ?? "image")}"/>`;
4131
+ return `<img class="att-thumb" data-oc-ref="${this.escapeAttr(a.storageRef)}"${srcAttr} alt="${this.escapeAttr(a.name ?? "image")}"/>`;
3717
4132
  }
3718
4133
  return `<div class="att-chip">\u{1F4CE} ${escapeHtml(a.name ?? a.contentType)}</div>`;
3719
4134
  }
3720
- renderSource(s) {
3721
- return `<div class="src-chip" title="${escapeHtml(s.snippet)}">${escapeHtml(s.title)}</div>`;
3722
- }
3723
4135
  hydrateAttachmentImages() {
3724
4136
  if (!this.client) return;
3725
- const imgs = this.shadowRoot.querySelectorAll("img.att-thumb[data-oc-ref]");
3726
- imgs.forEach((img) => {
4137
+ this.shadowRoot.querySelectorAll("img.att-thumb[data-oc-ref]").forEach((img) => {
3727
4138
  if (img.hasAttribute("src")) return;
3728
4139
  const ref = img.getAttribute("data-oc-ref");
3729
4140
  if (!ref) return;
@@ -3734,6 +4145,9 @@ Please report this to https://github.com/markedjs/marked.`, e) {
3734
4145
  });
3735
4146
  });
3736
4147
  }
4148
+ escapeAttr(s) {
4149
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
4150
+ }
3737
4151
  };
3738
4152
  function escapeHtml(s) {
3739
4153
  return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);