@wealthx/shadcn 1.3.2 → 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.
Files changed (313) hide show
  1. package/.turbo/turbo-build.log +262 -226
  2. package/CHANGELOG.md +6 -0
  3. package/dist/{chunk-2UM72RJ7.mjs → chunk-2D3HQPFN.mjs} +12 -10
  4. package/dist/chunk-2EM2FRU6.mjs +613 -0
  5. package/dist/{chunk-FH6QVUVZ.mjs → chunk-2GIYVERS.mjs} +2 -2
  6. package/dist/chunk-2P7HP7LR.mjs +68 -0
  7. package/dist/{chunk-HISNT2MG.mjs → chunk-37AE3OM5.mjs} +5 -5
  8. package/dist/{chunk-HBZLGDIN.mjs → chunk-3ERBUVHC.mjs} +169 -110
  9. package/dist/{chunk-C7CQJNMR.mjs → chunk-3VDET466.mjs} +2 -2
  10. package/dist/{chunk-462HMNO4.mjs → chunk-4MM7LHM5.mjs} +2 -2
  11. package/dist/{chunk-QMY3AZJH.mjs → chunk-4Z66LMIQ.mjs} +2 -2
  12. package/dist/{chunk-U5X52X37.mjs → chunk-57ZXILTS.mjs} +6 -6
  13. package/dist/{chunk-3OYFOX3X.mjs → chunk-5VOTTIXF.mjs} +2 -2
  14. package/dist/{chunk-LBMRIB3G.mjs → chunk-6AJUS7VX.mjs} +1 -1
  15. package/dist/{chunk-OODBHKG7.mjs → chunk-6HIOM2HL.mjs} +7 -4
  16. package/dist/{chunk-BDYZCBRT.mjs → chunk-6QAFGZC2.mjs} +2 -2
  17. package/dist/{chunk-U4NDAF2P.mjs → chunk-6TX73WG7.mjs} +1 -1
  18. package/dist/{chunk-GD4BJDJR.mjs → chunk-7BTFGCFC.mjs} +4 -4
  19. package/dist/{chunk-FAKPBKLT.mjs → chunk-7GWRPXHD.mjs} +4 -4
  20. package/dist/{chunk-NMOI6CQD.mjs → chunk-7YI3HEBH.mjs} +5 -5
  21. package/dist/{chunk-T4BJLT57.mjs → chunk-AE7MASLF.mjs} +5 -5
  22. package/dist/{chunk-VLQZANBF.mjs → chunk-AFML43VJ.mjs} +6 -1
  23. package/dist/chunk-BBXSNDS3.mjs +260 -0
  24. package/dist/chunk-BOW7U26Y.mjs +203 -0
  25. package/dist/{chunk-34NWQURD.mjs → chunk-BS75ICOO.mjs} +2 -2
  26. package/dist/chunk-D2NSIIXG.mjs +394 -0
  27. package/dist/{chunk-3GF7OVTP.mjs → chunk-DGNHGNYH.mjs} +2 -2
  28. package/dist/{chunk-VLARHE5V.mjs → chunk-DMXYRCHM.mjs} +6 -6
  29. package/dist/{chunk-OGOYQ7BG.mjs → chunk-DQB4EPIS.mjs} +1 -1
  30. package/dist/{chunk-MIZQHHUO.mjs → chunk-FL6DZFJK.mjs} +106 -38
  31. package/dist/{chunk-I3RZS7V2.mjs → chunk-FLL633WS.mjs} +19 -33
  32. package/dist/{chunk-PBL4OQV2.mjs → chunk-FTPBQVQ6.mjs} +4 -4
  33. package/dist/chunk-FYPSTTEJ.mjs +169 -0
  34. package/dist/{chunk-6O6KD7CE.mjs → chunk-G27TSQLQ.mjs} +6 -6
  35. package/dist/{chunk-66MI7Q4B.mjs → chunk-GT3RU6GA.mjs} +2 -2
  36. package/dist/{chunk-D6ID6M4V.mjs → chunk-GTAVSBDO.mjs} +2 -2
  37. package/dist/{chunk-24FUO7TD.mjs → chunk-H6NQTIF4.mjs} +2 -2
  38. package/dist/{chunk-7DHU4VGG.mjs → chunk-HK4HUQTV.mjs} +2 -2
  39. package/dist/chunk-I4KVSZCH.mjs +101 -0
  40. package/dist/{chunk-RGVKLTLH.mjs → chunk-IKXYTCSB.mjs} +2 -2
  41. package/dist/{chunk-Y6DWJSKZ.mjs → chunk-ISUA7DSB.mjs} +1 -1
  42. package/dist/{chunk-2A5RRQGG.mjs → chunk-JD3YWRNP.mjs} +10 -14
  43. package/dist/{chunk-J5UICVJS.mjs → chunk-JPGL36WQ.mjs} +2 -2
  44. package/dist/{chunk-7XJHLGUV.mjs → chunk-JTK6VJXY.mjs} +2 -2
  45. package/dist/{chunk-7YAU5CY6.mjs → chunk-JVMXMFBB.mjs} +2 -2
  46. package/dist/{chunk-IAE3F7DR.mjs → chunk-JZY6TNIS.mjs} +21 -21
  47. package/dist/{chunk-K5A5L6T2.mjs → chunk-K4KOD3KR.mjs} +12 -12
  48. package/dist/{chunk-MBON7YRJ.mjs → chunk-K5QV4TT6.mjs} +3 -3
  49. package/dist/{chunk-IHMFS7NZ.mjs → chunk-K5VHK7CM.mjs} +21 -21
  50. package/dist/{chunk-RJI6GKVF.mjs → chunk-KCWNDYPZ.mjs} +5 -5
  51. package/dist/{chunk-UFYSFDER.mjs → chunk-KFH36NKF.mjs} +1 -1
  52. package/dist/{chunk-EBXQWIYG.mjs → chunk-KLTACJ2G.mjs} +5 -5
  53. package/dist/{chunk-3TTACBDP.mjs → chunk-KWD6GANL.mjs} +4 -4
  54. package/dist/{chunk-IOJRDS6V.mjs → chunk-L4NSRQ3T.mjs} +218 -147
  55. package/dist/{chunk-GYMYRIZP.mjs → chunk-LBTHZSBT.mjs} +2 -2
  56. package/dist/{chunk-AMQZRHEZ.mjs → chunk-LQULK2E3.mjs} +5 -5
  57. package/dist/{chunk-YJG55G2H.mjs → chunk-LR6LHDP3.mjs} +5 -5
  58. package/dist/{chunk-7PV3IWCN.mjs → chunk-M4VYX2PV.mjs} +19 -1
  59. package/dist/{chunk-P76HMUI6.mjs → chunk-MDUKXXIL.mjs} +2 -2
  60. package/dist/{chunk-LV35NGVG.mjs → chunk-N6Q5IPKT.mjs} +9 -9
  61. package/dist/{chunk-DOEO3CDL.mjs → chunk-NB3ZL36B.mjs} +1 -1
  62. package/dist/{chunk-XREGSKX3.mjs → chunk-NOOEKOWY.mjs} +5 -5
  63. package/dist/{chunk-NL3ZO62D.mjs → chunk-NT4FX27K.mjs} +1 -1
  64. package/dist/{chunk-QZ4RE6NA.mjs → chunk-NTYQWVLI.mjs} +6 -6
  65. package/dist/{chunk-ERGGHC2V.mjs → chunk-OEOOYMC2.mjs} +2 -2
  66. package/dist/{chunk-4GAWMKMI.mjs → chunk-OIKBW2QD.mjs} +291 -54
  67. package/dist/{chunk-DUJTAXMH.mjs → chunk-OKTJFDPN.mjs} +6 -6
  68. package/dist/chunk-OLKMCXAR.mjs +1219 -0
  69. package/dist/{chunk-EI5F6FMT.mjs → chunk-OWFQSXVD.mjs} +3 -3
  70. package/dist/{chunk-6DZEXFNB.mjs → chunk-P2N2PEFY.mjs} +3 -3
  71. package/dist/{chunk-NSLMILBT.mjs → chunk-P7CEBZM6.mjs} +2 -2
  72. package/dist/{chunk-7S5AESZO.mjs → chunk-PNRUH7JY.mjs} +6 -6
  73. package/dist/{chunk-ZU4NV6RG.mjs → chunk-PNSYFE3K.mjs} +2 -2
  74. package/dist/{chunk-JKGDCQTZ.mjs → chunk-QTRSCVQ3.mjs} +5 -5
  75. package/dist/{chunk-ABFDMHOR.mjs → chunk-QX7IFQSF.mjs} +5 -5
  76. package/dist/{chunk-CFMQP5QS.mjs → chunk-QXKGOMUX.mjs} +6 -6
  77. package/dist/{chunk-NQPOYKAQ.mjs → chunk-R2ON6CAN.mjs} +2 -2
  78. package/dist/{chunk-DBHJ5KC3.mjs → chunk-R4HCRDU5.mjs} +1 -1
  79. package/dist/{chunk-EWRB4PAD.mjs → chunk-RCAOCHWA.mjs} +14 -14
  80. package/dist/{chunk-EFRENWEJ.mjs → chunk-RSUIPKGX.mjs} +2 -2
  81. package/dist/{chunk-DGHAXJBN.mjs → chunk-S2FKV4M5.mjs} +5 -5
  82. package/dist/{chunk-RGU7HOEC.mjs → chunk-SET2ANTY.mjs} +5 -7
  83. package/dist/chunk-SFH2NJEJ.mjs +47 -0
  84. package/dist/{chunk-6AW4KJHE.mjs → chunk-SIVYAI3M.mjs} +12 -12
  85. package/dist/{chunk-5FQIKDKP.mjs → chunk-THVO2N47.mjs} +8 -8
  86. package/dist/{chunk-JMHR3YGZ.mjs → chunk-TLAWKTSA.mjs} +3 -3
  87. package/dist/{chunk-HVY6KCCF.mjs → chunk-TOWTPLRC.mjs} +68 -72
  88. package/dist/{chunk-6JQFUE5I.mjs → chunk-UALR6JGV.mjs} +2 -2
  89. package/dist/{chunk-N6TNTQL6.mjs → chunk-UJZ4UHWI.mjs} +9 -11
  90. package/dist/{chunk-MARPPFOJ.mjs → chunk-UNACI2YK.mjs} +2 -2
  91. package/dist/{chunk-3NCUZIFP.mjs → chunk-V6XGXYCJ.mjs} +7 -7
  92. package/dist/chunk-VB5M6OZQ.mjs +57 -0
  93. package/dist/{chunk-5IS7G74I.mjs → chunk-VY5NEUP7.mjs} +6 -6
  94. package/dist/{chunk-JHJHG4GO.mjs → chunk-WE4YKBDE.mjs} +2 -2
  95. package/dist/{chunk-BKNFWEH2.mjs → chunk-WL6WVV47.mjs} +3 -3
  96. package/dist/{chunk-FWCSY2DS.mjs → chunk-WNQUEZJF.mjs} +22 -1
  97. package/dist/{chunk-2Y7YJKPE.mjs → chunk-WZ6UJCBL.mjs} +1 -1
  98. package/dist/{chunk-UMTOX62O.mjs → chunk-XYPW2XA5.mjs} +13 -10
  99. package/dist/chunk-Y2MTAVAK.mjs +34 -0
  100. package/dist/{chunk-6CR5N2JW.mjs → chunk-YCWLFG27.mjs} +6 -6
  101. package/dist/{chunk-PU4YZQXV.mjs → chunk-YE67AALL.mjs} +12 -12
  102. package/dist/{chunk-M3FV7LOK.mjs → chunk-YEWNFK5S.mjs} +6 -1
  103. package/dist/{chunk-R3VSPKNP.mjs → chunk-YIZHS72Z.mjs} +11 -12
  104. package/dist/{chunk-7PYJD5JI.mjs → chunk-ZEDMKQK2.mjs} +2 -2
  105. package/dist/{chunk-N2PT566P.mjs → chunk-ZFCDYW6N.mjs} +4 -4
  106. package/dist/chunk-ZGQIVGIN.mjs +57 -0
  107. package/dist/{chunk-Q2BGOAMG.mjs → chunk-ZKWXDQDG.mjs} +4 -4
  108. package/dist/{chunk-GHC7LLUX.mjs → chunk-ZOWL2L5J.mjs} +5 -5
  109. package/dist/components/ui/accordion.mjs +3 -3
  110. package/dist/components/ui/add-column-modal.js +2 -2
  111. package/dist/components/ui/add-column-modal.mjs +10 -10
  112. package/dist/components/ui/add-lead-modal.js +424 -82
  113. package/dist/components/ui/add-lead-modal.mjs +12 -9
  114. package/dist/components/ui/advisor-card.js +2 -2
  115. package/dist/components/ui/advisor-card.mjs +8 -8
  116. package/dist/components/ui/ai-assistant-drawer.js +2 -2
  117. package/dist/components/ui/ai-assistant-drawer.mjs +9 -9
  118. package/dist/components/ui/ai-builder.js +958 -0
  119. package/dist/components/ui/ai-builder.mjs +25 -0
  120. package/dist/components/ui/ai-conversations.js +2045 -0
  121. package/dist/components/ui/ai-conversations.mjs +41 -0
  122. package/dist/components/ui/alert-dialog.js +2 -2
  123. package/dist/components/ui/alert-dialog.mjs +5 -5
  124. package/dist/components/ui/alert.mjs +3 -3
  125. package/dist/components/ui/appointment-action-dialogs.js +19 -3
  126. package/dist/components/ui/appointment-action-dialogs.mjs +15 -14
  127. package/dist/components/ui/appointment-availability-settings.js +181 -111
  128. package/dist/components/ui/appointment-availability-settings.mjs +20 -18
  129. package/dist/components/ui/appointment-book-dialog.js +113 -24
  130. package/dist/components/ui/appointment-book-dialog.mjs +21 -20
  131. package/dist/components/ui/appointment-calendar-view.js +19 -3
  132. package/dist/components/ui/appointment-calendar-view.mjs +10 -9
  133. package/dist/components/ui/appointment-detail-sheet.js +19 -3
  134. package/dist/components/ui/appointment-detail-sheet.mjs +18 -17
  135. package/dist/components/ui/appointment-gmail-connect.js +49 -89
  136. package/dist/components/ui/appointment-gmail-connect.mjs +8 -9
  137. package/dist/components/ui/appointment-mini-card.js +2 -2
  138. package/dist/components/ui/appointment-mini-card.mjs +6 -6
  139. package/dist/components/ui/appointment-time-slot-picker.mjs +6 -6
  140. package/dist/components/ui/appointment-upcoming-card.js +19 -3
  141. package/dist/components/ui/appointment-upcoming-card.mjs +15 -14
  142. package/dist/components/ui/auth-logo.js +95 -0
  143. package/dist/components/ui/auth-logo.mjs +8 -0
  144. package/dist/components/ui/auth-page-layout.js +108 -0
  145. package/dist/components/ui/auth-page-layout.mjs +8 -0
  146. package/dist/components/ui/avatar.mjs +3 -3
  147. package/dist/components/ui/backoffice-alert-history-chart.js +2 -2
  148. package/dist/components/ui/backoffice-alert-history-chart.mjs +9 -9
  149. package/dist/components/ui/backoffice-alerts-chart.js +2 -2
  150. package/dist/components/ui/backoffice-alerts-chart.mjs +11 -11
  151. package/dist/components/ui/backoffice-connections-chart.js +2 -2
  152. package/dist/components/ui/backoffice-connections-chart.mjs +11 -11
  153. package/dist/components/ui/backoffice-contact-history-chart.js +2 -2
  154. package/dist/components/ui/backoffice-contact-history-chart.mjs +9 -9
  155. package/dist/components/ui/badge.mjs +4 -4
  156. package/dist/components/ui/borrowing-capacity-line-chart.js +145 -132
  157. package/dist/components/ui/borrowing-capacity-line-chart.mjs +9 -9
  158. package/dist/components/ui/button.js +2 -2
  159. package/dist/components/ui/button.mjs +4 -4
  160. package/dist/components/ui/calendar.js +17 -3
  161. package/dist/components/ui/calendar.mjs +6 -5
  162. package/dist/components/ui/card.mjs +3 -3
  163. package/dist/components/ui/cash-balance-line-chart.js +157 -152
  164. package/dist/components/ui/cash-balance-line-chart.mjs +9 -9
  165. package/dist/components/ui/cashflow-bar-chart.js +2 -2
  166. package/dist/components/ui/cashflow-bar-chart.mjs +9 -9
  167. package/dist/components/ui/chat-widget-primitives.js +573 -0
  168. package/dist/components/ui/chat-widget-primitives.mjs +21 -0
  169. package/dist/components/ui/chat-widget.js +1268 -0
  170. package/dist/components/ui/chat-widget.mjs +29 -0
  171. package/dist/components/ui/checkbox.mjs +3 -3
  172. package/dist/components/ui/chip.js +2 -2
  173. package/dist/components/ui/chip.mjs +6 -6
  174. package/dist/components/ui/color-picker.js +2 -2
  175. package/dist/components/ui/color-picker.mjs +7 -7
  176. package/dist/components/ui/combobox.mjs +3 -3
  177. package/dist/components/ui/data-table.js +2 -2
  178. package/dist/components/ui/data-table.mjs +12 -12
  179. package/dist/components/ui/date-picker.js +22 -6
  180. package/dist/components/ui/date-picker.mjs +9 -8
  181. package/dist/components/ui/dialog.js +2 -2
  182. package/dist/components/ui/dialog.mjs +5 -5
  183. package/dist/components/ui/document-checklist-template.js +630 -0
  184. package/dist/components/ui/document-checklist-template.mjs +15 -0
  185. package/dist/components/ui/drawer.js +2 -2
  186. package/dist/components/ui/drawer.mjs +3 -3
  187. package/dist/components/ui/dropdown-menu.mjs +3 -3
  188. package/dist/components/ui/empty.mjs +3 -3
  189. package/dist/components/ui/expense-bar-chart.js +2 -2
  190. package/dist/components/ui/expense-bar-chart.mjs +9 -9
  191. package/dist/components/ui/field.mjs +5 -5
  192. package/dist/components/ui/financial-cards.js +431 -291
  193. package/dist/components/ui/financial-cards.mjs +10 -9
  194. package/dist/components/ui/financial-drawers.js +4 -4
  195. package/dist/components/ui/financial-drawers.mjs +8 -8
  196. package/dist/components/ui/financial-primitives.mjs +3 -3
  197. package/dist/components/ui/financial-sections.js +8 -9
  198. package/dist/components/ui/financial-sections.mjs +12 -12
  199. package/dist/components/ui/form-primitives.mjs +8 -8
  200. package/dist/components/ui/income-bar-chart.js +2 -2
  201. package/dist/components/ui/income-bar-chart.mjs +9 -9
  202. package/dist/components/ui/input-group.js +2 -2
  203. package/dist/components/ui/input-group.mjs +7 -7
  204. package/dist/components/ui/input-otp.mjs +3 -3
  205. package/dist/components/ui/input.mjs +3 -3
  206. package/dist/components/ui/kanban-column.js +19 -23
  207. package/dist/components/ui/kanban-column.mjs +14 -14
  208. package/dist/components/ui/label.mjs +3 -3
  209. package/dist/components/ui/onboarding-layout.js +476 -0
  210. package/dist/components/ui/onboarding-layout.mjs +11 -0
  211. package/dist/components/ui/opportunity-card.js +2 -2
  212. package/dist/components/ui/opportunity-card.mjs +12 -12
  213. package/dist/components/ui/opportunity-edit-modals.js +22 -6
  214. package/dist/components/ui/opportunity-edit-modals.mjs +21 -20
  215. package/dist/components/ui/opportunity-summary-tab.js +991 -674
  216. package/dist/components/ui/opportunity-summary-tab.mjs +26 -26
  217. package/dist/components/ui/page-header.mjs +3 -3
  218. package/dist/components/ui/page-top-bar.mjs +3 -3
  219. package/dist/components/ui/pagination.js +2 -2
  220. package/dist/components/ui/pagination.mjs +6 -6
  221. package/dist/components/ui/password-strength-tooltip.js +197 -0
  222. package/dist/components/ui/password-strength-tooltip.mjs +11 -0
  223. package/dist/components/ui/pipeline-alerts.mjs +3 -3
  224. package/dist/components/ui/pipeline-board.js +19 -23
  225. package/dist/components/ui/pipeline-board.mjs +18 -18
  226. package/dist/components/ui/pipeline-chart.js +12 -6
  227. package/dist/components/ui/pipeline-chart.mjs +4 -3
  228. package/dist/components/ui/pipeline-dialogs.js +28 -12
  229. package/dist/components/ui/pipeline-dialogs.mjs +14 -13
  230. package/dist/components/ui/pipeline-primitives.mjs +6 -6
  231. package/dist/components/ui/popover.mjs +3 -3
  232. package/dist/components/ui/progress.mjs +3 -3
  233. package/dist/components/ui/property-cashflow-doughnut-chart.js +2 -2
  234. package/dist/components/ui/property-cashflow-doughnut-chart.mjs +9 -9
  235. package/dist/components/ui/property-debt-equity-doughnut-chart.js +2 -2
  236. package/dist/components/ui/property-debt-equity-doughnut-chart.mjs +9 -9
  237. package/dist/components/ui/property-mobile-estimate-line-chart.js +2 -2
  238. package/dist/components/ui/property-mobile-estimate-line-chart.mjs +9 -9
  239. package/dist/components/ui/radio-group.mjs +3 -3
  240. package/dist/components/ui/select.mjs +3 -3
  241. package/dist/components/ui/separator.mjs +3 -3
  242. package/dist/components/ui/sheet.mjs +3 -3
  243. package/dist/components/ui/sidebar-nav.js +6 -5
  244. package/dist/components/ui/sidebar-nav.mjs +7 -7
  245. package/dist/components/ui/skeleton.mjs +3 -3
  246. package/dist/components/ui/slider.mjs +3 -3
  247. package/dist/components/ui/sonner.mjs +2 -2
  248. package/dist/components/ui/spinner.mjs +3 -3
  249. package/dist/components/ui/stage-timeline.mjs +10 -10
  250. package/dist/components/ui/stepper.mjs +3 -3
  251. package/dist/components/ui/switch.mjs +3 -3
  252. package/dist/components/ui/table.mjs +3 -3
  253. package/dist/components/ui/tabs.mjs +3 -3
  254. package/dist/components/ui/textarea.mjs +3 -3
  255. package/dist/components/ui/toggle-group.mjs +4 -4
  256. package/dist/components/ui/toggle.mjs +3 -3
  257. package/dist/components/ui/tooltip.mjs +3 -3
  258. package/dist/components/ui/transactions-expense-categories-doughnut-chart.js +2 -2
  259. package/dist/components/ui/transactions-expense-categories-doughnut-chart.mjs +9 -9
  260. package/dist/components/ui/transactions-income-expense-bar-chart.js +2 -2
  261. package/dist/components/ui/transactions-income-expense-bar-chart.mjs +9 -9
  262. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.js +2 -2
  263. package/dist/components/ui/transactions-liabilities-breakdown-doughnut-chart.mjs +9 -9
  264. package/dist/components/ui/two-fa-setup-form.js +612 -0
  265. package/dist/components/ui/two-fa-setup-form.mjs +16 -0
  266. package/dist/components/ui/upload-card.js +187 -0
  267. package/dist/components/ui/upload-card.mjs +10 -0
  268. package/dist/components/ui/video-background.js +118 -0
  269. package/dist/components/ui/video-background.mjs +8 -0
  270. package/dist/index.js +12764 -9396
  271. package/dist/index.mjs +341 -245
  272. package/dist/lib/colors.mjs +1 -1
  273. package/dist/lib/theme-provider.mjs +1 -1
  274. package/dist/lib/typography.mjs +2 -2
  275. package/dist/lib/utils.js +8 -2
  276. package/dist/lib/utils.mjs +6 -4
  277. package/dist/styles.css +1 -1
  278. package/package.json +61 -1
  279. package/src/components/index.tsx +126 -1
  280. package/src/components/ui/add-lead-modal.tsx +101 -142
  281. package/src/components/ui/ai-builder.tsx +560 -0
  282. package/src/components/ui/ai-conversations.tsx +1690 -0
  283. package/src/components/ui/appointment-availability-settings.tsx +152 -101
  284. package/src/components/ui/appointment-book-dialog.tsx +138 -24
  285. package/src/components/ui/appointment-calendar-view.tsx +2 -3
  286. package/src/components/ui/appointment-gmail-connect.tsx +23 -42
  287. package/src/components/ui/auth-logo.tsx +50 -0
  288. package/src/components/ui/auth-page-layout.tsx +59 -0
  289. package/src/components/ui/borrowing-capacity-line-chart.tsx +10 -8
  290. package/src/components/ui/button.tsx +2 -2
  291. package/src/components/ui/calendar.tsx +2 -1
  292. package/src/components/ui/cash-balance-line-chart.tsx +10 -14
  293. package/src/components/ui/chart-shared.tsx +10 -0
  294. package/src/components/ui/chat-widget-primitives.tsx +336 -0
  295. package/src/components/ui/chat-widget.tsx +822 -0
  296. package/src/components/ui/document-checklist-template.tsx +264 -0
  297. package/src/components/ui/drawer.tsx +2 -2
  298. package/src/components/ui/financial-cards.tsx +176 -78
  299. package/src/components/ui/financial-drawers.tsx +2 -2
  300. package/src/components/ui/financial-sections.tsx +1 -1
  301. package/src/components/ui/kanban-column.tsx +2 -5
  302. package/src/components/ui/onboarding-layout.tsx +109 -0
  303. package/src/components/ui/opportunity-summary-tab.tsx +469 -142
  304. package/src/components/ui/password-strength-tooltip.tsx +70 -0
  305. package/src/components/ui/pipeline-chart.tsx +2 -6
  306. package/src/components/ui/sidebar-nav.tsx +1 -11
  307. package/src/components/ui/two-fa-setup-form.tsx +229 -0
  308. package/src/components/ui/upload-card.tsx +98 -0
  309. package/src/components/ui/video-background.tsx +55 -0
  310. package/src/lib/format-date.ts +26 -0
  311. package/src/lib/utils.ts +11 -0
  312. package/src/styles/styles-css.ts +1 -1
  313. package/tsup.config.ts +13 -0
@@ -0,0 +1,1690 @@
1
+ import React, { useState } from "react";
2
+ import {
3
+ Archive,
4
+ ArrowLeft,
5
+ Bot,
6
+ Briefcase,
7
+ Calendar,
8
+ CheckCircle2,
9
+ ChevronLeft,
10
+ ChevronRight,
11
+ Flag,
12
+ HelpCircle,
13
+ Lock,
14
+ Mail,
15
+ MapPin,
16
+ MessageSquare,
17
+ MoreHorizontal,
18
+ Phone,
19
+ PhoneCall,
20
+ Plus,
21
+ Search,
22
+ Send,
23
+ UserCheck,
24
+ UserPlus,
25
+ Video,
26
+ } from "lucide-react";
27
+ import { cn, getInitials } from "@/lib/utils";
28
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
29
+ import { Badge } from "@/components/ui/badge";
30
+ import { Button, buttonVariants } from "@/components/ui/button";
31
+ import { Input } from "@/components/ui/input";
32
+ import {
33
+ DropdownMenu,
34
+ DropdownMenuContent,
35
+ DropdownMenuItem,
36
+ DropdownMenuSeparator,
37
+ DropdownMenuTrigger,
38
+ } from "@/components/ui/dropdown-menu";
39
+ import {
40
+ Dialog,
41
+ DialogContent,
42
+ DialogDescription,
43
+ DialogFooter,
44
+ DialogHeader,
45
+ DialogTitle,
46
+ } from "@/components/ui/dialog";
47
+ import { Separator } from "@/components/ui/separator";
48
+ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
49
+ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
50
+ import { Textarea } from "@/components/ui/textarea";
51
+ import {
52
+ Tooltip,
53
+ TooltipContent,
54
+ TooltipProvider,
55
+ TooltipTrigger,
56
+ } from "@/components/ui/tooltip";
57
+
58
+ /**
59
+ * AI Conversations — WealthX Backoffice
60
+ *
61
+ * 3-panel inbox for managing AI + advisor conversations from website chatbot leads.
62
+ *
63
+ * Component hierarchy:
64
+ * Atom → ConversationStatusChip
65
+ * Molecule → ConversationListItem, ChatBubble
66
+ * Organism → ConversationList, ChatThread, ChatComposer, AICollectedDataSection, LeadInfoPanel
67
+ * Template → ConversationsPage
68
+ */
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Types
72
+ // ---------------------------------------------------------------------------
73
+
74
+ export type AiConvStatus =
75
+ | "ai-active"
76
+ | "manual"
77
+ | "needs-attention"
78
+ | "closed";
79
+
80
+ export type AiConvMessageRole = "bot" | "visitor" | "advisor" | "system";
81
+
82
+ export type AiConvFieldConfidence = "confirmed" | "estimated";
83
+
84
+ export type AiConvFilterTab =
85
+ | "all"
86
+ | "open"
87
+ | "ai-active"
88
+ | "needs-attention"
89
+ | "closed";
90
+
91
+ export type AiConvMode = "ai" | "manual";
92
+
93
+ export type AiConvMeetingType = "video" | "phone" | "in-person";
94
+
95
+ export interface AiConvContact {
96
+ id: string;
97
+ name: string;
98
+ email?: string;
99
+ phone?: string;
100
+ }
101
+
102
+ export interface AiConvAdvisor {
103
+ id: string;
104
+ name: string;
105
+ initials: string;
106
+ role?: string;
107
+ }
108
+
109
+ export interface AiConvListItemData {
110
+ id: string;
111
+ contact: AiConvContact;
112
+ status: AiConvStatus;
113
+ lastMessage: string;
114
+ lastMessageRole?: AiConvMessageRole;
115
+ timestamp: string;
116
+ unreadCount?: number;
117
+ /** Assigned advisor display name. */
118
+ assignedTo?: string;
119
+ }
120
+
121
+ export interface AiConvMessage {
122
+ id: string;
123
+ role: AiConvMessageRole;
124
+ content: string;
125
+ timestamp?: string;
126
+ /** Display name shown above the bubble. */
127
+ senderName?: string;
128
+ }
129
+
130
+ export interface AiConvDataField {
131
+ label: string;
132
+ value: string;
133
+ confidence: AiConvFieldConfidence;
134
+ }
135
+
136
+ export interface AiConvAppointmentData {
137
+ datetime: string;
138
+ meetingType: AiConvMeetingType;
139
+ status: "requested" | "confirmed" | "pending" | "cancelled";
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Internal helpers & shared constants
144
+ // ---------------------------------------------------------------------------
145
+
146
+ function displayContactName(name: string): string {
147
+ return name.trim() || "Website User";
148
+ }
149
+
150
+ /** Shared fixed height for all three panel top-bar sections so bottom dividers align. */
151
+ const PANEL_HEADER_HEIGHT = "h-[94px]";
152
+
153
+ const APPOINTMENT_STATUS_LABEL: Record<
154
+ AiConvAppointmentData["status"],
155
+ string
156
+ > = {
157
+ requested: "Lead requested",
158
+ confirmed: "Confirmed",
159
+ pending: "Pending confirmation",
160
+ cancelled: "Cancelled",
161
+ };
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // ConversationStatusChip
165
+ // ---------------------------------------------------------------------------
166
+
167
+ type BadgeVariant = "success" | "default" | "warning" | "secondary";
168
+
169
+ const STATUS_CONFIG: Record<
170
+ AiConvStatus,
171
+ { label: string; variant: BadgeVariant; dotClass: string }
172
+ > = {
173
+ "ai-active": {
174
+ label: "AI Active",
175
+ variant: "success",
176
+ dotClass: "bg-success",
177
+ },
178
+ manual: {
179
+ label: "Manual",
180
+ variant: "default",
181
+ dotClass: "bg-primary",
182
+ },
183
+ "needs-attention": {
184
+ label: "Needs Attention",
185
+ variant: "warning",
186
+ dotClass: "bg-warning",
187
+ },
188
+ closed: {
189
+ label: "Closed",
190
+ variant: "secondary",
191
+ dotClass: "bg-muted-foreground/50",
192
+ },
193
+ };
194
+
195
+ export interface ConversationStatusChipProps {
196
+ status: AiConvStatus;
197
+ showDot?: boolean;
198
+ className?: string;
199
+ }
200
+
201
+ export function ConversationStatusChip({
202
+ status,
203
+ showDot = false,
204
+ className,
205
+ }: ConversationStatusChipProps) {
206
+ const { label, variant, dotClass } = STATUS_CONFIG[status];
207
+ return (
208
+ <Badge variant={variant} className={className}>
209
+ {showDot && (
210
+ <span className={cn("size-1.5 shrink-0 rounded-full", dotClass)} />
211
+ )}
212
+ {label}
213
+ </Badge>
214
+ );
215
+ }
216
+
217
+ // ---------------------------------------------------------------------------
218
+ // ContactAvatar — internal
219
+ // ---------------------------------------------------------------------------
220
+
221
+ interface ContactAvatarProps {
222
+ name: string;
223
+ size?: "sm" | "md" | "lg";
224
+ className?: string;
225
+ }
226
+
227
+ function ContactAvatar({ name, size = "md", className }: ContactAvatarProps) {
228
+ const avatarSize = size === "sm" ? "sm" : size === "lg" ? "lg" : "default";
229
+ return (
230
+ <Avatar size={avatarSize} className={className}>
231
+ <AvatarFallback className="font-semibold">
232
+ {getInitials(name)}
233
+ </AvatarFallback>
234
+ </Avatar>
235
+ );
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // ConversationListItem
240
+ // ---------------------------------------------------------------------------
241
+
242
+ export interface ConversationListItemProps {
243
+ data: AiConvListItemData;
244
+ isActive?: boolean;
245
+ onClick?: (id: string) => void;
246
+ onRead?: (id: string) => void;
247
+ }
248
+
249
+ export function ConversationListItem({
250
+ data,
251
+ isActive,
252
+ onClick,
253
+ onRead,
254
+ }: ConversationListItemProps) {
255
+ return (
256
+ <button
257
+ type="button"
258
+ onClick={() => {
259
+ onClick?.(data.id);
260
+ onRead?.(data.id);
261
+ }}
262
+ className={cn(
263
+ "w-full flex items-start gap-3 px-3 py-3 text-left transition-colors",
264
+ "border-b border-border last:border-b-0",
265
+ isActive ? "bg-muted" : "hover:bg-muted/40",
266
+ )}
267
+ >
268
+ <ContactAvatar name={data.contact.name} />
269
+ <div className="min-w-0 flex-1">
270
+ {/* Row 1 — name + icons + timestamp */}
271
+ <div className="mb-1 flex items-center justify-between gap-2">
272
+ <span className="truncate text-sm font-semibold text-foreground">
273
+ {displayContactName(data.contact.name)}
274
+ </span>
275
+ <div className="flex shrink-0 items-center gap-1">
276
+ {data.status === "needs-attention" && (
277
+ <Flag className="size-3 text-warning-text" />
278
+ )}
279
+ <span className="whitespace-nowrap text-xs text-muted-foreground">
280
+ {data.timestamp}
281
+ </span>
282
+ </div>
283
+ </div>
284
+ {/* Row 2 — status chip + assigned advisor + unread badge */}
285
+ <div className="mb-1.5 flex items-center justify-between gap-2">
286
+ <div className="flex min-w-0 items-center gap-1.5">
287
+ <ConversationStatusChip status={data.status} showDot />
288
+ {data.assignedTo && (
289
+ <span className="truncate text-xs text-muted-foreground">
290
+ → {data.assignedTo}
291
+ </span>
292
+ )}
293
+ </div>
294
+ {data.unreadCount ? (
295
+ <Badge
296
+ variant="default"
297
+ className="size-4 shrink-0 justify-center px-0 text-[10px]"
298
+ >
299
+ {data.unreadCount}
300
+ </Badge>
301
+ ) : null}
302
+ </div>
303
+ {/* Row 3 — last message preview */}
304
+ <p className="line-clamp-2 text-sm leading-relaxed text-muted-foreground">
305
+ {data.lastMessageRole === "bot" ? (
306
+ <span className="font-medium text-foreground/60">AI: </span>
307
+ ) : data.lastMessageRole === "advisor" ? (
308
+ <span className="font-medium text-foreground/60">You: </span>
309
+ ) : data.lastMessageRole === "visitor" ? (
310
+ <span className="font-medium text-foreground/60">
311
+ {displayContactName(data.contact.name).split(" ")[0]}:{" "}
312
+ </span>
313
+ ) : null}
314
+ {data.lastMessage}
315
+ </p>
316
+ </div>
317
+ </button>
318
+ );
319
+ }
320
+
321
+ // ---------------------------------------------------------------------------
322
+ // ConversationList
323
+ // ---------------------------------------------------------------------------
324
+
325
+ function filterConversations(
326
+ conversations: AiConvListItemData[],
327
+ query: string,
328
+ filter: AiConvFilterTab,
329
+ ): AiConvListItemData[] {
330
+ const q = query.toLowerCase();
331
+ return conversations.filter((c) => {
332
+ const matchesFilter =
333
+ filter === "all" ||
334
+ (filter === "open" ? c.status !== "closed" : c.status === filter);
335
+ const matchesSearch =
336
+ !q ||
337
+ c.contact.name.toLowerCase().includes(q) ||
338
+ c.lastMessage.toLowerCase().includes(q);
339
+ return matchesFilter && matchesSearch;
340
+ });
341
+ }
342
+
343
+ const FILTER_TABS: { id: AiConvFilterTab; label: string }[] = [
344
+ { id: "all", label: "All" },
345
+ { id: "open", label: "Open" },
346
+ { id: "ai-active", label: "AI Active" },
347
+ { id: "needs-attention", label: "Urgent" },
348
+ { id: "closed", label: "Archived" },
349
+ ];
350
+
351
+ export interface ConversationListProps {
352
+ conversations: AiConvListItemData[];
353
+ activeId?: string;
354
+ searchQuery?: string;
355
+ activeFilter?: AiConvFilterTab;
356
+ hasMore?: boolean;
357
+ isLoadingMore?: boolean;
358
+ onSearchChange?: (v: string) => void;
359
+ onFilterChange?: (f: AiConvFilterTab) => void;
360
+ onSelect?: (id: string) => void;
361
+ onRead?: (id: string) => void;
362
+ onLoadMore?: () => void;
363
+ className?: string;
364
+ }
365
+
366
+ export function ConversationList({
367
+ conversations,
368
+ activeId,
369
+ searchQuery = "",
370
+ activeFilter = "all",
371
+ hasMore,
372
+ isLoadingMore,
373
+ onSearchChange,
374
+ onFilterChange,
375
+ onSelect,
376
+ onRead,
377
+ onLoadMore,
378
+ className,
379
+ }: ConversationListProps) {
380
+ return (
381
+ <div
382
+ className={cn(
383
+ "flex flex-col border-r border-border bg-background",
384
+ className,
385
+ )}
386
+ >
387
+ <div className={cn(PANEL_HEADER_HEIGHT, "flex shrink-0 flex-col")}>
388
+ {/* Search */}
389
+ <div className="border-b border-border p-3 shrink-0">
390
+ <div className="relative">
391
+ <Search className="absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
392
+ <Input
393
+ value={searchQuery}
394
+ onChange={(e) => onSearchChange?.(e.target.value)}
395
+ placeholder="Search conversations..."
396
+ className="h-8 pl-8 text-sm"
397
+ />
398
+ </div>
399
+ </div>
400
+
401
+ {/* Filter tabs */}
402
+ <div className="flex flex-1 items-center border-b border-border">
403
+ <Tabs
404
+ value={activeFilter}
405
+ onValueChange={(v) => v && onFilterChange?.(v as AiConvFilterTab)}
406
+ className="w-full"
407
+ >
408
+ <TabsList
409
+ variant="line"
410
+ className="w-full justify-start gap-0 h-auto"
411
+ >
412
+ {FILTER_TABS.map((tab) => (
413
+ <TabsTrigger
414
+ key={tab.id}
415
+ value={tab.id}
416
+ className="flex-none px-3 py-2 text-xs"
417
+ >
418
+ {tab.label}
419
+ </TabsTrigger>
420
+ ))}
421
+ </TabsList>
422
+ </Tabs>
423
+ </div>
424
+ </div>
425
+
426
+ {/* List */}
427
+ <div className="flex-1 overflow-y-auto" tabIndex={0}>
428
+ {(() => {
429
+ const filtered = filterConversations(
430
+ conversations,
431
+ searchQuery,
432
+ activeFilter,
433
+ );
434
+ return filtered.length === 0 ? (
435
+ <div className="flex flex-col items-center justify-center gap-2 p-8 text-muted-foreground">
436
+ <MessageSquare className="size-8 opacity-30" />
437
+ <p className="text-sm">No conversations</p>
438
+ {searchQuery && (
439
+ <Button
440
+ variant="outline"
441
+ size="sm"
442
+ onClick={() => onSearchChange?.("")}
443
+ >
444
+ Clear search
445
+ </Button>
446
+ )}
447
+ {!searchQuery && activeFilter !== "all" && (
448
+ <Button
449
+ variant="outline"
450
+ size="sm"
451
+ onClick={() => onFilterChange?.("all")}
452
+ >
453
+ Clear filter
454
+ </Button>
455
+ )}
456
+ </div>
457
+ ) : (
458
+ <>
459
+ {filtered.map((item) => (
460
+ <ConversationListItem
461
+ key={item.id}
462
+ data={item}
463
+ isActive={activeId === item.id}
464
+ onClick={onSelect}
465
+ onRead={onRead}
466
+ />
467
+ ))}
468
+ {hasMore && (
469
+ <div className="border-t border-border p-3">
470
+ <Button
471
+ variant="outline"
472
+ size="sm"
473
+ className="w-full"
474
+ disabled={isLoadingMore}
475
+ onClick={onLoadMore}
476
+ >
477
+ {isLoadingMore ? "Loading..." : "Load more"}
478
+ </Button>
479
+ </div>
480
+ )}
481
+ </>
482
+ );
483
+ })()}
484
+ </div>
485
+ </div>
486
+ );
487
+ }
488
+
489
+ // ---------------------------------------------------------------------------
490
+ // ChatBubble
491
+ // ---------------------------------------------------------------------------
492
+
493
+ export interface ChatBubbleProps {
494
+ message: AiConvMessage;
495
+ className?: string;
496
+ }
497
+
498
+ function BubbleAvatar({
499
+ role,
500
+ senderName,
501
+ }: {
502
+ role: AiConvMessageRole;
503
+ senderName?: string;
504
+ }) {
505
+ if (role === "bot") {
506
+ return (
507
+ <Avatar size="sm">
508
+ <AvatarFallback className="border border-border bg-muted">
509
+ <Bot className="size-3.5 text-muted-foreground" />
510
+ </AvatarFallback>
511
+ </Avatar>
512
+ );
513
+ }
514
+ if (role === "advisor") {
515
+ return (
516
+ <Avatar size="sm">
517
+ <AvatarFallback className="font-semibold">
518
+ {getInitials(senderName ?? "Advisor")}
519
+ </AvatarFallback>
520
+ </Avatar>
521
+ );
522
+ }
523
+ return (
524
+ <Avatar size="sm">
525
+ <AvatarFallback>{getInitials(senderName ?? "?")}</AvatarFallback>
526
+ </Avatar>
527
+ );
528
+ }
529
+
530
+ export function ChatBubble({ message, className }: ChatBubbleProps) {
531
+ const { role, content, timestamp, senderName } = message;
532
+
533
+ if (role === "system") {
534
+ return (
535
+ <div className={cn("my-2 flex items-center gap-3 px-2", className)}>
536
+ <Separator className="flex-1" />
537
+ <span className="shrink-0 text-caption text-muted-foreground">
538
+ {content}
539
+ </span>
540
+ <Separator className="flex-1" />
541
+ </div>
542
+ );
543
+ }
544
+
545
+ const isAdvisor = role === "advisor";
546
+ const isBot = role === "bot";
547
+ const isVisitor = role === "visitor";
548
+
549
+ const displayName = isBot
550
+ ? "AI Assistant"
551
+ : isAdvisor
552
+ ? (senderName ?? "Advisor")
553
+ : (senderName ?? "Lead");
554
+
555
+ return (
556
+ <div
557
+ className={cn(
558
+ "flex gap-2.5",
559
+ isAdvisor ? "flex-row-reverse" : "flex-row",
560
+ className,
561
+ )}
562
+ >
563
+ <BubbleAvatar role={role} senderName={senderName} />
564
+
565
+ {/* Bubble + label */}
566
+ <div
567
+ className={cn(
568
+ "flex max-w-[70%] flex-col gap-1",
569
+ isAdvisor && "items-end",
570
+ )}
571
+ >
572
+ {/* Sender label — always shown for context */}
573
+ <span className="text-caption text-muted-foreground">
574
+ {displayName}
575
+ </span>
576
+
577
+ {/* Bubble */}
578
+ <div
579
+ className={cn(
580
+ "px-3 py-2 text-sm leading-relaxed",
581
+ isBot && "border border-border bg-muted/60 text-foreground",
582
+ isVisitor && "border border-border bg-background text-foreground",
583
+ isAdvisor && "bg-primary text-primary-foreground",
584
+ )}
585
+ >
586
+ {content}
587
+ </div>
588
+
589
+ {timestamp && (
590
+ <span className="text-caption text-muted-foreground">
591
+ {timestamp}
592
+ </span>
593
+ )}
594
+ </div>
595
+ </div>
596
+ );
597
+ }
598
+
599
+ // ---------------------------------------------------------------------------
600
+ // ChatComposer
601
+ // ---------------------------------------------------------------------------
602
+
603
+ export interface ChatComposerProps {
604
+ mode: AiConvMode;
605
+ inputValue?: string;
606
+ onInputChange?: (v: string) => void;
607
+ onSend?: (v: string) => void;
608
+ onTakeOver?: () => void;
609
+ onLetAiHandle?: () => void;
610
+ className?: string;
611
+ }
612
+
613
+ export function ChatComposer({
614
+ mode,
615
+ inputValue = "",
616
+ onInputChange,
617
+ onSend,
618
+ onTakeOver,
619
+ onLetAiHandle,
620
+ className,
621
+ }: ChatComposerProps) {
622
+ // AI mode — informational banner only; primary Take Over button is in the thread header
623
+ if (mode === "ai") {
624
+ return (
625
+ <div
626
+ className={cn(
627
+ "flex items-center gap-2 border-t border-border bg-muted/30 px-4 py-2.5 text-[12px] text-muted-foreground",
628
+ className,
629
+ )}
630
+ >
631
+ <Bot className="size-3.5 shrink-0 text-muted-foreground" />
632
+ <span>AI is handling this conversation.</span>
633
+ <Button
634
+ variant="link"
635
+ size="sm"
636
+ className="h-auto p-0 text-[12px] font-medium text-foreground"
637
+ onClick={onTakeOver}
638
+ >
639
+ Take Over
640
+ </Button>
641
+ <span>to reply directly.</span>
642
+ </div>
643
+ );
644
+ }
645
+
646
+ // Manual mode — full composer
647
+ return (
648
+ <div
649
+ className={cn(
650
+ "flex flex-col gap-2 border-t border-border bg-background p-3",
651
+ className,
652
+ )}
653
+ >
654
+ <Textarea
655
+ value={inputValue}
656
+ onChange={(e) => onInputChange?.(e.target.value)}
657
+ placeholder="Reply to lead..."
658
+ rows={3}
659
+ className="resize-none text-sm"
660
+ />
661
+ <div className="flex items-center justify-between">
662
+ <Button variant="outline" size="sm" onClick={onLetAiHandle}>
663
+ <Bot className="mr-1.5 size-3.5" />
664
+ Let AI Handle
665
+ </Button>
666
+ <Button
667
+ size="sm"
668
+ onClick={() => onSend?.(inputValue)}
669
+ disabled={!inputValue.trim()}
670
+ >
671
+ <Send className="mr-1.5 size-3.5" />
672
+ Send
673
+ </Button>
674
+ </div>
675
+ </div>
676
+ );
677
+ }
678
+
679
+ // ---------------------------------------------------------------------------
680
+ // ChatThread
681
+ // ---------------------------------------------------------------------------
682
+
683
+ export interface ChatThreadProps {
684
+ contact: AiConvContact;
685
+ status: AiConvStatus;
686
+ mode: AiConvMode;
687
+ messages: AiConvMessage[];
688
+ isAiTyping?: boolean;
689
+ inputValue?: string;
690
+ onInputChange?: (v: string) => void;
691
+ onSend?: (v: string) => void;
692
+ onTakeOver?: () => void;
693
+ onLetAiHandle?: () => void;
694
+ onReopen?: () => void;
695
+ onMarkUrgent?: () => void;
696
+ onUnmarkUrgent?: () => void;
697
+ onArchive?: () => void;
698
+ onAssignToAdvisor?: () => void;
699
+ /** Mobile only — back to conversation list. */
700
+ onBack?: () => void;
701
+ /** Mobile only — show lead info panel. */
702
+ onShowLeadInfo?: () => void;
703
+ className?: string;
704
+ }
705
+
706
+ export function ChatThread({
707
+ contact,
708
+ status,
709
+ mode,
710
+ messages,
711
+ isAiTyping = false,
712
+ inputValue,
713
+ onInputChange,
714
+ onSend,
715
+ onTakeOver,
716
+ onLetAiHandle,
717
+ onReopen,
718
+ onMarkUrgent,
719
+ onUnmarkUrgent,
720
+ onArchive,
721
+ onAssignToAdvisor,
722
+ onBack,
723
+ onShowLeadInfo,
724
+ className,
725
+ }: ChatThreadProps) {
726
+ const aiIsHandling = mode === "ai";
727
+ const isClosed = status === "closed";
728
+
729
+ return (
730
+ <div className={cn("flex flex-col bg-background", className)}>
731
+ {/* Header */}
732
+ <div
733
+ className={cn(
734
+ PANEL_HEADER_HEIGHT,
735
+ "flex items-center gap-3 border-b border-border px-4",
736
+ )}
737
+ >
738
+ {/* Mobile back button */}
739
+ {onBack && (
740
+ <Button
741
+ variant="ghost"
742
+ size="icon"
743
+ className="size-8 shrink-0 md:hidden"
744
+ onClick={onBack}
745
+ aria-label="Back to conversations"
746
+ >
747
+ <ArrowLeft className="size-4" />
748
+ </Button>
749
+ )}
750
+
751
+ <ContactAvatar name={contact.name} size="md" />
752
+
753
+ <div className="min-w-0 flex-1">
754
+ <div className="flex items-center gap-2">
755
+ <span className="text-sm font-semibold text-foreground">
756
+ {displayContactName(contact.name)}
757
+ </span>
758
+ <ConversationStatusChip status={status} showDot />
759
+ </div>
760
+ {contact.email && (
761
+ <p className="text-sm text-muted-foreground">{contact.email}</p>
762
+ )}
763
+ </div>
764
+
765
+ <div className="flex shrink-0 items-center gap-2">
766
+ {/* Action buttons — desktop only (mobile: already in bottom bar) */}
767
+ <div className="hidden items-center gap-2 md:flex">
768
+ {isClosed && (
769
+ <Button variant="outline" size="sm" onClick={onReopen}>
770
+ Reopen
771
+ </Button>
772
+ )}
773
+ {!isClosed && aiIsHandling && (
774
+ <Button size="sm" onClick={onTakeOver}>
775
+ Take Over
776
+ </Button>
777
+ )}
778
+ {!isClosed && !aiIsHandling && (
779
+ <Button variant="outline" size="sm" onClick={onLetAiHandle}>
780
+ <Bot className="mr-1.5 size-3.5" />
781
+ Let AI Handle
782
+ </Button>
783
+ )}
784
+ </div>
785
+
786
+ {/* More actions dropdown */}
787
+ <DropdownMenu>
788
+ <DropdownMenuTrigger
789
+ className={cn(
790
+ buttonVariants({ variant: "ghost", size: "icon" }),
791
+ "size-8",
792
+ )}
793
+ aria-label="More actions"
794
+ >
795
+ <MoreHorizontal className="size-4" />
796
+ </DropdownMenuTrigger>
797
+ <DropdownMenuContent>
798
+ {/* Lead Info — mobile only */}
799
+ {onShowLeadInfo && (
800
+ <>
801
+ <DropdownMenuItem
802
+ className="md:hidden"
803
+ onClick={onShowLeadInfo}
804
+ >
805
+ <ChevronRight className="mr-2 size-4" />
806
+ Lead Info
807
+ </DropdownMenuItem>
808
+ <DropdownMenuSeparator className="md:hidden" />
809
+ </>
810
+ )}
811
+ {status === "needs-attention" ? (
812
+ <DropdownMenuItem onClick={onUnmarkUrgent}>
813
+ <Flag className="mr-2 size-4" />
814
+ Unmark Urgent
815
+ </DropdownMenuItem>
816
+ ) : (
817
+ <DropdownMenuItem onClick={onMarkUrgent}>
818
+ <Flag className="mr-2 size-4" />
819
+ Mark as Urgent
820
+ </DropdownMenuItem>
821
+ )}
822
+ <DropdownMenuItem onClick={onAssignToAdvisor}>
823
+ <UserCheck className="mr-2 size-4" />
824
+ Assign to advisor
825
+ </DropdownMenuItem>
826
+ <DropdownMenuSeparator />
827
+ <DropdownMenuItem onClick={onArchive}>
828
+ <Archive className="mr-2 size-4" />
829
+ Archive
830
+ </DropdownMenuItem>
831
+ </DropdownMenuContent>
832
+ </DropdownMenu>
833
+ </div>
834
+ </div>
835
+
836
+ {/* Messages */}
837
+ <div
838
+ className="flex flex-1 flex-col gap-4 overflow-y-auto p-4"
839
+ tabIndex={0}
840
+ >
841
+ {messages.length === 0 ? (
842
+ <div className="flex flex-1 flex-col items-center justify-center gap-2 text-muted-foreground">
843
+ <MessageSquare className="size-8 opacity-30" />
844
+ <p className="text-sm">No messages yet</p>
845
+ </div>
846
+ ) : (
847
+ messages.map((msg) => <ChatBubble key={msg.id} message={msg} />)
848
+ )}
849
+
850
+ {/* AI typing indicator */}
851
+ {isAiTyping && !isClosed && (
852
+ <div className="flex gap-2.5">
853
+ <BubbleAvatar role="bot" />
854
+ <div className="flex flex-col gap-1">
855
+ <span className="text-caption text-muted-foreground">
856
+ AI Assistant
857
+ </span>
858
+ <div className="flex items-center gap-1 border border-border bg-muted/60 px-3 py-2.5">
859
+ <span className="size-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:0ms]" />
860
+ <span className="size-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:150ms]" />
861
+ <span className="size-1.5 animate-bounce rounded-full bg-muted-foreground [animation-delay:300ms]" />
862
+ </div>
863
+ </div>
864
+ </div>
865
+ )}
866
+ </div>
867
+
868
+ {/* Composer / locked banner */}
869
+ {isClosed ? (
870
+ <div className="flex items-center gap-3 border-t border-border bg-muted/30 px-4 py-3 text-sm text-muted-foreground">
871
+ <Lock className="size-3.5 shrink-0" />
872
+ <span>This conversation is closed.</span>
873
+ <Button
874
+ variant="outline"
875
+ size="sm"
876
+ className="ml-auto"
877
+ onClick={onReopen}
878
+ >
879
+ Reopen
880
+ </Button>
881
+ </div>
882
+ ) : (
883
+ <ChatComposer
884
+ mode={mode}
885
+ inputValue={inputValue}
886
+ onInputChange={onInputChange}
887
+ onSend={onSend}
888
+ onTakeOver={onTakeOver}
889
+ onLetAiHandle={onLetAiHandle}
890
+ />
891
+ )}
892
+ </div>
893
+ );
894
+ }
895
+
896
+ // ---------------------------------------------------------------------------
897
+ // AICollectedDataSection
898
+ // ---------------------------------------------------------------------------
899
+
900
+ export interface AICollectedDataSectionProps {
901
+ fields: AiConvDataField[];
902
+ className?: string;
903
+ }
904
+
905
+ export function AICollectedDataSection({
906
+ fields,
907
+ className,
908
+ }: AICollectedDataSectionProps) {
909
+ return (
910
+ <div className={cn("flex flex-col", className)}>
911
+ {fields.map((field, i) => (
912
+ <div
913
+ key={i}
914
+ className="flex items-center justify-between gap-2 border-b border-border/40 py-1.5 last:border-b-0"
915
+ >
916
+ <span className="shrink-0 text-sm text-muted-foreground">
917
+ {field.label}
918
+ </span>
919
+ <div className="flex items-center gap-1.5">
920
+ <span className="text-right text-sm font-medium text-foreground">
921
+ {field.value}
922
+ </span>
923
+ {field.confidence === "confirmed" ? (
924
+ <CheckCircle2 className="size-3 shrink-0 text-success-text" />
925
+ ) : (
926
+ <HelpCircle className="size-3 shrink-0 text-warning-text" />
927
+ )}
928
+ </div>
929
+ </div>
930
+ ))}
931
+ </div>
932
+ );
933
+ }
934
+
935
+ // ---------------------------------------------------------------------------
936
+ // LeadInfoPanel
937
+ // ---------------------------------------------------------------------------
938
+
939
+ const MEETING_ICON: Record<AiConvMeetingType, React.ElementType> = {
940
+ video: Video,
941
+ phone: Phone,
942
+ "in-person": MapPin,
943
+ };
944
+
945
+ const MEETING_LABEL: Record<AiConvMeetingType, string> = {
946
+ video: "Video Call",
947
+ phone: "Phone Call",
948
+ "in-person": "In Person",
949
+ };
950
+
951
+ interface AppointmentSectionProps {
952
+ appointment: AiConvAppointmentData;
953
+ contactId: string;
954
+ isAnonymous: boolean;
955
+ onApproveAppointment?: () => void;
956
+ onDeclineAppointment?: () => void;
957
+ onRescheduleAppointment?: (contactId: string) => void;
958
+ }
959
+
960
+ function AppointmentSection({
961
+ appointment,
962
+ contactId,
963
+ isAnonymous,
964
+ onApproveAppointment,
965
+ onDeclineAppointment,
966
+ onRescheduleAppointment,
967
+ }: AppointmentSectionProps) {
968
+ const AppointmentIcon = MEETING_ICON[appointment.meetingType];
969
+ const canReschedule = !isAnonymous && !!onRescheduleAppointment;
970
+
971
+ return (
972
+ <div className="flex flex-col gap-2">
973
+ <div className="flex items-center gap-2">
974
+ <Calendar className="size-3.5 shrink-0 text-muted-foreground" />
975
+ <span className="text-sm font-medium text-foreground">
976
+ {appointment.datetime}
977
+ </span>
978
+ </div>
979
+ <div className="flex items-center gap-2">
980
+ <AppointmentIcon className="size-4 shrink-0 text-muted-foreground" />
981
+ <span className="text-sm text-muted-foreground">
982
+ {MEETING_LABEL[appointment.meetingType]}
983
+ </span>
984
+ </div>
985
+ <span
986
+ className={cn("text-sm font-medium", {
987
+ "text-warning-text": appointment.status === "requested",
988
+ "text-success-text": appointment.status === "confirmed",
989
+ "text-muted-foreground": appointment.status === "pending",
990
+ "text-destructive line-through": appointment.status === "cancelled",
991
+ })}
992
+ >
993
+ {APPOINTMENT_STATUS_LABEL[appointment.status]}
994
+ </span>
995
+
996
+ {appointment.status === "requested" && (
997
+ <div className="flex gap-2 pt-1">
998
+ <Button size="sm" className="flex-1" onClick={onApproveAppointment}>
999
+ Approve
1000
+ </Button>
1001
+ <Button
1002
+ variant="outline"
1003
+ size="sm"
1004
+ className="flex-1"
1005
+ onClick={onDeclineAppointment}
1006
+ >
1007
+ Decline
1008
+ </Button>
1009
+ {canReschedule && (
1010
+ <Button
1011
+ variant="ghost"
1012
+ size="sm"
1013
+ className="flex-1"
1014
+ onClick={() => onRescheduleAppointment!(contactId)}
1015
+ >
1016
+ Reschedule
1017
+ </Button>
1018
+ )}
1019
+ </div>
1020
+ )}
1021
+
1022
+ {(appointment.status === "confirmed" ||
1023
+ appointment.status === "cancelled") &&
1024
+ canReschedule && (
1025
+ <Button
1026
+ variant="outline"
1027
+ size="sm"
1028
+ className="mt-1 w-full justify-start"
1029
+ onClick={() => onRescheduleAppointment!(contactId)}
1030
+ >
1031
+ Reschedule
1032
+ </Button>
1033
+ )}
1034
+ </div>
1035
+ );
1036
+ }
1037
+
1038
+ function PanelSectionHeader({ children }: { children: React.ReactNode }) {
1039
+ return (
1040
+ <p className="mb-2.5 text-overline text-muted-foreground">{children}</p>
1041
+ );
1042
+ }
1043
+
1044
+ function PanelSection({
1045
+ children,
1046
+ last = false,
1047
+ }: {
1048
+ children: React.ReactNode;
1049
+ last?: boolean;
1050
+ }) {
1051
+ return (
1052
+ <div className={cn("px-4 py-4", !last && "border-b border-border")}>
1053
+ {children}
1054
+ </div>
1055
+ );
1056
+ }
1057
+
1058
+ export interface LeadInfoPanelProps {
1059
+ contact: AiConvContact;
1060
+ firstSeen?: string;
1061
+ source?: string;
1062
+ aiFields?: AiConvDataField[];
1063
+ appointment?: AiConvAppointmentData;
1064
+ internalNotes?: string;
1065
+ notesSaveStatus?: "idle" | "saving" | "saved";
1066
+ isCollapsed?: boolean;
1067
+ /** True when this lead's contact info already exists in the system. Disables Add to Contacts. */
1068
+ isKnownContact?: boolean;
1069
+ onAddToContacts?: () => void;
1070
+ onCreateOpportunity?: () => void;
1071
+ onBookAppointment?: () => void;
1072
+ onApproveAppointment?: () => void;
1073
+ onDeclineAppointment?: () => void;
1074
+ onRescheduleAppointment?: (contactId: string) => void;
1075
+ onNotesChange?: (v: string) => void;
1076
+ onToggleCollapse?: () => void;
1077
+ /** Mobile only — back to chat thread. */
1078
+ onBack?: () => void;
1079
+ className?: string;
1080
+ }
1081
+
1082
+ export function LeadInfoPanel({
1083
+ contact,
1084
+ firstSeen,
1085
+ source = "Website Chatbot",
1086
+ aiFields = [],
1087
+ appointment,
1088
+ internalNotes = "",
1089
+ notesSaveStatus = "idle",
1090
+ isCollapsed = false,
1091
+ isKnownContact = false,
1092
+ onAddToContacts,
1093
+ onCreateOpportunity,
1094
+ onBookAppointment,
1095
+ onApproveAppointment,
1096
+ onDeclineAppointment,
1097
+ onRescheduleAppointment,
1098
+ onNotesChange,
1099
+ onToggleCollapse,
1100
+ onBack,
1101
+ className,
1102
+ }: LeadInfoPanelProps) {
1103
+ const isAnonymous = !contact.name.trim();
1104
+ const addToContactsDisabled = isAnonymous || isKnownContact;
1105
+
1106
+ return (
1107
+ <div className={cn("flex flex-col bg-background", className)}>
1108
+ {/* Panel header */}
1109
+ <div
1110
+ className={cn(
1111
+ PANEL_HEADER_HEIGHT,
1112
+ "flex items-center justify-between border-b border-border px-4",
1113
+ )}
1114
+ >
1115
+ {/* Mobile back button */}
1116
+ {onBack && (
1117
+ <Button
1118
+ variant="ghost"
1119
+ size="icon"
1120
+ className="size-8 shrink-0 md:hidden"
1121
+ onClick={onBack}
1122
+ aria-label="Back to conversation"
1123
+ >
1124
+ <ArrowLeft className="size-4" />
1125
+ </Button>
1126
+ )}
1127
+ <span className="text-sm font-semibold text-foreground">Lead Info</span>
1128
+ {onToggleCollapse && (
1129
+ <Tooltip>
1130
+ <TooltipTrigger
1131
+ render={
1132
+ <Button
1133
+ variant="ghost"
1134
+ size="icon"
1135
+ className="size-7"
1136
+ onClick={onToggleCollapse}
1137
+ aria-label={isCollapsed ? "Expand panel" : "Collapse panel"}
1138
+ >
1139
+ <ChevronRight
1140
+ className={cn(
1141
+ "size-4 transition-transform duration-150",
1142
+ isCollapsed && "-rotate-180",
1143
+ )}
1144
+ />
1145
+ </Button>
1146
+ }
1147
+ />
1148
+ <TooltipContent>
1149
+ {isCollapsed ? "Expand panel" : "Collapse panel"}
1150
+ </TooltipContent>
1151
+ </Tooltip>
1152
+ )}
1153
+ </div>
1154
+
1155
+ {!isCollapsed && (
1156
+ <div className="flex-1 overflow-y-auto" tabIndex={0}>
1157
+ <PanelSection>
1158
+ <PanelSectionHeader>Contact</PanelSectionHeader>
1159
+ <div className="flex items-center gap-3">
1160
+ <ContactAvatar name={contact.name} size="lg" />
1161
+ <div className="min-w-0 flex-1">
1162
+ <p className="truncate text-sm font-semibold text-foreground">
1163
+ {displayContactName(contact.name)}
1164
+ </p>
1165
+ <p className="text-sm text-muted-foreground">{source}</p>
1166
+ </div>
1167
+ </div>
1168
+ {(contact.email || contact.phone || firstSeen) && (
1169
+ <div className="mt-3 flex flex-col gap-1.5">
1170
+ {contact.email && (
1171
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
1172
+ <Mail className="size-4 shrink-0" />
1173
+ <a
1174
+ href={`mailto:${contact.email}`}
1175
+ className="truncate hover:underline"
1176
+ >
1177
+ {contact.email}
1178
+ </a>
1179
+ </div>
1180
+ )}
1181
+ {contact.phone && (
1182
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
1183
+ <PhoneCall className="size-4 shrink-0" />
1184
+ <a
1185
+ href={`tel:${contact.phone}`}
1186
+ className="hover:underline"
1187
+ >
1188
+ {contact.phone}
1189
+ </a>
1190
+ </div>
1191
+ )}
1192
+ {firstSeen && (
1193
+ <p className="text-sm text-muted-foreground">
1194
+ First seen: {firstSeen}
1195
+ </p>
1196
+ )}
1197
+ </div>
1198
+ )}
1199
+ </PanelSection>
1200
+
1201
+ <PanelSection>
1202
+ <PanelSectionHeader>AI-Collected Data</PanelSectionHeader>
1203
+ {aiFields.length > 0 ? (
1204
+ <>
1205
+ <AICollectedDataSection fields={aiFields} />
1206
+ <div className="mt-2.5 flex items-center gap-3 text-xs text-muted-foreground">
1207
+ <span className="flex items-center gap-1">
1208
+ <CheckCircle2 className="size-3 text-success-text" />
1209
+ confirmed
1210
+ </span>
1211
+ <span className="flex items-center gap-1">
1212
+ <HelpCircle className="size-3 text-warning-text" />
1213
+ estimated
1214
+ </span>
1215
+ </div>
1216
+ </>
1217
+ ) : (
1218
+ <p className="text-sm text-muted-foreground">
1219
+ AI is still gathering information...
1220
+ </p>
1221
+ )}
1222
+ </PanelSection>
1223
+
1224
+ <PanelSection>
1225
+ <PanelSectionHeader>Appointment</PanelSectionHeader>
1226
+ {appointment ? (
1227
+ <AppointmentSection
1228
+ appointment={appointment}
1229
+ contactId={contact.id}
1230
+ isAnonymous={isAnonymous}
1231
+ onApproveAppointment={onApproveAppointment}
1232
+ onDeclineAppointment={onDeclineAppointment}
1233
+ onRescheduleAppointment={onRescheduleAppointment}
1234
+ />
1235
+ ) : (
1236
+ <Button
1237
+ variant="outline"
1238
+ size="sm"
1239
+ className="w-full justify-start"
1240
+ disabled={isAnonymous}
1241
+ onClick={onBookAppointment}
1242
+ >
1243
+ <Plus className="mr-1.5 size-3.5" />
1244
+ Book Appointment
1245
+ </Button>
1246
+ )}
1247
+ </PanelSection>
1248
+
1249
+ <PanelSection>
1250
+ <PanelSectionHeader>CRM Actions</PanelSectionHeader>
1251
+ <div className="flex flex-col gap-2">
1252
+ <Tooltip>
1253
+ <TooltipTrigger
1254
+ render={
1255
+ <Button
1256
+ variant="outline"
1257
+ size="sm"
1258
+ className="w-full justify-start"
1259
+ disabled={addToContactsDisabled}
1260
+ onClick={onAddToContacts}
1261
+ >
1262
+ <UserPlus className="mr-1.5 size-3.5" />
1263
+ Add to Contacts
1264
+ </Button>
1265
+ }
1266
+ />
1267
+ {isKnownContact && (
1268
+ <TooltipContent>Already in contacts</TooltipContent>
1269
+ )}
1270
+ </Tooltip>
1271
+ <Button
1272
+ variant="outline"
1273
+ size="sm"
1274
+ className="w-full justify-start"
1275
+ disabled={isAnonymous}
1276
+ onClick={onCreateOpportunity}
1277
+ >
1278
+ <Briefcase className="mr-1.5 size-3.5" />
1279
+ Add to CRM
1280
+ </Button>
1281
+ </div>
1282
+ </PanelSection>
1283
+
1284
+ <PanelSection last>
1285
+ <div className="mb-2.5 flex items-center justify-between">
1286
+ <p className="text-overline text-muted-foreground">
1287
+ Internal Notes
1288
+ </p>
1289
+ {notesSaveStatus === "saving" && (
1290
+ <span className="text-xs text-muted-foreground">Saving...</span>
1291
+ )}
1292
+ {notesSaveStatus === "saved" && (
1293
+ <span className="flex items-center gap-1 text-xs text-success-text">
1294
+ <CheckCircle2 className="size-3" />
1295
+ Saved
1296
+ </span>
1297
+ )}
1298
+ </div>
1299
+ <Textarea
1300
+ value={internalNotes}
1301
+ onChange={(e) => onNotesChange?.(e.target.value)}
1302
+ placeholder="Private notes — not visible to lead..."
1303
+ rows={4}
1304
+ className="resize-none text-sm"
1305
+ />
1306
+ </PanelSection>
1307
+ </div>
1308
+ )}
1309
+ </div>
1310
+ );
1311
+ }
1312
+
1313
+ // ---------------------------------------------------------------------------
1314
+ // ConversationsPage
1315
+ // ---------------------------------------------------------------------------
1316
+
1317
+ export interface ConversationsPageProps {
1318
+ conversations: AiConvListItemData[];
1319
+ activeConversationId?: string;
1320
+ /** Contact for the currently-selected conversation. */
1321
+ contact?: AiConvContact;
1322
+ status?: AiConvStatus;
1323
+ mode?: AiConvMode;
1324
+ messages?: AiConvMessage[];
1325
+ aiFields?: AiConvDataField[];
1326
+ appointment?: AiConvAppointmentData;
1327
+ /** When the lead first contacted via the chatbot. */
1328
+ leadFirstSeen?: string;
1329
+ /** Traffic source label (e.g. "Website Chatbot"). */
1330
+ leadSource?: string;
1331
+ searchQuery?: string;
1332
+ activeFilter?: AiConvFilterTab;
1333
+ inputValue?: string;
1334
+ internalNotes?: string;
1335
+ showLeadPanel?: boolean;
1336
+ hasMore?: boolean;
1337
+ isLoadingMore?: boolean;
1338
+ onSelectConversation?: (id: string) => void;
1339
+ onRead?: (id: string) => void;
1340
+ onSearchChange?: (v: string) => void;
1341
+ onFilterChange?: (f: AiConvFilterTab) => void;
1342
+ onInputChange?: (v: string) => void;
1343
+ onSend?: (v: string) => void;
1344
+ onTakeOver?: () => void;
1345
+ onLetAiHandle?: () => void;
1346
+ onReopen?: () => void;
1347
+ onMarkUrgent?: () => void;
1348
+ onUnmarkUrgent?: () => void;
1349
+ onArchive?: () => void;
1350
+ onAssignToAdvisor?: () => void;
1351
+ /** True when this lead is already a contact in the system. Disables Add to Contacts. */
1352
+ isKnownContact?: boolean;
1353
+ onAddToContacts?: () => void;
1354
+ onCreateOpportunity?: () => void;
1355
+ onBookAppointment?: () => void;
1356
+ onApproveAppointment?: () => void;
1357
+ onDeclineAppointment?: () => void;
1358
+ onRescheduleAppointment?: (contactId: string) => void;
1359
+ onNotesChange?: (v: string) => void;
1360
+ onToggleLeadPanel?: () => void;
1361
+ onLoadMore?: () => void;
1362
+ isAiTyping?: boolean;
1363
+ notesSaveStatus?: "idle" | "saving" | "saved";
1364
+ className?: string;
1365
+ }
1366
+
1367
+ export function ConversationsPage({
1368
+ conversations,
1369
+ activeConversationId,
1370
+ contact,
1371
+ status = "ai-active",
1372
+ mode = "ai",
1373
+ messages = [],
1374
+ aiFields = [],
1375
+ appointment,
1376
+ leadFirstSeen,
1377
+ leadSource,
1378
+ searchQuery,
1379
+ activeFilter,
1380
+ inputValue,
1381
+ internalNotes,
1382
+ showLeadPanel = true,
1383
+ hasMore,
1384
+ isLoadingMore,
1385
+ isAiTyping,
1386
+ notesSaveStatus,
1387
+ onSelectConversation,
1388
+ onRead,
1389
+ onSearchChange,
1390
+ onFilterChange,
1391
+ onInputChange,
1392
+ onSend,
1393
+ onTakeOver,
1394
+ onLetAiHandle,
1395
+ onReopen,
1396
+ onMarkUrgent,
1397
+ onUnmarkUrgent,
1398
+ onArchive,
1399
+ onAssignToAdvisor,
1400
+ isKnownContact,
1401
+ onAddToContacts,
1402
+ onCreateOpportunity,
1403
+ onBookAppointment,
1404
+ onApproveAppointment,
1405
+ onDeclineAppointment,
1406
+ onRescheduleAppointment,
1407
+ onNotesChange,
1408
+ onToggleLeadPanel,
1409
+ onLoadMore,
1410
+ className,
1411
+ }: ConversationsPageProps) {
1412
+ // Mobile panel state — "list" | "chat" | "lead"
1413
+ const [mobilePanel, setMobilePanel] = useState<"list" | "chat" | "lead">(
1414
+ "list",
1415
+ );
1416
+
1417
+ const handleSelectConversation = (id: string) => {
1418
+ onSelectConversation?.(id);
1419
+ setMobilePanel("chat");
1420
+ };
1421
+
1422
+ const handleToggleLeadPanel = () => {
1423
+ onToggleLeadPanel?.();
1424
+ // When expanding lead panel → go to lead on mobile; collapsing → back to chat
1425
+ setMobilePanel(showLeadPanel ? "chat" : "lead");
1426
+ };
1427
+
1428
+ return (
1429
+ <TooltipProvider>
1430
+ <div
1431
+ className={cn("flex h-full overflow-hidden bg-background", className)}
1432
+ >
1433
+ {/* Left — Conversation List */}
1434
+ <ConversationList
1435
+ conversations={conversations}
1436
+ activeId={activeConversationId}
1437
+ searchQuery={searchQuery}
1438
+ activeFilter={activeFilter}
1439
+ hasMore={hasMore}
1440
+ isLoadingMore={isLoadingMore}
1441
+ onSearchChange={onSearchChange}
1442
+ onFilterChange={onFilterChange}
1443
+ onSelect={handleSelectConversation}
1444
+ onRead={onRead}
1445
+ onLoadMore={onLoadMore}
1446
+ className={cn(
1447
+ "shrink-0 md:w-[320px]",
1448
+ // Mobile: full width, visible only on list panel
1449
+ mobilePanel === "list" ? "flex w-full md:flex" : "hidden md:flex",
1450
+ )}
1451
+ />
1452
+
1453
+ {/* Center — Chat Thread */}
1454
+ {contact ? (
1455
+ <ChatThread
1456
+ contact={contact}
1457
+ status={status}
1458
+ mode={mode}
1459
+ messages={messages}
1460
+ isAiTyping={isAiTyping}
1461
+ inputValue={inputValue}
1462
+ onInputChange={onInputChange}
1463
+ onSend={onSend}
1464
+ onTakeOver={onTakeOver}
1465
+ onLetAiHandle={onLetAiHandle}
1466
+ onReopen={onReopen}
1467
+ onMarkUrgent={onMarkUrgent}
1468
+ onUnmarkUrgent={onUnmarkUrgent}
1469
+ onArchive={onArchive}
1470
+ onAssignToAdvisor={onAssignToAdvisor}
1471
+ onBack={() => setMobilePanel("list")}
1472
+ onShowLeadInfo={() => setMobilePanel("lead")}
1473
+ className={cn(
1474
+ "min-w-0 flex-1 border-r border-border",
1475
+ mobilePanel === "chat"
1476
+ ? "flex flex-col md:flex"
1477
+ : "hidden md:flex",
1478
+ )}
1479
+ />
1480
+ ) : (
1481
+ <div
1482
+ className={cn(
1483
+ "min-w-0 flex-1 items-center justify-center border-r border-border bg-muted/10",
1484
+ mobilePanel === "chat" ? "flex md:flex" : "hidden md:flex",
1485
+ )}
1486
+ >
1487
+ <div className="flex flex-col items-center gap-2 text-muted-foreground">
1488
+ <MessageSquare className="size-10 opacity-30" />
1489
+ <p className="text-sm">Select a conversation</p>
1490
+ </div>
1491
+ </div>
1492
+ )}
1493
+
1494
+ {/* Right — Lead Info Panel */}
1495
+ {contact && (
1496
+ <>
1497
+ {/* Desktop: always in DOM, width-animated. Mobile: show/hide by mobilePanel. */}
1498
+ <div
1499
+ className={cn(
1500
+ // Mobile: full-width, instant show/hide based on mobilePanel
1501
+ mobilePanel === "lead"
1502
+ ? "flex w-full shrink-0 flex-col"
1503
+ : "hidden",
1504
+ // Desktop: always rendered, animate width open/close
1505
+ "md:block md:shrink-0 md:overflow-hidden md:transition-[width] md:duration-200 md:ease-in-out",
1506
+ showLeadPanel ? "md:w-[320px]" : "md:w-0",
1507
+ )}
1508
+ >
1509
+ <LeadInfoPanel
1510
+ contact={contact}
1511
+ firstSeen={leadFirstSeen}
1512
+ source={leadSource}
1513
+ aiFields={aiFields}
1514
+ appointment={appointment}
1515
+ internalNotes={internalNotes}
1516
+ notesSaveStatus={notesSaveStatus}
1517
+ isKnownContact={isKnownContact}
1518
+ onAddToContacts={onAddToContacts}
1519
+ onCreateOpportunity={onCreateOpportunity}
1520
+ onBookAppointment={onBookAppointment}
1521
+ onApproveAppointment={onApproveAppointment}
1522
+ onDeclineAppointment={onDeclineAppointment}
1523
+ onRescheduleAppointment={onRescheduleAppointment}
1524
+ onNotesChange={onNotesChange}
1525
+ onToggleCollapse={handleToggleLeadPanel}
1526
+ onBack={() => setMobilePanel("chat")}
1527
+ className="flex h-full w-full flex-col border-l border-border md:w-[320px]"
1528
+ />
1529
+ </div>
1530
+
1531
+ {/* Collapsed expand button — desktop only, shown when panel is closed */}
1532
+ {!showLeadPanel && onToggleLeadPanel && (
1533
+ <div className="hidden shrink-0 items-start border-l border-border pt-[29px] md:flex">
1534
+ <Tooltip>
1535
+ <TooltipTrigger
1536
+ render={
1537
+ <Button
1538
+ variant="ghost"
1539
+ size="icon"
1540
+ className="size-8"
1541
+ aria-label="Show lead info"
1542
+ onClick={handleToggleLeadPanel}
1543
+ >
1544
+ <ChevronLeft className="size-4" />
1545
+ </Button>
1546
+ }
1547
+ />
1548
+ <TooltipContent>Show lead info</TooltipContent>
1549
+ </Tooltip>
1550
+ </div>
1551
+ )}
1552
+ </>
1553
+ )}
1554
+ </div>
1555
+ </TooltipProvider>
1556
+ );
1557
+ }
1558
+
1559
+ // ---------------------------------------------------------------------------
1560
+ // AiConvAssignAdvisorDialog
1561
+ // ---------------------------------------------------------------------------
1562
+
1563
+ export interface AiConvAssignAdvisorDialogProps {
1564
+ open: boolean;
1565
+ onOpenChange: (open: boolean) => void;
1566
+ advisors: AiConvAdvisor[];
1567
+ /** Currently selected advisor id */
1568
+ value: string;
1569
+ onValueChange: (id: string) => void;
1570
+ onConfirm: () => void;
1571
+ }
1572
+
1573
+ export function AiConvAssignAdvisorDialog({
1574
+ open,
1575
+ onOpenChange,
1576
+ advisors,
1577
+ value,
1578
+ onValueChange,
1579
+ onConfirm,
1580
+ }: AiConvAssignAdvisorDialogProps) {
1581
+ const [search, setSearch] = useState("");
1582
+ const [roleFilter, setRoleFilter] = useState("");
1583
+
1584
+ const roles = Array.from(
1585
+ new Set(advisors.map((a) => a.role).filter(Boolean)),
1586
+ ) as string[];
1587
+
1588
+ const filtered = advisors.filter(
1589
+ (a) =>
1590
+ a.name.toLowerCase().includes(search.toLowerCase()) &&
1591
+ (!roleFilter || a.role === roleFilter),
1592
+ );
1593
+
1594
+ const handleOpenChange = (v: boolean) => {
1595
+ onOpenChange(v);
1596
+ if (!v) {
1597
+ setSearch("");
1598
+ setRoleFilter("");
1599
+ }
1600
+ };
1601
+
1602
+ return (
1603
+ <Dialog open={open} onOpenChange={handleOpenChange}>
1604
+ <DialogContent align="top">
1605
+ <DialogHeader>
1606
+ <DialogTitle>Assign to advisor</DialogTitle>
1607
+ <DialogDescription>
1608
+ Choose an advisor to handle this conversation.
1609
+ </DialogDescription>
1610
+ </DialogHeader>
1611
+ <div className="flex flex-col gap-0">
1612
+ {/* Role filter */}
1613
+ {roles.length > 0 && (
1614
+ <div className="pb-3">
1615
+ <ToggleGroup
1616
+ type="single"
1617
+ variant="outline"
1618
+ spacing={1.5}
1619
+ size="sm"
1620
+ value={roleFilter ? [roleFilter] : ["__all__"]}
1621
+ onValueChange={(values) => {
1622
+ const v = values[0];
1623
+ setRoleFilter(!v || v === "__all__" ? "" : v);
1624
+ }}
1625
+ >
1626
+ <ToggleGroupItem value="__all__">All</ToggleGroupItem>
1627
+ {roles.map((role) => (
1628
+ <ToggleGroupItem key={role} value={role}>
1629
+ {role}
1630
+ </ToggleGroupItem>
1631
+ ))}
1632
+ </ToggleGroup>
1633
+ </div>
1634
+ )}
1635
+ {/* Search */}
1636
+ <div className="flex items-center gap-2 border border-input px-3">
1637
+ <Search className="size-4 shrink-0 text-muted-foreground" />
1638
+ <input
1639
+ type="text"
1640
+ placeholder="Search advisors..."
1641
+ value={search}
1642
+ onChange={(e) => setSearch(e.target.value)}
1643
+ className="h-9 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
1644
+ />
1645
+ </div>
1646
+ {/* List */}
1647
+ <div className="max-h-52 overflow-y-auto border border-t-0 border-input">
1648
+ {filtered.length === 0 ? (
1649
+ <p className="py-6 text-center text-sm text-muted-foreground">
1650
+ No advisors found.
1651
+ </p>
1652
+ ) : (
1653
+ filtered.map((advisor) => (
1654
+ <button
1655
+ key={advisor.id}
1656
+ type="button"
1657
+ onClick={() => onValueChange(advisor.id)}
1658
+ className={cn(
1659
+ "flex w-full items-center gap-3 px-3 py-2.5 text-left text-sm transition-colors hover:bg-muted",
1660
+ value === advisor.id && "bg-muted font-medium",
1661
+ )}
1662
+ >
1663
+ <Avatar size="sm">
1664
+ <AvatarFallback className="font-semibold">
1665
+ {advisor.initials}
1666
+ </AvatarFallback>
1667
+ </Avatar>
1668
+ <div className="min-w-0 flex-1">
1669
+ <p className="truncate text-sm">{advisor.name}</p>
1670
+ {advisor.role && (
1671
+ <p className="truncate text-xs text-muted-foreground">
1672
+ {advisor.role}
1673
+ </p>
1674
+ )}
1675
+ </div>
1676
+ </button>
1677
+ ))
1678
+ )}
1679
+ </div>
1680
+ </div>
1681
+ <DialogFooter>
1682
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
1683
+ Cancel
1684
+ </Button>
1685
+ <Button onClick={onConfirm}>Assign</Button>
1686
+ </DialogFooter>
1687
+ </DialogContent>
1688
+ </Dialog>
1689
+ );
1690
+ }