@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,526 @@
1
+ import * as React from "react";
2
+ import {
3
+ BrainCircuit,
4
+ Check,
5
+ ChevronDown,
6
+ Minus,
7
+ RotateCcw,
8
+ X,
9
+ } from "lucide-react";
10
+ import { cn } from "@/lib/utils";
11
+ import { Button } from "@/components/ui/button";
12
+ import { ChatInputArea } from "@/components/ui/chat-input-area";
13
+ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
14
+ import { ChatWidgetMessage } from "@/components/ui/chat-widget-primitives";
15
+ import {
16
+ PolicySingleBankAnswer,
17
+ PolicyComparisonTable,
18
+ PolicyRankedList,
19
+ } from "./policy-ai-responses";
20
+ import type {
21
+ PolicyAIMessage,
22
+ PolicyQueryContext,
23
+ PolicyResponseContent,
24
+ } from "./policy-ai-primitives";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // PolicyAIFAB props
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export interface PolicyAIFABProps {
31
+ onClick: () => void;
32
+ hasNudge?: boolean;
33
+ className?: string;
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // PolicyAIPanelProps
38
+ // ---------------------------------------------------------------------------
39
+
40
+ export interface PolicyAIPanelProps {
41
+ open: boolean;
42
+ onClose: () => void;
43
+ messages?: PolicyAIMessage[];
44
+ suggestedQuestions?: Record<string, string[]>;
45
+ isStreaming?: boolean;
46
+ isLoading?: boolean;
47
+ /**
48
+ * Custom thinking steps shown during loading/streaming.
49
+ * Defaults to the built-in 4-step sequence if not provided.
50
+ */
51
+ thinkingSteps?: string[];
52
+ onSendMessage?: (text: string) => void;
53
+ /** Called when the user selects files via the paperclip attachment button. */
54
+ onAttachFile?: (files: FileList) => void;
55
+ /** Called when the user selects images via the image upload button. */
56
+ onAttachImage?: (files: FileList) => void;
57
+ onReset?: () => void;
58
+ /** Position relative to viewport — defaults to bottom-right. Ignored when `inline` is true. */
59
+ position?: "bottom-right" | "bottom-left";
60
+ /**
61
+ * When true, renders as an inline block filling its parent container instead
62
+ * of a fixed-position float widget. Use inside drawers, sheets, or tab panels.
63
+ * Hides the minimize and close buttons.
64
+ */
65
+ inline?: boolean;
66
+ className?: string;
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Internal helpers
71
+ // ---------------------------------------------------------------------------
72
+
73
+ type PolicyTypeTab =
74
+ | "Income"
75
+ | "Security"
76
+ | "Serviceability"
77
+ | "Loan Type"
78
+ | "Borrower";
79
+
80
+ const DEFAULT_SUGGESTED: Record<PolicyTypeTab, string[]> = {
81
+ Income: [
82
+ "Which banks accept casual income?",
83
+ "Which lenders accept bonus income, ranked best to worst?",
84
+ "How is self-employed income assessed across lenders?",
85
+ ],
86
+ Security: [
87
+ "Which banks accept apartments under 40sqm?",
88
+ "What is the maximum LVR for a serviced apartment?",
89
+ "Do any lenders accept hobby farm properties?",
90
+ ],
91
+ Serviceability: [
92
+ "Which banks have the most flexible DTI ratio?",
93
+ "How is HECS debt treated by different lenders?",
94
+ "Which lenders apply the lowest assessment buffer rate?",
95
+ ],
96
+ "Loan Type": [
97
+ "Which lenders offer cashback on refinances?",
98
+ "Which banks accept low-doc construction loans?",
99
+ "What are the pre-approval policies across lenders?",
100
+ ],
101
+ Borrower: [
102
+ "Which banks accept non-resident borrowers?",
103
+ "Which lenders offer LMI waiver for medical professionals?",
104
+ "How do lenders treat prior credit impairment?",
105
+ ],
106
+ };
107
+
108
+ const DEFAULT_THINKING_STEPS = [
109
+ "Classifying query type…",
110
+ "Searching across 40+ banks…",
111
+ "Retrieving policy documents…",
112
+ "Generating response…",
113
+ ];
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // PolicyAIThinkingSteps
117
+ // ---------------------------------------------------------------------------
118
+
119
+ /**
120
+ * Animated thinking steps shown while the AI is working.
121
+ * Steps progress automatically on a timer; completed steps show a check mark.
122
+ */
123
+ function PolicyAIThinkingSteps({ steps }: { steps: string[] }) {
124
+ const [currentStep, setCurrentStep] = React.useState(0);
125
+
126
+ React.useEffect(() => {
127
+ setCurrentStep(0);
128
+ }, [steps]);
129
+
130
+ React.useEffect(() => {
131
+ if (currentStep >= steps.length - 1) return;
132
+ const timer = setTimeout(() => setCurrentStep((s) => s + 1), 1400);
133
+ return () => clearTimeout(timer);
134
+ }, [currentStep, steps.length]);
135
+
136
+ return (
137
+ <div className="flex flex-col gap-2.5 px-4 py-3">
138
+ {steps.map((step, i) => {
139
+ const isDone = i < currentStep;
140
+ const isCurrent = i === currentStep;
141
+ const isPending = i > currentStep;
142
+
143
+ return (
144
+ <div
145
+ key={step}
146
+ className={cn(
147
+ "flex items-center gap-2.5 transition-opacity duration-300",
148
+ isPending && "opacity-30",
149
+ )}
150
+ >
151
+ <span className="shrink-0 size-4 flex items-center justify-center">
152
+ {isDone ? (
153
+ <Check className="size-3.5 text-primary" aria-hidden="true" />
154
+ ) : isCurrent ? (
155
+ <span
156
+ className="flex items-center gap-0.5"
157
+ aria-label="In progress"
158
+ >
159
+ {[0, 1, 2].map((j) => (
160
+ <span
161
+ key={j}
162
+ className="size-1 bg-muted-foreground/70 animate-bounce"
163
+ style={{ animationDelay: `${j * 150}ms` }}
164
+ />
165
+ ))}
166
+ </span>
167
+ ) : null}
168
+ </span>
169
+
170
+ <span
171
+ className={cn(
172
+ "text-sm leading-snug transition-colors duration-200",
173
+ isDone && "text-muted-foreground",
174
+ isCurrent && "text-foreground font-medium",
175
+ isPending && "text-muted-foreground",
176
+ )}
177
+ >
178
+ {step}
179
+ </span>
180
+ </div>
181
+ );
182
+ })}
183
+ </div>
184
+ );
185
+ }
186
+
187
+ // ---------------------------------------------------------------------------
188
+ // ResponseCard — dispatches to the correct response molecule
189
+ // ---------------------------------------------------------------------------
190
+
191
+ function ResponseCard({
192
+ content,
193
+ queryContext,
194
+ }: {
195
+ content: PolicyResponseContent;
196
+ queryContext?: PolicyQueryContext;
197
+ }) {
198
+ if (content.type === "single_bank") {
199
+ return (
200
+ <PolicySingleBankAnswer
201
+ bankName={content.bankName}
202
+ verdict={content.verdict}
203
+ answer={content.answer}
204
+ citations={content.citations}
205
+ queryContext={queryContext}
206
+ />
207
+ );
208
+ }
209
+ if (content.type === "cross_bank_comparison") {
210
+ return (
211
+ <PolicyComparisonTable
212
+ categories={content.categories}
213
+ banks={content.banks}
214
+ summaryCounts={content.summaryCounts}
215
+ citations={content.citations}
216
+ queryContext={queryContext}
217
+ />
218
+ );
219
+ }
220
+ if (content.type === "ranked_list") {
221
+ return (
222
+ <PolicyRankedList
223
+ categories={content.categories}
224
+ banks={content.banks}
225
+ citations={content.citations}
226
+ queryContext={queryContext}
227
+ />
228
+ );
229
+ }
230
+ return null;
231
+ }
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // PolicyAIFAB (Molecule)
235
+ // ---------------------------------------------------------------------------
236
+
237
+ /**
238
+ * Floating action button that opens the PolicyAIPanel.
239
+ *
240
+ * @example
241
+ * <PolicyAIFAB onClick={() => setOpen(true)} />
242
+ */
243
+ export function PolicyAIFAB({
244
+ onClick,
245
+ hasNudge,
246
+ className,
247
+ }: PolicyAIFABProps) {
248
+ return (
249
+ <div className={cn("relative inline-flex shrink-0", className)}>
250
+ <Button
251
+ data-slot="policy-ai-fab"
252
+ size="icon"
253
+ onClick={onClick}
254
+ className="size-12 shadow-lg rounded-full"
255
+ aria-label="Open Policy AI"
256
+ >
257
+ <BrainCircuit className="size-5" aria-hidden="true" />
258
+ </Button>
259
+ {hasNudge && (
260
+ <span className="absolute -top-1 -right-1 size-3 bg-destructive rounded-full border-2 border-background" />
261
+ )}
262
+ </div>
263
+ );
264
+ }
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // PolicyAIPanel (Organism) — Jira Rovo-style floating / inline widget
268
+ // ---------------------------------------------------------------------------
269
+
270
+ /**
271
+ * Policy AI chat panel — shows suggested questions in home state and
272
+ * structured response cards (SingleBankAnswer / ComparisonTable / RankedList)
273
+ * in chat state.
274
+ *
275
+ * Float mode: fixed-position overlay next to the FAB.
276
+ * Inline mode: fills its parent container — use inside drawers or tab panels.
277
+ *
278
+ * @example
279
+ * <PolicyAIFAB onClick={() => setOpen(true)} />
280
+ * <PolicyAIPanel
281
+ * open={open}
282
+ * onClose={() => setOpen(false)}
283
+ * messages={messages}
284
+ * onSendMessage={handleSend}
285
+ * onReset={() => setMessages([])}
286
+ * />
287
+ */
288
+ export function PolicyAIPanel({
289
+ open,
290
+ onClose,
291
+ messages = [],
292
+ suggestedQuestions,
293
+ isStreaming = false,
294
+ isLoading = false,
295
+ thinkingSteps,
296
+ onSendMessage,
297
+ onAttachFile,
298
+ onAttachImage,
299
+ onReset,
300
+ position = "bottom-right",
301
+ inline = false,
302
+ className,
303
+ }: PolicyAIPanelProps) {
304
+ const resolvedThinkingSteps = thinkingSteps ?? DEFAULT_THINKING_STEPS;
305
+ const [inputValue, setInputValue] = React.useState("");
306
+ const [minimised, setMinimised] = React.useState(false);
307
+ const [activeTab, setActiveTab] = React.useState<PolicyTypeTab>("Income");
308
+ const messagesEndRef = React.useRef<HTMLDivElement>(null);
309
+
310
+ const isChatMode = messages.length > 0;
311
+
312
+ // Auto-scroll to latest message
313
+ React.useEffect(() => {
314
+ if (open && !minimised) {
315
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
316
+ }
317
+ }, [messages, open, minimised]);
318
+
319
+ const handleSend = React.useCallback(
320
+ (text: string) => {
321
+ if (!text || isStreaming) return;
322
+ onSendMessage?.(text);
323
+ setInputValue("");
324
+ },
325
+ [isStreaming, onSendMessage],
326
+ );
327
+
328
+ if (!open) return null;
329
+
330
+ const positionClass = position === "bottom-left" ? "left-6" : "right-6";
331
+
332
+ return (
333
+ <div
334
+ data-slot="policy-ai-panel"
335
+ className={cn(
336
+ "flex flex-col bg-card",
337
+ inline
338
+ ? "h-full border-0"
339
+ : [
340
+ "fixed z-50 border border-border shadow-2xl",
341
+ "bottom-20 w-[400px]",
342
+ "animate-in slide-in-from-bottom-4 fade-in duration-200",
343
+ positionClass,
344
+ minimised && "shadow-lg",
345
+ ],
346
+ className,
347
+ )}
348
+ style={
349
+ inline
350
+ ? undefined
351
+ : {
352
+ maxHeight: minimised ? "auto" : "min(600px, calc(100vh - 120px))",
353
+ }
354
+ }
355
+ >
356
+ {/* ── Header — float mode only ── */}
357
+ {!inline && (
358
+ <div className="shrink-0 flex items-center gap-2.5 px-3 py-2.5 border-b border-border bg-card">
359
+ <BrainCircuit
360
+ className="size-4 text-primary shrink-0"
361
+ aria-hidden="true"
362
+ />
363
+ <span className="text-sm font-semibold text-foreground flex-1 truncate">
364
+ Policy AI
365
+ </span>
366
+
367
+ <div className="flex items-center gap-0.5">
368
+ {isChatMode && onReset && !minimised && (
369
+ <Button
370
+ variant="ghost"
371
+ size="icon-sm"
372
+ onClick={onReset}
373
+ title="New conversation"
374
+ aria-label="New conversation"
375
+ >
376
+ <RotateCcw className="size-3.5" aria-hidden="true" />
377
+ </Button>
378
+ )}
379
+ <Button
380
+ variant="ghost"
381
+ size="icon-sm"
382
+ onClick={() => setMinimised((v) => !v)}
383
+ title={minimised ? "Restore" : "Minimise"}
384
+ aria-label={minimised ? "Restore panel" : "Minimise panel"}
385
+ >
386
+ {minimised ? (
387
+ <ChevronDown className="size-3.5" aria-hidden="true" />
388
+ ) : (
389
+ <Minus className="size-3.5" aria-hidden="true" />
390
+ )}
391
+ </Button>
392
+ <Button
393
+ variant="ghost"
394
+ size="icon-sm"
395
+ onClick={onClose}
396
+ title="Close"
397
+ aria-label="Close Policy AI"
398
+ >
399
+ <X className="size-3.5" aria-hidden="true" />
400
+ </Button>
401
+ </div>
402
+ </div>
403
+ )}
404
+
405
+ {/* ── Body ── */}
406
+ {(!minimised || inline) && (
407
+ <>
408
+ {/* Scroll container:
409
+ Float — scrolls internally (flex-1 + overflow-y-auto).
410
+ Inline — parent tab/drawer is the scroller; content flows naturally. */}
411
+ <div className={cn(!inline && "flex-1 overflow-y-auto min-h-0")}>
412
+ {isLoading ? (
413
+ <div className="flex flex-col justify-center h-full py-8">
414
+ <PolicyAIThinkingSteps steps={resolvedThinkingSteps} />
415
+ </div>
416
+ ) : !isChatMode ? (
417
+ /* Home state — suggested questions by policy type */
418
+ <div className="flex flex-col">
419
+ <div className="px-3 pt-3 pb-2">
420
+ <p className="text-xs text-muted-foreground leading-relaxed">
421
+ Ask me about lending policies across 40+ Australian banks —
422
+ income, LVR, security types, serviceability, and more.
423
+ </p>
424
+ </div>
425
+
426
+ <div className="border-b border-border px-3 pt-1">
427
+ <Tabs
428
+ value={activeTab}
429
+ onValueChange={(v) => setActiveTab(v as PolicyTypeTab)}
430
+ >
431
+ <TabsList
432
+ variant="line"
433
+ className="justify-start h-8 gap-0 -mb-px overflow-x-auto"
434
+ >
435
+ {(Object.keys(DEFAULT_SUGGESTED) as PolicyTypeTab[]).map(
436
+ (type) => (
437
+ <TabsTrigger
438
+ key={type}
439
+ value={type}
440
+ className="text-xs px-2 h-7 shrink-0"
441
+ >
442
+ {type}
443
+ </TabsTrigger>
444
+ ),
445
+ )}
446
+ </TabsList>
447
+ </Tabs>
448
+ </div>
449
+
450
+ <div className="flex flex-col px-3 py-2 gap-1">
451
+ {(
452
+ (suggestedQuestions ?? DEFAULT_SUGGESTED)[activeTab] ?? []
453
+ ).map((q) => (
454
+ <button
455
+ key={q}
456
+ onClick={() => setInputValue(q)}
457
+ className="w-full text-left text-xs text-foreground px-3 py-2 border border-border hover:bg-muted/60 transition-colors leading-relaxed"
458
+ >
459
+ {q}
460
+ </button>
461
+ ))}
462
+ </div>
463
+ </div>
464
+ ) : (
465
+ /* Chat state — messages + structured response cards */
466
+ <div className="flex flex-col gap-4 px-3 py-3">
467
+ {messages.map((msg) =>
468
+ msg.role === "user" ? (
469
+ <ChatWidgetMessage
470
+ key={msg.id}
471
+ role="user"
472
+ content={msg.content}
473
+ />
474
+ ) : (
475
+ <div key={msg.id} className="flex flex-col gap-1.5">
476
+ {msg.content && (
477
+ <p className="text-sm text-foreground leading-relaxed">
478
+ {msg.content}
479
+ </p>
480
+ )}
481
+ {msg.responseContent && (
482
+ <ResponseCard
483
+ content={msg.responseContent}
484
+ queryContext={msg.queryContext}
485
+ />
486
+ )}
487
+ </div>
488
+ ),
489
+ )}
490
+
491
+ {isStreaming && (
492
+ <PolicyAIThinkingSteps steps={resolvedThinkingSteps} />
493
+ )}
494
+
495
+ <div ref={messagesEndRef} />
496
+ </div>
497
+ )}
498
+ </div>
499
+
500
+ {/* Footer input — hidden during loading.
501
+ Float: shrink-0 pins it to the flex column bottom.
502
+ Inline: sticky bottom-0 keeps it visible as the parent tab scrolls. */}
503
+ {!isLoading && (
504
+ <div
505
+ className={cn(
506
+ "border-t border-border px-3 py-3 bg-card",
507
+ inline ? "sticky bottom-0 z-10" : "shrink-0",
508
+ )}
509
+ >
510
+ <ChatInputArea
511
+ value={inputValue}
512
+ onChange={setInputValue}
513
+ onSend={handleSend}
514
+ onAttachFile={onAttachFile}
515
+ onAttachImage={onAttachImage}
516
+ disabled={isStreaming}
517
+ placeholder="Ask about lending policies…"
518
+ autoFocus={!minimised}
519
+ />
520
+ </div>
521
+ )}
522
+ </>
523
+ )}
524
+ </div>
525
+ );
526
+ }