wealth-alpha-chat-widget 1.0.1
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 +139 -0
- package/dist/index.cjs +1110 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +388 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +219 -0
- package/dist/index.d.ts +219 -0
- package/dist/index.mjs +1079 -0
- package/dist/index.mjs.map +1 -0
- package/docs/BACKEND_CHAT_WIDGET.md +357 -0
- package/docs/DEPLOY.md +283 -0
- package/docs/PUBLISH.md +202 -0
- package/docs/SETUP.md +180 -0
- package/package.json +77 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
var DOMPurify = require('isomorphic-dompurify');
|
|
7
|
+
|
|
8
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var DOMPurify__default = /*#__PURE__*/_interopDefault(DOMPurify);
|
|
11
|
+
|
|
12
|
+
// src/components/WealthChat.tsx
|
|
13
|
+
|
|
14
|
+
// src/utils/session.ts
|
|
15
|
+
var SESSION_KEY = "wac_session";
|
|
16
|
+
var DEFAULT_SESSION_TTL_SECONDS = 1800;
|
|
17
|
+
var MAX_HISTORY = 100;
|
|
18
|
+
var FALLBACK_TOKEN_KEYS = ["token", "access_token", "auth_token", "jwt"];
|
|
19
|
+
var isBrowser = () => typeof window !== "undefined" && typeof window.localStorage !== "undefined";
|
|
20
|
+
function base64UrlDecode(input) {
|
|
21
|
+
const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
22
|
+
const padded = base64 + "===".slice((base64.length + 3) % 4);
|
|
23
|
+
return atob(padded);
|
|
24
|
+
}
|
|
25
|
+
function decodeJwt(token) {
|
|
26
|
+
try {
|
|
27
|
+
const parts = token.split(".");
|
|
28
|
+
if (parts.length !== 3 || !parts[1]) return null;
|
|
29
|
+
return JSON.parse(base64UrlDecode(parts[1]));
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function jwtExpiresAt(token) {
|
|
35
|
+
const payload = decodeJwt(token);
|
|
36
|
+
const exp = payload?.["exp"];
|
|
37
|
+
if (typeof exp !== "number") return null;
|
|
38
|
+
return exp * 1e3;
|
|
39
|
+
}
|
|
40
|
+
function jwtUserId(token) {
|
|
41
|
+
const payload = decodeJwt(token);
|
|
42
|
+
if (!payload) return "";
|
|
43
|
+
const id = payload["id"] ?? payload["sub"] ?? payload["user_id"];
|
|
44
|
+
return id != null ? String(id) : "";
|
|
45
|
+
}
|
|
46
|
+
function readFallbackToken() {
|
|
47
|
+
if (!isBrowser()) return null;
|
|
48
|
+
for (const key of FALLBACK_TOKEN_KEYS) {
|
|
49
|
+
const value = window.localStorage.getItem(key);
|
|
50
|
+
if (value && value.length > 20) return { token: value, key };
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
var generateId = (prefix) => {
|
|
55
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
56
|
+
return `${prefix}_${crypto.randomUUID()}`;
|
|
57
|
+
}
|
|
58
|
+
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
59
|
+
};
|
|
60
|
+
function newSessionId() {
|
|
61
|
+
return generateId("ses");
|
|
62
|
+
}
|
|
63
|
+
function newMessageId() {
|
|
64
|
+
return generateId("msg");
|
|
65
|
+
}
|
|
66
|
+
function readSession() {
|
|
67
|
+
if (!isBrowser()) return null;
|
|
68
|
+
try {
|
|
69
|
+
const raw = window.localStorage.getItem(SESSION_KEY);
|
|
70
|
+
if (raw) {
|
|
71
|
+
const session = JSON.parse(raw);
|
|
72
|
+
const valid = session && typeof session.token === "string" && session.token.length > 0 && typeof session.expiresAt === "number" && Date.now() < session.expiresAt;
|
|
73
|
+
if (valid) {
|
|
74
|
+
const fallback2 = readFallbackToken();
|
|
75
|
+
if (!fallback2 && session.bootSource === "fallback") {
|
|
76
|
+
clearSession();
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (fallback2 && fallback2.token !== session.token) {
|
|
80
|
+
return adoptFallbackToken(fallback2.token, session);
|
|
81
|
+
}
|
|
82
|
+
return session;
|
|
83
|
+
}
|
|
84
|
+
clearSession();
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
clearSession();
|
|
88
|
+
}
|
|
89
|
+
const fallback = readFallbackToken();
|
|
90
|
+
if (!fallback) return null;
|
|
91
|
+
return adoptFallbackToken(fallback.token, null);
|
|
92
|
+
}
|
|
93
|
+
function adoptFallbackToken(token, prior) {
|
|
94
|
+
const jwtExp = jwtExpiresAt(token);
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
const expiresAt = jwtExp ?? now + DEFAULT_SESSION_TTL_SECONDS * 1e3;
|
|
97
|
+
if (now >= expiresAt) return null;
|
|
98
|
+
const session = {
|
|
99
|
+
token,
|
|
100
|
+
userId: jwtUserId(token) || prior?.userId || "",
|
|
101
|
+
sessionId: prior?.sessionId ?? newSessionId(),
|
|
102
|
+
expiresAt,
|
|
103
|
+
lastActive: now,
|
|
104
|
+
history: prior?.history ?? [],
|
|
105
|
+
bootSource: "fallback"
|
|
106
|
+
};
|
|
107
|
+
if (isBrowser()) {
|
|
108
|
+
try {
|
|
109
|
+
window.localStorage.setItem(SESSION_KEY, JSON.stringify(session));
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return session;
|
|
114
|
+
}
|
|
115
|
+
function writeSession(data, ttlSeconds = DEFAULT_SESSION_TTL_SECONDS) {
|
|
116
|
+
if (!isBrowser()) return null;
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
const existing = readSession();
|
|
119
|
+
const base = existing ?? {
|
|
120
|
+
token: "",
|
|
121
|
+
userId: "",
|
|
122
|
+
sessionId: newSessionId(),
|
|
123
|
+
expiresAt: now + ttlSeconds * 1e3,
|
|
124
|
+
lastActive: now,
|
|
125
|
+
history: []
|
|
126
|
+
};
|
|
127
|
+
const merged = {
|
|
128
|
+
...base,
|
|
129
|
+
...data,
|
|
130
|
+
sessionId: data.sessionId ?? base.sessionId ?? newSessionId(),
|
|
131
|
+
lastActive: now,
|
|
132
|
+
expiresAt: now + ttlSeconds * 1e3,
|
|
133
|
+
history: trimHistory(data.history ?? base.history)
|
|
134
|
+
};
|
|
135
|
+
try {
|
|
136
|
+
window.localStorage.setItem(SESSION_KEY, JSON.stringify(merged));
|
|
137
|
+
return merged;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function clearSession() {
|
|
143
|
+
if (!isBrowser()) return;
|
|
144
|
+
try {
|
|
145
|
+
window.localStorage.removeItem(SESSION_KEY);
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function trimHistory(history) {
|
|
150
|
+
if (!Array.isArray(history)) return [];
|
|
151
|
+
if (history.length <= MAX_HISTORY) return history;
|
|
152
|
+
return history.slice(history.length - MAX_HISTORY);
|
|
153
|
+
}
|
|
154
|
+
function touchSession(ttlSeconds = DEFAULT_SESSION_TTL_SECONDS) {
|
|
155
|
+
const current = readSession();
|
|
156
|
+
if (!current) return null;
|
|
157
|
+
return writeSession({}, ttlSeconds);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/api/chatApi.ts
|
|
161
|
+
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
162
|
+
var DEFAULT_RETRIES = 2;
|
|
163
|
+
var RETRY_BASE_DELAY_MS = 400;
|
|
164
|
+
var ApiError = class extends Error {
|
|
165
|
+
constructor(message, status, requestId) {
|
|
166
|
+
super(message);
|
|
167
|
+
this.name = "ApiError";
|
|
168
|
+
this.status = status;
|
|
169
|
+
this.requestId = requestId;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var AuthExpiredError = class extends ApiError {
|
|
173
|
+
constructor(requestId) {
|
|
174
|
+
super("Session expired or invalid", 401, requestId);
|
|
175
|
+
this.name = "AuthExpiredError";
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
var generateRequestId = () => {
|
|
179
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
180
|
+
return crypto.randomUUID();
|
|
181
|
+
}
|
|
182
|
+
return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
183
|
+
};
|
|
184
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
185
|
+
var joinUrl = (base, path) => {
|
|
186
|
+
const trimmedBase = base.replace(/\/+$/, "");
|
|
187
|
+
const trimmedPath = path.startsWith("/") ? path : `/${path}`;
|
|
188
|
+
return `${trimmedBase}${trimmedPath}`;
|
|
189
|
+
};
|
|
190
|
+
async function request(apiBase, path, opts = {}) {
|
|
191
|
+
const {
|
|
192
|
+
method = "GET",
|
|
193
|
+
body,
|
|
194
|
+
signal,
|
|
195
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
196
|
+
retries = DEFAULT_RETRIES,
|
|
197
|
+
auth = true
|
|
198
|
+
} = opts;
|
|
199
|
+
const url = /^https?:\/\//i.test(path) ? path : joinUrl(apiBase, path);
|
|
200
|
+
const requestId = generateRequestId();
|
|
201
|
+
let lastError = null;
|
|
202
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
203
|
+
const controller = new AbortController();
|
|
204
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
205
|
+
const onUserAbort = () => controller.abort();
|
|
206
|
+
if (signal) {
|
|
207
|
+
if (signal.aborted) {
|
|
208
|
+
clearTimeout(timeoutId);
|
|
209
|
+
throw new DOMException("Aborted", "AbortError");
|
|
210
|
+
}
|
|
211
|
+
signal.addEventListener("abort", onUserAbort, { once: true });
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const headers = {
|
|
215
|
+
"Content-Type": "application/json",
|
|
216
|
+
"X-Request-Id": requestId
|
|
217
|
+
};
|
|
218
|
+
if (auth) {
|
|
219
|
+
const session = readSession();
|
|
220
|
+
if (session?.token) headers["Authorization"] = `Bearer ${session.token}`;
|
|
221
|
+
}
|
|
222
|
+
const res = await fetch(url, {
|
|
223
|
+
method,
|
|
224
|
+
headers,
|
|
225
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
226
|
+
signal: controller.signal,
|
|
227
|
+
credentials: "same-origin"
|
|
228
|
+
});
|
|
229
|
+
if (res.status === 401 || res.status === 403) {
|
|
230
|
+
clearSession();
|
|
231
|
+
throw new AuthExpiredError(requestId);
|
|
232
|
+
}
|
|
233
|
+
if (res.status >= 500 && attempt < retries) {
|
|
234
|
+
lastError = new ApiError(`Server error ${res.status}`, res.status, requestId);
|
|
235
|
+
throw lastError;
|
|
236
|
+
}
|
|
237
|
+
if (!res.ok) {
|
|
238
|
+
let detail = res.statusText;
|
|
239
|
+
try {
|
|
240
|
+
const errBody = await res.json();
|
|
241
|
+
detail = errBody.message ?? errBody.detail ?? detail;
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
throw new ApiError(detail || `HTTP ${res.status}`, res.status, requestId);
|
|
245
|
+
}
|
|
246
|
+
if (res.status === 204) return void 0;
|
|
247
|
+
return await res.json();
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const error = err;
|
|
250
|
+
const isAbort = error.name === "AbortError";
|
|
251
|
+
const isAuth = error instanceof AuthExpiredError;
|
|
252
|
+
const isRetryable = !isAuth && !signal?.aborted && (isAbort || error instanceof TypeError || error.status >= 500);
|
|
253
|
+
if (isAuth || !isRetryable || attempt === retries) {
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
lastError = error;
|
|
257
|
+
await sleep(RETRY_BASE_DELAY_MS * Math.pow(2, attempt));
|
|
258
|
+
} finally {
|
|
259
|
+
clearTimeout(timeoutId);
|
|
260
|
+
if (signal) signal.removeEventListener("abort", onUserAbort);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
throw lastError ?? new Error("Request failed");
|
|
264
|
+
}
|
|
265
|
+
async function checkAuth(apiBase, authCheck, signal) {
|
|
266
|
+
return request(apiBase, authCheck, {
|
|
267
|
+
method: "GET",
|
|
268
|
+
signal,
|
|
269
|
+
retries: 1
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async function sendChip(apiBase, chip, sessionId, signal) {
|
|
273
|
+
const path = chip.apiPath ?? `/chip/${chip.id}`;
|
|
274
|
+
return request(apiBase, path, {
|
|
275
|
+
method: "POST",
|
|
276
|
+
body: { chipId: chip.id, sessionId },
|
|
277
|
+
signal
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
async function sendMessage(apiBase, message, sessionId, context, signal) {
|
|
281
|
+
return request(apiBase, "/message", {
|
|
282
|
+
method: "POST",
|
|
283
|
+
body: { message, sessionId, context },
|
|
284
|
+
signal
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
async function logout(apiBase, signal) {
|
|
288
|
+
try {
|
|
289
|
+
await request(apiBase, "/auth/logout", {
|
|
290
|
+
method: "POST",
|
|
291
|
+
signal,
|
|
292
|
+
retries: 0
|
|
293
|
+
});
|
|
294
|
+
} finally {
|
|
295
|
+
clearSession();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/hooks/useAuth.ts
|
|
300
|
+
function useAuth(opts) {
|
|
301
|
+
const { apiBase, authCheck, enabled = true, onError } = opts;
|
|
302
|
+
const [user, setUser] = react.useState(null);
|
|
303
|
+
const [loading, setLoading] = react.useState(true);
|
|
304
|
+
const [error, setError] = react.useState(null);
|
|
305
|
+
const [tick, setTick] = react.useState(0);
|
|
306
|
+
const onErrorRef = react.useRef(onError);
|
|
307
|
+
react.useEffect(() => {
|
|
308
|
+
onErrorRef.current = onError;
|
|
309
|
+
}, [onError]);
|
|
310
|
+
react.useEffect(() => {
|
|
311
|
+
if (!enabled) {
|
|
312
|
+
setLoading(false);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const session = readSession();
|
|
316
|
+
if (!session?.token) {
|
|
317
|
+
setUser(null);
|
|
318
|
+
setLoading(false);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const controller = new AbortController();
|
|
322
|
+
setLoading(true);
|
|
323
|
+
setError(null);
|
|
324
|
+
checkAuth(apiBase, authCheck, controller.signal).then((u) => {
|
|
325
|
+
setUser(u);
|
|
326
|
+
setError(null);
|
|
327
|
+
}).catch((err) => {
|
|
328
|
+
if (err.name === "AbortError") return;
|
|
329
|
+
if (err instanceof AuthExpiredError) {
|
|
330
|
+
clearSession();
|
|
331
|
+
setUser(null);
|
|
332
|
+
} else {
|
|
333
|
+
setError(err);
|
|
334
|
+
onErrorRef.current?.(err);
|
|
335
|
+
}
|
|
336
|
+
}).finally(() => {
|
|
337
|
+
if (!controller.signal.aborted) setLoading(false);
|
|
338
|
+
});
|
|
339
|
+
return () => controller.abort();
|
|
340
|
+
}, [apiBase, authCheck, enabled, tick]);
|
|
341
|
+
const refresh = react.useCallback(() => setTick((t) => t + 1), []);
|
|
342
|
+
return {
|
|
343
|
+
isLoggedIn: user !== null,
|
|
344
|
+
user,
|
|
345
|
+
loading,
|
|
346
|
+
error,
|
|
347
|
+
refresh
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
var initialState = {
|
|
351
|
+
messages: [],
|
|
352
|
+
isTyping: false,
|
|
353
|
+
status: "idle",
|
|
354
|
+
error: null
|
|
355
|
+
};
|
|
356
|
+
function reducer(state, action) {
|
|
357
|
+
switch (action.type) {
|
|
358
|
+
case "ADD_MESSAGE": {
|
|
359
|
+
const next = [...state.messages, action.payload];
|
|
360
|
+
const trimmed = next.length > MAX_HISTORY ? next.slice(next.length - MAX_HISTORY) : next;
|
|
361
|
+
return { ...state, messages: trimmed };
|
|
362
|
+
}
|
|
363
|
+
case "SET_TYPING":
|
|
364
|
+
return { ...state, isTyping: action.payload };
|
|
365
|
+
case "SET_STATUS":
|
|
366
|
+
return { ...state, status: action.payload };
|
|
367
|
+
case "SET_ERROR":
|
|
368
|
+
return { ...state, error: action.payload };
|
|
369
|
+
case "DEACTIVATE_CHIPS":
|
|
370
|
+
return {
|
|
371
|
+
...state,
|
|
372
|
+
messages: state.messages.map(
|
|
373
|
+
(m) => m.id === action.payload.exceptId ? m : { ...m, chipsActive: false }
|
|
374
|
+
)
|
|
375
|
+
};
|
|
376
|
+
case "LOAD_HISTORY":
|
|
377
|
+
return { ...state, messages: action.payload };
|
|
378
|
+
case "CLEAR":
|
|
379
|
+
return initialState;
|
|
380
|
+
default:
|
|
381
|
+
return state;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function useChat(opts) {
|
|
385
|
+
const { apiBase, sessionId, initialMessages, onHistoryChange, onError, onAuthExpired } = opts;
|
|
386
|
+
const [state, dispatch] = react.useReducer(reducer, initialState);
|
|
387
|
+
const abortRef = react.useRef(null);
|
|
388
|
+
const onHistoryChangeRef = react.useRef(onHistoryChange);
|
|
389
|
+
const onErrorRef = react.useRef(onError);
|
|
390
|
+
const onAuthExpiredRef = react.useRef(onAuthExpired);
|
|
391
|
+
react.useEffect(() => {
|
|
392
|
+
onHistoryChangeRef.current = onHistoryChange;
|
|
393
|
+
onErrorRef.current = onError;
|
|
394
|
+
onAuthExpiredRef.current = onAuthExpired;
|
|
395
|
+
}, [onHistoryChange, onError, onAuthExpired]);
|
|
396
|
+
react.useEffect(() => {
|
|
397
|
+
if (initialMessages && initialMessages.length > 0) {
|
|
398
|
+
dispatch({ type: "LOAD_HISTORY", payload: initialMessages });
|
|
399
|
+
}
|
|
400
|
+
}, []);
|
|
401
|
+
react.useEffect(() => {
|
|
402
|
+
onHistoryChangeRef.current?.(state.messages);
|
|
403
|
+
}, [state.messages]);
|
|
404
|
+
react.useEffect(() => {
|
|
405
|
+
return () => {
|
|
406
|
+
abortRef.current?.abort();
|
|
407
|
+
};
|
|
408
|
+
}, []);
|
|
409
|
+
const appendUserMessage = react.useCallback((content) => {
|
|
410
|
+
const msg = {
|
|
411
|
+
id: newMessageId(),
|
|
412
|
+
role: "user",
|
|
413
|
+
content,
|
|
414
|
+
timestamp: Date.now()
|
|
415
|
+
};
|
|
416
|
+
dispatch({ type: "ADD_MESSAGE", payload: msg });
|
|
417
|
+
return msg;
|
|
418
|
+
}, []);
|
|
419
|
+
const appendBotResponse = react.useCallback((resp) => {
|
|
420
|
+
const botMsg = {
|
|
421
|
+
id: newMessageId(),
|
|
422
|
+
role: "bot",
|
|
423
|
+
content: resp.message,
|
|
424
|
+
chips: resp.chips ?? [],
|
|
425
|
+
chipsActive: (resp.chips ?? []).length > 0,
|
|
426
|
+
timestamp: Date.now()
|
|
427
|
+
};
|
|
428
|
+
dispatch({ type: "ADD_MESSAGE", payload: botMsg });
|
|
429
|
+
}, []);
|
|
430
|
+
const deactivatePriorChips = react.useCallback((keepId) => {
|
|
431
|
+
dispatch({ type: "DEACTIVATE_CHIPS", payload: { exceptId: keepId } });
|
|
432
|
+
}, []);
|
|
433
|
+
const sendText = react.useCallback(
|
|
434
|
+
async (text) => {
|
|
435
|
+
const trimmed = text.trim();
|
|
436
|
+
if (!trimmed || !sessionId) return;
|
|
437
|
+
abortRef.current?.abort();
|
|
438
|
+
const controller = new AbortController();
|
|
439
|
+
abortRef.current = controller;
|
|
440
|
+
deactivatePriorChips();
|
|
441
|
+
appendUserMessage(trimmed);
|
|
442
|
+
dispatch({ type: "SET_TYPING", payload: true });
|
|
443
|
+
dispatch({ type: "SET_STATUS", payload: "sending" });
|
|
444
|
+
dispatch({ type: "SET_ERROR", payload: null });
|
|
445
|
+
try {
|
|
446
|
+
const context = state.messages.slice(-20);
|
|
447
|
+
const resp = await sendMessage(apiBase, trimmed, sessionId, context, controller.signal);
|
|
448
|
+
appendBotResponse(resp);
|
|
449
|
+
dispatch({ type: "SET_STATUS", payload: "idle" });
|
|
450
|
+
} catch (err) {
|
|
451
|
+
const error = err;
|
|
452
|
+
if (error.name === "AbortError") return;
|
|
453
|
+
if (error instanceof AuthExpiredError) {
|
|
454
|
+
onAuthExpiredRef.current?.();
|
|
455
|
+
} else {
|
|
456
|
+
onErrorRef.current?.(error);
|
|
457
|
+
dispatch({ type: "SET_ERROR", payload: error.message });
|
|
458
|
+
}
|
|
459
|
+
dispatch({ type: "SET_STATUS", payload: "error" });
|
|
460
|
+
} finally {
|
|
461
|
+
dispatch({ type: "SET_TYPING", payload: false });
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
[apiBase, sessionId, state.messages, appendUserMessage, appendBotResponse, deactivatePriorChips]
|
|
465
|
+
);
|
|
466
|
+
const clear = react.useCallback(() => {
|
|
467
|
+
abortRef.current?.abort();
|
|
468
|
+
dispatch({ type: "CLEAR" });
|
|
469
|
+
}, []);
|
|
470
|
+
const loadHistory = react.useCallback((history) => {
|
|
471
|
+
dispatch({ type: "LOAD_HISTORY", payload: history });
|
|
472
|
+
}, []);
|
|
473
|
+
const setTyping = react.useCallback((value) => {
|
|
474
|
+
dispatch({ type: "SET_TYPING", payload: value });
|
|
475
|
+
}, []);
|
|
476
|
+
return {
|
|
477
|
+
state,
|
|
478
|
+
sendText,
|
|
479
|
+
appendBotResponse,
|
|
480
|
+
appendUserMessage,
|
|
481
|
+
deactivatePriorChips,
|
|
482
|
+
clear,
|
|
483
|
+
loadHistory,
|
|
484
|
+
setTyping
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function useChip(opts) {
|
|
488
|
+
const { apiBase, sessionId, onAuthExpired, onError } = opts;
|
|
489
|
+
const abortRef = react.useRef(null);
|
|
490
|
+
const onAuthExpiredRef = react.useRef(onAuthExpired);
|
|
491
|
+
const onErrorRef = react.useRef(onError);
|
|
492
|
+
react.useEffect(() => {
|
|
493
|
+
onAuthExpiredRef.current = onAuthExpired;
|
|
494
|
+
onErrorRef.current = onError;
|
|
495
|
+
}, [onAuthExpired, onError]);
|
|
496
|
+
react.useEffect(() => {
|
|
497
|
+
return () => abortRef.current?.abort();
|
|
498
|
+
}, []);
|
|
499
|
+
const callChip = react.useCallback(
|
|
500
|
+
async (chip) => {
|
|
501
|
+
if (!sessionId) return null;
|
|
502
|
+
abortRef.current?.abort();
|
|
503
|
+
const controller = new AbortController();
|
|
504
|
+
abortRef.current = controller;
|
|
505
|
+
try {
|
|
506
|
+
return await sendChip(apiBase, chip, sessionId, controller.signal);
|
|
507
|
+
} catch (err) {
|
|
508
|
+
const error = err;
|
|
509
|
+
if (error.name === "AbortError") return null;
|
|
510
|
+
if (error instanceof AuthExpiredError) {
|
|
511
|
+
onAuthExpiredRef.current?.();
|
|
512
|
+
} else {
|
|
513
|
+
onErrorRef.current?.(error);
|
|
514
|
+
}
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
[apiBase, sessionId]
|
|
519
|
+
);
|
|
520
|
+
return { callChip };
|
|
521
|
+
}
|
|
522
|
+
function useSession(opts = {}) {
|
|
523
|
+
const { ttlSeconds = DEFAULT_SESSION_TTL_SECONDS, onExpire } = opts;
|
|
524
|
+
const [session, setSession] = react.useState(null);
|
|
525
|
+
const [remainingMs, setRemainingMs] = react.useState(0);
|
|
526
|
+
const expiredRef = react.useRef(false);
|
|
527
|
+
const onExpireRef = react.useRef(onExpire);
|
|
528
|
+
react.useEffect(() => {
|
|
529
|
+
onExpireRef.current = onExpire;
|
|
530
|
+
}, [onExpire]);
|
|
531
|
+
react.useEffect(() => {
|
|
532
|
+
const current = readSession();
|
|
533
|
+
setSession(current);
|
|
534
|
+
setRemainingMs(current ? current.expiresAt - Date.now() : 0);
|
|
535
|
+
}, []);
|
|
536
|
+
react.useEffect(() => {
|
|
537
|
+
if (!session) return;
|
|
538
|
+
const interval = window.setInterval(() => {
|
|
539
|
+
const current = readSession();
|
|
540
|
+
if (!current) {
|
|
541
|
+
if (!expiredRef.current) {
|
|
542
|
+
expiredRef.current = true;
|
|
543
|
+
onExpireRef.current?.();
|
|
544
|
+
}
|
|
545
|
+
setSession(null);
|
|
546
|
+
setRemainingMs(0);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
setRemainingMs(current.expiresAt - Date.now());
|
|
550
|
+
}, 15e3);
|
|
551
|
+
return () => window.clearInterval(interval);
|
|
552
|
+
}, [session]);
|
|
553
|
+
react.useEffect(() => {
|
|
554
|
+
if (typeof window === "undefined") return;
|
|
555
|
+
const onStorage = (e) => {
|
|
556
|
+
if (e.key && e.key !== "wac_session") return;
|
|
557
|
+
const current = readSession();
|
|
558
|
+
setSession(current);
|
|
559
|
+
setRemainingMs(current ? current.expiresAt - Date.now() : 0);
|
|
560
|
+
};
|
|
561
|
+
window.addEventListener("storage", onStorage);
|
|
562
|
+
return () => window.removeEventListener("storage", onStorage);
|
|
563
|
+
}, []);
|
|
564
|
+
const setToken = react.useCallback(
|
|
565
|
+
(token, userId) => {
|
|
566
|
+
expiredRef.current = false;
|
|
567
|
+
const updated = writeSession(
|
|
568
|
+
{
|
|
569
|
+
token,
|
|
570
|
+
userId,
|
|
571
|
+
sessionId: readSession()?.sessionId ?? newSessionId(),
|
|
572
|
+
history: readSession()?.history ?? []
|
|
573
|
+
},
|
|
574
|
+
ttlSeconds
|
|
575
|
+
);
|
|
576
|
+
setSession(updated);
|
|
577
|
+
setRemainingMs(updated ? updated.expiresAt - Date.now() : 0);
|
|
578
|
+
},
|
|
579
|
+
[ttlSeconds]
|
|
580
|
+
);
|
|
581
|
+
const setHistory = react.useCallback(
|
|
582
|
+
(history) => {
|
|
583
|
+
const updated = writeSession({ history }, ttlSeconds);
|
|
584
|
+
if (updated) {
|
|
585
|
+
setSession(updated);
|
|
586
|
+
setRemainingMs(updated.expiresAt - Date.now());
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
[ttlSeconds]
|
|
590
|
+
);
|
|
591
|
+
const touch = react.useCallback(() => {
|
|
592
|
+
const updated = touchSession(ttlSeconds);
|
|
593
|
+
if (updated) {
|
|
594
|
+
setSession(updated);
|
|
595
|
+
setRemainingMs(updated.expiresAt - Date.now());
|
|
596
|
+
}
|
|
597
|
+
}, [ttlSeconds]);
|
|
598
|
+
const clear = react.useCallback(() => {
|
|
599
|
+
clearSession();
|
|
600
|
+
setSession(null);
|
|
601
|
+
setRemainingMs(0);
|
|
602
|
+
expiredRef.current = true;
|
|
603
|
+
}, []);
|
|
604
|
+
return { session, remainingMs, setToken, setHistory, touch, clear };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/styles/chat.module.css
|
|
608
|
+
var chat_default = {
|
|
609
|
+
root: "chat_root",
|
|
610
|
+
floatingButton: "chat_floatingButton",
|
|
611
|
+
positionRight: "chat_positionRight",
|
|
612
|
+
positionLeft: "chat_positionLeft",
|
|
613
|
+
widget: "chat_widget",
|
|
614
|
+
header: "chat_header",
|
|
615
|
+
headerTitle: "chat_headerTitle",
|
|
616
|
+
headerMeta: "chat_headerMeta",
|
|
617
|
+
headerActions: "chat_headerActions",
|
|
618
|
+
iconButton: "chat_iconButton",
|
|
619
|
+
body: "chat_body",
|
|
620
|
+
bubble: "chat_bubble",
|
|
621
|
+
markdown: "chat_markdown",
|
|
622
|
+
bubbleBot: "chat_bubbleBot",
|
|
623
|
+
bubbleUser: "chat_bubbleUser",
|
|
624
|
+
chipRow: "chat_chipRow",
|
|
625
|
+
chip: "chat_chip",
|
|
626
|
+
chipActive: "chat_chipActive",
|
|
627
|
+
chipDisabled: "chat_chipDisabled",
|
|
628
|
+
typing: "chat_typing",
|
|
629
|
+
typingDot: "chat_typingDot",
|
|
630
|
+
input: "chat_input",
|
|
631
|
+
inputBox: "chat_inputBox",
|
|
632
|
+
sendButton: "chat_sendButton",
|
|
633
|
+
authGate: "chat_authGate",
|
|
634
|
+
authGateIcon: "chat_authGateIcon",
|
|
635
|
+
authGateTitle: "chat_authGateTitle",
|
|
636
|
+
authGateText: "chat_authGateText",
|
|
637
|
+
authGateButton: "chat_authGateButton",
|
|
638
|
+
authGateDisclaimer: "chat_authGateDisclaimer",
|
|
639
|
+
authGateDisclaimerTitle: "chat_authGateDisclaimerTitle",
|
|
640
|
+
errorBanner: "chat_errorBanner"
|
|
641
|
+
};
|
|
642
|
+
function AuthGate({ brandName, loginUrl, onLoginClick }) {
|
|
643
|
+
const handleClick = () => {
|
|
644
|
+
if (onLoginClick) {
|
|
645
|
+
onLoginClick();
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (typeof window !== "undefined") {
|
|
649
|
+
window.location.href = loginUrl;
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chat_default.authGate, children: [
|
|
653
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: chat_default.authGateIcon, "aria-hidden": "true", children: "\u{1F512}" }),
|
|
654
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: chat_default.authGateTitle, children: "Login required" }),
|
|
655
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: chat_default.authGateText, children: [
|
|
656
|
+
"To access ",
|
|
657
|
+
brandName,
|
|
658
|
+
", please sign in first."
|
|
659
|
+
] }),
|
|
660
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: chat_default.authGateButton, onClick: handleClick, children: "Sign in / Register \u2192" }),
|
|
661
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: chat_default.authGateDisclaimer, children: [
|
|
662
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: chat_default.authGateDisclaimerTitle, children: "DISCLAIMER" }),
|
|
663
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: "\u2022 AI-assisted analysis for educational purposes only." }),
|
|
664
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: "\u2022 Not financial advice. Markets involve risk." }),
|
|
665
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { children: "\u2022 Consult a SEBI-registered advisor before investing." })
|
|
666
|
+
] })
|
|
667
|
+
] });
|
|
668
|
+
}
|
|
669
|
+
function Chip({ chip, disabled, active, onClick }) {
|
|
670
|
+
const classes = [
|
|
671
|
+
chat_default.chip,
|
|
672
|
+
active ? chat_default.chipActive : "",
|
|
673
|
+
disabled ? chat_default.chipDisabled : ""
|
|
674
|
+
].filter(Boolean).join(" ");
|
|
675
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
676
|
+
"button",
|
|
677
|
+
{
|
|
678
|
+
type: "button",
|
|
679
|
+
className: classes,
|
|
680
|
+
disabled,
|
|
681
|
+
onClick: () => !disabled && onClick(chip),
|
|
682
|
+
children: [
|
|
683
|
+
chip.icon ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: chip.icon }) : null,
|
|
684
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: chip.label })
|
|
685
|
+
]
|
|
686
|
+
}
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
function ChipRow({ chips, disabled, onClick }) {
|
|
690
|
+
if (!chips || chips.length === 0) return null;
|
|
691
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: chat_default.chipRow, children: chips.map((c) => /* @__PURE__ */ jsxRuntime.jsx(Chip, { chip: c, disabled, onClick }, c.id)) });
|
|
692
|
+
}
|
|
693
|
+
var ALLOWED_TAGS = [
|
|
694
|
+
"b",
|
|
695
|
+
"strong",
|
|
696
|
+
"i",
|
|
697
|
+
"em",
|
|
698
|
+
"u",
|
|
699
|
+
"ins",
|
|
700
|
+
"s",
|
|
701
|
+
"strike",
|
|
702
|
+
"del",
|
|
703
|
+
"br",
|
|
704
|
+
"p",
|
|
705
|
+
"code",
|
|
706
|
+
"pre",
|
|
707
|
+
"a",
|
|
708
|
+
"ul",
|
|
709
|
+
"ol",
|
|
710
|
+
"li",
|
|
711
|
+
"blockquote",
|
|
712
|
+
"span"
|
|
713
|
+
];
|
|
714
|
+
var ALLOWED_ATTR = ["href", "target", "rel", "class"];
|
|
715
|
+
var SANITIZE_CFG = {
|
|
716
|
+
ALLOWED_TAGS,
|
|
717
|
+
ALLOWED_ATTR,
|
|
718
|
+
// Block javascript: / data: hrefs by stripping URI schemes other than http(s) / mailto
|
|
719
|
+
ALLOW_UNKNOWN_PROTOCOLS: false,
|
|
720
|
+
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i
|
|
721
|
+
};
|
|
722
|
+
function inlineMarkdownToHtml(text) {
|
|
723
|
+
return text.replace(/^[ \t]*#{1,6}[ \t]+(.+?)[ \t]*$/gm, "<b>$1</b>").replace(/\*\*([^\n<>]+?)\*\*/g, "<b>$1</b>").replace(/__([^\n<>]+?)__/g, "<b>$1</b>").replace(/(?<!\*)\*([^\n*<>]+?)\*(?!\*)/g, "<b>$1</b>").replace(/(?<![_a-zA-Z0-9])_([^\n_<>]+?)_(?![_a-zA-Z0-9])/g, "<i>$1</i>");
|
|
724
|
+
}
|
|
725
|
+
function renderMarkdown(text) {
|
|
726
|
+
if (!text) return "";
|
|
727
|
+
const inlined = inlineMarkdownToHtml(text);
|
|
728
|
+
const paragraphs = inlined.split(/\n{2,}/).map((para) => para.replace(/\n/g, "<br>")).filter((p) => p.length > 0);
|
|
729
|
+
const html = paragraphs.length > 1 ? paragraphs.map((p) => `<p>${p}</p>`).join("") : paragraphs[0] ?? "";
|
|
730
|
+
return DOMPurify__default.default.sanitize(html, SANITIZE_CFG);
|
|
731
|
+
}
|
|
732
|
+
function MessageBubble({ message, onChipClick }) {
|
|
733
|
+
const isBot = message.role === "bot";
|
|
734
|
+
const bubbleClass = isBot ? `${chat_default.bubble} ${chat_default.bubbleBot}` : `${chat_default.bubble} ${chat_default.bubbleUser}`;
|
|
735
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column" }, children: [
|
|
736
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: bubbleClass, children: isBot ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
737
|
+
"div",
|
|
738
|
+
{
|
|
739
|
+
className: chat_default.markdown,
|
|
740
|
+
dangerouslySetInnerHTML: { __html: renderMarkdown(message.content) }
|
|
741
|
+
}
|
|
742
|
+
) : message.content }),
|
|
743
|
+
isBot && message.chips && message.chips.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
744
|
+
ChipRow,
|
|
745
|
+
{
|
|
746
|
+
chips: message.chips,
|
|
747
|
+
disabled: !message.chipsActive,
|
|
748
|
+
onClick: onChipClick
|
|
749
|
+
}
|
|
750
|
+
) : null
|
|
751
|
+
] });
|
|
752
|
+
}
|
|
753
|
+
function TypingIndicator() {
|
|
754
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chat_default.typing, "aria-live": "polite", "aria-label": "Assistant is typing", children: [
|
|
755
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: chat_default.typingDot }),
|
|
756
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: chat_default.typingDot }),
|
|
757
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: chat_default.typingDot })
|
|
758
|
+
] });
|
|
759
|
+
}
|
|
760
|
+
function ChatBody({
|
|
761
|
+
isLoggedIn,
|
|
762
|
+
loading,
|
|
763
|
+
messages,
|
|
764
|
+
isTyping,
|
|
765
|
+
brandName,
|
|
766
|
+
loginUrl,
|
|
767
|
+
error,
|
|
768
|
+
onChipClick,
|
|
769
|
+
onLoginClick
|
|
770
|
+
}) {
|
|
771
|
+
const endRef = react.useRef(null);
|
|
772
|
+
react.useEffect(() => {
|
|
773
|
+
endRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
774
|
+
}, [messages.length, isTyping]);
|
|
775
|
+
if (loading) {
|
|
776
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: chat_default.body, children: /* @__PURE__ */ jsxRuntime.jsx(TypingIndicator, {}) });
|
|
777
|
+
}
|
|
778
|
+
if (!isLoggedIn) {
|
|
779
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: chat_default.body, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
780
|
+
AuthGate,
|
|
781
|
+
{
|
|
782
|
+
brandName,
|
|
783
|
+
loginUrl,
|
|
784
|
+
onLoginClick
|
|
785
|
+
}
|
|
786
|
+
) });
|
|
787
|
+
}
|
|
788
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chat_default.body, children: [
|
|
789
|
+
messages.map((m) => /* @__PURE__ */ jsxRuntime.jsx(MessageBubble, { message: m, onChipClick }, m.id)),
|
|
790
|
+
isTyping ? /* @__PURE__ */ jsxRuntime.jsx(TypingIndicator, {}) : null,
|
|
791
|
+
error ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: chat_default.errorBanner, children: error }) : null,
|
|
792
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { ref: endRef })
|
|
793
|
+
] });
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/utils/time.ts
|
|
797
|
+
function formatCountdown(remainingMs) {
|
|
798
|
+
if (remainingMs <= 0) return "expired";
|
|
799
|
+
const totalSeconds = Math.floor(remainingMs / 1e3);
|
|
800
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
801
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
802
|
+
const seconds = totalSeconds % 60;
|
|
803
|
+
if (hours > 0) return `${hours}h ${minutes}m left`;
|
|
804
|
+
if (minutes > 0) return `${minutes}m ${seconds}s left`;
|
|
805
|
+
return `${seconds}s left`;
|
|
806
|
+
}
|
|
807
|
+
function ChatHeader({
|
|
808
|
+
brandName,
|
|
809
|
+
remainingMs,
|
|
810
|
+
showCountdown,
|
|
811
|
+
onClose,
|
|
812
|
+
onClear
|
|
813
|
+
}) {
|
|
814
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chat_default.header, children: [
|
|
815
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
816
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: chat_default.headerTitle, children: brandName }),
|
|
817
|
+
showCountdown && remainingMs > 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chat_default.headerMeta, children: [
|
|
818
|
+
"Session: ",
|
|
819
|
+
formatCountdown(remainingMs)
|
|
820
|
+
] }) : null
|
|
821
|
+
] }),
|
|
822
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: chat_default.headerActions, children: [
|
|
823
|
+
onClear ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
824
|
+
"button",
|
|
825
|
+
{
|
|
826
|
+
type: "button",
|
|
827
|
+
className: chat_default.iconButton,
|
|
828
|
+
onClick: onClear,
|
|
829
|
+
"aria-label": "Clear conversation",
|
|
830
|
+
title: "Clear conversation",
|
|
831
|
+
children: "\u21BA"
|
|
832
|
+
}
|
|
833
|
+
) : null,
|
|
834
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
835
|
+
"button",
|
|
836
|
+
{
|
|
837
|
+
type: "button",
|
|
838
|
+
className: chat_default.iconButton,
|
|
839
|
+
onClick: onClose,
|
|
840
|
+
"aria-label": "Close chat",
|
|
841
|
+
title: "Close",
|
|
842
|
+
children: "\u2715"
|
|
843
|
+
}
|
|
844
|
+
)
|
|
845
|
+
] })
|
|
846
|
+
] });
|
|
847
|
+
}
|
|
848
|
+
function ChatInput({ disabled, placeholder, onSend }) {
|
|
849
|
+
const [value, setValue] = react.useState("");
|
|
850
|
+
const submit = () => {
|
|
851
|
+
const trimmed = value.trim();
|
|
852
|
+
if (!trimmed || disabled) return;
|
|
853
|
+
onSend(trimmed);
|
|
854
|
+
setValue("");
|
|
855
|
+
};
|
|
856
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chat_default.input, children: [
|
|
857
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
858
|
+
"input",
|
|
859
|
+
{
|
|
860
|
+
type: "text",
|
|
861
|
+
className: chat_default.inputBox,
|
|
862
|
+
value,
|
|
863
|
+
onChange: (e) => setValue(e.target.value),
|
|
864
|
+
onKeyDown: (e) => {
|
|
865
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
866
|
+
e.preventDefault();
|
|
867
|
+
submit();
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
disabled,
|
|
871
|
+
placeholder: placeholder ?? "Type a message\u2026",
|
|
872
|
+
"aria-label": "Message input"
|
|
873
|
+
}
|
|
874
|
+
),
|
|
875
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
876
|
+
"button",
|
|
877
|
+
{
|
|
878
|
+
type: "button",
|
|
879
|
+
className: chat_default.sendButton,
|
|
880
|
+
onClick: submit,
|
|
881
|
+
disabled: disabled || value.trim().length === 0,
|
|
882
|
+
children: "Send"
|
|
883
|
+
}
|
|
884
|
+
)
|
|
885
|
+
] });
|
|
886
|
+
}
|
|
887
|
+
function FloatingButton({ position, onClick, brandColor }) {
|
|
888
|
+
const posClass = position === "bottom-left" ? chat_default.positionLeft : chat_default.positionRight;
|
|
889
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
890
|
+
"button",
|
|
891
|
+
{
|
|
892
|
+
type: "button",
|
|
893
|
+
className: `${chat_default.floatingButton} ${posClass}`,
|
|
894
|
+
onClick,
|
|
895
|
+
"aria-label": "Open chat",
|
|
896
|
+
style: brandColor ? { background: brandColor } : void 0,
|
|
897
|
+
children: "\u{1F4AC}"
|
|
898
|
+
}
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
var DEFAULT_BRAND_NAME = "Wealth Alpha AI";
|
|
902
|
+
var DEFAULT_BRAND_COLOR = "#1a2d5a";
|
|
903
|
+
var DEFAULT_AUTH_CHECK = "/me";
|
|
904
|
+
var DISCLAIMER_BLOCK = "DISCLAIMER:\n\u2022 AI-assisted analysis for educational purposes only.\n\u2022 Not financial advice. Markets involve risk.\n\u2022 Consult a SEBI-registered advisor before investing.";
|
|
905
|
+
var CTA_LINE = "Select a quick command below \u2014 or type your query to ask our AI Research Analyst directly.";
|
|
906
|
+
function buildWelcome(name, plan) {
|
|
907
|
+
const greeting = name ? `Welcome back, ${name}! You have ${plan ? plan : "Premium"} access.` : `Welcome! You have ${plan ? plan : "Premium"} access.`;
|
|
908
|
+
return `${greeting}
|
|
909
|
+
|
|
910
|
+
${DISCLAIMER_BLOCK}
|
|
911
|
+
|
|
912
|
+
${CTA_LINE}`;
|
|
913
|
+
}
|
|
914
|
+
function WealthChat(props) {
|
|
915
|
+
const {
|
|
916
|
+
apiBase,
|
|
917
|
+
authCheck = DEFAULT_AUTH_CHECK,
|
|
918
|
+
loginUrl,
|
|
919
|
+
sessionTTL = DEFAULT_SESSION_TTL_SECONDS,
|
|
920
|
+
welcomeMessage,
|
|
921
|
+
brandName = DEFAULT_BRAND_NAME,
|
|
922
|
+
brandColor = DEFAULT_BRAND_COLOR,
|
|
923
|
+
position = "bottom-right",
|
|
924
|
+
defaultOpen = false,
|
|
925
|
+
showCountdown = true,
|
|
926
|
+
onLogin,
|
|
927
|
+
onLogout,
|
|
928
|
+
onSessionExpire,
|
|
929
|
+
onError
|
|
930
|
+
} = props;
|
|
931
|
+
const [mounted, setMounted] = react.useState(false);
|
|
932
|
+
const [open, setOpen] = react.useState(defaultOpen);
|
|
933
|
+
react.useEffect(() => {
|
|
934
|
+
setMounted(true);
|
|
935
|
+
}, []);
|
|
936
|
+
const {
|
|
937
|
+
session,
|
|
938
|
+
remainingMs,
|
|
939
|
+
setHistory,
|
|
940
|
+
touch,
|
|
941
|
+
clear: clearSessionState
|
|
942
|
+
} = useSession({
|
|
943
|
+
ttlSeconds: sessionTTL,
|
|
944
|
+
onExpire: onSessionExpire
|
|
945
|
+
});
|
|
946
|
+
const { isLoggedIn, user, loading: authLoading } = useAuth({
|
|
947
|
+
apiBase,
|
|
948
|
+
authCheck,
|
|
949
|
+
enabled: mounted && open,
|
|
950
|
+
onError
|
|
951
|
+
});
|
|
952
|
+
const handleAuthExpired = react.useCallback(() => {
|
|
953
|
+
clearSessionState();
|
|
954
|
+
onSessionExpire?.();
|
|
955
|
+
}, [clearSessionState, onSessionExpire]);
|
|
956
|
+
const handleHistoryChange = react.useCallback(
|
|
957
|
+
(history) => {
|
|
958
|
+
if (!session) return;
|
|
959
|
+
setHistory(history);
|
|
960
|
+
},
|
|
961
|
+
[session, setHistory]
|
|
962
|
+
);
|
|
963
|
+
const {
|
|
964
|
+
state: chatState,
|
|
965
|
+
sendText,
|
|
966
|
+
appendBotResponse,
|
|
967
|
+
appendUserMessage,
|
|
968
|
+
deactivatePriorChips,
|
|
969
|
+
setTyping,
|
|
970
|
+
loadHistory,
|
|
971
|
+
clear: clearChat
|
|
972
|
+
} = useChat({
|
|
973
|
+
apiBase,
|
|
974
|
+
sessionId: session?.sessionId ?? null,
|
|
975
|
+
initialMessages: session?.history,
|
|
976
|
+
onHistoryChange: handleHistoryChange,
|
|
977
|
+
onAuthExpired: handleAuthExpired,
|
|
978
|
+
onError
|
|
979
|
+
});
|
|
980
|
+
const { callChip } = useChip({
|
|
981
|
+
apiBase,
|
|
982
|
+
sessionId: session?.sessionId ?? null,
|
|
983
|
+
onAuthExpired: handleAuthExpired,
|
|
984
|
+
onError
|
|
985
|
+
});
|
|
986
|
+
const welcomeShownRef = react.useMemo(() => ({ shown: false }), []);
|
|
987
|
+
react.useEffect(() => {
|
|
988
|
+
if (!isLoggedIn || !open) return;
|
|
989
|
+
if (chatState.messages.length > 0) return;
|
|
990
|
+
if (welcomeShownRef.shown) return;
|
|
991
|
+
welcomeShownRef.shown = true;
|
|
992
|
+
const messageText = welcomeMessage ?? user?.welcomeMessage ?? buildWelcome(user?.name, user?.plan);
|
|
993
|
+
appendBotResponse({
|
|
994
|
+
message: messageText,
|
|
995
|
+
chips: user?.rootChips ?? [],
|
|
996
|
+
sessionId: session?.sessionId ?? "",
|
|
997
|
+
endOfFlow: false
|
|
998
|
+
});
|
|
999
|
+
onLogin?.();
|
|
1000
|
+
}, [isLoggedIn, open, chatState.messages.length, welcomeMessage, session, user, appendBotResponse, onLogin, welcomeShownRef]);
|
|
1001
|
+
const handleChipClick = react.useCallback(
|
|
1002
|
+
async (chip) => {
|
|
1003
|
+
touch();
|
|
1004
|
+
deactivatePriorChips();
|
|
1005
|
+
appendUserMessage(chip.label);
|
|
1006
|
+
setTyping(true);
|
|
1007
|
+
try {
|
|
1008
|
+
const resp = await callChip(chip);
|
|
1009
|
+
if (resp) appendBotResponse(resp);
|
|
1010
|
+
} finally {
|
|
1011
|
+
setTyping(false);
|
|
1012
|
+
}
|
|
1013
|
+
},
|
|
1014
|
+
[touch, deactivatePriorChips, appendUserMessage, callChip, setTyping, appendBotResponse]
|
|
1015
|
+
);
|
|
1016
|
+
const handleSend = react.useCallback(
|
|
1017
|
+
(text) => {
|
|
1018
|
+
touch();
|
|
1019
|
+
void sendText(text);
|
|
1020
|
+
},
|
|
1021
|
+
[touch, sendText]
|
|
1022
|
+
);
|
|
1023
|
+
const handleClose = react.useCallback(() => setOpen(false), []);
|
|
1024
|
+
const handleClear = react.useCallback(() => {
|
|
1025
|
+
clearChat();
|
|
1026
|
+
welcomeShownRef.shown = false;
|
|
1027
|
+
setHistory([]);
|
|
1028
|
+
}, [clearChat, setHistory, welcomeShownRef]);
|
|
1029
|
+
const handleLoginClick = react.useCallback(() => {
|
|
1030
|
+
if (typeof window !== "undefined") {
|
|
1031
|
+
window.location.href = loginUrl;
|
|
1032
|
+
}
|
|
1033
|
+
}, [loginUrl]);
|
|
1034
|
+
if (!mounted) return null;
|
|
1035
|
+
const rootStyle = {
|
|
1036
|
+
["--wac-brand"]: brandColor
|
|
1037
|
+
};
|
|
1038
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chat_default.root, style: rootStyle, children: [
|
|
1039
|
+
!open ? /* @__PURE__ */ jsxRuntime.jsx(FloatingButton, { position, onClick: () => setOpen(true), brandColor }) : null,
|
|
1040
|
+
open ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1041
|
+
"div",
|
|
1042
|
+
{
|
|
1043
|
+
className: `${chat_default.widget} ${position === "bottom-left" ? chat_default.positionLeft : chat_default.positionRight}`,
|
|
1044
|
+
children: [
|
|
1045
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1046
|
+
ChatHeader,
|
|
1047
|
+
{
|
|
1048
|
+
brandName,
|
|
1049
|
+
remainingMs,
|
|
1050
|
+
showCountdown: showCountdown && isLoggedIn,
|
|
1051
|
+
onClose: handleClose,
|
|
1052
|
+
onClear: isLoggedIn ? handleClear : void 0
|
|
1053
|
+
}
|
|
1054
|
+
),
|
|
1055
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1056
|
+
ChatBody,
|
|
1057
|
+
{
|
|
1058
|
+
isLoggedIn,
|
|
1059
|
+
loading: authLoading,
|
|
1060
|
+
messages: chatState.messages,
|
|
1061
|
+
isTyping: chatState.isTyping,
|
|
1062
|
+
brandName,
|
|
1063
|
+
loginUrl,
|
|
1064
|
+
error: chatState.error,
|
|
1065
|
+
onChipClick: handleChipClick,
|
|
1066
|
+
onLoginClick: handleLoginClick
|
|
1067
|
+
}
|
|
1068
|
+
),
|
|
1069
|
+
isLoggedIn ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1070
|
+
ChatInput,
|
|
1071
|
+
{
|
|
1072
|
+
disabled: chatState.isTyping,
|
|
1073
|
+
onSend: handleSend,
|
|
1074
|
+
placeholder: "Type a message\u2026"
|
|
1075
|
+
}
|
|
1076
|
+
) : null
|
|
1077
|
+
]
|
|
1078
|
+
}
|
|
1079
|
+
) : null
|
|
1080
|
+
] });
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
exports.ApiError = ApiError;
|
|
1084
|
+
exports.AuthExpiredError = AuthExpiredError;
|
|
1085
|
+
exports.AuthGate = AuthGate;
|
|
1086
|
+
exports.ChatBody = ChatBody;
|
|
1087
|
+
exports.ChatHeader = ChatHeader;
|
|
1088
|
+
exports.ChatInput = ChatInput;
|
|
1089
|
+
exports.Chip = Chip;
|
|
1090
|
+
exports.ChipRow = ChipRow;
|
|
1091
|
+
exports.DEFAULT_SESSION_TTL_SECONDS = DEFAULT_SESSION_TTL_SECONDS;
|
|
1092
|
+
exports.FloatingButton = FloatingButton;
|
|
1093
|
+
exports.MessageBubble = MessageBubble;
|
|
1094
|
+
exports.SESSION_KEY = SESSION_KEY;
|
|
1095
|
+
exports.TypingIndicator = TypingIndicator;
|
|
1096
|
+
exports.WealthChat = WealthChat;
|
|
1097
|
+
exports.checkAuth = checkAuth;
|
|
1098
|
+
exports.clearSession = clearSession;
|
|
1099
|
+
exports.logout = logout;
|
|
1100
|
+
exports.readSession = readSession;
|
|
1101
|
+
exports.sendChip = sendChip;
|
|
1102
|
+
exports.sendMessage = sendMessage;
|
|
1103
|
+
exports.touchSession = touchSession;
|
|
1104
|
+
exports.useAuth = useAuth;
|
|
1105
|
+
exports.useChat = useChat;
|
|
1106
|
+
exports.useChip = useChip;
|
|
1107
|
+
exports.useSession = useSession;
|
|
1108
|
+
exports.writeSession = writeSession;
|
|
1109
|
+
//# sourceMappingURL=index.cjs.map
|
|
1110
|
+
//# sourceMappingURL=index.cjs.map
|