@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.
Files changed (60) hide show
  1. package/.turbo/turbo-build.log +142 -133
  2. package/CHANGELOG.md +12 -0
  3. package/dist/{chunk-LSSIWLYU.mjs → chunk-6XNEHTII.mjs} +1 -1
  4. package/dist/{chunk-ULQ53FRJ.mjs → chunk-7NQKFPXE.mjs} +1 -1
  5. package/dist/{chunk-734FOOJC.mjs → chunk-B5PSUONN.mjs} +25 -58
  6. package/dist/{chunk-DSVKEVX6.mjs → chunk-CZOGJC76.mjs} +1 -1
  7. package/dist/chunk-EFHPSKVF.mjs +192 -0
  8. package/dist/{chunk-JPGL36WQ.mjs → chunk-FL7DEYUA.mjs} +6 -7
  9. package/dist/{chunk-2CHH5QOA.mjs → chunk-FQUT5XD6.mjs} +1 -1
  10. package/dist/chunk-MGIDYXOP.mjs +814 -0
  11. package/dist/{chunk-OG2VM34K.mjs → chunk-MHBQJVHE.mjs} +1 -1
  12. package/dist/{chunk-NB3ZL36B.mjs → chunk-MZI77ZMX.mjs} +17 -2
  13. package/dist/chunk-R7M657QL.mjs +587 -0
  14. package/dist/{chunk-DIH2NZZ3.mjs → chunk-RRROLESJ.mjs} +33 -23
  15. package/dist/components/ui/ai-assistant-drawer.js +269 -121
  16. package/dist/components/ui/ai-assistant-drawer.mjs +2 -1
  17. package/dist/components/ui/ai-conversations/index.js +474 -286
  18. package/dist/components/ui/ai-conversations/index.mjs +2 -1
  19. package/dist/components/ui/chat-input-area.js +429 -0
  20. package/dist/components/ui/chat-input-area.mjs +11 -0
  21. package/dist/components/ui/file-preview-dialog.js +6 -7
  22. package/dist/components/ui/file-preview-dialog.mjs +2 -2
  23. package/dist/components/ui/kanban-column.js +6 -7
  24. package/dist/components/ui/kanban-column.mjs +3 -3
  25. package/dist/components/ui/opportunity-card.js +6 -7
  26. package/dist/components/ui/opportunity-card.mjs +2 -2
  27. package/dist/components/ui/page-top-bar.js +182 -5
  28. package/dist/components/ui/page-top-bar.mjs +3 -1
  29. package/dist/components/ui/pipeline-board.js +6 -7
  30. package/dist/components/ui/pipeline-board.mjs +4 -4
  31. package/dist/components/ui/policy-ai/index.js +1636 -0
  32. package/dist/components/ui/policy-ai/index.mjs +36 -0
  33. package/dist/components/ui/progress.js +6 -7
  34. package/dist/components/ui/progress.mjs +1 -1
  35. package/dist/components/ui/stage-timeline.js +6 -7
  36. package/dist/components/ui/stage-timeline.mjs +2 -2
  37. package/dist/components/ui/support-agent/index.js +1131 -0
  38. package/dist/components/ui/support-agent/index.mjs +27 -0
  39. package/dist/index.js +5609 -4100
  40. package/dist/index.mjs +77 -41
  41. package/dist/styles.css +1 -1
  42. package/package.json +16 -1
  43. package/src/components/index.tsx +54 -0
  44. package/src/components/ui/ai-assistant-drawer.tsx +24 -51
  45. package/src/components/ui/ai-conversations/index.tsx +16 -8
  46. package/src/components/ui/ai-conversations/thread.tsx +38 -27
  47. package/src/components/ui/chat-input-area.tsx +244 -0
  48. package/src/components/ui/page-top-bar.tsx +31 -5
  49. package/src/components/ui/policy-ai/index.tsx +41 -0
  50. package/src/components/ui/policy-ai/policy-ai-panel.tsx +526 -0
  51. package/src/components/ui/policy-ai/policy-ai-primitives.tsx +332 -0
  52. package/src/components/ui/policy-ai/policy-ai-responses.tsx +543 -0
  53. package/src/components/ui/progress.tsx +15 -12
  54. package/src/components/ui/support-agent/index.tsx +25 -0
  55. package/src/components/ui/support-agent/support-agent-fab.tsx +116 -0
  56. package/src/components/ui/support-agent/support-agent-panel.tsx +498 -0
  57. package/src/components/ui/support-agent/support-agent-primitives.tsx +354 -0
  58. package/src/styles/globals.css +1 -0
  59. package/src/styles/styles-css.ts +1 -1
  60. 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
+ }