@oyehello/chat 0.3.0 → 0.5.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 +4 -1
- package/dist/index.cjs +299 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -3
- package/dist/index.d.ts +42 -3
- package/dist/index.global.js +307 -41
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +299 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.global.js
CHANGED
|
@@ -15,6 +15,9 @@ var OyehelloChat = (function (exports) {
|
|
|
15
15
|
meta;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
// src/protocol.ts
|
|
19
|
+
var CHAT_PROTOCOL_VERSION = 1;
|
|
20
|
+
|
|
18
21
|
// src/transport.ts
|
|
19
22
|
async function asError(res, fallbackCode) {
|
|
20
23
|
let body = {};
|
|
@@ -25,9 +28,10 @@ var OyehelloChat = (function (exports) {
|
|
|
25
28
|
return new ChatApiError(body.error ?? `HTTP ${res.status}`, res.status, body.code ?? fallbackCode, body.meta);
|
|
26
29
|
}
|
|
27
30
|
async function openSession(opts) {
|
|
28
|
-
const body = { agentId: opts.agentId };
|
|
31
|
+
const body = { agentId: opts.agentId, protocol: CHAT_PROTOCOL_VERSION };
|
|
29
32
|
if (opts.user) body.user = opts.user;
|
|
30
33
|
if (opts.clientTools && opts.clientTools.length > 0) body.clientTools = opts.clientTools;
|
|
34
|
+
if (opts.fingerprint) body.fingerprint = opts.fingerprint;
|
|
31
35
|
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions`, {
|
|
32
36
|
method: "POST",
|
|
33
37
|
credentials: "omit",
|
|
@@ -37,6 +41,15 @@ var OyehelloChat = (function (exports) {
|
|
|
37
41
|
if (!res.ok) throw await asError(res);
|
|
38
42
|
return await res.json();
|
|
39
43
|
}
|
|
44
|
+
async function getWidgetConfig(opts) {
|
|
45
|
+
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/widget-config?agentId=${encodeURIComponent(opts.agentId)}`, {
|
|
46
|
+
method: "GET",
|
|
47
|
+
credentials: "omit",
|
|
48
|
+
headers: { Authorization: `Bearer ${opts.apiKey}` }
|
|
49
|
+
});
|
|
50
|
+
if (!res.ok) throw await asError(res);
|
|
51
|
+
return await res.json();
|
|
52
|
+
}
|
|
40
53
|
async function postMessage(opts) {
|
|
41
54
|
const body = { text: opts.text };
|
|
42
55
|
if (opts.attachments && opts.attachments.length > 0) body.attachments = opts.attachments;
|
|
@@ -102,6 +115,50 @@ var OyehelloChat = (function (exports) {
|
|
|
102
115
|
if (!res.ok) throw new ChatApiError(`attachment upload failed with HTTP ${res.status}`, res.status);
|
|
103
116
|
}
|
|
104
117
|
|
|
118
|
+
// src/fingerprint.ts
|
|
119
|
+
async function sha256Hex(input) {
|
|
120
|
+
const g = globalThis;
|
|
121
|
+
try {
|
|
122
|
+
if (g.crypto?.subtle && typeof TextEncoder !== "undefined") {
|
|
123
|
+
const buf = await g.crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
124
|
+
return Array.from(new Uint8Array(buf)).map((b2) => b2.toString(16).padStart(2, "0")).join("").slice(0, 32);
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
let h = 5381;
|
|
129
|
+
for (let i = 0; i < input.length; i++) h = (h << 5) + h + input.charCodeAt(i) >>> 0;
|
|
130
|
+
return h.toString(16).padStart(8, "0");
|
|
131
|
+
}
|
|
132
|
+
async function computeDeviceFingerprint() {
|
|
133
|
+
try {
|
|
134
|
+
const nav = typeof navigator !== "undefined" ? navigator : null;
|
|
135
|
+
if (!nav) return null;
|
|
136
|
+
const scr = typeof screen !== "undefined" ? screen : null;
|
|
137
|
+
const tz = (() => {
|
|
138
|
+
try {
|
|
139
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
140
|
+
} catch {
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
})();
|
|
144
|
+
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : "";
|
|
145
|
+
const signals = [
|
|
146
|
+
nav.userAgent,
|
|
147
|
+
Array.isArray(nav.languages) ? nav.languages.join(",") : "",
|
|
148
|
+
nav.language,
|
|
149
|
+
nav.platform,
|
|
150
|
+
nav.hardwareConcurrency,
|
|
151
|
+
nav.deviceMemory,
|
|
152
|
+
scr ? `${scr.width}x${scr.height}x${scr.colorDepth}` : "",
|
|
153
|
+
dpr,
|
|
154
|
+
tz
|
|
155
|
+
].map((v2) => String(v2 ?? "")).join("|");
|
|
156
|
+
return await sha256Hex(signals);
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
105
162
|
// src/client.ts
|
|
106
163
|
var seq = 0;
|
|
107
164
|
var uid = (p) => `${p}_${Date.now().toString(36)}_${(seq++).toString(36)}`;
|
|
@@ -122,6 +179,8 @@ var OyehelloChat = (function (exports) {
|
|
|
122
179
|
let reconnectTimer = null;
|
|
123
180
|
let reconnectAttempts = 0;
|
|
124
181
|
let closed = false;
|
|
182
|
+
let warnedProtocol = false;
|
|
183
|
+
let deviceFingerprint;
|
|
125
184
|
const toolHandlers = /* @__PURE__ */ new Map();
|
|
126
185
|
const clearReconnect = () => {
|
|
127
186
|
if (reconnectTimer) {
|
|
@@ -214,6 +273,14 @@ var OyehelloChat = (function (exports) {
|
|
|
214
273
|
}
|
|
215
274
|
} else if (ev.type === "agent_typing") {
|
|
216
275
|
emit({ type: "agentTyping", active: !!ev.active });
|
|
276
|
+
} else if (ev.type === "suggestions") {
|
|
277
|
+
emit({ type: "suggestions", items: Array.isArray(ev.items) ? ev.items : [] });
|
|
278
|
+
} else if (ev.type === "done") {
|
|
279
|
+
closed = true;
|
|
280
|
+
clearReconnect();
|
|
281
|
+
es?.close();
|
|
282
|
+
es = null;
|
|
283
|
+
setState("ended");
|
|
217
284
|
}
|
|
218
285
|
};
|
|
219
286
|
const openStream = async () => {
|
|
@@ -256,7 +323,15 @@ var OyehelloChat = (function (exports) {
|
|
|
256
323
|
}, delay);
|
|
257
324
|
};
|
|
258
325
|
const open = async () => {
|
|
259
|
-
|
|
326
|
+
if (deviceFingerprint === void 0) deviceFingerprint = await computeDeviceFingerprint().catch(() => null);
|
|
327
|
+
session = await openSession({ baseUrl: options.baseUrl, apiKey: options.apiKey, agentId: options.agentId, user: options.user, clientTools: Array.from(toolHandlers.keys()), fingerprint: deviceFingerprint ?? void 0, fetchImpl });
|
|
328
|
+
if (!warnedProtocol && typeof session.protocol === "number" && session.protocol !== CHAT_PROTOCOL_VERSION) {
|
|
329
|
+
warnedProtocol = true;
|
|
330
|
+
try {
|
|
331
|
+
console.warn(`[oyehello-chat] SDK protocol v${CHAT_PROTOCOL_VERSION} vs server v${session.protocol} - update @oyehello/chat to the latest version.`);
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
334
|
+
}
|
|
260
335
|
emit({ type: "agent", agent: session.agent });
|
|
261
336
|
await openStream();
|
|
262
337
|
};
|
|
@@ -281,6 +356,9 @@ var OyehelloChat = (function (exports) {
|
|
|
281
356
|
if (!session) throw new Error("call start() before getAttachmentUrl");
|
|
282
357
|
return getAttachmentUrl({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, storageRef, fetchImpl });
|
|
283
358
|
},
|
|
359
|
+
getWidgetConfig() {
|
|
360
|
+
return getWidgetConfig({ baseUrl: options.baseUrl, apiKey: options.apiKey, agentId: options.agentId, fetchImpl });
|
|
361
|
+
},
|
|
284
362
|
async start() {
|
|
285
363
|
if (state === "opening" || state === "ready") return;
|
|
286
364
|
closed = false;
|
|
@@ -344,6 +422,20 @@ var OyehelloChat = (function (exports) {
|
|
|
344
422
|
es?.close();
|
|
345
423
|
es = null;
|
|
346
424
|
setState("ended");
|
|
425
|
+
},
|
|
426
|
+
reset() {
|
|
427
|
+
es?.close();
|
|
428
|
+
es = null;
|
|
429
|
+
clearReconnect();
|
|
430
|
+
session = null;
|
|
431
|
+
streamingId = null;
|
|
432
|
+
handoffState = "ai";
|
|
433
|
+
messages.length = 0;
|
|
434
|
+
seenTurnIds.clear();
|
|
435
|
+
seenCallIds.clear();
|
|
436
|
+
emit({ type: "messages", messages: [] });
|
|
437
|
+
emit({ type: "handoff", state: "ai" });
|
|
438
|
+
setState("idle");
|
|
347
439
|
}
|
|
348
440
|
};
|
|
349
441
|
}
|
|
@@ -1972,8 +2064,8 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
1972
2064
|
DOMPurify.isSupported = false;
|
|
1973
2065
|
return DOMPurify;
|
|
1974
2066
|
}
|
|
1975
|
-
let
|
|
1976
|
-
const originalDocument =
|
|
2067
|
+
let document2 = window2.document;
|
|
2068
|
+
const originalDocument = document2;
|
|
1977
2069
|
const currentScript = originalDocument.currentScript;
|
|
1978
2070
|
window2.DocumentFragment;
|
|
1979
2071
|
const HTMLTemplateElement = window2.HTMLTemplateElement, Node = window2.Node, Element = window2.Element, NodeFilter = window2.NodeFilter, _window$NamedNodeMap = window2.NamedNodeMap;
|
|
@@ -1999,9 +2091,9 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
1999
2091
|
return getNodeName ? getNodeName(node) : node.nodeName;
|
|
2000
2092
|
};
|
|
2001
2093
|
if (typeof HTMLTemplateElement === "function") {
|
|
2002
|
-
const template =
|
|
2094
|
+
const template = document2.createElement("template");
|
|
2003
2095
|
if (template.content && template.content.ownerDocument) {
|
|
2004
|
-
|
|
2096
|
+
document2 = template.content.ownerDocument;
|
|
2005
2097
|
}
|
|
2006
2098
|
}
|
|
2007
2099
|
let trustedTypesPolicy;
|
|
@@ -2039,7 +2131,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
2039
2131
|
}
|
|
2040
2132
|
return defaultTrustedTypesPolicy;
|
|
2041
2133
|
};
|
|
2042
|
-
const _document =
|
|
2134
|
+
const _document = document2, implementation = _document.implementation, createNodeIterator = _document.createNodeIterator, createDocumentFragment = _document.createDocumentFragment, getElementsByTagName = _document.getElementsByTagName;
|
|
2043
2135
|
const importNode = originalDocument.importNode;
|
|
2044
2136
|
let hooks = _createHooksMap();
|
|
2045
2137
|
DOMPurify.isSupported = typeof entries === "function" && typeof getParentNode === "function" && implementation && implementation.createHTMLDocument !== void 0;
|
|
@@ -2164,7 +2256,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
2164
2256
|
const DEFAULT_PARSER_MEDIA_TYPE = "text/html";
|
|
2165
2257
|
let transformCaseFunc = null;
|
|
2166
2258
|
let CONFIG = null;
|
|
2167
|
-
const formElement =
|
|
2259
|
+
const formElement = document2.createElement("form");
|
|
2168
2260
|
const isRegexOrFunction = function isRegexOrFunction2(testValue) {
|
|
2169
2261
|
return testValue instanceof RegExp || testValue instanceof Function;
|
|
2170
2262
|
};
|
|
@@ -2592,7 +2684,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
2592
2684
|
}
|
|
2593
2685
|
const body = doc.body || doc.documentElement;
|
|
2594
2686
|
if (dirty && leadingWhitespace) {
|
|
2595
|
-
body.insertBefore(
|
|
2687
|
+
body.insertBefore(document2.createTextNode(leadingWhitespace), body.childNodes[0] || null);
|
|
2596
2688
|
}
|
|
2597
2689
|
if (NAMESPACE === HTML_NAMESPACE) {
|
|
2598
2690
|
return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? "html" : "body")[0];
|
|
@@ -2827,7 +2919,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
2827
2919
|
if (_isPatchLinkageAttribute(lcName, lcTag)) {
|
|
2828
2920
|
return false;
|
|
2829
2921
|
}
|
|
2830
|
-
if (SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in
|
|
2922
|
+
if (SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in document2 || value in formElement)) {
|
|
2831
2923
|
return false;
|
|
2832
2924
|
}
|
|
2833
2925
|
const nameIsPermitted = ALLOWED_ATTR2[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag);
|
|
@@ -3407,8 +3499,17 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3407
3499
|
.i-expand { opacity: 1; } .i-shrink { opacity: 0; }
|
|
3408
3500
|
.oc.max .i-expand { opacity: 0; } .oc.max .i-shrink { opacity: 1; }
|
|
3409
3501
|
|
|
3410
|
-
|
|
3411
|
-
.err
|
|
3502
|
+
/* Inline connection error, shown inside the chat window (no banner) */
|
|
3503
|
+
.oc-err { margin: auto; display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 24px 16px; text-align: center; }
|
|
3504
|
+
.oc-err-t { font-size: 13px; line-height: 1.5; color: var(--oc-fg-muted); text-wrap: pretty; }
|
|
3505
|
+
.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); }
|
|
3506
|
+
.oc-err-retry:hover { background: var(--oc-tint-3); }
|
|
3507
|
+
|
|
3508
|
+
/* Session-ended state, inside the chat window */
|
|
3509
|
+
.oc-ended { margin: auto; display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 24px 16px; text-align: center; }
|
|
3510
|
+
.oc-ended-t { font-size: 13.5px; line-height: 1.5; color: var(--oc-fg-muted); text-wrap: pretty; }
|
|
3511
|
+
.oc-newmsg { padding: 9px 18px; font-size: 13px; font-weight: 600; border-radius: var(--oc-r-full); border: 1px solid var(--oc-btn-rim); background: var(--oc-ac); color: #fff; box-shadow: var(--oc-btn-edge); cursor: pointer; transition: filter .18s var(--oc-ease); }
|
|
3512
|
+
.oc-newmsg:hover { filter: brightness(1.08); }
|
|
3412
3513
|
|
|
3413
3514
|
/* \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 */
|
|
3414
3515
|
.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; }
|
|
@@ -3676,6 +3777,24 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3676
3777
|
renderedCount = 0;
|
|
3677
3778
|
renderedBotFinal = 0;
|
|
3678
3779
|
timers = [];
|
|
3780
|
+
// Server (flow) suggestions for the latest turn; null until the first `suggestions` event, then the
|
|
3781
|
+
// widget prefers these over the static `suggestions` attribute.
|
|
3782
|
+
serverSuggestions = null;
|
|
3783
|
+
// The live streaming assistant bubble's markdown element + message id — so growing tokens patch just
|
|
3784
|
+
// that element in place instead of rebuilding the whole message list on every token.
|
|
3785
|
+
streamId = null;
|
|
3786
|
+
streamEl = null;
|
|
3787
|
+
audioCtx = null;
|
|
3788
|
+
teaserText = "Need a hand? Ask me anything.";
|
|
3789
|
+
teaserShownOnce = false;
|
|
3790
|
+
// Staffed-hours: set from widget-config. When offline, the panel shows offlineMessage and the
|
|
3791
|
+
// composer is disabled (opening a session is refused server-side anyway).
|
|
3792
|
+
offline = false;
|
|
3793
|
+
offlineMessage = "We're offline right now. Please check back during business hours.";
|
|
3794
|
+
configLoaded = false;
|
|
3795
|
+
// widget-config resolved (so we know online/offline before starting)
|
|
3796
|
+
idleTimer = null;
|
|
3797
|
+
idleReset = null;
|
|
3679
3798
|
// ── shadow refs ──
|
|
3680
3799
|
oc;
|
|
3681
3800
|
bodyEl;
|
|
@@ -3683,7 +3802,6 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3683
3802
|
fileEl;
|
|
3684
3803
|
titleEl;
|
|
3685
3804
|
statusEl;
|
|
3686
|
-
errEl;
|
|
3687
3805
|
emojiListEl;
|
|
3688
3806
|
emojiSearchEl;
|
|
3689
3807
|
connectedCallback() {
|
|
@@ -3695,6 +3813,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3695
3813
|
this.renderError("oyehello-chat: api-key, agent-id and base-url are required");
|
|
3696
3814
|
return;
|
|
3697
3815
|
}
|
|
3816
|
+
this.teaserText = this.getAttribute("teaser") || this.teaserText;
|
|
3698
3817
|
this.client = createChatClient({ apiKey, agentId, baseUrl });
|
|
3699
3818
|
if (Object.keys(this.pendingTools).length > 0) {
|
|
3700
3819
|
this.client.registerTools(this.pendingTools);
|
|
@@ -3706,6 +3825,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3706
3825
|
this.updateHeader();
|
|
3707
3826
|
this.updateStatus();
|
|
3708
3827
|
this.updateClasses();
|
|
3828
|
+
void this.loadWidgetConfig();
|
|
3709
3829
|
let wasOpen = false;
|
|
3710
3830
|
try {
|
|
3711
3831
|
wasOpen = sessionStorage.getItem(this.key("open")) === "1";
|
|
@@ -3719,24 +3839,93 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3719
3839
|
this.timers.push(setTimeout(() => {
|
|
3720
3840
|
this.oc?.classList.remove("ring");
|
|
3721
3841
|
}, 6e3));
|
|
3722
|
-
|
|
3842
|
+
this.armIdleTeaser();
|
|
3843
|
+
}
|
|
3844
|
+
// Show the teaser once the visitor has been idle (no page interaction) for 10s — unless the panel
|
|
3845
|
+
// is already open or the teaser was dismissed this session. Fires at most once.
|
|
3846
|
+
armIdleTeaser() {
|
|
3723
3847
|
let dismissed = false;
|
|
3724
3848
|
try {
|
|
3725
3849
|
dismissed = sessionStorage.getItem(this.key("teaser")) === "0";
|
|
3726
3850
|
} catch {
|
|
3727
3851
|
}
|
|
3728
|
-
if (
|
|
3729
|
-
|
|
3730
|
-
|
|
3852
|
+
if (dismissed) return;
|
|
3853
|
+
const reset = () => {
|
|
3854
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
3855
|
+
this.idleTimer = setTimeout(() => {
|
|
3856
|
+
if (!this.uiOpen && !this.teaserShownOnce) {
|
|
3731
3857
|
this.teaserActive = true;
|
|
3858
|
+
this.teaserShownOnce = true;
|
|
3732
3859
|
this.updateClasses();
|
|
3860
|
+
this.teardownIdle();
|
|
3733
3861
|
}
|
|
3734
|
-
},
|
|
3862
|
+
}, 1e4);
|
|
3863
|
+
};
|
|
3864
|
+
this.idleReset = reset;
|
|
3865
|
+
for (const ev of ["mousemove", "keydown", "scroll", "touchstart", "click"]) document.addEventListener(ev, reset, { passive: true });
|
|
3866
|
+
reset();
|
|
3867
|
+
}
|
|
3868
|
+
teardownIdle() {
|
|
3869
|
+
if (this.idleTimer) {
|
|
3870
|
+
clearTimeout(this.idleTimer);
|
|
3871
|
+
this.idleTimer = null;
|
|
3735
3872
|
}
|
|
3873
|
+
if (this.idleReset) {
|
|
3874
|
+
for (const ev of ["mousemove", "keydown", "scroll", "touchstart", "click"]) document.removeEventListener(ev, this.idleReset);
|
|
3875
|
+
this.idleReset = null;
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
async loadWidgetConfig() {
|
|
3879
|
+
try {
|
|
3880
|
+
const cfg = await this.client.getWidgetConfig();
|
|
3881
|
+
if (!this.getAttribute("teaser") && cfg.teaser) {
|
|
3882
|
+
this.teaserText = cfg.teaser;
|
|
3883
|
+
this.applyTeaserText();
|
|
3884
|
+
}
|
|
3885
|
+
if (cfg.brand) {
|
|
3886
|
+
if (cfg.brand.accentColor) this.style.setProperty("--oc-accent", cfg.brand.accentColor);
|
|
3887
|
+
if (cfg.brand.name && !this.getAttribute("title")) {
|
|
3888
|
+
this.agentName = cfg.brand.name;
|
|
3889
|
+
this.updateHeader();
|
|
3890
|
+
}
|
|
3891
|
+
if (cfg.brand.logoUrl) this.applyBrandLogo(cfg.brand.logoUrl);
|
|
3892
|
+
}
|
|
3893
|
+
if (cfg.offline) {
|
|
3894
|
+
this.offline = true;
|
|
3895
|
+
if (cfg.offlineMessage) this.offlineMessage = cfg.offlineMessage;
|
|
3896
|
+
this.renderMessages();
|
|
3897
|
+
this.updateStatus();
|
|
3898
|
+
}
|
|
3899
|
+
} catch {
|
|
3900
|
+
} finally {
|
|
3901
|
+
this.configLoaded = true;
|
|
3902
|
+
if (this.uiOpen && !this.started && !this.offline && this.client) {
|
|
3903
|
+
this.started = true;
|
|
3904
|
+
void this.client.start();
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
applyTeaserText() {
|
|
3909
|
+
const el = this.shadowRoot?.querySelector(".teaser-btn span:last-child");
|
|
3910
|
+
if (el) el.textContent = this.teaserText;
|
|
3911
|
+
}
|
|
3912
|
+
applyBrandLogo(url) {
|
|
3913
|
+
const set = (host) => {
|
|
3914
|
+
if (!host) return;
|
|
3915
|
+
host.textContent = "";
|
|
3916
|
+
const img = document.createElement("img");
|
|
3917
|
+
img.src = url;
|
|
3918
|
+
img.alt = "";
|
|
3919
|
+
img.style.cssText = "width:100%;height:100%;object-fit:contain";
|
|
3920
|
+
host.appendChild(img);
|
|
3921
|
+
};
|
|
3922
|
+
set(this.shadowRoot?.querySelector(".hd .logo"));
|
|
3923
|
+
set(this.shadowRoot?.querySelector(".teaser-btn .logo"));
|
|
3736
3924
|
}
|
|
3737
3925
|
disconnectedCallback() {
|
|
3738
3926
|
this.off?.();
|
|
3739
3927
|
void this.client?.end();
|
|
3928
|
+
this.teardownIdle();
|
|
3740
3929
|
for (const t of this.timers) clearTimeout(t);
|
|
3741
3930
|
for (const url of this.imagePreviewCache.values()) {
|
|
3742
3931
|
if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
|
|
@@ -3750,18 +3939,22 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3750
3939
|
key(k2) {
|
|
3751
3940
|
return `oyehello-chat:${k2}:${this.getAttribute("agent-id") ?? ""}`;
|
|
3752
3941
|
}
|
|
3753
|
-
|
|
3942
|
+
// Static fallback chips from the `suggestions` attribute (for embedders without flow-driven ones).
|
|
3943
|
+
get attrSuggestions() {
|
|
3754
3944
|
return (this.getAttribute("suggestions") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
3755
3945
|
}
|
|
3946
|
+
// Effective chips: server (flow) suggestions once received, else the static attribute fallback.
|
|
3947
|
+
get chipLabels() {
|
|
3948
|
+
return this.serverSuggestions ?? this.attrSuggestions;
|
|
3949
|
+
}
|
|
3756
3950
|
// ── Shell (built once) ─────────────────────────────────────────────────────────────────────────
|
|
3757
3951
|
renderShell() {
|
|
3758
3952
|
const root = this.shadowRoot;
|
|
3759
|
-
const hasTeaser = !!this.getAttribute("teaser");
|
|
3760
3953
|
const hasPrivacy = !!this.getAttribute("privacy-url");
|
|
3761
|
-
const teaserText = this.
|
|
3954
|
+
const teaserText = this.teaserText;
|
|
3762
3955
|
root.innerHTML = `
|
|
3763
3956
|
<style>${STYLES}</style>
|
|
3764
|
-
<div class="oc
|
|
3957
|
+
<div class="oc has-teaser${hasPrivacy ? " has-privacy" : ""}">
|
|
3765
3958
|
<div class="panel" role="dialog" aria-label="Chat">
|
|
3766
3959
|
<div class="hd">
|
|
3767
3960
|
<span class="hd-grid"></span>
|
|
@@ -3770,7 +3963,6 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3770
3963
|
<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>
|
|
3771
3964
|
<button type="button" class="hd-btn min" aria-label="Minimise">${ICON.minus}</button>
|
|
3772
3965
|
</div>
|
|
3773
|
-
<div class="err" hidden></div>
|
|
3774
3966
|
<div class="body"></div>
|
|
3775
3967
|
<span class="emoji-scrim"></span>
|
|
3776
3968
|
<div class="emoji-panel">
|
|
@@ -3814,7 +4006,6 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3814
4006
|
this.fileEl = root.querySelector('input[type="file"]');
|
|
3815
4007
|
this.titleEl = root.querySelector(".hd-title");
|
|
3816
4008
|
this.statusEl = root.querySelector(".hd-status .t");
|
|
3817
|
-
this.errEl = root.querySelector(".err");
|
|
3818
4009
|
this.emojiListEl = root.querySelector(".emoji-list");
|
|
3819
4010
|
this.emojiSearchEl = root.querySelector(".emoji-search input");
|
|
3820
4011
|
root.querySelector(".launcher").addEventListener("click", () => this.toggle());
|
|
@@ -3895,11 +4086,12 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3895
4086
|
this.uiOpen = true;
|
|
3896
4087
|
this.teaserActive = false;
|
|
3897
4088
|
this.unseen = 0;
|
|
4089
|
+
this.teardownIdle();
|
|
3898
4090
|
try {
|
|
3899
4091
|
sessionStorage.setItem(this.key("open"), "1");
|
|
3900
4092
|
} catch {
|
|
3901
4093
|
}
|
|
3902
|
-
if (!this.started && this.client) {
|
|
4094
|
+
if (!this.started && this.client && this.configLoaded && !this.offline) {
|
|
3903
4095
|
this.started = true;
|
|
3904
4096
|
void this.client.start();
|
|
3905
4097
|
}
|
|
@@ -3937,6 +4129,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3937
4129
|
this.updateHeader();
|
|
3938
4130
|
} else if (e.type === "handoff") this.handoffState = e.state;
|
|
3939
4131
|
else if (e.type === "agentTyping") this.agentTyping = e.active;
|
|
4132
|
+
else if (e.type === "suggestions") this.serverSuggestions = e.items;
|
|
3940
4133
|
else if (e.type === "error") this.lastError = e.error.message;
|
|
3941
4134
|
this.updateStatus();
|
|
3942
4135
|
this.renderMessages();
|
|
@@ -3946,26 +4139,22 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3946
4139
|
}
|
|
3947
4140
|
updateStatus() {
|
|
3948
4141
|
if (!this.textareaEl) return;
|
|
3949
|
-
const disabled = this.state !== "ready";
|
|
4142
|
+
const disabled = this.state !== "ready" || this.offline;
|
|
3950
4143
|
this.textareaEl.disabled = disabled;
|
|
3951
4144
|
const handoff = HANDOFF_STATUS[this.handoffState];
|
|
3952
|
-
const text2 = this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
|
|
4145
|
+
const text2 = this.offline ? "Offline" : this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
|
|
3953
4146
|
if (this.statusEl) this.statusEl.textContent = text2;
|
|
3954
|
-
if (this.state === "error" && this.lastError) {
|
|
3955
|
-
this.errEl.hidden = false;
|
|
3956
|
-
this.errEl.innerHTML = `<span>Couldn't start chat: ${escapeHtml(this.lastError)}</span><button class="retry" type="button">Retry</button>`;
|
|
3957
|
-
this.errEl.querySelector(".retry").addEventListener("click", () => {
|
|
3958
|
-
void this.client?.start();
|
|
3959
|
-
});
|
|
3960
|
-
} else {
|
|
3961
|
-
this.errEl.hidden = true;
|
|
3962
|
-
this.errEl.innerHTML = "";
|
|
3963
|
-
}
|
|
3964
4147
|
}
|
|
3965
4148
|
// ── Message list + scroll ─────────────────────────────────────────────────────────────────────
|
|
3966
4149
|
renderMessages() {
|
|
3967
4150
|
if (!this.bodyEl) return;
|
|
3968
4151
|
const body = this.bodyEl;
|
|
4152
|
+
const last = this.messages[this.messages.length - 1];
|
|
4153
|
+
if (this.uiOpen && this.streamEl && last && last.id === this.streamId && last.role === "assistant" && last.streaming && this.messages.length === this.renderedCount) {
|
|
4154
|
+
this.streamEl.innerHTML = renderMarkdown(last.text || "");
|
|
4155
|
+
if (!this.away) this.scrollToBottom();
|
|
4156
|
+
return;
|
|
4157
|
+
}
|
|
3969
4158
|
const prevTop = body.scrollTop;
|
|
3970
4159
|
const added = this.messages.length - this.renderedCount;
|
|
3971
4160
|
const botFinal = this.messages.filter((m2) => m2.role === "assistant" && !m2.streaming).length;
|
|
@@ -3978,17 +4167,36 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3978
4167
|
})();
|
|
3979
4168
|
let html2 = this.messages.map((m2, i) => this.turnHtml(m2, i === lastBot && !this.agentTyping)).join("");
|
|
3980
4169
|
if (this.agentTyping) html2 += '<span class="typing"><span></span><span></span><span></span></span>';
|
|
3981
|
-
const sugg = this.
|
|
4170
|
+
const sugg = this.chipLabels;
|
|
3982
4171
|
if (sugg.length > 0 && !this.agentTyping && this.state === "ready") {
|
|
3983
4172
|
html2 += `<span class="chips">${sugg.map((s, i) => `<button type="button" class="chip" data-chip="${i}">${escapeHtml(s)}</button>`).join("")}</span>`;
|
|
3984
4173
|
}
|
|
4174
|
+
if (this.state === "error" && this.lastError) {
|
|
4175
|
+
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>`;
|
|
4176
|
+
}
|
|
4177
|
+
if (this.state === "ended") {
|
|
4178
|
+
html2 += `<div class="oc-ended"><span class="oc-ended-t">Your conversation has ended.</span><button type="button" class="oc-newmsg">New message</button></div>`;
|
|
4179
|
+
}
|
|
4180
|
+
if (this.offline && this.messages.length === 0 && this.state !== "ended") {
|
|
4181
|
+
html2 += `<div class="oc-ended"><span class="oc-ended-t">${escapeHtml(this.offlineMessage)}</span></div>`;
|
|
4182
|
+
}
|
|
3985
4183
|
body.innerHTML = html2;
|
|
3986
4184
|
this.hydrateAttachmentImages();
|
|
3987
4185
|
this.bindChips();
|
|
3988
4186
|
this.renderedCount = this.messages.length;
|
|
3989
4187
|
this.renderedBotFinal = botFinal;
|
|
4188
|
+
this.streamId = null;
|
|
4189
|
+
this.streamEl = null;
|
|
4190
|
+
if (last && last.role === "assistant" && last.streaming) {
|
|
4191
|
+
const mds = this.bodyEl.querySelectorAll(".turn.bot .md");
|
|
4192
|
+
this.streamEl = mds[mds.length - 1] ?? null;
|
|
4193
|
+
if (this.streamEl) this.streamId = last.id;
|
|
4194
|
+
}
|
|
3990
4195
|
if (!this.uiOpen) {
|
|
3991
|
-
if (added > 0)
|
|
4196
|
+
if (added > 0) {
|
|
4197
|
+
this.unseen += added;
|
|
4198
|
+
this.maybeChime();
|
|
4199
|
+
}
|
|
3992
4200
|
this.updateClasses();
|
|
3993
4201
|
return;
|
|
3994
4202
|
}
|
|
@@ -3996,7 +4204,10 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3996
4204
|
else if (!this.away) this.scrollToBottom();
|
|
3997
4205
|
else {
|
|
3998
4206
|
body.scrollTop = prevTop;
|
|
3999
|
-
if (added > 0)
|
|
4207
|
+
if (added > 0) {
|
|
4208
|
+
this.unseen += added;
|
|
4209
|
+
this.maybeChime();
|
|
4210
|
+
}
|
|
4000
4211
|
}
|
|
4001
4212
|
this.updateClasses();
|
|
4002
4213
|
}
|
|
@@ -4047,6 +4258,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4047
4258
|
}
|
|
4048
4259
|
this.away = false;
|
|
4049
4260
|
this.emojiOpen = false;
|
|
4261
|
+
this.serverSuggestions = [];
|
|
4050
4262
|
void this.client?.sendMessage(text2, files.length > 0 ? files : void 0);
|
|
4051
4263
|
this.textareaEl.value = "";
|
|
4052
4264
|
this.textareaEl.style.height = "auto";
|
|
@@ -4055,16 +4267,70 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4055
4267
|
sendChip(text2) {
|
|
4056
4268
|
this.away = false;
|
|
4057
4269
|
this.emojiOpen = false;
|
|
4270
|
+
this.serverSuggestions = [];
|
|
4058
4271
|
void this.client?.sendMessage(text2);
|
|
4059
4272
|
this.updateClasses();
|
|
4060
4273
|
}
|
|
4061
4274
|
bindChips() {
|
|
4062
|
-
const sugg = this.
|
|
4275
|
+
const sugg = this.chipLabels;
|
|
4063
4276
|
this.bodyEl.querySelectorAll(".chip[data-chip]").forEach((btn) => {
|
|
4064
4277
|
const i = Number(btn.getAttribute("data-chip"));
|
|
4065
4278
|
const text2 = sugg[i];
|
|
4066
4279
|
if (text2) btn.addEventListener("click", () => this.sendChip(text2));
|
|
4067
4280
|
});
|
|
4281
|
+
this.bodyEl.querySelector(".oc-err-retry")?.addEventListener("click", () => {
|
|
4282
|
+
void this.client?.start();
|
|
4283
|
+
});
|
|
4284
|
+
this.bodyEl.querySelector(".oc-newmsg")?.addEventListener("click", () => this.startFresh());
|
|
4285
|
+
}
|
|
4286
|
+
// Reset the thread and open a brand-new session (the widget's "New message" after a session ended).
|
|
4287
|
+
startFresh() {
|
|
4288
|
+
this.serverSuggestions = null;
|
|
4289
|
+
this.receivedAt.clear();
|
|
4290
|
+
this.mdCache.clear();
|
|
4291
|
+
this.streamId = null;
|
|
4292
|
+
this.streamEl = null;
|
|
4293
|
+
this.renderedCount = 0;
|
|
4294
|
+
this.renderedBotFinal = 0;
|
|
4295
|
+
this.away = false;
|
|
4296
|
+
this.unseen = 0;
|
|
4297
|
+
this.client?.reset();
|
|
4298
|
+
this.started = true;
|
|
4299
|
+
void this.client?.start();
|
|
4300
|
+
setTimeout(() => this.textareaEl?.focus(), 60);
|
|
4301
|
+
}
|
|
4302
|
+
// A short two-tone chime for an incoming message the visitor hasn't seen (panel closed or scrolled
|
|
4303
|
+
// up). Off when the `sound="off"` attribute is set. Uses WebAudio (no asset); silent if unavailable
|
|
4304
|
+
// or the browser hasn't allowed audio yet.
|
|
4305
|
+
maybeChime() {
|
|
4306
|
+
const last = this.messages[this.messages.length - 1];
|
|
4307
|
+
if (last && (last.role === "assistant" || last.role === "human")) this.playChime();
|
|
4308
|
+
}
|
|
4309
|
+
playChime() {
|
|
4310
|
+
if (this.getAttribute("sound") === "off") return;
|
|
4311
|
+
try {
|
|
4312
|
+
const Ctx = window.AudioContext ?? window.webkitAudioContext;
|
|
4313
|
+
if (!Ctx) return;
|
|
4314
|
+
if (!this.audioCtx) this.audioCtx = new Ctx();
|
|
4315
|
+
const ctx = this.audioCtx;
|
|
4316
|
+
if (ctx.state === "suspended") void ctx.resume();
|
|
4317
|
+
const now = ctx.currentTime;
|
|
4318
|
+
[880, 1174.66].forEach((freq, i) => {
|
|
4319
|
+
const osc = ctx.createOscillator();
|
|
4320
|
+
const gain = ctx.createGain();
|
|
4321
|
+
osc.type = "sine";
|
|
4322
|
+
osc.frequency.value = freq;
|
|
4323
|
+
const t = now + i * 0.11;
|
|
4324
|
+
gain.gain.setValueAtTime(1e-4, t);
|
|
4325
|
+
gain.gain.exponentialRampToValueAtTime(0.12, t + 0.02);
|
|
4326
|
+
gain.gain.exponentialRampToValueAtTime(1e-4, t + 0.18);
|
|
4327
|
+
osc.connect(gain);
|
|
4328
|
+
gain.connect(ctx.destination);
|
|
4329
|
+
osc.start(t);
|
|
4330
|
+
osc.stop(t + 0.2);
|
|
4331
|
+
});
|
|
4332
|
+
} catch {
|
|
4333
|
+
}
|
|
4068
4334
|
}
|
|
4069
4335
|
// ── Emoji ─────────────────────────────────────────────────────────────────────────────────────
|
|
4070
4336
|
toggleEmoji() {
|