@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.
- package/.turbo/turbo-build.log +164 -158
- package/CHANGELOG.md +12 -0
- package/dist/{chunk-2LLFNGJZ.mjs → chunk-3OOUI5TO.mjs} +1 -1
- package/dist/{chunk-AUEUTZIC.mjs → chunk-3QAQQBCM.mjs} +2 -2
- package/dist/chunk-3Z75IKFO.mjs +34 -0
- package/dist/{chunk-RAKBWNQH.mjs → chunk-5WCIGJ3E.mjs} +26 -65
- package/dist/chunk-65PZNG4Y.mjs +129 -0
- package/dist/{chunk-D447W45Z.mjs → chunk-7OLKVKJF.mjs} +41 -49
- package/dist/{chunk-UEREFDAE.mjs → chunk-AZGLSIHF.mjs} +1 -1
- package/dist/{chunk-QRVEI6J3.mjs → chunk-BQBSYM2I.mjs} +1 -3
- package/dist/{chunk-BFB3UH7V.mjs → chunk-C2QMHKLT.mjs} +1 -1
- package/dist/chunk-CEEVYRQA.mjs +61 -0
- package/dist/{chunk-VJ3GC7W3.mjs → chunk-CXGZSIQN.mjs} +4 -30
- package/dist/{chunk-VLELWBEW.mjs → chunk-E2BNCA6L.mjs} +1 -1
- package/dist/{chunk-46Q4335I.mjs → chunk-F73QFUZH.mjs} +10 -20
- package/dist/{chunk-Q35PNFJ7.mjs → chunk-FBNEIYSE.mjs} +1 -1
- package/dist/{chunk-HO6S3ECM.mjs → chunk-G4WLCKFV.mjs} +1 -1
- package/dist/{chunk-ODO6BUOF.mjs → chunk-HRHXZIX5.mjs} +1 -3
- package/dist/{chunk-IXR4BQSQ.mjs → chunk-I6QVWQCD.mjs} +1 -1
- package/dist/{chunk-PV7PNA6K.mjs → chunk-IKDTOCSY.mjs} +2 -4
- package/dist/{chunk-Q5SGEIJV.mjs → chunk-IXKE6LHN.mjs} +1 -1
- package/dist/{chunk-OL65UQHQ.mjs → chunk-JCH2BG24.mjs} +26 -60
- package/dist/{chunk-PV3Y7QGK.mjs → chunk-LHWJQNLG.mjs} +3 -3
- package/dist/{chunk-JTG5R5YV.mjs → chunk-MS4PDUSU.mjs} +284 -58
- package/dist/{chunk-VFH632TB.mjs → chunk-SDNG4XL6.mjs} +1 -1
- package/dist/{chunk-DFL5CV75.mjs → chunk-T6MMCOX6.mjs} +1 -1
- package/dist/{chunk-6TX73WG7.mjs → chunk-TCE5L44O.mjs} +3 -2
- package/dist/{chunk-HROG643K.mjs → chunk-W5QJ57PU.mjs} +1 -1
- package/dist/{chunk-EW72FINW.mjs → chunk-XVZGXPIX.mjs} +1 -1
- package/dist/components/ui/about-you-form.mjs +3 -3
- package/dist/components/ui/add-column-modal.js +56 -227
- package/dist/components/ui/add-column-modal.mjs +1 -2
- package/dist/components/ui/ai-conversations.js +278 -58
- package/dist/components/ui/ai-conversations.mjs +1 -1
- package/dist/components/ui/appointment-action-dialogs.mjs +3 -3
- package/dist/components/ui/appointment-availability-settings.js +2 -28
- package/dist/components/ui/appointment-availability-settings.mjs +4 -4
- package/dist/components/ui/appointment-book-dialog.js +11 -21
- package/dist/components/ui/appointment-book-dialog.mjs +3 -3
- package/dist/components/ui/appointment-calendar-view.mjs +2 -2
- package/dist/components/ui/appointment-detail-sheet.mjs +4 -4
- package/dist/components/ui/appointment-upcoming-card.mjs +3 -3
- package/dist/components/ui/bank-statement-generate-dialog.mjs +4 -4
- package/dist/components/ui/calendar.mjs +2 -2
- package/dist/components/ui/dashboard-transactions-table.mjs +1 -1
- package/dist/components/ui/date-picker.mjs +3 -3
- package/dist/components/ui/financial-cards.mjs +2 -2
- package/dist/components/ui/financial-sections.mjs +3 -3
- package/dist/components/ui/integration-card.js +326 -0
- package/dist/components/ui/integration-card.mjs +11 -0
- package/dist/components/ui/kanban-column.js +151 -243
- package/dist/components/ui/kanban-column.mjs +3 -4
- package/dist/components/ui/loan-application-cards.mjs +1 -1
- package/dist/components/ui/onboarding-layout.js +65 -4
- package/dist/components/ui/onboarding-layout.mjs +6 -4
- package/dist/components/ui/opportunity-card.js +126 -216
- package/dist/components/ui/opportunity-card.mjs +2 -3
- package/dist/components/ui/opportunity-edit-modals.mjs +4 -4
- package/dist/components/ui/opportunity-summary-tab.mjs +6 -6
- package/dist/components/ui/pipeline-board.js +167 -261
- package/dist/components/ui/pipeline-board.mjs +4 -5
- package/dist/components/ui/pipeline-chart.js +128 -55
- package/dist/components/ui/pipeline-chart.mjs +2 -1
- package/dist/components/ui/pipeline-dialogs.mjs +4 -4
- package/dist/components/ui/savings-goal-modal.mjs +3 -3
- package/dist/components/ui/selectable-card.js +163 -0
- package/dist/components/ui/selectable-card.mjs +9 -0
- package/dist/components/ui/sidebar-nav.js +2 -4
- package/dist/components/ui/sidebar-nav.mjs +1 -1
- package/dist/components/ui/signup-shell.js +3 -2
- package/dist/components/ui/signup-shell.mjs +2 -2
- package/dist/components/ui/stepper.js +3 -2
- package/dist/components/ui/stepper.mjs +1 -1
- package/dist/index.js +2122 -1881
- package/dist/index.mjs +56 -46
- package/dist/styles.css +1 -1
- package/package.json +11 -1
- package/src/components/index.tsx +26 -2
- package/src/components/ui/add-column-modal.tsx +9 -58
- package/src/components/ui/ai-conversations.tsx +308 -42
- package/src/components/ui/appointment-availability-settings.tsx +2 -35
- package/src/components/ui/appointment-book-dialog.tsx +25 -48
- package/src/components/ui/integration-card.tsx +88 -0
- package/src/components/ui/kanban-column.tsx +0 -7
- package/src/components/ui/onboarding-layout.tsx +102 -1
- package/src/components/ui/opportunity-card.tsx +5 -53
- package/src/components/ui/pipeline-board.tsx +0 -3
- package/src/components/ui/pipeline-chart.tsx +47 -53
- package/src/components/ui/selectable-card.tsx +37 -0
- package/src/components/ui/sidebar-nav.tsx +0 -2
- package/src/components/ui/stepper.tsx +3 -2
- package/src/lib/format-date.ts +6 -6
- package/src/styles/styles-css.ts +1 -1
- package/tsup.config.ts +2 -0
- 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
|
-
|
|
389
|
-
|
|
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
|
-
//
|
|
623
|
-
|
|
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
|
-
<
|
|
716
|
+
<button
|
|
717
|
+
type="button"
|
|
718
|
+
aria-label={label}
|
|
719
|
+
aria-pressed={pressed}
|
|
720
|
+
onClick={onToggle}
|
|
626
721
|
className={cn(
|
|
627
|
-
"flex items-center
|
|
628
|
-
|
|
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
|
-
<
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
|
753
|
+
"flex flex-col border-t border-border bg-background",
|
|
651
754
|
className,
|
|
652
755
|
)}
|
|
653
756
|
>
|
|
654
|
-
<
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
<
|
|
672
|
-
|
|
673
|
-
|
|
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) => (
|