@wealthx/shadcn 1.5.39 → 1.5.41

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 (30) hide show
  1. package/.turbo/turbo-build.log +118 -118
  2. package/CHANGELOG.md +12 -0
  3. package/dist/{chunk-MGIDYXOP.mjs → chunk-DWNLBUDC.mjs} +459 -67
  4. package/dist/{chunk-EFHPSKVF.mjs → chunk-EGM4DARZ.mjs} +110 -1
  5. package/dist/{chunk-R7M657QL.mjs → chunk-GIQGZFP6.mjs} +138 -46
  6. package/dist/{chunk-B5PSUONN.mjs → chunk-TF5TOVIM.mjs} +1 -1
  7. package/dist/{chunk-RRROLESJ.mjs → chunk-XHZONBL4.mjs} +1 -1
  8. package/dist/components/ui/ai-assistant-drawer.js +101 -0
  9. package/dist/components/ui/ai-assistant-drawer.mjs +2 -2
  10. package/dist/components/ui/ai-conversations/index.js +101 -0
  11. package/dist/components/ui/ai-conversations/index.mjs +2 -2
  12. package/dist/components/ui/chat-input-area.js +101 -0
  13. package/dist/components/ui/chat-input-area.mjs +1 -1
  14. package/dist/components/ui/policy-ai/index.js +818 -261
  15. package/dist/components/ui/policy-ai/index.mjs +11 -2
  16. package/dist/components/ui/support-agent/index.js +233 -45
  17. package/dist/components/ui/support-agent/index.mjs +2 -2
  18. package/dist/index.js +3521 -3330
  19. package/dist/index.mjs +5 -5
  20. package/dist/styles.css +1 -1
  21. package/package.json +1 -1
  22. package/src/components/ui/chat-input-area.tsx +181 -2
  23. package/src/components/ui/policy-ai/index.tsx +12 -0
  24. package/src/components/ui/policy-ai/policy-ai-context-sidebar.tsx +231 -0
  25. package/src/components/ui/policy-ai/policy-ai-history-panel.tsx +175 -0
  26. package/src/components/ui/policy-ai/policy-ai-page.tsx +243 -0
  27. package/src/components/ui/policy-ai/policy-ai-panel.tsx +64 -57
  28. package/src/components/ui/policy-ai/policy-ai-responses.tsx +8 -12
  29. package/src/components/ui/support-agent/support-agent-panel.tsx +170 -48
  30. package/src/styles/styles-css.ts +1 -1
@@ -0,0 +1,243 @@
1
+ import * as React from "react";
2
+ import { PanelRight } from "lucide-react";
3
+ import { cn } from "@/lib/utils";
4
+ import { Button } from "@/components/ui/button";
5
+ import { PageTopBar } from "@/components/ui/page-top-bar";
6
+ import { PolicyAIPanel } from "./policy-ai-panel";
7
+ import { PolicyAIHistoryPanel } from "./policy-ai-history-panel";
8
+ import { PolicyAIContextSidebar } from "./policy-ai-context-sidebar";
9
+ import type {
10
+ PolicyAIMessage,
11
+ PolicyConversationItem,
12
+ PolicyQueryContext,
13
+ } from "./policy-ai-primitives";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Props
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export interface PolicyAIPageProps {
20
+ // ── Chat area (pass-through to PolicyAIPanel inline) ──
21
+ messages?: PolicyAIMessage[];
22
+ suggestedQuestions?: Record<string, string[]>;
23
+ isStreaming?: boolean;
24
+ isLoading?: boolean;
25
+ thinkingSteps?: string[];
26
+ onSendMessage?: (text: string) => void;
27
+ onAttachFile?: (files: FileList) => void;
28
+ onAttachImage?: (files: FileList) => void;
29
+ onReset?: () => void;
30
+
31
+ // ── History panel (left) ──
32
+ conversations?: PolicyConversationItem[];
33
+ activeConversationId?: string;
34
+ onSelectConversation?: (id: string) => void;
35
+ onNewChat?: () => void;
36
+
37
+ // ── Context sidebar (right) ──
38
+ /**
39
+ * Whether the context panel starts visible.
40
+ * Renamed from `showContextPanel` to make uncontrolled semantics explicit —
41
+ * the toggle button owns the state after mount.
42
+ */
43
+ defaultShowContextPanel?: boolean;
44
+ bankCount?: number;
45
+ categoryCount?: number;
46
+ lastUpdated?: string;
47
+ /** Opens the Support Agent panel — wired to PageTopBar "Ask Support" button. */
48
+ onAskSupport?: () => void;
49
+
50
+ className?: string;
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Internal helpers
55
+ // ---------------------------------------------------------------------------
56
+
57
+ /** Derives suggested follow-up questions from the last assistant message's query context. */
58
+ function deriveFollowUps(context: PolicyQueryContext | undefined): string[] {
59
+ if (!context) return [];
60
+
61
+ const { queryType, policyType, bankName } = context;
62
+
63
+ if (queryType === "cross_bank_comparison") {
64
+ return [
65
+ `Which of these banks is best overall for ${context.categories[0] ?? policyType}?`,
66
+ bankName
67
+ ? `Does ${bankName} have any exceptions?`
68
+ : "Which bank has the most flexible policy?",
69
+ "Show me the ranked list of lenders.",
70
+ ];
71
+ }
72
+ if (queryType === "ranking") {
73
+ return [
74
+ `What are the full details for the #1 ranked bank?`,
75
+ `Which banks in the list accept cases under 80% LVR?`,
76
+ `Compare the top 3 banks side by side.`,
77
+ ];
78
+ }
79
+ if (queryType === "single_bank") {
80
+ return [
81
+ `How does ${bankName ?? "this bank"} compare to other lenders?`,
82
+ `What documentation does ${bankName ?? "this bank"} require?`,
83
+ `Which banks have a better policy than ${bankName ?? "this bank"}?`,
84
+ ];
85
+ }
86
+ if (queryType === "threshold_filter") {
87
+ return [
88
+ `What is the maximum LVR across all filtered banks?`,
89
+ `Are any of these banks on a specialist product?`,
90
+ `Rank the filtered banks best to worst.`,
91
+ ];
92
+ }
93
+ return [];
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // PolicyAIPage (Template)
98
+ // ---------------------------------------------------------------------------
99
+
100
+ /**
101
+ * Dedicated full-page layout for the Policy AI feature.
102
+ *
103
+ * 3-panel layout modelled on the AI Conversations page:
104
+ * - **Left** (260px): `PolicyAIHistoryPanel` — recent conversations + search
105
+ * - **Center** (flex-1): `PolicyAIPanel` in inline mode — chat + response cards
106
+ * - **Right** (280px, toggle-able): `PolicyAIContextSidebar` — coverage stats +
107
+ * last query context + suggested follow-ups
108
+ *
109
+ * @example
110
+ * <PolicyAIPage
111
+ * messages={messages}
112
+ * conversations={conversations}
113
+ * activeConversationId={activeId}
114
+ * onSendMessage={handleSend}
115
+ * onSelectConversation={setActiveId}
116
+ * onNewChat={handleNewChat}
117
+ * />
118
+ */
119
+ export function PolicyAIPage({
120
+ // Chat
121
+ messages = [],
122
+ suggestedQuestions,
123
+ isStreaming = false,
124
+ isLoading = false,
125
+ thinkingSteps,
126
+ onSendMessage,
127
+ onAttachFile,
128
+ onAttachImage,
129
+ onReset,
130
+ // History
131
+ conversations = [],
132
+ activeConversationId,
133
+ onSelectConversation,
134
+ onNewChat,
135
+ // Context
136
+ defaultShowContextPanel = true,
137
+ bankCount = 42,
138
+ categoryCount = 86,
139
+ lastUpdated = "June 2026",
140
+ onAskSupport,
141
+ className,
142
+ }: PolicyAIPageProps) {
143
+ const [showContextPanel, setShowContextPanel] = React.useState(
144
+ defaultShowContextPanel,
145
+ );
146
+ const [historySearch, setHistorySearch] = React.useState("");
147
+
148
+ // Walk backwards to find the most recent assistant message that carries a
149
+ // queryContext. This keeps the sidebar populated while a new (streaming)
150
+ // reply is pending — the in-flight message won't carry a queryContext yet.
151
+ // Single-pass: no intermediate array allocation.
152
+ const lastQueryContext = React.useMemo(() => {
153
+ for (let i = messages.length - 1; i >= 0; i--) {
154
+ const m = messages[i];
155
+ if (m.role === "assistant" && m.queryContext) return m.queryContext;
156
+ }
157
+ return undefined;
158
+ }, [messages]);
159
+
160
+ // deriveFollowUps is a cheap pure function; no useMemo needed.
161
+ const followUps = deriveFollowUps(lastQueryContext);
162
+
163
+ const handleNewChat = () => {
164
+ onReset?.();
165
+ onNewChat?.();
166
+ };
167
+
168
+ // Derive once — used for both aria-label and title on the toggle button.
169
+ const panelToggleLabel = showContextPanel
170
+ ? "Hide context panel"
171
+ : "Show context panel";
172
+
173
+ return (
174
+ <div
175
+ data-slot="policy-ai-page"
176
+ className={cn("flex flex-col h-full bg-background", className)}
177
+ >
178
+ {/* ── Page header — uses shared PageTopBar (same as Loan CRM, Contact, etc.) ── */}
179
+ <PageTopBar
180
+ title="Policy AI"
181
+ actions={
182
+ <Button
183
+ variant="ghost"
184
+ size="icon-sm"
185
+ onClick={() => setShowContextPanel((v) => !v)}
186
+ aria-label={panelToggleLabel}
187
+ aria-pressed={showContextPanel}
188
+ title={panelToggleLabel}
189
+ >
190
+ <PanelRight className="size-4" aria-hidden="true" />
191
+ </Button>
192
+ }
193
+ onAskSupport={onAskSupport}
194
+ />
195
+
196
+ {/* ── 3-panel body ── */}
197
+ <div className="flex flex-1 min-h-0 overflow-hidden">
198
+ {/* Left — conversation history */}
199
+ <PolicyAIHistoryPanel
200
+ conversations={conversations}
201
+ activeId={activeConversationId}
202
+ onSelect={onSelectConversation}
203
+ onNewChat={handleNewChat}
204
+ searchQuery={historySearch}
205
+ onSearchChange={setHistorySearch}
206
+ className="w-[260px]"
207
+ />
208
+
209
+ {/* Center — chat area (PolicyAIPanel inline).
210
+ min-h-0 lets it shrink inside the flex row; the panel owns its
211
+ own overflow-y-auto so the footer stays pinned at the bottom. */}
212
+ <div className="flex-1 flex flex-col min-w-0 min-h-0">
213
+ <PolicyAIPanel
214
+ inline
215
+ messages={messages}
216
+ suggestedQuestions={suggestedQuestions}
217
+ isStreaming={isStreaming}
218
+ isLoading={isLoading}
219
+ thinkingSteps={thinkingSteps}
220
+ onSendMessage={onSendMessage}
221
+ onAttachFile={onAttachFile}
222
+ onAttachImage={onAttachImage}
223
+ onReset={onReset}
224
+ className="flex-1 min-h-0"
225
+ />
226
+ </div>
227
+
228
+ {/* Right — context sidebar: only shown once a conversation is active */}
229
+ {showContextPanel && messages.length > 0 && (
230
+ <PolicyAIContextSidebar
231
+ lastQueryContext={lastQueryContext}
232
+ suggestedFollowUps={followUps}
233
+ onFollowUpClick={onSendMessage}
234
+ bankCount={bankCount}
235
+ categoryCount={categoryCount}
236
+ lastUpdated={lastUpdated}
237
+ className="w-[280px]"
238
+ />
239
+ )}
240
+ </div>
241
+ </div>
242
+ );
243
+ }
@@ -9,6 +9,7 @@ import {
9
9
  } from "lucide-react";
10
10
  import { cn } from "@/lib/utils";
11
11
  import { Button } from "@/components/ui/button";
12
+ import { Card } from "@/components/ui/card";
12
13
  import { ChatInputArea } from "@/components/ui/chat-input-area";
13
14
  import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
14
15
  import { ChatWidgetMessage } from "@/components/ui/chat-widget-primitives";
@@ -38,8 +39,10 @@ export interface PolicyAIFABProps {
38
39
  // ---------------------------------------------------------------------------
39
40
 
40
41
  export interface PolicyAIPanelProps {
41
- open: boolean;
42
- onClose: () => void;
42
+ /** Required in float mode; ignored in `inline` mode. */
43
+ open?: boolean;
44
+ /** Required in float mode; ignored in `inline` mode. */
45
+ onClose?: () => void;
43
46
  messages?: PolicyAIMessage[];
44
47
  suggestedQuestions?: Record<string, string[]>;
45
48
  isStreaming?: boolean;
@@ -286,7 +289,7 @@ export function PolicyAIFAB({
286
289
  * />
287
290
  */
288
291
  export function PolicyAIPanel({
289
- open,
292
+ open = true,
290
293
  onClose,
291
294
  messages = [],
292
295
  suggestedQuestions,
@@ -405,60 +408,69 @@ export function PolicyAIPanel({
405
408
  {/* ── Body ── */}
406
409
  {(!minimised || inline) && (
407
410
  <>
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")}>
411
+ {/* Scroll container — always flex-1 + overflow-y-auto so the footer
412
+ stays pinned at the bottom in both float and inline modes. */}
413
+ <div className="flex-1 overflow-y-auto min-h-0">
412
414
  {isLoading ? (
413
415
  <div className="flex flex-col justify-center h-full py-8">
414
416
  <PolicyAIThinkingSteps steps={resolvedThinkingSteps} />
415
417
  </div>
416
418
  ) : !isChatMode ? (
417
419
  /* 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.
420
+ <div className="flex flex-col gap-3 px-3 py-3">
421
+ {/* Description */}
422
+ <p className="text-sm text-muted-foreground">
423
+ Ask me about lending policies across 40+ Australian banks —
424
+ income, LVR, security types, serviceability, and more.
425
+ </p>
426
+
427
+ {/* Suggested questions — wrapped in a Card for visual separation */}
428
+ <div className="flex flex-col gap-1.5">
429
+ <p className="text-overline text-muted-foreground">
430
+ Suggested
423
431
  </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>
432
+ <Card className="gap-0 py-0 shadow-none overflow-hidden">
433
+ {/* Policy type tabs */}
434
+ <div className="border-b border-border px-3 pt-2">
435
+ <Tabs
436
+ value={activeTab}
437
+ onValueChange={(v) => setActiveTab(v as PolicyTypeTab)}
438
+ >
439
+ <TabsList
440
+ variant="line"
441
+ className="justify-start h-8 gap-0 -mb-px overflow-x-auto"
442
+ >
443
+ {(
444
+ Object.keys(DEFAULT_SUGGESTED) as PolicyTypeTab[]
445
+ ).map((type) => (
446
+ <TabsTrigger
447
+ key={type}
448
+ value={type}
449
+ className="px-2 h-7 shrink-0"
450
+ >
451
+ {type}
452
+ </TabsTrigger>
453
+ ))}
454
+ </TabsList>
455
+ </Tabs>
456
+ </div>
449
457
 
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
- ))}
458
+ {/* Question list */}
459
+ <div className="divide-y divide-border">
460
+ {(
461
+ (suggestedQuestions ?? DEFAULT_SUGGESTED)[activeTab] ??
462
+ []
463
+ ).map((q) => (
464
+ <button
465
+ key={q}
466
+ onClick={() => setInputValue(q)}
467
+ className="w-full text-left text-sm text-foreground px-3 py-2.5 hover:bg-muted/60 transition-colors"
468
+ >
469
+ {q}
470
+ </button>
471
+ ))}
472
+ </div>
473
+ </Card>
462
474
  </div>
463
475
  </div>
464
476
  ) : (
@@ -498,15 +510,9 @@ export function PolicyAIPanel({
498
510
  </div>
499
511
 
500
512
  {/* 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. */}
513
+ shrink-0 pins it to the flex column bottom in both modes. */}
503
514
  {!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
- >
515
+ <div className="shrink-0 border-t border-border px-3 py-3 bg-card">
510
516
  <ChatInputArea
511
517
  value={inputValue}
512
518
  onChange={setInputValue}
@@ -516,6 +522,7 @@ export function PolicyAIPanel({
516
522
  disabled={isStreaming}
517
523
  placeholder="Ask about lending policies…"
518
524
  autoFocus={!minimised}
525
+ showMarkdownToolbar
519
526
  />
520
527
  </div>
521
528
  )}
@@ -1,6 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { Building2, Info } from "lucide-react";
3
3
  import { cn } from "@/lib/utils";
4
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
4
5
  import { Badge } from "@/components/ui/badge";
5
6
  import { Input } from "@/components/ui/input";
6
7
  import {
@@ -86,12 +87,9 @@ export interface PolicyRankedListProps {
86
87
  /** Bank initial avatar — consistent across all response formats. */
87
88
  function BankAvatar({ name }: { name: string }) {
88
89
  return (
89
- <span
90
- aria-hidden="true"
91
- className="shrink-0 inline-flex items-center justify-center size-7 bg-muted text-muted-foreground text-xs font-semibold border border-border"
92
- >
93
- {name.charAt(0).toUpperCase()}
94
- </span>
90
+ <Avatar aria-hidden="true" className="size-7 shrink-0">
91
+ <AvatarFallback>{name.charAt(0).toUpperCase()}</AvatarFallback>
92
+ </Avatar>
95
93
  );
96
94
  }
97
95
 
@@ -140,16 +138,14 @@ function CitationBadge({ citation }: { citation: PolicyCitationItem }) {
140
138
  <PopoverContent className="w-72 p-3" sideOffset={6}>
141
139
  <div className="flex flex-col gap-2">
142
140
  <div className="flex items-center gap-1.5 flex-wrap">
143
- <span className="text-xs font-semibold text-foreground">
141
+ <span className="text-sm font-semibold text-foreground">
144
142
  {citation.bankName}
145
143
  </span>
146
144
  <Badge variant="secondary" className="text-xs px-1.5 py-0">
147
145
  {citation.category}
148
146
  </Badge>
149
147
  </div>
150
- <p className="text-xs text-muted-foreground leading-relaxed">
151
- {citation.excerpt}
152
- </p>
148
+ <p className="text-sm text-muted-foreground">{citation.excerpt}</p>
153
149
  </div>
154
150
  </PopoverContent>
155
151
  </Popover>
@@ -175,7 +171,7 @@ function AnswerWithCitations({
175
171
  const citation = citationMap.get(parseInt(match[1], 10));
176
172
  if (citation) return <CitationBadge key={i} citation={citation} />;
177
173
  return (
178
- <span key={i} className="text-xs text-muted-foreground">
174
+ <span key={i} className="text-caption text-muted-foreground">
179
175
  {part}
180
176
  </span>
181
177
  );
@@ -385,7 +381,7 @@ export function PolicyComparisonTable({
385
381
  {bank.bankName}
386
382
  </span>
387
383
  {bank.details && (
388
- <span className="text-xs text-muted-foreground leading-snug">
384
+ <span className="text-caption text-muted-foreground">
389
385
  {bank.details}
390
386
  </span>
391
387
  )}