@wealthx/shadcn 1.5.24 → 1.5.25

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 (72) hide show
  1. package/.turbo/turbo-build.log +144 -144
  2. package/CHANGELOG.md +6 -0
  3. package/dist/{chunk-RYGZRDP6.mjs → chunk-4FJC64FV.mjs} +468 -410
  4. package/dist/{chunk-FGHM34AV.mjs → chunk-7JVKSZ4O.mjs} +1 -1
  5. package/dist/{chunk-6HTE24TP.mjs → chunk-EY5YPFKX.mjs} +1 -1
  6. package/dist/{chunk-3HFOSFOM.mjs → chunk-IG7DEIWU.mjs} +4 -4
  7. package/dist/{chunk-2VTOF7PW.mjs → chunk-K2KX3NX7.mjs} +1 -1
  8. package/dist/{chunk-NXZ2F4JA.mjs → chunk-KGBLORCQ.mjs} +1 -1
  9. package/dist/{chunk-2SDEURIQ.mjs → chunk-PX2B3Q3A.mjs} +7 -7
  10. package/dist/{chunk-66NM4AX2.mjs → chunk-QM7LU2BR.mjs} +10 -10
  11. package/dist/{chunk-4UCRTTVL.mjs → chunk-RNLIZRAK.mjs} +1 -1
  12. package/dist/{chunk-BWG7AX6X.mjs → chunk-SQ54W3JH.mjs} +1 -1
  13. package/dist/{chunk-RJHE3V4M.mjs → chunk-UMR3HVZF.mjs} +4 -4
  14. package/dist/{chunk-JGUC3KCA.mjs → chunk-VNB5E7SI.mjs} +5 -5
  15. package/dist/{chunk-ETT5JAXF.mjs → chunk-Z5QI7CW2.mjs} +1 -1
  16. package/dist/components/ui/about-you-form.mjs +4 -4
  17. package/dist/components/ui/ai-assistant-drawer.mjs +2 -2
  18. package/dist/components/ui/{ai-conversations.js → ai-conversations/index.js} +818 -763
  19. package/dist/components/ui/ai-conversations/index.mjs +42 -0
  20. package/dist/components/ui/appointment-action-dialogs.mjs +1 -1
  21. package/dist/components/ui/appointment-availability-settings.mjs +5 -5
  22. package/dist/components/ui/appointment-book-dialog.mjs +4 -4
  23. package/dist/components/ui/appointment-detail-sheet.mjs +3 -3
  24. package/dist/components/ui/appointment-upcoming-card.mjs +1 -1
  25. package/dist/components/ui/assets-liabilities-side-card.mjs +6 -6
  26. package/dist/components/ui/backoffice-signup-steps.mjs +4 -4
  27. package/dist/components/ui/bank-statement-generate-dialog.mjs +6 -6
  28. package/dist/components/ui/contact-alert-dialog/index.mjs +2 -2
  29. package/dist/components/ui/create-contact-modal.mjs +3 -3
  30. package/dist/components/ui/date-picker.mjs +2 -2
  31. package/dist/components/ui/expense-detail-item.mjs +3 -3
  32. package/dist/components/ui/expense-work-details.mjs +9 -9
  33. package/dist/components/ui/file-preview-dialog.mjs +2 -2
  34. package/dist/components/ui/financial-drawers.mjs +2 -2
  35. package/dist/components/ui/form-primitives.mjs +2 -2
  36. package/dist/components/ui/frontend-signup-steps.mjs +5 -5
  37. package/dist/components/ui/income-work-details.mjs +3 -3
  38. package/dist/components/ui/kanban-column.mjs +4 -4
  39. package/dist/components/ui/opportunity-card.mjs +1 -1
  40. package/dist/components/ui/opportunity-edit-modals.mjs +6 -6
  41. package/dist/components/ui/opportunity-summary-tab.mjs +8 -8
  42. package/dist/components/ui/pipeline-board.mjs +6 -6
  43. package/dist/components/ui/pipeline-dialogs.mjs +5 -5
  44. package/dist/components/ui/property-report-dialog.mjs +4 -4
  45. package/dist/components/ui/savings-goal-modal.mjs +4 -4
  46. package/dist/components/ui/share-details-dialog.mjs +2 -2
  47. package/dist/components/ui/signup-form-primitives.mjs +1 -1
  48. package/dist/index.js +4913 -4858
  49. package/dist/index.mjs +160 -160
  50. package/dist/styles.css +1 -1
  51. package/package.json +4 -4
  52. package/src/components/index.tsx +1 -0
  53. package/src/components/ui/ai-conversations/helpers.tsx +31 -0
  54. package/src/components/ui/ai-conversations/index.tsx +468 -0
  55. package/src/components/ui/ai-conversations/lead-panel.tsx +517 -0
  56. package/src/components/ui/ai-conversations/list.tsx +335 -0
  57. package/src/components/ui/ai-conversations/thread.tsx +919 -0
  58. package/src/components/ui/ai-conversations/types.ts +83 -0
  59. package/src/styles/styles-css.ts +1 -1
  60. package/tsup.config.ts +1 -1
  61. package/dist/components/ui/ai-conversations.mjs +0 -42
  62. package/src/components/ui/ai-conversations.tsx +0 -2270
  63. package/dist/{chunk-A43XIVO6.mjs → chunk-2PWTXE66.mjs} +3 -3
  64. package/dist/{chunk-PSBQ4I3M.mjs → chunk-53ZB2JWT.mjs} +6 -6
  65. package/dist/{chunk-G6RCC2SF.mjs → chunk-5ST6BK7R.mjs} +3 -3
  66. package/dist/{chunk-H5NI6ZIU.mjs → chunk-734FOOJC.mjs} +3 -3
  67. package/dist/{chunk-ONYADWSO.mjs → chunk-ABVCQWDY.mjs} +3 -3
  68. package/dist/{chunk-AADJ5IT6.mjs → chunk-ECC2LLZM.mjs} +3 -3
  69. package/dist/{chunk-DK55HZPN.mjs → chunk-KENLXFJ7.mjs} +6 -6
  70. package/dist/{chunk-ET4MTPIY.mjs → chunk-LDC6V6DJ.mjs} +3 -3
  71. package/dist/{chunk-3S4XQTAL.mjs → chunk-SO4RB3XB.mjs} +9 -9
  72. package/dist/{chunk-UMF6LLQK.mjs → chunk-TRM3KIHT.mjs} +3 -3
@@ -0,0 +1,517 @@
1
+ import React from "react";
2
+ import {
3
+ ArrowLeft,
4
+ Briefcase,
5
+ Calendar,
6
+ CheckCircle2,
7
+ ChevronRight,
8
+ HelpCircle,
9
+ Link2,
10
+ Mail,
11
+ MapPin,
12
+ Navigation,
13
+ Phone,
14
+ PhoneCall,
15
+ Plus,
16
+ UserPlus,
17
+ Video,
18
+ } from "lucide-react";
19
+ import { cn } from "@/lib/utils";
20
+ import { Badge } from "@/components/ui/badge";
21
+ import { Button } from "@/components/ui/button";
22
+ import { Textarea } from "@/components/ui/textarea";
23
+ import {
24
+ Tooltip,
25
+ TooltipContent,
26
+ TooltipTrigger,
27
+ } from "@/components/ui/tooltip";
28
+ import type {
29
+ AiConvAppointmentData,
30
+ AiConvContact,
31
+ AiConvDataField,
32
+ AiConvMeetingType,
33
+ } from "./types";
34
+ import {
35
+ ContactAvatar,
36
+ displayContactName,
37
+ PANEL_HEADER_HEIGHT,
38
+ } from "./helpers.tsx";
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // AICollectedDataSection
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export interface AICollectedDataSectionProps {
45
+ fields: AiConvDataField[];
46
+ className?: string;
47
+ }
48
+
49
+ export function AICollectedDataSection({
50
+ fields,
51
+ className,
52
+ }: AICollectedDataSectionProps) {
53
+ return (
54
+ <div className={cn("flex flex-col", className)}>
55
+ {fields.map((field, i) => (
56
+ <div
57
+ key={i}
58
+ className="flex items-center justify-between gap-2 border-b border-border/40 py-1.5 last:border-b-0"
59
+ >
60
+ <span className="shrink-0 text-sm text-muted-foreground">
61
+ {field.label}
62
+ </span>
63
+ <div className="flex items-center gap-1.5">
64
+ <span className="text-right text-sm font-medium text-foreground">
65
+ {field.value}
66
+ </span>
67
+ {field.confidence === "confirmed" ? (
68
+ <CheckCircle2 className="size-3 shrink-0 text-success-text" />
69
+ ) : (
70
+ <HelpCircle className="size-3 shrink-0 text-warning-text" />
71
+ )}
72
+ </div>
73
+ </div>
74
+ ))}
75
+ </div>
76
+ );
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // LeadInfoPanel — internal helpers
81
+ // ---------------------------------------------------------------------------
82
+
83
+ const APPOINTMENT_STATUS_LABEL: Record<
84
+ AiConvAppointmentData["status"],
85
+ string
86
+ > = {
87
+ requested: "Lead requested",
88
+ confirmed: "Confirmed",
89
+ pending: "Pending confirmation",
90
+ cancelled: "Cancelled",
91
+ };
92
+
93
+ const MEETING_ICON: Record<AiConvMeetingType, React.ElementType> = {
94
+ video: Video,
95
+ phone: Phone,
96
+ "in-person": MapPin,
97
+ };
98
+
99
+ const MEETING_LABEL: Record<AiConvMeetingType, string> = {
100
+ video: "Video Call",
101
+ phone: "Phone Call",
102
+ "in-person": "In Person",
103
+ };
104
+
105
+ const MEETING_DETAIL_ICON: Record<AiConvMeetingType, React.ElementType> = {
106
+ video: Link2,
107
+ phone: PhoneCall,
108
+ "in-person": Navigation,
109
+ };
110
+
111
+ function MeetingDetailRow({
112
+ meetingType,
113
+ detail,
114
+ }: {
115
+ meetingType: AiConvMeetingType;
116
+ detail: string;
117
+ }) {
118
+ const DetailIcon = MEETING_DETAIL_ICON[meetingType];
119
+ const isLink = detail.startsWith("http");
120
+ return (
121
+ <div className="flex items-center gap-2">
122
+ <DetailIcon className="size-4 shrink-0 text-muted-foreground" />
123
+ {isLink ? (
124
+ <a
125
+ href={detail}
126
+ target="_blank"
127
+ rel="noopener noreferrer"
128
+ className="text-sm text-primary underline underline-offset-2 break-all hover:text-primary/80"
129
+ >
130
+ {detail}
131
+ </a>
132
+ ) : (
133
+ <span className="text-sm text-muted-foreground break-all">
134
+ {detail}
135
+ </span>
136
+ )}
137
+ </div>
138
+ );
139
+ }
140
+
141
+ interface AppointmentSectionProps {
142
+ appointment: AiConvAppointmentData;
143
+ contactId: string;
144
+ isAnonymous: boolean;
145
+ onApproveAppointment?: () => void;
146
+ onDeclineAppointment?: () => void;
147
+ onRescheduleAppointment?: (contactId: string) => void;
148
+ }
149
+
150
+ function AppointmentSection({
151
+ appointment,
152
+ contactId,
153
+ isAnonymous,
154
+ onApproveAppointment,
155
+ onDeclineAppointment,
156
+ onRescheduleAppointment,
157
+ }: AppointmentSectionProps) {
158
+ const AppointmentIcon = MEETING_ICON[appointment.meetingType];
159
+ const canReschedule = !isAnonymous && !!onRescheduleAppointment;
160
+
161
+ return (
162
+ <div className="flex flex-col gap-2">
163
+ <div className="flex items-center gap-2">
164
+ <Calendar className="size-3.5 shrink-0 text-muted-foreground" />
165
+ <span className="text-sm font-medium text-foreground">
166
+ {appointment.datetime}
167
+ </span>
168
+ </div>
169
+ <div className="flex items-center gap-2">
170
+ <AppointmentIcon className="size-4 shrink-0 text-muted-foreground" />
171
+ <span className="text-sm text-muted-foreground">
172
+ {MEETING_LABEL[appointment.meetingType]}
173
+ </span>
174
+ </div>
175
+ {appointment.meetingDetail && (
176
+ <MeetingDetailRow
177
+ meetingType={appointment.meetingType}
178
+ detail={appointment.meetingDetail}
179
+ />
180
+ )}
181
+ <span
182
+ className={cn("text-sm font-medium", {
183
+ "text-warning-text": appointment.status === "requested",
184
+ "text-success-text": appointment.status === "confirmed",
185
+ "text-muted-foreground": appointment.status === "pending",
186
+ "text-destructive line-through": appointment.status === "cancelled",
187
+ })}
188
+ >
189
+ {APPOINTMENT_STATUS_LABEL[appointment.status]}
190
+ </span>
191
+
192
+ {appointment.status === "requested" && (
193
+ <div className="flex gap-2 pt-1">
194
+ <Button size="sm" className="flex-1" onClick={onApproveAppointment}>
195
+ Approve
196
+ </Button>
197
+ <Button
198
+ variant="outline"
199
+ size="sm"
200
+ className="flex-1"
201
+ onClick={onDeclineAppointment}
202
+ >
203
+ Decline
204
+ </Button>
205
+ {canReschedule && (
206
+ <Button
207
+ variant="ghost"
208
+ size="sm"
209
+ className="flex-1"
210
+ onClick={() => onRescheduleAppointment!(contactId)}
211
+ >
212
+ Reschedule
213
+ </Button>
214
+ )}
215
+ </div>
216
+ )}
217
+
218
+ {(appointment.status === "confirmed" ||
219
+ appointment.status === "cancelled") &&
220
+ canReschedule && (
221
+ <Button
222
+ variant="outline"
223
+ size="sm"
224
+ className="mt-1 w-full justify-start"
225
+ onClick={() => onRescheduleAppointment!(contactId)}
226
+ >
227
+ Reschedule
228
+ </Button>
229
+ )}
230
+ </div>
231
+ );
232
+ }
233
+
234
+ function PanelSectionHeader({ children }: { children: React.ReactNode }) {
235
+ return (
236
+ <p className="mb-2.5 text-overline text-muted-foreground">{children}</p>
237
+ );
238
+ }
239
+
240
+ function PanelSection({
241
+ children,
242
+ last = false,
243
+ }: {
244
+ children: React.ReactNode;
245
+ last?: boolean;
246
+ }) {
247
+ return (
248
+ <div className={cn("px-4 py-4", !last && "border-b border-border")}>
249
+ {children}
250
+ </div>
251
+ );
252
+ }
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // LeadInfoPanel
256
+ // ---------------------------------------------------------------------------
257
+
258
+ export interface LeadInfoPanelProps {
259
+ contact: AiConvContact;
260
+ firstSeen?: string;
261
+ source?: string;
262
+ /** Conversation topic tag — e.g. "Buy a Home", "Refinance", "Investment". */
263
+ topic?: string;
264
+ aiFields?: AiConvDataField[];
265
+ appointment?: AiConvAppointmentData;
266
+ internalNotes?: string;
267
+ notesSaveStatus?: "idle" | "saving" | "saved";
268
+ isCollapsed?: boolean;
269
+ /** True when this lead's contact info already exists in the system. Disables Add to Contacts. */
270
+ isKnownContact?: boolean;
271
+ onAddToContacts?: () => void;
272
+ onCreateOpportunity?: () => void;
273
+ onBookAppointment?: () => void;
274
+ onApproveAppointment?: () => void;
275
+ onDeclineAppointment?: () => void;
276
+ onRescheduleAppointment?: (contactId: string) => void;
277
+ onNotesChange?: (v: string) => void;
278
+ onToggleCollapse?: () => void;
279
+ /** Mobile only — back to chat thread. */
280
+ onBack?: () => void;
281
+ className?: string;
282
+ }
283
+
284
+ export function LeadInfoPanel({
285
+ contact,
286
+ firstSeen,
287
+ source = "Website Chatbot",
288
+ topic,
289
+ aiFields = [],
290
+ appointment,
291
+ internalNotes = "",
292
+ notesSaveStatus = "idle",
293
+ isCollapsed = false,
294
+ isKnownContact = false,
295
+ onAddToContacts,
296
+ onCreateOpportunity,
297
+ onBookAppointment,
298
+ onApproveAppointment,
299
+ onDeclineAppointment,
300
+ onRescheduleAppointment,
301
+ onNotesChange,
302
+ onToggleCollapse,
303
+ onBack,
304
+ className,
305
+ }: LeadInfoPanelProps) {
306
+ const isAnonymous = !contact.name.trim();
307
+ const addToContactsDisabled = isAnonymous || isKnownContact;
308
+
309
+ return (
310
+ <div className={cn("flex flex-col bg-background", className)}>
311
+ <div
312
+ className={cn(
313
+ PANEL_HEADER_HEIGHT,
314
+ "flex items-center justify-between border-b border-border px-4",
315
+ )}
316
+ >
317
+ {onBack && (
318
+ <Button
319
+ variant="ghost"
320
+ size="icon"
321
+ className="size-8 shrink-0 md:hidden"
322
+ onClick={onBack}
323
+ aria-label="Back to conversation"
324
+ >
325
+ <ArrowLeft className="size-4" />
326
+ </Button>
327
+ )}
328
+ <span className="text-sm font-semibold text-foreground">Lead Info</span>
329
+ {onToggleCollapse && (
330
+ <Tooltip>
331
+ <TooltipTrigger
332
+ render={
333
+ <Button
334
+ variant="ghost"
335
+ size="icon"
336
+ className="size-7"
337
+ onClick={onToggleCollapse}
338
+ aria-label={isCollapsed ? "Expand panel" : "Collapse panel"}
339
+ >
340
+ <ChevronRight
341
+ className={cn(
342
+ "size-4 transition-transform duration-150",
343
+ isCollapsed && "-rotate-180",
344
+ )}
345
+ />
346
+ </Button>
347
+ }
348
+ />
349
+ <TooltipContent>
350
+ {isCollapsed ? "Expand panel" : "Collapse panel"}
351
+ </TooltipContent>
352
+ </Tooltip>
353
+ )}
354
+ </div>
355
+
356
+ {!isCollapsed && (
357
+ <div className="flex-1 overflow-y-auto" tabIndex={0}>
358
+ <PanelSection>
359
+ <PanelSectionHeader>Contact</PanelSectionHeader>
360
+ <div className="flex items-center gap-3">
361
+ <ContactAvatar name={contact.name} size="lg" />
362
+ <div className="min-w-0 flex-1">
363
+ <p className="truncate text-sm font-semibold text-foreground">
364
+ {displayContactName(contact.name)}
365
+ </p>
366
+ <p className="text-sm text-muted-foreground">{source}</p>
367
+ {topic && (
368
+ <Badge variant="secondary" className="mt-1 text-xs">
369
+ {topic}
370
+ </Badge>
371
+ )}
372
+ </div>
373
+ </div>
374
+ {(contact.email || contact.phone || firstSeen) && (
375
+ <div className="mt-3 flex flex-col gap-1.5">
376
+ {contact.email && (
377
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
378
+ <Mail className="size-4 shrink-0" />
379
+ <a
380
+ href={`mailto:${contact.email}`}
381
+ className="truncate hover:underline"
382
+ >
383
+ {contact.email}
384
+ </a>
385
+ </div>
386
+ )}
387
+ {contact.phone && (
388
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
389
+ <PhoneCall className="size-4 shrink-0" />
390
+ <a
391
+ href={`tel:${contact.phone}`}
392
+ className="hover:underline"
393
+ >
394
+ {contact.phone}
395
+ </a>
396
+ </div>
397
+ )}
398
+ {firstSeen && (
399
+ <p className="text-sm text-muted-foreground">
400
+ First seen: {firstSeen}
401
+ </p>
402
+ )}
403
+ </div>
404
+ )}
405
+ </PanelSection>
406
+
407
+ <PanelSection>
408
+ <PanelSectionHeader>AI-Collected Data</PanelSectionHeader>
409
+ {aiFields.length > 0 ? (
410
+ <>
411
+ <AICollectedDataSection fields={aiFields} />
412
+ <div className="mt-2.5 flex items-center gap-3 text-xs text-muted-foreground">
413
+ <span className="flex items-center gap-1">
414
+ <CheckCircle2 className="size-3 text-success-text" />
415
+ confirmed
416
+ </span>
417
+ <span className="flex items-center gap-1">
418
+ <HelpCircle className="size-3 text-warning-text" />
419
+ estimated
420
+ </span>
421
+ </div>
422
+ </>
423
+ ) : (
424
+ <p className="text-sm text-muted-foreground">
425
+ AI is still gathering information...
426
+ </p>
427
+ )}
428
+ </PanelSection>
429
+
430
+ <PanelSection>
431
+ <PanelSectionHeader>Appointment</PanelSectionHeader>
432
+ {appointment ? (
433
+ <AppointmentSection
434
+ appointment={appointment}
435
+ contactId={contact.id}
436
+ isAnonymous={isAnonymous}
437
+ onApproveAppointment={onApproveAppointment}
438
+ onDeclineAppointment={onDeclineAppointment}
439
+ onRescheduleAppointment={onRescheduleAppointment}
440
+ />
441
+ ) : (
442
+ <Button
443
+ variant="outline"
444
+ size="sm"
445
+ className="w-full justify-start"
446
+ disabled={isAnonymous}
447
+ onClick={onBookAppointment}
448
+ >
449
+ <Plus className="mr-1.5 size-3.5" />
450
+ Book Appointment
451
+ </Button>
452
+ )}
453
+ </PanelSection>
454
+
455
+ <PanelSection>
456
+ <PanelSectionHeader>CRM Actions</PanelSectionHeader>
457
+ <div className="flex flex-col gap-2">
458
+ <Tooltip>
459
+ <TooltipTrigger
460
+ render={
461
+ <Button
462
+ variant="outline"
463
+ size="sm"
464
+ className="w-full justify-start"
465
+ disabled={addToContactsDisabled}
466
+ onClick={onAddToContacts}
467
+ >
468
+ <UserPlus className="mr-1.5 size-3.5" />
469
+ Add to Contacts
470
+ </Button>
471
+ }
472
+ />
473
+ {isKnownContact && (
474
+ <TooltipContent>Already in contacts</TooltipContent>
475
+ )}
476
+ </Tooltip>
477
+ <Button
478
+ variant="outline"
479
+ size="sm"
480
+ className="w-full justify-start"
481
+ disabled={isAnonymous}
482
+ onClick={onCreateOpportunity}
483
+ >
484
+ <Briefcase className="mr-1.5 size-3.5" />
485
+ Add to CRM
486
+ </Button>
487
+ </div>
488
+ </PanelSection>
489
+
490
+ <PanelSection last>
491
+ <div className="mb-2.5 flex items-center justify-between">
492
+ <p className="text-overline text-muted-foreground">
493
+ Internal Notes
494
+ </p>
495
+ {notesSaveStatus === "saving" && (
496
+ <span className="text-xs text-muted-foreground">Saving...</span>
497
+ )}
498
+ {notesSaveStatus === "saved" && (
499
+ <span className="flex items-center gap-1 text-xs text-success-text">
500
+ <CheckCircle2 className="size-3" />
501
+ Saved
502
+ </span>
503
+ )}
504
+ </div>
505
+ <Textarea
506
+ value={internalNotes}
507
+ onChange={(e) => onNotesChange?.(e.target.value)}
508
+ placeholder="Private notes — not visible to lead..."
509
+ rows={4}
510
+ className="resize-none text-sm"
511
+ />
512
+ </PanelSection>
513
+ </div>
514
+ )}
515
+ </div>
516
+ );
517
+ }