@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wealthx/shadcn",
3
- "version": "1.5.24",
3
+ "version": "1.5.25",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -557,9 +557,9 @@
557
557
  "require": "./dist/components/ui/ai-assistant-drawer.js"
558
558
  },
559
559
  "./ai-conversations": {
560
- "types": "./src/components/ui/ai-conversations.tsx",
561
- "import": "./dist/components/ui/ai-conversations.mjs",
562
- "require": "./dist/components/ui/ai-conversations.js"
560
+ "types": "./src/components/ui/ai-conversations/index.tsx",
561
+ "import": "./dist/components/ui/ai-conversations/index.mjs",
562
+ "require": "./dist/components/ui/ai-conversations/index.js"
563
563
  },
564
564
  "./chat-widget-primitives": {
565
565
  "types": "./src/components/ui/chat-widget-primitives.tsx",
@@ -56,6 +56,7 @@ export type {
56
56
  AiConvMessage,
57
57
  AiConvDataField,
58
58
  AiConvAppointmentData,
59
+ AiConvEmailPayload,
59
60
  ConversationStatusChipProps,
60
61
  ConversationListItemProps,
61
62
  ConversationListProps,
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ import { cn, getInitials } from "@/lib/utils";
3
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
4
+
5
+ export function displayContactName(name: string): string {
6
+ return name.trim() || "Website User";
7
+ }
8
+
9
+ /** Shared fixed height for all three panel top-bar sections so bottom dividers align. */
10
+ export const PANEL_HEADER_HEIGHT = "h-[94px]";
11
+
12
+ interface ContactAvatarProps {
13
+ name: string;
14
+ size?: "sm" | "md" | "lg";
15
+ className?: string;
16
+ }
17
+
18
+ export function ContactAvatar({
19
+ name,
20
+ size = "md",
21
+ className,
22
+ }: ContactAvatarProps) {
23
+ const avatarSize = size === "sm" ? "sm" : size === "lg" ? "lg" : "default";
24
+ return (
25
+ <Avatar size={avatarSize} className={className}>
26
+ <AvatarFallback className="font-semibold">
27
+ {getInitials(name)}
28
+ </AvatarFallback>
29
+ </Avatar>
30
+ );
31
+ }
@@ -0,0 +1,468 @@
1
+ /**
2
+ * AI Conversations — WealthX Backoffice
3
+ *
4
+ * 3-panel inbox for managing AI + advisor conversations from website chatbot leads.
5
+ *
6
+ * Component hierarchy:
7
+ * Atom → ConversationStatusChip
8
+ * Molecule → ConversationListItem, ChatBubble
9
+ * Organism → ConversationList, ChatThread, ChatComposer, AICollectedDataSection, LeadInfoPanel
10
+ * Template → ConversationsPage
11
+ */
12
+
13
+ import React, { useState } from "react";
14
+ import { ChevronLeft, MessageSquare, Search } from "lucide-react";
15
+ import { cn } from "@/lib/utils";
16
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
17
+ import { Button } from "@/components/ui/button";
18
+ import {
19
+ Dialog,
20
+ DialogContent,
21
+ DialogDescription,
22
+ DialogFooter,
23
+ DialogHeader,
24
+ DialogTitle,
25
+ } from "@/components/ui/dialog";
26
+ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
27
+ import {
28
+ Tooltip,
29
+ TooltipContent,
30
+ TooltipProvider,
31
+ TooltipTrigger,
32
+ } from "@/components/ui/tooltip";
33
+ import type {
34
+ AiConvAdvisor,
35
+ AiConvAppointmentData,
36
+ AiConvChannel,
37
+ AiConvChannelFilter,
38
+ AiConvContact,
39
+ AiConvDataField,
40
+ AiConvFilterTab,
41
+ AiConvListItemData,
42
+ AiConvMessage,
43
+ AiConvMode,
44
+ AiConvStatus,
45
+ } from "./types";
46
+ import type { AiConvEmailPayload } from "./thread";
47
+ import { ConversationList } from "./list";
48
+ import { ChatThread } from "./thread";
49
+ import { LeadInfoPanel } from "./lead-panel";
50
+
51
+ // Re-export everything so consumers import from the one public path.
52
+ export * from "./types";
53
+ export * from "./list";
54
+ export * from "./thread";
55
+ export * from "./lead-panel";
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // ConversationsPage
59
+ // ---------------------------------------------------------------------------
60
+
61
+ export interface ConversationsPageProps {
62
+ conversations: AiConvListItemData[];
63
+ activeConversationId?: string;
64
+ /** Contact for the currently-selected conversation. */
65
+ contact?: AiConvContact;
66
+ status?: AiConvStatus;
67
+ mode?: AiConvMode;
68
+ messages?: AiConvMessage[];
69
+ aiFields?: AiConvDataField[];
70
+ appointment?: AiConvAppointmentData;
71
+ /** When the lead first contacted via the chatbot. */
72
+ leadFirstSeen?: string;
73
+ /** Traffic source label (e.g. "Website Chatbot"). */
74
+ leadSource?: string;
75
+ /** Conversation topic tag — e.g. "Buy a Home", "Refinance", "Investment". */
76
+ leadTopic?: string;
77
+ searchQuery?: string;
78
+ activeFilter?: AiConvFilterTab;
79
+ /** Filter conversation list by channel type. */
80
+ channelFilter?: AiConvChannelFilter;
81
+ /** Active reply channel — "chat" (default) or "email". */
82
+ channel?: AiConvChannel;
83
+ onChannelChange?: (channel: AiConvChannel) => void;
84
+ /** When true, the Email tab is shown in the composer. Defaults to false. */
85
+ isEmailIntegrated?: boolean;
86
+ inputValue?: string;
87
+ internalNotes?: string;
88
+ showLeadPanel?: boolean;
89
+ hasMore?: boolean;
90
+ isLoadingMore?: boolean;
91
+ /** True when older messages can be loaded for the active conversation. */
92
+ hasMoreMessages?: boolean;
93
+ /** True while a `onLoadMoreMessages` request is in-flight. */
94
+ isLoadingMoreMessages?: boolean;
95
+ /** Fired when the consumer should fetch older messages for the active conversation. */
96
+ onLoadMoreMessages?: () => void;
97
+ onSelectConversation?: (id: string) => void;
98
+ onRead?: (id: string) => void;
99
+ onSearchChange?: (v: string) => void;
100
+ onFilterChange?: (f: AiConvFilterTab) => void;
101
+ onChannelFilterChange?: (f: AiConvChannelFilter) => void;
102
+ onInputChange?: (v: string) => void;
103
+ /** Fired when the user sends a chat message. */
104
+ onSend?: (content: string) => void;
105
+ /** Fired when the user sends an email. */
106
+ onSendEmail?: (payload: AiConvEmailPayload) => void;
107
+ onTakeOver?: () => void;
108
+ onLetAiHandle?: () => void;
109
+ /** Pre-fills the email Subject field with "Re: [emailReplySubject]" for reply threads. */
110
+ emailReplySubject?: string;
111
+ onReopen?: () => void;
112
+ onMarkUrgent?: () => void;
113
+ onUnmarkUrgent?: () => void;
114
+ onArchive?: () => void;
115
+ onAssignToAdvisor?: () => void;
116
+ /** True when this lead is already a contact in the system. Disables Add to Contacts. */
117
+ isKnownContact?: boolean;
118
+ onAddToContacts?: () => void;
119
+ onCreateOpportunity?: () => void;
120
+ onBookAppointment?: () => void;
121
+ onApproveAppointment?: () => void;
122
+ onDeclineAppointment?: () => void;
123
+ onRescheduleAppointment?: (contactId: string) => void;
124
+ onNotesChange?: (v: string) => void;
125
+ onToggleLeadPanel?: () => void;
126
+ onLoadMore?: () => void;
127
+ isAiTyping?: boolean;
128
+ notesSaveStatus?: "idle" | "saving" | "saved";
129
+ className?: string;
130
+ }
131
+
132
+ export function ConversationsPage({
133
+ conversations,
134
+ activeConversationId,
135
+ contact,
136
+ status = "ai-active",
137
+ mode = "ai",
138
+ messages = [],
139
+ aiFields = [],
140
+ appointment,
141
+ leadFirstSeen,
142
+ leadSource,
143
+ leadTopic,
144
+ searchQuery,
145
+ activeFilter,
146
+ channelFilter,
147
+ onChannelFilterChange,
148
+ channel,
149
+ onChannelChange,
150
+ isEmailIntegrated,
151
+ inputValue,
152
+ internalNotes,
153
+ showLeadPanel = true,
154
+ hasMore,
155
+ isLoadingMore,
156
+ hasMoreMessages,
157
+ isLoadingMoreMessages,
158
+ onLoadMoreMessages,
159
+ isAiTyping,
160
+ notesSaveStatus,
161
+ onSelectConversation,
162
+ onRead,
163
+ onSearchChange,
164
+ onFilterChange,
165
+ onInputChange,
166
+ onSend,
167
+ onSendEmail,
168
+ onTakeOver,
169
+ onLetAiHandle,
170
+ emailReplySubject,
171
+ onReopen,
172
+ onMarkUrgent,
173
+ onUnmarkUrgent,
174
+ onArchive,
175
+ onAssignToAdvisor,
176
+ isKnownContact,
177
+ onAddToContacts,
178
+ onCreateOpportunity,
179
+ onBookAppointment,
180
+ onApproveAppointment,
181
+ onDeclineAppointment,
182
+ onRescheduleAppointment,
183
+ onNotesChange,
184
+ onToggleLeadPanel,
185
+ onLoadMore,
186
+ className,
187
+ }: ConversationsPageProps) {
188
+ const [mobilePanel, setMobilePanel] = useState<"list" | "chat" | "lead">(
189
+ "list",
190
+ );
191
+
192
+ const handleSelectConversation = (id: string) => {
193
+ onSelectConversation?.(id);
194
+ setMobilePanel("chat");
195
+ };
196
+
197
+ const handleToggleLeadPanel = () => {
198
+ onToggleLeadPanel?.();
199
+ setMobilePanel(showLeadPanel ? "chat" : "lead");
200
+ };
201
+
202
+ return (
203
+ <TooltipProvider>
204
+ <div
205
+ className={cn("flex h-full overflow-hidden bg-background", className)}
206
+ >
207
+ {/* Left — Conversation List */}
208
+ <ConversationList
209
+ conversations={conversations}
210
+ activeId={activeConversationId}
211
+ searchQuery={searchQuery}
212
+ activeFilter={activeFilter}
213
+ channelFilter={channelFilter}
214
+ hasMore={hasMore}
215
+ isLoadingMore={isLoadingMore}
216
+ onSearchChange={onSearchChange}
217
+ onFilterChange={onFilterChange}
218
+ onChannelFilterChange={onChannelFilterChange}
219
+ onSelect={handleSelectConversation}
220
+ onRead={onRead}
221
+ onLoadMore={onLoadMore}
222
+ className={cn(
223
+ "shrink-0 md:w-[320px]",
224
+ mobilePanel === "list" ? "flex w-full md:flex" : "hidden md:flex",
225
+ )}
226
+ />
227
+
228
+ {/* Center — Chat Thread */}
229
+ {contact ? (
230
+ <ChatThread
231
+ key={contact.id}
232
+ contact={contact}
233
+ status={status}
234
+ mode={mode}
235
+ messages={messages}
236
+ isAiTyping={isAiTyping}
237
+ channel={channel}
238
+ onChannelChange={onChannelChange}
239
+ isEmailIntegrated={isEmailIntegrated}
240
+ inputValue={inputValue}
241
+ onInputChange={onInputChange}
242
+ onSend={onSend}
243
+ onSendEmail={onSendEmail}
244
+ onTakeOver={onTakeOver}
245
+ onLetAiHandle={onLetAiHandle}
246
+ emailReplySubject={emailReplySubject}
247
+ onReopen={onReopen}
248
+ onMarkUrgent={onMarkUrgent}
249
+ onUnmarkUrgent={onUnmarkUrgent}
250
+ onArchive={onArchive}
251
+ onAssignToAdvisor={onAssignToAdvisor}
252
+ hasMoreMessages={hasMoreMessages}
253
+ isLoadingMoreMessages={isLoadingMoreMessages}
254
+ onLoadMoreMessages={onLoadMoreMessages}
255
+ onBack={() => setMobilePanel("list")}
256
+ onShowLeadInfo={() => setMobilePanel("lead")}
257
+ className={cn(
258
+ "min-w-0 flex-1 border-r border-border",
259
+ mobilePanel === "chat"
260
+ ? "flex flex-col md:flex"
261
+ : "hidden md:flex",
262
+ )}
263
+ />
264
+ ) : (
265
+ <div
266
+ className={cn(
267
+ "min-w-0 flex-1 items-center justify-center border-r border-border bg-muted/10",
268
+ mobilePanel === "chat" ? "flex md:flex" : "hidden md:flex",
269
+ )}
270
+ >
271
+ <div className="flex flex-col items-center gap-2 text-muted-foreground">
272
+ <MessageSquare className="size-10 opacity-30" />
273
+ <p className="text-sm">Select a conversation</p>
274
+ </div>
275
+ </div>
276
+ )}
277
+
278
+ {/* Right — Lead Info Panel */}
279
+ {contact && (
280
+ <>
281
+ <div
282
+ className={cn(
283
+ mobilePanel === "lead"
284
+ ? "flex w-full shrink-0 flex-col"
285
+ : "hidden",
286
+ "md:block md:shrink-0 md:overflow-hidden md:transition-[width] md:duration-200 md:ease-in-out",
287
+ showLeadPanel ? "md:w-[320px]" : "md:w-0",
288
+ )}
289
+ >
290
+ <LeadInfoPanel
291
+ contact={contact}
292
+ firstSeen={leadFirstSeen}
293
+ source={leadSource}
294
+ topic={leadTopic}
295
+ aiFields={aiFields}
296
+ appointment={appointment}
297
+ internalNotes={internalNotes}
298
+ notesSaveStatus={notesSaveStatus}
299
+ isKnownContact={isKnownContact}
300
+ onAddToContacts={onAddToContacts}
301
+ onCreateOpportunity={onCreateOpportunity}
302
+ onBookAppointment={onBookAppointment}
303
+ onApproveAppointment={onApproveAppointment}
304
+ onDeclineAppointment={onDeclineAppointment}
305
+ onRescheduleAppointment={onRescheduleAppointment}
306
+ onNotesChange={onNotesChange}
307
+ onToggleCollapse={handleToggleLeadPanel}
308
+ onBack={() => setMobilePanel("chat")}
309
+ className="flex h-full w-full flex-col border-l border-border md:w-[320px]"
310
+ />
311
+ </div>
312
+
313
+ {!showLeadPanel && onToggleLeadPanel && (
314
+ <div className="hidden shrink-0 items-start border-l border-border pt-[29px] md:flex">
315
+ <Tooltip>
316
+ <TooltipTrigger
317
+ render={
318
+ <Button
319
+ variant="ghost"
320
+ size="icon"
321
+ className="size-8"
322
+ aria-label="Show lead info"
323
+ onClick={handleToggleLeadPanel}
324
+ >
325
+ <ChevronLeft className="size-4" />
326
+ </Button>
327
+ }
328
+ />
329
+ <TooltipContent>Show lead info</TooltipContent>
330
+ </Tooltip>
331
+ </div>
332
+ )}
333
+ </>
334
+ )}
335
+ </div>
336
+ </TooltipProvider>
337
+ );
338
+ }
339
+
340
+ // ---------------------------------------------------------------------------
341
+ // AiConvAssignAdvisorDialog
342
+ // ---------------------------------------------------------------------------
343
+
344
+ export interface AiConvAssignAdvisorDialogProps {
345
+ open: boolean;
346
+ onOpenChange: (open: boolean) => void;
347
+ advisors: AiConvAdvisor[];
348
+ /** Currently selected advisor id */
349
+ value: string;
350
+ onValueChange: (id: string) => void;
351
+ onConfirm: () => void;
352
+ }
353
+
354
+ export function AiConvAssignAdvisorDialog({
355
+ open,
356
+ onOpenChange,
357
+ advisors,
358
+ value,
359
+ onValueChange,
360
+ onConfirm,
361
+ }: AiConvAssignAdvisorDialogProps) {
362
+ const [search, setSearch] = useState("");
363
+ const [roleFilter, setRoleFilter] = useState("");
364
+
365
+ const roles = Array.from(
366
+ new Set(advisors.map((a) => a.role).filter(Boolean)),
367
+ ) as string[];
368
+
369
+ const filtered = advisors.filter(
370
+ (a) =>
371
+ a.name.toLowerCase().includes(search.toLowerCase()) &&
372
+ (!roleFilter || a.role === roleFilter),
373
+ );
374
+
375
+ const handleOpenChange = (v: boolean) => {
376
+ onOpenChange(v);
377
+ if (!v) {
378
+ setSearch("");
379
+ setRoleFilter("");
380
+ }
381
+ };
382
+
383
+ return (
384
+ <Dialog open={open} onOpenChange={handleOpenChange}>
385
+ <DialogContent>
386
+ <DialogHeader>
387
+ <DialogTitle>Assign to advisor</DialogTitle>
388
+ <DialogDescription>
389
+ Choose an advisor to handle this conversation.
390
+ </DialogDescription>
391
+ </DialogHeader>
392
+ <div className="flex flex-col gap-0">
393
+ {roles.length > 0 && (
394
+ <div className="pb-3">
395
+ <ToggleGroup
396
+ type="single"
397
+ variant="outline"
398
+ spacing={1.5}
399
+ size="sm"
400
+ value={roleFilter ? [roleFilter] : ["__all__"]}
401
+ onValueChange={(values) => {
402
+ const v = values[0];
403
+ setRoleFilter(!v || v === "__all__" ? "" : v);
404
+ }}
405
+ >
406
+ <ToggleGroupItem value="__all__">All</ToggleGroupItem>
407
+ {roles.map((role) => (
408
+ <ToggleGroupItem key={role} value={role}>
409
+ {role}
410
+ </ToggleGroupItem>
411
+ ))}
412
+ </ToggleGroup>
413
+ </div>
414
+ )}
415
+ <div className="flex items-center gap-2 border border-input px-3">
416
+ <Search className="size-4 shrink-0 text-muted-foreground" />
417
+ <input
418
+ type="text"
419
+ placeholder="Search advisors..."
420
+ value={search}
421
+ onChange={(e) => setSearch(e.target.value)}
422
+ className="h-9 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
423
+ />
424
+ </div>
425
+ <div className="max-h-52 overflow-y-auto border border-t-0 border-input">
426
+ {filtered.length === 0 ? (
427
+ <p className="py-6 text-center text-sm text-muted-foreground">
428
+ No advisors found.
429
+ </p>
430
+ ) : (
431
+ filtered.map((advisor) => (
432
+ <button
433
+ key={advisor.id}
434
+ type="button"
435
+ onClick={() => onValueChange(advisor.id)}
436
+ className={cn(
437
+ "flex w-full items-center gap-3 px-3 py-2.5 text-left text-sm transition-colors hover:bg-muted",
438
+ value === advisor.id && "bg-muted font-medium",
439
+ )}
440
+ >
441
+ <Avatar size="sm">
442
+ <AvatarFallback className="font-semibold">
443
+ {advisor.initials}
444
+ </AvatarFallback>
445
+ </Avatar>
446
+ <div className="min-w-0 flex-1">
447
+ <p className="truncate text-sm">{advisor.name}</p>
448
+ {advisor.role && (
449
+ <p className="truncate text-xs text-muted-foreground">
450
+ {advisor.role}
451
+ </p>
452
+ )}
453
+ </div>
454
+ </button>
455
+ ))
456
+ )}
457
+ </div>
458
+ </div>
459
+ <DialogFooter>
460
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
461
+ Cancel
462
+ </Button>
463
+ <Button onClick={onConfirm}>Assign</Button>
464
+ </DialogFooter>
465
+ </DialogContent>
466
+ </Dialog>
467
+ );
468
+ }