@wealthx/shadcn 1.5.37 → 1.5.39
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 +142 -133
- package/CHANGELOG.md +12 -0
- package/dist/{chunk-LSSIWLYU.mjs → chunk-6XNEHTII.mjs} +1 -1
- package/dist/{chunk-ULQ53FRJ.mjs → chunk-7NQKFPXE.mjs} +1 -1
- package/dist/{chunk-734FOOJC.mjs → chunk-B5PSUONN.mjs} +25 -58
- package/dist/{chunk-DSVKEVX6.mjs → chunk-CZOGJC76.mjs} +1 -1
- package/dist/chunk-EFHPSKVF.mjs +192 -0
- package/dist/{chunk-JPGL36WQ.mjs → chunk-FL7DEYUA.mjs} +6 -7
- package/dist/{chunk-2CHH5QOA.mjs → chunk-FQUT5XD6.mjs} +1 -1
- package/dist/chunk-MGIDYXOP.mjs +814 -0
- package/dist/{chunk-OG2VM34K.mjs → chunk-MHBQJVHE.mjs} +1 -1
- package/dist/{chunk-NB3ZL36B.mjs → chunk-MZI77ZMX.mjs} +17 -2
- package/dist/chunk-R7M657QL.mjs +587 -0
- package/dist/{chunk-DIH2NZZ3.mjs → chunk-RRROLESJ.mjs} +33 -23
- package/dist/components/ui/ai-assistant-drawer.js +269 -121
- package/dist/components/ui/ai-assistant-drawer.mjs +2 -1
- package/dist/components/ui/ai-conversations/index.js +474 -286
- package/dist/components/ui/ai-conversations/index.mjs +2 -1
- package/dist/components/ui/chat-input-area.js +429 -0
- package/dist/components/ui/chat-input-area.mjs +11 -0
- package/dist/components/ui/file-preview-dialog.js +6 -7
- package/dist/components/ui/file-preview-dialog.mjs +2 -2
- package/dist/components/ui/kanban-column.js +6 -7
- package/dist/components/ui/kanban-column.mjs +3 -3
- package/dist/components/ui/opportunity-card.js +6 -7
- package/dist/components/ui/opportunity-card.mjs +2 -2
- package/dist/components/ui/page-top-bar.js +182 -5
- package/dist/components/ui/page-top-bar.mjs +3 -1
- package/dist/components/ui/pipeline-board.js +6 -7
- package/dist/components/ui/pipeline-board.mjs +4 -4
- package/dist/components/ui/policy-ai/index.js +1636 -0
- package/dist/components/ui/policy-ai/index.mjs +36 -0
- package/dist/components/ui/progress.js +6 -7
- package/dist/components/ui/progress.mjs +1 -1
- package/dist/components/ui/stage-timeline.js +6 -7
- package/dist/components/ui/stage-timeline.mjs +2 -2
- package/dist/components/ui/support-agent/index.js +1131 -0
- package/dist/components/ui/support-agent/index.mjs +27 -0
- package/dist/index.js +5609 -4100
- package/dist/index.mjs +77 -41
- package/dist/styles.css +1 -1
- package/package.json +16 -1
- package/src/components/index.tsx +54 -0
- package/src/components/ui/ai-assistant-drawer.tsx +24 -51
- package/src/components/ui/ai-conversations/index.tsx +16 -8
- package/src/components/ui/ai-conversations/thread.tsx +38 -27
- package/src/components/ui/chat-input-area.tsx +244 -0
- package/src/components/ui/page-top-bar.tsx +31 -5
- package/src/components/ui/policy-ai/index.tsx +41 -0
- package/src/components/ui/policy-ai/policy-ai-panel.tsx +526 -0
- package/src/components/ui/policy-ai/policy-ai-primitives.tsx +332 -0
- package/src/components/ui/policy-ai/policy-ai-responses.tsx +543 -0
- package/src/components/ui/progress.tsx +15 -12
- package/src/components/ui/support-agent/index.tsx +25 -0
- package/src/components/ui/support-agent/support-agent-fab.tsx +116 -0
- package/src/components/ui/support-agent/support-agent-panel.tsx +498 -0
- package/src/components/ui/support-agent/support-agent-primitives.tsx +354 -0
- package/src/styles/globals.css +1 -0
- package/src/styles/styles-css.ts +1 -1
- package/tsup.config.ts +3 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { HelpCircle, X } from "lucide-react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SupportAgentFAB — WealthX DS
|
|
8
|
+
*
|
|
9
|
+
* Floating action button that opens/closes the Support Agent panel.
|
|
10
|
+
* Positioned fixed at bottom-right (or bottom-left) of the viewport.
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Nudge badge: red dot shown when the agent has a proactive suggestion
|
|
14
|
+
* - Nudge message: small dismissible tooltip bubble above the button
|
|
15
|
+
* - Toggles between HelpCircle (closed) and X (open) icon
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Types
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
export interface SupportAgentFABProps {
|
|
23
|
+
/** Whether the support panel is currently open. */
|
|
24
|
+
isOpen?: boolean;
|
|
25
|
+
/** Called when the FAB is clicked. */
|
|
26
|
+
onClick: () => void;
|
|
27
|
+
/** Show a red nudge dot on the button. */
|
|
28
|
+
hasNudge?: boolean;
|
|
29
|
+
/** Short message shown in a bubble above the FAB when hasNudge is true. */
|
|
30
|
+
nudgeMessage?: string;
|
|
31
|
+
/** Called when the user dismisses the nudge bubble. */
|
|
32
|
+
onDismissNudge?: () => void;
|
|
33
|
+
/** Position of the FAB on the screen. */
|
|
34
|
+
position?: "bottom-right" | "bottom-left";
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// SupportAgentFAB
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export function SupportAgentFAB({
|
|
43
|
+
isOpen = false,
|
|
44
|
+
onClick,
|
|
45
|
+
hasNudge = false,
|
|
46
|
+
nudgeMessage,
|
|
47
|
+
onDismissNudge,
|
|
48
|
+
position = "bottom-right",
|
|
49
|
+
className,
|
|
50
|
+
}: SupportAgentFABProps) {
|
|
51
|
+
const showBubble = hasNudge && !!nudgeMessage && !isOpen;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
className={cn(
|
|
56
|
+
"fixed bottom-6 z-50 flex flex-col items-end gap-2",
|
|
57
|
+
position === "bottom-right" ? "right-6" : "left-6",
|
|
58
|
+
className
|
|
59
|
+
)}
|
|
60
|
+
data-slot="support-agent-fab"
|
|
61
|
+
>
|
|
62
|
+
{/* Nudge message bubble */}
|
|
63
|
+
{showBubble && (
|
|
64
|
+
<div className="relative flex max-w-[220px] items-start gap-2 border border-border bg-background px-3 py-2 shadow-md">
|
|
65
|
+
<p className="flex-1 text-xs text-foreground leading-relaxed">
|
|
66
|
+
{nudgeMessage}
|
|
67
|
+
</p>
|
|
68
|
+
{onDismissNudge && (
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
onClick={onDismissNudge}
|
|
72
|
+
className="mt-0.5 shrink-0 text-muted-foreground hover:text-foreground"
|
|
73
|
+
aria-label="Dismiss"
|
|
74
|
+
>
|
|
75
|
+
<X className="size-3" />
|
|
76
|
+
</button>
|
|
77
|
+
)}
|
|
78
|
+
{/* Bubble tail pointing down */}
|
|
79
|
+
<span
|
|
80
|
+
className="absolute -bottom-[5px] right-4 size-2.5 rotate-45 border-b border-r border-border bg-background"
|
|
81
|
+
aria-hidden="true"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{/* FAB button */}
|
|
87
|
+
<div className="relative">
|
|
88
|
+
<Button
|
|
89
|
+
type="button"
|
|
90
|
+
size="icon"
|
|
91
|
+
onClick={onClick}
|
|
92
|
+
className="size-12 rounded-full shadow-lg"
|
|
93
|
+
aria-label={
|
|
94
|
+
isOpen ? "Close support assistant" : "Open support assistant"
|
|
95
|
+
}
|
|
96
|
+
data-state={isOpen ? "open" : "closed"}
|
|
97
|
+
>
|
|
98
|
+
{isOpen ? (
|
|
99
|
+
<X className="size-5" aria-hidden="true" />
|
|
100
|
+
) : (
|
|
101
|
+
<HelpCircle className="size-5" aria-hidden="true" />
|
|
102
|
+
)}
|
|
103
|
+
</Button>
|
|
104
|
+
|
|
105
|
+
{/* Nudge dot */}
|
|
106
|
+
{hasNudge && !isOpen && (
|
|
107
|
+
<span
|
|
108
|
+
className="absolute -right-0.5 -top-0.5 size-3 rounded-full border-2 border-background bg-destructive"
|
|
109
|
+
aria-label="New suggestion"
|
|
110
|
+
role="status"
|
|
111
|
+
/>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Bot, ChevronLeft, ChevronRight, SquarePen, X } from "lucide-react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { ChatInputArea } from "@/components/ui/chat-input-area";
|
|
7
|
+
import { Spinner } from "@/components/ui/spinner";
|
|
8
|
+
import {
|
|
9
|
+
SupportContextChip,
|
|
10
|
+
SupportSuggestedQuestion,
|
|
11
|
+
SupportStepGuideCard,
|
|
12
|
+
SupportArticleCard,
|
|
13
|
+
} from "./support-agent-primitives";
|
|
14
|
+
import type {
|
|
15
|
+
SupportAgentContext,
|
|
16
|
+
SupportAgentRichContent,
|
|
17
|
+
} from "./support-agent-primitives";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* SupportAgentPanel — WealthX DS (Organism)
|
|
21
|
+
*
|
|
22
|
+
* Right-side slide-over panel providing system guidance to backoffice users.
|
|
23
|
+
* Answers questions about how to use WealthX, optionally grounded in the
|
|
24
|
+
* current page and entity context (Rovo pattern).
|
|
25
|
+
*
|
|
26
|
+
* Follows Jira Rovo's sidebar chat pattern exactly:
|
|
27
|
+
* — Non-blocking: no overlay, main UI remains interactive behind the panel
|
|
28
|
+
* — Two header modes: home (bot icon) vs chat (back arrow + conversation title)
|
|
29
|
+
* — Context chip: optional page+entity label below header (Rovo: context awareness)
|
|
30
|
+
* — Home state: New Chat CTA button + Recents list + Suggested questions
|
|
31
|
+
* — Chat state: message thread with optional rich content cards
|
|
32
|
+
* — Rich messages: AI responses can embed StepGuideCard or ArticleCard
|
|
33
|
+
*
|
|
34
|
+
* Pure display component — all state and API calls are managed by the consumer.
|
|
35
|
+
* Only local state: textarea value (input) and textarea height (auto-resize).
|
|
36
|
+
*
|
|
37
|
+
* Layout — Home state:
|
|
38
|
+
* Header — [Bot icon] Support Assistant [✕]
|
|
39
|
+
* Content — [✏ New chat]
|
|
40
|
+
* RECENTS
|
|
41
|
+
* > Previous conversation title
|
|
42
|
+
* View all →
|
|
43
|
+
* SUGGESTED
|
|
44
|
+
* > Suggested question card
|
|
45
|
+
* Footer — textarea + send
|
|
46
|
+
*
|
|
47
|
+
* Layout — Chat state:
|
|
48
|
+
* Header — [←] Conversation title [✕]
|
|
49
|
+
* Content — chat thread (bubbles + optional rich cards)
|
|
50
|
+
* Footer — textarea + send
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Types
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
export interface SupportAgentMessage {
|
|
58
|
+
id: string;
|
|
59
|
+
role: "user" | "assistant";
|
|
60
|
+
/** Plain text content always shown. */
|
|
61
|
+
content: string;
|
|
62
|
+
/**
|
|
63
|
+
* Optional rich content rendered below the text bubble.
|
|
64
|
+
* Supported: step-guide | article
|
|
65
|
+
*/
|
|
66
|
+
richContent?: SupportAgentRichContent;
|
|
67
|
+
/** True while the assistant response is streaming in. */
|
|
68
|
+
isStreaming?: boolean;
|
|
69
|
+
/** True if the message errored. */
|
|
70
|
+
isErrored?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A recent conversation entry shown in the home-state Recents list. */
|
|
74
|
+
export interface SupportAgentRecentConversation {
|
|
75
|
+
id: string;
|
|
76
|
+
/** Short title (first user message or AI-generated summary). */
|
|
77
|
+
title: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface SupportAgentPanelProps {
|
|
81
|
+
open: boolean;
|
|
82
|
+
onClose: () => void;
|
|
83
|
+
/** Chat message history. Empty array = show home state. */
|
|
84
|
+
messages?: SupportAgentMessage[];
|
|
85
|
+
/**
|
|
86
|
+
* Recent conversations shown in the home state Recents list (Rovo pattern).
|
|
87
|
+
* Clicking an entry fires onOpenConversation(id).
|
|
88
|
+
*/
|
|
89
|
+
recentConversations?: SupportAgentRecentConversation[];
|
|
90
|
+
/**
|
|
91
|
+
* Pre-set help questions shown in the home state (Rovo: conversation starters).
|
|
92
|
+
* Shown below Recents when provided.
|
|
93
|
+
*/
|
|
94
|
+
suggestedQuestions?: string[];
|
|
95
|
+
/**
|
|
96
|
+
* Title shown in the chat-mode header (replaces bot icon row).
|
|
97
|
+
* Typically the first user message or an AI-generated session title.
|
|
98
|
+
* Required for the back-arrow chat header to appear.
|
|
99
|
+
*/
|
|
100
|
+
conversationTitle?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Current backoffice page context — page name and optional entity being viewed.
|
|
103
|
+
* When provided, renders a SupportContextChip below the panel header so the
|
|
104
|
+
* AI can ground responses to the user's current location (Rovo pattern).
|
|
105
|
+
*
|
|
106
|
+
* Example: { pageLabel: "Loan Applications", entityLabel: "John Smith — #4521" }
|
|
107
|
+
*/
|
|
108
|
+
context?: SupportAgentContext;
|
|
109
|
+
/** True while the assistant is generating a response. */
|
|
110
|
+
isStreaming?: boolean;
|
|
111
|
+
/** True while initial data is loading — shows full-panel spinner. */
|
|
112
|
+
isLoading?: boolean;
|
|
113
|
+
/** Called when the user submits a message. Input is cleared after firing. */
|
|
114
|
+
onSendMessage?: (text: string) => void;
|
|
115
|
+
/** Called when the user selects files via the attachment button. */
|
|
116
|
+
onAttachFile?: (files: FileList) => void;
|
|
117
|
+
/** Called when the user selects images via the image upload button. */
|
|
118
|
+
onAttachImage?: (files: FileList) => void;
|
|
119
|
+
/** Called when the user clicks "New chat" — resets to home state. */
|
|
120
|
+
onNewChat?: () => void;
|
|
121
|
+
/**
|
|
122
|
+
* Called when the user clicks ← in the chat header to return to the home state.
|
|
123
|
+
* If not provided, no back button is shown.
|
|
124
|
+
*/
|
|
125
|
+
onBack?: () => void;
|
|
126
|
+
/** Called when the user clicks a recent conversation entry. */
|
|
127
|
+
onOpenConversation?: (id: string) => void;
|
|
128
|
+
/** Called when the user clicks "View all conversations". */
|
|
129
|
+
onViewAllConversations?: () => void;
|
|
130
|
+
className?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// TypingIndicator — three bouncing dots (same pattern as AiAssistantDrawer)
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
function SupportTypingIndicator() {
|
|
138
|
+
return (
|
|
139
|
+
<span
|
|
140
|
+
className="flex items-center gap-1 py-1"
|
|
141
|
+
aria-label="Assistant is thinking"
|
|
142
|
+
role="status"
|
|
143
|
+
>
|
|
144
|
+
{[0, 150, 300].map((delay) => (
|
|
145
|
+
<span
|
|
146
|
+
key={delay}
|
|
147
|
+
className="size-1.5 rounded-full bg-current animate-bounce"
|
|
148
|
+
style={{ animationDelay: `${delay}ms` }}
|
|
149
|
+
aria-hidden="true"
|
|
150
|
+
/>
|
|
151
|
+
))}
|
|
152
|
+
</span>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// MessageBubble — renders a single message with optional rich content
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
interface MessageBubbleProps {
|
|
161
|
+
message: SupportAgentMessage;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function MessageBubble({ message }: MessageBubbleProps) {
|
|
165
|
+
const isUser = message.role === "user";
|
|
166
|
+
const isEmpty = !message.content.trim();
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div
|
|
170
|
+
className={cn(
|
|
171
|
+
"flex w-full flex-col",
|
|
172
|
+
isUser ? "items-end" : "items-start",
|
|
173
|
+
)}
|
|
174
|
+
>
|
|
175
|
+
{/* Text bubble */}
|
|
176
|
+
<div
|
|
177
|
+
className={cn(
|
|
178
|
+
"max-w-[88%] px-3 py-2 text-sm",
|
|
179
|
+
isUser
|
|
180
|
+
? "bg-primary text-primary-foreground"
|
|
181
|
+
: "bg-muted text-foreground",
|
|
182
|
+
message.isErrored && "bg-destructive/10 text-destructive",
|
|
183
|
+
)}
|
|
184
|
+
data-slot="support-message-bubble"
|
|
185
|
+
data-role={message.role}
|
|
186
|
+
>
|
|
187
|
+
{isEmpty && message.isStreaming ? (
|
|
188
|
+
<SupportTypingIndicator />
|
|
189
|
+
) : (
|
|
190
|
+
<span className="whitespace-pre-wrap break-words leading-relaxed">
|
|
191
|
+
{message.content}
|
|
192
|
+
</span>
|
|
193
|
+
)}
|
|
194
|
+
{message.isErrored && (
|
|
195
|
+
<p className="mt-1 text-xs opacity-70">
|
|
196
|
+
Failed to send. Please try again.
|
|
197
|
+
</p>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{/* Rich content — only on assistant messages */}
|
|
202
|
+
{!isUser && message.richContent && (
|
|
203
|
+
<div className="w-full max-w-[88%]">
|
|
204
|
+
<RichContentRenderer richContent={message.richContent} />
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// RichContentRenderer — picks the right card for richContent.type
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
interface RichContentRendererProps {
|
|
216
|
+
richContent: SupportAgentRichContent;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function RichContentRenderer({ richContent }: RichContentRendererProps) {
|
|
220
|
+
if (richContent.type === "step-guide") {
|
|
221
|
+
return (
|
|
222
|
+
<SupportStepGuideCard
|
|
223
|
+
title={richContent.title}
|
|
224
|
+
steps={richContent.steps}
|
|
225
|
+
/>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (richContent.type === "article") {
|
|
230
|
+
return (
|
|
231
|
+
<SupportArticleCard
|
|
232
|
+
title={richContent.title}
|
|
233
|
+
excerpt={richContent.excerpt}
|
|
234
|
+
href={richContent.href}
|
|
235
|
+
isExternal={richContent.isExternal}
|
|
236
|
+
/>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// SupportAgentPanel
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
export function SupportAgentPanel({
|
|
248
|
+
open,
|
|
249
|
+
onClose,
|
|
250
|
+
messages = [],
|
|
251
|
+
recentConversations,
|
|
252
|
+
suggestedQuestions = [],
|
|
253
|
+
conversationTitle,
|
|
254
|
+
context,
|
|
255
|
+
isStreaming = false,
|
|
256
|
+
isLoading = false,
|
|
257
|
+
onSendMessage,
|
|
258
|
+
onAttachFile,
|
|
259
|
+
onAttachImage,
|
|
260
|
+
onNewChat,
|
|
261
|
+
onBack,
|
|
262
|
+
onOpenConversation,
|
|
263
|
+
onViewAllConversations,
|
|
264
|
+
className,
|
|
265
|
+
}: SupportAgentPanelProps) {
|
|
266
|
+
const [inputValue, setInputValue] = React.useState("");
|
|
267
|
+
const messagesEndRef = React.useRef<HTMLDivElement>(null);
|
|
268
|
+
|
|
269
|
+
const hasMessages = messages.length > 0;
|
|
270
|
+
|
|
271
|
+
// Chat header mode: has messages AND a conversation title
|
|
272
|
+
const isChatMode = hasMessages && !!conversationTitle;
|
|
273
|
+
|
|
274
|
+
// Auto-scroll to latest message
|
|
275
|
+
React.useEffect(() => {
|
|
276
|
+
if (!messagesEndRef.current) return;
|
|
277
|
+
messagesEndRef.current.scrollIntoView({
|
|
278
|
+
behavior: "smooth",
|
|
279
|
+
block: "nearest",
|
|
280
|
+
});
|
|
281
|
+
}, [messages.length]);
|
|
282
|
+
|
|
283
|
+
const handleSend = React.useCallback(
|
|
284
|
+
(text: string) => {
|
|
285
|
+
onSendMessage?.(text);
|
|
286
|
+
setInputValue("");
|
|
287
|
+
},
|
|
288
|
+
[onSendMessage],
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const handleQuestionSelect = React.useCallback(
|
|
292
|
+
(question: string) => {
|
|
293
|
+
onSendMessage?.(question);
|
|
294
|
+
},
|
|
295
|
+
[onSendMessage],
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const hasRecents = !!recentConversations?.length;
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<Sheet open={open} onOpenChange={(o) => !o && onClose()}>
|
|
302
|
+
<SheetContent
|
|
303
|
+
side="right"
|
|
304
|
+
showCloseButton={false}
|
|
305
|
+
className={cn(
|
|
306
|
+
"flex w-[400px] max-w-full flex-col gap-0 p-0",
|
|
307
|
+
className,
|
|
308
|
+
)}
|
|
309
|
+
data-slot="support-agent-panel"
|
|
310
|
+
>
|
|
311
|
+
{/* ── Header ── */}
|
|
312
|
+
<div className="shrink-0 border-b border-border px-3 py-2.5">
|
|
313
|
+
{isChatMode ? (
|
|
314
|
+
/* Chat mode: [←] Conversation title [✕] */
|
|
315
|
+
<div className="flex items-center gap-1">
|
|
316
|
+
{onBack && (
|
|
317
|
+
<Button
|
|
318
|
+
variant="ghost"
|
|
319
|
+
size="icon"
|
|
320
|
+
className="size-7 shrink-0"
|
|
321
|
+
onClick={onBack}
|
|
322
|
+
title="Back to conversations"
|
|
323
|
+
>
|
|
324
|
+
<ChevronLeft className="size-3.5" />
|
|
325
|
+
<span className="sr-only">Back</span>
|
|
326
|
+
</Button>
|
|
327
|
+
)}
|
|
328
|
+
<span className="flex-1 truncate px-1 text-sm font-medium text-foreground">
|
|
329
|
+
{conversationTitle}
|
|
330
|
+
</span>
|
|
331
|
+
<Button
|
|
332
|
+
variant="ghost"
|
|
333
|
+
size="icon"
|
|
334
|
+
className="size-7 shrink-0"
|
|
335
|
+
onClick={onClose}
|
|
336
|
+
title="Close"
|
|
337
|
+
>
|
|
338
|
+
<X className="size-3.5" />
|
|
339
|
+
<span className="sr-only">Close</span>
|
|
340
|
+
</Button>
|
|
341
|
+
</div>
|
|
342
|
+
) : (
|
|
343
|
+
/* Home mode: [Bot icon] Support Assistant [✕] */
|
|
344
|
+
<div className="flex items-center justify-between">
|
|
345
|
+
<div className="flex items-center gap-2.5">
|
|
346
|
+
<span className="flex size-7 shrink-0 items-center justify-center rounded-full border border-primary/40 bg-primary/10">
|
|
347
|
+
<Bot
|
|
348
|
+
className="size-3.5 text-foreground"
|
|
349
|
+
aria-hidden="true"
|
|
350
|
+
/>
|
|
351
|
+
</span>
|
|
352
|
+
<span className="text-sm font-semibold text-foreground">
|
|
353
|
+
Support Assistant
|
|
354
|
+
</span>
|
|
355
|
+
</div>
|
|
356
|
+
<Button
|
|
357
|
+
variant="ghost"
|
|
358
|
+
size="icon"
|
|
359
|
+
className="size-7"
|
|
360
|
+
onClick={onClose}
|
|
361
|
+
title="Close"
|
|
362
|
+
>
|
|
363
|
+
<X className="size-3.5" />
|
|
364
|
+
<span className="sr-only">Close</span>
|
|
365
|
+
</Button>
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
{/* Context chip — shown below header title in both modes */}
|
|
369
|
+
{context && (
|
|
370
|
+
<div className="mt-2">
|
|
371
|
+
<SupportContextChip context={context} />
|
|
372
|
+
</div>
|
|
373
|
+
)}
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
{/* ── Content ── */}
|
|
377
|
+
<div className="flex flex-1 flex-col overflow-y-auto">
|
|
378
|
+
{isLoading ? (
|
|
379
|
+
/* Loading state */
|
|
380
|
+
<div className="flex flex-1 items-center justify-center py-20">
|
|
381
|
+
<div className="flex flex-col items-center gap-3">
|
|
382
|
+
<Spinner size="lg" className="text-muted-foreground" />
|
|
383
|
+
<p className="text-sm text-muted-foreground">Loading…</p>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
) : !hasMessages ? (
|
|
387
|
+
/* Home state — Rovo pattern: New Chat CTA + Recents + Suggested */
|
|
388
|
+
<div className="flex flex-col gap-5 p-4">
|
|
389
|
+
{/* New Chat — prominent CTA button */}
|
|
390
|
+
{onNewChat && (
|
|
391
|
+
<Button
|
|
392
|
+
variant="outline"
|
|
393
|
+
className="w-full justify-start gap-2 text-sm font-medium"
|
|
394
|
+
onClick={onNewChat}
|
|
395
|
+
disabled={isLoading}
|
|
396
|
+
>
|
|
397
|
+
<SquarePen className="size-3.5" aria-hidden="true" />
|
|
398
|
+
New chat
|
|
399
|
+
</Button>
|
|
400
|
+
)}
|
|
401
|
+
|
|
402
|
+
{/* Recents — previous conversations */}
|
|
403
|
+
{hasRecents && (
|
|
404
|
+
<div className="flex flex-col gap-1">
|
|
405
|
+
<p className="px-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
406
|
+
Recents
|
|
407
|
+
</p>
|
|
408
|
+
<div className="flex flex-col">
|
|
409
|
+
{recentConversations!.map((conv) => (
|
|
410
|
+
<button
|
|
411
|
+
key={conv.id}
|
|
412
|
+
type="button"
|
|
413
|
+
onClick={() => onOpenConversation?.(conv.id)}
|
|
414
|
+
className="flex w-full items-center justify-between gap-2 px-1 py-2.5 text-left text-sm text-foreground hover:bg-muted/50"
|
|
415
|
+
>
|
|
416
|
+
<span className="flex-1 truncate">{conv.title}</span>
|
|
417
|
+
<ChevronRight
|
|
418
|
+
className="size-3.5 shrink-0 text-muted-foreground"
|
|
419
|
+
aria-hidden="true"
|
|
420
|
+
/>
|
|
421
|
+
</button>
|
|
422
|
+
))}
|
|
423
|
+
</div>
|
|
424
|
+
{onViewAllConversations && (
|
|
425
|
+
<Button
|
|
426
|
+
variant="ghost"
|
|
427
|
+
size="sm"
|
|
428
|
+
onClick={onViewAllConversations}
|
|
429
|
+
className="h-auto w-fit gap-0.5 px-1 py-1 text-xs text-muted-foreground hover:bg-transparent hover:text-foreground"
|
|
430
|
+
>
|
|
431
|
+
View all conversations
|
|
432
|
+
<ChevronRight className="size-3.5" aria-hidden="true" />
|
|
433
|
+
</Button>
|
|
434
|
+
)}
|
|
435
|
+
</div>
|
|
436
|
+
)}
|
|
437
|
+
|
|
438
|
+
{/* Suggested — conversation starters */}
|
|
439
|
+
{suggestedQuestions.length > 0 && (
|
|
440
|
+
<div className="flex flex-col gap-1.5">
|
|
441
|
+
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
442
|
+
Suggested
|
|
443
|
+
</p>
|
|
444
|
+
<div className="flex flex-col gap-1.5">
|
|
445
|
+
{suggestedQuestions.map((q) => (
|
|
446
|
+
<SupportSuggestedQuestion
|
|
447
|
+
key={q}
|
|
448
|
+
question={q}
|
|
449
|
+
onSelect={handleQuestionSelect}
|
|
450
|
+
/>
|
|
451
|
+
))}
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
|
|
456
|
+
{/* Fallback — nothing to show */}
|
|
457
|
+
{!onNewChat && !hasRecents && !suggestedQuestions.length && (
|
|
458
|
+
<p className="text-sm text-muted-foreground">
|
|
459
|
+
How can I help you today?
|
|
460
|
+
</p>
|
|
461
|
+
)}
|
|
462
|
+
</div>
|
|
463
|
+
) : (
|
|
464
|
+
/* Chat thread */
|
|
465
|
+
<div className="flex flex-col gap-3 p-4">
|
|
466
|
+
{messages.map((msg) => (
|
|
467
|
+
<MessageBubble key={msg.id} message={msg} />
|
|
468
|
+
))}
|
|
469
|
+
{/* Streaming indicator — shown when last message is from the user */}
|
|
470
|
+
{isStreaming &&
|
|
471
|
+
messages[messages.length - 1]?.role === "user" && (
|
|
472
|
+
<div className="flex justify-start">
|
|
473
|
+
<div className="bg-muted px-3 py-2 text-muted-foreground">
|
|
474
|
+
<SupportTypingIndicator />
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
)}
|
|
478
|
+
<div ref={messagesEndRef} aria-hidden="true" />
|
|
479
|
+
</div>
|
|
480
|
+
)}
|
|
481
|
+
</div>
|
|
482
|
+
|
|
483
|
+
{/* ── Footer ── */}
|
|
484
|
+
<div className="shrink-0 border-t border-border p-3">
|
|
485
|
+
<ChatInputArea
|
|
486
|
+
value={inputValue}
|
|
487
|
+
onChange={setInputValue}
|
|
488
|
+
onSend={handleSend}
|
|
489
|
+
onAttachFile={onAttachFile}
|
|
490
|
+
onAttachImage={onAttachImage}
|
|
491
|
+
disabled={isLoading || isStreaming}
|
|
492
|
+
placeholder="Ask anything…"
|
|
493
|
+
/>
|
|
494
|
+
</div>
|
|
495
|
+
</SheetContent>
|
|
496
|
+
</Sheet>
|
|
497
|
+
);
|
|
498
|
+
}
|