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