wealth-alpha-chat-widget 1.0.1 → 1.0.2
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/dist/index.cjs +184 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +103 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.cts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.mjs +185 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/docs/BACKEND_CHAT_WIDGET.md +0 -357
package/dist/index.cjs
CHANGED
|
@@ -284,6 +284,50 @@ async function sendMessage(apiBase, message, sessionId, context, signal) {
|
|
|
284
284
|
signal
|
|
285
285
|
});
|
|
286
286
|
}
|
|
287
|
+
async function uploadPortfolioCsv(apiBase, file, sessionId, signal) {
|
|
288
|
+
const url = joinUrl(apiBase, "/upload-portfolio-csv");
|
|
289
|
+
const requestId = generateRequestId();
|
|
290
|
+
const session = readSession();
|
|
291
|
+
const form = new FormData();
|
|
292
|
+
form.append("sessionId", sessionId);
|
|
293
|
+
form.append("file", file, file.name);
|
|
294
|
+
const controller = new AbortController();
|
|
295
|
+
const timeoutId = setTimeout(() => controller.abort(), 6e4);
|
|
296
|
+
if (signal) {
|
|
297
|
+
if (signal.aborted) {
|
|
298
|
+
clearTimeout(timeoutId);
|
|
299
|
+
throw new DOMException("Aborted", "AbortError");
|
|
300
|
+
}
|
|
301
|
+
signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
const headers = { "X-Request-Id": requestId };
|
|
305
|
+
if (session?.token) headers["Authorization"] = `Bearer ${session.token}`;
|
|
306
|
+
const res = await fetch(url, {
|
|
307
|
+
method: "POST",
|
|
308
|
+
headers,
|
|
309
|
+
body: form,
|
|
310
|
+
signal: controller.signal,
|
|
311
|
+
credentials: "same-origin"
|
|
312
|
+
});
|
|
313
|
+
if (res.status === 401 || res.status === 403) {
|
|
314
|
+
clearSession();
|
|
315
|
+
throw new AuthExpiredError(requestId);
|
|
316
|
+
}
|
|
317
|
+
if (!res.ok) {
|
|
318
|
+
let detail = res.statusText;
|
|
319
|
+
try {
|
|
320
|
+
const errBody = await res.json();
|
|
321
|
+
detail = errBody.message ?? errBody.detail ?? detail;
|
|
322
|
+
} catch {
|
|
323
|
+
}
|
|
324
|
+
throw new ApiError(detail || `HTTP ${res.status}`, res.status, requestId);
|
|
325
|
+
}
|
|
326
|
+
return await res.json();
|
|
327
|
+
} finally {
|
|
328
|
+
clearTimeout(timeoutId);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
287
331
|
async function logout(apiBase, signal) {
|
|
288
332
|
try {
|
|
289
333
|
await request(apiBase, "/auth/logout", {
|
|
@@ -430,6 +474,37 @@ function useChat(opts) {
|
|
|
430
474
|
const deactivatePriorChips = react.useCallback((keepId) => {
|
|
431
475
|
dispatch({ type: "DEACTIVATE_CHIPS", payload: { exceptId: keepId } });
|
|
432
476
|
}, []);
|
|
477
|
+
const uploadCsv = react.useCallback(
|
|
478
|
+
async (file) => {
|
|
479
|
+
if (!sessionId) return;
|
|
480
|
+
abortRef.current?.abort();
|
|
481
|
+
const controller = new AbortController();
|
|
482
|
+
abortRef.current = controller;
|
|
483
|
+
deactivatePriorChips();
|
|
484
|
+
appendUserMessage(`Uploaded ${file.name}`);
|
|
485
|
+
dispatch({ type: "SET_TYPING", payload: true });
|
|
486
|
+
dispatch({ type: "SET_STATUS", payload: "sending" });
|
|
487
|
+
dispatch({ type: "SET_ERROR", payload: null });
|
|
488
|
+
try {
|
|
489
|
+
const resp = await uploadPortfolioCsv(apiBase, file, sessionId, controller.signal);
|
|
490
|
+
appendBotResponse(resp);
|
|
491
|
+
dispatch({ type: "SET_STATUS", payload: "idle" });
|
|
492
|
+
} catch (err) {
|
|
493
|
+
const error = err;
|
|
494
|
+
if (error.name === "AbortError") return;
|
|
495
|
+
if (error instanceof AuthExpiredError) {
|
|
496
|
+
onAuthExpiredRef.current?.();
|
|
497
|
+
} else {
|
|
498
|
+
onErrorRef.current?.(error);
|
|
499
|
+
dispatch({ type: "SET_ERROR", payload: error.message });
|
|
500
|
+
}
|
|
501
|
+
dispatch({ type: "SET_STATUS", payload: "error" });
|
|
502
|
+
} finally {
|
|
503
|
+
dispatch({ type: "SET_TYPING", payload: false });
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
[apiBase, sessionId, appendUserMessage, appendBotResponse, deactivatePriorChips]
|
|
507
|
+
);
|
|
433
508
|
const sendText = react.useCallback(
|
|
434
509
|
async (text) => {
|
|
435
510
|
const trimmed = text.trim();
|
|
@@ -476,6 +551,7 @@ function useChat(opts) {
|
|
|
476
551
|
return {
|
|
477
552
|
state,
|
|
478
553
|
sendText,
|
|
554
|
+
uploadCsv,
|
|
479
555
|
appendBotResponse,
|
|
480
556
|
appendUserMessage,
|
|
481
557
|
deactivatePriorChips,
|
|
@@ -611,6 +687,8 @@ var chat_default = {
|
|
|
611
687
|
positionRight: "chat_positionRight",
|
|
612
688
|
positionLeft: "chat_positionLeft",
|
|
613
689
|
widget: "chat_widget",
|
|
690
|
+
popupBubbleLeft: "chat_popupBubbleLeft",
|
|
691
|
+
popupBubbleRight: "chat_popupBubbleRight",
|
|
614
692
|
header: "chat_header",
|
|
615
693
|
headerTitle: "chat_headerTitle",
|
|
616
694
|
headerMeta: "chat_headerMeta",
|
|
@@ -630,6 +708,7 @@ var chat_default = {
|
|
|
630
708
|
input: "chat_input",
|
|
631
709
|
inputBox: "chat_inputBox",
|
|
632
710
|
sendButton: "chat_sendButton",
|
|
711
|
+
hiddenFileInput: "chat_hiddenFileInput",
|
|
633
712
|
authGate: "chat_authGate",
|
|
634
713
|
authGateIcon: "chat_authGateIcon",
|
|
635
714
|
authGateTitle: "chat_authGateTitle",
|
|
@@ -637,7 +716,9 @@ var chat_default = {
|
|
|
637
716
|
authGateButton: "chat_authGateButton",
|
|
638
717
|
authGateDisclaimer: "chat_authGateDisclaimer",
|
|
639
718
|
authGateDisclaimerTitle: "chat_authGateDisclaimerTitle",
|
|
640
|
-
errorBanner: "chat_errorBanner"
|
|
719
|
+
errorBanner: "chat_errorBanner",
|
|
720
|
+
popupBubble: "chat_popupBubble",
|
|
721
|
+
popupDismiss: "chat_popupDismiss"
|
|
641
722
|
};
|
|
642
723
|
function AuthGate({ brandName, loginUrl, onLoginClick }) {
|
|
643
724
|
const handleClick = () => {
|
|
@@ -847,16 +928,24 @@ function ChatHeader({
|
|
|
847
928
|
}
|
|
848
929
|
function ChatInput({ disabled, placeholder, onSend }) {
|
|
849
930
|
const [value, setValue] = react.useState("");
|
|
931
|
+
const inputRef = react.useRef(null);
|
|
932
|
+
react.useEffect(() => {
|
|
933
|
+
if (!disabled) {
|
|
934
|
+
inputRef.current?.focus();
|
|
935
|
+
}
|
|
936
|
+
}, [disabled]);
|
|
850
937
|
const submit = () => {
|
|
851
938
|
const trimmed = value.trim();
|
|
852
939
|
if (!trimmed || disabled) return;
|
|
853
940
|
onSend(trimmed);
|
|
854
941
|
setValue("");
|
|
942
|
+
inputRef.current?.focus();
|
|
855
943
|
};
|
|
856
944
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chat_default.input, children: [
|
|
857
945
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
858
946
|
"input",
|
|
859
947
|
{
|
|
948
|
+
ref: inputRef,
|
|
860
949
|
type: "text",
|
|
861
950
|
className: chat_default.inputBox,
|
|
862
951
|
value,
|
|
@@ -884,25 +973,51 @@ function ChatInput({ disabled, placeholder, onSend }) {
|
|
|
884
973
|
)
|
|
885
974
|
] });
|
|
886
975
|
}
|
|
887
|
-
function FloatingButton({ position, onClick, brandColor }) {
|
|
976
|
+
function FloatingButton({ position, onClick, brandColor, showPopup, popupMessage, onPopupDismiss }) {
|
|
888
977
|
const posClass = position === "bottom-left" ? chat_default.positionLeft : chat_default.positionRight;
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
978
|
+
const popupPosClass = position === "bottom-left" ? chat_default.popupBubbleLeft : chat_default.popupBubbleRight;
|
|
979
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
980
|
+
showPopup && popupMessage ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
981
|
+
"div",
|
|
982
|
+
{
|
|
983
|
+
className: `${chat_default.popupBubble} ${popupPosClass}`,
|
|
984
|
+
role: "status",
|
|
985
|
+
"aria-live": "polite",
|
|
986
|
+
children: [
|
|
987
|
+
popupMessage,
|
|
988
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
989
|
+
"button",
|
|
990
|
+
{
|
|
991
|
+
type: "button",
|
|
992
|
+
className: chat_default.popupDismiss,
|
|
993
|
+
onClick: onPopupDismiss,
|
|
994
|
+
"aria-label": "Dismiss greeting",
|
|
995
|
+
children: "\u2715"
|
|
996
|
+
}
|
|
997
|
+
)
|
|
998
|
+
]
|
|
999
|
+
}
|
|
1000
|
+
) : null,
|
|
1001
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1002
|
+
"button",
|
|
1003
|
+
{
|
|
1004
|
+
type: "button",
|
|
1005
|
+
className: `${chat_default.floatingButton} ${posClass}`,
|
|
1006
|
+
onClick,
|
|
1007
|
+
"aria-label": "Open chat",
|
|
1008
|
+
style: brandColor ? { background: brandColor } : void 0,
|
|
1009
|
+
children: "\u{1F4AC}"
|
|
1010
|
+
}
|
|
1011
|
+
)
|
|
1012
|
+
] });
|
|
900
1013
|
}
|
|
901
1014
|
var DEFAULT_BRAND_NAME = "Wealth Alpha AI";
|
|
902
1015
|
var DEFAULT_BRAND_COLOR = "#1a2d5a";
|
|
903
1016
|
var DEFAULT_AUTH_CHECK = "/me";
|
|
1017
|
+
var DEFAULT_GREETING = "Hi! I'm your Wealth Alpha AI assistant. How can I help you?";
|
|
904
1018
|
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
1019
|
var CTA_LINE = "Select a quick command below \u2014 or type your query to ask our AI Research Analyst directly.";
|
|
1020
|
+
var PORTFOLIO_CSV_CHIP_ID = "__portfolio_csv_upload";
|
|
906
1021
|
function buildWelcome(name, plan) {
|
|
907
1022
|
const greeting = name ? `Welcome back, ${name}! You have ${plan ? plan : "Premium"} access.` : `Welcome! You have ${plan ? plan : "Premium"} access.`;
|
|
908
1023
|
return `${greeting}
|
|
@@ -923,6 +1038,7 @@ function WealthChat(props) {
|
|
|
923
1038
|
position = "bottom-right",
|
|
924
1039
|
defaultOpen = false,
|
|
925
1040
|
showCountdown = true,
|
|
1041
|
+
greetingMessage = DEFAULT_GREETING,
|
|
926
1042
|
onLogin,
|
|
927
1043
|
onLogout,
|
|
928
1044
|
onSessionExpire,
|
|
@@ -930,9 +1046,19 @@ function WealthChat(props) {
|
|
|
930
1046
|
} = props;
|
|
931
1047
|
const [mounted, setMounted] = react.useState(false);
|
|
932
1048
|
const [open, setOpen] = react.useState(defaultOpen);
|
|
1049
|
+
const [showPopup, setShowPopup] = react.useState(false);
|
|
1050
|
+
const popupShownRef = react.useRef(defaultOpen);
|
|
933
1051
|
react.useEffect(() => {
|
|
934
1052
|
setMounted(true);
|
|
935
1053
|
}, []);
|
|
1054
|
+
react.useEffect(() => {
|
|
1055
|
+
if (!mounted || open || popupShownRef.current) return;
|
|
1056
|
+
const timer = setTimeout(() => {
|
|
1057
|
+
setShowPopup(true);
|
|
1058
|
+
popupShownRef.current = true;
|
|
1059
|
+
}, 1e3);
|
|
1060
|
+
return () => clearTimeout(timer);
|
|
1061
|
+
}, [mounted, open]);
|
|
936
1062
|
const {
|
|
937
1063
|
session,
|
|
938
1064
|
remainingMs,
|
|
@@ -960,9 +1086,11 @@ function WealthChat(props) {
|
|
|
960
1086
|
},
|
|
961
1087
|
[session, setHistory]
|
|
962
1088
|
);
|
|
1089
|
+
const csvFileRef = react.useRef(null);
|
|
963
1090
|
const {
|
|
964
1091
|
state: chatState,
|
|
965
1092
|
sendText,
|
|
1093
|
+
uploadCsv,
|
|
966
1094
|
appendBotResponse,
|
|
967
1095
|
appendUserMessage,
|
|
968
1096
|
deactivatePriorChips,
|
|
@@ -1002,6 +1130,11 @@ function WealthChat(props) {
|
|
|
1002
1130
|
async (chip) => {
|
|
1003
1131
|
touch();
|
|
1004
1132
|
deactivatePriorChips();
|
|
1133
|
+
if (chip.id === PORTFOLIO_CSV_CHIP_ID) {
|
|
1134
|
+
appendUserMessage(chip.label);
|
|
1135
|
+
csvFileRef.current?.click();
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1005
1138
|
appendUserMessage(chip.label);
|
|
1006
1139
|
setTyping(true);
|
|
1007
1140
|
try {
|
|
@@ -1013,6 +1146,17 @@ function WealthChat(props) {
|
|
|
1013
1146
|
},
|
|
1014
1147
|
[touch, deactivatePriorChips, appendUserMessage, callChip, setTyping, appendBotResponse]
|
|
1015
1148
|
);
|
|
1149
|
+
const handleCsvFileSelected = react.useCallback(
|
|
1150
|
+
(e) => {
|
|
1151
|
+
const file = e.target.files?.[0];
|
|
1152
|
+
e.target.value = "";
|
|
1153
|
+
if (file) {
|
|
1154
|
+
touch();
|
|
1155
|
+
void uploadCsv(file);
|
|
1156
|
+
}
|
|
1157
|
+
},
|
|
1158
|
+
[touch, uploadCsv]
|
|
1159
|
+
);
|
|
1016
1160
|
const handleSend = react.useCallback(
|
|
1017
1161
|
(text) => {
|
|
1018
1162
|
touch();
|
|
@@ -1020,6 +1164,10 @@ function WealthChat(props) {
|
|
|
1020
1164
|
},
|
|
1021
1165
|
[touch, sendText]
|
|
1022
1166
|
);
|
|
1167
|
+
const handleFloatingButtonClick = react.useCallback(() => {
|
|
1168
|
+
setOpen(true);
|
|
1169
|
+
setShowPopup(false);
|
|
1170
|
+
}, []);
|
|
1023
1171
|
const handleClose = react.useCallback(() => setOpen(false), []);
|
|
1024
1172
|
const handleClear = react.useCallback(() => {
|
|
1025
1173
|
clearChat();
|
|
@@ -1036,7 +1184,17 @@ function WealthChat(props) {
|
|
|
1036
1184
|
["--wac-brand"]: brandColor
|
|
1037
1185
|
};
|
|
1038
1186
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chat_default.root, style: rootStyle, children: [
|
|
1039
|
-
!open ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1187
|
+
!open ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1188
|
+
FloatingButton,
|
|
1189
|
+
{
|
|
1190
|
+
position,
|
|
1191
|
+
onClick: handleFloatingButtonClick,
|
|
1192
|
+
brandColor,
|
|
1193
|
+
showPopup,
|
|
1194
|
+
popupMessage: greetingMessage,
|
|
1195
|
+
onPopupDismiss: () => setShowPopup(false)
|
|
1196
|
+
}
|
|
1197
|
+
) : null,
|
|
1040
1198
|
open ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1041
1199
|
"div",
|
|
1042
1200
|
{
|
|
@@ -1066,6 +1224,18 @@ function WealthChat(props) {
|
|
|
1066
1224
|
onLoginClick: handleLoginClick
|
|
1067
1225
|
}
|
|
1068
1226
|
),
|
|
1227
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1228
|
+
"input",
|
|
1229
|
+
{
|
|
1230
|
+
ref: csvFileRef,
|
|
1231
|
+
type: "file",
|
|
1232
|
+
accept: ".csv,text/csv",
|
|
1233
|
+
className: chat_default.hiddenFileInput,
|
|
1234
|
+
onChange: handleCsvFileSelected,
|
|
1235
|
+
"aria-hidden": true,
|
|
1236
|
+
tabIndex: -1
|
|
1237
|
+
}
|
|
1238
|
+
),
|
|
1069
1239
|
isLoggedIn ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
1070
1240
|
ChatInput,
|
|
1071
1241
|
{
|