@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.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { marked } from 'marked';
|
|
2
|
+
import DOMPurify from 'dompurify';
|
|
3
|
+
|
|
1
4
|
// src/errors.ts
|
|
2
5
|
var ChatApiError = class extends Error {
|
|
3
6
|
constructor(message, status, code, meta) {
|
|
@@ -157,7 +160,7 @@ function createChatClient(options) {
|
|
|
157
160
|
seenTurnIds.add(ev.turnId);
|
|
158
161
|
const m = streamingId ? messages.find((x) => x.id === streamingId) : void 0;
|
|
159
162
|
if (m) {
|
|
160
|
-
m.text = ev.text;
|
|
163
|
+
m.text = ev.text || m.text;
|
|
161
164
|
m.streaming = false;
|
|
162
165
|
m.turnId = ev.turnId;
|
|
163
166
|
} else {
|
|
@@ -279,7 +282,10 @@ function createChatClient(options) {
|
|
|
279
282
|
return getAttachmentUrl({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, storageRef, fetchImpl });
|
|
280
283
|
},
|
|
281
284
|
async start() {
|
|
282
|
-
if (state
|
|
285
|
+
if (state === "opening" || state === "ready") return;
|
|
286
|
+
closed = false;
|
|
287
|
+
clearReconnect();
|
|
288
|
+
reconnectAttempts = 0;
|
|
283
289
|
setState("opening");
|
|
284
290
|
try {
|
|
285
291
|
await open();
|
|
@@ -341,42 +347,465 @@ function createChatClient(options) {
|
|
|
341
347
|
}
|
|
342
348
|
};
|
|
343
349
|
}
|
|
350
|
+
marked.setOptions({ gfm: true, breaks: true });
|
|
351
|
+
var hookInstalled = false;
|
|
352
|
+
function installLinkHook() {
|
|
353
|
+
if (hookInstalled) return;
|
|
354
|
+
DOMPurify.addHook("afterSanitizeAttributes", (node) => {
|
|
355
|
+
if (node.nodeName === "A") {
|
|
356
|
+
node.setAttribute("target", "_blank");
|
|
357
|
+
node.setAttribute("rel", "noopener noreferrer nofollow");
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
hookInstalled = true;
|
|
361
|
+
}
|
|
362
|
+
var ALLOWED_TAGS = [
|
|
363
|
+
"p",
|
|
364
|
+
"br",
|
|
365
|
+
"strong",
|
|
366
|
+
"em",
|
|
367
|
+
"b",
|
|
368
|
+
"i",
|
|
369
|
+
"del",
|
|
370
|
+
"s",
|
|
371
|
+
"code",
|
|
372
|
+
"pre",
|
|
373
|
+
"blockquote",
|
|
374
|
+
"ul",
|
|
375
|
+
"ol",
|
|
376
|
+
"li",
|
|
377
|
+
"a",
|
|
378
|
+
"h1",
|
|
379
|
+
"h2",
|
|
380
|
+
"h3",
|
|
381
|
+
"h4",
|
|
382
|
+
"h5",
|
|
383
|
+
"h6",
|
|
384
|
+
"hr",
|
|
385
|
+
"table",
|
|
386
|
+
"thead",
|
|
387
|
+
"tbody",
|
|
388
|
+
"tr",
|
|
389
|
+
"th",
|
|
390
|
+
"td",
|
|
391
|
+
"span"
|
|
392
|
+
];
|
|
393
|
+
var ALLOWED_ATTR = ["href", "target", "rel", "class"];
|
|
394
|
+
function renderMarkdown(text) {
|
|
395
|
+
installLinkHook();
|
|
396
|
+
const raw = marked.parse(text, { async: false });
|
|
397
|
+
return DOMPurify.sanitize(raw, {
|
|
398
|
+
ALLOWED_TAGS,
|
|
399
|
+
ALLOWED_ATTR,
|
|
400
|
+
ALLOW_DATA_ATTR: false,
|
|
401
|
+
ALLOWED_URI_REGEXP: /^(?:https?:|mailto:)/i
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/styles.ts
|
|
406
|
+
var STYLES = `
|
|
407
|
+
:host {
|
|
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);
|
|
450
|
+
}
|
|
451
|
+
* { box-sizing: border-box; }
|
|
452
|
+
button { font-family: inherit; }
|
|
453
|
+
|
|
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 */
|
|
457
|
+
.launcher {
|
|
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;
|
|
463
|
+
}
|
|
464
|
+
.launcher:hover { transform: scale(1.06); }
|
|
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);
|
|
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); }
|
|
503
|
+
|
|
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 */
|
|
505
|
+
.panel {
|
|
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); }
|
|
515
|
+
|
|
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; }
|
|
529
|
+
|
|
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; }
|
|
532
|
+
|
|
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); }
|
|
544
|
+
.human-label { font-size: 11px; font-weight: 600; opacity: .7; margin-bottom: 2px; }
|
|
545
|
+
|
|
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); }
|
|
554
|
+
.md h1, .md h2, .md h3, .md h4 { margin: 8px 0 6px; font-size: 15px; }
|
|
555
|
+
.md table { border-collapse: collapse; font-size: 13px; margin: 0 0 8px; display: block; overflow-x: auto; }
|
|
556
|
+
.md th, .md td { border: 1px solid var(--oc-border); padding: 4px 8px; }
|
|
557
|
+
.att-thumb { display: block; max-width: 180px; max-height: 180px; border-radius: 8px; margin-bottom: 6px; }
|
|
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); }
|
|
568
|
+
|
|
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); }
|
|
574
|
+
|
|
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); }
|
|
617
|
+
|
|
618
|
+
@media (max-width: 480px) {
|
|
619
|
+
:host { right: 0; bottom: 0; }
|
|
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; }
|
|
623
|
+
}
|
|
624
|
+
@media (prefers-reduced-motion: reduce) {
|
|
625
|
+
.launcher, .panel, .teaser, .turn, .jump, .emoji-panel { transition: none; animation: none; }
|
|
626
|
+
.ring { animation: none; } .body { scroll-behavior: auto; }
|
|
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; } }
|
|
632
|
+
`;
|
|
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
|
+
}
|
|
344
758
|
|
|
345
759
|
// src/element.ts
|
|
346
|
-
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 = {
|
|
347
764
|
ai: null,
|
|
348
765
|
awaiting_human: "Connecting you to a person\u2026",
|
|
349
766
|
human: "You're chatting with a person"
|
|
350
767
|
};
|
|
351
|
-
var ATTACHMENT_ACCEPT = "image/*,application/pdf,text/plain,text/csv";
|
|
352
|
-
var AGENT_TYPING_TEXT = "The agent is typing\u2026";
|
|
353
768
|
var OyehelloChatElement = class extends HTMLElement {
|
|
354
|
-
static observedAttributes = ["api-key", "agent-id", "base-url", "title"];
|
|
769
|
+
static observedAttributes = ["api-key", "agent-id", "base-url", "title", "theme"];
|
|
770
|
+
// ── live client state ──
|
|
355
771
|
client = null;
|
|
356
772
|
off = null;
|
|
357
773
|
messages = [];
|
|
358
774
|
state = "idle";
|
|
359
775
|
agentName = null;
|
|
360
776
|
handoffState = "ai";
|
|
361
|
-
// Transient "agent is typing…" affordance (Phase 7 followups) - never a message, only rendered
|
|
362
|
-
// while a handoff is active (human/awaiting_human).
|
|
363
777
|
agentTyping = false;
|
|
364
|
-
// Last connection/runtime error message (client emits an 'error' event on a failed start() or
|
|
365
|
-
// send). Surfaced as a visible banner so a disabled composer always shows WHY, instead of sitting
|
|
366
|
-
// dead. Cleared when the client reaches a healthy state again.
|
|
367
778
|
lastError = null;
|
|
368
|
-
// registerTools() may be called before connectedCallback creates the client (e.g. right after
|
|
369
|
-
// document.createElement, before the element is attached). Buffer until the client exists.
|
|
370
779
|
pendingTools = {};
|
|
371
|
-
// Files picked in the file input, staged until the form submits alongside the text.
|
|
372
780
|
pendingFiles = [];
|
|
373
|
-
// Best-effort local thumbnail cache for images THIS widget instance just sent, keyed by filename -
|
|
374
|
-
// an instant blob src, no round-trip needed.
|
|
375
781
|
imagePreviewCache = /* @__PURE__ */ new Map();
|
|
376
|
-
// Read-back URLs resolved lazily (via client.getAttachmentUrl) for images with no local blob (a
|
|
377
|
-
// reconnect, or an `attachment` SSE echo), keyed by storageRef. Cached across re-renders so the
|
|
378
|
-
// same image isn't re-fetched on every render() call.
|
|
379
782
|
resolvedUrlCache = /* @__PURE__ */ new Map();
|
|
783
|
+
mdCache = /* @__PURE__ */ new Map();
|
|
784
|
+
receivedAt = /* @__PURE__ */ new Map();
|
|
785
|
+
// ── widget UI state ──
|
|
786
|
+
uiOpen = false;
|
|
787
|
+
started = false;
|
|
788
|
+
uiMax = false;
|
|
789
|
+
emojiOpen = false;
|
|
790
|
+
emojiQuery = "";
|
|
791
|
+
away = false;
|
|
792
|
+
// reader scrolled up
|
|
793
|
+
unseen = 0;
|
|
794
|
+
focused = false;
|
|
795
|
+
teaserActive = false;
|
|
796
|
+
renderedCount = 0;
|
|
797
|
+
renderedBotFinal = 0;
|
|
798
|
+
timers = [];
|
|
799
|
+
// ── shadow refs ──
|
|
800
|
+
oc;
|
|
801
|
+
bodyEl;
|
|
802
|
+
textareaEl;
|
|
803
|
+
fileEl;
|
|
804
|
+
titleEl;
|
|
805
|
+
statusEl;
|
|
806
|
+
errEl;
|
|
807
|
+
emojiListEl;
|
|
808
|
+
emojiSearchEl;
|
|
380
809
|
connectedCallback() {
|
|
381
810
|
if (!this.shadowRoot) this.attachShadow({ mode: "open" });
|
|
382
811
|
const apiKey = this.getAttribute("api-key");
|
|
@@ -391,23 +820,44 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
391
820
|
this.client.registerTools(this.pendingTools);
|
|
392
821
|
this.pendingTools = {};
|
|
393
822
|
}
|
|
394
|
-
this.off = this.client.on((e) =>
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
823
|
+
this.off = this.client.on((e) => this.onClientEvent(e));
|
|
824
|
+
this.renderShell();
|
|
825
|
+
this.renderMessages();
|
|
826
|
+
this.updateHeader();
|
|
827
|
+
this.updateStatus();
|
|
828
|
+
this.updateClasses();
|
|
829
|
+
let wasOpen = false;
|
|
830
|
+
try {
|
|
831
|
+
wasOpen = sessionStorage.getItem(this.key("open")) === "1";
|
|
832
|
+
} catch {
|
|
833
|
+
}
|
|
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
|
+
}
|
|
407
856
|
}
|
|
408
857
|
disconnectedCallback() {
|
|
409
858
|
this.off?.();
|
|
410
859
|
void this.client?.end();
|
|
860
|
+
for (const t of this.timers) clearTimeout(t);
|
|
411
861
|
for (const url of this.imagePreviewCache.values()) {
|
|
412
862
|
if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
|
|
413
863
|
}
|
|
@@ -417,37 +867,394 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
417
867
|
if (this.client) this.client.registerTools(handlers);
|
|
418
868
|
else Object.assign(this.pendingTools, handlers);
|
|
419
869
|
}
|
|
420
|
-
|
|
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);
|
|
875
|
+
}
|
|
876
|
+
// ── Shell (built once) ─────────────────────────────────────────────────────────────────────────
|
|
877
|
+
renderShell() {
|
|
878
|
+
const root = this.shadowRoot;
|
|
879
|
+
const hasTeaser = !!this.getAttribute("teaser");
|
|
880
|
+
const hasPrivacy = !!this.getAttribute("privacy-url");
|
|
881
|
+
const teaserText = this.getAttribute("teaser") ?? "";
|
|
882
|
+
root.innerHTML = `
|
|
883
|
+
<style>${STYLES}</style>
|
|
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>
|
|
930
|
+
</div>`;
|
|
931
|
+
this.oc = root.querySelector(".oc");
|
|
932
|
+
this.bodyEl = root.querySelector(".body");
|
|
933
|
+
this.textareaEl = root.querySelector("textarea.input");
|
|
934
|
+
this.fileEl = root.querySelector('input[type="file"]');
|
|
935
|
+
this.titleEl = root.querySelector(".hd-title");
|
|
936
|
+
this.statusEl = root.querySelector(".hd-status .t");
|
|
937
|
+
this.errEl = root.querySelector(".err");
|
|
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());
|
|
949
|
+
root.querySelector(".attach").addEventListener("click", () => this.fileEl.click());
|
|
950
|
+
this.fileEl.addEventListener("change", () => {
|
|
951
|
+
this.pendingFiles = this.fileEl.files ? Array.from(this.fileEl.files) : [];
|
|
952
|
+
});
|
|
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) => {
|
|
961
|
+
e.preventDefault();
|
|
962
|
+
this.doSend();
|
|
963
|
+
});
|
|
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
|
+
});
|
|
976
|
+
this.textareaEl.addEventListener("keydown", (e) => {
|
|
977
|
+
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
|
978
|
+
e.preventDefault();
|
|
979
|
+
this.doSend();
|
|
980
|
+
}
|
|
981
|
+
if (e.key === "Escape") {
|
|
982
|
+
if (this.emojiOpen) this.closeEmoji();
|
|
983
|
+
else this.closePanel();
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
renderError(msg) {
|
|
988
|
+
if (!this.shadowRoot) this.attachShadow({ mode: "open" });
|
|
989
|
+
console.error(msg);
|
|
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>`;
|
|
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
|
+
}
|
|
1010
|
+
// ── Open / close ───────────────────────────────────────────────────────────────────────────────
|
|
1011
|
+
toggle() {
|
|
1012
|
+
this.uiOpen ? this.closePanel() : this.openPanel();
|
|
1013
|
+
}
|
|
1014
|
+
openPanel() {
|
|
1015
|
+
this.uiOpen = true;
|
|
1016
|
+
this.teaserActive = false;
|
|
1017
|
+
this.unseen = 0;
|
|
1018
|
+
try {
|
|
1019
|
+
sessionStorage.setItem(this.key("open"), "1");
|
|
1020
|
+
} catch {
|
|
1021
|
+
}
|
|
1022
|
+
if (!this.started && this.client) {
|
|
1023
|
+
this.started = true;
|
|
1024
|
+
void this.client.start();
|
|
1025
|
+
}
|
|
1026
|
+
this.updateClasses();
|
|
1027
|
+
this.renderMessages();
|
|
1028
|
+
this.scrollToBottom();
|
|
1029
|
+
if (!this.textareaEl.disabled) setTimeout(() => this.textareaEl.focus(), 60);
|
|
1030
|
+
}
|
|
1031
|
+
closePanel() {
|
|
1032
|
+
this.uiOpen = false;
|
|
1033
|
+
this.uiMax = false;
|
|
1034
|
+
this.emojiOpen = false;
|
|
1035
|
+
try {
|
|
1036
|
+
sessionStorage.setItem(this.key("open"), "0");
|
|
1037
|
+
} catch {
|
|
1038
|
+
}
|
|
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();
|
|
1048
|
+
}
|
|
1049
|
+
// ── Client events ─────────────────────────────────────────────────────────────────────────────
|
|
1050
|
+
onClientEvent(e) {
|
|
1051
|
+
if (e.type === "messages") this.messages = e.messages;
|
|
1052
|
+
else if (e.type === "state") {
|
|
1053
|
+
this.state = e.state;
|
|
1054
|
+
if (e.state === "ready" || e.state === "opening") this.lastError = null;
|
|
1055
|
+
} else if (e.type === "agent") {
|
|
1056
|
+
this.agentName = e.agent.name;
|
|
1057
|
+
this.updateHeader();
|
|
1058
|
+
} else if (e.type === "handoff") this.handoffState = e.state;
|
|
1059
|
+
else if (e.type === "agentTyping") this.agentTyping = e.active;
|
|
1060
|
+
else if (e.type === "error") this.lastError = e.error.message;
|
|
1061
|
+
this.updateStatus();
|
|
1062
|
+
this.renderMessages();
|
|
1063
|
+
}
|
|
1064
|
+
updateHeader() {
|
|
1065
|
+
if (this.titleEl) this.titleEl.textContent = this.getAttribute("title") ?? this.agentName ?? "Chat";
|
|
1066
|
+
}
|
|
1067
|
+
updateStatus() {
|
|
1068
|
+
if (!this.textareaEl) return;
|
|
1069
|
+
const disabled = this.state !== "ready";
|
|
1070
|
+
this.textareaEl.disabled = disabled;
|
|
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;
|
|
1074
|
+
if (this.state === "error" && this.lastError) {
|
|
1075
|
+
this.errEl.hidden = false;
|
|
1076
|
+
this.errEl.innerHTML = `<span>Couldn't start chat: ${escapeHtml(this.lastError)}</span><button class="retry" type="button">Retry</button>`;
|
|
1077
|
+
this.errEl.querySelector(".retry").addEventListener("click", () => {
|
|
1078
|
+
void this.client?.start();
|
|
1079
|
+
});
|
|
1080
|
+
} else {
|
|
1081
|
+
this.errEl.hidden = true;
|
|
1082
|
+
this.errEl.innerHTML = "";
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
// ── Message list + scroll ─────────────────────────────────────────────────────────────────────
|
|
1086
|
+
renderMessages() {
|
|
1087
|
+
if (!this.bodyEl) return;
|
|
1088
|
+
const body = this.bodyEl;
|
|
1089
|
+
const prevTop = body.scrollTop;
|
|
1090
|
+
const added = this.messages.length - this.renderedCount;
|
|
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
|
+
}
|
|
1105
|
+
body.innerHTML = html;
|
|
1106
|
+
this.hydrateAttachmentImages();
|
|
1107
|
+
this.bindChips();
|
|
1108
|
+
this.renderedCount = this.messages.length;
|
|
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();
|
|
1117
|
+
else {
|
|
1118
|
+
body.scrollTop = prevTop;
|
|
1119
|
+
if (added > 0) this.unseen += added;
|
|
1120
|
+
}
|
|
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" });
|
|
1134
|
+
}
|
|
1135
|
+
onScroll() {
|
|
1136
|
+
const b = this.bodyEl;
|
|
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();
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
scrollToBottom() {
|
|
1145
|
+
if (!this.bodyEl) return;
|
|
1146
|
+
this.bodyEl.scrollTop = this.bodyEl.scrollHeight;
|
|
1147
|
+
this.away = false;
|
|
1148
|
+
this.unseen = 0;
|
|
1149
|
+
this.updateClasses();
|
|
1150
|
+
}
|
|
1151
|
+
// ── Composer ──────────────────────────────────────────────────────────────────────────────────
|
|
1152
|
+
autoGrow() {
|
|
1153
|
+
const ta = this.textareaEl;
|
|
1154
|
+
ta.style.height = "auto";
|
|
1155
|
+
ta.style.height = `${Math.min(ta.scrollHeight, 108)}px`;
|
|
1156
|
+
}
|
|
1157
|
+
doSend() {
|
|
1158
|
+
const text = this.textareaEl.value;
|
|
421
1159
|
if (!text.trim()) return;
|
|
1160
|
+
const files = this.pendingFiles;
|
|
1161
|
+
this.pendingFiles = [];
|
|
1162
|
+
this.fileEl.value = "";
|
|
422
1163
|
for (const f of files) {
|
|
423
1164
|
if (f.type.startsWith("image/") && typeof URL.createObjectURL === "function" && !this.imagePreviewCache.has(f.name)) {
|
|
424
1165
|
this.imagePreviewCache.set(f.name, URL.createObjectURL(f));
|
|
425
1166
|
}
|
|
426
1167
|
}
|
|
1168
|
+
this.away = false;
|
|
1169
|
+
this.emojiOpen = false;
|
|
427
1170
|
void this.client?.sendMessage(text, files.length > 0 ? files : void 0);
|
|
1171
|
+
this.textareaEl.value = "";
|
|
1172
|
+
this.textareaEl.style.height = "auto";
|
|
1173
|
+
this.updateClasses();
|
|
428
1174
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
this.
|
|
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
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
// ── Bubble rendering ──────────────────────────────────────────────────────────────────────────
|
|
1222
|
+
turnHtml(m, isLastBot) {
|
|
1223
|
+
const role = m.role === "user" ? "user" : m.role === "human" ? "human" : "bot";
|
|
1224
|
+
const atts = (m.attachments ?? []).map((a) => this.renderAttachment(a)).join("");
|
|
1225
|
+
let content;
|
|
1226
|
+
if (m.role === "assistant") {
|
|
1227
|
+
let rendered = m.streaming ? void 0 : this.mdCache.get(m.id);
|
|
1228
|
+
if (rendered === void 0) {
|
|
1229
|
+
rendered = renderMarkdown(m.text || "");
|
|
1230
|
+
if (!m.streaming) this.mdCache.set(m.id, rendered);
|
|
1231
|
+
}
|
|
1232
|
+
content = `<span class="b">${atts}<span class="md">${rendered}</span></span>`;
|
|
1233
|
+
} else {
|
|
1234
|
+
const label = m.role === "human" ? '<div class="human-label">Support</div>' : "";
|
|
1235
|
+
content = `<span class="b">${label}${atts}${escapeHtml(m.text)}</span>`;
|
|
1236
|
+
}
|
|
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`;
|
|
432
1246
|
}
|
|
433
1247
|
renderAttachment(a) {
|
|
434
1248
|
if (a.contentType.startsWith("image/")) {
|
|
435
1249
|
const cached = (a.name ? this.imagePreviewCache.get(a.name) : void 0) ?? this.resolvedUrlCache.get(a.storageRef);
|
|
436
1250
|
const srcAttr = cached ? ` src="${cached}"` : "";
|
|
437
|
-
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")}"/>`;
|
|
438
1252
|
}
|
|
439
1253
|
return `<div class="att-chip">\u{1F4CE} ${escapeHtml(a.name ?? a.contentType)}</div>`;
|
|
440
1254
|
}
|
|
441
|
-
renderSource(s) {
|
|
442
|
-
return `<div class="src-chip" title="${escapeHtml(s.snippet)}">${escapeHtml(s.title)}</div>`;
|
|
443
|
-
}
|
|
444
|
-
// After render() attaches the markup, resolve a read-back URL (via the client) for every image
|
|
445
|
-
// placeholder that has no src yet - a local blob or a previously-resolved URL already set one, so
|
|
446
|
-
// those are left untouched (no redundant network call).
|
|
447
1255
|
hydrateAttachmentImages() {
|
|
448
1256
|
if (!this.client) return;
|
|
449
|
-
|
|
450
|
-
imgs.forEach((img) => {
|
|
1257
|
+
this.shadowRoot.querySelectorAll("img.att-thumb[data-oc-ref]").forEach((img) => {
|
|
451
1258
|
if (img.hasAttribute("src")) return;
|
|
452
1259
|
const ref = img.getAttribute("data-oc-ref");
|
|
453
1260
|
if (!ref) return;
|
|
@@ -458,82 +1265,8 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
458
1265
|
});
|
|
459
1266
|
});
|
|
460
1267
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const bubbles = this.messages.map((m) => {
|
|
464
|
-
const atts = (m.attachments ?? []).map((a) => this.renderAttachment(a)).join("");
|
|
465
|
-
const label = m.role === "human" ? '<div class="human-label">Support</div>' : "";
|
|
466
|
-
const bubble = `<div class="b ${m.role}">${label}${atts}${m.text ? escapeHtml(m.text) : ""}${m.streaming ? "\u2026" : ""}</div>`;
|
|
467
|
-
const sources = m.sources && m.sources.length > 0 ? `<div class="src-row">${m.sources.map((s) => this.renderSource(s)).join("")}</div>` : "";
|
|
468
|
-
return bubble + sources;
|
|
469
|
-
}).join("");
|
|
470
|
-
const statusText = HANDOFF_STATUS_TEXT[this.handoffState];
|
|
471
|
-
const status = statusText ? `<div class="status">${escapeHtml(statusText)}</div>` : "";
|
|
472
|
-
const errBanner = this.state === "error" && this.lastError ? `<div class="err">Couldn't start chat: ${escapeHtml(this.lastError)}</div>` : "";
|
|
473
|
-
const showTyping = this.agentTyping && (this.handoffState === "human" || this.handoffState === "awaiting_human");
|
|
474
|
-
const typingIndicator = showTyping ? `<div class="b assistant typing">${escapeHtml(AGENT_TYPING_TEXT)}</div>` : "";
|
|
475
|
-
const disabled = this.state !== "ready" ? "disabled" : "";
|
|
476
|
-
const prevInput = this.shadowRoot.querySelector('input[aria-label="message"]');
|
|
477
|
-
const prevValue = prevInput?.value ?? "";
|
|
478
|
-
const prevSelectionStart = prevInput?.selectionStart ?? null;
|
|
479
|
-
const prevSelectionEnd = prevInput?.selectionEnd ?? null;
|
|
480
|
-
const hadFocus = !!prevInput && this.shadowRoot.activeElement === prevInput;
|
|
481
|
-
this.shadowRoot.innerHTML = `
|
|
482
|
-
<style>
|
|
483
|
-
:host{display:block;height:100%;min-height:320px;font-family:system-ui,sans-serif;--oc-accent:#2563eb}
|
|
484
|
-
.wrap{display:flex;flex-direction:column;height:100%;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden}
|
|
485
|
-
.hd{padding:10px 14px;border-bottom:1px solid #e5e7eb;font-weight:600}
|
|
486
|
-
.body{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:8px}
|
|
487
|
-
.b{max-width:80%;padding:8px 12px;border-radius:12px;white-space:pre-wrap;word-break:break-word}
|
|
488
|
-
.b.user{align-self:flex-end;background:var(--oc-accent);color:#fff}
|
|
489
|
-
.b.assistant{align-self:flex-start;background:#f3f4f6;color:#111}
|
|
490
|
-
.b.human{align-self:flex-start;background:#dbeafe;color:#111}
|
|
491
|
-
.b.typing{font-style:italic;color:#6b7280;opacity:.85}
|
|
492
|
-
.human-label{font-size:11px;font-weight:600;opacity:.7;margin-bottom:2px}
|
|
493
|
-
.status{padding:6px 14px;font-size:12px;color:#6b7280;background:#f9fafb;border-bottom:1px solid #e5e7eb}
|
|
494
|
-
.err{padding:8px 14px;font-size:12px;color:#b91c1c;background:#fef2f2;border-bottom:1px solid #fecaca}
|
|
495
|
-
.att-thumb{display:block;max-width:160px;max-height:160px;border-radius:8px;margin-bottom:6px}
|
|
496
|
-
.att-chip{display:inline-block;font-size:12px;background:rgba(0,0,0,.08);border-radius:6px;padding:2px 8px;margin:0 0 6px}
|
|
497
|
-
.b.user .att-chip{background:rgba(255,255,255,.25)}
|
|
498
|
-
.src-row{align-self:flex-start;display:flex;flex-wrap:wrap;gap:6px;max-width:80%}
|
|
499
|
-
.src-chip{display:inline-block;font-size:11px;background:rgba(0,0,0,.06);color:#374151;border-radius:6px;padding:2px 8px;cursor:default}
|
|
500
|
-
form{display:flex;gap:8px;padding:10px;border-top:1px solid #e5e7eb;align-items:center}
|
|
501
|
-
input[aria-label="message"]{flex:1;padding:8px 10px;border-radius:8px;border:1px solid #d1d5db;font:inherit}
|
|
502
|
-
input[type="file"]{max-width:120px;font-size:12px}
|
|
503
|
-
button{padding:8px 14px;border-radius:8px;border:none;background:var(--oc-accent);color:#fff;font-weight:600;cursor:pointer}
|
|
504
|
-
button:disabled,input:disabled{opacity:.6;cursor:not-allowed}
|
|
505
|
-
</style>
|
|
506
|
-
<div class="wrap">
|
|
507
|
-
<div class="hd">${escapeHtml(title)}</div>
|
|
508
|
-
${status}${errBanner}
|
|
509
|
-
<div class="body">${bubbles}${typingIndicator}</div>
|
|
510
|
-
<form>
|
|
511
|
-
<input aria-label="message" placeholder="Type a message\u2026" ${disabled}/>
|
|
512
|
-
<input type="file" accept="${ATTACHMENT_ACCEPT}" multiple ${disabled}/>
|
|
513
|
-
<button type="submit" ${disabled}>Send</button>
|
|
514
|
-
</form>
|
|
515
|
-
</div>`;
|
|
516
|
-
const form = this.shadowRoot.querySelector("form");
|
|
517
|
-
const input = this.shadowRoot.querySelector('input[aria-label="message"]');
|
|
518
|
-
const fileInput = this.shadowRoot.querySelector('input[type="file"]');
|
|
519
|
-
fileInput.addEventListener("change", () => {
|
|
520
|
-
this.pendingFiles = fileInput.files ? Array.from(fileInput.files) : [];
|
|
521
|
-
});
|
|
522
|
-
form.addEventListener("submit", (e) => {
|
|
523
|
-
e.preventDefault();
|
|
524
|
-
const files = this.pendingFiles;
|
|
525
|
-
this.pendingFiles = [];
|
|
526
|
-
this.send(input.value, files);
|
|
527
|
-
input.value = "";
|
|
528
|
-
});
|
|
529
|
-
if (prevValue) {
|
|
530
|
-
input.value = prevValue;
|
|
531
|
-
if (prevSelectionStart !== null && prevSelectionEnd !== null) input.setSelectionRange(prevSelectionStart, prevSelectionEnd);
|
|
532
|
-
}
|
|
533
|
-
if (hadFocus) input.focus();
|
|
534
|
-
const body = this.shadowRoot.querySelector(".body");
|
|
535
|
-
body.scrollTop = body.scrollHeight;
|
|
536
|
-
this.hydrateAttachmentImages();
|
|
1268
|
+
escapeAttr(s) {
|
|
1269
|
+
return s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
537
1270
|
}
|
|
538
1271
|
};
|
|
539
1272
|
function escapeHtml(s) {
|