@oyehello/chat 0.4.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 +2 -1
- package/dist/index.cjs +191 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.global.js +191 -7
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +191 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,9 +36,10 @@ npm install @oyehello/chat
|
|
|
36
36
|
```
|
|
37
37
|
Or via a plain script tag (no bundler):
|
|
38
38
|
```html
|
|
39
|
-
<script src="https://unpkg.com/@oyehello/chat"></script>
|
|
39
|
+
<script src="https://unpkg.com/@oyehello/chat@0.5.0"></script>
|
|
40
40
|
<oyehello-chat api-key="ohk_pub_…" agent-id="agt_…" base-url="https://api.oyehello.ai"></oyehello-chat>
|
|
41
41
|
```
|
|
42
|
+
Pin a specific version (`@0.5.0` above) rather than the bare package name, so a customer's site loads the exact widget it was tested against instead of silently auto-upgrading on every publish.
|
|
42
43
|
Theme the accent with the `--oc-accent` CSS variable.
|
|
43
44
|
|
|
44
45
|
### React
|
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,9 @@ var ChatApiError = class extends Error {
|
|
|
21
21
|
meta;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
// src/protocol.ts
|
|
25
|
+
var CHAT_PROTOCOL_VERSION = 1;
|
|
26
|
+
|
|
24
27
|
// src/transport.ts
|
|
25
28
|
async function asError(res, fallbackCode) {
|
|
26
29
|
let body = {};
|
|
@@ -31,9 +34,10 @@ async function asError(res, fallbackCode) {
|
|
|
31
34
|
return new ChatApiError(body.error ?? `HTTP ${res.status}`, res.status, body.code ?? fallbackCode, body.meta);
|
|
32
35
|
}
|
|
33
36
|
async function openSession(opts) {
|
|
34
|
-
const body = { agentId: opts.agentId };
|
|
37
|
+
const body = { agentId: opts.agentId, protocol: CHAT_PROTOCOL_VERSION };
|
|
35
38
|
if (opts.user) body.user = opts.user;
|
|
36
39
|
if (opts.clientTools && opts.clientTools.length > 0) body.clientTools = opts.clientTools;
|
|
40
|
+
if (opts.fingerprint) body.fingerprint = opts.fingerprint;
|
|
37
41
|
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions`, {
|
|
38
42
|
method: "POST",
|
|
39
43
|
credentials: "omit",
|
|
@@ -117,6 +121,50 @@ async function uploadToPresigned(uploadUrl, file, fetchImpl) {
|
|
|
117
121
|
if (!res.ok) throw new ChatApiError(`attachment upload failed with HTTP ${res.status}`, res.status);
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
// src/fingerprint.ts
|
|
125
|
+
async function sha256Hex(input) {
|
|
126
|
+
const g = globalThis;
|
|
127
|
+
try {
|
|
128
|
+
if (g.crypto?.subtle && typeof TextEncoder !== "undefined") {
|
|
129
|
+
const buf = await g.crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
130
|
+
return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 32);
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
let h = 5381;
|
|
135
|
+
for (let i = 0; i < input.length; i++) h = (h << 5) + h + input.charCodeAt(i) >>> 0;
|
|
136
|
+
return h.toString(16).padStart(8, "0");
|
|
137
|
+
}
|
|
138
|
+
async function computeDeviceFingerprint() {
|
|
139
|
+
try {
|
|
140
|
+
const nav = typeof navigator !== "undefined" ? navigator : null;
|
|
141
|
+
if (!nav) return null;
|
|
142
|
+
const scr = typeof screen !== "undefined" ? screen : null;
|
|
143
|
+
const tz = (() => {
|
|
144
|
+
try {
|
|
145
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
146
|
+
} catch {
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
})();
|
|
150
|
+
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : "";
|
|
151
|
+
const signals = [
|
|
152
|
+
nav.userAgent,
|
|
153
|
+
Array.isArray(nav.languages) ? nav.languages.join(",") : "",
|
|
154
|
+
nav.language,
|
|
155
|
+
nav.platform,
|
|
156
|
+
nav.hardwareConcurrency,
|
|
157
|
+
nav.deviceMemory,
|
|
158
|
+
scr ? `${scr.width}x${scr.height}x${scr.colorDepth}` : "",
|
|
159
|
+
dpr,
|
|
160
|
+
tz
|
|
161
|
+
].map((v) => String(v ?? "")).join("|");
|
|
162
|
+
return await sha256Hex(signals);
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
120
168
|
// src/client.ts
|
|
121
169
|
var seq = 0;
|
|
122
170
|
var uid = (p) => `${p}_${Date.now().toString(36)}_${(seq++).toString(36)}`;
|
|
@@ -137,6 +185,8 @@ function createChatClient(options) {
|
|
|
137
185
|
let reconnectTimer = null;
|
|
138
186
|
let reconnectAttempts = 0;
|
|
139
187
|
let closed = false;
|
|
188
|
+
let warnedProtocol = false;
|
|
189
|
+
let deviceFingerprint;
|
|
140
190
|
const toolHandlers = /* @__PURE__ */ new Map();
|
|
141
191
|
const clearReconnect = () => {
|
|
142
192
|
if (reconnectTimer) {
|
|
@@ -231,6 +281,12 @@ function createChatClient(options) {
|
|
|
231
281
|
emit({ type: "agentTyping", active: !!ev.active });
|
|
232
282
|
} else if (ev.type === "suggestions") {
|
|
233
283
|
emit({ type: "suggestions", items: Array.isArray(ev.items) ? ev.items : [] });
|
|
284
|
+
} else if (ev.type === "done") {
|
|
285
|
+
closed = true;
|
|
286
|
+
clearReconnect();
|
|
287
|
+
es?.close();
|
|
288
|
+
es = null;
|
|
289
|
+
setState("ended");
|
|
234
290
|
}
|
|
235
291
|
};
|
|
236
292
|
const openStream = async () => {
|
|
@@ -273,7 +329,15 @@ function createChatClient(options) {
|
|
|
273
329
|
}, delay);
|
|
274
330
|
};
|
|
275
331
|
const open = async () => {
|
|
276
|
-
|
|
332
|
+
if (deviceFingerprint === void 0) deviceFingerprint = await computeDeviceFingerprint().catch(() => null);
|
|
333
|
+
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 });
|
|
334
|
+
if (!warnedProtocol && typeof session.protocol === "number" && session.protocol !== CHAT_PROTOCOL_VERSION) {
|
|
335
|
+
warnedProtocol = true;
|
|
336
|
+
try {
|
|
337
|
+
console.warn(`[oyehello-chat] SDK protocol v${CHAT_PROTOCOL_VERSION} vs server v${session.protocol} - update @oyehello/chat to the latest version.`);
|
|
338
|
+
} catch {
|
|
339
|
+
}
|
|
340
|
+
}
|
|
277
341
|
emit({ type: "agent", agent: session.agent });
|
|
278
342
|
await openStream();
|
|
279
343
|
};
|
|
@@ -364,6 +428,20 @@ function createChatClient(options) {
|
|
|
364
428
|
es?.close();
|
|
365
429
|
es = null;
|
|
366
430
|
setState("ended");
|
|
431
|
+
},
|
|
432
|
+
reset() {
|
|
433
|
+
es?.close();
|
|
434
|
+
es = null;
|
|
435
|
+
clearReconnect();
|
|
436
|
+
session = null;
|
|
437
|
+
streamingId = null;
|
|
438
|
+
handoffState = "ai";
|
|
439
|
+
messages.length = 0;
|
|
440
|
+
seenTurnIds.clear();
|
|
441
|
+
seenCallIds.clear();
|
|
442
|
+
emit({ type: "messages", messages: [] });
|
|
443
|
+
emit({ type: "handoff", state: "ai" });
|
|
444
|
+
setState("idle");
|
|
367
445
|
}
|
|
368
446
|
};
|
|
369
447
|
}
|
|
@@ -553,6 +631,12 @@ var STYLES = `
|
|
|
553
631
|
.oc-err-retry { padding: 7px 15px; font-size: 12.5px; font-weight: 500; border-radius: var(--oc-r-full); border: 1px solid var(--oc-border); background: var(--oc-tint-2); color: var(--oc-fg); cursor: pointer; transition: background .18s var(--oc-ease); }
|
|
554
632
|
.oc-err-retry:hover { background: var(--oc-tint-3); }
|
|
555
633
|
|
|
634
|
+
/* Session-ended state, inside the chat window */
|
|
635
|
+
.oc-ended { margin: auto; display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 24px 16px; text-align: center; }
|
|
636
|
+
.oc-ended-t { font-size: 13.5px; line-height: 1.5; color: var(--oc-fg-muted); text-wrap: pretty; }
|
|
637
|
+
.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); }
|
|
638
|
+
.oc-newmsg:hover { filter: brightness(1.08); }
|
|
639
|
+
|
|
556
640
|
/* \u2500\u2500 Message list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
557
641
|
.body { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; padding: 16px 16px 26px; background: var(--oc-bg); scroll-behavior: smooth; }
|
|
558
642
|
.turn { display: flex; flex-direction: column; gap: 6px; animation: w-in .42s var(--oc-ease) both; }
|
|
@@ -822,8 +906,19 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
822
906
|
// Server (flow) suggestions for the latest turn; null until the first `suggestions` event, then the
|
|
823
907
|
// widget prefers these over the static `suggestions` attribute.
|
|
824
908
|
serverSuggestions = null;
|
|
909
|
+
// The live streaming assistant bubble's markdown element + message id — so growing tokens patch just
|
|
910
|
+
// that element in place instead of rebuilding the whole message list on every token.
|
|
911
|
+
streamId = null;
|
|
912
|
+
streamEl = null;
|
|
913
|
+
audioCtx = null;
|
|
825
914
|
teaserText = "Need a hand? Ask me anything.";
|
|
826
915
|
teaserShownOnce = false;
|
|
916
|
+
// Staffed-hours: set from widget-config. When offline, the panel shows offlineMessage and the
|
|
917
|
+
// composer is disabled (opening a session is refused server-side anyway).
|
|
918
|
+
offline = false;
|
|
919
|
+
offlineMessage = "We're offline right now. Please check back during business hours.";
|
|
920
|
+
configLoaded = false;
|
|
921
|
+
// widget-config resolved (so we know online/offline before starting)
|
|
827
922
|
idleTimer = null;
|
|
828
923
|
idleReset = null;
|
|
829
924
|
// ── shadow refs ──
|
|
@@ -921,7 +1016,19 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
921
1016
|
}
|
|
922
1017
|
if (cfg.brand.logoUrl) this.applyBrandLogo(cfg.brand.logoUrl);
|
|
923
1018
|
}
|
|
1019
|
+
if (cfg.offline) {
|
|
1020
|
+
this.offline = true;
|
|
1021
|
+
if (cfg.offlineMessage) this.offlineMessage = cfg.offlineMessage;
|
|
1022
|
+
this.renderMessages();
|
|
1023
|
+
this.updateStatus();
|
|
1024
|
+
}
|
|
924
1025
|
} catch {
|
|
1026
|
+
} finally {
|
|
1027
|
+
this.configLoaded = true;
|
|
1028
|
+
if (this.uiOpen && !this.started && !this.offline && this.client) {
|
|
1029
|
+
this.started = true;
|
|
1030
|
+
void this.client.start();
|
|
1031
|
+
}
|
|
925
1032
|
}
|
|
926
1033
|
}
|
|
927
1034
|
applyTeaserText() {
|
|
@@ -1110,7 +1217,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1110
1217
|
sessionStorage.setItem(this.key("open"), "1");
|
|
1111
1218
|
} catch {
|
|
1112
1219
|
}
|
|
1113
|
-
if (!this.started && this.client) {
|
|
1220
|
+
if (!this.started && this.client && this.configLoaded && !this.offline) {
|
|
1114
1221
|
this.started = true;
|
|
1115
1222
|
void this.client.start();
|
|
1116
1223
|
}
|
|
@@ -1158,16 +1265,22 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1158
1265
|
}
|
|
1159
1266
|
updateStatus() {
|
|
1160
1267
|
if (!this.textareaEl) return;
|
|
1161
|
-
const disabled = this.state !== "ready";
|
|
1268
|
+
const disabled = this.state !== "ready" || this.offline;
|
|
1162
1269
|
this.textareaEl.disabled = disabled;
|
|
1163
1270
|
const handoff = HANDOFF_STATUS[this.handoffState];
|
|
1164
|
-
const text = this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
|
|
1271
|
+
const text = this.offline ? "Offline" : this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
|
|
1165
1272
|
if (this.statusEl) this.statusEl.textContent = text;
|
|
1166
1273
|
}
|
|
1167
1274
|
// ── Message list + scroll ─────────────────────────────────────────────────────────────────────
|
|
1168
1275
|
renderMessages() {
|
|
1169
1276
|
if (!this.bodyEl) return;
|
|
1170
1277
|
const body = this.bodyEl;
|
|
1278
|
+
const last = this.messages[this.messages.length - 1];
|
|
1279
|
+
if (this.uiOpen && this.streamEl && last && last.id === this.streamId && last.role === "assistant" && last.streaming && this.messages.length === this.renderedCount) {
|
|
1280
|
+
this.streamEl.innerHTML = renderMarkdown(last.text || "");
|
|
1281
|
+
if (!this.away) this.scrollToBottom();
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1171
1284
|
const prevTop = body.scrollTop;
|
|
1172
1285
|
const added = this.messages.length - this.renderedCount;
|
|
1173
1286
|
const botFinal = this.messages.filter((m) => m.role === "assistant" && !m.streaming).length;
|
|
@@ -1187,13 +1300,29 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1187
1300
|
if (this.state === "error" && this.lastError) {
|
|
1188
1301
|
html += `<div class="oc-err"><span class="oc-err-t">${escapeHtml(this.lastError)}</span><button type="button" class="oc-err-retry">Try again</button></div>`;
|
|
1189
1302
|
}
|
|
1303
|
+
if (this.state === "ended") {
|
|
1304
|
+
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>`;
|
|
1305
|
+
}
|
|
1306
|
+
if (this.offline && this.messages.length === 0 && this.state !== "ended") {
|
|
1307
|
+
html += `<div class="oc-ended"><span class="oc-ended-t">${escapeHtml(this.offlineMessage)}</span></div>`;
|
|
1308
|
+
}
|
|
1190
1309
|
body.innerHTML = html;
|
|
1191
1310
|
this.hydrateAttachmentImages();
|
|
1192
1311
|
this.bindChips();
|
|
1193
1312
|
this.renderedCount = this.messages.length;
|
|
1194
1313
|
this.renderedBotFinal = botFinal;
|
|
1314
|
+
this.streamId = null;
|
|
1315
|
+
this.streamEl = null;
|
|
1316
|
+
if (last && last.role === "assistant" && last.streaming) {
|
|
1317
|
+
const mds = this.bodyEl.querySelectorAll(".turn.bot .md");
|
|
1318
|
+
this.streamEl = mds[mds.length - 1] ?? null;
|
|
1319
|
+
if (this.streamEl) this.streamId = last.id;
|
|
1320
|
+
}
|
|
1195
1321
|
if (!this.uiOpen) {
|
|
1196
|
-
if (added > 0)
|
|
1322
|
+
if (added > 0) {
|
|
1323
|
+
this.unseen += added;
|
|
1324
|
+
this.maybeChime();
|
|
1325
|
+
}
|
|
1197
1326
|
this.updateClasses();
|
|
1198
1327
|
return;
|
|
1199
1328
|
}
|
|
@@ -1201,7 +1330,10 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1201
1330
|
else if (!this.away) this.scrollToBottom();
|
|
1202
1331
|
else {
|
|
1203
1332
|
body.scrollTop = prevTop;
|
|
1204
|
-
if (added > 0)
|
|
1333
|
+
if (added > 0) {
|
|
1334
|
+
this.unseen += added;
|
|
1335
|
+
this.maybeChime();
|
|
1336
|
+
}
|
|
1205
1337
|
}
|
|
1206
1338
|
this.updateClasses();
|
|
1207
1339
|
}
|
|
@@ -1252,6 +1384,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1252
1384
|
}
|
|
1253
1385
|
this.away = false;
|
|
1254
1386
|
this.emojiOpen = false;
|
|
1387
|
+
this.serverSuggestions = [];
|
|
1255
1388
|
void this.client?.sendMessage(text, files.length > 0 ? files : void 0);
|
|
1256
1389
|
this.textareaEl.value = "";
|
|
1257
1390
|
this.textareaEl.style.height = "auto";
|
|
@@ -1260,6 +1393,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1260
1393
|
sendChip(text) {
|
|
1261
1394
|
this.away = false;
|
|
1262
1395
|
this.emojiOpen = false;
|
|
1396
|
+
this.serverSuggestions = [];
|
|
1263
1397
|
void this.client?.sendMessage(text);
|
|
1264
1398
|
this.updateClasses();
|
|
1265
1399
|
}
|
|
@@ -1273,6 +1407,56 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1273
1407
|
this.bodyEl.querySelector(".oc-err-retry")?.addEventListener("click", () => {
|
|
1274
1408
|
void this.client?.start();
|
|
1275
1409
|
});
|
|
1410
|
+
this.bodyEl.querySelector(".oc-newmsg")?.addEventListener("click", () => this.startFresh());
|
|
1411
|
+
}
|
|
1412
|
+
// Reset the thread and open a brand-new session (the widget's "New message" after a session ended).
|
|
1413
|
+
startFresh() {
|
|
1414
|
+
this.serverSuggestions = null;
|
|
1415
|
+
this.receivedAt.clear();
|
|
1416
|
+
this.mdCache.clear();
|
|
1417
|
+
this.streamId = null;
|
|
1418
|
+
this.streamEl = null;
|
|
1419
|
+
this.renderedCount = 0;
|
|
1420
|
+
this.renderedBotFinal = 0;
|
|
1421
|
+
this.away = false;
|
|
1422
|
+
this.unseen = 0;
|
|
1423
|
+
this.client?.reset();
|
|
1424
|
+
this.started = true;
|
|
1425
|
+
void this.client?.start();
|
|
1426
|
+
setTimeout(() => this.textareaEl?.focus(), 60);
|
|
1427
|
+
}
|
|
1428
|
+
// A short two-tone chime for an incoming message the visitor hasn't seen (panel closed or scrolled
|
|
1429
|
+
// up). Off when the `sound="off"` attribute is set. Uses WebAudio (no asset); silent if unavailable
|
|
1430
|
+
// or the browser hasn't allowed audio yet.
|
|
1431
|
+
maybeChime() {
|
|
1432
|
+
const last = this.messages[this.messages.length - 1];
|
|
1433
|
+
if (last && (last.role === "assistant" || last.role === "human")) this.playChime();
|
|
1434
|
+
}
|
|
1435
|
+
playChime() {
|
|
1436
|
+
if (this.getAttribute("sound") === "off") return;
|
|
1437
|
+
try {
|
|
1438
|
+
const Ctx = window.AudioContext ?? window.webkitAudioContext;
|
|
1439
|
+
if (!Ctx) return;
|
|
1440
|
+
if (!this.audioCtx) this.audioCtx = new Ctx();
|
|
1441
|
+
const ctx = this.audioCtx;
|
|
1442
|
+
if (ctx.state === "suspended") void ctx.resume();
|
|
1443
|
+
const now = ctx.currentTime;
|
|
1444
|
+
[880, 1174.66].forEach((freq, i) => {
|
|
1445
|
+
const osc = ctx.createOscillator();
|
|
1446
|
+
const gain = ctx.createGain();
|
|
1447
|
+
osc.type = "sine";
|
|
1448
|
+
osc.frequency.value = freq;
|
|
1449
|
+
const t = now + i * 0.11;
|
|
1450
|
+
gain.gain.setValueAtTime(1e-4, t);
|
|
1451
|
+
gain.gain.exponentialRampToValueAtTime(0.12, t + 0.02);
|
|
1452
|
+
gain.gain.exponentialRampToValueAtTime(1e-4, t + 0.18);
|
|
1453
|
+
osc.connect(gain);
|
|
1454
|
+
gain.connect(ctx.destination);
|
|
1455
|
+
osc.start(t);
|
|
1456
|
+
osc.stop(t + 0.2);
|
|
1457
|
+
});
|
|
1458
|
+
} catch {
|
|
1459
|
+
}
|
|
1276
1460
|
}
|
|
1277
1461
|
// ── Emoji ─────────────────────────────────────────────────────────────────────────────────────
|
|
1278
1462
|
toggleEmoji() {
|