@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.global.js
CHANGED
|
@@ -37,6 +37,15 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
160
169
|
seenTurnIds.add(ev.turnId);
|
|
161
170
|
const m2 = streamingId ? messages.find((x2) => x2.id === streamingId) : void 0;
|
|
162
171
|
if (m2) {
|
|
163
|
-
m2.text = ev.text;
|
|
172
|
+
m2.text = ev.text || m2.text;
|
|
164
173
|
m2.streaming = false;
|
|
165
174
|
m2.turnId = ev.turnId;
|
|
166
175
|
} else {
|
|
@@ -214,6 +223,8 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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;
|
|
@@ -1972,8 +1986,8 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
1972
1986
|
DOMPurify.isSupported = false;
|
|
1973
1987
|
return DOMPurify;
|
|
1974
1988
|
}
|
|
1975
|
-
let
|
|
1976
|
-
const originalDocument =
|
|
1989
|
+
let document2 = window2.document;
|
|
1990
|
+
const originalDocument = document2;
|
|
1977
1991
|
const currentScript = originalDocument.currentScript;
|
|
1978
1992
|
window2.DocumentFragment;
|
|
1979
1993
|
const HTMLTemplateElement = window2.HTMLTemplateElement, Node = window2.Node, Element = window2.Element, NodeFilter = window2.NodeFilter, _window$NamedNodeMap = window2.NamedNodeMap;
|
|
@@ -1999,9 +2013,9 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
1999
2013
|
return getNodeName ? getNodeName(node) : node.nodeName;
|
|
2000
2014
|
};
|
|
2001
2015
|
if (typeof HTMLTemplateElement === "function") {
|
|
2002
|
-
const template =
|
|
2016
|
+
const template = document2.createElement("template");
|
|
2003
2017
|
if (template.content && template.content.ownerDocument) {
|
|
2004
|
-
|
|
2018
|
+
document2 = template.content.ownerDocument;
|
|
2005
2019
|
}
|
|
2006
2020
|
}
|
|
2007
2021
|
let trustedTypesPolicy;
|
|
@@ -2039,7 +2053,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
2039
2053
|
}
|
|
2040
2054
|
return defaultTrustedTypesPolicy;
|
|
2041
2055
|
};
|
|
2042
|
-
const _document =
|
|
2056
|
+
const _document = document2, implementation = _document.implementation, createNodeIterator = _document.createNodeIterator, createDocumentFragment = _document.createDocumentFragment, getElementsByTagName = _document.getElementsByTagName;
|
|
2043
2057
|
const importNode = originalDocument.importNode;
|
|
2044
2058
|
let hooks = _createHooksMap();
|
|
2045
2059
|
DOMPurify.isSupported = typeof entries === "function" && typeof getParentNode === "function" && implementation && implementation.createHTMLDocument !== void 0;
|
|
@@ -2164,7 +2178,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
2164
2178
|
const DEFAULT_PARSER_MEDIA_TYPE = "text/html";
|
|
2165
2179
|
let transformCaseFunc = null;
|
|
2166
2180
|
let CONFIG = null;
|
|
2167
|
-
const formElement =
|
|
2181
|
+
const formElement = document2.createElement("form");
|
|
2168
2182
|
const isRegexOrFunction = function isRegexOrFunction2(testValue) {
|
|
2169
2183
|
return testValue instanceof RegExp || testValue instanceof Function;
|
|
2170
2184
|
};
|
|
@@ -2592,7 +2606,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
2592
2606
|
}
|
|
2593
2607
|
const body = doc.body || doc.documentElement;
|
|
2594
2608
|
if (dirty && leadingWhitespace) {
|
|
2595
|
-
body.insertBefore(
|
|
2609
|
+
body.insertBefore(document2.createTextNode(leadingWhitespace), body.childNodes[0] || null);
|
|
2596
2610
|
}
|
|
2597
2611
|
if (NAMESPACE === HTML_NAMESPACE) {
|
|
2598
2612
|
return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? "html" : "body")[0];
|
|
@@ -2827,7 +2841,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
2827
2841
|
if (_isPatchLinkageAttribute(lcName, lcTag)) {
|
|
2828
2842
|
return false;
|
|
2829
2843
|
}
|
|
2830
|
-
if (SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in
|
|
2844
|
+
if (SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in document2 || value in formElement)) {
|
|
2831
2845
|
return false;
|
|
2832
2846
|
}
|
|
2833
2847
|
const nameIsPermitted = ALLOWED_ATTR2[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
|
|
@@ -3285,132 +3299,372 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3285
3299
|
// src/styles.ts
|
|
3286
3300
|
var STYLES = `
|
|
3287
3301
|
:host {
|
|
3288
|
-
--oc-accent
|
|
3289
|
-
--oc-
|
|
3290
|
-
--oc-
|
|
3291
|
-
--oc-
|
|
3292
|
-
--oc-
|
|
3293
|
-
--oc-
|
|
3294
|
-
--oc-
|
|
3295
|
-
|
|
3296
|
-
|
|
3302
|
+
/* accent (consumer-overridable via --oc-accent) + its derivations */
|
|
3303
|
+
--oc-ac: var(--oc-accent, #096DD9);
|
|
3304
|
+
--oc-btn-rim: color-mix(in oklab, var(--oc-ac) 70%, black);
|
|
3305
|
+
--oc-btn-edge: 0 6px 16px -10px color-mix(in oklab, var(--oc-ac) 55%, transparent);
|
|
3306
|
+
--oc-danger: #D92D20;
|
|
3307
|
+
--oc-ease: cubic-bezier(0.16,1,0.3,1);
|
|
3308
|
+
--oc-bounce: cubic-bezier(0.18,0.89,0.32,1.13);
|
|
3309
|
+
--oc-mono: 'Geist Mono', ui-monospace, monospace;
|
|
3310
|
+
--oc-r-full: 999px;
|
|
3311
|
+
|
|
3312
|
+
/* light palette (base) */
|
|
3313
|
+
--oc-bg: #FFFFFF; --oc-bg-sunk: #F6F8FB;
|
|
3314
|
+
--oc-fg: #0A0D12; --oc-fg-muted: rgba(10,13,18,0.66); --oc-fg-dim: rgba(10,13,18,0.44);
|
|
3315
|
+
--oc-border: rgba(9,13,20,0.10);
|
|
3316
|
+
--oc-accent-text: color-mix(in oklab, var(--oc-ac) 88%, black);
|
|
3317
|
+
--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);
|
|
3318
|
+
--oc-field-bg: #FFFFFF; --oc-field-border: rgba(9,13,20,0.22);
|
|
3319
|
+
--oc-glass-border: rgba(9,13,20,0.12);
|
|
3320
|
+
|
|
3321
|
+
position: fixed; right: 24px; bottom: 24px; z-index: 2147483000;
|
|
3322
|
+
font-family: 'Geist', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
|
|
3323
|
+
-webkit-font-smoothing: antialiased;
|
|
3324
|
+
}
|
|
3325
|
+
@media (prefers-color-scheme: dark) {
|
|
3326
|
+
:host(:not([theme="light"])) {
|
|
3327
|
+
--oc-bg: #000000; --oc-bg-sunk: #000000;
|
|
3328
|
+
--oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
|
|
3329
|
+
--oc-border: rgba(255,255,255,0.09);
|
|
3330
|
+
--oc-accent-text: color-mix(in oklab, var(--oc-ac) 62%, white);
|
|
3331
|
+
--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);
|
|
3332
|
+
--oc-field-bg: rgba(255,255,255,0.045); --oc-field-border: rgba(255,255,255,0.20);
|
|
3333
|
+
--oc-glass-border: rgba(255,255,255,0.12);
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
:host([theme="dark"]) {
|
|
3337
|
+
--oc-bg: #000000; --oc-bg-sunk: #000000;
|
|
3338
|
+
--oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
|
|
3339
|
+
--oc-border: rgba(255,255,255,0.09);
|
|
3340
|
+
--oc-accent-text: color-mix(in oklab, var(--oc-ac) 62%, white);
|
|
3341
|
+
--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);
|
|
3342
|
+
--oc-field-bg: rgba(255,255,255,0.045); --oc-field-border: rgba(255,255,255,0.20);
|
|
3343
|
+
--oc-glass-border: rgba(255,255,255,0.12);
|
|
3297
3344
|
}
|
|
3298
3345
|
* { box-sizing: border-box; }
|
|
3346
|
+
button { font-family: inherit; }
|
|
3347
|
+
|
|
3348
|
+
.oc { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; }
|
|
3299
3349
|
|
|
3300
|
-
/* Launcher */
|
|
3350
|
+
/* \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 */
|
|
3301
3351
|
.launcher {
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3352
|
+
position: relative; flex: none; width: 46px; height: 46px; border-radius: var(--oc-r-full);
|
|
3353
|
+
border: 1px solid var(--oc-btn-rim); background: var(--oc-ac);
|
|
3354
|
+
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);
|
|
3355
|
+
color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer;
|
|
3356
|
+
transition: transform .26s var(--oc-bounce), filter .2s var(--oc-ease); align-self: flex-end;
|
|
3305
3357
|
}
|
|
3306
3358
|
.launcher:hover { transform: scale(1.06); }
|
|
3307
|
-
.launcher
|
|
3308
|
-
.
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3359
|
+
.launcher:active { transform: scale(0.93); }
|
|
3360
|
+
.ring { pointer-events: none; position: absolute; inset: -1px; border-radius: var(--oc-r-full); border: 2px solid var(--oc-ac); opacity: 0; }
|
|
3361
|
+
.oc.ring:not(.open) .ring { animation: w-ring 2.4s ease-out 1.1s 3; }
|
|
3362
|
+
.ic-chat, .ic-close { position: absolute; display: flex; align-items: center; justify-content: center; transition: all .3s var(--oc-bounce); }
|
|
3363
|
+
.ic-chat { width: 28px; height: 28px; opacity: 1; transform: rotate(0) scale(1); }
|
|
3364
|
+
.ic-close { opacity: 0; transform: rotate(90deg) scale(0.6); }
|
|
3365
|
+
.oc.open .ic-chat { opacity: 0; transform: rotate(-90deg) scale(0.6); }
|
|
3366
|
+
.oc.open .ic-close { opacity: 1; transform: rotate(0) scale(1); }
|
|
3367
|
+
.badge {
|
|
3368
|
+
position: absolute; top: -2px; right: -2px; min-width: 17px; height: 17px; padding: 0 5px;
|
|
3369
|
+
border-radius: var(--oc-r-full); border: 2px solid var(--oc-bg-sunk); background: var(--oc-danger);
|
|
3370
|
+
color: #fff; font-family: var(--oc-mono); font-size: 10px; font-weight: 700;
|
|
3371
|
+
display: flex; align-items: center; justify-content: center; opacity: 0; transform: scale(0.4);
|
|
3372
|
+
transition: all .26s var(--oc-bounce);
|
|
3312
3373
|
}
|
|
3374
|
+
.oc.unread:not(.open) .badge { opacity: 1; transform: scale(1); }
|
|
3313
3375
|
|
|
3314
|
-
/*
|
|
3376
|
+
/* \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 */
|
|
3377
|
+
.teaser {
|
|
3378
|
+
position: absolute; right: 0; bottom: 60px; display: none; align-items: center; gap: 11px;
|
|
3379
|
+
opacity: 0; transform: translateY(8px) scale(0.94); pointer-events: none;
|
|
3380
|
+
transition: opacity .3s var(--oc-ease), transform .38s var(--oc-bounce);
|
|
3381
|
+
}
|
|
3382
|
+
.oc.has-teaser .teaser { display: flex; }
|
|
3383
|
+
.oc.teaser:not(.open) .teaser { opacity: 1; transform: none; pointer-events: auto; }
|
|
3384
|
+
.teaser-btn {
|
|
3385
|
+
max-width: 250px; display: flex; align-items: center; gap: 10px; padding: 11px 15px;
|
|
3386
|
+
border-radius: 16px 16px 5px 16px; border: 1px solid var(--oc-glass-border); background: var(--oc-bg);
|
|
3387
|
+
box-shadow: 0 10px 30px -14px rgba(0,0,0,0.5); color: var(--oc-fg); font-size: 13.5px; line-height: 1.45;
|
|
3388
|
+
text-align: left; cursor: pointer; transition: transform .2s var(--oc-ease);
|
|
3389
|
+
}
|
|
3390
|
+
.teaser-btn:hover { transform: translateY(-1px); }
|
|
3391
|
+
.teaser-btn .logo { flex: none; width: 24px; height: 24px; border-radius: 6px; overflow: hidden; color: var(--oc-ac); }
|
|
3392
|
+
.teaser-x {
|
|
3393
|
+
flex: none; width: 22px; height: 22px; border-radius: var(--oc-r-full); border: 1px solid var(--oc-border);
|
|
3394
|
+
background: var(--oc-bg); color: var(--oc-fg-dim); display: flex; align-items: center; justify-content: center; cursor: pointer;
|
|
3395
|
+
}
|
|
3396
|
+
.teaser-x:hover { color: var(--oc-fg); }
|
|
3397
|
+
|
|
3398
|
+
/* \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 */
|
|
3315
3399
|
.panel {
|
|
3316
|
-
position:
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3400
|
+
position: relative; width: min(384px, calc(100vw - 40px)); height: min(560px, calc(100vh - 108px));
|
|
3401
|
+
display: flex; flex-direction: column; border-radius: 20px; border: 1px solid var(--oc-glass-border);
|
|
3402
|
+
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);
|
|
3403
|
+
overflow: hidden; transform-origin: bottom right;
|
|
3404
|
+
opacity: 0; transform: translateY(16px) scale(0.94); pointer-events: none; visibility: hidden;
|
|
3405
|
+
transition: opacity .24s var(--oc-ease), transform .34s var(--oc-bounce), visibility .34s, width .3s var(--oc-ease), height .3s var(--oc-ease);
|
|
3321
3406
|
}
|
|
3322
|
-
.panel
|
|
3323
|
-
.
|
|
3407
|
+
.oc.open .panel { opacity: 1; transform: none; pointer-events: auto; visibility: visible; }
|
|
3408
|
+
.oc.max .panel { width: min(720px, calc(100vw - 40px)); height: calc(100vh - 108px); }
|
|
3324
3409
|
|
|
3325
|
-
.hd { display: flex; align-items: center; gap:
|
|
3326
|
-
.hd
|
|
3327
|
-
.hd .
|
|
3328
|
-
.hd
|
|
3329
|
-
.hd .
|
|
3410
|
+
.hd { position: relative; flex: none; display: flex; align-items: center; gap: 12px; padding: 13px 14px 12px; background: var(--oc-ac); color: #fff; overflow: hidden; }
|
|
3411
|
+
.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; }
|
|
3412
|
+
.hd .logo { position: relative; flex: none; width: 32px; height: 32px; border-radius: 7px; overflow: hidden; color: rgba(255,255,255,0.22); }
|
|
3413
|
+
.hd-txt { position: relative; flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
|
|
3414
|
+
.hd-title { font-size: 14.5px; font-weight: 600; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3415
|
+
.hd-status { display: flex; align-items: center; gap: 6px; font-size: 11.5px; opacity: 0.9; }
|
|
3416
|
+
.hd-status .dot { width: 6px; height: 6px; border-radius: var(--oc-r-full); background: #7BE3A6; flex: none; }
|
|
3417
|
+
.hd-status .t { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
3418
|
+
.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); }
|
|
3419
|
+
.hd-btn:hover { background: rgba(255,255,255,0.28); }
|
|
3420
|
+
.hd-btn .i { position: absolute; display: flex; transition: opacity .2s var(--oc-ease); }
|
|
3421
|
+
.i-expand { opacity: 1; } .i-shrink { opacity: 0; }
|
|
3422
|
+
.oc.max .i-expand { opacity: 0; } .oc.max .i-shrink { opacity: 1; }
|
|
3330
3423
|
|
|
3331
|
-
|
|
3332
|
-
.err {
|
|
3333
|
-
.err
|
|
3424
|
+
/* Inline connection error, shown inside the chat window (no banner) */
|
|
3425
|
+
.oc-err { margin: auto; display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 24px 16px; text-align: center; }
|
|
3426
|
+
.oc-err-t { font-size: 13px; line-height: 1.5; color: var(--oc-fg-muted); text-wrap: pretty; }
|
|
3427
|
+
.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); }
|
|
3428
|
+
.oc-err-retry:hover { background: var(--oc-tint-3); }
|
|
3334
3429
|
|
|
3335
|
-
/* Message list */
|
|
3336
|
-
.body { flex: 1;
|
|
3337
|
-
.
|
|
3338
|
-
.
|
|
3339
|
-
.b
|
|
3340
|
-
.
|
|
3341
|
-
.
|
|
3430
|
+
/* \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 */
|
|
3431
|
+
.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; }
|
|
3432
|
+
.turn { display: flex; flex-direction: column; gap: 6px; animation: w-in .42s var(--oc-ease) both; }
|
|
3433
|
+
.turn.bot { align-items: flex-start; } .turn.user { align-items: flex-end; }
|
|
3434
|
+
.b { font-size: 13.5px; line-height: 1.55; text-wrap: pretty; word-break: break-word; }
|
|
3435
|
+
.turn.bot .b { max-width: 86%; padding: 13px 15px; border-radius: 20px; background: var(--oc-tint-2); color: var(--oc-fg); }
|
|
3436
|
+
.turn.user .b { max-width: 82%; padding: 11px 15px; border-radius: var(--oc-r-full); background: var(--oc-ac); color: #fff; }
|
|
3437
|
+
.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); }
|
|
3438
|
+
.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%; }
|
|
3439
|
+
.src .t { min-width: 0; font-size: 11.5px; color: var(--oc-fg-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3440
|
+
.byline { display: flex; align-items: center; gap: 6px; padding: 0 4px; font-size: 11.5px; color: var(--oc-fg-dim); }
|
|
3342
3441
|
.human-label { font-size: 11px; font-weight: 600; opacity: .7; margin-bottom: 2px; }
|
|
3343
3442
|
|
|
3344
|
-
/*
|
|
3345
|
-
.md > :first-child { margin-top: 0; }
|
|
3346
|
-
.md
|
|
3347
|
-
.md
|
|
3348
|
-
.md
|
|
3349
|
-
.md
|
|
3350
|
-
.md
|
|
3351
|
-
.
|
|
3352
|
-
.md code { font-family: ui-monospace, monospace; font-size: 12.5px; background: rgba(0,0,0,.07); padding: 1px 5px; border-radius: 5px; }
|
|
3353
|
-
.md pre { background: #0f172a; color: #e2e8f0; padding: 10px 12px; border-radius: 8px; overflow-x: auto; margin: 0 0 8px; }
|
|
3354
|
-
.md pre code { background: none; padding: 0; color: inherit; }
|
|
3355
|
-
.md blockquote { margin: 0 0 8px; padding-left: 10px; border-left: 3px solid var(--oc-border); color: var(--oc-muted); }
|
|
3443
|
+
/* markdown inside a bot bubble */
|
|
3444
|
+
.md > :first-child { margin-top: 0; } .md > :last-child { margin-bottom: 0; }
|
|
3445
|
+
.md p { margin: 0 0 8px; } .md ul, .md ol { margin: 0 0 8px; padding-left: 20px; } .md li { margin: 2px 0; }
|
|
3446
|
+
.md a { color: var(--oc-accent-text); text-decoration: underline; text-underline-offset: 2px; }
|
|
3447
|
+
.md code { font-family: var(--oc-mono); font-size: 12.5px; background: var(--oc-tint-3); padding: 1px 5px; border-radius: 5px; }
|
|
3448
|
+
.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; }
|
|
3449
|
+
.md pre code { background: none; padding: 0; }
|
|
3450
|
+
.md blockquote { margin: 0 0 8px; padding-left: 10px; border-left: 3px solid var(--oc-border); color: var(--oc-fg-muted); }
|
|
3356
3451
|
.md h1, .md h2, .md h3, .md h4 { margin: 8px 0 6px; font-size: 15px; }
|
|
3357
3452
|
.md table { border-collapse: collapse; font-size: 13px; margin: 0 0 8px; display: block; overflow-x: auto; }
|
|
3358
3453
|
.md th, .md td { border: 1px solid var(--oc-border); padding: 4px 8px; }
|
|
3359
|
-
.md hr { border: none; border-top: 1px solid var(--oc-border); margin: 8px 0; }
|
|
3360
|
-
|
|
3361
|
-
.src-row { align-self: flex-start; display: flex; flex-wrap: wrap; gap: 6px; max-width: 85%; }
|
|
3362
|
-
.src-chip { font-size: 11px; background: rgba(0,0,0,.06); color: #374151; border-radius: 6px; padding: 2px 8px; cursor: default; }
|
|
3363
3454
|
.att-thumb { display: block; max-width: 180px; max-height: 180px; border-radius: 8px; margin-bottom: 6px; }
|
|
3364
|
-
.att-chip { display: inline-block; font-size: 12px; background:
|
|
3365
|
-
.
|
|
3455
|
+
.att-chip { display: inline-block; font-size: 12px; background: var(--oc-tint-3); border-radius: 6px; padding: 2px 8px; margin: 0 0 6px; }
|
|
3456
|
+
.turn.user .att-chip { background: rgba(255,255,255,0.25); }
|
|
3366
3457
|
|
|
3367
|
-
|
|
3368
|
-
.
|
|
3369
|
-
|
|
3370
|
-
background: var(--oc-accent); color: #fff; border: none; cursor: pointer;
|
|
3371
|
-
font-size: 12px; font-weight: 600; padding: 6px 12px; border-radius: 999px;
|
|
3372
|
-
box-shadow: 0 4px 14px rgba(0,0,0,.25); display: flex; align-items: center; gap: 6px;
|
|
3373
|
-
}
|
|
3374
|
-
.pill[hidden] { display: none; }
|
|
3375
|
-
.pill svg { width: 13px; height: 13px; }
|
|
3458
|
+
.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; }
|
|
3459
|
+
.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; }
|
|
3460
|
+
.typing span:nth-child(2) { animation-delay: .15s; } .typing span:nth-child(3) { animation-delay: .3s; }
|
|
3376
3461
|
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
}
|
|
3383
|
-
|
|
3384
|
-
.
|
|
3385
|
-
.
|
|
3462
|
+
.chips { display: flex; flex-wrap: wrap; gap: 7px; padding-top: 2px; animation: w-in .42s var(--oc-ease) both; animation-delay: .3s; }
|
|
3463
|
+
.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); }
|
|
3464
|
+
.chip:hover { background: color-mix(in oklab, var(--oc-ac) 18%, transparent); }
|
|
3465
|
+
|
|
3466
|
+
/* \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 */
|
|
3467
|
+
.jump-wrap { position: relative; flex: none; height: 0; z-index: 2; display: flex; justify-content: center; }
|
|
3468
|
+
.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); }
|
|
3469
|
+
.oc.away .jump { opacity: 1; transform: none; pointer-events: auto; }
|
|
3470
|
+
.jump:hover { color: var(--oc-fg); border-color: var(--oc-fg-dim); }
|
|
3471
|
+
|
|
3472
|
+
/* \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 */
|
|
3473
|
+
.composer { flex: none; display: flex; flex-direction: column; gap: 9px; padding: 12px 14px 13px; background: var(--oc-bg); }
|
|
3474
|
+
.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); }
|
|
3475
|
+
.oc.focus .field { border-color: var(--oc-ac); }
|
|
3476
|
+
.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; }
|
|
3477
|
+
.input::placeholder { color: var(--oc-fg-dim); }
|
|
3478
|
+
.acts { display: flex; align-items: center; gap: 16px; }
|
|
3479
|
+
.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); }
|
|
3480
|
+
.act:hover { color: var(--oc-fg-muted); }
|
|
3481
|
+
.oc.emoji .act.emoji-btn { color: var(--oc-accent-text); }
|
|
3482
|
+
.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); }
|
|
3483
|
+
.oc.ready .send { border-color: var(--oc-btn-rim); background: var(--oc-ac); color: #fff; box-shadow: var(--oc-btn-edge); cursor: pointer; }
|
|
3484
|
+
.oc.ready .send:hover { filter: brightness(1.1); }
|
|
3485
|
+
.send:active { transform: scale(0.92); }
|
|
3486
|
+
|
|
3487
|
+
.foot { position: relative; display: none; text-align: center; font-size: 11.5px; line-height: 1.5; color: var(--oc-fg-dim); }
|
|
3488
|
+
.oc.has-privacy .foot { display: block; }
|
|
3489
|
+
.foot .priv { color: var(--oc-fg-muted); text-decoration: underline; text-underline-offset: 2px; cursor: default; }
|
|
3490
|
+
.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); }
|
|
3491
|
+
.oc.max .foot .tip { right: 188px; }
|
|
3492
|
+
.foot:hover .tip { opacity: 1; }
|
|
3493
|
+
.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); }
|
|
3494
|
+
.foot:hover .tip-card { transform: none; }
|
|
3495
|
+
.foot .tip-card a { color: var(--oc-fg); text-decoration: underline; text-underline-offset: 2px; }
|
|
3496
|
+
|
|
3497
|
+
/* \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 */
|
|
3498
|
+
.emoji-scrim { position: absolute; inset: 0; z-index: 3; display: none; }
|
|
3499
|
+
.oc.emoji .emoji-scrim { display: block; }
|
|
3500
|
+
.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; }
|
|
3501
|
+
.oc.emoji .emoji-panel { opacity: 1; transform: none; pointer-events: auto; visibility: visible; }
|
|
3502
|
+
.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); }
|
|
3503
|
+
.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; }
|
|
3504
|
+
.emoji-search input::placeholder { color: var(--oc-fg-dim); }
|
|
3505
|
+
.emoji-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding: 12px 14px 16px; }
|
|
3506
|
+
.emoji-grp { display: flex; flex-direction: column; gap: 6px; }
|
|
3507
|
+
.emoji-grp + .emoji-grp { padding-top: 8px; }
|
|
3508
|
+
.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); }
|
|
3509
|
+
.emoji-grid { display: grid; grid-template-columns: repeat(7, minmax(0,1fr)); gap: 2px; }
|
|
3510
|
+
.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); }
|
|
3511
|
+
.emoji-grid button:hover { background: var(--oc-tint-3); }
|
|
3512
|
+
.emoji-empty { display: none; flex-direction: column; align-items: center; gap: 5px; padding: 26px 10px; text-align: center; }
|
|
3513
|
+
.emoji-empty .h { font-size: 14px; color: var(--oc-fg); } .emoji-empty .s { font-size: 12.5px; color: var(--oc-fg-dim); }
|
|
3386
3514
|
|
|
3387
3515
|
@media (max-width: 480px) {
|
|
3388
3516
|
:host { right: 0; bottom: 0; }
|
|
3389
|
-
.
|
|
3390
|
-
.
|
|
3391
|
-
.
|
|
3517
|
+
.oc { padding: 0 16px 16px; }
|
|
3518
|
+
.panel { position: fixed; inset: 0; width: 100vw; height: 100dvh; border-radius: 0; }
|
|
3519
|
+
.oc.max .panel { width: 100vw; height: 100dvh; }
|
|
3392
3520
|
}
|
|
3393
3521
|
@media (prefers-reduced-motion: reduce) {
|
|
3394
|
-
.launcher, .panel { transition: none; }
|
|
3395
|
-
.body { scroll-behavior: auto; }
|
|
3522
|
+
.launcher, .panel, .teaser, .turn, .jump, .emoji-panel { transition: none; animation: none; }
|
|
3523
|
+
.ring { animation: none; } .body { scroll-behavior: auto; }
|
|
3396
3524
|
}
|
|
3525
|
+
|
|
3526
|
+
@keyframes w-in { from { opacity: 0; transform: translateY(10px) scale(0.97); } to { opacity: 1; transform: none; } }
|
|
3527
|
+
@keyframes w-dot { 0%,60%,100% { transform: translateY(0); opacity: .45; } 30% { transform: translateY(-4px); opacity: 1; } }
|
|
3528
|
+
@keyframes w-ring { 0% { transform: scale(1); opacity: .5; } 70%,100% { transform: scale(1.7); opacity: 0; } }
|
|
3397
3529
|
`;
|
|
3398
3530
|
|
|
3531
|
+
// src/icons.ts
|
|
3532
|
+
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>`;
|
|
3533
|
+
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>`;
|
|
3534
|
+
var ICON = {
|
|
3535
|
+
close: (size = 19) => stroke(2.4, '<path d="M18 6 6 18M6 6l12 12"/>', size),
|
|
3536
|
+
minus: stroke(2, '<path d="M6 12h12"/>', 15),
|
|
3537
|
+
expand: stroke(2, '<path d="M15 4h5v5M20 4l-6.5 6.5M9 20H4v-5M4 20l6.5-6.5"/>', 15),
|
|
3538
|
+
shrink: stroke(2, '<path d="M20 9h-5V4M15 9l5.5-5.5M4 15h5v5M9 15l-5.5 5.5"/>', 15),
|
|
3539
|
+
chevronDown: stroke(2.1, '<path d="m6 9 6 6 6-6"/>', 16),
|
|
3540
|
+
send: stroke(2.2, '<path d="M12 19V5M6 11l6-6 6 6"/>', 16),
|
|
3541
|
+
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),
|
|
3542
|
+
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),
|
|
3543
|
+
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),
|
|
3544
|
+
search: stroke(1.9, '<circle cx="11" cy="11" r="7.5"/><path d="m21 21-4.3-4.3"/>', 17),
|
|
3545
|
+
clear: stroke(2.2, '<path d="M18 6 6 18M6 6l12 12"/>', 15),
|
|
3546
|
+
// Two-page "source" glyph for citation chips.
|
|
3547
|
+
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>`,
|
|
3548
|
+
teaserDismiss: stroke(2.4, '<path d="M18 6 6 18M6 6l12 12"/>', 12)
|
|
3549
|
+
};
|
|
3550
|
+
|
|
3551
|
+
// src/emoji.ts
|
|
3552
|
+
var CATS = [
|
|
3553
|
+
{ name: "Frequently used", items: [
|
|
3554
|
+
["\u{1F44D}", "thumbs up"],
|
|
3555
|
+
["\u{1F64F}", "thanks"],
|
|
3556
|
+
["\u{1F44B}", "wave"],
|
|
3557
|
+
["\u2705", "done"],
|
|
3558
|
+
["\u{1F4DE}", "call"],
|
|
3559
|
+
["\u{1F60A}", "smile"],
|
|
3560
|
+
["\u2764\uFE0F", "heart"]
|
|
3561
|
+
] },
|
|
3562
|
+
{ name: "Smileys and emotion", items: [
|
|
3563
|
+
["\u{1F600}", "grinning"],
|
|
3564
|
+
["\u{1F603}", "smiley"],
|
|
3565
|
+
["\u{1F604}", "grin"],
|
|
3566
|
+
["\u{1F601}", "beaming"],
|
|
3567
|
+
["\u{1F606}", "squinting"],
|
|
3568
|
+
["\u{1F605}", "relief"],
|
|
3569
|
+
["\u{1F923}", "rolling"],
|
|
3570
|
+
["\u{1F602}", "tears of joy"],
|
|
3571
|
+
["\u{1F642}", "slightly smiling"],
|
|
3572
|
+
["\u{1F643}", "upside down"],
|
|
3573
|
+
["\u{1F609}", "wink"],
|
|
3574
|
+
["\u{1F607}", "halo"],
|
|
3575
|
+
["\u{1F970}", "smiling hearts"],
|
|
3576
|
+
["\u{1F60D}", "heart eyes"],
|
|
3577
|
+
["\u{1F618}", "kiss"],
|
|
3578
|
+
["\u{1F60B}", "yum"],
|
|
3579
|
+
["\u{1F914}", "thinking"],
|
|
3580
|
+
["\u{1F928}", "raised brow"],
|
|
3581
|
+
["\u{1F610}", "neutral"],
|
|
3582
|
+
["\u{1F644}", "eye roll"],
|
|
3583
|
+
["\u{1F62C}", "grimace"],
|
|
3584
|
+
["\u{1F625}", "sad"],
|
|
3585
|
+
["\u{1F622}", "crying"],
|
|
3586
|
+
["\u{1F62D}", "sobbing"],
|
|
3587
|
+
["\u{1F620}", "angry"],
|
|
3588
|
+
["\u{1F633}", "flushed"],
|
|
3589
|
+
["\u{1F929}", "star struck"],
|
|
3590
|
+
["\u{1F634}", "sleeping"]
|
|
3591
|
+
] },
|
|
3592
|
+
{ name: "Gestures", items: [
|
|
3593
|
+
["\u{1F44E}", "thumbs down"],
|
|
3594
|
+
["\u{1F44F}", "clap"],
|
|
3595
|
+
["\u{1F64C}", "raised hands"],
|
|
3596
|
+
["\u{1F91D}", "handshake"],
|
|
3597
|
+
["\u{1F44C}", "ok hand"],
|
|
3598
|
+
["\u270C\uFE0F", "victory"],
|
|
3599
|
+
["\u{1F4AA}", "strong"],
|
|
3600
|
+
["\u{1F440}", "eyes"],
|
|
3601
|
+
["\u{1F9E0}", "brain"],
|
|
3602
|
+
["\u{1F91F}", "love you"],
|
|
3603
|
+
["\u261D\uFE0F", "point up"],
|
|
3604
|
+
["\u{1F447}", "point down"],
|
|
3605
|
+
["\u{1F91A}", "raised back"],
|
|
3606
|
+
["\u{1F91E}", "fingers crossed"]
|
|
3607
|
+
] },
|
|
3608
|
+
{ name: "Work and travel", items: [
|
|
3609
|
+
["\u{1F4C5}", "calendar"],
|
|
3610
|
+
["\u{1F4C4}", "document"],
|
|
3611
|
+
["\u{1F4B0}", "money"],
|
|
3612
|
+
["\u{1F4CA}", "chart"],
|
|
3613
|
+
["\u{1F4E6}", "package"],
|
|
3614
|
+
["\u{1F3E5}", "hospital"],
|
|
3615
|
+
["\u{1F3E6}", "bank"],
|
|
3616
|
+
["\u{1F69A}", "truck"],
|
|
3617
|
+
["\u2708\uFE0F", "plane"],
|
|
3618
|
+
["\u{1F4F1}", "phone"],
|
|
3619
|
+
["\u{1F4BB}", "laptop"],
|
|
3620
|
+
["\u{1F50D}", "search"],
|
|
3621
|
+
["\u{1F4CC}", "pin"],
|
|
3622
|
+
["\u{1F514}", "bell"]
|
|
3623
|
+
] },
|
|
3624
|
+
{ name: "Symbols", items: [
|
|
3625
|
+
["\u{1F525}", "fire"],
|
|
3626
|
+
["\u2728", "sparkles"],
|
|
3627
|
+
["\u{1F389}", "celebrate"],
|
|
3628
|
+
["\u{1F4AF}", "hundred"],
|
|
3629
|
+
["\u26A0\uFE0F", "warning"],
|
|
3630
|
+
["\u274C", "cross"],
|
|
3631
|
+
["\u2B50", "star"],
|
|
3632
|
+
["\u{1F680}", "launch"],
|
|
3633
|
+
["\u{1F49A}", "green heart"],
|
|
3634
|
+
["\u{1F499}", "blue heart"],
|
|
3635
|
+
["\u{1F552}", "clock"],
|
|
3636
|
+
["\u{1F4A1}", "idea"],
|
|
3637
|
+
["\u{1F512}", "lock"],
|
|
3638
|
+
["\u{1F3AF}", "target"]
|
|
3639
|
+
] }
|
|
3640
|
+
];
|
|
3641
|
+
var toGroup = (name, items) => ({
|
|
3642
|
+
name,
|
|
3643
|
+
items: items.map(([ch, kw]) => ({ ch, name: kw }))
|
|
3644
|
+
});
|
|
3645
|
+
function emojiGroups(query) {
|
|
3646
|
+
const q2 = query.trim().toLowerCase();
|
|
3647
|
+
if (!q2) return CATS.map((c) => toGroup(c.name, c.items));
|
|
3648
|
+
const hits = [];
|
|
3649
|
+
for (const c of CATS) for (const e of c.items) {
|
|
3650
|
+
if (e[1].indexOf(q2) >= 0 && !hits.some((h) => h[0] === e[0])) hits.push(e);
|
|
3651
|
+
}
|
|
3652
|
+
if (!hits.length) return [];
|
|
3653
|
+
return [toGroup(`${hits.length} result${hits.length === 1 ? "" : "s"}`, hits)];
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3399
3656
|
// src/element.ts
|
|
3400
|
-
var
|
|
3657
|
+
var DEFAULT_STATUS = "Replies in a few seconds";
|
|
3658
|
+
var ATTACHMENT_ACCEPT = "image/*,application/pdf,text/plain,text/csv";
|
|
3659
|
+
var AWAY_THRESHOLD = 60;
|
|
3660
|
+
var HANDOFF_STATUS = {
|
|
3401
3661
|
ai: null,
|
|
3402
3662
|
awaiting_human: "Connecting you to a person\u2026",
|
|
3403
3663
|
human: "You're chatting with a person"
|
|
3404
3664
|
};
|
|
3405
|
-
var ATTACHMENT_ACCEPT = "image/*,application/pdf,text/plain,text/csv";
|
|
3406
|
-
var AGENT_TYPING_TEXT = "The agent is typing\u2026";
|
|
3407
|
-
var PIN_THRESHOLD = 48;
|
|
3408
|
-
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>';
|
|
3409
|
-
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>';
|
|
3410
|
-
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>';
|
|
3411
|
-
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>';
|
|
3412
3665
|
var OyehelloChatElement = class extends HTMLElement {
|
|
3413
|
-
static observedAttributes = ["api-key", "agent-id", "base-url", "title"];
|
|
3666
|
+
static observedAttributes = ["api-key", "agent-id", "base-url", "title", "theme"];
|
|
3667
|
+
// ── live client state ──
|
|
3414
3668
|
client = null;
|
|
3415
3669
|
off = null;
|
|
3416
3670
|
messages = [];
|
|
@@ -3419,35 +3673,42 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3419
3673
|
handoffState = "ai";
|
|
3420
3674
|
agentTyping = false;
|
|
3421
3675
|
lastError = null;
|
|
3422
|
-
// registerTools() may run before connectedCallback creates the client — buffer until it exists.
|
|
3423
3676
|
pendingTools = {};
|
|
3424
3677
|
pendingFiles = [];
|
|
3425
3678
|
imagePreviewCache = /* @__PURE__ */ new Map();
|
|
3426
3679
|
resolvedUrlCache = /* @__PURE__ */ new Map();
|
|
3427
|
-
// Rendered-markdown cache for FINALIZED assistant/human messages (keyed by message id) so the whole
|
|
3428
|
-
// history isn't re-parsed on every streamed token — only the live streaming message is re-rendered.
|
|
3429
3680
|
mdCache = /* @__PURE__ */ new Map();
|
|
3430
|
-
|
|
3431
|
-
|
|
3681
|
+
receivedAt = /* @__PURE__ */ new Map();
|
|
3682
|
+
// ── widget UI state ──
|
|
3683
|
+
uiOpen = false;
|
|
3432
3684
|
started = false;
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3685
|
+
uiMax = false;
|
|
3686
|
+
emojiOpen = false;
|
|
3687
|
+
emojiQuery = "";
|
|
3688
|
+
away = false;
|
|
3689
|
+
// reader scrolled up
|
|
3436
3690
|
unseen = 0;
|
|
3437
|
-
|
|
3691
|
+
focused = false;
|
|
3692
|
+
teaserActive = false;
|
|
3438
3693
|
renderedCount = 0;
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3694
|
+
renderedBotFinal = 0;
|
|
3695
|
+
timers = [];
|
|
3696
|
+
// Server (flow) suggestions for the latest turn; null until the first `suggestions` event, then the
|
|
3697
|
+
// widget prefers these over the static `suggestions` attribute.
|
|
3698
|
+
serverSuggestions = null;
|
|
3699
|
+
teaserText = "Need a hand? Ask me anything.";
|
|
3700
|
+
teaserShownOnce = false;
|
|
3701
|
+
idleTimer = null;
|
|
3702
|
+
idleReset = null;
|
|
3703
|
+
// ── shadow refs ──
|
|
3704
|
+
oc;
|
|
3442
3705
|
bodyEl;
|
|
3443
3706
|
textareaEl;
|
|
3444
|
-
sendEl;
|
|
3445
3707
|
fileEl;
|
|
3446
|
-
pillEl;
|
|
3447
|
-
badgeEl;
|
|
3448
|
-
statusEl;
|
|
3449
|
-
errEl;
|
|
3450
3708
|
titleEl;
|
|
3709
|
+
statusEl;
|
|
3710
|
+
emojiListEl;
|
|
3711
|
+
emojiSearchEl;
|
|
3451
3712
|
connectedCallback() {
|
|
3452
3713
|
if (!this.shadowRoot) this.attachShadow({ mode: "open" });
|
|
3453
3714
|
const apiKey = this.getAttribute("api-key");
|
|
@@ -3457,6 +3718,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3457
3718
|
this.renderError("oyehello-chat: api-key, agent-id and base-url are required");
|
|
3458
3719
|
return;
|
|
3459
3720
|
}
|
|
3721
|
+
this.teaserText = this.getAttribute("teaser") || this.teaserText;
|
|
3460
3722
|
this.client = createChatClient({ apiKey, agentId, baseUrl });
|
|
3461
3723
|
if (Object.keys(this.pendingTools).length > 0) {
|
|
3462
3724
|
this.client.registerTools(this.pendingTools);
|
|
@@ -3464,18 +3726,100 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3464
3726
|
}
|
|
3465
3727
|
this.off = this.client.on((e) => this.onClientEvent(e));
|
|
3466
3728
|
this.renderShell();
|
|
3467
|
-
this.
|
|
3729
|
+
this.renderMessages();
|
|
3730
|
+
this.updateHeader();
|
|
3468
3731
|
this.updateStatus();
|
|
3732
|
+
this.updateClasses();
|
|
3733
|
+
void this.loadWidgetConfig();
|
|
3469
3734
|
let wasOpen = false;
|
|
3470
3735
|
try {
|
|
3471
|
-
wasOpen = sessionStorage.getItem(this.
|
|
3736
|
+
wasOpen = sessionStorage.getItem(this.key("open")) === "1";
|
|
3737
|
+
} catch {
|
|
3738
|
+
}
|
|
3739
|
+
if (wasOpen) {
|
|
3740
|
+
this.openPanel();
|
|
3741
|
+
return;
|
|
3742
|
+
}
|
|
3743
|
+
this.updateClasses(true);
|
|
3744
|
+
this.timers.push(setTimeout(() => {
|
|
3745
|
+
this.oc?.classList.remove("ring");
|
|
3746
|
+
}, 6e3));
|
|
3747
|
+
this.armIdleTeaser();
|
|
3748
|
+
}
|
|
3749
|
+
// Show the teaser once the visitor has been idle (no page interaction) for 10s — unless the panel
|
|
3750
|
+
// is already open or the teaser was dismissed this session. Fires at most once.
|
|
3751
|
+
armIdleTeaser() {
|
|
3752
|
+
let dismissed = false;
|
|
3753
|
+
try {
|
|
3754
|
+
dismissed = sessionStorage.getItem(this.key("teaser")) === "0";
|
|
3472
3755
|
} catch {
|
|
3473
3756
|
}
|
|
3474
|
-
if (
|
|
3757
|
+
if (dismissed) return;
|
|
3758
|
+
const reset = () => {
|
|
3759
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
3760
|
+
this.idleTimer = setTimeout(() => {
|
|
3761
|
+
if (!this.uiOpen && !this.teaserShownOnce) {
|
|
3762
|
+
this.teaserActive = true;
|
|
3763
|
+
this.teaserShownOnce = true;
|
|
3764
|
+
this.updateClasses();
|
|
3765
|
+
this.teardownIdle();
|
|
3766
|
+
}
|
|
3767
|
+
}, 1e4);
|
|
3768
|
+
};
|
|
3769
|
+
this.idleReset = reset;
|
|
3770
|
+
for (const ev of ["mousemove", "keydown", "scroll", "touchstart", "click"]) document.addEventListener(ev, reset, { passive: true });
|
|
3771
|
+
reset();
|
|
3772
|
+
}
|
|
3773
|
+
teardownIdle() {
|
|
3774
|
+
if (this.idleTimer) {
|
|
3775
|
+
clearTimeout(this.idleTimer);
|
|
3776
|
+
this.idleTimer = null;
|
|
3777
|
+
}
|
|
3778
|
+
if (this.idleReset) {
|
|
3779
|
+
for (const ev of ["mousemove", "keydown", "scroll", "touchstart", "click"]) document.removeEventListener(ev, this.idleReset);
|
|
3780
|
+
this.idleReset = null;
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
async loadWidgetConfig() {
|
|
3784
|
+
try {
|
|
3785
|
+
const cfg = await this.client.getWidgetConfig();
|
|
3786
|
+
if (!this.getAttribute("teaser") && cfg.teaser) {
|
|
3787
|
+
this.teaserText = cfg.teaser;
|
|
3788
|
+
this.applyTeaserText();
|
|
3789
|
+
}
|
|
3790
|
+
if (cfg.brand) {
|
|
3791
|
+
if (cfg.brand.accentColor) this.style.setProperty("--oc-accent", cfg.brand.accentColor);
|
|
3792
|
+
if (cfg.brand.name && !this.getAttribute("title")) {
|
|
3793
|
+
this.agentName = cfg.brand.name;
|
|
3794
|
+
this.updateHeader();
|
|
3795
|
+
}
|
|
3796
|
+
if (cfg.brand.logoUrl) this.applyBrandLogo(cfg.brand.logoUrl);
|
|
3797
|
+
}
|
|
3798
|
+
} catch {
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
applyTeaserText() {
|
|
3802
|
+
const el = this.shadowRoot?.querySelector(".teaser-btn span:last-child");
|
|
3803
|
+
if (el) el.textContent = this.teaserText;
|
|
3804
|
+
}
|
|
3805
|
+
applyBrandLogo(url) {
|
|
3806
|
+
const set = (host) => {
|
|
3807
|
+
if (!host) return;
|
|
3808
|
+
host.textContent = "";
|
|
3809
|
+
const img = document.createElement("img");
|
|
3810
|
+
img.src = url;
|
|
3811
|
+
img.alt = "";
|
|
3812
|
+
img.style.cssText = "width:100%;height:100%;object-fit:contain";
|
|
3813
|
+
host.appendChild(img);
|
|
3814
|
+
};
|
|
3815
|
+
set(this.shadowRoot?.querySelector(".hd .logo"));
|
|
3816
|
+
set(this.shadowRoot?.querySelector(".teaser-btn .logo"));
|
|
3475
3817
|
}
|
|
3476
3818
|
disconnectedCallback() {
|
|
3477
3819
|
this.off?.();
|
|
3478
3820
|
void this.client?.end();
|
|
3821
|
+
this.teardownIdle();
|
|
3822
|
+
for (const t of this.timers) clearTimeout(t);
|
|
3479
3823
|
for (const url of this.imagePreviewCache.values()) {
|
|
3480
3824
|
if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
|
|
3481
3825
|
}
|
|
@@ -3485,61 +3829,123 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3485
3829
|
if (this.client) this.client.registerTools(handlers);
|
|
3486
3830
|
else Object.assign(this.pendingTools, handlers);
|
|
3487
3831
|
}
|
|
3488
|
-
|
|
3489
|
-
return `oyehello-chat
|
|
3832
|
+
key(k2) {
|
|
3833
|
+
return `oyehello-chat:${k2}:${this.getAttribute("agent-id") ?? ""}`;
|
|
3834
|
+
}
|
|
3835
|
+
// Static fallback chips from the `suggestions` attribute (for embedders without flow-driven ones).
|
|
3836
|
+
get attrSuggestions() {
|
|
3837
|
+
return (this.getAttribute("suggestions") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
3838
|
+
}
|
|
3839
|
+
// Effective chips: server (flow) suggestions once received, else the static attribute fallback.
|
|
3840
|
+
get chipLabels() {
|
|
3841
|
+
return this.serverSuggestions ?? this.attrSuggestions;
|
|
3490
3842
|
}
|
|
3491
3843
|
// ── Shell (built once) ─────────────────────────────────────────────────────────────────────────
|
|
3492
3844
|
renderShell() {
|
|
3493
3845
|
const root = this.shadowRoot;
|
|
3846
|
+
const hasPrivacy = !!this.getAttribute("privacy-url");
|
|
3847
|
+
const teaserText = this.teaserText;
|
|
3494
3848
|
root.innerHTML = `
|
|
3495
3849
|
<style>${STYLES}</style>
|
|
3496
|
-
<
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
<
|
|
3507
|
-
<
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3850
|
+
<div class="oc has-teaser${hasPrivacy ? " has-privacy" : ""}">
|
|
3851
|
+
<div class="panel" role="dialog" aria-label="Chat">
|
|
3852
|
+
<div class="hd">
|
|
3853
|
+
<span class="hd-grid"></span>
|
|
3854
|
+
<span class="logo">${logoMark("currentColor")}</span>
|
|
3855
|
+
<span class="hd-txt"><span class="hd-title">Chat</span><span class="hd-status"><span class="dot"></span><span class="t"></span></span></span>
|
|
3856
|
+
<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>
|
|
3857
|
+
<button type="button" class="hd-btn min" aria-label="Minimise">${ICON.minus}</button>
|
|
3858
|
+
</div>
|
|
3859
|
+
<div class="body"></div>
|
|
3860
|
+
<span class="emoji-scrim"></span>
|
|
3861
|
+
<div class="emoji-panel">
|
|
3862
|
+
<div class="emoji-search">${ICON.search}<input type="text" placeholder="Search emoji\u2026" aria-label="Search emoji" /></div>
|
|
3863
|
+
<div class="emoji-list"></div>
|
|
3864
|
+
</div>
|
|
3865
|
+
<div class="jump-wrap"><button type="button" class="jump" aria-label="Jump to latest">${ICON.chevronDown}</button></div>
|
|
3866
|
+
<div class="composer">
|
|
3867
|
+
<form class="field">
|
|
3868
|
+
<textarea class="input" rows="1" aria-label="message" placeholder="Ask a question\u2026"></textarea>
|
|
3869
|
+
<div class="acts">
|
|
3870
|
+
<button type="button" class="act attach" aria-label="Attach a file">${ICON.paperclip}</button>
|
|
3871
|
+
<input type="file" accept="${ATTACHMENT_ACCEPT}" multiple hidden />
|
|
3872
|
+
<button type="button" class="act emoji-btn" aria-label="Insert emoji">${ICON.emoji}</button>
|
|
3873
|
+
<button type="button" class="act mic" aria-label="Send a voice note">${ICON.mic}</button>
|
|
3874
|
+
<button type="submit" class="send" aria-label="Send">${ICON.send}</button>
|
|
3875
|
+
</div>
|
|
3876
|
+
</form>
|
|
3877
|
+
<div class="foot">
|
|
3878
|
+
<span>By chatting with us, you agree to our </span><span class="priv">Privacy Policy</span>
|
|
3879
|
+
<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>
|
|
3880
|
+
</div>
|
|
3881
|
+
</div>
|
|
3882
|
+
</div>
|
|
3883
|
+
|
|
3884
|
+
<div class="teaser">
|
|
3885
|
+
<button type="button" class="teaser-btn"><span class="logo">${logoMark("currentColor")}</span><span>${escapeHtml(teaserText)}</span></button>
|
|
3886
|
+
<button type="button" class="teaser-x" aria-label="Dismiss">${ICON.teaserDismiss}</button>
|
|
3887
|
+
</div>
|
|
3888
|
+
|
|
3889
|
+
<button type="button" class="launcher" aria-label="Open chat">
|
|
3890
|
+
<span class="ring"></span>
|
|
3891
|
+
<span class="ic-chat">${logoMark("var(--oc-ac)")}</span>
|
|
3892
|
+
<span class="ic-close">${ICON.close(19)}</span>
|
|
3893
|
+
<span class="badge"></span>
|
|
3894
|
+
</button>
|
|
3511
3895
|
</div>`;
|
|
3512
|
-
this.
|
|
3896
|
+
this.oc = root.querySelector(".oc");
|
|
3513
3897
|
this.bodyEl = root.querySelector(".body");
|
|
3514
|
-
this.textareaEl = root.querySelector("textarea");
|
|
3515
|
-
this.sendEl = root.querySelector(".send");
|
|
3898
|
+
this.textareaEl = root.querySelector("textarea.input");
|
|
3516
3899
|
this.fileEl = root.querySelector('input[type="file"]');
|
|
3517
|
-
this.
|
|
3518
|
-
this.
|
|
3519
|
-
this.
|
|
3520
|
-
this.
|
|
3521
|
-
|
|
3522
|
-
root.querySelector(".
|
|
3523
|
-
root.querySelector(".
|
|
3900
|
+
this.titleEl = root.querySelector(".hd-title");
|
|
3901
|
+
this.statusEl = root.querySelector(".hd-status .t");
|
|
3902
|
+
this.emojiListEl = root.querySelector(".emoji-list");
|
|
3903
|
+
this.emojiSearchEl = root.querySelector(".emoji-search input");
|
|
3904
|
+
root.querySelector(".launcher").addEventListener("click", () => this.toggle());
|
|
3905
|
+
root.querySelector(".hd-btn.min").addEventListener("click", () => this.closePanel());
|
|
3906
|
+
root.querySelector(".hd-btn.max").addEventListener("click", () => {
|
|
3907
|
+
this.uiMax = !this.uiMax;
|
|
3908
|
+
this.updateClasses();
|
|
3909
|
+
});
|
|
3910
|
+
root.querySelector(".teaser-btn").addEventListener("click", () => this.openPanel());
|
|
3911
|
+
root.querySelector(".teaser-x").addEventListener("click", () => this.dismissTeaser());
|
|
3912
|
+
root.querySelector(".jump").addEventListener("click", () => this.scrollToBottom());
|
|
3524
3913
|
root.querySelector(".attach").addEventListener("click", () => this.fileEl.click());
|
|
3525
3914
|
this.fileEl.addEventListener("change", () => {
|
|
3526
3915
|
this.pendingFiles = this.fileEl.files ? Array.from(this.fileEl.files) : [];
|
|
3527
3916
|
});
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3917
|
+
root.querySelector(".emoji-btn").addEventListener("click", () => this.toggleEmoji());
|
|
3918
|
+
root.querySelector(".emoji-scrim").addEventListener("click", () => this.closeEmoji());
|
|
3919
|
+
this.emojiSearchEl.addEventListener("input", () => {
|
|
3920
|
+
this.emojiQuery = this.emojiSearchEl.value;
|
|
3921
|
+
this.renderEmoji();
|
|
3922
|
+
});
|
|
3923
|
+
this.bodyEl.addEventListener("scroll", () => this.onScroll());
|
|
3924
|
+
root.querySelector("form.field").addEventListener("submit", (e) => {
|
|
3531
3925
|
e.preventDefault();
|
|
3532
3926
|
this.doSend();
|
|
3533
3927
|
});
|
|
3534
|
-
this.textareaEl.addEventListener("input", () =>
|
|
3928
|
+
this.textareaEl.addEventListener("input", () => {
|
|
3929
|
+
this.autoGrow();
|
|
3930
|
+
this.updateClasses();
|
|
3931
|
+
});
|
|
3932
|
+
this.textareaEl.addEventListener("focus", () => {
|
|
3933
|
+
this.focused = true;
|
|
3934
|
+
this.updateClasses();
|
|
3935
|
+
});
|
|
3936
|
+
this.textareaEl.addEventListener("blur", () => {
|
|
3937
|
+
this.focused = false;
|
|
3938
|
+
this.updateClasses();
|
|
3939
|
+
});
|
|
3535
3940
|
this.textareaEl.addEventListener("keydown", (e) => {
|
|
3536
3941
|
if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
|
|
3537
3942
|
e.preventDefault();
|
|
3538
3943
|
this.doSend();
|
|
3539
3944
|
}
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3945
|
+
if (e.key === "Escape") {
|
|
3946
|
+
if (this.emojiOpen) this.closeEmoji();
|
|
3947
|
+
else this.closePanel();
|
|
3948
|
+
}
|
|
3543
3949
|
});
|
|
3544
3950
|
}
|
|
3545
3951
|
renderError(msg) {
|
|
@@ -3547,37 +3953,63 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3547
3953
|
console.error(msg);
|
|
3548
3954
|
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>`;
|
|
3549
3955
|
}
|
|
3956
|
+
// ── State-class sync ──────────────────────────────────────────────────────────────────────────
|
|
3957
|
+
updateClasses(initialRing = false) {
|
|
3958
|
+
if (!this.oc) return;
|
|
3959
|
+
const c = this.oc.classList;
|
|
3960
|
+
c.toggle("open", this.uiOpen);
|
|
3961
|
+
c.toggle("max", this.uiMax);
|
|
3962
|
+
c.toggle("emoji", this.emojiOpen);
|
|
3963
|
+
c.toggle("teaser", this.teaserActive);
|
|
3964
|
+
c.toggle("away", this.away);
|
|
3965
|
+
c.toggle("focus", this.focused);
|
|
3966
|
+
c.toggle("ready", (this.textareaEl?.value.trim().length ?? 0) > 0);
|
|
3967
|
+
c.toggle("unread", this.unseen > 0);
|
|
3968
|
+
if (initialRing) c.add("ring");
|
|
3969
|
+
const badge = this.oc.querySelector(".badge");
|
|
3970
|
+
if (badge) badge.textContent = this.unseen > 9 ? "9+" : String(this.unseen);
|
|
3971
|
+
const launcher = this.oc.querySelector(".launcher");
|
|
3972
|
+
if (launcher) launcher.setAttribute("aria-label", this.uiOpen ? "Close chat" : "Open chat");
|
|
3973
|
+
}
|
|
3550
3974
|
// ── Open / close ───────────────────────────────────────────────────────────────────────────────
|
|
3551
|
-
|
|
3552
|
-
this.
|
|
3975
|
+
toggle() {
|
|
3976
|
+
this.uiOpen ? this.closePanel() : this.openPanel();
|
|
3553
3977
|
}
|
|
3554
3978
|
openPanel() {
|
|
3555
|
-
this.
|
|
3979
|
+
this.uiOpen = true;
|
|
3980
|
+
this.teaserActive = false;
|
|
3981
|
+
this.unseen = 0;
|
|
3982
|
+
this.teardownIdle();
|
|
3556
3983
|
try {
|
|
3557
|
-
sessionStorage.setItem(this.
|
|
3984
|
+
sessionStorage.setItem(this.key("open"), "1");
|
|
3558
3985
|
} catch {
|
|
3559
3986
|
}
|
|
3560
|
-
this.panelEl.hidden = false;
|
|
3561
|
-
this.panelEl.classList.add("enter");
|
|
3562
|
-
requestAnimationFrame(() => this.panelEl.classList.remove("enter"));
|
|
3563
3987
|
if (!this.started && this.client) {
|
|
3564
3988
|
this.started = true;
|
|
3565
3989
|
void this.client.start();
|
|
3566
3990
|
}
|
|
3991
|
+
this.updateClasses();
|
|
3567
3992
|
this.renderMessages();
|
|
3568
|
-
this.pinned = true;
|
|
3569
|
-
this.unseen = 0;
|
|
3570
|
-
this.updateBadge();
|
|
3571
3993
|
this.scrollToBottom();
|
|
3572
|
-
if (!this.textareaEl.disabled) this.textareaEl.focus();
|
|
3994
|
+
if (!this.textareaEl.disabled) setTimeout(() => this.textareaEl.focus(), 60);
|
|
3573
3995
|
}
|
|
3574
3996
|
closePanel() {
|
|
3575
|
-
this.
|
|
3997
|
+
this.uiOpen = false;
|
|
3998
|
+
this.uiMax = false;
|
|
3999
|
+
this.emojiOpen = false;
|
|
4000
|
+
try {
|
|
4001
|
+
sessionStorage.setItem(this.key("open"), "0");
|
|
4002
|
+
} catch {
|
|
4003
|
+
}
|
|
4004
|
+
this.updateClasses();
|
|
4005
|
+
}
|
|
4006
|
+
dismissTeaser() {
|
|
4007
|
+
this.teaserActive = false;
|
|
3576
4008
|
try {
|
|
3577
|
-
sessionStorage.setItem(this.
|
|
4009
|
+
sessionStorage.setItem(this.key("teaser"), "0");
|
|
3578
4010
|
} catch {
|
|
3579
4011
|
}
|
|
3580
|
-
this.
|
|
4012
|
+
this.updateClasses();
|
|
3581
4013
|
}
|
|
3582
4014
|
// ── Client events ─────────────────────────────────────────────────────────────────────────────
|
|
3583
4015
|
onClientEvent(e) {
|
|
@@ -3587,91 +4019,99 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3587
4019
|
if (e.state === "ready" || e.state === "opening") this.lastError = null;
|
|
3588
4020
|
} else if (e.type === "agent") {
|
|
3589
4021
|
this.agentName = e.agent.name;
|
|
3590
|
-
this.
|
|
4022
|
+
this.updateHeader();
|
|
3591
4023
|
} else if (e.type === "handoff") this.handoffState = e.state;
|
|
3592
4024
|
else if (e.type === "agentTyping") this.agentTyping = e.active;
|
|
4025
|
+
else if (e.type === "suggestions") this.serverSuggestions = e.items;
|
|
3593
4026
|
else if (e.type === "error") this.lastError = e.error.message;
|
|
3594
4027
|
this.updateStatus();
|
|
3595
4028
|
this.renderMessages();
|
|
3596
4029
|
}
|
|
3597
|
-
|
|
4030
|
+
updateHeader() {
|
|
3598
4031
|
if (this.titleEl) this.titleEl.textContent = this.getAttribute("title") ?? this.agentName ?? "Chat";
|
|
3599
4032
|
}
|
|
3600
4033
|
updateStatus() {
|
|
3601
4034
|
if (!this.textareaEl) return;
|
|
3602
4035
|
const disabled = this.state !== "ready";
|
|
3603
4036
|
this.textareaEl.disabled = disabled;
|
|
3604
|
-
|
|
3605
|
-
const
|
|
3606
|
-
this.statusEl.
|
|
3607
|
-
this.statusEl.textContent = st2 ?? "";
|
|
3608
|
-
if (this.state === "error" && this.lastError) {
|
|
3609
|
-
this.errEl.hidden = false;
|
|
3610
|
-
this.errEl.innerHTML = `<span>Couldn't start chat: ${escapeHtml(this.lastError)}</span><button class="retry" type="button">Retry</button>`;
|
|
3611
|
-
this.errEl.querySelector(".retry").addEventListener("click", () => {
|
|
3612
|
-
void this.client?.start();
|
|
3613
|
-
});
|
|
3614
|
-
} else {
|
|
3615
|
-
this.errEl.hidden = true;
|
|
3616
|
-
this.errEl.innerHTML = "";
|
|
3617
|
-
}
|
|
4037
|
+
const handoff = HANDOFF_STATUS[this.handoffState];
|
|
4038
|
+
const text2 = this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
|
|
4039
|
+
if (this.statusEl) this.statusEl.textContent = text2;
|
|
3618
4040
|
}
|
|
3619
4041
|
// ── Message list + scroll ─────────────────────────────────────────────────────────────────────
|
|
3620
4042
|
renderMessages() {
|
|
3621
4043
|
if (!this.bodyEl) return;
|
|
3622
4044
|
const body = this.bodyEl;
|
|
3623
|
-
const atBottom = this.open && this.pinned;
|
|
3624
4045
|
const prevTop = body.scrollTop;
|
|
3625
4046
|
const added = this.messages.length - this.renderedCount;
|
|
3626
|
-
const
|
|
3627
|
-
|
|
3628
|
-
|
|
4047
|
+
const botFinal = this.messages.filter((m2) => m2.role === "assistant" && !m2.streaming).length;
|
|
4048
|
+
const newBotFinal = botFinal > this.renderedBotFinal;
|
|
4049
|
+
const now = Date.now();
|
|
4050
|
+
for (const m2 of this.messages) if (!this.receivedAt.has(m2.id)) this.receivedAt.set(m2.id, now);
|
|
4051
|
+
const lastBot = (() => {
|
|
4052
|
+
for (let i = this.messages.length - 1; i >= 0; i--) if (this.messages[i]?.role === "assistant") return i;
|
|
4053
|
+
return -1;
|
|
4054
|
+
})();
|
|
4055
|
+
let html2 = this.messages.map((m2, i) => this.turnHtml(m2, i === lastBot && !this.agentTyping)).join("");
|
|
4056
|
+
if (this.agentTyping) html2 += '<span class="typing"><span></span><span></span><span></span></span>';
|
|
4057
|
+
const sugg = this.chipLabels;
|
|
4058
|
+
if (sugg.length > 0 && !this.agentTyping && this.state === "ready") {
|
|
4059
|
+
html2 += `<span class="chips">${sugg.map((s, i) => `<button type="button" class="chip" data-chip="${i}">${escapeHtml(s)}</button>`).join("")}</span>`;
|
|
4060
|
+
}
|
|
4061
|
+
if (this.state === "error" && this.lastError) {
|
|
4062
|
+
html2 += `<div class="oc-err"><span class="oc-err-t">${escapeHtml(this.lastError)}</span><button type="button" class="oc-err-retry">Try again</button></div>`;
|
|
4063
|
+
}
|
|
3629
4064
|
body.innerHTML = html2;
|
|
3630
4065
|
this.hydrateAttachmentImages();
|
|
4066
|
+
this.bindChips();
|
|
3631
4067
|
this.renderedCount = this.messages.length;
|
|
3632
|
-
|
|
4068
|
+
this.renderedBotFinal = botFinal;
|
|
4069
|
+
if (!this.uiOpen) {
|
|
4070
|
+
if (added > 0) this.unseen += added;
|
|
4071
|
+
this.updateClasses();
|
|
4072
|
+
return;
|
|
4073
|
+
}
|
|
4074
|
+
if (newBotFinal && !this.away) this.parkLastQuestion();
|
|
4075
|
+
else if (!this.away) this.scrollToBottom();
|
|
3633
4076
|
else {
|
|
3634
|
-
|
|
4077
|
+
body.scrollTop = prevTop;
|
|
3635
4078
|
if (added > 0) this.unseen += added;
|
|
3636
4079
|
}
|
|
3637
|
-
this.
|
|
3638
|
-
this.updateBadge();
|
|
4080
|
+
this.updateClasses();
|
|
3639
4081
|
}
|
|
3640
|
-
|
|
4082
|
+
// After a reply lands, float the question that prompted it to the top so the answer reads from its
|
|
4083
|
+
// first line rather than its last (the design's behaviour).
|
|
4084
|
+
parkLastQuestion() {
|
|
4085
|
+
const users = this.bodyEl.querySelectorAll(".turn.user");
|
|
4086
|
+
const last = users[users.length - 1];
|
|
4087
|
+
if (!last) {
|
|
4088
|
+
this.scrollToBottom();
|
|
4089
|
+
return;
|
|
4090
|
+
}
|
|
4091
|
+
const top = Math.max(0, Math.min(last.offsetTop - 22, this.bodyEl.scrollHeight - this.bodyEl.clientHeight));
|
|
4092
|
+
this.bodyEl.scrollTo({ top, behavior: "smooth" });
|
|
4093
|
+
}
|
|
4094
|
+
onScroll() {
|
|
3641
4095
|
const b2 = this.bodyEl;
|
|
3642
|
-
const
|
|
3643
|
-
this.
|
|
3644
|
-
|
|
3645
|
-
this.unseen = 0;
|
|
3646
|
-
this.
|
|
4096
|
+
const away = b2.scrollHeight - b2.scrollTop - b2.clientHeight > AWAY_THRESHOLD;
|
|
4097
|
+
if (away !== this.away) {
|
|
4098
|
+
this.away = away;
|
|
4099
|
+
if (!away) this.unseen = 0;
|
|
4100
|
+
this.updateClasses();
|
|
3647
4101
|
}
|
|
3648
|
-
this.updatePill();
|
|
3649
4102
|
}
|
|
3650
4103
|
scrollToBottom() {
|
|
3651
4104
|
if (!this.bodyEl) return;
|
|
3652
4105
|
this.bodyEl.scrollTop = this.bodyEl.scrollHeight;
|
|
3653
|
-
this.
|
|
4106
|
+
this.away = false;
|
|
3654
4107
|
this.unseen = 0;
|
|
3655
|
-
this.
|
|
3656
|
-
this.updateBadge();
|
|
3657
|
-
}
|
|
3658
|
-
updatePill() {
|
|
3659
|
-
if (!this.pillEl) return;
|
|
3660
|
-
this.pillEl.hidden = !(this.open && !this.pinned && this.unseen > 0);
|
|
3661
|
-
const t = this.pillEl.querySelector(".pill-text");
|
|
3662
|
-
if (t) t.textContent = this.unseen === 1 ? "1 new message" : `${this.unseen} new messages`;
|
|
3663
|
-
}
|
|
3664
|
-
updateBadge() {
|
|
3665
|
-
if (!this.badgeEl) return;
|
|
3666
|
-
const show = !this.open && this.unseen > 0;
|
|
3667
|
-
this.badgeEl.hidden = !show;
|
|
3668
|
-
this.badgeEl.textContent = this.unseen > 9 ? "9+" : String(this.unseen);
|
|
4108
|
+
this.updateClasses();
|
|
3669
4109
|
}
|
|
3670
4110
|
// ── Composer ──────────────────────────────────────────────────────────────────────────────────
|
|
3671
4111
|
autoGrow() {
|
|
3672
4112
|
const ta = this.textareaEl;
|
|
3673
4113
|
ta.style.height = "auto";
|
|
3674
|
-
ta.style.height = `${Math.min(ta.scrollHeight,
|
|
4114
|
+
ta.style.height = `${Math.min(ta.scrollHeight, 108)}px`;
|
|
3675
4115
|
}
|
|
3676
4116
|
doSend() {
|
|
3677
4117
|
const text2 = this.textareaEl.value;
|
|
@@ -3684,15 +4124,66 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3684
4124
|
this.imagePreviewCache.set(f2.name, URL.createObjectURL(f2));
|
|
3685
4125
|
}
|
|
3686
4126
|
}
|
|
3687
|
-
this.
|
|
4127
|
+
this.away = false;
|
|
4128
|
+
this.emojiOpen = false;
|
|
3688
4129
|
void this.client?.sendMessage(text2, files.length > 0 ? files : void 0);
|
|
3689
4130
|
this.textareaEl.value = "";
|
|
3690
4131
|
this.textareaEl.style.height = "auto";
|
|
4132
|
+
this.updateClasses();
|
|
4133
|
+
}
|
|
4134
|
+
sendChip(text2) {
|
|
4135
|
+
this.away = false;
|
|
4136
|
+
this.emojiOpen = false;
|
|
4137
|
+
void this.client?.sendMessage(text2);
|
|
4138
|
+
this.updateClasses();
|
|
4139
|
+
}
|
|
4140
|
+
bindChips() {
|
|
4141
|
+
const sugg = this.chipLabels;
|
|
4142
|
+
this.bodyEl.querySelectorAll(".chip[data-chip]").forEach((btn) => {
|
|
4143
|
+
const i = Number(btn.getAttribute("data-chip"));
|
|
4144
|
+
const text2 = sugg[i];
|
|
4145
|
+
if (text2) btn.addEventListener("click", () => this.sendChip(text2));
|
|
4146
|
+
});
|
|
4147
|
+
this.bodyEl.querySelector(".oc-err-retry")?.addEventListener("click", () => {
|
|
4148
|
+
void this.client?.start();
|
|
4149
|
+
});
|
|
4150
|
+
}
|
|
4151
|
+
// ── Emoji ─────────────────────────────────────────────────────────────────────────────────────
|
|
4152
|
+
toggleEmoji() {
|
|
4153
|
+
this.emojiOpen = !this.emojiOpen;
|
|
4154
|
+
this.emojiQuery = "";
|
|
4155
|
+
this.emojiSearchEl.value = "";
|
|
4156
|
+
this.renderEmoji();
|
|
4157
|
+
this.updateClasses();
|
|
4158
|
+
if (this.emojiOpen) setTimeout(() => this.emojiSearchEl.focus(), 60);
|
|
4159
|
+
}
|
|
4160
|
+
closeEmoji() {
|
|
4161
|
+
this.emojiOpen = false;
|
|
4162
|
+
this.emojiQuery = "";
|
|
4163
|
+
this.updateClasses();
|
|
4164
|
+
}
|
|
4165
|
+
renderEmoji() {
|
|
4166
|
+
const groups = emojiGroups(this.emojiQuery);
|
|
4167
|
+
let html2 = groups.map((g) => `
|
|
4168
|
+
<div class="emoji-grp"><span class="emoji-grp-name">${escapeHtml(g.name)}</span>
|
|
4169
|
+
<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>
|
|
4170
|
+
</div>`).join("");
|
|
4171
|
+
if (groups.length === 0) html2 = '<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>';
|
|
4172
|
+
this.emojiListEl.innerHTML = html2;
|
|
4173
|
+
this.emojiListEl.querySelectorAll("button[data-ch]").forEach((btn) => {
|
|
4174
|
+
btn.addEventListener("click", () => {
|
|
4175
|
+
this.textareaEl.value += btn.getAttribute("data-ch") ?? "";
|
|
4176
|
+
this.emojiOpen = false;
|
|
4177
|
+
this.autoGrow();
|
|
4178
|
+
this.updateClasses();
|
|
4179
|
+
this.textareaEl.focus();
|
|
4180
|
+
});
|
|
4181
|
+
});
|
|
3691
4182
|
}
|
|
3692
4183
|
// ── Bubble rendering ──────────────────────────────────────────────────────────────────────────
|
|
3693
|
-
|
|
4184
|
+
turnHtml(m2, isLastBot) {
|
|
4185
|
+
const role = m2.role === "user" ? "user" : m2.role === "human" ? "human" : "bot";
|
|
3694
4186
|
const atts = (m2.attachments ?? []).map((a) => this.renderAttachment(a)).join("");
|
|
3695
|
-
const label = m2.role === "human" ? '<div class="human-label">Support</div>' : "";
|
|
3696
4187
|
let content;
|
|
3697
4188
|
if (m2.role === "assistant") {
|
|
3698
4189
|
let rendered = m2.streaming ? void 0 : this.mdCache.get(m2.id);
|
|
@@ -3700,30 +4191,32 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3700
4191
|
rendered = renderMarkdown(m2.text || "");
|
|
3701
4192
|
if (!m2.streaming) this.mdCache.set(m2.id, rendered);
|
|
3702
4193
|
}
|
|
3703
|
-
content = `<
|
|
4194
|
+
content = `<span class="b">${atts}<span class="md">${rendered}</span></span>`;
|
|
3704
4195
|
} else {
|
|
3705
|
-
|
|
4196
|
+
const label = m2.role === "human" ? '<div class="human-label">Support</div>' : "";
|
|
4197
|
+
content = `<span class="b">${label}${atts}${escapeHtml(m2.text)}</span>`;
|
|
3706
4198
|
}
|
|
3707
|
-
const
|
|
3708
|
-
const
|
|
3709
|
-
|
|
3710
|
-
|
|
4199
|
+
const src = m2.sources && m2.sources.length > 0 ? `<span class="src">${ICON.book}<span class="t">${escapeHtml("From " + (m2.sources[0]?.title ?? ""))}</span></span>` : "";
|
|
4200
|
+
const byline = role === "bot" && isLastBot ? `<span class="byline">${escapeHtml((this.agentName ?? "OyeHello") + " \xB7 AI agent \xB7 " + this.relTime(m2.id))}</span>` : "";
|
|
4201
|
+
return `<span class="turn ${role}">${content}${src}${byline}</span>`;
|
|
4202
|
+
}
|
|
4203
|
+
relTime(id) {
|
|
4204
|
+
const t = this.receivedAt.get(id);
|
|
4205
|
+
if (!t) return "just now";
|
|
4206
|
+
const mins = Math.floor((Date.now() - t) / 6e4);
|
|
4207
|
+
return mins <= 0 ? "just now" : `${mins} min ago`;
|
|
3711
4208
|
}
|
|
3712
4209
|
renderAttachment(a) {
|
|
3713
4210
|
if (a.contentType.startsWith("image/")) {
|
|
3714
4211
|
const cached = (a.name ? this.imagePreviewCache.get(a.name) : void 0) ?? this.resolvedUrlCache.get(a.storageRef);
|
|
3715
4212
|
const srcAttr = cached ? ` src="${cached}"` : "";
|
|
3716
|
-
return `<img class="att-thumb" data-oc-ref="${
|
|
4213
|
+
return `<img class="att-thumb" data-oc-ref="${this.escapeAttr(a.storageRef)}"${srcAttr} alt="${this.escapeAttr(a.name ?? "image")}"/>`;
|
|
3717
4214
|
}
|
|
3718
4215
|
return `<div class="att-chip">\u{1F4CE} ${escapeHtml(a.name ?? a.contentType)}</div>`;
|
|
3719
4216
|
}
|
|
3720
|
-
renderSource(s) {
|
|
3721
|
-
return `<div class="src-chip" title="${escapeHtml(s.snippet)}">${escapeHtml(s.title)}</div>`;
|
|
3722
|
-
}
|
|
3723
4217
|
hydrateAttachmentImages() {
|
|
3724
4218
|
if (!this.client) return;
|
|
3725
|
-
|
|
3726
|
-
imgs.forEach((img) => {
|
|
4219
|
+
this.shadowRoot.querySelectorAll("img.att-thumb[data-oc-ref]").forEach((img) => {
|
|
3727
4220
|
if (img.hasAttribute("src")) return;
|
|
3728
4221
|
const ref = img.getAttribute("data-oc-ref");
|
|
3729
4222
|
if (!ref) return;
|
|
@@ -3734,6 +4227,9 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3734
4227
|
});
|
|
3735
4228
|
});
|
|
3736
4229
|
}
|
|
4230
|
+
escapeAttr(s) {
|
|
4231
|
+
return s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
4232
|
+
}
|
|
3737
4233
|
};
|
|
3738
4234
|
function escapeHtml(s) {
|
|
3739
4235
|
return s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|