@wealthx/shadcn 1.5.10 → 1.5.12
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 +114 -114
- package/CHANGELOG.md +12 -0
- package/dist/{chunk-AANINK2B.mjs → chunk-2KNQZG5S.mjs} +1 -1
- package/dist/chunk-3KLJ4XRE.mjs +375 -0
- package/dist/{chunk-6U4NQGVM.mjs → chunk-4X4MGYHE.mjs} +2 -2
- package/dist/{chunk-CEEVYRQA.mjs → chunk-67DGIPQ4.mjs} +1 -1
- package/dist/{chunk-7UIL5UN3.mjs → chunk-7II6QRCZ.mjs} +1 -1
- package/dist/{chunk-W5QJ57PU.mjs → chunk-7LN5OGC2.mjs} +1 -1
- package/dist/{chunk-ZXEUBBHJ.mjs → chunk-7TMPOZDE.mjs} +1 -1
- package/dist/{chunk-AHSCWXYJ.mjs → chunk-AJUAJC5O.mjs} +1 -1
- package/dist/{chunk-3CGM3QXQ.mjs → chunk-AKWN5ZQG.mjs} +2 -2
- package/dist/{chunk-O5CP6VP6.mjs → chunk-CPM6P63C.mjs} +56 -44
- package/dist/{chunk-FRT3S72S.mjs → chunk-CQ7HKBEX.mjs} +1 -1
- package/dist/{chunk-54TRNCID.mjs → chunk-EB626HVW.mjs} +78 -11
- package/dist/{chunk-E2BNCA6L.mjs → chunk-EHQL64B7.mjs} +1 -1
- package/dist/{chunk-2WCIORP7.mjs → chunk-EXI64H46.mjs} +1 -1
- package/dist/{chunk-BBXSNDS3.mjs → chunk-FQYFPHDO.mjs} +1 -1
- package/dist/{chunk-3VZ6CYY2.mjs → chunk-GAXNO4JB.mjs} +1 -1
- package/dist/{chunk-3WGFIFP6.mjs → chunk-I4P7RXAE.mjs} +1 -1
- package/dist/{chunk-Z2BW5T7P.mjs → chunk-IODGRCQG.mjs} +1 -1
- package/dist/{chunk-GS47ZSSA.mjs → chunk-J7KQON2N.mjs} +20 -5
- package/dist/{chunk-IQGKOT7A.mjs → chunk-K35TFQUB.mjs} +1 -1
- package/dist/{chunk-4DO3WM7V.mjs → chunk-K4VWSDJJ.mjs} +1 -1
- package/dist/{chunk-KWD6GANL.mjs → chunk-MPA2HV5U.mjs} +1 -1
- package/dist/{chunk-5LZZYODG.mjs → chunk-QHAMVWDG.mjs} +19 -1
- package/dist/{chunk-XUCDPAVI.mjs → chunk-R6U246E4.mjs} +2 -2
- package/dist/{chunk-VCDGLN25.mjs → chunk-S6AYZJYO.mjs} +47 -21
- package/dist/{chunk-WL6WVV47.mjs → chunk-X6RC5UWB.mjs} +1 -1
- package/dist/{chunk-4BHDDLWK.mjs → chunk-XAS6KBIG.mjs} +2 -2
- package/dist/{chunk-VWZS32ZQ.mjs → chunk-XYWEGBAA.mjs} +1 -1
- package/dist/{chunk-54MTIKNC.mjs → chunk-YV7XF32X.mjs} +49 -24
- package/dist/{chunk-E5EDZQ5J.mjs → chunk-ZA44WICP.mjs} +1 -1
- package/dist/components/ui/advisor-card.js +144 -55
- package/dist/components/ui/advisor-card.mjs +5 -2
- package/dist/components/ui/agent-evaluation-toast.js +1 -1
- package/dist/components/ui/agent-evaluation-toast.mjs +2 -2
- package/dist/components/ui/ai-assistant-drawer.js +1 -1
- package/dist/components/ui/ai-assistant-drawer.mjs +2 -2
- package/dist/components/ui/ai-builder.js +1 -1
- package/dist/components/ui/ai-builder.mjs +2 -2
- package/dist/components/ui/ai-conversations.js +71 -4
- package/dist/components/ui/ai-conversations.mjs +3 -3
- package/dist/components/ui/appointment-action-dialogs.js +1 -1
- package/dist/components/ui/appointment-action-dialogs.mjs +3 -3
- package/dist/components/ui/appointment-book-dialog.js +19 -4
- package/dist/components/ui/appointment-book-dialog.mjs +3 -3
- package/dist/components/ui/appointment-calendar-view.js +1 -1
- package/dist/components/ui/appointment-calendar-view.mjs +2 -2
- package/dist/components/ui/appointment-detail-sheet.js +1 -1
- package/dist/components/ui/appointment-detail-sheet.mjs +4 -4
- package/dist/components/ui/appointment-gmail-connect.js +1 -1
- package/dist/components/ui/appointment-gmail-connect.mjs +2 -2
- package/dist/components/ui/appointment-time-slot-picker.js +1 -1
- package/dist/components/ui/appointment-time-slot-picker.mjs +2 -2
- package/dist/components/ui/appointment-upcoming-card.js +1 -1
- package/dist/components/ui/appointment-upcoming-card.mjs +3 -3
- package/dist/components/ui/badge.js +1 -1
- package/dist/components/ui/badge.mjs +1 -1
- package/dist/components/ui/bank-statement-generate-dialog.js +61 -46
- package/dist/components/ui/bank-statement-generate-dialog.mjs +1 -1
- package/dist/components/ui/chat-widget-primitives.js +1 -1
- package/dist/components/ui/chat-widget-primitives.mjs +2 -2
- package/dist/components/ui/chat-widget.js +1 -1
- package/dist/components/ui/chat-widget.mjs +3 -3
- package/dist/components/ui/chip.js +1 -1
- package/dist/components/ui/chip.mjs +2 -2
- package/dist/components/ui/contact-alert-dialog/index.js +19 -1
- package/dist/components/ui/contact-alert-dialog/index.mjs +1 -1
- package/dist/components/ui/dashboard-transactions-table.js +1 -1
- package/dist/components/ui/dashboard-transactions-table.mjs +2 -2
- package/dist/components/ui/financial-cards.js +1 -1
- package/dist/components/ui/financial-cards.mjs +2 -2
- package/dist/components/ui/financial-sections.js +1 -1
- package/dist/components/ui/financial-sections.mjs +3 -3
- package/dist/components/ui/income-summary-component.js +1 -1
- package/dist/components/ui/income-summary-component.mjs +1 -1
- package/dist/components/ui/integration-card.js +1 -1
- package/dist/components/ui/integration-card.mjs +2 -2
- package/dist/components/ui/kanban-column.js +46 -23
- package/dist/components/ui/kanban-column.mjs +4 -4
- package/dist/components/ui/loan-applicant-information.js +1 -1
- package/dist/components/ui/loan-applicant-information.mjs +1 -1
- package/dist/components/ui/loan-application-badge.js +1 -1
- package/dist/components/ui/loan-application-badge.mjs +2 -2
- package/dist/components/ui/opportunity-card.js +46 -23
- package/dist/components/ui/opportunity-card.mjs +3 -3
- package/dist/components/ui/opportunity-summary-tab.js +1 -1
- package/dist/components/ui/opportunity-summary-tab.mjs +3 -3
- package/dist/components/ui/pipeline-board.js +46 -23
- package/dist/components/ui/pipeline-board.mjs +5 -5
- package/dist/components/ui/pipeline-primitives.js +1 -1
- package/dist/components/ui/pipeline-primitives.mjs +2 -2
- package/dist/components/ui/property-asset-card.js +1 -1
- package/dist/components/ui/property-asset-card.mjs +1 -1
- package/dist/components/ui/resource-center.js +1 -1
- package/dist/components/ui/resource-center.mjs +2 -2
- package/dist/components/ui/share-details-dialog.js +326 -30
- package/dist/components/ui/share-details-dialog.mjs +4 -1
- package/dist/components/ui/stage-timeline.js +1 -1
- package/dist/components/ui/stage-timeline.mjs +3 -3
- package/dist/index.js +583 -232
- package/dist/index.mjs +45 -43
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/index.tsx +4 -0
- package/src/components/ui/advisor-card.tsx +75 -25
- package/src/components/ui/ai-conversations.tsx +157 -23
- package/src/components/ui/appointment-book-dialog.tsx +26 -3
- package/src/components/ui/appointment-time-slot-picker.tsx +1 -0
- package/src/components/ui/badge.tsx +1 -1
- package/src/components/ui/bank-statement-generate-dialog.tsx +84 -61
- package/src/components/ui/contact-alert-dialog/contact-alert-dialog.tsx +19 -1
- package/src/components/ui/opportunity-card.tsx +56 -20
- package/src/components/ui/share-details-dialog.tsx +251 -0
- package/src/styles/styles-css.ts +1 -1
- package/dist/chunk-OZ2R6ERP.mjs +0 -174
package/package.json
CHANGED
package/src/components/index.tsx
CHANGED
|
@@ -73,6 +73,7 @@ export type {
|
|
|
73
73
|
AdvisorCardProps,
|
|
74
74
|
AdvisorInviteCardProps,
|
|
75
75
|
AdvisorAppointmentStrip,
|
|
76
|
+
AdvisorCardMenuItem,
|
|
76
77
|
} from "./ui/advisor-card";
|
|
77
78
|
|
|
78
79
|
export { AppointmentUpcomingCard } from "./ui/appointment-upcoming-card";
|
|
@@ -492,10 +493,13 @@ export type {
|
|
|
492
493
|
export {
|
|
493
494
|
ShareDetailsDialog,
|
|
494
495
|
EmailTemplateDialog,
|
|
496
|
+
ShareContactDialog,
|
|
495
497
|
} from "./ui/share-details-dialog";
|
|
496
498
|
export type {
|
|
497
499
|
ShareDetailsDialogProps,
|
|
498
500
|
EmailTemplateDialogProps,
|
|
501
|
+
ShareContactDialogProps,
|
|
502
|
+
InternalAdvisor,
|
|
499
503
|
} from "./ui/share-details-dialog";
|
|
500
504
|
|
|
501
505
|
export { FilePreviewDialog } from "./ui/file-preview-dialog";
|
|
@@ -3,6 +3,12 @@ import { Badge } from "./badge";
|
|
|
3
3
|
import { Button } from "./button";
|
|
4
4
|
import { Avatar, AvatarFallback, AvatarImage } from "./avatar";
|
|
5
5
|
import { Separator } from "./separator";
|
|
6
|
+
import {
|
|
7
|
+
DropdownMenu,
|
|
8
|
+
DropdownMenuContent,
|
|
9
|
+
DropdownMenuItem,
|
|
10
|
+
DropdownMenuTrigger,
|
|
11
|
+
} from "./dropdown-menu";
|
|
6
12
|
import {
|
|
7
13
|
Calendar,
|
|
8
14
|
CalendarCheck,
|
|
@@ -18,6 +24,18 @@ import type { AppointmentStatus } from "./appointment-time-slot-picker";
|
|
|
18
24
|
// Types
|
|
19
25
|
// ---------------------------------------------------------------------------
|
|
20
26
|
|
|
27
|
+
/** A single item in the ⋮ overflow DropdownMenu */
|
|
28
|
+
export interface AdvisorCardMenuItem {
|
|
29
|
+
/** Display label */
|
|
30
|
+
label: string;
|
|
31
|
+
/** Called when the item is clicked */
|
|
32
|
+
onClick: () => void;
|
|
33
|
+
/** Visual variant — use "destructive" for delete/remove actions */
|
|
34
|
+
variant?: "default" | "destructive";
|
|
35
|
+
/** Disables the item without removing it */
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
/** Appointment data shown in the strip inside the advisor card */
|
|
22
40
|
export interface AdvisorAppointmentStrip {
|
|
23
41
|
status: AppointmentStatus;
|
|
@@ -57,7 +75,12 @@ export interface AdvisorCardProps {
|
|
|
57
75
|
appointments?: AdvisorAppointmentStrip[] | null;
|
|
58
76
|
/** Called when "Refer [name] to Others" is clicked */
|
|
59
77
|
onRefer?: () => void;
|
|
60
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Items to render in the ⋮ overflow DropdownMenu.
|
|
80
|
+
* When provided, the button opens an inline dropdown — `onMoreOptions` is ignored.
|
|
81
|
+
*/
|
|
82
|
+
menuItems?: AdvisorCardMenuItem[];
|
|
83
|
+
/** @deprecated Pass `menuItems` for an inline DropdownMenu instead */
|
|
61
84
|
onMoreOptions?: () => void;
|
|
62
85
|
/** Called when "Book Appointment" is clicked */
|
|
63
86
|
onBookAppointment?: () => void;
|
|
@@ -113,6 +136,7 @@ export function AdvisorCard({
|
|
|
113
136
|
isPrimary = false,
|
|
114
137
|
appointments,
|
|
115
138
|
onRefer,
|
|
139
|
+
menuItems,
|
|
116
140
|
onMoreOptions,
|
|
117
141
|
onBookAppointment,
|
|
118
142
|
onViewAppointment,
|
|
@@ -132,7 +156,7 @@ export function AdvisorCard({
|
|
|
132
156
|
{companyLogoUrl && (
|
|
133
157
|
<AvatarImage src={companyLogoUrl} alt={`${name} company logo`} />
|
|
134
158
|
)}
|
|
135
|
-
<AvatarFallback className="text-
|
|
159
|
+
<AvatarFallback className="text-caption">
|
|
136
160
|
{avatarInitials ??
|
|
137
161
|
(companyName
|
|
138
162
|
? companyName
|
|
@@ -146,14 +170,14 @@ export function AdvisorCard({
|
|
|
146
170
|
</Avatar>
|
|
147
171
|
|
|
148
172
|
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
149
|
-
<p className="text-
|
|
150
|
-
<p className="text-
|
|
173
|
+
<p className="text-label-large leading-tight">{name}</p>
|
|
174
|
+
<p className="text-body-small text-muted-foreground">{role}</p>
|
|
151
175
|
<div className="mt-2 flex flex-col gap-1">
|
|
152
|
-
<div className="flex items-center gap-1.5 text-
|
|
176
|
+
<div className="flex items-center gap-1.5 text-body-small text-muted-foreground">
|
|
153
177
|
<Phone className="h-4 w-4 shrink-0" />
|
|
154
178
|
<span>{phone}</span>
|
|
155
179
|
</div>
|
|
156
|
-
<div className="flex items-center gap-1.5 text-
|
|
180
|
+
<div className="flex items-center gap-1.5 text-body-small text-muted-foreground">
|
|
157
181
|
<Mail className="h-4 w-4 shrink-0" />
|
|
158
182
|
<span>{email}</span>
|
|
159
183
|
</div>
|
|
@@ -163,15 +187,44 @@ export function AdvisorCard({
|
|
|
163
187
|
{/* Badge + overflow menu */}
|
|
164
188
|
<div className="flex shrink-0 items-center gap-1.5">
|
|
165
189
|
{isPrimary && <Badge variant="success">Primary</Badge>}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
190
|
+
{menuItems && menuItems.length > 0 ? (
|
|
191
|
+
<DropdownMenu>
|
|
192
|
+
<DropdownMenuTrigger
|
|
193
|
+
render={
|
|
194
|
+
<Button
|
|
195
|
+
variant="ghost"
|
|
196
|
+
size="icon"
|
|
197
|
+
className="h-7 w-7"
|
|
198
|
+
aria-label="More options"
|
|
199
|
+
/>
|
|
200
|
+
}
|
|
201
|
+
>
|
|
202
|
+
<MoreVertical className="h-4 w-4" />
|
|
203
|
+
</DropdownMenuTrigger>
|
|
204
|
+
<DropdownMenuContent align="end">
|
|
205
|
+
{menuItems.map((item, i) => (
|
|
206
|
+
<DropdownMenuItem
|
|
207
|
+
key={i}
|
|
208
|
+
variant={item.variant}
|
|
209
|
+
disabled={item.disabled}
|
|
210
|
+
onClick={item.onClick}
|
|
211
|
+
>
|
|
212
|
+
{item.label}
|
|
213
|
+
</DropdownMenuItem>
|
|
214
|
+
))}
|
|
215
|
+
</DropdownMenuContent>
|
|
216
|
+
</DropdownMenu>
|
|
217
|
+
) : (
|
|
218
|
+
<Button
|
|
219
|
+
variant="ghost"
|
|
220
|
+
size="icon"
|
|
221
|
+
className="h-7 w-7"
|
|
222
|
+
onClick={onMoreOptions}
|
|
223
|
+
aria-label="More options"
|
|
224
|
+
>
|
|
225
|
+
<MoreVertical className="h-4 w-4" />
|
|
226
|
+
</Button>
|
|
227
|
+
)}
|
|
175
228
|
</div>
|
|
176
229
|
</div>
|
|
177
230
|
|
|
@@ -189,19 +242,16 @@ export function AdvisorCard({
|
|
|
189
242
|
<CalendarCheck className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
190
243
|
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
191
244
|
<div className="flex items-center gap-2">
|
|
192
|
-
<Badge
|
|
193
|
-
variant={STATUS_VARIANT[appt.status]}
|
|
194
|
-
className="text-[10px]"
|
|
195
|
-
>
|
|
245
|
+
<Badge variant={STATUS_VARIANT[appt.status]}>
|
|
196
246
|
{STATUS_LABEL[appt.status]}
|
|
197
247
|
</Badge>
|
|
198
248
|
{appt.appointmentType && (
|
|
199
|
-
<span className="truncate text-
|
|
249
|
+
<span className="truncate text-h6">
|
|
200
250
|
{appt.appointmentType}
|
|
201
251
|
</span>
|
|
202
252
|
)}
|
|
203
253
|
</div>
|
|
204
|
-
<p className="whitespace-nowrap text-
|
|
254
|
+
<p className="whitespace-nowrap text-body-small text-muted-foreground">
|
|
205
255
|
{appt.date} · {appt.timeStart}–{appt.timeEnd}
|
|
206
256
|
</p>
|
|
207
257
|
</div>
|
|
@@ -223,7 +273,7 @@ export function AdvisorCard({
|
|
|
223
273
|
/* Empty state */
|
|
224
274
|
<div className="flex items-center gap-3 px-4 py-3">
|
|
225
275
|
<Calendar className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
226
|
-
<p className="flex-1 text-
|
|
276
|
+
<p className="flex-1 text-caption text-muted-foreground">
|
|
227
277
|
No upcoming appointments
|
|
228
278
|
</p>
|
|
229
279
|
</div>
|
|
@@ -246,7 +296,7 @@ export function AdvisorCard({
|
|
|
246
296
|
)}
|
|
247
297
|
{onRefer && (
|
|
248
298
|
<Button
|
|
249
|
-
variant="
|
|
299
|
+
variant="default"
|
|
250
300
|
size="sm"
|
|
251
301
|
className="w-full"
|
|
252
302
|
onClick={onRefer}
|
|
@@ -274,8 +324,8 @@ export function AdvisorInviteCard({ onInvite }: AdvisorInviteCardProps) {
|
|
|
274
324
|
<Plus className="h-5 w-5 text-muted-foreground" />
|
|
275
325
|
</div>
|
|
276
326
|
<div className="flex flex-col gap-1">
|
|
277
|
-
<p className="text-
|
|
278
|
-
<p className="text-
|
|
327
|
+
<p className="text-label-large">Add Another Advisor</p>
|
|
328
|
+
<p className="text-caption text-muted-foreground">
|
|
279
329
|
Connect more advisors to your account
|
|
280
330
|
</p>
|
|
281
331
|
</div>
|
|
@@ -659,16 +659,31 @@ export function ChatBubble({ message, className }: ChatBubbleProps) {
|
|
|
659
659
|
// ChatComposer
|
|
660
660
|
// ---------------------------------------------------------------------------
|
|
661
661
|
|
|
662
|
+
export interface AiConvEmailPayload {
|
|
663
|
+
content: string;
|
|
664
|
+
to: string;
|
|
665
|
+
cc: string;
|
|
666
|
+
subject: string;
|
|
667
|
+
}
|
|
668
|
+
|
|
662
669
|
export interface ChatComposerProps {
|
|
663
670
|
mode: AiConvMode;
|
|
664
671
|
/** Active reply channel. Defaults to "chat". */
|
|
665
672
|
channel?: AiConvChannel;
|
|
666
673
|
onChannelChange?: (channel: AiConvChannel) => void;
|
|
674
|
+
/**
|
|
675
|
+
* When true, the Email tab is shown in the composer. Defaults to false —
|
|
676
|
+
* consumers must opt in once their tenant's email integration is wired up.
|
|
677
|
+
*/
|
|
678
|
+
isEmailIntegrated?: boolean;
|
|
667
679
|
/** Lead's email address — pre-fills the To field in email compose. */
|
|
668
680
|
contactEmail?: string;
|
|
669
681
|
inputValue?: string;
|
|
670
682
|
onInputChange?: (v: string) => void;
|
|
671
|
-
|
|
683
|
+
/** Fired when the user sends a chat message. */
|
|
684
|
+
onSend?: (content: string) => void;
|
|
685
|
+
/** Fired when the user sends an email. */
|
|
686
|
+
onSendEmail?: (payload: AiConvEmailPayload) => void;
|
|
672
687
|
onTakeOver?: () => void;
|
|
673
688
|
onLetAiHandle?: () => void;
|
|
674
689
|
className?: string;
|
|
@@ -678,16 +693,21 @@ export function ChatComposer({
|
|
|
678
693
|
mode,
|
|
679
694
|
channel: channelProp = "chat",
|
|
680
695
|
onChannelChange,
|
|
696
|
+
isEmailIntegrated = false,
|
|
681
697
|
contactEmail = "",
|
|
682
698
|
inputValue = "",
|
|
683
699
|
onInputChange,
|
|
684
700
|
onSend,
|
|
701
|
+
onSendEmail,
|
|
685
702
|
onTakeOver,
|
|
686
703
|
onLetAiHandle,
|
|
687
704
|
className,
|
|
688
705
|
}: ChatComposerProps) {
|
|
689
|
-
// Semi-controlled: owns channel state for uncontrolled usage, notifies parent on change
|
|
690
|
-
|
|
706
|
+
// Semi-controlled: owns channel state for uncontrolled usage, notifies parent on change.
|
|
707
|
+
// Force chat when email isn't integrated so the panel never lands on a hidden tab.
|
|
708
|
+
const [channel, setChannel] = React.useState<AiConvChannel>(
|
|
709
|
+
isEmailIntegrated ? channelProp : "chat",
|
|
710
|
+
);
|
|
691
711
|
const [emailTo, setEmailTo] = React.useState(contactEmail);
|
|
692
712
|
const [emailCc, setEmailCc] = React.useState("");
|
|
693
713
|
const [showCc, setShowCc] = React.useState(false);
|
|
@@ -754,23 +774,25 @@ export function ChatComposer({
|
|
|
754
774
|
className,
|
|
755
775
|
)}
|
|
756
776
|
>
|
|
757
|
-
|
|
758
|
-
<
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
<
|
|
764
|
-
<
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
<
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
777
|
+
{isEmailIntegrated && (
|
|
778
|
+
<div className="border-b border-border px-3 py-2">
|
|
779
|
+
<Tabs
|
|
780
|
+
value={channel}
|
|
781
|
+
onValueChange={(v) => v && handleChannelChange(v as AiConvChannel)}
|
|
782
|
+
>
|
|
783
|
+
<TabsList variant="default" className="w-full">
|
|
784
|
+
<TabsTrigger value="chat" className="flex-1 gap-1.5">
|
|
785
|
+
<MessageSquare className="size-3.5" />
|
|
786
|
+
Chat
|
|
787
|
+
</TabsTrigger>
|
|
788
|
+
<TabsTrigger value="email" className="flex-1 gap-1.5">
|
|
789
|
+
<Mail className="size-3.5" />
|
|
790
|
+
Email
|
|
791
|
+
</TabsTrigger>
|
|
792
|
+
</TabsList>
|
|
793
|
+
</Tabs>
|
|
794
|
+
</div>
|
|
795
|
+
)}
|
|
774
796
|
|
|
775
797
|
{mode === "ai" ? (
|
|
776
798
|
<div className="flex items-center gap-2 bg-muted/30 px-4 py-2.5 text-[12px] text-muted-foreground">
|
|
@@ -867,7 +889,14 @@ export function ChatComposer({
|
|
|
867
889
|
</div>
|
|
868
890
|
<Button
|
|
869
891
|
size="sm"
|
|
870
|
-
onClick={() =>
|
|
892
|
+
onClick={() =>
|
|
893
|
+
onSendEmail?.({
|
|
894
|
+
content: inputValue,
|
|
895
|
+
to: emailTo,
|
|
896
|
+
cc: emailCc,
|
|
897
|
+
subject: emailSubject,
|
|
898
|
+
})
|
|
899
|
+
}
|
|
871
900
|
disabled={!inputValue.trim() || !emailTo.trim()}
|
|
872
901
|
>
|
|
873
902
|
<Send className="mr-1.5 size-3.5" />
|
|
@@ -920,9 +949,16 @@ export interface ChatThreadProps {
|
|
|
920
949
|
/** Active reply channel — "chat" (default) or "email". */
|
|
921
950
|
channel?: AiConvChannel;
|
|
922
951
|
onChannelChange?: (channel: AiConvChannel) => void;
|
|
952
|
+
/**
|
|
953
|
+
* When true, the Email tab is shown in the composer. Defaults to false.
|
|
954
|
+
*/
|
|
955
|
+
isEmailIntegrated?: boolean;
|
|
923
956
|
inputValue?: string;
|
|
924
957
|
onInputChange?: (v: string) => void;
|
|
925
|
-
|
|
958
|
+
/** Fired when the user sends a chat message. */
|
|
959
|
+
onSend?: (content: string) => void;
|
|
960
|
+
/** Fired when the user sends an email. */
|
|
961
|
+
onSendEmail?: (payload: AiConvEmailPayload) => void;
|
|
926
962
|
onTakeOver?: () => void;
|
|
927
963
|
onLetAiHandle?: () => void;
|
|
928
964
|
onReopen?: () => void;
|
|
@@ -930,6 +966,12 @@ export interface ChatThreadProps {
|
|
|
930
966
|
onUnmarkUrgent?: () => void;
|
|
931
967
|
onArchive?: () => void;
|
|
932
968
|
onAssignToAdvisor?: () => void;
|
|
969
|
+
/** True when older messages can be loaded (e.g. paginated history). */
|
|
970
|
+
hasMoreMessages?: boolean;
|
|
971
|
+
/** True while a `onLoadMoreMessages` request is in-flight. */
|
|
972
|
+
isLoadingMoreMessages?: boolean;
|
|
973
|
+
/** Fired when the consumer should fetch older messages. */
|
|
974
|
+
onLoadMoreMessages?: () => void;
|
|
933
975
|
/** Mobile only — back to conversation list. */
|
|
934
976
|
onBack?: () => void;
|
|
935
977
|
/** Mobile only — show lead info panel. */
|
|
@@ -945,9 +987,11 @@ export function ChatThread({
|
|
|
945
987
|
isAiTyping = false,
|
|
946
988
|
channel,
|
|
947
989
|
onChannelChange,
|
|
990
|
+
isEmailIntegrated,
|
|
948
991
|
inputValue,
|
|
949
992
|
onInputChange,
|
|
950
993
|
onSend,
|
|
994
|
+
onSendEmail,
|
|
951
995
|
onTakeOver,
|
|
952
996
|
onLetAiHandle,
|
|
953
997
|
onReopen,
|
|
@@ -955,6 +999,9 @@ export function ChatThread({
|
|
|
955
999
|
onUnmarkUrgent,
|
|
956
1000
|
onArchive,
|
|
957
1001
|
onAssignToAdvisor,
|
|
1002
|
+
hasMoreMessages,
|
|
1003
|
+
isLoadingMoreMessages,
|
|
1004
|
+
onLoadMoreMessages,
|
|
958
1005
|
onBack,
|
|
959
1006
|
onShowLeadInfo,
|
|
960
1007
|
className,
|
|
@@ -962,6 +1009,61 @@ export function ChatThread({
|
|
|
962
1009
|
const aiIsHandling = mode === "ai";
|
|
963
1010
|
const isClosed = status === "closed";
|
|
964
1011
|
|
|
1012
|
+
const scrollRef = React.useRef<HTMLDivElement>(null);
|
|
1013
|
+
// Captures scrollHeight just before older messages are prepended, so we can
|
|
1014
|
+
// restore the user's visible scroll offset once the new nodes render.
|
|
1015
|
+
const preLoadScrollHeightRef = React.useRef<number | null>(null);
|
|
1016
|
+
|
|
1017
|
+
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
|
1018
|
+
if (!hasMoreMessages || isLoadingMoreMessages || !onLoadMoreMessages) {
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (e.currentTarget.scrollTop <= 80) {
|
|
1022
|
+
preLoadScrollHeightRef.current = e.currentTarget.scrollHeight;
|
|
1023
|
+
onLoadMoreMessages();
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
// Tracks the last "tail" message id so we can tell an append (new message,
|
|
1028
|
+
// tail changed) apart from a prepend (older history loaded, tail unchanged).
|
|
1029
|
+
const prevLastMessageIdRef = React.useRef<string | undefined>(undefined);
|
|
1030
|
+
const prevContactIdRef = React.useRef<string>(contact.id);
|
|
1031
|
+
|
|
1032
|
+
React.useLayoutEffect(() => {
|
|
1033
|
+
const el = scrollRef.current;
|
|
1034
|
+
if (!el) return;
|
|
1035
|
+
|
|
1036
|
+
// Prepend (older messages just loaded) — restore scroll so the user
|
|
1037
|
+
// stays anchored to the message they were reading.
|
|
1038
|
+
if (preLoadScrollHeightRef.current !== null) {
|
|
1039
|
+
el.scrollTop = el.scrollHeight - preLoadScrollHeightRef.current;
|
|
1040
|
+
preLoadScrollHeightRef.current = null;
|
|
1041
|
+
prevLastMessageIdRef.current = messages[messages.length - 1]?.id;
|
|
1042
|
+
prevContactIdRef.current = contact.id;
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const currentLastId = messages[messages.length - 1]?.id;
|
|
1047
|
+
const contactChanged = prevContactIdRef.current !== contact.id;
|
|
1048
|
+
const tailChanged = prevLastMessageIdRef.current !== currentLastId;
|
|
1049
|
+
|
|
1050
|
+
// Opening a conversation or appending a new message (sent, received,
|
|
1051
|
+
// or system) — pin to the bottom.
|
|
1052
|
+
if (contactChanged || tailChanged) {
|
|
1053
|
+
el.scrollTop = el.scrollHeight;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
prevLastMessageIdRef.current = currentLastId;
|
|
1057
|
+
prevContactIdRef.current = contact.id;
|
|
1058
|
+
}, [contact.id, messages]);
|
|
1059
|
+
|
|
1060
|
+
// Typing indicator adds DOM height — keep the view pinned to bottom.
|
|
1061
|
+
React.useLayoutEffect(() => {
|
|
1062
|
+
if (!isAiTyping) return;
|
|
1063
|
+
const el = scrollRef.current;
|
|
1064
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
1065
|
+
}, [isAiTyping]);
|
|
1066
|
+
|
|
965
1067
|
return (
|
|
966
1068
|
<div className={cn("flex flex-col bg-background", className)}>
|
|
967
1069
|
{/* Header */}
|
|
@@ -1071,9 +1173,16 @@ export function ChatThread({
|
|
|
1071
1173
|
|
|
1072
1174
|
{/* Messages */}
|
|
1073
1175
|
<div
|
|
1176
|
+
ref={scrollRef}
|
|
1177
|
+
onScroll={handleScroll}
|
|
1074
1178
|
className="flex flex-1 flex-col gap-4 overflow-y-auto p-4"
|
|
1075
1179
|
tabIndex={0}
|
|
1076
1180
|
>
|
|
1181
|
+
{isLoadingMoreMessages && (
|
|
1182
|
+
<div className="flex justify-center py-1 text-caption text-muted-foreground">
|
|
1183
|
+
Loading older messages...
|
|
1184
|
+
</div>
|
|
1185
|
+
)}
|
|
1077
1186
|
{messages.length === 0 ? (
|
|
1078
1187
|
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-muted-foreground">
|
|
1079
1188
|
<MessageSquare className="size-8 opacity-30" />
|
|
@@ -1120,10 +1229,12 @@ export function ChatThread({
|
|
|
1120
1229
|
mode={mode}
|
|
1121
1230
|
channel={channel}
|
|
1122
1231
|
onChannelChange={onChannelChange}
|
|
1232
|
+
isEmailIntegrated={isEmailIntegrated}
|
|
1123
1233
|
contactEmail={contact.email}
|
|
1124
1234
|
inputValue={inputValue}
|
|
1125
1235
|
onInputChange={onInputChange}
|
|
1126
1236
|
onSend={onSend}
|
|
1237
|
+
onSendEmail={onSendEmail}
|
|
1127
1238
|
onTakeOver={onTakeOver}
|
|
1128
1239
|
onLetAiHandle={onLetAiHandle}
|
|
1129
1240
|
/>
|
|
@@ -1584,18 +1695,31 @@ export interface ConversationsPageProps {
|
|
|
1584
1695
|
/** Active reply channel — "chat" (default) or "email". */
|
|
1585
1696
|
channel?: AiConvChannel;
|
|
1586
1697
|
onChannelChange?: (channel: AiConvChannel) => void;
|
|
1698
|
+
/**
|
|
1699
|
+
* When true, the Email tab is shown in the composer. Defaults to false.
|
|
1700
|
+
*/
|
|
1701
|
+
isEmailIntegrated?: boolean;
|
|
1587
1702
|
inputValue?: string;
|
|
1588
1703
|
internalNotes?: string;
|
|
1589
1704
|
showLeadPanel?: boolean;
|
|
1590
1705
|
hasMore?: boolean;
|
|
1591
1706
|
isLoadingMore?: boolean;
|
|
1707
|
+
/** True when older messages can be loaded for the active conversation. */
|
|
1708
|
+
hasMoreMessages?: boolean;
|
|
1709
|
+
/** True while a `onLoadMoreMessages` request is in-flight. */
|
|
1710
|
+
isLoadingMoreMessages?: boolean;
|
|
1711
|
+
/** Fired when the consumer should fetch older messages for the active conversation. */
|
|
1712
|
+
onLoadMoreMessages?: () => void;
|
|
1592
1713
|
onSelectConversation?: (id: string) => void;
|
|
1593
1714
|
onRead?: (id: string) => void;
|
|
1594
1715
|
onSearchChange?: (v: string) => void;
|
|
1595
1716
|
onFilterChange?: (f: AiConvFilterTab) => void;
|
|
1596
1717
|
onChannelFilterChange?: (f: AiConvChannelFilter) => void;
|
|
1597
1718
|
onInputChange?: (v: string) => void;
|
|
1598
|
-
|
|
1719
|
+
/** Fired when the user sends a chat message. */
|
|
1720
|
+
onSend?: (content: string) => void;
|
|
1721
|
+
/** Fired when the user sends an email. */
|
|
1722
|
+
onSendEmail?: (payload: AiConvEmailPayload) => void;
|
|
1599
1723
|
onTakeOver?: () => void;
|
|
1600
1724
|
onLetAiHandle?: () => void;
|
|
1601
1725
|
onReopen?: () => void;
|
|
@@ -1637,11 +1761,15 @@ export function ConversationsPage({
|
|
|
1637
1761
|
onChannelFilterChange,
|
|
1638
1762
|
channel,
|
|
1639
1763
|
onChannelChange,
|
|
1764
|
+
isEmailIntegrated,
|
|
1640
1765
|
inputValue,
|
|
1641
1766
|
internalNotes,
|
|
1642
1767
|
showLeadPanel = true,
|
|
1643
1768
|
hasMore,
|
|
1644
1769
|
isLoadingMore,
|
|
1770
|
+
hasMoreMessages,
|
|
1771
|
+
isLoadingMoreMessages,
|
|
1772
|
+
onLoadMoreMessages,
|
|
1645
1773
|
isAiTyping,
|
|
1646
1774
|
notesSaveStatus,
|
|
1647
1775
|
onSelectConversation,
|
|
@@ -1650,6 +1778,7 @@ export function ConversationsPage({
|
|
|
1650
1778
|
onFilterChange,
|
|
1651
1779
|
onInputChange,
|
|
1652
1780
|
onSend,
|
|
1781
|
+
onSendEmail,
|
|
1653
1782
|
onTakeOver,
|
|
1654
1783
|
onLetAiHandle,
|
|
1655
1784
|
onReopen,
|
|
@@ -1723,9 +1852,11 @@ export function ConversationsPage({
|
|
|
1723
1852
|
isAiTyping={isAiTyping}
|
|
1724
1853
|
channel={channel}
|
|
1725
1854
|
onChannelChange={onChannelChange}
|
|
1855
|
+
isEmailIntegrated={isEmailIntegrated}
|
|
1726
1856
|
inputValue={inputValue}
|
|
1727
1857
|
onInputChange={onInputChange}
|
|
1728
1858
|
onSend={onSend}
|
|
1859
|
+
onSendEmail={onSendEmail}
|
|
1729
1860
|
onTakeOver={onTakeOver}
|
|
1730
1861
|
onLetAiHandle={onLetAiHandle}
|
|
1731
1862
|
onReopen={onReopen}
|
|
@@ -1733,6 +1864,9 @@ export function ConversationsPage({
|
|
|
1733
1864
|
onUnmarkUrgent={onUnmarkUrgent}
|
|
1734
1865
|
onArchive={onArchive}
|
|
1735
1866
|
onAssignToAdvisor={onAssignToAdvisor}
|
|
1867
|
+
hasMoreMessages={hasMoreMessages}
|
|
1868
|
+
isLoadingMoreMessages={isLoadingMoreMessages}
|
|
1869
|
+
onLoadMoreMessages={onLoadMoreMessages}
|
|
1736
1870
|
onBack={() => setMobilePanel("list")}
|
|
1737
1871
|
onShowLeadInfo={() => setMobilePanel("lead")}
|
|
1738
1872
|
className={cn(
|
|
@@ -236,11 +236,14 @@ function ClientSearch({
|
|
|
236
236
|
// Meeting format sub-component
|
|
237
237
|
// ---------------------------------------------------------------------------
|
|
238
238
|
|
|
239
|
-
|
|
239
|
+
interface FormatOption {
|
|
240
240
|
value: AppointmentMeetingFormat;
|
|
241
241
|
label: string;
|
|
242
242
|
icon: React.ReactNode;
|
|
243
|
-
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** All four formats — shown in advisor mode */
|
|
246
|
+
const FORMAT_OPTIONS: FormatOption[] = [
|
|
244
247
|
{ value: "call", label: "Call", icon: <Phone className="h-4 w-4" /> },
|
|
245
248
|
{
|
|
246
249
|
value: "google-meet",
|
|
@@ -259,6 +262,21 @@ const FORMAT_OPTIONS: {
|
|
|
259
262
|
},
|
|
260
263
|
];
|
|
261
264
|
|
|
265
|
+
/** Simplified three-option set shown in client mode */
|
|
266
|
+
const CLIENT_FORMAT_OPTIONS: FormatOption[] = [
|
|
267
|
+
{ value: "call", label: "Call", icon: <Phone className="h-4 w-4" /> },
|
|
268
|
+
{
|
|
269
|
+
value: "online",
|
|
270
|
+
label: "Online Meeting",
|
|
271
|
+
icon: <Video className="h-4 w-4" />,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
value: "offline",
|
|
275
|
+
label: "Offline Meeting",
|
|
276
|
+
icon: <MapPin className="h-4 w-4" />,
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
|
|
262
280
|
function MeetingFormatSection({
|
|
263
281
|
format,
|
|
264
282
|
onFormatChange,
|
|
@@ -269,6 +287,7 @@ function MeetingFormatSection({
|
|
|
269
287
|
advisorOfficeAddress,
|
|
270
288
|
clientHomeAddress,
|
|
271
289
|
isClientMode,
|
|
290
|
+
formatOptions,
|
|
272
291
|
}: {
|
|
273
292
|
format: AppointmentMeetingFormat;
|
|
274
293
|
onFormatChange: (f: AppointmentMeetingFormat) => void;
|
|
@@ -279,11 +298,12 @@ function MeetingFormatSection({
|
|
|
279
298
|
advisorOfficeAddress?: string;
|
|
280
299
|
clientHomeAddress?: string;
|
|
281
300
|
isClientMode?: boolean;
|
|
301
|
+
formatOptions: FormatOption[];
|
|
282
302
|
}) {
|
|
283
303
|
return (
|
|
284
304
|
<div className="flex flex-col gap-2">
|
|
285
305
|
<div className="flex gap-2">
|
|
286
|
-
{
|
|
306
|
+
{formatOptions.map((opt) => (
|
|
287
307
|
<Toggle
|
|
288
308
|
key={opt.value}
|
|
289
309
|
variant="outline"
|
|
@@ -681,6 +701,9 @@ export function AppointmentBookDialog({
|
|
|
681
701
|
advisorOfficeAddress={advisorOfficeAddress}
|
|
682
702
|
clientHomeAddress={clientHomeAddress}
|
|
683
703
|
isClientMode={isClientMode}
|
|
704
|
+
formatOptions={
|
|
705
|
+
isClientMode ? CLIENT_FORMAT_OPTIONS : FORMAT_OPTIONS
|
|
706
|
+
}
|
|
684
707
|
/>
|
|
685
708
|
</div>
|
|
686
709
|
)}
|
|
@@ -14,7 +14,7 @@ import { Slot } from "@/lib/slot";
|
|
|
14
14
|
* This gives softer, more readable badges and works correctly in light and dark mode.
|
|
15
15
|
*/
|
|
16
16
|
const badgeVariants = cva(
|
|
17
|
-
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2.5 py-0.5 text-
|
|
17
|
+
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2.5 py-0.5 text-caption whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
|
|
18
18
|
{
|
|
19
19
|
variants: {
|
|
20
20
|
variant: {
|