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.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { useState, useRef, useEffect, useCallback, useReducer, useMemo } from 'react';
3
- import { jsxs, jsx } from 'react/jsx-runtime';
3
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
  import DOMPurify from 'isomorphic-dompurify';
5
5
 
6
6
  // src/components/WealthChat.tsx
@@ -278,6 +278,50 @@ async function sendMessage(apiBase, message, sessionId, context, signal) {
278
278
  signal
279
279
  });
280
280
  }
281
+ async function uploadPortfolioCsv(apiBase, file, sessionId, signal) {
282
+ const url = joinUrl(apiBase, "/upload-portfolio-csv");
283
+ const requestId = generateRequestId();
284
+ const session = readSession();
285
+ const form = new FormData();
286
+ form.append("sessionId", sessionId);
287
+ form.append("file", file, file.name);
288
+ const controller = new AbortController();
289
+ const timeoutId = setTimeout(() => controller.abort(), 6e4);
290
+ if (signal) {
291
+ if (signal.aborted) {
292
+ clearTimeout(timeoutId);
293
+ throw new DOMException("Aborted", "AbortError");
294
+ }
295
+ signal.addEventListener("abort", () => controller.abort(), { once: true });
296
+ }
297
+ try {
298
+ const headers = { "X-Request-Id": requestId };
299
+ if (session?.token) headers["Authorization"] = `Bearer ${session.token}`;
300
+ const res = await fetch(url, {
301
+ method: "POST",
302
+ headers,
303
+ body: form,
304
+ signal: controller.signal,
305
+ credentials: "same-origin"
306
+ });
307
+ if (res.status === 401 || res.status === 403) {
308
+ clearSession();
309
+ throw new AuthExpiredError(requestId);
310
+ }
311
+ if (!res.ok) {
312
+ let detail = res.statusText;
313
+ try {
314
+ const errBody = await res.json();
315
+ detail = errBody.message ?? errBody.detail ?? detail;
316
+ } catch {
317
+ }
318
+ throw new ApiError(detail || `HTTP ${res.status}`, res.status, requestId);
319
+ }
320
+ return await res.json();
321
+ } finally {
322
+ clearTimeout(timeoutId);
323
+ }
324
+ }
281
325
  async function logout(apiBase, signal) {
282
326
  try {
283
327
  await request(apiBase, "/auth/logout", {
@@ -424,6 +468,37 @@ function useChat(opts) {
424
468
  const deactivatePriorChips = useCallback((keepId) => {
425
469
  dispatch({ type: "DEACTIVATE_CHIPS", payload: { exceptId: keepId } });
426
470
  }, []);
471
+ const uploadCsv = useCallback(
472
+ async (file) => {
473
+ if (!sessionId) return;
474
+ abortRef.current?.abort();
475
+ const controller = new AbortController();
476
+ abortRef.current = controller;
477
+ deactivatePriorChips();
478
+ appendUserMessage(`Uploaded ${file.name}`);
479
+ dispatch({ type: "SET_TYPING", payload: true });
480
+ dispatch({ type: "SET_STATUS", payload: "sending" });
481
+ dispatch({ type: "SET_ERROR", payload: null });
482
+ try {
483
+ const resp = await uploadPortfolioCsv(apiBase, file, sessionId, controller.signal);
484
+ appendBotResponse(resp);
485
+ dispatch({ type: "SET_STATUS", payload: "idle" });
486
+ } catch (err) {
487
+ const error = err;
488
+ if (error.name === "AbortError") return;
489
+ if (error instanceof AuthExpiredError) {
490
+ onAuthExpiredRef.current?.();
491
+ } else {
492
+ onErrorRef.current?.(error);
493
+ dispatch({ type: "SET_ERROR", payload: error.message });
494
+ }
495
+ dispatch({ type: "SET_STATUS", payload: "error" });
496
+ } finally {
497
+ dispatch({ type: "SET_TYPING", payload: false });
498
+ }
499
+ },
500
+ [apiBase, sessionId, appendUserMessage, appendBotResponse, deactivatePriorChips]
501
+ );
427
502
  const sendText = useCallback(
428
503
  async (text) => {
429
504
  const trimmed = text.trim();
@@ -470,6 +545,7 @@ function useChat(opts) {
470
545
  return {
471
546
  state,
472
547
  sendText,
548
+ uploadCsv,
473
549
  appendBotResponse,
474
550
  appendUserMessage,
475
551
  deactivatePriorChips,
@@ -605,6 +681,8 @@ var chat_default = {
605
681
  positionRight: "chat_positionRight",
606
682
  positionLeft: "chat_positionLeft",
607
683
  widget: "chat_widget",
684
+ popupBubbleLeft: "chat_popupBubbleLeft",
685
+ popupBubbleRight: "chat_popupBubbleRight",
608
686
  header: "chat_header",
609
687
  headerTitle: "chat_headerTitle",
610
688
  headerMeta: "chat_headerMeta",
@@ -615,15 +693,19 @@ var chat_default = {
615
693
  markdown: "chat_markdown",
616
694
  bubbleBot: "chat_bubbleBot",
617
695
  bubbleUser: "chat_bubbleUser",
696
+ bubbleMeta: "chat_bubbleMeta",
618
697
  chipRow: "chat_chipRow",
619
698
  chip: "chat_chip",
620
699
  chipActive: "chat_chipActive",
621
700
  chipDisabled: "chat_chipDisabled",
622
701
  typing: "chat_typing",
623
702
  typingDot: "chat_typingDot",
703
+ wacBlink: "chat_wacBlink",
624
704
  input: "chat_input",
625
705
  inputBox: "chat_inputBox",
626
706
  sendButton: "chat_sendButton",
707
+ hiddenFileInput: "chat_hiddenFileInput",
708
+ csvButton: "chat_csvButton",
627
709
  authGate: "chat_authGate",
628
710
  authGateIcon: "chat_authGateIcon",
629
711
  authGateTitle: "chat_authGateTitle",
@@ -631,7 +713,23 @@ var chat_default = {
631
713
  authGateButton: "chat_authGateButton",
632
714
  authGateDisclaimer: "chat_authGateDisclaimer",
633
715
  authGateDisclaimerTitle: "chat_authGateDisclaimerTitle",
634
- errorBanner: "chat_errorBanner"
716
+ errorBanner: "chat_errorBanner",
717
+ popupBubble: "chat_popupBubble",
718
+ wacPopIn: "chat_wacPopIn",
719
+ popupDismiss: "chat_popupDismiss",
720
+ menuGrid: "chat_menuGrid",
721
+ menuItem: "chat_menuItem",
722
+ menuItemIcon: "chat_menuItemIcon",
723
+ menuItemTitle: "chat_menuItemTitle",
724
+ menuItemSub: "chat_menuItemSub",
725
+ menuIconGreen: "chat_menuIconGreen",
726
+ menuIconBlue: "chat_menuIconBlue",
727
+ menuIconLeaf: "chat_menuIconLeaf",
728
+ menuIconRed: "chat_menuIconRed",
729
+ menuIconOrange: "chat_menuIconOrange",
730
+ menuIconGold: "chat_menuIconGold",
731
+ menuIconTeal: "chat_menuIconTeal",
732
+ menuIconPurple: "chat_menuIconPurple"
635
733
  };
636
734
  function AuthGate({ brandName, loginUrl, onLoginClick }) {
637
735
  const handleClick = () => {
@@ -703,9 +801,10 @@ var ALLOWED_TAGS = [
703
801
  "ol",
704
802
  "li",
705
803
  "blockquote",
706
- "span"
804
+ "span",
805
+ "div"
707
806
  ];
708
- var ALLOWED_ATTR = ["href", "target", "rel", "class"];
807
+ var ALLOWED_ATTR = ["href", "target", "rel", "class", "style", "data-wa-gauge-pct"];
709
808
  var SANITIZE_CFG = {
710
809
  ALLOWED_TAGS,
711
810
  ALLOWED_ATTR,
@@ -719,25 +818,87 @@ function inlineMarkdownToHtml(text) {
719
818
  function renderMarkdown(text) {
720
819
  if (!text) return "";
721
820
  const inlined = inlineMarkdownToHtml(text);
821
+ if (/<div[\s>]/i.test(inlined)) {
822
+ return DOMPurify.sanitize(inlined, SANITIZE_CFG);
823
+ }
722
824
  const paragraphs = inlined.split(/\n{2,}/).map((para) => para.replace(/\n/g, "<br>")).filter((p) => p.length > 0);
723
825
  const html = paragraphs.length > 1 ? paragraphs.map((p) => `<p>${p}</p>`).join("") : paragraphs[0] ?? "";
724
826
  return DOMPurify.sanitize(html, SANITIZE_CFG);
725
827
  }
828
+ var ROOT_MENU_CHIP_IDS = /* @__PURE__ */ new Set([
829
+ "stock_analysis",
830
+ "stock_discovery",
831
+ "new_listings",
832
+ "portfolio_risk",
833
+ "market_forecast",
834
+ "crypto",
835
+ "tradable_picks",
836
+ "peer_compare"
837
+ ]);
838
+ var MENU_SUBTITLES = {
839
+ stock_analysis: "Fundamental & technical",
840
+ stock_discovery: "Find promising stocks",
841
+ new_listings: "Track IPOs & new stocks",
842
+ portfolio_risk: "Analyze your portfolio",
843
+ market_forecast: "Outlook & key events",
844
+ crypto: "BTC, ETH & trends",
845
+ tradable_picks: "Short-term setups",
846
+ peer_compare: "Compare peer stocks"
847
+ };
848
+ var MENU_ICON_KEY = {
849
+ stock_analysis: "menuIconGreen",
850
+ stock_discovery: "menuIconBlue",
851
+ new_listings: "menuIconLeaf",
852
+ portfolio_risk: "menuIconRed",
853
+ market_forecast: "menuIconOrange",
854
+ crypto: "menuIconGold",
855
+ tradable_picks: "menuIconTeal",
856
+ peer_compare: "menuIconPurple"
857
+ };
858
+ function isRootMenuChips(chips) {
859
+ return chips.some((c) => ROOT_MENU_CHIP_IDS.has(c.id));
860
+ }
726
861
  function MessageBubble({ message, onChipClick }) {
727
862
  const isBot = message.role === "bot";
728
863
  const bubbleClass = isBot ? `${chat_default.bubble} ${chat_default.bubbleBot}` : `${chat_default.bubble} ${chat_default.bubbleUser}`;
864
+ const handleMarkdownClick = (e) => {
865
+ const anchor = e.target.closest("a");
866
+ if (!anchor?.href) return;
867
+ e.preventDefault();
868
+ window.location.assign(anchor.href);
869
+ };
870
+ const chips = message.chips ?? [];
871
+ const showMenuGrid = isBot && message.chipsActive && isRootMenuChips(chips);
729
872
  return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column" }, children: [
730
873
  /* @__PURE__ */ jsx("div", { className: bubbleClass, children: isBot ? /* @__PURE__ */ jsx(
731
874
  "div",
732
875
  {
733
876
  className: chat_default.markdown,
734
- dangerouslySetInnerHTML: { __html: renderMarkdown(message.content) }
877
+ dangerouslySetInnerHTML: { __html: renderMarkdown(message.content) },
878
+ onClick: handleMarkdownClick
735
879
  }
736
880
  ) : message.content }),
737
- isBot && message.chips && message.chips.length > 0 ? /* @__PURE__ */ jsx(
881
+ showMenuGrid ? /* @__PURE__ */ jsx("div", { className: chat_default.menuGrid, children: chips.map((chip) => {
882
+ const iconKey = MENU_ICON_KEY[chip.id];
883
+ const iconClass = iconKey && chat_default[iconKey] || "";
884
+ return /* @__PURE__ */ jsxs(
885
+ "button",
886
+ {
887
+ type: "button",
888
+ className: chat_default.menuItem,
889
+ onClick: () => onChipClick(chip),
890
+ children: [
891
+ /* @__PURE__ */ jsx("div", { className: `${chat_default.menuItemIcon} ${iconClass}`, children: chip.icon }),
892
+ /* @__PURE__ */ jsx("div", { className: chat_default.menuItemTitle, children: chip.label }),
893
+ /* @__PURE__ */ jsx("div", { className: chat_default.menuItemSub, children: MENU_SUBTITLES[chip.id] ?? "" })
894
+ ]
895
+ },
896
+ chip.id
897
+ );
898
+ }) }) : isBot && chips.length > 0 ? /* @__PURE__ */ jsx(
738
899
  ChipRow,
739
900
  {
740
- chips: message.chips,
901
+ chips,
741
902
  disabled: !message.chipsActive,
742
903
  onClick: onChipClick
743
904
  }
@@ -841,16 +1002,24 @@ function ChatHeader({
841
1002
  }
842
1003
  function ChatInput({ disabled, placeholder, onSend }) {
843
1004
  const [value, setValue] = useState("");
1005
+ const inputRef = useRef(null);
1006
+ useEffect(() => {
1007
+ if (!disabled) {
1008
+ inputRef.current?.focus();
1009
+ }
1010
+ }, [disabled]);
844
1011
  const submit = () => {
845
1012
  const trimmed = value.trim();
846
1013
  if (!trimmed || disabled) return;
847
1014
  onSend(trimmed);
848
1015
  setValue("");
1016
+ inputRef.current?.focus();
849
1017
  };
850
1018
  return /* @__PURE__ */ jsxs("div", { className: chat_default.input, children: [
851
1019
  /* @__PURE__ */ jsx(
852
1020
  "input",
853
1021
  {
1022
+ ref: inputRef,
854
1023
  type: "text",
855
1024
  className: chat_default.inputBox,
856
1025
  value,
@@ -878,31 +1047,62 @@ function ChatInput({ disabled, placeholder, onSend }) {
878
1047
  )
879
1048
  ] });
880
1049
  }
881
- function FloatingButton({ position, onClick, brandColor }) {
1050
+ function FloatingButton({ position, onClick, brandColor, showPopup, popupMessage, onPopupDismiss }) {
882
1051
  const posClass = position === "bottom-left" ? chat_default.positionLeft : chat_default.positionRight;
883
- return /* @__PURE__ */ jsx(
884
- "button",
885
- {
886
- type: "button",
887
- className: `${chat_default.floatingButton} ${posClass}`,
888
- onClick,
889
- "aria-label": "Open chat",
890
- style: brandColor ? { background: brandColor } : void 0,
891
- children: "\u{1F4AC}"
892
- }
893
- );
1052
+ const popupPosClass = position === "bottom-left" ? chat_default.popupBubbleLeft : chat_default.popupBubbleRight;
1053
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1054
+ showPopup && popupMessage ? /* @__PURE__ */ jsxs(
1055
+ "div",
1056
+ {
1057
+ className: `${chat_default.popupBubble} ${popupPosClass}`,
1058
+ role: "status",
1059
+ "aria-live": "polite",
1060
+ children: [
1061
+ popupMessage,
1062
+ /* @__PURE__ */ jsx(
1063
+ "button",
1064
+ {
1065
+ type: "button",
1066
+ className: chat_default.popupDismiss,
1067
+ onClick: onPopupDismiss,
1068
+ "aria-label": "Dismiss greeting",
1069
+ children: "\u2715"
1070
+ }
1071
+ )
1072
+ ]
1073
+ }
1074
+ ) : null,
1075
+ /* @__PURE__ */ jsx(
1076
+ "button",
1077
+ {
1078
+ type: "button",
1079
+ className: `${chat_default.floatingButton} ${posClass}`,
1080
+ onClick,
1081
+ "aria-label": "Open chat",
1082
+ style: brandColor ? { background: brandColor } : void 0,
1083
+ children: "\u{1F4AC}"
1084
+ }
1085
+ )
1086
+ ] });
894
1087
  }
895
1088
  var DEFAULT_BRAND_NAME = "Wealth Alpha AI";
896
1089
  var DEFAULT_BRAND_COLOR = "#1a2d5a";
897
1090
  var DEFAULT_AUTH_CHECK = "/me";
898
- 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.";
899
- var CTA_LINE = "Select a quick command below \u2014 or type your query to ask our AI Research Analyst directly.";
1091
+ var DEFAULT_GREETING = "Hi! I'm your Wealth Alpha AI assistant. How can I help you?";
1092
+ var CTA_LINE = "Select a quick command below";
1093
+ var PORTFOLIO_CSV_CHIP_ID = "__portfolio_csv_upload";
1094
+ var UPGRADE_PREMIUM_CHIP_ID = "__upgrade_premium__";
1095
+ function resolvePricingUrl(upgradeUrl) {
1096
+ if (upgradeUrl) return upgradeUrl;
1097
+ if (typeof window !== "undefined") {
1098
+ return `${window.location.origin}/pricing`;
1099
+ }
1100
+ return "";
1101
+ }
900
1102
  function buildWelcome(name, plan) {
901
1103
  const greeting = name ? `Welcome back, ${name}! You have ${plan ? plan : "Premium"} access.` : `Welcome! You have ${plan ? plan : "Premium"} access.`;
902
1104
  return `${greeting}
903
1105
 
904
- ${DISCLAIMER_BLOCK}
905
-
906
1106
  ${CTA_LINE}`;
907
1107
  }
908
1108
  function WealthChat(props) {
@@ -917,6 +1117,7 @@ function WealthChat(props) {
917
1117
  position = "bottom-right",
918
1118
  defaultOpen = false,
919
1119
  showCountdown = true,
1120
+ greetingMessage = DEFAULT_GREETING,
920
1121
  onLogin,
921
1122
  onLogout,
922
1123
  onSessionExpire,
@@ -924,9 +1125,19 @@ function WealthChat(props) {
924
1125
  } = props;
925
1126
  const [mounted, setMounted] = useState(false);
926
1127
  const [open, setOpen] = useState(defaultOpen);
1128
+ const [showPopup, setShowPopup] = useState(false);
1129
+ const popupShownRef = useRef(defaultOpen);
927
1130
  useEffect(() => {
928
1131
  setMounted(true);
929
1132
  }, []);
1133
+ useEffect(() => {
1134
+ if (!mounted || open || popupShownRef.current) return;
1135
+ const timer = setTimeout(() => {
1136
+ setShowPopup(true);
1137
+ popupShownRef.current = true;
1138
+ }, 1e3);
1139
+ return () => clearTimeout(timer);
1140
+ }, [mounted, open]);
930
1141
  const {
931
1142
  session,
932
1143
  remainingMs,
@@ -954,9 +1165,11 @@ function WealthChat(props) {
954
1165
  },
955
1166
  [session, setHistory]
956
1167
  );
1168
+ const csvFileRef = useRef(null);
957
1169
  const {
958
1170
  state: chatState,
959
1171
  sendText,
1172
+ uploadCsv,
960
1173
  appendBotResponse,
961
1174
  appendUserMessage,
962
1175
  deactivatePriorChips,
@@ -996,16 +1209,46 @@ function WealthChat(props) {
996
1209
  async (chip) => {
997
1210
  touch();
998
1211
  deactivatePriorChips();
1212
+ if (chip.id === PORTFOLIO_CSV_CHIP_ID) {
1213
+ appendUserMessage(chip.label);
1214
+ csvFileRef.current?.click();
1215
+ return;
1216
+ }
1217
+ if (chip.id === UPGRADE_PREMIUM_CHIP_ID) {
1218
+ appendUserMessage(chip.label);
1219
+ const pricingUrl = resolvePricingUrl(user?.upgradeUrl);
1220
+ if (pricingUrl && typeof window !== "undefined") {
1221
+ window.location.assign(pricingUrl);
1222
+ }
1223
+ return;
1224
+ }
999
1225
  appendUserMessage(chip.label);
1000
1226
  setTyping(true);
1001
1227
  try {
1002
1228
  const resp = await callChip(chip);
1003
- if (resp) appendBotResponse(resp);
1229
+ if (!resp) return;
1230
+ const redirectUrl = resp.metadata?.redirectUrl;
1231
+ if (typeof redirectUrl === "string" && redirectUrl && typeof window !== "undefined") {
1232
+ window.location.assign(redirectUrl);
1233
+ return;
1234
+ }
1235
+ appendBotResponse(resp);
1004
1236
  } finally {
1005
1237
  setTyping(false);
1006
1238
  }
1007
1239
  },
1008
- [touch, deactivatePriorChips, appendUserMessage, callChip, setTyping, appendBotResponse]
1240
+ [touch, deactivatePriorChips, appendUserMessage, callChip, setTyping, appendBotResponse, user?.upgradeUrl]
1241
+ );
1242
+ const handleCsvFileSelected = useCallback(
1243
+ (e) => {
1244
+ const file = e.target.files?.[0];
1245
+ e.target.value = "";
1246
+ if (file) {
1247
+ touch();
1248
+ void uploadCsv(file);
1249
+ }
1250
+ },
1251
+ [touch, uploadCsv]
1009
1252
  );
1010
1253
  const handleSend = useCallback(
1011
1254
  (text) => {
@@ -1014,6 +1257,10 @@ function WealthChat(props) {
1014
1257
  },
1015
1258
  [touch, sendText]
1016
1259
  );
1260
+ const handleFloatingButtonClick = useCallback(() => {
1261
+ setOpen(true);
1262
+ setShowPopup(false);
1263
+ }, []);
1017
1264
  const handleClose = useCallback(() => setOpen(false), []);
1018
1265
  const handleClear = useCallback(() => {
1019
1266
  clearChat();
@@ -1030,7 +1277,17 @@ function WealthChat(props) {
1030
1277
  ["--wac-brand"]: brandColor
1031
1278
  };
1032
1279
  return /* @__PURE__ */ jsxs("div", { className: chat_default.root, style: rootStyle, children: [
1033
- !open ? /* @__PURE__ */ jsx(FloatingButton, { position, onClick: () => setOpen(true), brandColor }) : null,
1280
+ !open ? /* @__PURE__ */ jsx(
1281
+ FloatingButton,
1282
+ {
1283
+ position,
1284
+ onClick: handleFloatingButtonClick,
1285
+ brandColor,
1286
+ showPopup,
1287
+ popupMessage: greetingMessage,
1288
+ onPopupDismiss: () => setShowPopup(false)
1289
+ }
1290
+ ) : null,
1034
1291
  open ? /* @__PURE__ */ jsxs(
1035
1292
  "div",
1036
1293
  {
@@ -1060,6 +1317,18 @@ function WealthChat(props) {
1060
1317
  onLoginClick: handleLoginClick
1061
1318
  }
1062
1319
  ),
1320
+ /* @__PURE__ */ jsx(
1321
+ "input",
1322
+ {
1323
+ ref: csvFileRef,
1324
+ type: "file",
1325
+ accept: ".csv,text/csv",
1326
+ className: chat_default.hiddenFileInput,
1327
+ onChange: handleCsvFileSelected,
1328
+ "aria-hidden": true,
1329
+ tabIndex: -1
1330
+ }
1331
+ ),
1063
1332
  isLoggedIn ? /* @__PURE__ */ jsx(
1064
1333
  ChatInput,
1065
1334
  {