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