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 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
- 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
- );
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(FloatingButton, { position, onClick: () => setOpen(true), brandColor }) : null,
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
  {