@wealthx/shadcn 1.3.1 → 1.3.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/.turbo/turbo-build.log +259 -223
- package/CHANGELOG.md +12 -0
- package/dist/{chunk-2UM72RJ7.mjs → chunk-2D3HQPFN.mjs} +12 -10
- package/dist/chunk-2EM2FRU6.mjs +613 -0
- package/dist/{chunk-FH6QVUVZ.mjs → chunk-2GIYVERS.mjs} +2 -2
- package/dist/chunk-2P7HP7LR.mjs +68 -0
- package/dist/{chunk-HISNT2MG.mjs → chunk-37AE3OM5.mjs} +5 -5
- package/dist/{chunk-HBZLGDIN.mjs → chunk-3ERBUVHC.mjs} +169 -110
- package/dist/{chunk-C7CQJNMR.mjs → chunk-3VDET466.mjs} +2 -2
- package/dist/{chunk-462HMNO4.mjs → chunk-4MM7LHM5.mjs} +2 -2
- package/dist/{chunk-QMY3AZJH.mjs → chunk-4Z66LMIQ.mjs} +2 -2
- package/dist/{chunk-U5X52X37.mjs → chunk-57ZXILTS.mjs} +6 -6
- package/dist/{chunk-3OYFOX3X.mjs → chunk-5VOTTIXF.mjs} +2 -2
- package/dist/{chunk-LBMRIB3G.mjs → chunk-6AJUS7VX.mjs} +1 -1
- package/dist/{chunk-OODBHKG7.mjs → chunk-6HIOM2HL.mjs} +7 -4
- package/dist/{chunk-BDYZCBRT.mjs → chunk-6QAFGZC2.mjs} +2 -2
- package/dist/{chunk-U4NDAF2P.mjs → chunk-6TX73WG7.mjs} +1 -1
- package/dist/{chunk-GD4BJDJR.mjs → chunk-7BTFGCFC.mjs} +4 -4
- package/dist/{chunk-FAKPBKLT.mjs → chunk-7GWRPXHD.mjs} +4 -4
- package/dist/{chunk-NMOI6CQD.mjs → chunk-7YI3HEBH.mjs} +5 -5
- package/dist/{chunk-T4BJLT57.mjs → chunk-AE7MASLF.mjs} +5 -5
- package/dist/{chunk-VLQZANBF.mjs → chunk-AFML43VJ.mjs} +6 -1
- package/dist/chunk-BBXSNDS3.mjs +260 -0
- package/dist/chunk-BOW7U26Y.mjs +203 -0
- package/dist/{chunk-34NWQURD.mjs → chunk-BS75ICOO.mjs} +2 -2
- package/dist/chunk-D2NSIIXG.mjs +394 -0
- package/dist/{chunk-3GF7OVTP.mjs → chunk-DGNHGNYH.mjs} +2 -2
- package/dist/{chunk-VLARHE5V.mjs → chunk-DMXYRCHM.mjs} +6 -6
- package/dist/{chunk-OGOYQ7BG.mjs → chunk-DQB4EPIS.mjs} +1 -1
- package/dist/{chunk-MIZQHHUO.mjs → chunk-FL6DZFJK.mjs} +106 -38
- package/dist/{chunk-I3RZS7V2.mjs → chunk-FLL633WS.mjs} +19 -33
- package/dist/{chunk-PBL4OQV2.mjs → chunk-FTPBQVQ6.mjs} +4 -4
- package/dist/chunk-FYPSTTEJ.mjs +169 -0
- package/dist/{chunk-6O6KD7CE.mjs → chunk-G27TSQLQ.mjs} +6 -6
- package/dist/{chunk-66MI7Q4B.mjs → chunk-GT3RU6GA.mjs} +2 -2
- package/dist/{chunk-D6ID6M4V.mjs → chunk-GTAVSBDO.mjs} +2 -2
- package/dist/{chunk-24FUO7TD.mjs → chunk-H6NQTIF4.mjs} +2 -2
- package/dist/{chunk-7DHU4VGG.mjs → chunk-HK4HUQTV.mjs} +2 -2
- package/dist/chunk-I4KVSZCH.mjs +101 -0
- package/dist/{chunk-RGVKLTLH.mjs → chunk-IKXYTCSB.mjs} +2 -2
- package/dist/{chunk-Y6DWJSKZ.mjs → chunk-ISUA7DSB.mjs} +1 -1
- package/dist/{chunk-2A5RRQGG.mjs → chunk-JD3YWRNP.mjs} +10 -14
- package/dist/{chunk-J5UICVJS.mjs → chunk-JPGL36WQ.mjs} +2 -2
- package/dist/{chunk-7XJHLGUV.mjs → chunk-JTK6VJXY.mjs} +2 -2
- package/dist/{chunk-7YAU5CY6.mjs → chunk-JVMXMFBB.mjs} +2 -2
- package/dist/{chunk-IAE3F7DR.mjs → chunk-JZY6TNIS.mjs} +21 -21
- package/dist/{chunk-K5A5L6T2.mjs → chunk-K4KOD3KR.mjs} +12 -12
- package/dist/{chunk-MBON7YRJ.mjs → chunk-K5QV4TT6.mjs} +3 -3
- package/dist/{chunk-IHMFS7NZ.mjs → chunk-K5VHK7CM.mjs} +21 -21
- package/dist/{chunk-RJI6GKVF.mjs → chunk-KCWNDYPZ.mjs} +5 -5
- package/dist/{chunk-UFYSFDER.mjs → chunk-KFH36NKF.mjs} +1 -1
- package/dist/{chunk-EBXQWIYG.mjs → chunk-KLTACJ2G.mjs} +5 -5
- package/dist/{chunk-3TTACBDP.mjs → chunk-KWD6GANL.mjs} +4 -4
- package/dist/{chunk-IOJRDS6V.mjs → chunk-L4NSRQ3T.mjs} +218 -147
- package/dist/{chunk-GYMYRIZP.mjs → chunk-LBTHZSBT.mjs} +2 -2
- package/dist/{chunk-AMQZRHEZ.mjs → chunk-LQULK2E3.mjs} +5 -5
- package/dist/{chunk-YJG55G2H.mjs → chunk-LR6LHDP3.mjs} +5 -5
- package/dist/{chunk-7PV3IWCN.mjs → chunk-M4VYX2PV.mjs} +19 -1
- package/dist/{chunk-P76HMUI6.mjs → chunk-MDUKXXIL.mjs} +2 -2
- package/dist/{chunk-LV35NGVG.mjs → chunk-N6Q5IPKT.mjs} +9 -9
- package/dist/{chunk-DOEO3CDL.mjs → chunk-NB3ZL36B.mjs} +1 -1
- package/dist/{chunk-XREGSKX3.mjs → chunk-NOOEKOWY.mjs} +5 -5
- package/dist/{chunk-NL3ZO62D.mjs → chunk-NT4FX27K.mjs} +1 -1
- package/dist/{chunk-QZ4RE6NA.mjs → chunk-NTYQWVLI.mjs} +6 -6
- package/dist/{chunk-ERGGHC2V.mjs → chunk-OEOOYMC2.mjs} +2 -2
- package/dist/{chunk-4GAWMKMI.mjs → chunk-OIKBW2QD.mjs} +291 -54
- package/dist/{chunk-DUJTAXMH.mjs → chunk-OKTJFDPN.mjs} +6 -6
- package/dist/chunk-OLKMCXAR.mjs +1219 -0
- package/dist/{chunk-EI5F6FMT.mjs → chunk-OWFQSXVD.mjs} +3 -3
- package/dist/{chunk-6DZEXFNB.mjs → chunk-P2N2PEFY.mjs} +3 -3
- package/dist/{chunk-NSLMILBT.mjs → chunk-P7CEBZM6.mjs} +2 -2
- package/dist/{chunk-7S5AESZO.mjs → chunk-PNRUH7JY.mjs} +6 -6
- package/dist/{chunk-ZU4NV6RG.mjs → chunk-PNSYFE3K.mjs} +2 -2
- package/dist/{chunk-JKGDCQTZ.mjs → chunk-QTRSCVQ3.mjs} +5 -5
- package/dist/{chunk-ABFDMHOR.mjs → chunk-QX7IFQSF.mjs} +5 -5
- package/dist/{chunk-CFMQP5QS.mjs → chunk-QXKGOMUX.mjs} +6 -6
- package/dist/{chunk-NQPOYKAQ.mjs → chunk-R2ON6CAN.mjs} +2 -2
- package/dist/{chunk-DBHJ5KC3.mjs → chunk-R4HCRDU5.mjs} +1 -1
- package/dist/{chunk-EWRB4PAD.mjs → chunk-RCAOCHWA.mjs} +14 -14
- package/dist/{chunk-EFRENWEJ.mjs → chunk-RSUIPKGX.mjs} +2 -2
- package/dist/{chunk-DGHAXJBN.mjs → chunk-S2FKV4M5.mjs} +5 -5
- package/dist/{chunk-RGU7HOEC.mjs → chunk-SET2ANTY.mjs} +5 -7
- package/dist/chunk-SFH2NJEJ.mjs +47 -0
- package/dist/{chunk-6AW4KJHE.mjs → chunk-SIVYAI3M.mjs} +12 -12
- package/dist/{chunk-5FQIKDKP.mjs → chunk-THVO2N47.mjs} +8 -8
- package/dist/{chunk-JMHR3YGZ.mjs → chunk-TLAWKTSA.mjs} +3 -3
- package/dist/{chunk-HVY6KCCF.mjs → chunk-TOWTPLRC.mjs} +68 -72
- package/dist/{chunk-6JQFUE5I.mjs → chunk-UALR6JGV.mjs} +2 -2
- package/dist/{chunk-MLNEWRWV.mjs → chunk-UJZ4UHWI.mjs} +10 -15
- package/dist/{chunk-MARPPFOJ.mjs → chunk-UNACI2YK.mjs} +2 -2
- package/dist/{chunk-3NCUZIFP.mjs → chunk-V6XGXYCJ.mjs} +7 -7
- package/dist/chunk-VB5M6OZQ.mjs +57 -0
- package/dist/{chunk-5IS7G74I.mjs → chunk-VY5NEUP7.mjs} +6 -6
- package/dist/{chunk-JHJHG4GO.mjs → chunk-WE4YKBDE.mjs} +2 -2
- package/dist/{chunk-BKNFWEH2.mjs → chunk-WL6WVV47.mjs} +3 -3
- package/dist/{chunk-FWCSY2DS.mjs → chunk-WNQUEZJF.mjs} +22 -1
- package/dist/{chunk-2Y7YJKPE.mjs → chunk-WZ6UJCBL.mjs} +1 -1
- package/dist/{chunk-UMTOX62O.mjs → chunk-XYPW2XA5.mjs} +13 -10
- package/dist/chunk-Y2MTAVAK.mjs +34 -0
- package/dist/{chunk-6CR5N2JW.mjs → chunk-YCWLFG27.mjs} +6 -6
- package/dist/{chunk-PU4YZQXV.mjs → chunk-YE67AALL.mjs} +12 -12
- package/dist/{chunk-M3FV7LOK.mjs → chunk-YEWNFK5S.mjs} +6 -1
- package/dist/{chunk-R3VSPKNP.mjs → chunk-YIZHS72Z.mjs} +11 -12
- package/dist/{chunk-7PYJD5JI.mjs → chunk-ZEDMKQK2.mjs} +2 -2
- package/dist/{chunk-N2PT566P.mjs → chunk-ZFCDYW6N.mjs} +4 -4
- package/dist/chunk-ZGQIVGIN.mjs +57 -0
- package/dist/{chunk-Q2BGOAMG.mjs → chunk-ZKWXDQDG.mjs} +4 -4
- package/dist/{chunk-GHC7LLUX.mjs → chunk-ZOWL2L5J.mjs} +5 -5
- package/dist/components/ui/accordion.mjs +3 -3
- package/dist/components/ui/add-column-modal.js +2 -2
- package/dist/components/ui/add-column-modal.mjs +10 -10
- package/dist/components/ui/add-lead-modal.js +424 -82
- package/dist/components/ui/add-lead-modal.mjs +12 -9
- package/dist/components/ui/advisor-card.js +2 -2
- package/dist/components/ui/advisor-card.mjs +8 -8
- package/dist/components/ui/ai-assistant-drawer.js +2 -2
- package/dist/components/ui/ai-assistant-drawer.mjs +9 -9
- package/dist/components/ui/ai-builder.js +958 -0
- package/dist/components/ui/ai-builder.mjs +25 -0
- package/dist/components/ui/ai-conversations.js +2045 -0
- package/dist/components/ui/ai-conversations.mjs +41 -0
- package/dist/components/ui/alert-dialog.js +2 -2
- package/dist/components/ui/alert-dialog.mjs +5 -5
- package/dist/components/ui/alert.mjs +3 -3
- package/dist/components/ui/appointment-action-dialogs.js +19 -3
- package/dist/components/ui/appointment-action-dialogs.mjs +15 -14
- package/dist/components/ui/appointment-availability-settings.js +181 -111
- package/dist/components/ui/appointment-availability-settings.mjs +20 -18
- package/dist/components/ui/appointment-book-dialog.js +113 -24
- package/dist/components/ui/appointment-book-dialog.mjs +21 -20
- package/dist/components/ui/appointment-calendar-view.js +19 -3
- package/dist/components/ui/appointment-calendar-view.mjs +10 -9
- package/dist/components/ui/appointment-detail-sheet.js +19 -3
- package/dist/components/ui/appointment-detail-sheet.mjs +18 -17
- package/dist/components/ui/appointment-gmail-connect.js +49 -89
- package/dist/components/ui/appointment-gmail-connect.mjs +8 -9
- package/dist/components/ui/appointment-mini-card.js +2 -2
- package/dist/components/ui/appointment-mini-card.mjs +6 -6
- package/dist/components/ui/appointment-time-slot-picker.mjs +6 -6
- package/dist/components/ui/appointment-upcoming-card.js +19 -3
- package/dist/components/ui/appointment-upcoming-card.mjs +15 -14
- package/dist/components/ui/auth-logo.js +95 -0
- package/dist/components/ui/auth-logo.mjs +8 -0
- package/dist/components/ui/auth-page-layout.js +108 -0
- package/dist/components/ui/auth-page-layout.mjs +8 -0
- package/dist/components/ui/avatar.mjs +3 -3
- package/dist/components/ui/backoffice-alert-history-chart.js +2 -2
- package/dist/components/ui/backoffice-alert-history-chart.mjs +9 -9
- package/dist/components/ui/backoffice-alerts-chart.js +2 -2
- package/dist/components/ui/backoffice-alerts-chart.mjs +11 -11
- package/dist/components/ui/backoffice-connections-chart.js +2 -2
- package/dist/components/ui/backoffice-connections-chart.mjs +11 -11
- package/dist/components/ui/backoffice-contact-history-chart.js +2 -2
- package/dist/components/ui/backoffice-contact-history-chart.mjs +9 -9
- package/dist/components/ui/badge.mjs +4 -4
- package/dist/components/ui/borrowing-capacity-line-chart.js +145 -132
- package/dist/components/ui/borrowing-capacity-line-chart.mjs +9 -9
- package/dist/components/ui/button.js +2 -2
- package/dist/components/ui/button.mjs +4 -4
- package/dist/components/ui/calendar.js +17 -3
- package/dist/components/ui/calendar.mjs +6 -5
- package/dist/components/ui/card.mjs +3 -3
- package/dist/components/ui/cash-balance-line-chart.js +157 -152
- package/dist/components/ui/cash-balance-line-chart.mjs +9 -9
- package/dist/components/ui/cashflow-bar-chart.js +2 -2
- package/dist/components/ui/cashflow-bar-chart.mjs +9 -9
- package/dist/components/ui/chat-widget-primitives.js +573 -0
- package/dist/components/ui/chat-widget-primitives.mjs +21 -0
- package/dist/components/ui/chat-widget.js +1268 -0
- package/dist/components/ui/chat-widget.mjs +29 -0
- package/dist/components/ui/checkbox.mjs +3 -3
- package/dist/components/ui/chip.js +2 -2
- package/dist/components/ui/chip.mjs +6 -6
- package/dist/components/ui/color-picker.js +2 -2
- package/dist/components/ui/color-picker.mjs +7 -7
- package/dist/components/ui/combobox.mjs +3 -3
- package/dist/components/ui/data-table.js +2 -2
- package/dist/components/ui/data-table.mjs +12 -12
- package/dist/components/ui/date-picker.js +22 -6
- package/dist/components/ui/date-picker.mjs +9 -8
- package/dist/components/ui/dialog.js +2 -2
- package/dist/components/ui/dialog.mjs +5 -5
- package/dist/components/ui/document-checklist-template.js +630 -0
- package/dist/components/ui/document-checklist-template.mjs +15 -0
- package/dist/components/ui/drawer.js +2 -2
- package/dist/components/ui/drawer.mjs +3 -3
- package/dist/components/ui/dropdown-menu.mjs +3 -3
- package/dist/components/ui/empty.mjs +3 -3
- package/dist/components/ui/expense-bar-chart.js +2 -2
- package/dist/components/ui/expense-bar-chart.mjs +9 -9
- package/dist/components/ui/field.mjs +5 -5
- package/dist/components/ui/financial-cards.js +431 -291
- package/dist/components/ui/financial-cards.mjs +10 -9
- package/dist/components/ui/financial-drawers.js +4 -4
- package/dist/components/ui/financial-drawers.mjs +8 -8
- package/dist/components/ui/financial-primitives.mjs +3 -3
- package/dist/components/ui/financial-sections.js +8 -9
- package/dist/components/ui/financial-sections.mjs +12 -12
- package/dist/components/ui/form-primitives.mjs +8 -8
- package/dist/components/ui/income-bar-chart.js +2 -2
- package/dist/components/ui/income-bar-chart.mjs +9 -9
- package/dist/components/ui/input-group.js +2 -2
- package/dist/components/ui/input-group.mjs +7 -7
- package/dist/components/ui/input-otp.mjs +3 -3
- package/dist/components/ui/input.mjs +3 -3
- package/dist/components/ui/kanban-column.js +19 -23
- package/dist/components/ui/kanban-column.mjs +14 -14
- package/dist/components/ui/label.mjs +3 -3
- package/dist/components/ui/onboarding-layout.js +476 -0
- package/dist/components/ui/onboarding-layout.mjs +11 -0
- package/dist/components/ui/opportunity-card.js +2 -2
- package/dist/components/ui/opportunity-card.mjs +12 -12
- package/dist/components/ui/opportunity-edit-modals.js +22 -6
- package/dist/components/ui/opportunity-edit-modals.mjs +21 -20
- package/dist/components/ui/opportunity-summary-tab.js +991 -674
- package/dist/components/ui/opportunity-summary-tab.mjs +26 -26
- package/dist/components/ui/page-header.mjs +3 -3
- package/dist/components/ui/page-top-bar.mjs +3 -3
- package/dist/components/ui/pagination.js +2 -2
- package/dist/components/ui/pagination.mjs +6 -6
- package/dist/components/ui/password-strength-tooltip.js +197 -0
- package/dist/components/ui/password-strength-tooltip.mjs +11 -0
- package/dist/components/ui/pipeline-alerts.mjs +3 -3
- package/dist/components/ui/pipeline-board.js +19 -23
- package/dist/components/ui/pipeline-board.mjs +18 -18
- package/dist/components/ui/pipeline-chart.js +12 -6
- package/dist/components/ui/pipeline-chart.mjs +4 -3
- package/dist/components/ui/pipeline-dialogs.js +28 -12
- package/dist/components/ui/pipeline-dialogs.mjs +14 -13
- package/dist/components/ui/pipeline-primitives.mjs +6 -6
- package/dist/components/ui/popover.mjs +3 -3
- package/dist/components/ui/progress.mjs +3 -3
- package/dist/components/ui/property-cashflow-doughnut-chart.js +2 -2
- package/dist/components/ui/property-cashflow-doughnut-chart.mjs +9 -9
- package/dist/components/ui/property-debt-equity-doughnut-chart.js +2 -2
- package/dist/components/ui/property-debt-equity-doughnut-chart.mjs +9 -9
- package/dist/components/ui/property-mobile-estimate-line-chart.js +2 -2
- package/dist/components/ui/property-mobile-estimate-line-chart.mjs +9 -9
- package/dist/components/ui/radio-group.mjs +3 -3
- package/dist/components/ui/select.mjs +3 -3
- package/dist/components/ui/separator.mjs +3 -3
- package/dist/components/ui/sheet.mjs +3 -3
- package/dist/components/ui/sidebar-nav.js +7 -9
- package/dist/components/ui/sidebar-nav.mjs +7 -7
- package/dist/components/ui/skeleton.mjs +3 -3
- package/dist/components/ui/slider.mjs +3 -3
- package/dist/components/ui/sonner.mjs +2 -2
- package/dist/components/ui/spinner.mjs +3 -3
- package/dist/components/ui/stage-timeline.mjs +10 -10
- package/dist/components/ui/stepper.mjs +3 -3
- package/dist/components/ui/switch.mjs +3 -3
- package/dist/components/ui/table.mjs +3 -3
- package/dist/components/ui/tabs.mjs +3 -3
- package/dist/components/ui/textarea.mjs +3 -3
- package/dist/components/ui/toggle-group.mjs +4 -4
- package/dist/components/ui/toggle.mjs +3 -3
- package/dist/components/ui/tooltip.mjs +3 -3
- package/dist/components/ui/transactions-expense-categories-doughnut-chart.js +2 -2
- package/dist/components/ui/transactions-expense-categories-doughnut-chart.mjs +9 -9
- package/dist/components/ui/transactions-income-expense-bar-chart.js +2 -2
- package/dist/components/ui/transactions-income-expense-bar-chart.mjs +9 -9
- package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.js +2 -2
- package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.mjs +9 -9
- package/dist/components/ui/two-fa-setup-form.js +612 -0
- package/dist/components/ui/two-fa-setup-form.mjs +16 -0
- package/dist/components/ui/upload-card.js +187 -0
- package/dist/components/ui/upload-card.mjs +10 -0
- package/dist/components/ui/video-background.js +118 -0
- package/dist/components/ui/video-background.mjs +8 -0
- package/dist/index.js +12765 -9400
- package/dist/index.mjs +341 -245
- package/dist/lib/colors.mjs +1 -1
- package/dist/lib/theme-provider.mjs +1 -1
- package/dist/lib/typography.mjs +2 -2
- package/dist/lib/utils.js +8 -2
- package/dist/lib/utils.mjs +6 -4
- package/dist/styles.css +1 -1
- package/package.json +61 -1
- package/src/components/index.tsx +126 -1
- package/src/components/ui/add-lead-modal.tsx +101 -142
- package/src/components/ui/ai-builder.tsx +560 -0
- package/src/components/ui/ai-conversations.tsx +1690 -0
- package/src/components/ui/appointment-availability-settings.tsx +152 -101
- package/src/components/ui/appointment-book-dialog.tsx +138 -24
- package/src/components/ui/appointment-calendar-view.tsx +2 -3
- package/src/components/ui/appointment-gmail-connect.tsx +23 -42
- package/src/components/ui/auth-logo.tsx +50 -0
- package/src/components/ui/auth-page-layout.tsx +59 -0
- package/src/components/ui/borrowing-capacity-line-chart.tsx +10 -8
- package/src/components/ui/button.tsx +2 -2
- package/src/components/ui/calendar.tsx +2 -1
- package/src/components/ui/cash-balance-line-chart.tsx +10 -14
- package/src/components/ui/chart-shared.tsx +10 -0
- package/src/components/ui/chat-widget-primitives.tsx +336 -0
- package/src/components/ui/chat-widget.tsx +822 -0
- package/src/components/ui/document-checklist-template.tsx +264 -0
- package/src/components/ui/drawer.tsx +2 -2
- package/src/components/ui/financial-cards.tsx +176 -78
- package/src/components/ui/financial-drawers.tsx +2 -2
- package/src/components/ui/financial-sections.tsx +1 -1
- package/src/components/ui/kanban-column.tsx +2 -5
- package/src/components/ui/onboarding-layout.tsx +109 -0
- package/src/components/ui/opportunity-summary-tab.tsx +469 -142
- package/src/components/ui/password-strength-tooltip.tsx +70 -0
- package/src/components/ui/pipeline-chart.tsx +2 -6
- package/src/components/ui/sidebar-nav.tsx +2 -15
- package/src/components/ui/two-fa-setup-form.tsx +229 -0
- package/src/components/ui/upload-card.tsx +98 -0
- package/src/components/ui/video-background.tsx +55 -0
- package/src/lib/format-date.ts +26 -0
- package/src/lib/utils.ts +11 -0
- package/src/styles/styles-css.ts +1 -1
- package/tsup.config.ts +13 -0
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
import React, { useState, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Home,
|
|
4
|
+
RefreshCw,
|
|
5
|
+
TrendingUp,
|
|
6
|
+
Key,
|
|
7
|
+
CalendarDays,
|
|
8
|
+
HelpCircle,
|
|
9
|
+
Lock,
|
|
10
|
+
Calendar,
|
|
11
|
+
CheckCircle2,
|
|
12
|
+
ArrowLeft,
|
|
13
|
+
ArrowRight,
|
|
14
|
+
ExternalLink,
|
|
15
|
+
Video,
|
|
16
|
+
Phone,
|
|
17
|
+
MapPin,
|
|
18
|
+
MessageSquare,
|
|
19
|
+
User,
|
|
20
|
+
} from "lucide-react";
|
|
21
|
+
import { cn } from "@/lib/utils";
|
|
22
|
+
import { Button } from "@/components/ui/button";
|
|
23
|
+
import { Input } from "@/components/ui/input";
|
|
24
|
+
import { Field, FieldLabel, FieldError } from "@/components/ui/field";
|
|
25
|
+
import {
|
|
26
|
+
ChatWidgetLauncher,
|
|
27
|
+
ChatWidgetHeader,
|
|
28
|
+
ChatWidgetMessage,
|
|
29
|
+
ChatWidgetInputBar,
|
|
30
|
+
type ChatWidgetMessageRole,
|
|
31
|
+
} from "@/components/ui/chat-widget-primitives";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ChatWidget — WealthX DS (Broker Website Embeddable Chat)
|
|
35
|
+
*
|
|
36
|
+
* Two-screen flow:
|
|
37
|
+
* Screen 1 — TopicGrid: lead selects a conversation topic
|
|
38
|
+
* Screen 2 — Chat: AI conversation based on selected topic
|
|
39
|
+
*
|
|
40
|
+
* Exports (atomic order):
|
|
41
|
+
* ChatWidgetIntakeForm — Molecule — Screen 1 contact form
|
|
42
|
+
* ChatWidgetTopicCard — Molecule — Single topic option card
|
|
43
|
+
* ChatWidgetTopicGrid — Organism — Screen 2 topic selection grid
|
|
44
|
+
* ChatWidgetInteractiveCard — Molecule — AI-embedded interactive cards
|
|
45
|
+
* ChatWidgetWindow — Template — 3-screen container + localStorage cache
|
|
46
|
+
* ChatWidget — Root — Floating widget with FAB launcher
|
|
47
|
+
*
|
|
48
|
+
* Pure display component for story control. Real API wiring done in the app layer.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Shared types
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
export interface ChatWidgetUser {
|
|
56
|
+
name: string;
|
|
57
|
+
phone: string;
|
|
58
|
+
email: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ChatWidgetTopic {
|
|
62
|
+
id: string;
|
|
63
|
+
icon: React.ReactNode;
|
|
64
|
+
label: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ChatWidgetChatMessage {
|
|
69
|
+
id: string;
|
|
70
|
+
role: ChatWidgetMessageRole;
|
|
71
|
+
content: string;
|
|
72
|
+
timestamp?: string;
|
|
73
|
+
isStreaming?: boolean;
|
|
74
|
+
/** Optional inline interactive card rendered below the message bubble. */
|
|
75
|
+
interactiveCard?: ChatWidgetInteractiveCardData;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type ChatWidgetScreen = "intake" | "topics" | "chat" | "booking";
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Default topics
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
export const DEFAULT_CHAT_WIDGET_TOPICS: ChatWidgetTopic[] = [
|
|
85
|
+
{
|
|
86
|
+
id: "buy_home",
|
|
87
|
+
icon: <Home className="size-5" />,
|
|
88
|
+
label: "Buy a Home",
|
|
89
|
+
description: "Purchase your first or next property",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "refinance",
|
|
93
|
+
icon: <RefreshCw className="size-5" />,
|
|
94
|
+
label: "Refinance",
|
|
95
|
+
description: "Get a better rate on your current loan",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "investment",
|
|
99
|
+
icon: <TrendingUp className="size-5" />,
|
|
100
|
+
label: "Investment Property",
|
|
101
|
+
description: "Grow your property portfolio",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: "first_home_buyer",
|
|
105
|
+
icon: <Key className="size-5" />,
|
|
106
|
+
label: "First Home Buyer",
|
|
107
|
+
description: "First home buyer grants & schemes",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "book_meeting",
|
|
111
|
+
icon: <CalendarDays className="size-5" />,
|
|
112
|
+
label: "Book a Meeting",
|
|
113
|
+
description: "Talk directly with an advisor",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "general_question",
|
|
117
|
+
icon: <HelpCircle className="size-5" />,
|
|
118
|
+
label: "General Question",
|
|
119
|
+
description: "Ask anything about home loans",
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// ChatWidgetIntakeForm (Molecule — Screen 1)
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
export interface ChatWidgetIntakeFormProps {
|
|
128
|
+
brokerName?: string;
|
|
129
|
+
onSubmit: (user: ChatWidgetUser) => void;
|
|
130
|
+
isSubmitting?: boolean;
|
|
131
|
+
className?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function ChatWidgetIntakeForm({
|
|
135
|
+
brokerName,
|
|
136
|
+
onSubmit,
|
|
137
|
+
isSubmitting,
|
|
138
|
+
className,
|
|
139
|
+
}: ChatWidgetIntakeFormProps) {
|
|
140
|
+
const [name, setName] = useState("");
|
|
141
|
+
const [phone, setPhone] = useState("");
|
|
142
|
+
const [email, setEmail] = useState("");
|
|
143
|
+
const [errors, setErrors] = useState<
|
|
144
|
+
Partial<Record<keyof ChatWidgetUser, string>>
|
|
145
|
+
>({});
|
|
146
|
+
|
|
147
|
+
const clearError = (field: keyof ChatWidgetUser) =>
|
|
148
|
+
setErrors((prev) => ({ ...prev, [field]: undefined }));
|
|
149
|
+
|
|
150
|
+
const validate = (): Partial<Record<keyof ChatWidgetUser, string>> => {
|
|
151
|
+
const e: Partial<Record<keyof ChatWidgetUser, string>> = {};
|
|
152
|
+
if (!name.trim()) e.name = "Name is required";
|
|
153
|
+
if (!phone.trim()) e.phone = "Phone number is required";
|
|
154
|
+
if (!email.trim()) e.email = "Email is required";
|
|
155
|
+
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
|
|
156
|
+
e.email = "Enter a valid email address";
|
|
157
|
+
return e;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
const errs = validate();
|
|
163
|
+
if (Object.keys(errs).length > 0) {
|
|
164
|
+
setErrors(errs);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
onSubmit({ name: name.trim(), phone: phone.trim(), email: email.trim() });
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div className={cn("flex flex-col gap-5 p-5", className)}>
|
|
172
|
+
<div>
|
|
173
|
+
<p className="text-body-large font-semibold text-foreground">
|
|
174
|
+
{brokerName ? `Hi from ${brokerName}!` : "Hi there!"}
|
|
175
|
+
</p>
|
|
176
|
+
<p className="mt-1 text-body-medium text-muted-foreground">
|
|
177
|
+
To get started, please share your contact details.
|
|
178
|
+
</p>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<form onSubmit={handleSubmit} noValidate className="flex flex-col gap-3">
|
|
182
|
+
<Field>
|
|
183
|
+
<FieldLabel>
|
|
184
|
+
Full Name <span className="text-destructive">*</span>
|
|
185
|
+
</FieldLabel>
|
|
186
|
+
<Input
|
|
187
|
+
value={name}
|
|
188
|
+
onChange={(e) => {
|
|
189
|
+
setName(e.target.value);
|
|
190
|
+
clearError("name");
|
|
191
|
+
}}
|
|
192
|
+
placeholder="Jane Smith"
|
|
193
|
+
disabled={isSubmitting}
|
|
194
|
+
aria-invalid={!!errors.name || undefined}
|
|
195
|
+
/>
|
|
196
|
+
{errors.name && <FieldError>{errors.name}</FieldError>}
|
|
197
|
+
</Field>
|
|
198
|
+
|
|
199
|
+
<Field>
|
|
200
|
+
<FieldLabel>
|
|
201
|
+
Phone Number <span className="text-destructive">*</span>
|
|
202
|
+
</FieldLabel>
|
|
203
|
+
<Input
|
|
204
|
+
type="tel"
|
|
205
|
+
value={phone}
|
|
206
|
+
onChange={(e) => {
|
|
207
|
+
setPhone(e.target.value);
|
|
208
|
+
clearError("phone");
|
|
209
|
+
}}
|
|
210
|
+
placeholder="0400 000 000"
|
|
211
|
+
disabled={isSubmitting}
|
|
212
|
+
aria-invalid={!!errors.phone || undefined}
|
|
213
|
+
/>
|
|
214
|
+
{errors.phone && <FieldError>{errors.phone}</FieldError>}
|
|
215
|
+
</Field>
|
|
216
|
+
|
|
217
|
+
<Field>
|
|
218
|
+
<FieldLabel>
|
|
219
|
+
Email Address <span className="text-destructive">*</span>
|
|
220
|
+
</FieldLabel>
|
|
221
|
+
<Input
|
|
222
|
+
type="email"
|
|
223
|
+
value={email}
|
|
224
|
+
onChange={(e) => {
|
|
225
|
+
setEmail(e.target.value);
|
|
226
|
+
clearError("email");
|
|
227
|
+
}}
|
|
228
|
+
placeholder="jane@example.com"
|
|
229
|
+
disabled={isSubmitting}
|
|
230
|
+
aria-invalid={!!errors.email || undefined}
|
|
231
|
+
/>
|
|
232
|
+
{errors.email && <FieldError>{errors.email}</FieldError>}
|
|
233
|
+
</Field>
|
|
234
|
+
|
|
235
|
+
<Button
|
|
236
|
+
type="submit"
|
|
237
|
+
variant="default"
|
|
238
|
+
className="mt-1 w-full"
|
|
239
|
+
disabled={isSubmitting}
|
|
240
|
+
loading={isSubmitting}
|
|
241
|
+
>
|
|
242
|
+
Start Chat
|
|
243
|
+
<ArrowRight className="size-4" />
|
|
244
|
+
</Button>
|
|
245
|
+
</form>
|
|
246
|
+
|
|
247
|
+
<p className="flex items-center justify-center gap-1 text-center text-[11px] text-muted-foreground">
|
|
248
|
+
<Lock className="size-3" />
|
|
249
|
+
Your info is safe with us
|
|
250
|
+
</p>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// ChatWidgetTopicCard (Molecule — single topic)
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
export interface ChatWidgetTopicCardProps {
|
|
260
|
+
topic: ChatWidgetTopic;
|
|
261
|
+
onClick: (topicId: string) => void;
|
|
262
|
+
className?: string;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function ChatWidgetTopicCard({
|
|
266
|
+
topic,
|
|
267
|
+
onClick,
|
|
268
|
+
className,
|
|
269
|
+
}: ChatWidgetTopicCardProps) {
|
|
270
|
+
return (
|
|
271
|
+
<Button
|
|
272
|
+
variant="outline"
|
|
273
|
+
onClick={() => onClick(topic.id)}
|
|
274
|
+
className={cn(
|
|
275
|
+
"h-auto w-full flex-col items-start gap-1.5 whitespace-normal border-border px-3 py-3 text-left",
|
|
276
|
+
"hover:border-primary hover:bg-primary/5",
|
|
277
|
+
className,
|
|
278
|
+
)}
|
|
279
|
+
>
|
|
280
|
+
<span className="text-muted-foreground" aria-hidden="true">
|
|
281
|
+
{topic.icon}
|
|
282
|
+
</span>
|
|
283
|
+
<span className="text-body-medium font-semibold text-foreground">
|
|
284
|
+
{topic.label}
|
|
285
|
+
</span>
|
|
286
|
+
{topic.description && (
|
|
287
|
+
<span className="text-body-small leading-snug text-muted-foreground">
|
|
288
|
+
{topic.description}
|
|
289
|
+
</span>
|
|
290
|
+
)}
|
|
291
|
+
</Button>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// ChatWidgetTopicGrid (Organism — Screen 2)
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
export interface ChatWidgetTopicGridProps {
|
|
300
|
+
userName: string;
|
|
301
|
+
topics?: ChatWidgetTopic[];
|
|
302
|
+
onTopicSelect: (topicId: string) => void;
|
|
303
|
+
className?: string;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function ChatWidgetTopicGrid({
|
|
307
|
+
userName,
|
|
308
|
+
topics = DEFAULT_CHAT_WIDGET_TOPICS,
|
|
309
|
+
onTopicSelect,
|
|
310
|
+
className,
|
|
311
|
+
}: ChatWidgetTopicGridProps) {
|
|
312
|
+
return (
|
|
313
|
+
<div className={cn("flex flex-col gap-4 p-5", className)}>
|
|
314
|
+
<div>
|
|
315
|
+
<p className="text-body-large font-semibold text-foreground">
|
|
316
|
+
Hi {userName}!
|
|
317
|
+
</p>
|
|
318
|
+
<p className="mt-0.5 text-body-medium text-muted-foreground">
|
|
319
|
+
What would you like to discuss today?
|
|
320
|
+
</p>
|
|
321
|
+
</div>
|
|
322
|
+
<div className="grid grid-cols-2 gap-2">
|
|
323
|
+
{topics.map((topic) => (
|
|
324
|
+
<ChatWidgetTopicCard
|
|
325
|
+
key={topic.id}
|
|
326
|
+
topic={topic}
|
|
327
|
+
onClick={onTopicSelect}
|
|
328
|
+
/>
|
|
329
|
+
))}
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// ChatWidgetInteractiveCard (Molecule — AI-embedded cards)
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
export type ChatWidgetInteractiveCardType =
|
|
340
|
+
| "quick-reply"
|
|
341
|
+
| "appointment"
|
|
342
|
+
| "meeting-type"
|
|
343
|
+
| "confirmation";
|
|
344
|
+
|
|
345
|
+
export type ChatWidgetMeetingTypeId = "video" | "phone" | "in-person";
|
|
346
|
+
|
|
347
|
+
export interface ChatWidgetMeetingTypeOption {
|
|
348
|
+
id: ChatWidgetMeetingTypeId;
|
|
349
|
+
label: string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export interface ChatWidgetQuickReplyOption {
|
|
353
|
+
id: string;
|
|
354
|
+
label: string;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export interface ChatWidgetAppointmentSlot {
|
|
358
|
+
id: string;
|
|
359
|
+
/** Human-readable datetime, e.g. "Mon 28 Apr · 2:00 PM" */
|
|
360
|
+
datetime: string;
|
|
361
|
+
type: "video" | "in-person" | "phone";
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Data shape for an interactive card — no callbacks, safe to embed in message data. */
|
|
365
|
+
export interface ChatWidgetInteractiveCardData {
|
|
366
|
+
type: ChatWidgetInteractiveCardType;
|
|
367
|
+
options?: ChatWidgetQuickReplyOption[];
|
|
368
|
+
slots?: ChatWidgetAppointmentSlot[];
|
|
369
|
+
meetingTypes?: ChatWidgetMeetingTypeOption[];
|
|
370
|
+
confirmedSlot?: ChatWidgetAppointmentSlot;
|
|
371
|
+
meetingType?: ChatWidgetMeetingTypeId;
|
|
372
|
+
topic?: string;
|
|
373
|
+
advisorName?: string;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export interface ChatWidgetInteractiveCardProps extends ChatWidgetInteractiveCardData {
|
|
377
|
+
onQuickReply?: (optionId: string) => void;
|
|
378
|
+
onSlotSelect?: (slotId: string) => void;
|
|
379
|
+
onMeetingTypeSelect?: (typeId: ChatWidgetMeetingTypeId) => void;
|
|
380
|
+
className?: string;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const MEETING_TYPE_LABELS: Record<ChatWidgetMeetingTypeId, string> = {
|
|
384
|
+
video: "Video Call",
|
|
385
|
+
phone: "Phone Call",
|
|
386
|
+
"in-person": "In-Person Meeting",
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
function MeetingTypeIcon({
|
|
390
|
+
id,
|
|
391
|
+
className = "size-4",
|
|
392
|
+
}: {
|
|
393
|
+
id: ChatWidgetMeetingTypeId;
|
|
394
|
+
className?: string;
|
|
395
|
+
}) {
|
|
396
|
+
const Icon = id === "video" ? Video : id === "phone" ? Phone : MapPin;
|
|
397
|
+
return <Icon className={cn("shrink-0 text-muted-foreground", className)} />;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function ChatWidgetInteractiveCard({
|
|
401
|
+
type,
|
|
402
|
+
options,
|
|
403
|
+
onQuickReply,
|
|
404
|
+
slots,
|
|
405
|
+
onSlotSelect,
|
|
406
|
+
meetingTypes,
|
|
407
|
+
onMeetingTypeSelect,
|
|
408
|
+
confirmedSlot,
|
|
409
|
+
meetingType,
|
|
410
|
+
topic,
|
|
411
|
+
advisorName,
|
|
412
|
+
className,
|
|
413
|
+
}: ChatWidgetInteractiveCardProps) {
|
|
414
|
+
if (type === "quick-reply") {
|
|
415
|
+
return (
|
|
416
|
+
<div className={cn("flex flex-wrap gap-2 pt-1", className)}>
|
|
417
|
+
{options?.map((opt) => (
|
|
418
|
+
<Button
|
|
419
|
+
key={opt.id}
|
|
420
|
+
variant="outline-primary"
|
|
421
|
+
size="sm"
|
|
422
|
+
onClick={() => onQuickReply?.(opt.id)}
|
|
423
|
+
>
|
|
424
|
+
{opt.label}
|
|
425
|
+
</Button>
|
|
426
|
+
))}
|
|
427
|
+
</div>
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (type === "appointment") {
|
|
432
|
+
return (
|
|
433
|
+
<div
|
|
434
|
+
className={cn(
|
|
435
|
+
"flex flex-col gap-2 border border-border bg-card p-3",
|
|
436
|
+
className,
|
|
437
|
+
)}
|
|
438
|
+
>
|
|
439
|
+
<p className="text-body-small font-semibold text-foreground">
|
|
440
|
+
Choose a time
|
|
441
|
+
</p>
|
|
442
|
+
{slots?.map((slot) => (
|
|
443
|
+
<Button
|
|
444
|
+
key={slot.id}
|
|
445
|
+
variant="outline"
|
|
446
|
+
onClick={() => onSlotSelect?.(slot.id)}
|
|
447
|
+
className="h-auto w-full justify-start gap-2 px-3 py-2 hover:border-primary hover:bg-primary/5"
|
|
448
|
+
>
|
|
449
|
+
<Calendar className="size-4 shrink-0 text-muted-foreground" />
|
|
450
|
+
<span className="text-foreground">{slot.datetime}</span>
|
|
451
|
+
</Button>
|
|
452
|
+
))}
|
|
453
|
+
</div>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (type === "meeting-type") {
|
|
458
|
+
return (
|
|
459
|
+
<div
|
|
460
|
+
className={cn(
|
|
461
|
+
"flex flex-col gap-2 border border-border bg-card p-3",
|
|
462
|
+
className,
|
|
463
|
+
)}
|
|
464
|
+
>
|
|
465
|
+
<p className="text-body-small font-semibold text-foreground">
|
|
466
|
+
How would you prefer to meet?
|
|
467
|
+
</p>
|
|
468
|
+
{meetingTypes?.map((mt) => (
|
|
469
|
+
<Button
|
|
470
|
+
key={mt.id}
|
|
471
|
+
variant="outline"
|
|
472
|
+
onClick={() => onMeetingTypeSelect?.(mt.id)}
|
|
473
|
+
className="h-auto w-full justify-start gap-2 px-3 py-2 hover:border-primary hover:bg-primary/5"
|
|
474
|
+
>
|
|
475
|
+
<MeetingTypeIcon id={mt.id} />
|
|
476
|
+
<span className="text-foreground">{mt.label}</span>
|
|
477
|
+
</Button>
|
|
478
|
+
))}
|
|
479
|
+
</div>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (type === "confirmation") {
|
|
484
|
+
return (
|
|
485
|
+
<div
|
|
486
|
+
className={cn(
|
|
487
|
+
"flex flex-col gap-3 border border-border bg-card p-3",
|
|
488
|
+
className,
|
|
489
|
+
)}
|
|
490
|
+
>
|
|
491
|
+
<div className="flex items-center gap-2">
|
|
492
|
+
<CheckCircle2 className="size-4 shrink-0 text-green-600" />
|
|
493
|
+
<p className="text-body-medium font-semibold text-foreground">
|
|
494
|
+
Appointment Confirmed
|
|
495
|
+
</p>
|
|
496
|
+
</div>
|
|
497
|
+
<div className="flex flex-col gap-1.5 border-t border-border pt-2 text-body-small text-muted-foreground">
|
|
498
|
+
{confirmedSlot && (
|
|
499
|
+
<div className="flex items-center gap-2">
|
|
500
|
+
<Calendar className="size-3.5 shrink-0" />
|
|
501
|
+
<span>{confirmedSlot.datetime}</span>
|
|
502
|
+
</div>
|
|
503
|
+
)}
|
|
504
|
+
{meetingType && (
|
|
505
|
+
<div className="flex items-center gap-2">
|
|
506
|
+
<MeetingTypeIcon id={meetingType} className="size-3.5" />
|
|
507
|
+
<span>{MEETING_TYPE_LABELS[meetingType]}</span>
|
|
508
|
+
</div>
|
|
509
|
+
)}
|
|
510
|
+
{topic && (
|
|
511
|
+
<div className="flex items-center gap-2">
|
|
512
|
+
<MessageSquare className="size-3.5 shrink-0" />
|
|
513
|
+
<span>{topic}</span>
|
|
514
|
+
</div>
|
|
515
|
+
)}
|
|
516
|
+
{advisorName && (
|
|
517
|
+
<div className="flex items-center gap-2">
|
|
518
|
+
<User className="size-3.5 shrink-0" />
|
|
519
|
+
<span>with {advisorName}</span>
|
|
520
|
+
</div>
|
|
521
|
+
)}
|
|
522
|
+
</div>
|
|
523
|
+
</div>
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
// ChatWidgetWindow (Template — 3-screen container)
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
export interface ChatWidgetWindowProps {
|
|
535
|
+
isOpen: boolean;
|
|
536
|
+
brokerName?: string;
|
|
537
|
+
/** Solid brand color — sets --primary on the widget so all interactive elements match. */
|
|
538
|
+
brandColor?: string;
|
|
539
|
+
gradientFrom?: string;
|
|
540
|
+
gradientTo?: string;
|
|
541
|
+
onMinimize?: () => void;
|
|
542
|
+
// Screen 1
|
|
543
|
+
onIntakeSubmit?: (user: ChatWidgetUser) => void;
|
|
544
|
+
isSubmittingIntake?: boolean;
|
|
545
|
+
// Screen 2
|
|
546
|
+
topics?: ChatWidgetTopic[];
|
|
547
|
+
onTopicSelect?: (topicId: string) => void;
|
|
548
|
+
/** External booking URL. When provided and user selects "Book a Meeting", shows a booking screen instead of chat. */
|
|
549
|
+
bookingUrl?: string;
|
|
550
|
+
// Screen 3
|
|
551
|
+
messages?: ChatWidgetChatMessage[];
|
|
552
|
+
inputValue?: string;
|
|
553
|
+
onInputChange?: (value: string) => void;
|
|
554
|
+
onSend?: (value: string) => void;
|
|
555
|
+
isStreaming?: boolean;
|
|
556
|
+
onSlotSelect?: (slotId: string) => void;
|
|
557
|
+
onQuickReply?: (optionId: string) => void;
|
|
558
|
+
onMeetingTypeSelect?: (typeId: ChatWidgetMeetingTypeId) => void;
|
|
559
|
+
/** Override starting screen — used in stories to skip to a specific screen. */
|
|
560
|
+
initialScreen?: ChatWidgetScreen;
|
|
561
|
+
/** Override cached user — used in stories to pre-populate the user. */
|
|
562
|
+
initialUser?: ChatWidgetUser;
|
|
563
|
+
className?: string;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export function ChatWidgetWindow({
|
|
567
|
+
isOpen,
|
|
568
|
+
brokerName = "Your Broker",
|
|
569
|
+
brandColor,
|
|
570
|
+
gradientFrom,
|
|
571
|
+
gradientTo,
|
|
572
|
+
onMinimize,
|
|
573
|
+
onIntakeSubmit,
|
|
574
|
+
isSubmittingIntake,
|
|
575
|
+
topics,
|
|
576
|
+
onTopicSelect,
|
|
577
|
+
bookingUrl,
|
|
578
|
+
messages = [],
|
|
579
|
+
inputValue = "",
|
|
580
|
+
onInputChange,
|
|
581
|
+
onSend,
|
|
582
|
+
isStreaming,
|
|
583
|
+
onSlotSelect,
|
|
584
|
+
onQuickReply,
|
|
585
|
+
onMeetingTypeSelect,
|
|
586
|
+
initialScreen,
|
|
587
|
+
initialUser,
|
|
588
|
+
className,
|
|
589
|
+
}: ChatWidgetWindowProps) {
|
|
590
|
+
const [screen, setScreen] = useState<ChatWidgetScreen>(
|
|
591
|
+
() => initialScreen ?? "topics",
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
const [user, setUser] = useState<ChatWidgetUser | null>(
|
|
595
|
+
() => initialUser ?? null,
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
const handleIntakeSubmit = useCallback(
|
|
599
|
+
(data: ChatWidgetUser) => {
|
|
600
|
+
setUser(data);
|
|
601
|
+
setScreen("topics");
|
|
602
|
+
onIntakeSubmit?.(data);
|
|
603
|
+
},
|
|
604
|
+
[onIntakeSubmit],
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
const handleTopicSelect = useCallback(
|
|
608
|
+
(topicId: string) => {
|
|
609
|
+
setScreen(topicId === "book_meeting" && bookingUrl ? "booking" : "chat");
|
|
610
|
+
onTopicSelect?.(topicId);
|
|
611
|
+
},
|
|
612
|
+
[onTopicSelect, bookingUrl],
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
if (!isOpen) return null;
|
|
616
|
+
|
|
617
|
+
const showBackBar = screen === "chat" || screen === "booking";
|
|
618
|
+
|
|
619
|
+
return (
|
|
620
|
+
<div
|
|
621
|
+
className={cn(
|
|
622
|
+
"flex h-[600px] w-[360px] flex-col overflow-hidden border border-border bg-background shadow-xl",
|
|
623
|
+
className,
|
|
624
|
+
)}
|
|
625
|
+
style={
|
|
626
|
+
brandColor
|
|
627
|
+
? ({ "--primary": brandColor } as React.CSSProperties)
|
|
628
|
+
: undefined
|
|
629
|
+
}
|
|
630
|
+
>
|
|
631
|
+
<ChatWidgetHeader
|
|
632
|
+
brokerName={brokerName}
|
|
633
|
+
gradientFrom={gradientFrom}
|
|
634
|
+
gradientTo={gradientTo}
|
|
635
|
+
onMinimize={onMinimize}
|
|
636
|
+
/>
|
|
637
|
+
|
|
638
|
+
<div className="flex-1 overflow-y-auto" tabIndex={0}>
|
|
639
|
+
{screen === "intake" && (
|
|
640
|
+
<ChatWidgetIntakeForm
|
|
641
|
+
brokerName={brokerName}
|
|
642
|
+
onSubmit={handleIntakeSubmit}
|
|
643
|
+
isSubmitting={isSubmittingIntake}
|
|
644
|
+
/>
|
|
645
|
+
)}
|
|
646
|
+
{screen === "topics" && (
|
|
647
|
+
<ChatWidgetTopicGrid
|
|
648
|
+
userName={user?.name ?? "there"}
|
|
649
|
+
topics={topics}
|
|
650
|
+
onTopicSelect={handleTopicSelect}
|
|
651
|
+
/>
|
|
652
|
+
)}
|
|
653
|
+
{screen === "chat" && (
|
|
654
|
+
<div className="flex flex-col gap-3 p-4">
|
|
655
|
+
{messages.map((msg) => (
|
|
656
|
+
<React.Fragment key={msg.id}>
|
|
657
|
+
{(msg.content || msg.isStreaming) && (
|
|
658
|
+
<ChatWidgetMessage
|
|
659
|
+
role={msg.role}
|
|
660
|
+
content={msg.content}
|
|
661
|
+
timestamp={msg.timestamp}
|
|
662
|
+
isStreaming={msg.isStreaming}
|
|
663
|
+
/>
|
|
664
|
+
)}
|
|
665
|
+
{msg.interactiveCard && (
|
|
666
|
+
<ChatWidgetInteractiveCard
|
|
667
|
+
{...msg.interactiveCard}
|
|
668
|
+
onSlotSelect={onSlotSelect}
|
|
669
|
+
onQuickReply={onQuickReply}
|
|
670
|
+
onMeetingTypeSelect={onMeetingTypeSelect}
|
|
671
|
+
/>
|
|
672
|
+
)}
|
|
673
|
+
</React.Fragment>
|
|
674
|
+
))}
|
|
675
|
+
</div>
|
|
676
|
+
)}
|
|
677
|
+
{screen === "booking" && bookingUrl && (
|
|
678
|
+
<div className="flex flex-col items-center gap-5 px-6 py-10 text-center">
|
|
679
|
+
<div className="flex size-14 items-center justify-center bg-primary/10">
|
|
680
|
+
<CalendarDays className="size-7 text-foreground" />
|
|
681
|
+
</div>
|
|
682
|
+
<div className="flex flex-col gap-1">
|
|
683
|
+
<p className="text-body-large font-semibold text-foreground">
|
|
684
|
+
Book an Appointment
|
|
685
|
+
</p>
|
|
686
|
+
<p className="text-body-medium text-muted-foreground">
|
|
687
|
+
Choose a time that works for you with one of our advisors.
|
|
688
|
+
</p>
|
|
689
|
+
</div>
|
|
690
|
+
<Button
|
|
691
|
+
variant="default"
|
|
692
|
+
className="w-full"
|
|
693
|
+
onClick={() => window.open(bookingUrl, "_blank")}
|
|
694
|
+
>
|
|
695
|
+
Open Booking Page
|
|
696
|
+
<ExternalLink className="size-4" />
|
|
697
|
+
</Button>
|
|
698
|
+
</div>
|
|
699
|
+
)}
|
|
700
|
+
</div>
|
|
701
|
+
|
|
702
|
+
{showBackBar && (
|
|
703
|
+
<div className="border-t border-border px-4 py-1.5 text-center">
|
|
704
|
+
<Button variant="ghost" size="sm" onClick={() => setScreen("topics")}>
|
|
705
|
+
<ArrowLeft className="size-3" />
|
|
706
|
+
Change topic
|
|
707
|
+
</Button>
|
|
708
|
+
</div>
|
|
709
|
+
)}
|
|
710
|
+
{screen === "chat" && onInputChange && onSend && (
|
|
711
|
+
<ChatWidgetInputBar
|
|
712
|
+
value={inputValue}
|
|
713
|
+
onChange={onInputChange}
|
|
714
|
+
onSend={onSend}
|
|
715
|
+
disabled={isStreaming}
|
|
716
|
+
/>
|
|
717
|
+
)}
|
|
718
|
+
</div>
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// ---------------------------------------------------------------------------
|
|
723
|
+
// ChatWidget (Root orchestrator)
|
|
724
|
+
// ---------------------------------------------------------------------------
|
|
725
|
+
|
|
726
|
+
export interface ChatWidgetProps {
|
|
727
|
+
brokerName?: string;
|
|
728
|
+
/** Primary color for the FAB launcher button. */
|
|
729
|
+
brandColor?: string;
|
|
730
|
+
gradientFrom?: string;
|
|
731
|
+
gradientTo?: string;
|
|
732
|
+
/** ID used to connect to the correct AI agent. */
|
|
733
|
+
agentId?: string;
|
|
734
|
+
/** External booking URL. When provided, "Book a Meeting" shows a direct booking screen instead of chat. */
|
|
735
|
+
bookingUrl?: string;
|
|
736
|
+
position?: "bottom-right" | "bottom-left";
|
|
737
|
+
/** Opens the widget by default — useful for stories. */
|
|
738
|
+
defaultOpen?: boolean;
|
|
739
|
+
/** Callback when a topic is selected. */
|
|
740
|
+
onTopicSelected?: (topicId: string) => void;
|
|
741
|
+
/** Callback when the user sends a message. */
|
|
742
|
+
onMessageSent?: (message: string) => void;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export function ChatWidget({
|
|
746
|
+
brokerName = "Your Broker",
|
|
747
|
+
brandColor,
|
|
748
|
+
gradientFrom,
|
|
749
|
+
gradientTo,
|
|
750
|
+
bookingUrl,
|
|
751
|
+
position = "bottom-right",
|
|
752
|
+
defaultOpen = false,
|
|
753
|
+
onTopicSelected,
|
|
754
|
+
onMessageSent,
|
|
755
|
+
}: ChatWidgetProps) {
|
|
756
|
+
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
757
|
+
const [messages, setMessages] = useState<ChatWidgetChatMessage[]>([]);
|
|
758
|
+
const [inputValue, setInputValue] = useState("");
|
|
759
|
+
|
|
760
|
+
const handleTopicSelect = useCallback(
|
|
761
|
+
(topicId: string) => {
|
|
762
|
+
if (topicId !== "book_meeting" || !bookingUrl) {
|
|
763
|
+
const topic = DEFAULT_CHAT_WIDGET_TOPICS.find((t) => t.id === topicId);
|
|
764
|
+
if (topic) {
|
|
765
|
+
setMessages([
|
|
766
|
+
{
|
|
767
|
+
id: "welcome",
|
|
768
|
+
role: "bot",
|
|
769
|
+
content: `Great choice! I can help you with ${topic.label.toLowerCase()}. What would you like to know?`,
|
|
770
|
+
},
|
|
771
|
+
]);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
onTopicSelected?.(topicId);
|
|
775
|
+
},
|
|
776
|
+
[onTopicSelected, bookingUrl],
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
const handleSend = useCallback(
|
|
780
|
+
(value: string) => {
|
|
781
|
+
if (!value.trim()) return;
|
|
782
|
+
setMessages((prev) => [
|
|
783
|
+
...prev,
|
|
784
|
+
{ id: `msg-${Date.now()}`, role: "user", content: value },
|
|
785
|
+
]);
|
|
786
|
+
setInputValue("");
|
|
787
|
+
onMessageSent?.(value);
|
|
788
|
+
},
|
|
789
|
+
[onMessageSent],
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
return (
|
|
793
|
+
<div
|
|
794
|
+
className={cn(
|
|
795
|
+
"fixed bottom-6 z-50 flex flex-col items-end gap-3",
|
|
796
|
+
position === "bottom-right" ? "right-6" : "left-6",
|
|
797
|
+
)}
|
|
798
|
+
>
|
|
799
|
+
{isOpen && (
|
|
800
|
+
<ChatWidgetWindow
|
|
801
|
+
isOpen={isOpen}
|
|
802
|
+
brokerName={brokerName}
|
|
803
|
+
brandColor={brandColor}
|
|
804
|
+
gradientFrom={gradientFrom}
|
|
805
|
+
gradientTo={gradientTo}
|
|
806
|
+
bookingUrl={bookingUrl}
|
|
807
|
+
onMinimize={() => setIsOpen(false)}
|
|
808
|
+
messages={messages}
|
|
809
|
+
inputValue={inputValue}
|
|
810
|
+
onInputChange={setInputValue}
|
|
811
|
+
onSend={handleSend}
|
|
812
|
+
onTopicSelect={handleTopicSelect}
|
|
813
|
+
/>
|
|
814
|
+
)}
|
|
815
|
+
<ChatWidgetLauncher
|
|
816
|
+
isOpen={isOpen}
|
|
817
|
+
onClick={() => setIsOpen((prev) => !prev)}
|
|
818
|
+
brandColor={brandColor}
|
|
819
|
+
/>
|
|
820
|
+
</div>
|
|
821
|
+
);
|
|
822
|
+
}
|