@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.js
CHANGED
|
@@ -15,6 +15,9 @@ var ChatApiError = class extends Error {
|
|
|
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 @@ async function asError(res, fallbackCode) {
|
|
|
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 @@ async function openSession(opts) {
|
|
|
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 @@ async function uploadToPresigned(uploadUrl, file, fetchImpl) {
|
|
|
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((b) => b.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((v) => String(v ?? "")).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 @@ function createChatClient(options) {
|
|
|
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 @@ function createChatClient(options) {
|
|
|
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 @@ function createChatClient(options) {
|
|
|
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 @@ function createChatClient(options) {
|
|
|
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 @@ function createChatClient(options) {
|
|
|
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
|
}
|
|
@@ -527,8 +619,17 @@ var STYLES = `
|
|
|
527
619
|
.i-expand { opacity: 1; } .i-shrink { opacity: 0; }
|
|
528
620
|
.oc.max .i-expand { opacity: 0; } .oc.max .i-shrink { opacity: 1; }
|
|
529
621
|
|
|
530
|
-
|
|
531
|
-
.err
|
|
622
|
+
/* Inline connection error, shown inside the chat window (no banner) */
|
|
623
|
+
.oc-err { margin: auto; display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 24px 16px; text-align: center; }
|
|
624
|
+
.oc-err-t { font-size: 13px; line-height: 1.5; color: var(--oc-fg-muted); text-wrap: pretty; }
|
|
625
|
+
.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); }
|
|
626
|
+
.oc-err-retry:hover { background: var(--oc-tint-3); }
|
|
627
|
+
|
|
628
|
+
/* Session-ended state, inside the chat window */
|
|
629
|
+
.oc-ended { margin: auto; display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 24px 16px; text-align: center; }
|
|
630
|
+
.oc-ended-t { font-size: 13.5px; line-height: 1.5; color: var(--oc-fg-muted); text-wrap: pretty; }
|
|
631
|
+
.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); }
|
|
632
|
+
.oc-newmsg:hover { filter: brightness(1.08); }
|
|
532
633
|
|
|
533
634
|
/* \u2500\u2500 Message list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
534
635
|
.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; }
|
|
@@ -796,6 +897,24 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
796
897
|
renderedCount = 0;
|
|
797
898
|
renderedBotFinal = 0;
|
|
798
899
|
timers = [];
|
|
900
|
+
// Server (flow) suggestions for the latest turn; null until the first `suggestions` event, then the
|
|
901
|
+
// widget prefers these over the static `suggestions` attribute.
|
|
902
|
+
serverSuggestions = null;
|
|
903
|
+
// The live streaming assistant bubble's markdown element + message id — so growing tokens patch just
|
|
904
|
+
// that element in place instead of rebuilding the whole message list on every token.
|
|
905
|
+
streamId = null;
|
|
906
|
+
streamEl = null;
|
|
907
|
+
audioCtx = null;
|
|
908
|
+
teaserText = "Need a hand? Ask me anything.";
|
|
909
|
+
teaserShownOnce = false;
|
|
910
|
+
// Staffed-hours: set from widget-config. When offline, the panel shows offlineMessage and the
|
|
911
|
+
// composer is disabled (opening a session is refused server-side anyway).
|
|
912
|
+
offline = false;
|
|
913
|
+
offlineMessage = "We're offline right now. Please check back during business hours.";
|
|
914
|
+
configLoaded = false;
|
|
915
|
+
// widget-config resolved (so we know online/offline before starting)
|
|
916
|
+
idleTimer = null;
|
|
917
|
+
idleReset = null;
|
|
799
918
|
// ── shadow refs ──
|
|
800
919
|
oc;
|
|
801
920
|
bodyEl;
|
|
@@ -803,7 +922,6 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
803
922
|
fileEl;
|
|
804
923
|
titleEl;
|
|
805
924
|
statusEl;
|
|
806
|
-
errEl;
|
|
807
925
|
emojiListEl;
|
|
808
926
|
emojiSearchEl;
|
|
809
927
|
connectedCallback() {
|
|
@@ -815,6 +933,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
815
933
|
this.renderError("oyehello-chat: api-key, agent-id and base-url are required");
|
|
816
934
|
return;
|
|
817
935
|
}
|
|
936
|
+
this.teaserText = this.getAttribute("teaser") || this.teaserText;
|
|
818
937
|
this.client = createChatClient({ apiKey, agentId, baseUrl });
|
|
819
938
|
if (Object.keys(this.pendingTools).length > 0) {
|
|
820
939
|
this.client.registerTools(this.pendingTools);
|
|
@@ -826,6 +945,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
826
945
|
this.updateHeader();
|
|
827
946
|
this.updateStatus();
|
|
828
947
|
this.updateClasses();
|
|
948
|
+
void this.loadWidgetConfig();
|
|
829
949
|
let wasOpen = false;
|
|
830
950
|
try {
|
|
831
951
|
wasOpen = sessionStorage.getItem(this.key("open")) === "1";
|
|
@@ -839,24 +959,93 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
839
959
|
this.timers.push(setTimeout(() => {
|
|
840
960
|
this.oc?.classList.remove("ring");
|
|
841
961
|
}, 6e3));
|
|
842
|
-
|
|
962
|
+
this.armIdleTeaser();
|
|
963
|
+
}
|
|
964
|
+
// Show the teaser once the visitor has been idle (no page interaction) for 10s — unless the panel
|
|
965
|
+
// is already open or the teaser was dismissed this session. Fires at most once.
|
|
966
|
+
armIdleTeaser() {
|
|
843
967
|
let dismissed = false;
|
|
844
968
|
try {
|
|
845
969
|
dismissed = sessionStorage.getItem(this.key("teaser")) === "0";
|
|
846
970
|
} catch {
|
|
847
971
|
}
|
|
848
|
-
if (
|
|
849
|
-
|
|
850
|
-
|
|
972
|
+
if (dismissed) return;
|
|
973
|
+
const reset = () => {
|
|
974
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
975
|
+
this.idleTimer = setTimeout(() => {
|
|
976
|
+
if (!this.uiOpen && !this.teaserShownOnce) {
|
|
851
977
|
this.teaserActive = true;
|
|
978
|
+
this.teaserShownOnce = true;
|
|
852
979
|
this.updateClasses();
|
|
980
|
+
this.teardownIdle();
|
|
981
|
+
}
|
|
982
|
+
}, 1e4);
|
|
983
|
+
};
|
|
984
|
+
this.idleReset = reset;
|
|
985
|
+
for (const ev of ["mousemove", "keydown", "scroll", "touchstart", "click"]) document.addEventListener(ev, reset, { passive: true });
|
|
986
|
+
reset();
|
|
987
|
+
}
|
|
988
|
+
teardownIdle() {
|
|
989
|
+
if (this.idleTimer) {
|
|
990
|
+
clearTimeout(this.idleTimer);
|
|
991
|
+
this.idleTimer = null;
|
|
992
|
+
}
|
|
993
|
+
if (this.idleReset) {
|
|
994
|
+
for (const ev of ["mousemove", "keydown", "scroll", "touchstart", "click"]) document.removeEventListener(ev, this.idleReset);
|
|
995
|
+
this.idleReset = null;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
async loadWidgetConfig() {
|
|
999
|
+
try {
|
|
1000
|
+
const cfg = await this.client.getWidgetConfig();
|
|
1001
|
+
if (!this.getAttribute("teaser") && cfg.teaser) {
|
|
1002
|
+
this.teaserText = cfg.teaser;
|
|
1003
|
+
this.applyTeaserText();
|
|
1004
|
+
}
|
|
1005
|
+
if (cfg.brand) {
|
|
1006
|
+
if (cfg.brand.accentColor) this.style.setProperty("--oc-accent", cfg.brand.accentColor);
|
|
1007
|
+
if (cfg.brand.name && !this.getAttribute("title")) {
|
|
1008
|
+
this.agentName = cfg.brand.name;
|
|
1009
|
+
this.updateHeader();
|
|
853
1010
|
}
|
|
854
|
-
|
|
1011
|
+
if (cfg.brand.logoUrl) this.applyBrandLogo(cfg.brand.logoUrl);
|
|
1012
|
+
}
|
|
1013
|
+
if (cfg.offline) {
|
|
1014
|
+
this.offline = true;
|
|
1015
|
+
if (cfg.offlineMessage) this.offlineMessage = cfg.offlineMessage;
|
|
1016
|
+
this.renderMessages();
|
|
1017
|
+
this.updateStatus();
|
|
1018
|
+
}
|
|
1019
|
+
} catch {
|
|
1020
|
+
} finally {
|
|
1021
|
+
this.configLoaded = true;
|
|
1022
|
+
if (this.uiOpen && !this.started && !this.offline && this.client) {
|
|
1023
|
+
this.started = true;
|
|
1024
|
+
void this.client.start();
|
|
1025
|
+
}
|
|
855
1026
|
}
|
|
856
1027
|
}
|
|
1028
|
+
applyTeaserText() {
|
|
1029
|
+
const el = this.shadowRoot?.querySelector(".teaser-btn span:last-child");
|
|
1030
|
+
if (el) el.textContent = this.teaserText;
|
|
1031
|
+
}
|
|
1032
|
+
applyBrandLogo(url) {
|
|
1033
|
+
const set = (host) => {
|
|
1034
|
+
if (!host) return;
|
|
1035
|
+
host.textContent = "";
|
|
1036
|
+
const img = document.createElement("img");
|
|
1037
|
+
img.src = url;
|
|
1038
|
+
img.alt = "";
|
|
1039
|
+
img.style.cssText = "width:100%;height:100%;object-fit:contain";
|
|
1040
|
+
host.appendChild(img);
|
|
1041
|
+
};
|
|
1042
|
+
set(this.shadowRoot?.querySelector(".hd .logo"));
|
|
1043
|
+
set(this.shadowRoot?.querySelector(".teaser-btn .logo"));
|
|
1044
|
+
}
|
|
857
1045
|
disconnectedCallback() {
|
|
858
1046
|
this.off?.();
|
|
859
1047
|
void this.client?.end();
|
|
1048
|
+
this.teardownIdle();
|
|
860
1049
|
for (const t of this.timers) clearTimeout(t);
|
|
861
1050
|
for (const url of this.imagePreviewCache.values()) {
|
|
862
1051
|
if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
|
|
@@ -870,18 +1059,22 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
870
1059
|
key(k) {
|
|
871
1060
|
return `oyehello-chat:${k}:${this.getAttribute("agent-id") ?? ""}`;
|
|
872
1061
|
}
|
|
873
|
-
|
|
1062
|
+
// Static fallback chips from the `suggestions` attribute (for embedders without flow-driven ones).
|
|
1063
|
+
get attrSuggestions() {
|
|
874
1064
|
return (this.getAttribute("suggestions") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
875
1065
|
}
|
|
1066
|
+
// Effective chips: server (flow) suggestions once received, else the static attribute fallback.
|
|
1067
|
+
get chipLabels() {
|
|
1068
|
+
return this.serverSuggestions ?? this.attrSuggestions;
|
|
1069
|
+
}
|
|
876
1070
|
// ── Shell (built once) ─────────────────────────────────────────────────────────────────────────
|
|
877
1071
|
renderShell() {
|
|
878
1072
|
const root = this.shadowRoot;
|
|
879
|
-
const hasTeaser = !!this.getAttribute("teaser");
|
|
880
1073
|
const hasPrivacy = !!this.getAttribute("privacy-url");
|
|
881
|
-
const teaserText = this.
|
|
1074
|
+
const teaserText = this.teaserText;
|
|
882
1075
|
root.innerHTML = `
|
|
883
1076
|
<style>${STYLES}</style>
|
|
884
|
-
<div class="oc
|
|
1077
|
+
<div class="oc has-teaser${hasPrivacy ? " has-privacy" : ""}">
|
|
885
1078
|
<div class="panel" role="dialog" aria-label="Chat">
|
|
886
1079
|
<div class="hd">
|
|
887
1080
|
<span class="hd-grid"></span>
|
|
@@ -890,7 +1083,6 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
890
1083
|
<button type="button" class="hd-btn max" aria-label="Expand"><span class="i i-expand">${ICON.expand}</span><span class="i i-shrink">${ICON.shrink}</span></button>
|
|
891
1084
|
<button type="button" class="hd-btn min" aria-label="Minimise">${ICON.minus}</button>
|
|
892
1085
|
</div>
|
|
893
|
-
<div class="err" hidden></div>
|
|
894
1086
|
<div class="body"></div>
|
|
895
1087
|
<span class="emoji-scrim"></span>
|
|
896
1088
|
<div class="emoji-panel">
|
|
@@ -934,7 +1126,6 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
934
1126
|
this.fileEl = root.querySelector('input[type="file"]');
|
|
935
1127
|
this.titleEl = root.querySelector(".hd-title");
|
|
936
1128
|
this.statusEl = root.querySelector(".hd-status .t");
|
|
937
|
-
this.errEl = root.querySelector(".err");
|
|
938
1129
|
this.emojiListEl = root.querySelector(".emoji-list");
|
|
939
1130
|
this.emojiSearchEl = root.querySelector(".emoji-search input");
|
|
940
1131
|
root.querySelector(".launcher").addEventListener("click", () => this.toggle());
|
|
@@ -1015,11 +1206,12 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1015
1206
|
this.uiOpen = true;
|
|
1016
1207
|
this.teaserActive = false;
|
|
1017
1208
|
this.unseen = 0;
|
|
1209
|
+
this.teardownIdle();
|
|
1018
1210
|
try {
|
|
1019
1211
|
sessionStorage.setItem(this.key("open"), "1");
|
|
1020
1212
|
} catch {
|
|
1021
1213
|
}
|
|
1022
|
-
if (!this.started && this.client) {
|
|
1214
|
+
if (!this.started && this.client && this.configLoaded && !this.offline) {
|
|
1023
1215
|
this.started = true;
|
|
1024
1216
|
void this.client.start();
|
|
1025
1217
|
}
|
|
@@ -1057,6 +1249,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1057
1249
|
this.updateHeader();
|
|
1058
1250
|
} else if (e.type === "handoff") this.handoffState = e.state;
|
|
1059
1251
|
else if (e.type === "agentTyping") this.agentTyping = e.active;
|
|
1252
|
+
else if (e.type === "suggestions") this.serverSuggestions = e.items;
|
|
1060
1253
|
else if (e.type === "error") this.lastError = e.error.message;
|
|
1061
1254
|
this.updateStatus();
|
|
1062
1255
|
this.renderMessages();
|
|
@@ -1066,26 +1259,22 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1066
1259
|
}
|
|
1067
1260
|
updateStatus() {
|
|
1068
1261
|
if (!this.textareaEl) return;
|
|
1069
|
-
const disabled = this.state !== "ready";
|
|
1262
|
+
const disabled = this.state !== "ready" || this.offline;
|
|
1070
1263
|
this.textareaEl.disabled = disabled;
|
|
1071
1264
|
const handoff = HANDOFF_STATUS[this.handoffState];
|
|
1072
|
-
const text = this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
|
|
1265
|
+
const text = this.offline ? "Offline" : this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
|
|
1073
1266
|
if (this.statusEl) this.statusEl.textContent = text;
|
|
1074
|
-
if (this.state === "error" && this.lastError) {
|
|
1075
|
-
this.errEl.hidden = false;
|
|
1076
|
-
this.errEl.innerHTML = `<span>Couldn't start chat: ${escapeHtml(this.lastError)}</span><button class="retry" type="button">Retry</button>`;
|
|
1077
|
-
this.errEl.querySelector(".retry").addEventListener("click", () => {
|
|
1078
|
-
void this.client?.start();
|
|
1079
|
-
});
|
|
1080
|
-
} else {
|
|
1081
|
-
this.errEl.hidden = true;
|
|
1082
|
-
this.errEl.innerHTML = "";
|
|
1083
|
-
}
|
|
1084
1267
|
}
|
|
1085
1268
|
// ── Message list + scroll ─────────────────────────────────────────────────────────────────────
|
|
1086
1269
|
renderMessages() {
|
|
1087
1270
|
if (!this.bodyEl) return;
|
|
1088
1271
|
const body = this.bodyEl;
|
|
1272
|
+
const last = this.messages[this.messages.length - 1];
|
|
1273
|
+
if (this.uiOpen && this.streamEl && last && last.id === this.streamId && last.role === "assistant" && last.streaming && this.messages.length === this.renderedCount) {
|
|
1274
|
+
this.streamEl.innerHTML = renderMarkdown(last.text || "");
|
|
1275
|
+
if (!this.away) this.scrollToBottom();
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1089
1278
|
const prevTop = body.scrollTop;
|
|
1090
1279
|
const added = this.messages.length - this.renderedCount;
|
|
1091
1280
|
const botFinal = this.messages.filter((m) => m.role === "assistant" && !m.streaming).length;
|
|
@@ -1098,17 +1287,36 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1098
1287
|
})();
|
|
1099
1288
|
let html = this.messages.map((m, i) => this.turnHtml(m, i === lastBot && !this.agentTyping)).join("");
|
|
1100
1289
|
if (this.agentTyping) html += '<span class="typing"><span></span><span></span><span></span></span>';
|
|
1101
|
-
const sugg = this.
|
|
1290
|
+
const sugg = this.chipLabels;
|
|
1102
1291
|
if (sugg.length > 0 && !this.agentTyping && this.state === "ready") {
|
|
1103
1292
|
html += `<span class="chips">${sugg.map((s, i) => `<button type="button" class="chip" data-chip="${i}">${escapeHtml(s)}</button>`).join("")}</span>`;
|
|
1104
1293
|
}
|
|
1294
|
+
if (this.state === "error" && this.lastError) {
|
|
1295
|
+
html += `<div class="oc-err"><span class="oc-err-t">${escapeHtml(this.lastError)}</span><button type="button" class="oc-err-retry">Try again</button></div>`;
|
|
1296
|
+
}
|
|
1297
|
+
if (this.state === "ended") {
|
|
1298
|
+
html += `<div class="oc-ended"><span class="oc-ended-t">Your conversation has ended.</span><button type="button" class="oc-newmsg">New message</button></div>`;
|
|
1299
|
+
}
|
|
1300
|
+
if (this.offline && this.messages.length === 0 && this.state !== "ended") {
|
|
1301
|
+
html += `<div class="oc-ended"><span class="oc-ended-t">${escapeHtml(this.offlineMessage)}</span></div>`;
|
|
1302
|
+
}
|
|
1105
1303
|
body.innerHTML = html;
|
|
1106
1304
|
this.hydrateAttachmentImages();
|
|
1107
1305
|
this.bindChips();
|
|
1108
1306
|
this.renderedCount = this.messages.length;
|
|
1109
1307
|
this.renderedBotFinal = botFinal;
|
|
1308
|
+
this.streamId = null;
|
|
1309
|
+
this.streamEl = null;
|
|
1310
|
+
if (last && last.role === "assistant" && last.streaming) {
|
|
1311
|
+
const mds = this.bodyEl.querySelectorAll(".turn.bot .md");
|
|
1312
|
+
this.streamEl = mds[mds.length - 1] ?? null;
|
|
1313
|
+
if (this.streamEl) this.streamId = last.id;
|
|
1314
|
+
}
|
|
1110
1315
|
if (!this.uiOpen) {
|
|
1111
|
-
if (added > 0)
|
|
1316
|
+
if (added > 0) {
|
|
1317
|
+
this.unseen += added;
|
|
1318
|
+
this.maybeChime();
|
|
1319
|
+
}
|
|
1112
1320
|
this.updateClasses();
|
|
1113
1321
|
return;
|
|
1114
1322
|
}
|
|
@@ -1116,7 +1324,10 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1116
1324
|
else if (!this.away) this.scrollToBottom();
|
|
1117
1325
|
else {
|
|
1118
1326
|
body.scrollTop = prevTop;
|
|
1119
|
-
if (added > 0)
|
|
1327
|
+
if (added > 0) {
|
|
1328
|
+
this.unseen += added;
|
|
1329
|
+
this.maybeChime();
|
|
1330
|
+
}
|
|
1120
1331
|
}
|
|
1121
1332
|
this.updateClasses();
|
|
1122
1333
|
}
|
|
@@ -1167,6 +1378,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1167
1378
|
}
|
|
1168
1379
|
this.away = false;
|
|
1169
1380
|
this.emojiOpen = false;
|
|
1381
|
+
this.serverSuggestions = [];
|
|
1170
1382
|
void this.client?.sendMessage(text, files.length > 0 ? files : void 0);
|
|
1171
1383
|
this.textareaEl.value = "";
|
|
1172
1384
|
this.textareaEl.style.height = "auto";
|
|
@@ -1175,16 +1387,70 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1175
1387
|
sendChip(text) {
|
|
1176
1388
|
this.away = false;
|
|
1177
1389
|
this.emojiOpen = false;
|
|
1390
|
+
this.serverSuggestions = [];
|
|
1178
1391
|
void this.client?.sendMessage(text);
|
|
1179
1392
|
this.updateClasses();
|
|
1180
1393
|
}
|
|
1181
1394
|
bindChips() {
|
|
1182
|
-
const sugg = this.
|
|
1395
|
+
const sugg = this.chipLabels;
|
|
1183
1396
|
this.bodyEl.querySelectorAll(".chip[data-chip]").forEach((btn) => {
|
|
1184
1397
|
const i = Number(btn.getAttribute("data-chip"));
|
|
1185
1398
|
const text = sugg[i];
|
|
1186
1399
|
if (text) btn.addEventListener("click", () => this.sendChip(text));
|
|
1187
1400
|
});
|
|
1401
|
+
this.bodyEl.querySelector(".oc-err-retry")?.addEventListener("click", () => {
|
|
1402
|
+
void this.client?.start();
|
|
1403
|
+
});
|
|
1404
|
+
this.bodyEl.querySelector(".oc-newmsg")?.addEventListener("click", () => this.startFresh());
|
|
1405
|
+
}
|
|
1406
|
+
// Reset the thread and open a brand-new session (the widget's "New message" after a session ended).
|
|
1407
|
+
startFresh() {
|
|
1408
|
+
this.serverSuggestions = null;
|
|
1409
|
+
this.receivedAt.clear();
|
|
1410
|
+
this.mdCache.clear();
|
|
1411
|
+
this.streamId = null;
|
|
1412
|
+
this.streamEl = null;
|
|
1413
|
+
this.renderedCount = 0;
|
|
1414
|
+
this.renderedBotFinal = 0;
|
|
1415
|
+
this.away = false;
|
|
1416
|
+
this.unseen = 0;
|
|
1417
|
+
this.client?.reset();
|
|
1418
|
+
this.started = true;
|
|
1419
|
+
void this.client?.start();
|
|
1420
|
+
setTimeout(() => this.textareaEl?.focus(), 60);
|
|
1421
|
+
}
|
|
1422
|
+
// A short two-tone chime for an incoming message the visitor hasn't seen (panel closed or scrolled
|
|
1423
|
+
// up). Off when the `sound="off"` attribute is set. Uses WebAudio (no asset); silent if unavailable
|
|
1424
|
+
// or the browser hasn't allowed audio yet.
|
|
1425
|
+
maybeChime() {
|
|
1426
|
+
const last = this.messages[this.messages.length - 1];
|
|
1427
|
+
if (last && (last.role === "assistant" || last.role === "human")) this.playChime();
|
|
1428
|
+
}
|
|
1429
|
+
playChime() {
|
|
1430
|
+
if (this.getAttribute("sound") === "off") return;
|
|
1431
|
+
try {
|
|
1432
|
+
const Ctx = window.AudioContext ?? window.webkitAudioContext;
|
|
1433
|
+
if (!Ctx) return;
|
|
1434
|
+
if (!this.audioCtx) this.audioCtx = new Ctx();
|
|
1435
|
+
const ctx = this.audioCtx;
|
|
1436
|
+
if (ctx.state === "suspended") void ctx.resume();
|
|
1437
|
+
const now = ctx.currentTime;
|
|
1438
|
+
[880, 1174.66].forEach((freq, i) => {
|
|
1439
|
+
const osc = ctx.createOscillator();
|
|
1440
|
+
const gain = ctx.createGain();
|
|
1441
|
+
osc.type = "sine";
|
|
1442
|
+
osc.frequency.value = freq;
|
|
1443
|
+
const t = now + i * 0.11;
|
|
1444
|
+
gain.gain.setValueAtTime(1e-4, t);
|
|
1445
|
+
gain.gain.exponentialRampToValueAtTime(0.12, t + 0.02);
|
|
1446
|
+
gain.gain.exponentialRampToValueAtTime(1e-4, t + 0.18);
|
|
1447
|
+
osc.connect(gain);
|
|
1448
|
+
gain.connect(ctx.destination);
|
|
1449
|
+
osc.start(t);
|
|
1450
|
+
osc.stop(t + 0.2);
|
|
1451
|
+
});
|
|
1452
|
+
} catch {
|
|
1453
|
+
}
|
|
1188
1454
|
}
|
|
1189
1455
|
// ── Emoji ─────────────────────────────────────────────────────────────────────────────────────
|
|
1190
1456
|
toggleEmoji() {
|