@usereq/widget 0.1.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 +166 -0
- package/dist/widget.css +2 -0
- package/dist/widget.js +3569 -0
- package/package.json +98 -0
- package/src/chat-widget/chat-widget-appearance.ts +55 -0
- package/src/chat-widget/chat-widget-defaults.tsx +45 -0
- package/src/chat-widget/components/chat-widget-frame.tsx +56 -0
- package/src/chat-widget/components/chat-widget-layout.ts +6 -0
- package/src/chat-widget/components/chat-widget-primitives.tsx +720 -0
- package/src/chat-widget/components/confirmation.tsx +155 -0
- package/src/chat-widget/components/conversation.tsx +110 -0
- package/src/chat-widget/components/message.tsx +325 -0
- package/src/chat-widget/index.ts +9 -0
- package/src/chat-widget/stop-confirmation.ts +288 -0
- package/src/chat-widget/styles/chat-widget-box.tsx +258 -0
- package/src/chat-widget/styles/chat-widget-bubble.tsx +238 -0
- package/src/chat-widget/styles/chat-widget-chatbar.tsx +248 -0
- package/src/custom-element/agent-widget-element.tsx +584 -0
- package/src/embed.ts +8 -0
- package/src/index.ts +10 -0
- package/src/register.ts +15 -0
- package/src/renderer/index.ts +6 -0
- package/src/renderer/widget-runtime.tsx +205 -0
- package/src/runtime/analytics.ts +327 -0
- package/src/runtime/api-origin.ts +29 -0
- package/src/runtime/api.ts +151 -0
- package/src/runtime/bootstrap.ts +309 -0
- package/src/runtime/messages.ts +12 -0
- package/src/runtime/session-storage.ts +32 -0
- package/src/runtime/token-renewal.ts +13 -0
- package/src/runtime/trigger-rule.ts +182 -0
- package/src/shared/analytics.ts +54 -0
- package/src/shared/shadow-css.ts +15 -0
- package/src/shared/shadow-theme.ts +85 -0
- package/src/shared/stop-confirmation.ts +20 -0
- package/src/shared/widget-config.ts +104 -0
- package/src/styles/widget.css.ts +27 -0
- package/src/types.ts +85 -0
- package/src/vite-env.d.ts +4 -0
|
@@ -0,0 +1,309 @@
|
|
|
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
|
+
|
|
18
|
+
export type WidgetBootstrapState = {
|
|
19
|
+
session: WidgetSessionState;
|
|
20
|
+
conversationId?: string;
|
|
21
|
+
messages: WidgetMessage[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type WidgetBootstrapDeps = {
|
|
25
|
+
loadSession: typeof loadSession;
|
|
26
|
+
saveSession: typeof saveSession;
|
|
27
|
+
mintWidgetSession: typeof mintWidgetSession;
|
|
28
|
+
getCurrentConversation: typeof getCurrentConversation;
|
|
29
|
+
listConversationMessages: typeof listConversationMessages;
|
|
30
|
+
getOrigin: () => string;
|
|
31
|
+
now: () => number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const defaultBootstrapDeps: WidgetBootstrapDeps = {
|
|
35
|
+
loadSession,
|
|
36
|
+
saveSession,
|
|
37
|
+
mintWidgetSession,
|
|
38
|
+
getCurrentConversation,
|
|
39
|
+
listConversationMessages,
|
|
40
|
+
getOrigin: () => window.location.origin,
|
|
41
|
+
now: () => Date.now(),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function isSessionTokenValid(
|
|
45
|
+
session: WidgetSessionState | null,
|
|
46
|
+
nowMs: number = Date.now()
|
|
47
|
+
): session is WidgetSessionState {
|
|
48
|
+
if (!session) return false;
|
|
49
|
+
const expiryMs = new Date(session.expiresAt).getTime();
|
|
50
|
+
return Number.isFinite(expiryMs) && expiryMs > nowMs;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function ensureWidgetSession(input: {
|
|
54
|
+
agentId: string;
|
|
55
|
+
pageUrl?: string;
|
|
56
|
+
referrer?: string;
|
|
57
|
+
}, deps: WidgetBootstrapDeps = defaultBootstrapDeps, options?: {
|
|
58
|
+
refreshWidgetConfig?: boolean;
|
|
59
|
+
}): Promise<WidgetSessionState> {
|
|
60
|
+
const stored = deps.loadSession(input.agentId);
|
|
61
|
+
const hasValidStoredSession = isSessionTokenValid(stored, deps.now());
|
|
62
|
+
|
|
63
|
+
if (hasValidStoredSession && !options?.refreshWidgetConfig) {
|
|
64
|
+
return stored;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (hasValidStoredSession && options?.refreshWidgetConfig) {
|
|
68
|
+
try {
|
|
69
|
+
const minted = await deps.mintWidgetSession({
|
|
70
|
+
agentId: input.agentId,
|
|
71
|
+
origin: deps.getOrigin(),
|
|
72
|
+
pageUrl: input.pageUrl,
|
|
73
|
+
referrer: input.referrer,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const refreshed: WidgetSessionState = {
|
|
77
|
+
...stored,
|
|
78
|
+
widgetConfig: minted.widgetConfig,
|
|
79
|
+
};
|
|
80
|
+
deps.saveSession(input.agentId, refreshed);
|
|
81
|
+
return refreshed;
|
|
82
|
+
} catch {
|
|
83
|
+
return stored;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const minted = await deps.mintWidgetSession({
|
|
88
|
+
agentId: input.agentId,
|
|
89
|
+
origin: deps.getOrigin(),
|
|
90
|
+
pageUrl: input.pageUrl,
|
|
91
|
+
referrer: input.referrer,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const sessionState: WidgetSessionState = {
|
|
95
|
+
sessionToken: minted.sessionToken,
|
|
96
|
+
expiresAt: minted.expiresAt,
|
|
97
|
+
widgetConfig: minted.widgetConfig,
|
|
98
|
+
};
|
|
99
|
+
deps.saveSession(input.agentId, sessionState);
|
|
100
|
+
return sessionState;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function bootstrapWidgetWithDeps(input: {
|
|
104
|
+
agentId: string;
|
|
105
|
+
pageUrl?: string;
|
|
106
|
+
referrer?: string;
|
|
107
|
+
}, deps: WidgetBootstrapDeps): Promise<WidgetBootstrapState> {
|
|
108
|
+
const session = await ensureWidgetSession(input, deps, {
|
|
109
|
+
refreshWidgetConfig: true,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
let conversationId = session.conversationId;
|
|
113
|
+
let messages: WidgetMessage[] = [];
|
|
114
|
+
|
|
115
|
+
if (conversationId) {
|
|
116
|
+
try {
|
|
117
|
+
const transcript = await deps.listConversationMessages({
|
|
118
|
+
conversationId,
|
|
119
|
+
sessionToken: session.sessionToken,
|
|
120
|
+
});
|
|
121
|
+
messages = sortByCreatedAt(transcript.data);
|
|
122
|
+
} catch {
|
|
123
|
+
conversationId = undefined;
|
|
124
|
+
messages = [];
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
try {
|
|
128
|
+
const current = await deps.getCurrentConversation(session.sessionToken);
|
|
129
|
+
conversationId = current.conversation?.id ?? undefined;
|
|
130
|
+
if (conversationId) {
|
|
131
|
+
const transcript = await deps.listConversationMessages({
|
|
132
|
+
conversationId,
|
|
133
|
+
sessionToken: session.sessionToken,
|
|
134
|
+
});
|
|
135
|
+
messages = sortByCreatedAt(transcript.data);
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
conversationId = undefined;
|
|
139
|
+
messages = [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const sessionState: WidgetSessionState = {
|
|
144
|
+
...session,
|
|
145
|
+
conversationId,
|
|
146
|
+
};
|
|
147
|
+
deps.saveSession(input.agentId, sessionState);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
session: sessionState,
|
|
151
|
+
conversationId,
|
|
152
|
+
messages,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function bootstrapWidget(input: {
|
|
157
|
+
agentId: string;
|
|
158
|
+
pageUrl?: string;
|
|
159
|
+
referrer?: string;
|
|
160
|
+
}): Promise<WidgetBootstrapState> {
|
|
161
|
+
return bootstrapWidgetWithDeps(input, defaultBootstrapDeps);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function submitWidgetMessage(input: {
|
|
165
|
+
agentId: string;
|
|
166
|
+
content: string;
|
|
167
|
+
}): Promise<{
|
|
168
|
+
session: WidgetSessionState;
|
|
169
|
+
conversationId: string;
|
|
170
|
+
messages: WidgetMessage[];
|
|
171
|
+
userMessage: WidgetMessage;
|
|
172
|
+
assistantMessages: WidgetMessage[];
|
|
173
|
+
}> {
|
|
174
|
+
const stored = await ensureWidgetSession({
|
|
175
|
+
agentId: input.agentId,
|
|
176
|
+
pageUrl: window.location.href,
|
|
177
|
+
referrer: document.referrer || undefined,
|
|
178
|
+
});
|
|
179
|
+
if (!stored.conversationId) {
|
|
180
|
+
throw new Error("Widget conversation not started");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const response = await sendPublicChatMessage({
|
|
184
|
+
conversationId: stored.conversationId,
|
|
185
|
+
sessionToken: stored.sessionToken,
|
|
186
|
+
content: input.content,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const nextSession: WidgetSessionState = {
|
|
190
|
+
...stored,
|
|
191
|
+
conversationId: stored.conversationId,
|
|
192
|
+
};
|
|
193
|
+
saveSession(input.agentId, nextSession);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
session: nextSession,
|
|
197
|
+
conversationId: stored.conversationId,
|
|
198
|
+
messages: sortByCreatedAt([
|
|
199
|
+
response.userMessage,
|
|
200
|
+
...response.assistantMessages,
|
|
201
|
+
]),
|
|
202
|
+
userMessage: response.userMessage,
|
|
203
|
+
assistantMessages: response.assistantMessages,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function submitWidgetStopConfirmation(input: {
|
|
208
|
+
agentId: string;
|
|
209
|
+
confirmationId: string;
|
|
210
|
+
prompt?: string;
|
|
211
|
+
}): Promise<{
|
|
212
|
+
session: WidgetSessionState;
|
|
213
|
+
conversationId: string;
|
|
214
|
+
confirmationMessage: WidgetMessage;
|
|
215
|
+
}>;
|
|
216
|
+
export async function submitWidgetStopConfirmation(input: {
|
|
217
|
+
agentId: string;
|
|
218
|
+
confirmationId: string;
|
|
219
|
+
decision: StopConfirmationDecision;
|
|
220
|
+
}): Promise<{
|
|
221
|
+
session: WidgetSessionState;
|
|
222
|
+
conversationId: string;
|
|
223
|
+
confirmationMessage: WidgetMessage;
|
|
224
|
+
}>;
|
|
225
|
+
export async function submitWidgetStopConfirmation(input: {
|
|
226
|
+
agentId: string;
|
|
227
|
+
confirmationId: string;
|
|
228
|
+
prompt?: string;
|
|
229
|
+
decision?: StopConfirmationDecision;
|
|
230
|
+
}): Promise<{
|
|
231
|
+
session: WidgetSessionState;
|
|
232
|
+
conversationId: string;
|
|
233
|
+
confirmationMessage: WidgetMessage;
|
|
234
|
+
}> {
|
|
235
|
+
const stored = await ensureWidgetSession({
|
|
236
|
+
agentId: input.agentId,
|
|
237
|
+
pageUrl: window.location.href,
|
|
238
|
+
referrer: document.referrer || undefined,
|
|
239
|
+
});
|
|
240
|
+
if (!stored.conversationId) {
|
|
241
|
+
throw new Error("Widget conversation not started");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const content =
|
|
245
|
+
input.decision != null
|
|
246
|
+
? encodeStopConfirmationResult({
|
|
247
|
+
confirmationId: input.confirmationId,
|
|
248
|
+
decision: input.decision,
|
|
249
|
+
})
|
|
250
|
+
: encodeStopConfirmationPrompt({
|
|
251
|
+
confirmationId: input.confirmationId,
|
|
252
|
+
prompt: input.prompt,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const response = await sendPublicChatConfirmation({
|
|
256
|
+
conversationId: stored.conversationId,
|
|
257
|
+
sessionToken: stored.sessionToken,
|
|
258
|
+
content,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const nextSession: WidgetSessionState = {
|
|
262
|
+
...stored,
|
|
263
|
+
conversationId: stored.conversationId,
|
|
264
|
+
};
|
|
265
|
+
saveSession(input.agentId, nextSession);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
session: nextSession,
|
|
269
|
+
conversationId: stored.conversationId,
|
|
270
|
+
confirmationMessage: response.confirmationMessage,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function startWidgetConversation(input: {
|
|
275
|
+
agentId: string;
|
|
276
|
+
}): Promise<{
|
|
277
|
+
session: WidgetSessionState;
|
|
278
|
+
conversationId: string;
|
|
279
|
+
messages: WidgetMessage[];
|
|
280
|
+
welcomeMessage: WidgetMessage;
|
|
281
|
+
}> {
|
|
282
|
+
const stored = await ensureWidgetSession({
|
|
283
|
+
agentId: input.agentId,
|
|
284
|
+
pageUrl: window.location.href,
|
|
285
|
+
referrer: document.referrer || undefined,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const response = await startPublicChat({
|
|
289
|
+
agentId: input.agentId,
|
|
290
|
+
sessionToken: stored.sessionToken,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const nextSession: WidgetSessionState = {
|
|
294
|
+
...stored,
|
|
295
|
+
conversationId: response.conversation.id,
|
|
296
|
+
};
|
|
297
|
+
saveSession(input.agentId, nextSession);
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
session: nextSession,
|
|
301
|
+
conversationId: response.conversation.id,
|
|
302
|
+
messages: sortByCreatedAt([response.welcomeMessage]),
|
|
303
|
+
welcomeMessage: response.welcomeMessage,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function resetWidgetSession(agentId: string): Promise<void> {
|
|
308
|
+
clearSession(agentId);
|
|
309
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function scheduleRenewal(
|
|
2
|
+
expiresAt: string,
|
|
3
|
+
onRenew: () => Promise<void> | void
|
|
4
|
+
): () => void {
|
|
5
|
+
const expiryMs = new Date(expiresAt).getTime();
|
|
6
|
+
const delay = Math.max(5_000, expiryMs - Date.now() - 60_000);
|
|
7
|
+
const timer = window.setTimeout(() => {
|
|
8
|
+
void onRenew();
|
|
9
|
+
}, delay);
|
|
10
|
+
|
|
11
|
+
return () => window.clearTimeout(timer);
|
|
12
|
+
}
|
|
13
|
+
|
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const WIDGET_ANALYTICS_EVENT_TYPES = [
|
|
2
|
+
"SCRIPT_LOADED",
|
|
3
|
+
"UNIQUE_SCRIPT_LOADED",
|
|
4
|
+
"VIEW",
|
|
5
|
+
"CLICK",
|
|
6
|
+
"SESSION_START",
|
|
7
|
+
"DISPLAY_AFTER_ELEMENT_SHOWED",
|
|
8
|
+
"WIDGET_DISPLAY_AFTER_VSL_SHOWED",
|
|
9
|
+
"LINK_CLICKED",
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
export type WidgetAnalyticsEventType =
|
|
13
|
+
(typeof WIDGET_ANALYTICS_EVENT_TYPES)[number];
|
|
14
|
+
|
|
15
|
+
export const WIDGET_CLIENT_ANALYTICS_EVENT_TYPES = [
|
|
16
|
+
"SCRIPT_LOADED",
|
|
17
|
+
"UNIQUE_SCRIPT_LOADED",
|
|
18
|
+
"VIEW",
|
|
19
|
+
"CLICK",
|
|
20
|
+
"DISPLAY_AFTER_ELEMENT_SHOWED",
|
|
21
|
+
"LINK_CLICKED",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export type WidgetClientAnalyticsEventType =
|
|
25
|
+
(typeof WIDGET_CLIENT_ANALYTICS_EVENT_TYPES)[number];
|
|
26
|
+
|
|
27
|
+
export const WIDGET_TRIGGER_RULE_TYPES = [
|
|
28
|
+
"time_delay",
|
|
29
|
+
"scroll_to_bottom",
|
|
30
|
+
"element_clicked",
|
|
31
|
+
"element_displayed",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
export type WidgetTriggerRuleType = (typeof WIDGET_TRIGGER_RULE_TYPES)[number];
|
|
35
|
+
|
|
36
|
+
export type WidgetAnalyticsBatchEvent = {
|
|
37
|
+
eventType: WidgetClientAnalyticsEventType;
|
|
38
|
+
occurredAt?: string;
|
|
39
|
+
conversationId?: string | null;
|
|
40
|
+
triggerRuleType?: WidgetTriggerRuleType | null;
|
|
41
|
+
linkUrl?: string | null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type WidgetAnalyticsBatchRequest = {
|
|
45
|
+
events: WidgetAnalyticsBatchEvent[];
|
|
46
|
+
pageUrl?: string;
|
|
47
|
+
referrer?: string;
|
|
48
|
+
sessionToken?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type WidgetAnalyticsBatchResponse = {
|
|
52
|
+
accepted: number;
|
|
53
|
+
};
|
|
54
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const WIDGET_SHADOW_THEME_CSS = `
|
|
2
|
+
:host {
|
|
3
|
+
color-scheme: light;
|
|
4
|
+
--tw-border-style: solid;
|
|
5
|
+
--radius: 0.625rem;
|
|
6
|
+
--font-geist-sans: Geist, sans-serif;
|
|
7
|
+
--font-geist-mono: Geist_Mono, monospace;
|
|
8
|
+
--background: oklch(1 0 0);
|
|
9
|
+
--foreground: oklch(0.145 0 0);
|
|
10
|
+
--card: oklch(1 0 0);
|
|
11
|
+
--card-foreground: oklch(0.145 0 0);
|
|
12
|
+
--popover: oklch(1 0 0);
|
|
13
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
14
|
+
--primary: oklch(0.205 0 0);
|
|
15
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
16
|
+
--secondary: oklch(0.97 0 0);
|
|
17
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
18
|
+
--brand: oklch(0.55 0.22 263);
|
|
19
|
+
--brand-foreground: oklch(0.985 0 0);
|
|
20
|
+
--muted: oklch(0.97 0 0);
|
|
21
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
22
|
+
--accent: oklch(0.97 0 0);
|
|
23
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
24
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
25
|
+
--border: oklch(0.922 0 0);
|
|
26
|
+
--input: oklch(0.922 0 0);
|
|
27
|
+
--ring: oklch(0.708 0 0);
|
|
28
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
29
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
30
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
31
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
32
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
33
|
+
--sidebar: oklch(0.985 0 0);
|
|
34
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
35
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
36
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
37
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
38
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
39
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
40
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
41
|
+
--color-background: var(--background);
|
|
42
|
+
--color-foreground: var(--foreground);
|
|
43
|
+
--color-card: var(--card);
|
|
44
|
+
--color-card-foreground: var(--card-foreground);
|
|
45
|
+
--color-popover: var(--popover);
|
|
46
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
47
|
+
--color-primary: var(--primary);
|
|
48
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
49
|
+
--color-secondary: var(--secondary);
|
|
50
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
51
|
+
--color-brand: var(--brand);
|
|
52
|
+
--color-brand-foreground: var(--brand-foreground);
|
|
53
|
+
--color-muted: var(--muted);
|
|
54
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
55
|
+
--color-accent: var(--accent);
|
|
56
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
57
|
+
--color-destructive: var(--destructive);
|
|
58
|
+
--color-border: var(--border);
|
|
59
|
+
--color-input: var(--input);
|
|
60
|
+
--color-ring: var(--ring);
|
|
61
|
+
--color-chart-1: var(--chart-1);
|
|
62
|
+
--color-chart-2: var(--chart-2);
|
|
63
|
+
--color-chart-3: var(--chart-3);
|
|
64
|
+
--color-chart-4: var(--chart-4);
|
|
65
|
+
--color-chart-5: var(--chart-5);
|
|
66
|
+
--color-sidebar: var(--sidebar);
|
|
67
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
68
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
69
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
70
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
71
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
72
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
73
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
74
|
+
--font-sans: var(--font-geist-sans);
|
|
75
|
+
--font-mono: var(--font-geist-mono);
|
|
76
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
77
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
78
|
+
--radius-lg: var(--radius);
|
|
79
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
80
|
+
--radius-2xl: calc(var(--radius) + 8px);
|
|
81
|
+
--radius-3xl: calc(var(--radius) + 12px);
|
|
82
|
+
--radius-4xl: calc(var(--radius) + 16px);
|
|
83
|
+
--spacing-header: 3rem;
|
|
84
|
+
}
|
|
85
|
+
`;
|