@wealthx/shadcn 1.5.5 → 1.5.7

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 (95) hide show
  1. package/.turbo/turbo-build.log +164 -158
  2. package/CHANGELOG.md +12 -0
  3. package/dist/{chunk-2LLFNGJZ.mjs → chunk-3OOUI5TO.mjs} +1 -1
  4. package/dist/{chunk-AUEUTZIC.mjs → chunk-3QAQQBCM.mjs} +2 -2
  5. package/dist/chunk-3Z75IKFO.mjs +34 -0
  6. package/dist/{chunk-RAKBWNQH.mjs → chunk-5WCIGJ3E.mjs} +26 -65
  7. package/dist/chunk-65PZNG4Y.mjs +129 -0
  8. package/dist/{chunk-D447W45Z.mjs → chunk-7OLKVKJF.mjs} +41 -49
  9. package/dist/{chunk-UEREFDAE.mjs → chunk-AZGLSIHF.mjs} +1 -1
  10. package/dist/{chunk-QRVEI6J3.mjs → chunk-BQBSYM2I.mjs} +1 -3
  11. package/dist/{chunk-BFB3UH7V.mjs → chunk-C2QMHKLT.mjs} +1 -1
  12. package/dist/chunk-CEEVYRQA.mjs +61 -0
  13. package/dist/{chunk-VJ3GC7W3.mjs → chunk-CXGZSIQN.mjs} +4 -30
  14. package/dist/{chunk-VLELWBEW.mjs → chunk-E2BNCA6L.mjs} +1 -1
  15. package/dist/{chunk-46Q4335I.mjs → chunk-F73QFUZH.mjs} +10 -20
  16. package/dist/{chunk-Q35PNFJ7.mjs → chunk-FBNEIYSE.mjs} +1 -1
  17. package/dist/{chunk-HO6S3ECM.mjs → chunk-G4WLCKFV.mjs} +1 -1
  18. package/dist/{chunk-ODO6BUOF.mjs → chunk-HRHXZIX5.mjs} +1 -3
  19. package/dist/{chunk-IXR4BQSQ.mjs → chunk-I6QVWQCD.mjs} +1 -1
  20. package/dist/{chunk-PV7PNA6K.mjs → chunk-IKDTOCSY.mjs} +2 -4
  21. package/dist/{chunk-Q5SGEIJV.mjs → chunk-IXKE6LHN.mjs} +1 -1
  22. package/dist/{chunk-OL65UQHQ.mjs → chunk-JCH2BG24.mjs} +26 -60
  23. package/dist/{chunk-PV3Y7QGK.mjs → chunk-LHWJQNLG.mjs} +3 -3
  24. package/dist/{chunk-JTG5R5YV.mjs → chunk-MS4PDUSU.mjs} +284 -58
  25. package/dist/{chunk-VFH632TB.mjs → chunk-SDNG4XL6.mjs} +1 -1
  26. package/dist/{chunk-DFL5CV75.mjs → chunk-T6MMCOX6.mjs} +1 -1
  27. package/dist/{chunk-6TX73WG7.mjs → chunk-TCE5L44O.mjs} +3 -2
  28. package/dist/{chunk-HROG643K.mjs → chunk-W5QJ57PU.mjs} +1 -1
  29. package/dist/{chunk-EW72FINW.mjs → chunk-XVZGXPIX.mjs} +1 -1
  30. package/dist/components/ui/about-you-form.mjs +3 -3
  31. package/dist/components/ui/add-column-modal.js +56 -227
  32. package/dist/components/ui/add-column-modal.mjs +1 -2
  33. package/dist/components/ui/ai-conversations.js +278 -58
  34. package/dist/components/ui/ai-conversations.mjs +1 -1
  35. package/dist/components/ui/appointment-action-dialogs.mjs +3 -3
  36. package/dist/components/ui/appointment-availability-settings.js +2 -28
  37. package/dist/components/ui/appointment-availability-settings.mjs +4 -4
  38. package/dist/components/ui/appointment-book-dialog.js +11 -21
  39. package/dist/components/ui/appointment-book-dialog.mjs +3 -3
  40. package/dist/components/ui/appointment-calendar-view.mjs +2 -2
  41. package/dist/components/ui/appointment-detail-sheet.mjs +4 -4
  42. package/dist/components/ui/appointment-upcoming-card.mjs +3 -3
  43. package/dist/components/ui/bank-statement-generate-dialog.mjs +4 -4
  44. package/dist/components/ui/calendar.mjs +2 -2
  45. package/dist/components/ui/dashboard-transactions-table.mjs +1 -1
  46. package/dist/components/ui/date-picker.mjs +3 -3
  47. package/dist/components/ui/financial-cards.mjs +2 -2
  48. package/dist/components/ui/financial-sections.mjs +3 -3
  49. package/dist/components/ui/integration-card.js +326 -0
  50. package/dist/components/ui/integration-card.mjs +11 -0
  51. package/dist/components/ui/kanban-column.js +151 -243
  52. package/dist/components/ui/kanban-column.mjs +3 -4
  53. package/dist/components/ui/loan-application-cards.mjs +1 -1
  54. package/dist/components/ui/onboarding-layout.js +65 -4
  55. package/dist/components/ui/onboarding-layout.mjs +6 -4
  56. package/dist/components/ui/opportunity-card.js +126 -216
  57. package/dist/components/ui/opportunity-card.mjs +2 -3
  58. package/dist/components/ui/opportunity-edit-modals.mjs +4 -4
  59. package/dist/components/ui/opportunity-summary-tab.mjs +6 -6
  60. package/dist/components/ui/pipeline-board.js +167 -261
  61. package/dist/components/ui/pipeline-board.mjs +4 -5
  62. package/dist/components/ui/pipeline-chart.js +128 -55
  63. package/dist/components/ui/pipeline-chart.mjs +2 -1
  64. package/dist/components/ui/pipeline-dialogs.mjs +4 -4
  65. package/dist/components/ui/savings-goal-modal.mjs +3 -3
  66. package/dist/components/ui/selectable-card.js +163 -0
  67. package/dist/components/ui/selectable-card.mjs +9 -0
  68. package/dist/components/ui/sidebar-nav.js +2 -4
  69. package/dist/components/ui/sidebar-nav.mjs +1 -1
  70. package/dist/components/ui/signup-shell.js +3 -2
  71. package/dist/components/ui/signup-shell.mjs +2 -2
  72. package/dist/components/ui/stepper.js +3 -2
  73. package/dist/components/ui/stepper.mjs +1 -1
  74. package/dist/index.js +2122 -1881
  75. package/dist/index.mjs +56 -46
  76. package/dist/styles.css +1 -1
  77. package/package.json +11 -1
  78. package/src/components/index.tsx +26 -2
  79. package/src/components/ui/add-column-modal.tsx +9 -58
  80. package/src/components/ui/ai-conversations.tsx +308 -42
  81. package/src/components/ui/appointment-availability-settings.tsx +2 -35
  82. package/src/components/ui/appointment-book-dialog.tsx +25 -48
  83. package/src/components/ui/integration-card.tsx +88 -0
  84. package/src/components/ui/kanban-column.tsx +0 -7
  85. package/src/components/ui/onboarding-layout.tsx +102 -1
  86. package/src/components/ui/opportunity-card.tsx +5 -53
  87. package/src/components/ui/pipeline-board.tsx +0 -3
  88. package/src/components/ui/pipeline-chart.tsx +47 -53
  89. package/src/components/ui/selectable-card.tsx +37 -0
  90. package/src/components/ui/sidebar-nav.tsx +0 -2
  91. package/src/components/ui/stepper.tsx +3 -2
  92. package/src/lib/format-date.ts +6 -6
  93. package/src/styles/styles-css.ts +1 -1
  94. package/tsup.config.ts +2 -0
  95. package/dist/chunk-2P7HP7LR.mjs +0 -68
@@ -2,24 +2,30 @@ import React, { useState } from "react";
2
2
  import {
3
3
  Archive,
4
4
  ArrowLeft,
5
+ Bold,
5
6
  Bot,
6
7
  Briefcase,
7
8
  Calendar,
8
9
  CheckCircle2,
9
10
  ChevronLeft,
10
11
  ChevronRight,
12
+ ChevronDown,
11
13
  Flag,
12
14
  HelpCircle,
15
+ Italic,
16
+ Link2,
13
17
  Lock,
14
18
  Mail,
15
19
  MapPin,
16
20
  MessageSquare,
17
21
  MoreHorizontal,
22
+ Paperclip,
18
23
  Phone,
19
24
  PhoneCall,
20
25
  Plus,
21
26
  Search,
22
27
  Send,
28
+ Underline,
23
29
  UserCheck,
24
30
  UserPlus,
25
31
  Video,
@@ -90,6 +96,10 @@ export type AiConvFilterTab =
90
96
 
91
97
  export type AiConvMode = "ai" | "manual";
92
98
 
99
+ export type AiConvChannel = "chat" | "email";
100
+
101
+ export type AiConvChannelFilter = "all" | AiConvChannel;
102
+
93
103
  export type AiConvMeetingType = "video" | "phone" | "in-person";
94
104
 
95
105
  export interface AiConvContact {
@@ -116,6 +126,14 @@ export interface AiConvListItemData {
116
126
  unreadCount?: number;
117
127
  /** Assigned advisor display name. */
118
128
  assignedTo?: string;
129
+ /**
130
+ * Conversation topic tag — e.g. "Buy a Home", "Refinance", "Investment".
131
+ * Shown as a small secondary badge so the broker can categorise conversations
132
+ * at a glance without opening each thread.
133
+ */
134
+ topic?: string;
135
+ /** Channel this conversation is using — "chat" (default) or "email". */
136
+ channel?: AiConvChannel;
119
137
  }
120
138
 
121
139
  export interface AiConvMessage {
@@ -276,6 +294,11 @@ export function ConversationListItem({
276
294
  {data.status === "needs-attention" && (
277
295
  <Flag className="size-3 text-warning-text" />
278
296
  )}
297
+ {data.channel === "email" ? (
298
+ <Mail className="size-3 text-muted-foreground" />
299
+ ) : (
300
+ <MessageSquare className="size-3 text-muted-foreground" />
301
+ )}
279
302
  <span className="whitespace-nowrap text-xs text-muted-foreground">
280
303
  {data.timestamp}
281
304
  </span>
@@ -326,17 +349,20 @@ function filterConversations(
326
349
  conversations: AiConvListItemData[],
327
350
  query: string,
328
351
  filter: AiConvFilterTab,
352
+ channelFilter: AiConvChannelFilter,
329
353
  ): AiConvListItemData[] {
330
354
  const q = query.toLowerCase();
331
355
  return conversations.filter((c) => {
332
356
  const matchesFilter =
333
357
  filter === "all" ||
334
358
  (filter === "open" ? c.status !== "closed" : c.status === filter);
359
+ const matchesChannel =
360
+ channelFilter === "all" || (c.channel ?? "chat") === channelFilter;
335
361
  const matchesSearch =
336
362
  !q ||
337
363
  c.contact.name.toLowerCase().includes(q) ||
338
364
  c.lastMessage.toLowerCase().includes(q);
339
- return matchesFilter && matchesSearch;
365
+ return matchesFilter && matchesChannel && matchesSearch;
340
366
  });
341
367
  }
342
368
 
@@ -353,10 +379,12 @@ export interface ConversationListProps {
353
379
  activeId?: string;
354
380
  searchQuery?: string;
355
381
  activeFilter?: AiConvFilterTab;
382
+ channelFilter?: AiConvChannelFilter;
356
383
  hasMore?: boolean;
357
384
  isLoadingMore?: boolean;
358
385
  onSearchChange?: (v: string) => void;
359
386
  onFilterChange?: (f: AiConvFilterTab) => void;
387
+ onChannelFilterChange?: (f: AiConvChannelFilter) => void;
360
388
  onSelect?: (id: string) => void;
361
389
  onRead?: (id: string) => void;
362
390
  onLoadMore?: () => void;
@@ -368,15 +396,28 @@ export function ConversationList({
368
396
  activeId,
369
397
  searchQuery = "",
370
398
  activeFilter = "all",
399
+ channelFilter: channelFilterProp = "all",
371
400
  hasMore,
372
401
  isLoadingMore,
373
402
  onSearchChange,
374
403
  onFilterChange,
404
+ onChannelFilterChange,
375
405
  onSelect,
376
406
  onRead,
377
407
  onLoadMore,
378
408
  className,
379
409
  }: ConversationListProps) {
410
+ const [channelFilter, setChannelFilter] =
411
+ useState<AiConvChannelFilter>(channelFilterProp);
412
+
413
+ function handleChannelFilterChange(vals: string[]) {
414
+ const v = vals[0] as AiConvChannelFilter | undefined;
415
+ if (v) {
416
+ setChannelFilter(v);
417
+ onChannelFilterChange?.(v);
418
+ }
419
+ }
420
+
380
421
  return (
381
422
  <div
382
423
  className={cn(
@@ -385,9 +426,8 @@ export function ConversationList({
385
426
  )}
386
427
  >
387
428
  <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">
429
+ <div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-2.5">
430
+ <div className="relative flex-1">
391
431
  <Search className="absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
392
432
  <Input
393
433
  value={searchQuery}
@@ -396,9 +436,27 @@ export function ConversationList({
396
436
  className="h-8 pl-8 text-sm"
397
437
  />
398
438
  </div>
439
+ <ToggleGroup
440
+ type="single"
441
+ variant="outline"
442
+ size="sm"
443
+ spacing={0}
444
+ value={[channelFilter]}
445
+ onValueChange={handleChannelFilterChange}
446
+ className="shrink-0"
447
+ >
448
+ <ToggleGroupItem value="all" aria-label="All channels">
449
+ All
450
+ </ToggleGroupItem>
451
+ <ToggleGroupItem value="chat" aria-label="Chat">
452
+ <MessageSquare className="size-3.5" />
453
+ </ToggleGroupItem>
454
+ <ToggleGroupItem value="email" aria-label="Email">
455
+ <Mail className="size-3.5" />
456
+ </ToggleGroupItem>
457
+ </ToggleGroup>
399
458
  </div>
400
459
 
401
- {/* Filter tabs */}
402
460
  <div className="flex flex-1 items-center border-b border-border">
403
461
  <Tabs
404
462
  value={activeFilter}
@@ -430,6 +488,7 @@ export function ConversationList({
430
488
  conversations,
431
489
  searchQuery,
432
490
  activeFilter,
491
+ channelFilter,
433
492
  );
434
493
  return filtered.length === 0 ? (
435
494
  <div className="flex flex-col items-center justify-center gap-2 p-8 text-muted-foreground">
@@ -602,6 +661,11 @@ export function ChatBubble({ message, className }: ChatBubbleProps) {
602
661
 
603
662
  export interface ChatComposerProps {
604
663
  mode: AiConvMode;
664
+ /** Active reply channel. Defaults to "chat". */
665
+ channel?: AiConvChannel;
666
+ onChannelChange?: (channel: AiConvChannel) => void;
667
+ /** Lead's email address — pre-fills the To field in email compose. */
668
+ contactEmail?: string;
605
669
  inputValue?: string;
606
670
  onInputChange?: (v: string) => void;
607
671
  onSend?: (v: string) => void;
@@ -612,6 +676,9 @@ export interface ChatComposerProps {
612
676
 
613
677
  export function ChatComposer({
614
678
  mode,
679
+ channel: channelProp = "chat",
680
+ onChannelChange,
681
+ contactEmail = "",
615
682
  inputValue = "",
616
683
  onInputChange,
617
684
  onSend,
@@ -619,59 +686,223 @@ export function ChatComposer({
619
686
  onLetAiHandle,
620
687
  className,
621
688
  }: ChatComposerProps) {
622
- // AI mode informational banner only; primary Take Over button is in the thread header
623
- if (mode === "ai") {
689
+ // Semi-controlled: owns channel state for uncontrolled usage, notifies parent on change
690
+ const [channel, setChannel] = React.useState<AiConvChannel>(channelProp);
691
+ const [emailTo, setEmailTo] = React.useState(contactEmail);
692
+ const [emailCc, setEmailCc] = React.useState("");
693
+ const [showCc, setShowCc] = React.useState(false);
694
+ const [emailSubject, setEmailSubject] = React.useState("");
695
+ const [bold, setBold] = React.useState(false);
696
+ const [italic, setItalic] = React.useState(false);
697
+ const [underline, setUnderline] = React.useState(false);
698
+
699
+ const handleChannelChange = (c: AiConvChannel) => {
700
+ setChannel(c);
701
+ onChannelChange?.(c);
702
+ };
703
+
704
+ function ToolbarButton({
705
+ label,
706
+ icon: Icon,
707
+ pressed,
708
+ onToggle,
709
+ }: {
710
+ label: string;
711
+ icon: React.ElementType;
712
+ pressed?: boolean;
713
+ onToggle?: () => void;
714
+ }) {
624
715
  return (
625
- <div
716
+ <button
717
+ type="button"
718
+ aria-label={label}
719
+ aria-pressed={pressed}
720
+ onClick={onToggle}
626
721
  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,
722
+ "flex size-7 items-center justify-center transition-colors",
723
+ pressed
724
+ ? "bg-foreground text-background"
725
+ : "text-muted-foreground hover:bg-muted hover:text-foreground",
629
726
  )}
630
727
  >
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>
728
+ <Icon className="size-3.5" />
729
+ </button>
730
+ );
731
+ }
732
+
733
+ function EmailFieldRow({
734
+ label,
735
+ children,
736
+ }: {
737
+ label: string;
738
+ children: React.ReactNode;
739
+ }) {
740
+ return (
741
+ <div className="flex items-center gap-3 border-b border-border px-4 py-2.5">
742
+ <span className="w-14 shrink-0 text-sm text-muted-foreground">
743
+ {label}
744
+ </span>
745
+ {children}
642
746
  </div>
643
747
  );
644
748
  }
645
749
 
646
- // Manual mode — full composer
647
750
  return (
648
751
  <div
649
752
  className={cn(
650
- "flex flex-col gap-2 border-t border-border bg-background p-3",
753
+ "flex flex-col border-t border-border bg-background",
651
754
  className,
652
755
  )}
653
756
  >
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()}
757
+ <div className="border-b border-border px-3 py-2">
758
+ <Tabs
759
+ value={channel}
760
+ onValueChange={(v) => v && handleChannelChange(v as AiConvChannel)}
670
761
  >
671
- <Send className="mr-1.5 size-3.5" />
672
- Send
673
- </Button>
762
+ <TabsList variant="default" className="w-full">
763
+ <TabsTrigger value="chat" className="flex-1 gap-1.5">
764
+ <MessageSquare className="size-3.5" />
765
+ Chat
766
+ </TabsTrigger>
767
+ <TabsTrigger value="email" className="flex-1 gap-1.5">
768
+ <Mail className="size-3.5" />
769
+ Email
770
+ </TabsTrigger>
771
+ </TabsList>
772
+ </Tabs>
674
773
  </div>
774
+
775
+ {mode === "ai" ? (
776
+ <div className="flex items-center gap-2 bg-muted/30 px-4 py-2.5 text-[12px] text-muted-foreground">
777
+ <Bot className="size-3.5 shrink-0 text-muted-foreground" />
778
+ <span>AI is handling this conversation.</span>
779
+ <Button
780
+ variant="link"
781
+ size="sm"
782
+ className="h-auto p-0 text-[12px] font-medium text-foreground"
783
+ onClick={onTakeOver}
784
+ >
785
+ Take Over
786
+ </Button>
787
+ <span>to reply directly.</span>
788
+ </div>
789
+ ) : (
790
+ /* Email panel stays in normal flow to anchor container height;
791
+ chat panel is an absolute overlay so both tabs share identical dimensions */
792
+ <div className="relative">
793
+ <div
794
+ className={cn(
795
+ "flex flex-col",
796
+ channel !== "email" && "invisible pointer-events-none",
797
+ )}
798
+ aria-hidden={channel !== "email"}
799
+ >
800
+ <EmailFieldRow label="To">
801
+ <input
802
+ type="email"
803
+ value={emailTo}
804
+ onChange={(e) => setEmailTo(e.target.value)}
805
+ placeholder="Recipient email"
806
+ className="min-w-0 flex-1 bg-transparent text-base text-foreground outline-none placeholder:text-muted-foreground"
807
+ />
808
+ <button
809
+ type="button"
810
+ onClick={() => setShowCc((v) => !v)}
811
+ className="flex shrink-0 items-center gap-0.5 text-sm text-muted-foreground hover:text-foreground"
812
+ >
813
+ CC
814
+ <ChevronDown className="size-3.5" />
815
+ </button>
816
+ </EmailFieldRow>
817
+ {showCc && (
818
+ <EmailFieldRow label="CC">
819
+ <input
820
+ type="email"
821
+ value={emailCc}
822
+ onChange={(e) => setEmailCc(e.target.value)}
823
+ placeholder="CC email"
824
+ className="min-w-0 flex-1 bg-transparent text-base text-foreground outline-none placeholder:text-muted-foreground"
825
+ />
826
+ </EmailFieldRow>
827
+ )}
828
+ <EmailFieldRow label="Subject">
829
+ <input
830
+ type="text"
831
+ value={emailSubject}
832
+ onChange={(e) => setEmailSubject(e.target.value)}
833
+ placeholder="Email subject"
834
+ className="min-w-0 flex-1 bg-transparent text-base text-foreground outline-none placeholder:text-muted-foreground"
835
+ />
836
+ </EmailFieldRow>
837
+ <Textarea
838
+ value={inputValue}
839
+ onChange={(e) => onInputChange?.(e.target.value)}
840
+ placeholder="Write your email..."
841
+ rows={6}
842
+ className="resize-y border-0 px-4 py-3 text-base shadow-none focus-visible:ring-0"
843
+ />
844
+ <div className="flex items-center justify-between border-t border-border px-3 py-2">
845
+ <div className="flex items-center gap-0.5">
846
+ <ToolbarButton
847
+ label="Bold"
848
+ icon={Bold}
849
+ pressed={bold}
850
+ onToggle={() => setBold((v) => !v)}
851
+ />
852
+ <ToolbarButton
853
+ label="Italic"
854
+ icon={Italic}
855
+ pressed={italic}
856
+ onToggle={() => setItalic((v) => !v)}
857
+ />
858
+ <ToolbarButton
859
+ label="Underline"
860
+ icon={Underline}
861
+ pressed={underline}
862
+ onToggle={() => setUnderline((v) => !v)}
863
+ />
864
+ <Separator orientation="vertical" className="mx-1.5 h-4" />
865
+ <ToolbarButton label="Link" icon={Link2} />
866
+ <ToolbarButton label="Attach file" icon={Paperclip} />
867
+ </div>
868
+ <Button
869
+ size="sm"
870
+ onClick={() => onSend?.(inputValue)}
871
+ disabled={!inputValue.trim() || !emailTo.trim()}
872
+ >
873
+ <Send className="mr-1.5 size-3.5" />
874
+ Send Email
875
+ </Button>
876
+ </div>
877
+ </div>
878
+
879
+ {/* Chat compose — absolute overlay, fills exact same height as email panel */}
880
+ {channel === "chat" && (
881
+ <div className="absolute inset-0 flex flex-col gap-3 p-4">
882
+ <Textarea
883
+ value={inputValue}
884
+ onChange={(e) => onInputChange?.(e.target.value)}
885
+ placeholder="Reply to lead..."
886
+ className="min-h-0 flex-1 resize-none text-base"
887
+ />
888
+ <div className="flex items-center justify-between">
889
+ <Button variant="outline" size="sm" onClick={onLetAiHandle}>
890
+ <Bot className="mr-1.5 size-3.5" />
891
+ Let AI Handle
892
+ </Button>
893
+ <Button
894
+ size="sm"
895
+ onClick={() => onSend?.(inputValue)}
896
+ disabled={!inputValue.trim()}
897
+ >
898
+ <Send className="mr-1.5 size-3.5" />
899
+ Send
900
+ </Button>
901
+ </div>
902
+ </div>
903
+ )}
904
+ </div>
905
+ )}
675
906
  </div>
676
907
  );
677
908
  }
@@ -686,6 +917,9 @@ export interface ChatThreadProps {
686
917
  mode: AiConvMode;
687
918
  messages: AiConvMessage[];
688
919
  isAiTyping?: boolean;
920
+ /** Active reply channel — "chat" (default) or "email". */
921
+ channel?: AiConvChannel;
922
+ onChannelChange?: (channel: AiConvChannel) => void;
689
923
  inputValue?: string;
690
924
  onInputChange?: (v: string) => void;
691
925
  onSend?: (v: string) => void;
@@ -709,6 +943,8 @@ export function ChatThread({
709
943
  mode,
710
944
  messages,
711
945
  isAiTyping = false,
946
+ channel,
947
+ onChannelChange,
712
948
  inputValue,
713
949
  onInputChange,
714
950
  onSend,
@@ -882,6 +1118,9 @@ export function ChatThread({
882
1118
  ) : (
883
1119
  <ChatComposer
884
1120
  mode={mode}
1121
+ channel={channel}
1122
+ onChannelChange={onChannelChange}
1123
+ contactEmail={contact.email}
885
1124
  inputValue={inputValue}
886
1125
  onInputChange={onInputChange}
887
1126
  onSend={onSend}
@@ -1059,6 +1298,8 @@ export interface LeadInfoPanelProps {
1059
1298
  contact: AiConvContact;
1060
1299
  firstSeen?: string;
1061
1300
  source?: string;
1301
+ /** Conversation topic tag — e.g. "Buy a Home", "Refinance", "Investment". */
1302
+ topic?: string;
1062
1303
  aiFields?: AiConvDataField[];
1063
1304
  appointment?: AiConvAppointmentData;
1064
1305
  internalNotes?: string;
@@ -1083,6 +1324,7 @@ export function LeadInfoPanel({
1083
1324
  contact,
1084
1325
  firstSeen,
1085
1326
  source = "Website Chatbot",
1327
+ topic,
1086
1328
  aiFields = [],
1087
1329
  appointment,
1088
1330
  internalNotes = "",
@@ -1163,6 +1405,11 @@ export function LeadInfoPanel({
1163
1405
  {displayContactName(contact.name)}
1164
1406
  </p>
1165
1407
  <p className="text-sm text-muted-foreground">{source}</p>
1408
+ {topic && (
1409
+ <Badge variant="secondary" className="mt-1 text-xs">
1410
+ {topic}
1411
+ </Badge>
1412
+ )}
1166
1413
  </div>
1167
1414
  </div>
1168
1415
  {(contact.email || contact.phone || firstSeen) && (
@@ -1328,8 +1575,15 @@ export interface ConversationsPageProps {
1328
1575
  leadFirstSeen?: string;
1329
1576
  /** Traffic source label (e.g. "Website Chatbot"). */
1330
1577
  leadSource?: string;
1578
+ /** Conversation topic tag — e.g. "Buy a Home", "Refinance", "Investment". */
1579
+ leadTopic?: string;
1331
1580
  searchQuery?: string;
1332
1581
  activeFilter?: AiConvFilterTab;
1582
+ /** Filter conversation list by channel type. */
1583
+ channelFilter?: AiConvChannelFilter;
1584
+ /** Active reply channel — "chat" (default) or "email". */
1585
+ channel?: AiConvChannel;
1586
+ onChannelChange?: (channel: AiConvChannel) => void;
1333
1587
  inputValue?: string;
1334
1588
  internalNotes?: string;
1335
1589
  showLeadPanel?: boolean;
@@ -1339,6 +1593,7 @@ export interface ConversationsPageProps {
1339
1593
  onRead?: (id: string) => void;
1340
1594
  onSearchChange?: (v: string) => void;
1341
1595
  onFilterChange?: (f: AiConvFilterTab) => void;
1596
+ onChannelFilterChange?: (f: AiConvChannelFilter) => void;
1342
1597
  onInputChange?: (v: string) => void;
1343
1598
  onSend?: (v: string) => void;
1344
1599
  onTakeOver?: () => void;
@@ -1375,8 +1630,13 @@ export function ConversationsPage({
1375
1630
  appointment,
1376
1631
  leadFirstSeen,
1377
1632
  leadSource,
1633
+ leadTopic,
1378
1634
  searchQuery,
1379
1635
  activeFilter,
1636
+ channelFilter,
1637
+ onChannelFilterChange,
1638
+ channel,
1639
+ onChannelChange,
1380
1640
  inputValue,
1381
1641
  internalNotes,
1382
1642
  showLeadPanel = true,
@@ -1436,10 +1696,12 @@ export function ConversationsPage({
1436
1696
  activeId={activeConversationId}
1437
1697
  searchQuery={searchQuery}
1438
1698
  activeFilter={activeFilter}
1699
+ channelFilter={channelFilter}
1439
1700
  hasMore={hasMore}
1440
1701
  isLoadingMore={isLoadingMore}
1441
1702
  onSearchChange={onSearchChange}
1442
1703
  onFilterChange={onFilterChange}
1704
+ onChannelFilterChange={onChannelFilterChange}
1443
1705
  onSelect={handleSelectConversation}
1444
1706
  onRead={onRead}
1445
1707
  onLoadMore={onLoadMore}
@@ -1453,11 +1715,14 @@ export function ConversationsPage({
1453
1715
  {/* Center — Chat Thread */}
1454
1716
  {contact ? (
1455
1717
  <ChatThread
1718
+ key={contact.id}
1456
1719
  contact={contact}
1457
1720
  status={status}
1458
1721
  mode={mode}
1459
1722
  messages={messages}
1460
1723
  isAiTyping={isAiTyping}
1724
+ channel={channel}
1725
+ onChannelChange={onChannelChange}
1461
1726
  inputValue={inputValue}
1462
1727
  onInputChange={onInputChange}
1463
1728
  onSend={onSend}
@@ -1510,6 +1775,7 @@ export function ConversationsPage({
1510
1775
  contact={contact}
1511
1776
  firstSeen={leadFirstSeen}
1512
1777
  source={leadSource}
1778
+ topic={leadTopic}
1513
1779
  aiFields={aiFields}
1514
1780
  appointment={appointment}
1515
1781
  internalNotes={internalNotes}
@@ -131,35 +131,6 @@ export const MEETING_PLATFORM_OPTIONS: {
131
131
  { value: "any", label: "Let client choose" },
132
132
  ];
133
133
 
134
- const MEETING_DURATION_OPTIONS: { value: string; label: string }[] = [
135
- { value: "15", label: "15 minutes" },
136
- { value: "30", label: "30 minutes" },
137
- { value: "45", label: "45 minutes" },
138
- { value: "60", label: "60 minutes" },
139
- { value: "90", label: "90 minutes" },
140
- ];
141
-
142
- const SCHEDULING_BUFFER_OPTIONS: { value: string; label: string }[] = [
143
- { value: "0", label: "No buffer" },
144
- { value: "5", label: "5 minutes" },
145
- { value: "10", label: "10 minutes" },
146
- { value: "15", label: "15 minutes" },
147
- { value: "30", label: "30 minutes" },
148
- ];
149
-
150
- const MAX_SLOTS_OPTIONS: { value: string; label: string }[] = [
151
- { value: "2", label: "2 per day" },
152
- { value: "4", label: "4 per day" },
153
- { value: "6", label: "6 per day" },
154
- { value: "8", label: "8 per day" },
155
- { value: "10", label: "10 per day" },
156
- { value: "unlimited", label: "Unlimited" },
157
- ];
158
-
159
- /** Map a Base UI SelectValue `v` to its display label from an options array. */
160
- const selectLabel = (opts: { value: string; label: string }[], v: unknown) =>
161
- opts.find((o) => o.value === String(v))?.label ?? String(v ?? "");
162
-
163
134
  // 30-min increments from 06:00 to 21:30
164
135
  const TIME_OPTIONS: { value: string; label: string }[] = (() => {
165
136
  const opts: { value: string; label: string }[] = [];
@@ -662,9 +633,7 @@ export function AppointmentAvailabilitySettings({
662
633
  onValueChange={(v) => setTimezone(v as string)}
663
634
  >
664
635
  <SelectTrigger className="w-56">
665
- <SelectValue>
666
- {(v) => selectLabel(TIMEZONE_OPTIONS, v)}
667
- </SelectValue>
636
+ <SelectValue />
668
637
  </SelectTrigger>
669
638
  <SelectContent>
670
639
  {TIMEZONE_OPTIONS.map((opt) => (
@@ -689,9 +658,7 @@ export function AppointmentAvailabilitySettings({
689
658
  }
690
659
  >
691
660
  <SelectTrigger className="w-48">
692
- <SelectValue>
693
- {(v) => selectLabel(MEETING_PLATFORM_OPTIONS, v)}
694
- </SelectValue>
661
+ <SelectValue />
695
662
  </SelectTrigger>
696
663
  <SelectContent>
697
664
  {MEETING_PLATFORM_OPTIONS.map((opt) => (