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