@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/dist/index.d.cts
CHANGED
|
@@ -64,6 +64,8 @@ interface WidgetConfig {
|
|
|
64
64
|
logoUrl: string | null;
|
|
65
65
|
accentColor: string | null;
|
|
66
66
|
} | null;
|
|
67
|
+
offline?: boolean;
|
|
68
|
+
offlineMessage?: string;
|
|
67
69
|
}
|
|
68
70
|
interface OpenSessionResponse {
|
|
69
71
|
sessionId: string;
|
|
@@ -73,6 +75,7 @@ interface OpenSessionResponse {
|
|
|
73
75
|
id: string;
|
|
74
76
|
name: string;
|
|
75
77
|
};
|
|
78
|
+
protocol?: number;
|
|
76
79
|
}
|
|
77
80
|
interface ChatClientOptions {
|
|
78
81
|
apiKey: string;
|
|
@@ -137,6 +140,7 @@ interface ChatClient {
|
|
|
137
140
|
start(): Promise<void>;
|
|
138
141
|
sendMessage(text: string, files?: File[]): Promise<void>;
|
|
139
142
|
end(): Promise<void>;
|
|
143
|
+
reset(): void;
|
|
140
144
|
getMessages(): ChatMessage[];
|
|
141
145
|
getState(): ChatClientState;
|
|
142
146
|
on(listener: (e: ChatClientEvent) => void): () => void;
|
|
@@ -175,8 +179,14 @@ declare class OyehelloChatElement extends HTMLElement {
|
|
|
175
179
|
private renderedBotFinal;
|
|
176
180
|
private timers;
|
|
177
181
|
private serverSuggestions;
|
|
182
|
+
private streamId;
|
|
183
|
+
private streamEl;
|
|
184
|
+
private audioCtx;
|
|
178
185
|
private teaserText;
|
|
179
186
|
private teaserShownOnce;
|
|
187
|
+
private offline;
|
|
188
|
+
private offlineMessage;
|
|
189
|
+
private configLoaded;
|
|
180
190
|
private idleTimer;
|
|
181
191
|
private idleReset;
|
|
182
192
|
private oc;
|
|
@@ -216,6 +226,9 @@ declare class OyehelloChatElement extends HTMLElement {
|
|
|
216
226
|
private doSend;
|
|
217
227
|
private sendChip;
|
|
218
228
|
private bindChips;
|
|
229
|
+
private startFresh;
|
|
230
|
+
private maybeChime;
|
|
231
|
+
private playChime;
|
|
219
232
|
private toggleEmoji;
|
|
220
233
|
private closeEmoji;
|
|
221
234
|
private renderEmoji;
|
package/dist/index.d.ts
CHANGED
|
@@ -64,6 +64,8 @@ interface WidgetConfig {
|
|
|
64
64
|
logoUrl: string | null;
|
|
65
65
|
accentColor: string | null;
|
|
66
66
|
} | null;
|
|
67
|
+
offline?: boolean;
|
|
68
|
+
offlineMessage?: string;
|
|
67
69
|
}
|
|
68
70
|
interface OpenSessionResponse {
|
|
69
71
|
sessionId: string;
|
|
@@ -73,6 +75,7 @@ interface OpenSessionResponse {
|
|
|
73
75
|
id: string;
|
|
74
76
|
name: string;
|
|
75
77
|
};
|
|
78
|
+
protocol?: number;
|
|
76
79
|
}
|
|
77
80
|
interface ChatClientOptions {
|
|
78
81
|
apiKey: string;
|
|
@@ -137,6 +140,7 @@ interface ChatClient {
|
|
|
137
140
|
start(): Promise<void>;
|
|
138
141
|
sendMessage(text: string, files?: File[]): Promise<void>;
|
|
139
142
|
end(): Promise<void>;
|
|
143
|
+
reset(): void;
|
|
140
144
|
getMessages(): ChatMessage[];
|
|
141
145
|
getState(): ChatClientState;
|
|
142
146
|
on(listener: (e: ChatClientEvent) => void): () => void;
|
|
@@ -175,8 +179,14 @@ declare class OyehelloChatElement extends HTMLElement {
|
|
|
175
179
|
private renderedBotFinal;
|
|
176
180
|
private timers;
|
|
177
181
|
private serverSuggestions;
|
|
182
|
+
private streamId;
|
|
183
|
+
private streamEl;
|
|
184
|
+
private audioCtx;
|
|
178
185
|
private teaserText;
|
|
179
186
|
private teaserShownOnce;
|
|
187
|
+
private offline;
|
|
188
|
+
private offlineMessage;
|
|
189
|
+
private configLoaded;
|
|
180
190
|
private idleTimer;
|
|
181
191
|
private idleReset;
|
|
182
192
|
private oc;
|
|
@@ -216,6 +226,9 @@ declare class OyehelloChatElement extends HTMLElement {
|
|
|
216
226
|
private doSend;
|
|
217
227
|
private sendChip;
|
|
218
228
|
private bindChips;
|
|
229
|
+
private startFresh;
|
|
230
|
+
private maybeChime;
|
|
231
|
+
private playChime;
|
|
219
232
|
private toggleEmoji;
|
|
220
233
|
private closeEmoji;
|
|
221
234
|
private renderEmoji;
|
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",
|
|
@@ -111,6 +115,50 @@ var OyehelloChat = (function (exports) {
|
|
|
111
115
|
if (!res.ok) throw new ChatApiError(`attachment upload failed with HTTP ${res.status}`, res.status);
|
|
112
116
|
}
|
|
113
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
|
+
|
|
114
162
|
// src/client.ts
|
|
115
163
|
var seq = 0;
|
|
116
164
|
var uid = (p) => `${p}_${Date.now().toString(36)}_${(seq++).toString(36)}`;
|
|
@@ -131,6 +179,8 @@ var OyehelloChat = (function (exports) {
|
|
|
131
179
|
let reconnectTimer = null;
|
|
132
180
|
let reconnectAttempts = 0;
|
|
133
181
|
let closed = false;
|
|
182
|
+
let warnedProtocol = false;
|
|
183
|
+
let deviceFingerprint;
|
|
134
184
|
const toolHandlers = /* @__PURE__ */ new Map();
|
|
135
185
|
const clearReconnect = () => {
|
|
136
186
|
if (reconnectTimer) {
|
|
@@ -225,6 +275,12 @@ var OyehelloChat = (function (exports) {
|
|
|
225
275
|
emit({ type: "agentTyping", active: !!ev.active });
|
|
226
276
|
} else if (ev.type === "suggestions") {
|
|
227
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");
|
|
228
284
|
}
|
|
229
285
|
};
|
|
230
286
|
const openStream = async () => {
|
|
@@ -267,7 +323,15 @@ var OyehelloChat = (function (exports) {
|
|
|
267
323
|
}, delay);
|
|
268
324
|
};
|
|
269
325
|
const open = async () => {
|
|
270
|
-
|
|
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
|
+
}
|
|
271
335
|
emit({ type: "agent", agent: session.agent });
|
|
272
336
|
await openStream();
|
|
273
337
|
};
|
|
@@ -358,6 +422,20 @@ var OyehelloChat = (function (exports) {
|
|
|
358
422
|
es?.close();
|
|
359
423
|
es = null;
|
|
360
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");
|
|
361
439
|
}
|
|
362
440
|
};
|
|
363
441
|
}
|
|
@@ -3427,6 +3505,12 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3427
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); }
|
|
3428
3506
|
.oc-err-retry:hover { background: var(--oc-tint-3); }
|
|
3429
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); }
|
|
3513
|
+
|
|
3430
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 */
|
|
3431
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; }
|
|
3432
3516
|
.turn { display: flex; flex-direction: column; gap: 6px; animation: w-in .42s var(--oc-ease) both; }
|
|
@@ -3696,8 +3780,19 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3696
3780
|
// Server (flow) suggestions for the latest turn; null until the first `suggestions` event, then the
|
|
3697
3781
|
// widget prefers these over the static `suggestions` attribute.
|
|
3698
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;
|
|
3699
3788
|
teaserText = "Need a hand? Ask me anything.";
|
|
3700
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)
|
|
3701
3796
|
idleTimer = null;
|
|
3702
3797
|
idleReset = null;
|
|
3703
3798
|
// ── shadow refs ──
|
|
@@ -3795,7 +3890,19 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3795
3890
|
}
|
|
3796
3891
|
if (cfg.brand.logoUrl) this.applyBrandLogo(cfg.brand.logoUrl);
|
|
3797
3892
|
}
|
|
3893
|
+
if (cfg.offline) {
|
|
3894
|
+
this.offline = true;
|
|
3895
|
+
if (cfg.offlineMessage) this.offlineMessage = cfg.offlineMessage;
|
|
3896
|
+
this.renderMessages();
|
|
3897
|
+
this.updateStatus();
|
|
3898
|
+
}
|
|
3798
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
|
+
}
|
|
3799
3906
|
}
|
|
3800
3907
|
}
|
|
3801
3908
|
applyTeaserText() {
|
|
@@ -3984,7 +4091,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3984
4091
|
sessionStorage.setItem(this.key("open"), "1");
|
|
3985
4092
|
} catch {
|
|
3986
4093
|
}
|
|
3987
|
-
if (!this.started && this.client) {
|
|
4094
|
+
if (!this.started && this.client && this.configLoaded && !this.offline) {
|
|
3988
4095
|
this.started = true;
|
|
3989
4096
|
void this.client.start();
|
|
3990
4097
|
}
|
|
@@ -4032,16 +4139,22 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4032
4139
|
}
|
|
4033
4140
|
updateStatus() {
|
|
4034
4141
|
if (!this.textareaEl) return;
|
|
4035
|
-
const disabled = this.state !== "ready";
|
|
4142
|
+
const disabled = this.state !== "ready" || this.offline;
|
|
4036
4143
|
this.textareaEl.disabled = disabled;
|
|
4037
4144
|
const handoff = HANDOFF_STATUS[this.handoffState];
|
|
4038
|
-
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);
|
|
4039
4146
|
if (this.statusEl) this.statusEl.textContent = text2;
|
|
4040
4147
|
}
|
|
4041
4148
|
// ── Message list + scroll ─────────────────────────────────────────────────────────────────────
|
|
4042
4149
|
renderMessages() {
|
|
4043
4150
|
if (!this.bodyEl) return;
|
|
4044
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
|
+
}
|
|
4045
4158
|
const prevTop = body.scrollTop;
|
|
4046
4159
|
const added = this.messages.length - this.renderedCount;
|
|
4047
4160
|
const botFinal = this.messages.filter((m2) => m2.role === "assistant" && !m2.streaming).length;
|
|
@@ -4061,13 +4174,29 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4061
4174
|
if (this.state === "error" && this.lastError) {
|
|
4062
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>`;
|
|
4063
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
|
+
}
|
|
4064
4183
|
body.innerHTML = html2;
|
|
4065
4184
|
this.hydrateAttachmentImages();
|
|
4066
4185
|
this.bindChips();
|
|
4067
4186
|
this.renderedCount = this.messages.length;
|
|
4068
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
|
+
}
|
|
4069
4195
|
if (!this.uiOpen) {
|
|
4070
|
-
if (added > 0)
|
|
4196
|
+
if (added > 0) {
|
|
4197
|
+
this.unseen += added;
|
|
4198
|
+
this.maybeChime();
|
|
4199
|
+
}
|
|
4071
4200
|
this.updateClasses();
|
|
4072
4201
|
return;
|
|
4073
4202
|
}
|
|
@@ -4075,7 +4204,10 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4075
4204
|
else if (!this.away) this.scrollToBottom();
|
|
4076
4205
|
else {
|
|
4077
4206
|
body.scrollTop = prevTop;
|
|
4078
|
-
if (added > 0)
|
|
4207
|
+
if (added > 0) {
|
|
4208
|
+
this.unseen += added;
|
|
4209
|
+
this.maybeChime();
|
|
4210
|
+
}
|
|
4079
4211
|
}
|
|
4080
4212
|
this.updateClasses();
|
|
4081
4213
|
}
|
|
@@ -4126,6 +4258,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4126
4258
|
}
|
|
4127
4259
|
this.away = false;
|
|
4128
4260
|
this.emojiOpen = false;
|
|
4261
|
+
this.serverSuggestions = [];
|
|
4129
4262
|
void this.client?.sendMessage(text2, files.length > 0 ? files : void 0);
|
|
4130
4263
|
this.textareaEl.value = "";
|
|
4131
4264
|
this.textareaEl.style.height = "auto";
|
|
@@ -4134,6 +4267,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4134
4267
|
sendChip(text2) {
|
|
4135
4268
|
this.away = false;
|
|
4136
4269
|
this.emojiOpen = false;
|
|
4270
|
+
this.serverSuggestions = [];
|
|
4137
4271
|
void this.client?.sendMessage(text2);
|
|
4138
4272
|
this.updateClasses();
|
|
4139
4273
|
}
|
|
@@ -4147,6 +4281,56 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4147
4281
|
this.bodyEl.querySelector(".oc-err-retry")?.addEventListener("click", () => {
|
|
4148
4282
|
void this.client?.start();
|
|
4149
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
|
+
}
|
|
4150
4334
|
}
|
|
4151
4335
|
// ── Emoji ─────────────────────────────────────────────────────────────────────────────────────
|
|
4152
4336
|
toggleEmoji() {
|