@usereq/widget 0.2.25 → 1.0.0-experimental.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 +35 -145
- package/dist/embed.js +38 -0
- package/dist/embed.js.map +1 -0
- package/dist/embed.mjs +2839 -0
- package/dist/embed.mjs.map +1 -0
- package/package.json +35 -81
- package/dist/widget.js +0 -255
- package/src/chat-widget/chat-widget-appearance.ts +0 -134
- package/src/chat-widget/message-reveal.ts +0 -52
- package/src/chat-widget/stop-confirmation.ts +0 -288
- package/src/custom-element/agent-widget-element.tsx +0 -682
- package/src/embed.ts +0 -8
- package/src/index.ts +0 -9
- package/src/register.ts +0 -15
- package/src/renderer/index.ts +0 -6
- package/src/renderer/widget-runtime.tsx +0 -395
- package/src/runtime/api-origin.ts +0 -29
- package/src/runtime/api.ts +0 -123
- package/src/runtime/bootstrap.ts +0 -366
- package/src/runtime/debug.ts +0 -104
- package/src/runtime/messages.ts +0 -12
- package/src/runtime/session-storage.ts +0 -32
- package/src/runtime/trigger-rule.ts +0 -182
- package/src/shared/shadow-css.ts +0 -15
- package/src/shared/shadow-theme.ts +0 -115
- package/src/shared/stop-confirmation.ts +0 -20
- package/src/shared/widget-config.ts +0 -171
- package/src/styles/widget.css.ts +0 -83
- package/src/types.ts +0 -98
- package/src/vite-env.d.ts +0 -4
package/src/runtime/bootstrap.ts
DELETED
|
@@ -1,366 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getCurrentConversation,
|
|
3
|
-
listConversationMessages,
|
|
4
|
-
mintWidgetSession,
|
|
5
|
-
sendPublicChatConfirmation,
|
|
6
|
-
sendPublicChatMessage,
|
|
7
|
-
startPublicChat,
|
|
8
|
-
} from "./api";
|
|
9
|
-
import { clearSession, loadSession, saveSession } from "./session-storage";
|
|
10
|
-
import type { WidgetMessage, WidgetSessionState } from "../types";
|
|
11
|
-
import { sortByCreatedAt } from "./messages";
|
|
12
|
-
import {
|
|
13
|
-
encodeStopConfirmationPrompt,
|
|
14
|
-
encodeStopConfirmationResult,
|
|
15
|
-
type StopConfirmationDecision,
|
|
16
|
-
} from "../shared/stop-confirmation";
|
|
17
|
-
import { widgetDebug, widgetWarn } from "./debug";
|
|
18
|
-
|
|
19
|
-
export type WidgetBootstrapState = {
|
|
20
|
-
session: WidgetSessionState;
|
|
21
|
-
conversationId?: string;
|
|
22
|
-
messages: WidgetMessage[];
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Compact, JSON-safe error shape for the verbose logs. Strips noisy
|
|
27
|
-
* stack lines but keeps message + name + (if present) `code` so AppError
|
|
28
|
-
* responses surface their server-side code in the customer's console.
|
|
29
|
-
*/
|
|
30
|
-
function serializeError(error: unknown): {
|
|
31
|
-
name: string;
|
|
32
|
-
message: string;
|
|
33
|
-
code?: string;
|
|
34
|
-
status?: number;
|
|
35
|
-
} {
|
|
36
|
-
if (error instanceof Error) {
|
|
37
|
-
const err = error as Error & { code?: unknown; status?: unknown };
|
|
38
|
-
return {
|
|
39
|
-
name: err.name,
|
|
40
|
-
message: err.message,
|
|
41
|
-
code: typeof err.code === "string" ? err.code : undefined,
|
|
42
|
-
status: typeof err.status === "number" ? err.status : undefined,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
return { name: "Unknown", message: String(error) };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export type WidgetBootstrapDeps = {
|
|
49
|
-
loadSession: typeof loadSession;
|
|
50
|
-
saveSession: typeof saveSession;
|
|
51
|
-
mintWidgetSession: typeof mintWidgetSession;
|
|
52
|
-
getCurrentConversation: typeof getCurrentConversation;
|
|
53
|
-
listConversationMessages: typeof listConversationMessages;
|
|
54
|
-
getOrigin: () => string;
|
|
55
|
-
now: () => number;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const defaultBootstrapDeps: WidgetBootstrapDeps = {
|
|
59
|
-
loadSession,
|
|
60
|
-
saveSession,
|
|
61
|
-
mintWidgetSession,
|
|
62
|
-
getCurrentConversation,
|
|
63
|
-
listConversationMessages,
|
|
64
|
-
getOrigin: () => window.location.origin,
|
|
65
|
-
now: () => Date.now(),
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export function isSessionTokenValid(
|
|
69
|
-
session: WidgetSessionState | null,
|
|
70
|
-
nowMs: number = Date.now()
|
|
71
|
-
): session is WidgetSessionState {
|
|
72
|
-
if (!session) return false;
|
|
73
|
-
const expiryMs = new Date(session.expiresAt).getTime();
|
|
74
|
-
return Number.isFinite(expiryMs) && expiryMs > nowMs;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function ensureWidgetSession(input: {
|
|
78
|
-
agentId: string;
|
|
79
|
-
pageUrl?: string;
|
|
80
|
-
referrer?: string;
|
|
81
|
-
}, deps: WidgetBootstrapDeps = defaultBootstrapDeps, options?: {
|
|
82
|
-
refreshWidgetConfig?: boolean;
|
|
83
|
-
}): Promise<WidgetSessionState> {
|
|
84
|
-
const stored = deps.loadSession(input.agentId);
|
|
85
|
-
const hasValidStoredSession = isSessionTokenValid(stored, deps.now());
|
|
86
|
-
|
|
87
|
-
if (hasValidStoredSession && !options?.refreshWidgetConfig) {
|
|
88
|
-
return stored;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (hasValidStoredSession && options?.refreshWidgetConfig) {
|
|
92
|
-
try {
|
|
93
|
-
const minted = await deps.mintWidgetSession({
|
|
94
|
-
agentId: input.agentId,
|
|
95
|
-
origin: deps.getOrigin(),
|
|
96
|
-
pageUrl: input.pageUrl,
|
|
97
|
-
referrer: input.referrer,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const refreshed: WidgetSessionState = {
|
|
101
|
-
...stored,
|
|
102
|
-
widgetConfig: minted.widgetConfig,
|
|
103
|
-
};
|
|
104
|
-
deps.saveSession(input.agentId, refreshed);
|
|
105
|
-
return refreshed;
|
|
106
|
-
} catch (error) {
|
|
107
|
-
// Network blip during the refresh is recoverable — the cached
|
|
108
|
-
// session is still valid, so we fall back to it. But log so the
|
|
109
|
-
// customer's DevTools shows the underlying API failure (origin
|
|
110
|
-
// not allowed, agent archived, server 5xx, etc.).
|
|
111
|
-
widgetWarn(
|
|
112
|
-
"bootstrap.refreshSession",
|
|
113
|
-
"config refresh failed, using cached session",
|
|
114
|
-
{ agentId: input.agentId, error: serializeError(error) },
|
|
115
|
-
);
|
|
116
|
-
return stored;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const minted = await deps.mintWidgetSession({
|
|
121
|
-
agentId: input.agentId,
|
|
122
|
-
origin: deps.getOrigin(),
|
|
123
|
-
pageUrl: input.pageUrl,
|
|
124
|
-
referrer: input.referrer,
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const sessionState: WidgetSessionState = {
|
|
128
|
-
sessionToken: minted.sessionToken,
|
|
129
|
-
expiresAt: minted.expiresAt,
|
|
130
|
-
widgetConfig: minted.widgetConfig,
|
|
131
|
-
};
|
|
132
|
-
deps.saveSession(input.agentId, sessionState);
|
|
133
|
-
return sessionState;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export async function bootstrapWidgetWithDeps(input: {
|
|
137
|
-
agentId: string;
|
|
138
|
-
pageUrl?: string;
|
|
139
|
-
referrer?: string;
|
|
140
|
-
}, deps: WidgetBootstrapDeps): Promise<WidgetBootstrapState> {
|
|
141
|
-
const session = await ensureWidgetSession(input, deps, {
|
|
142
|
-
refreshWidgetConfig: true,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
let conversationId = session.conversationId;
|
|
146
|
-
let messages: WidgetMessage[] = [];
|
|
147
|
-
|
|
148
|
-
if (conversationId) {
|
|
149
|
-
try {
|
|
150
|
-
const transcript = await deps.listConversationMessages({
|
|
151
|
-
conversationId,
|
|
152
|
-
sessionToken: session.sessionToken,
|
|
153
|
-
});
|
|
154
|
-
messages = sortByCreatedAt(transcript.data);
|
|
155
|
-
} catch (error) {
|
|
156
|
-
// Stored conversation id was orphaned (deleted server-side,
|
|
157
|
-
// expired session, etc.). Drop it and act as if we're a fresh
|
|
158
|
-
// visitor. Surface the underlying failure — silent-drop here is
|
|
159
|
-
// exactly what makes "no welcome message + no send" so hard to
|
|
160
|
-
// debug from the outside.
|
|
161
|
-
widgetWarn(
|
|
162
|
-
"bootstrap.listMessages",
|
|
163
|
-
"transcript fetch failed, resetting conversation",
|
|
164
|
-
{
|
|
165
|
-
agentId: input.agentId,
|
|
166
|
-
conversationId,
|
|
167
|
-
error: serializeError(error),
|
|
168
|
-
},
|
|
169
|
-
);
|
|
170
|
-
conversationId = undefined;
|
|
171
|
-
messages = [];
|
|
172
|
-
}
|
|
173
|
-
} else {
|
|
174
|
-
try {
|
|
175
|
-
const current = await deps.getCurrentConversation(session.sessionToken);
|
|
176
|
-
conversationId = current.conversation?.id ?? undefined;
|
|
177
|
-
if (conversationId) {
|
|
178
|
-
const transcript = await deps.listConversationMessages({
|
|
179
|
-
conversationId,
|
|
180
|
-
sessionToken: session.sessionToken,
|
|
181
|
-
});
|
|
182
|
-
messages = sortByCreatedAt(transcript.data);
|
|
183
|
-
}
|
|
184
|
-
} catch (error) {
|
|
185
|
-
// The `current conversation` lookup failed (most commonly a 401
|
|
186
|
-
// because the session token is stale, or a 403 because the host
|
|
187
|
-
// origin isn't in the agent's allow-list). Without surfacing
|
|
188
|
-
// this, the panel renders empty + `sendDisabled=true` and the
|
|
189
|
-
// visitor has no way to recover or signal something's wrong.
|
|
190
|
-
widgetWarn(
|
|
191
|
-
"bootstrap.currentConversation",
|
|
192
|
-
"no conversation could be resolved, starting fresh",
|
|
193
|
-
{ agentId: input.agentId, error: serializeError(error) },
|
|
194
|
-
);
|
|
195
|
-
conversationId = undefined;
|
|
196
|
-
messages = [];
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const sessionState: WidgetSessionState = {
|
|
201
|
-
...session,
|
|
202
|
-
conversationId,
|
|
203
|
-
};
|
|
204
|
-
deps.saveSession(input.agentId, sessionState);
|
|
205
|
-
|
|
206
|
-
widgetDebug("bootstrap.ready", "session resolved", {
|
|
207
|
-
agentId: input.agentId,
|
|
208
|
-
hasConversation: Boolean(conversationId),
|
|
209
|
-
messageCount: messages.length,
|
|
210
|
-
autoStart: Boolean(sessionState.widgetConfig.widgetBehavior?.autoStart),
|
|
211
|
-
defaultOpen: Boolean(sessionState.widgetConfig.widgetBehavior?.defaultOpen),
|
|
212
|
-
alwaysOpen: Boolean(sessionState.widgetConfig.widgetBehavior?.alwaysOpen),
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
return {
|
|
216
|
-
session: sessionState,
|
|
217
|
-
conversationId,
|
|
218
|
-
messages,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export async function bootstrapWidget(input: {
|
|
223
|
-
agentId: string;
|
|
224
|
-
pageUrl?: string;
|
|
225
|
-
referrer?: string;
|
|
226
|
-
}): Promise<WidgetBootstrapState> {
|
|
227
|
-
return bootstrapWidgetWithDeps(input, defaultBootstrapDeps);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Send a user message. Lazy-starts the conversation on the first send —
|
|
232
|
-
* `startPublicChat` is called transparently when no `conversationId` is
|
|
233
|
-
* stored yet, so visitors never create empty conversations by opening
|
|
234
|
-
* the panel and walking away. When a start happens as part of this call
|
|
235
|
-
* the server-inserted welcome message rides back on `welcomeMessage`
|
|
236
|
-
* so the caller can reconcile the client-side placeholder bubble with
|
|
237
|
-
* the real DB row.
|
|
238
|
-
*/
|
|
239
|
-
export async function submitWidgetMessage(input: {
|
|
240
|
-
agentId: string;
|
|
241
|
-
content: string;
|
|
242
|
-
}): Promise<{
|
|
243
|
-
session: WidgetSessionState;
|
|
244
|
-
conversationId: string;
|
|
245
|
-
messages: WidgetMessage[];
|
|
246
|
-
userMessage: WidgetMessage;
|
|
247
|
-
assistantMessages: WidgetMessage[];
|
|
248
|
-
welcomeMessage?: WidgetMessage;
|
|
249
|
-
}> {
|
|
250
|
-
let stored = await ensureWidgetSession({
|
|
251
|
-
agentId: input.agentId,
|
|
252
|
-
pageUrl: window.location.href,
|
|
253
|
-
referrer: document.referrer || undefined,
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
let welcomeMessage: WidgetMessage | undefined;
|
|
257
|
-
let conversationId: string;
|
|
258
|
-
if (stored.conversationId) {
|
|
259
|
-
conversationId = stored.conversationId;
|
|
260
|
-
} else {
|
|
261
|
-
const started = await startPublicChat({
|
|
262
|
-
agentId: input.agentId,
|
|
263
|
-
sessionToken: stored.sessionToken,
|
|
264
|
-
});
|
|
265
|
-
conversationId = started.conversation.id;
|
|
266
|
-
stored = { ...stored, conversationId };
|
|
267
|
-
saveSession(input.agentId, stored);
|
|
268
|
-
welcomeMessage = started.welcomeMessage;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const response = await sendPublicChatMessage({
|
|
272
|
-
conversationId,
|
|
273
|
-
sessionToken: stored.sessionToken,
|
|
274
|
-
content: input.content,
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
const nextSession: WidgetSessionState = {
|
|
278
|
-
...stored,
|
|
279
|
-
conversationId,
|
|
280
|
-
};
|
|
281
|
-
saveSession(input.agentId, nextSession);
|
|
282
|
-
|
|
283
|
-
return {
|
|
284
|
-
session: nextSession,
|
|
285
|
-
conversationId,
|
|
286
|
-
messages: sortByCreatedAt([
|
|
287
|
-
...(welcomeMessage ? [welcomeMessage] : []),
|
|
288
|
-
response.userMessage,
|
|
289
|
-
...response.assistantMessages,
|
|
290
|
-
]),
|
|
291
|
-
userMessage: response.userMessage,
|
|
292
|
-
assistantMessages: response.assistantMessages,
|
|
293
|
-
welcomeMessage,
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
export async function submitWidgetStopConfirmation(input: {
|
|
298
|
-
agentId: string;
|
|
299
|
-
confirmationId: string;
|
|
300
|
-
prompt?: string;
|
|
301
|
-
}): Promise<{
|
|
302
|
-
session: WidgetSessionState;
|
|
303
|
-
conversationId: string;
|
|
304
|
-
confirmationMessage: WidgetMessage;
|
|
305
|
-
}>;
|
|
306
|
-
export async function submitWidgetStopConfirmation(input: {
|
|
307
|
-
agentId: string;
|
|
308
|
-
confirmationId: string;
|
|
309
|
-
decision: StopConfirmationDecision;
|
|
310
|
-
}): Promise<{
|
|
311
|
-
session: WidgetSessionState;
|
|
312
|
-
conversationId: string;
|
|
313
|
-
confirmationMessage: WidgetMessage;
|
|
314
|
-
}>;
|
|
315
|
-
export async function submitWidgetStopConfirmation(input: {
|
|
316
|
-
agentId: string;
|
|
317
|
-
confirmationId: string;
|
|
318
|
-
prompt?: string;
|
|
319
|
-
decision?: StopConfirmationDecision;
|
|
320
|
-
}): Promise<{
|
|
321
|
-
session: WidgetSessionState;
|
|
322
|
-
conversationId: string;
|
|
323
|
-
confirmationMessage: WidgetMessage;
|
|
324
|
-
}> {
|
|
325
|
-
const stored = await ensureWidgetSession({
|
|
326
|
-
agentId: input.agentId,
|
|
327
|
-
pageUrl: window.location.href,
|
|
328
|
-
referrer: document.referrer || undefined,
|
|
329
|
-
});
|
|
330
|
-
if (!stored.conversationId) {
|
|
331
|
-
throw new Error("Widget conversation not started");
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const content =
|
|
335
|
-
input.decision != null
|
|
336
|
-
? encodeStopConfirmationResult({
|
|
337
|
-
confirmationId: input.confirmationId,
|
|
338
|
-
decision: input.decision,
|
|
339
|
-
})
|
|
340
|
-
: encodeStopConfirmationPrompt({
|
|
341
|
-
confirmationId: input.confirmationId,
|
|
342
|
-
prompt: input.prompt,
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
const response = await sendPublicChatConfirmation({
|
|
346
|
-
conversationId: stored.conversationId,
|
|
347
|
-
sessionToken: stored.sessionToken,
|
|
348
|
-
content,
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
const nextSession: WidgetSessionState = {
|
|
352
|
-
...stored,
|
|
353
|
-
conversationId: stored.conversationId,
|
|
354
|
-
};
|
|
355
|
-
saveSession(input.agentId, nextSession);
|
|
356
|
-
|
|
357
|
-
return {
|
|
358
|
-
session: nextSession,
|
|
359
|
-
conversationId: stored.conversationId,
|
|
360
|
-
confirmationMessage: response.confirmationMessage,
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
export async function resetWidgetSession(agentId: string): Promise<void> {
|
|
365
|
-
clearSession(agentId);
|
|
366
|
-
}
|
package/src/runtime/debug.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Widget debug logging.
|
|
3
|
-
*
|
|
4
|
-
* Two tiers:
|
|
5
|
-
*
|
|
6
|
-
* 1. ALWAYS-ON warnings (`widgetWarn`) — fired on every silent failure
|
|
7
|
-
* path that would otherwise leave the visitor with a broken widget
|
|
8
|
-
* and no signal in the console. e.g. `/start` returns 403, the
|
|
9
|
-
* transcript fetch dies, the session token is stale. Surfaced via
|
|
10
|
-
* `console.warn` so a customer running our widget on their site can
|
|
11
|
-
* open DevTools and immediately see WHY the chat isn't working —
|
|
12
|
-
* no env vars, no rebuilds, no extra steps.
|
|
13
|
-
*
|
|
14
|
-
* 2. OPT-IN traces (`widgetDebug`) — verbose state-transition logs
|
|
15
|
-
* ("bootstrap started", "session restored", "auto-start fired"). Off
|
|
16
|
-
* by default; turned on by either:
|
|
17
|
-
* - `localStorage.setItem("usereq_debug", "1")` (sticky)
|
|
18
|
-
* - `?usereq_debug=1` in the embed-host's URL (one-shot)
|
|
19
|
-
*
|
|
20
|
-
* Both routes are namespace-prefixed so customer's own console output
|
|
21
|
-
* doesn't get tangled with ours.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
const PREFIX = "[usereq-widget]";
|
|
25
|
-
|
|
26
|
-
/** True when verbose-trace logging is enabled. Memoized per page load. */
|
|
27
|
-
let debugEnabled: boolean | null = null;
|
|
28
|
-
|
|
29
|
-
export function isWidgetDebugEnabled(): boolean {
|
|
30
|
-
if (debugEnabled !== null) return debugEnabled;
|
|
31
|
-
debugEnabled = computeDebugEnabled();
|
|
32
|
-
return debugEnabled;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function computeDebugEnabled(): boolean {
|
|
36
|
-
if (typeof window === "undefined") return false;
|
|
37
|
-
try {
|
|
38
|
-
// Query param is one-shot per page load — handy for "ask the
|
|
39
|
-
// customer to add `?usereq_debug=1` to their URL and reproduce."
|
|
40
|
-
const params = new URLSearchParams(window.location.search);
|
|
41
|
-
if (params.get("usereq_debug") === "1") return true;
|
|
42
|
-
} catch {
|
|
43
|
-
/* iframe with restricted location.search — ignore */
|
|
44
|
-
}
|
|
45
|
-
try {
|
|
46
|
-
if (window.localStorage?.getItem("usereq_debug") === "1") return true;
|
|
47
|
-
} catch {
|
|
48
|
-
/* incognito / disabled localStorage — ignore */
|
|
49
|
-
}
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Always-on warning. Use for silent-failure paths where we currently
|
|
55
|
-
* swallow an error but the visitor's chat will visibly break.
|
|
56
|
-
*
|
|
57
|
-
* Output shape:
|
|
58
|
-
* [usereq-widget] bootstrap.listMessages failed { agentId, sessionToken, error }
|
|
59
|
-
*/
|
|
60
|
-
export function widgetWarn(scope: string, message: string, context?: unknown): void {
|
|
61
|
-
if (context === undefined) {
|
|
62
|
-
console.warn(`${PREFIX} ${scope}: ${message}`);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
console.warn(`${PREFIX} ${scope}: ${message}`, sanitizeForLog(context));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Opt-in trace. Logs state transitions / network calls when debug mode
|
|
70
|
-
* is on. Production builds keep these as `console.debug` calls; modern
|
|
71
|
-
* DevTools hide `debug` by default so the warning channel stays clean.
|
|
72
|
-
*/
|
|
73
|
-
export function widgetDebug(scope: string, message: string, context?: unknown): void {
|
|
74
|
-
if (!isWidgetDebugEnabled()) return;
|
|
75
|
-
if (context === undefined) {
|
|
76
|
-
console.debug(`${PREFIX} ${scope}: ${message}`);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
console.debug(`${PREFIX} ${scope}: ${message}`, sanitizeForLog(context));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Strip session tokens / PII from arbitrary context objects before
|
|
84
|
-
* logging. Keeps prefixes long enough to recognize the session at a
|
|
85
|
-
* glance ("eyJh…") without leaking the secret.
|
|
86
|
-
*/
|
|
87
|
-
function sanitizeForLog(context: unknown): unknown {
|
|
88
|
-
if (context === null || typeof context !== "object") return context;
|
|
89
|
-
try {
|
|
90
|
-
const json = JSON.parse(
|
|
91
|
-
JSON.stringify(context, (_key, value) => {
|
|
92
|
-
if (typeof value === "string") {
|
|
93
|
-
if (value.length > 32 && /^[A-Za-z0-9_.\-+/=]+$/.test(value)) {
|
|
94
|
-
return `${value.slice(0, 8)}…(redacted, len=${value.length})`;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return value;
|
|
98
|
-
}),
|
|
99
|
-
);
|
|
100
|
-
return json;
|
|
101
|
-
} catch {
|
|
102
|
-
return context;
|
|
103
|
-
}
|
|
104
|
-
}
|
package/src/runtime/messages.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { WidgetMessage } from "../types";
|
|
2
|
-
|
|
3
|
-
export function sortByCreatedAt(messages: WidgetMessage[]): WidgetMessage[] {
|
|
4
|
-
return [...messages].sort(
|
|
5
|
-
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
|
6
|
-
);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function messageRoleLabel(role: WidgetMessage["role"]): string {
|
|
10
|
-
return role === "user" ? "You" : "Assistant";
|
|
11
|
-
}
|
|
12
|
-
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { WidgetSessionState } from "../types";
|
|
2
|
-
|
|
3
|
-
function key(agentId: string): string {
|
|
4
|
-
return `usereq-widget:${agentId}`;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function loadSession(agentId: string): WidgetSessionState | null {
|
|
8
|
-
try {
|
|
9
|
-
const raw = sessionStorage.getItem(key(agentId));
|
|
10
|
-
if (!raw) return null;
|
|
11
|
-
const parsed = JSON.parse(raw) as WidgetSessionState;
|
|
12
|
-
if (
|
|
13
|
-
!parsed?.sessionToken ||
|
|
14
|
-
!parsed?.expiresAt ||
|
|
15
|
-
!parsed?.widgetConfig?.agentId ||
|
|
16
|
-
!parsed?.widgetConfig?.name
|
|
17
|
-
) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
return parsed;
|
|
21
|
-
} catch {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function saveSession(agentId: string, value: WidgetSessionState): void {
|
|
27
|
-
sessionStorage.setItem(key(agentId), JSON.stringify(value));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function clearSession(agentId: string): void {
|
|
31
|
-
sessionStorage.removeItem(key(agentId));
|
|
32
|
-
}
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { useEffect, useLayoutEffect, useState } from "react";
|
|
2
|
-
import type { WidgetTriggerRule } from "../types";
|
|
3
|
-
|
|
4
|
-
type Cleanup = () => void;
|
|
5
|
-
|
|
6
|
-
function isValidDelay(value: unknown): value is number {
|
|
7
|
-
return typeof value === "number" && !Number.isNaN(value) && value >= 0;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function waitForElementById(
|
|
11
|
-
id: string,
|
|
12
|
-
callback: (element: HTMLElement) => void,
|
|
13
|
-
): Cleanup {
|
|
14
|
-
const immediate = document.getElementById(id);
|
|
15
|
-
if (immediate instanceof HTMLElement) {
|
|
16
|
-
callback(immediate);
|
|
17
|
-
return () => undefined;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const observedRoot = document.body ?? document.documentElement;
|
|
21
|
-
if (!observedRoot) {
|
|
22
|
-
return () => undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
let settled = false;
|
|
26
|
-
const observer = new MutationObserver(() => {
|
|
27
|
-
if (settled) return;
|
|
28
|
-
const node = document.getElementById(id);
|
|
29
|
-
if (node instanceof HTMLElement) {
|
|
30
|
-
settled = true;
|
|
31
|
-
observer.disconnect();
|
|
32
|
-
callback(node);
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
observer.observe(observedRoot, { childList: true, subtree: true });
|
|
37
|
-
|
|
38
|
-
return () => {
|
|
39
|
-
settled = true;
|
|
40
|
-
observer.disconnect();
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function useWidgetTriggerGate(triggerRule?: WidgetTriggerRule | null) {
|
|
45
|
-
const [ready, setReady] = useState(triggerRule == null);
|
|
46
|
-
const useReadyEffect =
|
|
47
|
-
typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
48
|
-
|
|
49
|
-
useReadyEffect(() => {
|
|
50
|
-
let canceled = false;
|
|
51
|
-
|
|
52
|
-
const markReady = () => {
|
|
53
|
-
if (!canceled) {
|
|
54
|
-
setReady(true);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
if (!triggerRule) {
|
|
59
|
-
setReady(true);
|
|
60
|
-
return () => {
|
|
61
|
-
canceled = true;
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
setReady(false);
|
|
66
|
-
|
|
67
|
-
let cleanup: Cleanup = () => undefined;
|
|
68
|
-
|
|
69
|
-
switch (triggerRule.triggerRuleType) {
|
|
70
|
-
case "time_delay": {
|
|
71
|
-
if (!isValidDelay(triggerRule.timeDelay)) {
|
|
72
|
-
markReady();
|
|
73
|
-
return () => {
|
|
74
|
-
canceled = true;
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const timeout = window.setTimeout(
|
|
79
|
-
markReady,
|
|
80
|
-
triggerRule.timeDelay * 1000,
|
|
81
|
-
);
|
|
82
|
-
cleanup = () => {
|
|
83
|
-
window.clearTimeout(timeout);
|
|
84
|
-
};
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
case "scroll_to_bottom": {
|
|
89
|
-
if (!triggerRule.scrollToBottom) {
|
|
90
|
-
markReady();
|
|
91
|
-
return () => {
|
|
92
|
-
canceled = true;
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const checkBottom = () => {
|
|
97
|
-
const scrollTop = window.scrollY;
|
|
98
|
-
const viewportHeight = window.innerHeight;
|
|
99
|
-
const scrollHeight = document.documentElement.scrollHeight;
|
|
100
|
-
if (scrollTop + viewportHeight >= scrollHeight - 1) {
|
|
101
|
-
markReady();
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
checkBottom();
|
|
106
|
-
window.addEventListener("scroll", checkBottom, { passive: true });
|
|
107
|
-
window.addEventListener("resize", checkBottom);
|
|
108
|
-
cleanup = () => {
|
|
109
|
-
window.removeEventListener("scroll", checkBottom);
|
|
110
|
-
window.removeEventListener("resize", checkBottom);
|
|
111
|
-
};
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
case "element_displayed": {
|
|
116
|
-
if (!triggerRule.elementDisplayed) {
|
|
117
|
-
markReady();
|
|
118
|
-
return () => {
|
|
119
|
-
canceled = true;
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
let observer: IntersectionObserver | null = null;
|
|
124
|
-
const stopWaiting = waitForElementById(
|
|
125
|
-
triggerRule.elementDisplayed,
|
|
126
|
-
(node) => {
|
|
127
|
-
observer = new IntersectionObserver((entries) => {
|
|
128
|
-
if (entries.some((entry) => entry.isIntersecting)) {
|
|
129
|
-
markReady();
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
observer.observe(node);
|
|
133
|
-
},
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
cleanup = () => {
|
|
137
|
-
observer?.disconnect();
|
|
138
|
-
stopWaiting();
|
|
139
|
-
};
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
case "element_clicked": {
|
|
144
|
-
if (!triggerRule.elementClicked) {
|
|
145
|
-
markReady();
|
|
146
|
-
return () => {
|
|
147
|
-
canceled = true;
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const selector = `#${CSS.escape(triggerRule.elementClicked)}`;
|
|
152
|
-
const handler = (event: MouseEvent) => {
|
|
153
|
-
const target = event.target;
|
|
154
|
-
if (!(target instanceof Element)) return;
|
|
155
|
-
if (target.closest(selector)) {
|
|
156
|
-
markReady();
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
document.addEventListener("click", handler, true);
|
|
161
|
-
cleanup = () => {
|
|
162
|
-
document.removeEventListener("click", handler, true);
|
|
163
|
-
};
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
default: {
|
|
168
|
-
markReady();
|
|
169
|
-
return () => {
|
|
170
|
-
canceled = true;
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return () => {
|
|
176
|
-
canceled = true;
|
|
177
|
-
cleanup();
|
|
178
|
-
};
|
|
179
|
-
}, [triggerRule]);
|
|
180
|
-
|
|
181
|
-
return { ready: ready || triggerRule == null };
|
|
182
|
-
}
|
package/src/shared/shadow-css.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
type WidgetShadowCssGlobal = typeof globalThis & {
|
|
2
|
-
__USEREQ_WIDGET_SHADOW_CSS__?: string;
|
|
3
|
-
};
|
|
4
|
-
|
|
5
|
-
export const WIDGET_SHADOW_CSS_GLOBAL = "__USEREQ_WIDGET_SHADOW_CSS__";
|
|
6
|
-
|
|
7
|
-
export function setWidgetShadowCss(css: string | null | undefined) {
|
|
8
|
-
const global = globalThis as WidgetShadowCssGlobal;
|
|
9
|
-
global[WIDGET_SHADOW_CSS_GLOBAL] = css?.trim() ? css : undefined;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function getWidgetShadowCss(): string | null {
|
|
13
|
-
const global = globalThis as WidgetShadowCssGlobal;
|
|
14
|
-
return global[WIDGET_SHADOW_CSS_GLOBAL]?.trim() || null;
|
|
15
|
-
}
|