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