@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.
- package/.turbo/turbo-build.log +144 -144
- package/CHANGELOG.md +6 -0
- package/dist/{chunk-RYGZRDP6.mjs → chunk-4FJC64FV.mjs} +468 -410
- package/dist/{chunk-FGHM34AV.mjs → chunk-7JVKSZ4O.mjs} +1 -1
- package/dist/{chunk-6HTE24TP.mjs → chunk-EY5YPFKX.mjs} +1 -1
- package/dist/{chunk-3HFOSFOM.mjs → chunk-IG7DEIWU.mjs} +4 -4
- package/dist/{chunk-2VTOF7PW.mjs → chunk-K2KX3NX7.mjs} +1 -1
- package/dist/{chunk-NXZ2F4JA.mjs → chunk-KGBLORCQ.mjs} +1 -1
- package/dist/{chunk-2SDEURIQ.mjs → chunk-PX2B3Q3A.mjs} +7 -7
- package/dist/{chunk-66NM4AX2.mjs → chunk-QM7LU2BR.mjs} +10 -10
- package/dist/{chunk-4UCRTTVL.mjs → chunk-RNLIZRAK.mjs} +1 -1
- package/dist/{chunk-BWG7AX6X.mjs → chunk-SQ54W3JH.mjs} +1 -1
- package/dist/{chunk-RJHE3V4M.mjs → chunk-UMR3HVZF.mjs} +4 -4
- package/dist/{chunk-JGUC3KCA.mjs → chunk-VNB5E7SI.mjs} +5 -5
- package/dist/{chunk-ETT5JAXF.mjs → chunk-Z5QI7CW2.mjs} +1 -1
- package/dist/components/ui/about-you-form.mjs +4 -4
- package/dist/components/ui/ai-assistant-drawer.mjs +2 -2
- package/dist/components/ui/{ai-conversations.js → ai-conversations/index.js} +818 -763
- package/dist/components/ui/ai-conversations/index.mjs +42 -0
- package/dist/components/ui/appointment-action-dialogs.mjs +1 -1
- package/dist/components/ui/appointment-availability-settings.mjs +5 -5
- package/dist/components/ui/appointment-book-dialog.mjs +4 -4
- package/dist/components/ui/appointment-detail-sheet.mjs +3 -3
- package/dist/components/ui/appointment-upcoming-card.mjs +1 -1
- package/dist/components/ui/assets-liabilities-side-card.mjs +6 -6
- package/dist/components/ui/backoffice-signup-steps.mjs +4 -4
- package/dist/components/ui/bank-statement-generate-dialog.mjs +6 -6
- package/dist/components/ui/contact-alert-dialog/index.mjs +2 -2
- package/dist/components/ui/create-contact-modal.mjs +3 -3
- package/dist/components/ui/date-picker.mjs +2 -2
- package/dist/components/ui/expense-detail-item.mjs +3 -3
- package/dist/components/ui/expense-work-details.mjs +9 -9
- package/dist/components/ui/file-preview-dialog.mjs +2 -2
- package/dist/components/ui/financial-drawers.mjs +2 -2
- package/dist/components/ui/form-primitives.mjs +2 -2
- package/dist/components/ui/frontend-signup-steps.mjs +5 -5
- package/dist/components/ui/income-work-details.mjs +3 -3
- package/dist/components/ui/kanban-column.mjs +4 -4
- package/dist/components/ui/opportunity-card.mjs +1 -1
- package/dist/components/ui/opportunity-edit-modals.mjs +6 -6
- package/dist/components/ui/opportunity-summary-tab.mjs +8 -8
- package/dist/components/ui/pipeline-board.mjs +6 -6
- package/dist/components/ui/pipeline-dialogs.mjs +5 -5
- package/dist/components/ui/property-report-dialog.mjs +4 -4
- package/dist/components/ui/savings-goal-modal.mjs +4 -4
- package/dist/components/ui/share-details-dialog.mjs +2 -2
- package/dist/components/ui/signup-form-primitives.mjs +1 -1
- package/dist/index.js +4913 -4858
- package/dist/index.mjs +160 -160
- package/dist/styles.css +1 -1
- package/package.json +4 -4
- package/src/components/index.tsx +1 -0
- package/src/components/ui/ai-conversations/helpers.tsx +31 -0
- package/src/components/ui/ai-conversations/index.tsx +468 -0
- package/src/components/ui/ai-conversations/lead-panel.tsx +517 -0
- package/src/components/ui/ai-conversations/list.tsx +335 -0
- package/src/components/ui/ai-conversations/thread.tsx +919 -0
- package/src/components/ui/ai-conversations/types.ts +83 -0
- package/src/styles/styles-css.ts +1 -1
- package/tsup.config.ts +1 -1
- package/dist/components/ui/ai-conversations.mjs +0 -42
- package/src/components/ui/ai-conversations.tsx +0 -2270
- package/dist/{chunk-A43XIVO6.mjs → chunk-2PWTXE66.mjs} +3 -3
- package/dist/{chunk-PSBQ4I3M.mjs → chunk-53ZB2JWT.mjs} +6 -6
- package/dist/{chunk-G6RCC2SF.mjs → chunk-5ST6BK7R.mjs} +3 -3
- package/dist/{chunk-H5NI6ZIU.mjs → chunk-734FOOJC.mjs} +3 -3
- package/dist/{chunk-ONYADWSO.mjs → chunk-ABVCQWDY.mjs} +3 -3
- package/dist/{chunk-AADJ5IT6.mjs → chunk-ECC2LLZM.mjs} +3 -3
- package/dist/{chunk-DK55HZPN.mjs → chunk-KENLXFJ7.mjs} +6 -6
- package/dist/{chunk-ET4MTPIY.mjs → chunk-LDC6V6DJ.mjs} +3 -3
- package/dist/{chunk-3S4XQTAL.mjs → chunk-SO4RB3XB.mjs} +9 -9
- 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.
|
|
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",
|
package/src/components/index.tsx
CHANGED
|
@@ -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
|
+
}
|