wealth-alpha-chat-widget 1.0.1 → 1.0.3

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",
@@ -621,15 +699,19 @@ var chat_default = {
621
699
  markdown: "chat_markdown",
622
700
  bubbleBot: "chat_bubbleBot",
623
701
  bubbleUser: "chat_bubbleUser",
702
+ bubbleMeta: "chat_bubbleMeta",
624
703
  chipRow: "chat_chipRow",
625
704
  chip: "chat_chip",
626
705
  chipActive: "chat_chipActive",
627
706
  chipDisabled: "chat_chipDisabled",
628
707
  typing: "chat_typing",
629
708
  typingDot: "chat_typingDot",
709
+ wacBlink: "chat_wacBlink",
630
710
  input: "chat_input",
631
711
  inputBox: "chat_inputBox",
632
712
  sendButton: "chat_sendButton",
713
+ hiddenFileInput: "chat_hiddenFileInput",
714
+ csvButton: "chat_csvButton",
633
715
  authGate: "chat_authGate",
634
716
  authGateIcon: "chat_authGateIcon",
635
717
  authGateTitle: "chat_authGateTitle",
@@ -637,7 +719,23 @@ var chat_default = {
637
719
  authGateButton: "chat_authGateButton",
638
720
  authGateDisclaimer: "chat_authGateDisclaimer",
639
721
  authGateDisclaimerTitle: "chat_authGateDisclaimerTitle",
640
- errorBanner: "chat_errorBanner"
722
+ errorBanner: "chat_errorBanner",
723
+ popupBubble: "chat_popupBubble",
724
+ wacPopIn: "chat_wacPopIn",
725
+ popupDismiss: "chat_popupDismiss",
726
+ menuGrid: "chat_menuGrid",
727
+ menuItem: "chat_menuItem",
728
+ menuItemIcon: "chat_menuItemIcon",
729
+ menuItemTitle: "chat_menuItemTitle",
730
+ menuItemSub: "chat_menuItemSub",
731
+ menuIconGreen: "chat_menuIconGreen",
732
+ menuIconBlue: "chat_menuIconBlue",
733
+ menuIconLeaf: "chat_menuIconLeaf",
734
+ menuIconRed: "chat_menuIconRed",
735
+ menuIconOrange: "chat_menuIconOrange",
736
+ menuIconGold: "chat_menuIconGold",
737
+ menuIconTeal: "chat_menuIconTeal",
738
+ menuIconPurple: "chat_menuIconPurple"
641
739
  };
642
740
  function AuthGate({ brandName, loginUrl, onLoginClick }) {
643
741
  const handleClick = () => {
@@ -709,9 +807,10 @@ var ALLOWED_TAGS = [
709
807
  "ol",
710
808
  "li",
711
809
  "blockquote",
712
- "span"
810
+ "span",
811
+ "div"
713
812
  ];
714
- var ALLOWED_ATTR = ["href", "target", "rel", "class"];
813
+ var ALLOWED_ATTR = ["href", "target", "rel", "class", "style", "data-wa-gauge-pct"];
715
814
  var SANITIZE_CFG = {
716
815
  ALLOWED_TAGS,
717
816
  ALLOWED_ATTR,
@@ -725,25 +824,87 @@ function inlineMarkdownToHtml(text) {
725
824
  function renderMarkdown(text) {
726
825
  if (!text) return "";
727
826
  const inlined = inlineMarkdownToHtml(text);
827
+ if (/<div[\s>]/i.test(inlined)) {
828
+ return DOMPurify__default.default.sanitize(inlined, SANITIZE_CFG);
829
+ }
728
830
  const paragraphs = inlined.split(/\n{2,}/).map((para) => para.replace(/\n/g, "<br>")).filter((p) => p.length > 0);
729
831
  const html = paragraphs.length > 1 ? paragraphs.map((p) => `<p>${p}</p>`).join("") : paragraphs[0] ?? "";
730
832
  return DOMPurify__default.default.sanitize(html, SANITIZE_CFG);
731
833
  }
834
+ var ROOT_MENU_CHIP_IDS = /* @__PURE__ */ new Set([
835
+ "stock_analysis",
836
+ "stock_discovery",
837
+ "new_listings",
838
+ "portfolio_risk",
839
+ "market_forecast",
840
+ "crypto",
841
+ "tradable_picks",
842
+ "peer_compare"
843
+ ]);
844
+ var MENU_SUBTITLES = {
845
+ stock_analysis: "Fundamental & technical",
846
+ stock_discovery: "Find promising stocks",
847
+ new_listings: "Track IPOs & new stocks",
848
+ portfolio_risk: "Analyze your portfolio",
849
+ market_forecast: "Outlook & key events",
850
+ crypto: "BTC, ETH & trends",
851
+ tradable_picks: "Short-term setups",
852
+ peer_compare: "Compare peer stocks"
853
+ };
854
+ var MENU_ICON_KEY = {
855
+ stock_analysis: "menuIconGreen",
856
+ stock_discovery: "menuIconBlue",
857
+ new_listings: "menuIconLeaf",
858
+ portfolio_risk: "menuIconRed",
859
+ market_forecast: "menuIconOrange",
860
+ crypto: "menuIconGold",
861
+ tradable_picks: "menuIconTeal",
862
+ peer_compare: "menuIconPurple"
863
+ };
864
+ function isRootMenuChips(chips) {
865
+ return chips.some((c) => ROOT_MENU_CHIP_IDS.has(c.id));
866
+ }
732
867
  function MessageBubble({ message, onChipClick }) {
733
868
  const isBot = message.role === "bot";
734
869
  const bubbleClass = isBot ? `${chat_default.bubble} ${chat_default.bubbleBot}` : `${chat_default.bubble} ${chat_default.bubbleUser}`;
870
+ const handleMarkdownClick = (e) => {
871
+ const anchor = e.target.closest("a");
872
+ if (!anchor?.href) return;
873
+ e.preventDefault();
874
+ window.location.assign(anchor.href);
875
+ };
876
+ const chips = message.chips ?? [];
877
+ const showMenuGrid = isBot && message.chipsActive && isRootMenuChips(chips);
735
878
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column" }, children: [
736
879
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: bubbleClass, children: isBot ? /* @__PURE__ */ jsxRuntime.jsx(
737
880
  "div",
738
881
  {
739
882
  className: chat_default.markdown,
740
- dangerouslySetInnerHTML: { __html: renderMarkdown(message.content) }
883
+ dangerouslySetInnerHTML: { __html: renderMarkdown(message.content) },
884
+ onClick: handleMarkdownClick
741
885
  }
742
886
  ) : message.content }),
743
- isBot && message.chips && message.chips.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
887
+ showMenuGrid ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: chat_default.menuGrid, children: chips.map((chip) => {
888
+ const iconKey = MENU_ICON_KEY[chip.id];
889
+ const iconClass = iconKey && chat_default[iconKey] || "";
890
+ return /* @__PURE__ */ jsxRuntime.jsxs(
891
+ "button",
892
+ {
893
+ type: "button",
894
+ className: chat_default.menuItem,
895
+ onClick: () => onChipClick(chip),
896
+ children: [
897
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: `${chat_default.menuItemIcon} ${iconClass}`, children: chip.icon }),
898
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: chat_default.menuItemTitle, children: chip.label }),
899
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: chat_default.menuItemSub, children: MENU_SUBTITLES[chip.id] ?? "" })
900
+ ]
901
+ },
902
+ chip.id
903
+ );
904
+ }) }) : isBot && chips.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
744
905
  ChipRow,
745
906
  {
746
- chips: message.chips,
907
+ chips,
747
908
  disabled: !message.chipsActive,
748
909
  onClick: onChipClick
749
910
  }
@@ -847,16 +1008,24 @@ function ChatHeader({
847
1008
  }
848
1009
  function ChatInput({ disabled, placeholder, onSend }) {
849
1010
  const [value, setValue] = react.useState("");
1011
+ const inputRef = react.useRef(null);
1012
+ react.useEffect(() => {
1013
+ if (!disabled) {
1014
+ inputRef.current?.focus();
1015
+ }
1016
+ }, [disabled]);
850
1017
  const submit = () => {
851
1018
  const trimmed = value.trim();
852
1019
  if (!trimmed || disabled) return;
853
1020
  onSend(trimmed);
854
1021
  setValue("");
1022
+ inputRef.current?.focus();
855
1023
  };
856
1024
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chat_default.input, children: [
857
1025
  /* @__PURE__ */ jsxRuntime.jsx(
858
1026
  "input",
859
1027
  {
1028
+ ref: inputRef,
860
1029
  type: "text",
861
1030
  className: chat_default.inputBox,
862
1031
  value,
@@ -884,31 +1053,62 @@ function ChatInput({ disabled, placeholder, onSend }) {
884
1053
  )
885
1054
  ] });
886
1055
  }
887
- function FloatingButton({ position, onClick, brandColor }) {
1056
+ function FloatingButton({ position, onClick, brandColor, showPopup, popupMessage, onPopupDismiss }) {
888
1057
  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
- );
1058
+ const popupPosClass = position === "bottom-left" ? chat_default.popupBubbleLeft : chat_default.popupBubbleRight;
1059
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1060
+ showPopup && popupMessage ? /* @__PURE__ */ jsxRuntime.jsxs(
1061
+ "div",
1062
+ {
1063
+ className: `${chat_default.popupBubble} ${popupPosClass}`,
1064
+ role: "status",
1065
+ "aria-live": "polite",
1066
+ children: [
1067
+ popupMessage,
1068
+ /* @__PURE__ */ jsxRuntime.jsx(
1069
+ "button",
1070
+ {
1071
+ type: "button",
1072
+ className: chat_default.popupDismiss,
1073
+ onClick: onPopupDismiss,
1074
+ "aria-label": "Dismiss greeting",
1075
+ children: "\u2715"
1076
+ }
1077
+ )
1078
+ ]
1079
+ }
1080
+ ) : null,
1081
+ /* @__PURE__ */ jsxRuntime.jsx(
1082
+ "button",
1083
+ {
1084
+ type: "button",
1085
+ className: `${chat_default.floatingButton} ${posClass}`,
1086
+ onClick,
1087
+ "aria-label": "Open chat",
1088
+ style: brandColor ? { background: brandColor } : void 0,
1089
+ children: "\u{1F4AC}"
1090
+ }
1091
+ )
1092
+ ] });
900
1093
  }
901
1094
  var DEFAULT_BRAND_NAME = "Wealth Alpha AI";
902
1095
  var DEFAULT_BRAND_COLOR = "#1a2d5a";
903
1096
  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.";
1097
+ var DEFAULT_GREETING = "Hi! I'm your Wealth Alpha AI assistant. How can I help you?";
1098
+ var CTA_LINE = "Select a quick command below";
1099
+ var PORTFOLIO_CSV_CHIP_ID = "__portfolio_csv_upload";
1100
+ var UPGRADE_PREMIUM_CHIP_ID = "__upgrade_premium__";
1101
+ function resolvePricingUrl(upgradeUrl) {
1102
+ if (upgradeUrl) return upgradeUrl;
1103
+ if (typeof window !== "undefined") {
1104
+ return `${window.location.origin}/pricing`;
1105
+ }
1106
+ return "";
1107
+ }
906
1108
  function buildWelcome(name, plan) {
907
1109
  const greeting = name ? `Welcome back, ${name}! You have ${plan ? plan : "Premium"} access.` : `Welcome! You have ${plan ? plan : "Premium"} access.`;
908
1110
  return `${greeting}
909
1111
 
910
- ${DISCLAIMER_BLOCK}
911
-
912
1112
  ${CTA_LINE}`;
913
1113
  }
914
1114
  function WealthChat(props) {
@@ -923,6 +1123,7 @@ function WealthChat(props) {
923
1123
  position = "bottom-right",
924
1124
  defaultOpen = false,
925
1125
  showCountdown = true,
1126
+ greetingMessage = DEFAULT_GREETING,
926
1127
  onLogin,
927
1128
  onLogout,
928
1129
  onSessionExpire,
@@ -930,9 +1131,19 @@ function WealthChat(props) {
930
1131
  } = props;
931
1132
  const [mounted, setMounted] = react.useState(false);
932
1133
  const [open, setOpen] = react.useState(defaultOpen);
1134
+ const [showPopup, setShowPopup] = react.useState(false);
1135
+ const popupShownRef = react.useRef(defaultOpen);
933
1136
  react.useEffect(() => {
934
1137
  setMounted(true);
935
1138
  }, []);
1139
+ react.useEffect(() => {
1140
+ if (!mounted || open || popupShownRef.current) return;
1141
+ const timer = setTimeout(() => {
1142
+ setShowPopup(true);
1143
+ popupShownRef.current = true;
1144
+ }, 1e3);
1145
+ return () => clearTimeout(timer);
1146
+ }, [mounted, open]);
936
1147
  const {
937
1148
  session,
938
1149
  remainingMs,
@@ -960,9 +1171,11 @@ function WealthChat(props) {
960
1171
  },
961
1172
  [session, setHistory]
962
1173
  );
1174
+ const csvFileRef = react.useRef(null);
963
1175
  const {
964
1176
  state: chatState,
965
1177
  sendText,
1178
+ uploadCsv,
966
1179
  appendBotResponse,
967
1180
  appendUserMessage,
968
1181
  deactivatePriorChips,
@@ -1002,16 +1215,46 @@ function WealthChat(props) {
1002
1215
  async (chip) => {
1003
1216
  touch();
1004
1217
  deactivatePriorChips();
1218
+ if (chip.id === PORTFOLIO_CSV_CHIP_ID) {
1219
+ appendUserMessage(chip.label);
1220
+ csvFileRef.current?.click();
1221
+ return;
1222
+ }
1223
+ if (chip.id === UPGRADE_PREMIUM_CHIP_ID) {
1224
+ appendUserMessage(chip.label);
1225
+ const pricingUrl = resolvePricingUrl(user?.upgradeUrl);
1226
+ if (pricingUrl && typeof window !== "undefined") {
1227
+ window.location.assign(pricingUrl);
1228
+ }
1229
+ return;
1230
+ }
1005
1231
  appendUserMessage(chip.label);
1006
1232
  setTyping(true);
1007
1233
  try {
1008
1234
  const resp = await callChip(chip);
1009
- if (resp) appendBotResponse(resp);
1235
+ if (!resp) return;
1236
+ const redirectUrl = resp.metadata?.redirectUrl;
1237
+ if (typeof redirectUrl === "string" && redirectUrl && typeof window !== "undefined") {
1238
+ window.location.assign(redirectUrl);
1239
+ return;
1240
+ }
1241
+ appendBotResponse(resp);
1010
1242
  } finally {
1011
1243
  setTyping(false);
1012
1244
  }
1013
1245
  },
1014
- [touch, deactivatePriorChips, appendUserMessage, callChip, setTyping, appendBotResponse]
1246
+ [touch, deactivatePriorChips, appendUserMessage, callChip, setTyping, appendBotResponse, user?.upgradeUrl]
1247
+ );
1248
+ const handleCsvFileSelected = react.useCallback(
1249
+ (e) => {
1250
+ const file = e.target.files?.[0];
1251
+ e.target.value = "";
1252
+ if (file) {
1253
+ touch();
1254
+ void uploadCsv(file);
1255
+ }
1256
+ },
1257
+ [touch, uploadCsv]
1015
1258
  );
1016
1259
  const handleSend = react.useCallback(
1017
1260
  (text) => {
@@ -1020,6 +1263,10 @@ function WealthChat(props) {
1020
1263
  },
1021
1264
  [touch, sendText]
1022
1265
  );
1266
+ const handleFloatingButtonClick = react.useCallback(() => {
1267
+ setOpen(true);
1268
+ setShowPopup(false);
1269
+ }, []);
1023
1270
  const handleClose = react.useCallback(() => setOpen(false), []);
1024
1271
  const handleClear = react.useCallback(() => {
1025
1272
  clearChat();
@@ -1036,7 +1283,17 @@ function WealthChat(props) {
1036
1283
  ["--wac-brand"]: brandColor
1037
1284
  };
1038
1285
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: chat_default.root, style: rootStyle, children: [
1039
- !open ? /* @__PURE__ */ jsxRuntime.jsx(FloatingButton, { position, onClick: () => setOpen(true), brandColor }) : null,
1286
+ !open ? /* @__PURE__ */ jsxRuntime.jsx(
1287
+ FloatingButton,
1288
+ {
1289
+ position,
1290
+ onClick: handleFloatingButtonClick,
1291
+ brandColor,
1292
+ showPopup,
1293
+ popupMessage: greetingMessage,
1294
+ onPopupDismiss: () => setShowPopup(false)
1295
+ }
1296
+ ) : null,
1040
1297
  open ? /* @__PURE__ */ jsxRuntime.jsxs(
1041
1298
  "div",
1042
1299
  {
@@ -1066,6 +1323,18 @@ function WealthChat(props) {
1066
1323
  onLoginClick: handleLoginClick
1067
1324
  }
1068
1325
  ),
1326
+ /* @__PURE__ */ jsxRuntime.jsx(
1327
+ "input",
1328
+ {
1329
+ ref: csvFileRef,
1330
+ type: "file",
1331
+ accept: ".csv,text/csv",
1332
+ className: chat_default.hiddenFileInput,
1333
+ onChange: handleCsvFileSelected,
1334
+ "aria-hidden": true,
1335
+ tabIndex: -1
1336
+ }
1337
+ ),
1069
1338
  isLoggedIn ? /* @__PURE__ */ jsxRuntime.jsx(
1070
1339
  ChatInput,
1071
1340
  {