@wealthx/shadcn 1.5.38 → 1.5.40
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 +98 -95
- 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-DSVKEVX6.mjs → chunk-CZOGJC76.mjs} +1 -1
- 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-R7M657QL.mjs → chunk-STN5QIWN.mjs} +36 -1
- 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/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 +31 -1
- package/dist/components/ui/support-agent/index.mjs +1 -1
- package/dist/index.js +4105 -3299
- package/dist/index.mjs +25 -7
- package/dist/styles.css +1 -1
- package/package.json +10 -5
- package/src/components/index.tsx +30 -0
- 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/support-agent-panel.tsx +46 -2
- package/src/styles/styles-css.ts +1 -1
- package/tsup.config.ts +2 -1
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Building2, Info } from "lucide-react";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
import { Badge } from "@/components/ui/badge";
|
|
5
|
+
import { Input } from "@/components/ui/input";
|
|
6
|
+
import {
|
|
7
|
+
Tooltip,
|
|
8
|
+
TooltipContent,
|
|
9
|
+
TooltipProvider,
|
|
10
|
+
TooltipTrigger,
|
|
11
|
+
} from "@/components/ui/tooltip";
|
|
12
|
+
import {
|
|
13
|
+
Popover,
|
|
14
|
+
PopoverContent,
|
|
15
|
+
PopoverTrigger,
|
|
16
|
+
} from "@/components/ui/popover";
|
|
17
|
+
import { Progress } from "@/components/ui/progress";
|
|
18
|
+
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
19
|
+
import { PolicyVerdictBadge } from "./policy-ai-primitives";
|
|
20
|
+
import type {
|
|
21
|
+
PolicyBankVerdict,
|
|
22
|
+
PolicyCitationItem,
|
|
23
|
+
PolicyQueryContext,
|
|
24
|
+
PolicyRankedBankItem,
|
|
25
|
+
PolicyVerdict,
|
|
26
|
+
} from "./policy-ai-primitives";
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// PolicySingleBankAnswer props
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export interface PolicySingleBankAnswerProps {
|
|
33
|
+
bankName: string;
|
|
34
|
+
verdict: PolicyVerdict;
|
|
35
|
+
/** Prose answer text. May contain inline [N] citation markers. */
|
|
36
|
+
answer: string;
|
|
37
|
+
citations: PolicyCitationItem[];
|
|
38
|
+
/**
|
|
39
|
+
* Optional query context — when provided, policy type and categories are
|
|
40
|
+
* rendered as an Info icon tooltip inside the bank header.
|
|
41
|
+
*/
|
|
42
|
+
queryContext?: PolicyQueryContext;
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// PolicyComparisonTable props
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
export interface PolicySummaryCounts {
|
|
51
|
+
yes: number;
|
|
52
|
+
softNo: number;
|
|
53
|
+
no: number;
|
|
54
|
+
total: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface PolicyComparisonTableProps {
|
|
58
|
+
/** Column headers — one per policy category queried. */
|
|
59
|
+
categories: string[];
|
|
60
|
+
banks: PolicyBankVerdict[];
|
|
61
|
+
summaryCounts: PolicySummaryCounts;
|
|
62
|
+
citations: PolicyCitationItem[];
|
|
63
|
+
/** Optional query context — shown as Info icon tooltip in the summary header. */
|
|
64
|
+
queryContext?: PolicyQueryContext;
|
|
65
|
+
className?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// PolicyRankedList props
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
export interface PolicyRankedListProps {
|
|
73
|
+
/** Categories used in the TOPSIS ranking (shown in header). */
|
|
74
|
+
categories: string[];
|
|
75
|
+
banks: PolicyRankedBankItem[];
|
|
76
|
+
citations: PolicyCitationItem[];
|
|
77
|
+
/** Optional query context — shown as Info icon tooltip in the ranking header. */
|
|
78
|
+
queryContext?: PolicyQueryContext;
|
|
79
|
+
className?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Internal helpers
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/** Bank initial avatar — consistent across all response formats. */
|
|
87
|
+
function BankAvatar({ name }: { name: string }) {
|
|
88
|
+
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>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Info icon with a tooltip. Pass `label` as the tooltip text. */
|
|
99
|
+
function QueryContextInfo({ label }: { label: string }) {
|
|
100
|
+
return (
|
|
101
|
+
<TooltipProvider>
|
|
102
|
+
<Tooltip>
|
|
103
|
+
<TooltipTrigger asChild>
|
|
104
|
+
<span
|
|
105
|
+
className="shrink-0 text-muted-foreground/60 hover:text-muted-foreground cursor-default transition-colors"
|
|
106
|
+
aria-label={label}
|
|
107
|
+
>
|
|
108
|
+
<Info className="size-3.5" aria-hidden="true" />
|
|
109
|
+
</span>
|
|
110
|
+
</TooltipTrigger>
|
|
111
|
+
<TooltipContent side="top">{label}</TooltipContent>
|
|
112
|
+
</Tooltip>
|
|
113
|
+
</TooltipProvider>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Format a PolicyQueryContext into a human-readable tooltip label. */
|
|
118
|
+
function queryContextLabel(context: PolicyQueryContext): string {
|
|
119
|
+
return [
|
|
120
|
+
context.policyType,
|
|
121
|
+
...context.categories,
|
|
122
|
+
context.queryType.replace(/_/g, " "),
|
|
123
|
+
].join(" · ");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Inline [N] badge that opens a Popover with the source citation details. */
|
|
127
|
+
function CitationBadge({ citation }: { citation: PolicyCitationItem }) {
|
|
128
|
+
return (
|
|
129
|
+
<Popover>
|
|
130
|
+
<PopoverTrigger asChild>
|
|
131
|
+
<button type="button" aria-label={`View source ${citation.index}`}>
|
|
132
|
+
<Badge
|
|
133
|
+
variant="secondary"
|
|
134
|
+
className="cursor-pointer px-1.5 py-0 text-xs mx-0.5 align-baseline hover:bg-secondary/80 transition-colors"
|
|
135
|
+
>
|
|
136
|
+
{citation.index}
|
|
137
|
+
</Badge>
|
|
138
|
+
</button>
|
|
139
|
+
</PopoverTrigger>
|
|
140
|
+
<PopoverContent className="w-72 p-3" sideOffset={6}>
|
|
141
|
+
<div className="flex flex-col gap-2">
|
|
142
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
143
|
+
<span className="text-xs font-semibold text-foreground">
|
|
144
|
+
{citation.bankName}
|
|
145
|
+
</span>
|
|
146
|
+
<Badge variant="secondary" className="text-xs px-1.5 py-0">
|
|
147
|
+
{citation.category}
|
|
148
|
+
</Badge>
|
|
149
|
+
</div>
|
|
150
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
151
|
+
{citation.excerpt}
|
|
152
|
+
</p>
|
|
153
|
+
</div>
|
|
154
|
+
</PopoverContent>
|
|
155
|
+
</Popover>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Renders answer prose with [N] markers replaced by inline CitationBadge. */
|
|
160
|
+
function AnswerWithCitations({
|
|
161
|
+
answer,
|
|
162
|
+
citations,
|
|
163
|
+
}: {
|
|
164
|
+
answer: string;
|
|
165
|
+
citations: PolicyCitationItem[];
|
|
166
|
+
}) {
|
|
167
|
+
const citationMap = new Map(citations.map((c) => [c.index, c]));
|
|
168
|
+
const parts = answer.split(/(\[\d+\])/g);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<p className="text-sm text-foreground leading-relaxed whitespace-pre-line">
|
|
172
|
+
{parts.map((part, i) => {
|
|
173
|
+
const match = part.match(/^\[(\d+)\]$/);
|
|
174
|
+
if (match) {
|
|
175
|
+
const citation = citationMap.get(parseInt(match[1], 10));
|
|
176
|
+
if (citation) return <CitationBadge key={i} citation={citation} />;
|
|
177
|
+
return (
|
|
178
|
+
<span key={i} className="text-xs text-muted-foreground">
|
|
179
|
+
{part}
|
|
180
|
+
</span>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return <React.Fragment key={i}>{part}</React.Fragment>;
|
|
184
|
+
})}
|
|
185
|
+
</p>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// PolicySingleBankAnswer (Molecule) — Type A response
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Displays a single-bank policy answer (Type A RAG retrieval).
|
|
195
|
+
* Shows the bank name, verdict badge, AI-generated prose with inline citation
|
|
196
|
+
* markers, and a collapsible citation panel listing source excerpts.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* <PolicySingleBankAnswer
|
|
200
|
+
* bankName="AMP"
|
|
201
|
+
* verdict="yes"
|
|
202
|
+
* answer="AMP accepts casual income after 6 months [1]."
|
|
203
|
+
* citations={citations}
|
|
204
|
+
* />
|
|
205
|
+
*/
|
|
206
|
+
export function PolicySingleBankAnswer({
|
|
207
|
+
bankName,
|
|
208
|
+
verdict,
|
|
209
|
+
answer,
|
|
210
|
+
citations,
|
|
211
|
+
queryContext,
|
|
212
|
+
className,
|
|
213
|
+
}: PolicySingleBankAnswerProps) {
|
|
214
|
+
return (
|
|
215
|
+
<div
|
|
216
|
+
data-slot="policy-single-bank-answer"
|
|
217
|
+
className={cn("border border-border bg-card flex flex-col", className)}
|
|
218
|
+
>
|
|
219
|
+
{/* Bank header */}
|
|
220
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-border">
|
|
221
|
+
<BankAvatar name={bankName} />
|
|
222
|
+
<div className="flex-1 flex items-center gap-1.5 min-w-0">
|
|
223
|
+
<span className="font-semibold text-sm text-foreground truncate">
|
|
224
|
+
{bankName}
|
|
225
|
+
</span>
|
|
226
|
+
{queryContext && (
|
|
227
|
+
<QueryContextInfo label={queryContextLabel(queryContext)} />
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
<PolicyVerdictBadge verdict={verdict} />
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
{/* Answer prose */}
|
|
234
|
+
<div className="px-4 py-3">
|
|
235
|
+
<AnswerWithCitations answer={answer} citations={citations} />
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// PolicyComparisonTable (Molecule) — Type B/C cross-bank response
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
type VerdictFilter = "all" | PolicyVerdict;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Displays a cross-bank policy comparison matrix (Type B/C retrieval).
|
|
249
|
+
* Shows a summary count row, search input, verdict filter tabs, a table
|
|
250
|
+
* with one row per bank and one column per policy category, and a
|
|
251
|
+
* collapsible citation panel at the bottom.
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* <PolicyComparisonTable
|
|
255
|
+
* categories={["Casual Income"]}
|
|
256
|
+
* banks={banks}
|
|
257
|
+
* summaryCounts={{ yes: 11, softNo: 5, no: 4, total: 20 }}
|
|
258
|
+
* citations={citations}
|
|
259
|
+
* />
|
|
260
|
+
*/
|
|
261
|
+
export function PolicyComparisonTable({
|
|
262
|
+
categories,
|
|
263
|
+
banks,
|
|
264
|
+
summaryCounts,
|
|
265
|
+
citations,
|
|
266
|
+
queryContext,
|
|
267
|
+
className,
|
|
268
|
+
}: PolicyComparisonTableProps) {
|
|
269
|
+
const [search, setSearch] = React.useState("");
|
|
270
|
+
const [filter, setFilter] = React.useState<VerdictFilter>("all");
|
|
271
|
+
|
|
272
|
+
const filtered = React.useMemo(() => {
|
|
273
|
+
let result = banks;
|
|
274
|
+
if (search.trim()) {
|
|
275
|
+
const q = search.toLowerCase();
|
|
276
|
+
result = result.filter((b) => b.bankName.toLowerCase().includes(q));
|
|
277
|
+
}
|
|
278
|
+
if (filter !== "all") {
|
|
279
|
+
result = result.filter((b) => b.verdict === filter);
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}, [banks, search, filter]);
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<div
|
|
286
|
+
data-slot="policy-comparison-table"
|
|
287
|
+
className={cn("border border-border bg-card flex flex-col", className)}
|
|
288
|
+
>
|
|
289
|
+
{/* Summary counts */}
|
|
290
|
+
<div className="flex items-center gap-3 px-4 py-2.5 border-b border-border bg-muted/30">
|
|
291
|
+
<Building2
|
|
292
|
+
className="size-3.5 text-muted-foreground shrink-0"
|
|
293
|
+
aria-hidden="true"
|
|
294
|
+
/>
|
|
295
|
+
<div className="flex items-center gap-2 text-sm flex-wrap flex-1 min-w-0">
|
|
296
|
+
<span className="font-semibold text-foreground">
|
|
297
|
+
{summaryCounts.total} banks
|
|
298
|
+
</span>
|
|
299
|
+
<span className="text-muted-foreground">·</span>
|
|
300
|
+
<Badge variant="success" className="text-xs px-1.5 py-0">
|
|
301
|
+
{summaryCounts.yes} Yes
|
|
302
|
+
</Badge>
|
|
303
|
+
<Badge variant="warning" className="text-xs px-1.5 py-0">
|
|
304
|
+
{summaryCounts.softNo} Soft No
|
|
305
|
+
</Badge>
|
|
306
|
+
<Badge variant="destructive" className="text-xs px-1.5 py-0">
|
|
307
|
+
{summaryCounts.no} No
|
|
308
|
+
</Badge>
|
|
309
|
+
</div>
|
|
310
|
+
{queryContext && (
|
|
311
|
+
<QueryContextInfo label={queryContextLabel(queryContext)} />
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
{/* Search input */}
|
|
316
|
+
<div className="px-4 py-2.5 border-b border-border">
|
|
317
|
+
<Input
|
|
318
|
+
aria-label="Search banks"
|
|
319
|
+
placeholder="Search banks…"
|
|
320
|
+
value={search}
|
|
321
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
322
|
+
className="h-8 text-sm"
|
|
323
|
+
/>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
{/* Verdict filter tabs */}
|
|
327
|
+
<div className="border-b border-border px-4 pt-1">
|
|
328
|
+
<Tabs
|
|
329
|
+
value={filter}
|
|
330
|
+
onValueChange={(v) => setFilter(v as VerdictFilter)}
|
|
331
|
+
>
|
|
332
|
+
<TabsList variant="line" className="justify-start h-8 gap-0 -mb-px">
|
|
333
|
+
<TabsTrigger value="all" className="text-sm px-3 h-7">
|
|
334
|
+
All
|
|
335
|
+
</TabsTrigger>
|
|
336
|
+
<TabsTrigger value="yes" className="text-sm px-3 h-7">
|
|
337
|
+
Yes
|
|
338
|
+
</TabsTrigger>
|
|
339
|
+
<TabsTrigger value="soft_no" className="text-sm px-3 h-7">
|
|
340
|
+
Soft No
|
|
341
|
+
</TabsTrigger>
|
|
342
|
+
<TabsTrigger value="no" className="text-sm px-3 h-7">
|
|
343
|
+
No
|
|
344
|
+
</TabsTrigger>
|
|
345
|
+
</TabsList>
|
|
346
|
+
</Tabs>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
{/* Bank table */}
|
|
350
|
+
<div className="overflow-x-auto">
|
|
351
|
+
<table className="w-full text-sm border-collapse">
|
|
352
|
+
<thead>
|
|
353
|
+
<tr className="border-b border-border bg-muted/20">
|
|
354
|
+
<th className="text-left text-sm font-medium text-muted-foreground px-4 py-2">
|
|
355
|
+
Bank
|
|
356
|
+
</th>
|
|
357
|
+
{categories.map((cat) => (
|
|
358
|
+
<th
|
|
359
|
+
key={cat}
|
|
360
|
+
className="text-left text-sm font-medium text-muted-foreground px-3 py-2 w-px whitespace-nowrap"
|
|
361
|
+
>
|
|
362
|
+
{cat}
|
|
363
|
+
</th>
|
|
364
|
+
))}
|
|
365
|
+
</tr>
|
|
366
|
+
</thead>
|
|
367
|
+
<tbody>
|
|
368
|
+
{filtered.length === 0 ? (
|
|
369
|
+
<tr>
|
|
370
|
+
<td
|
|
371
|
+
colSpan={categories.length + 1}
|
|
372
|
+
className="px-4 py-6 text-center text-sm text-muted-foreground"
|
|
373
|
+
>
|
|
374
|
+
No banks match your search.
|
|
375
|
+
</td>
|
|
376
|
+
</tr>
|
|
377
|
+
) : (
|
|
378
|
+
filtered.map((bank) => (
|
|
379
|
+
<tr key={bank.bankName} className="border-b border-border">
|
|
380
|
+
<td className="px-4 py-2.5">
|
|
381
|
+
<div className="flex items-center gap-2">
|
|
382
|
+
<BankAvatar name={bank.bankName} />
|
|
383
|
+
<div className="flex flex-col gap-0.5 min-w-0">
|
|
384
|
+
<span className="text-sm font-medium text-foreground leading-snug">
|
|
385
|
+
{bank.bankName}
|
|
386
|
+
</span>
|
|
387
|
+
{bank.details && (
|
|
388
|
+
<span className="text-xs text-muted-foreground leading-snug">
|
|
389
|
+
{bank.details}
|
|
390
|
+
</span>
|
|
391
|
+
)}
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
</td>
|
|
395
|
+
{categories.map((cat) => (
|
|
396
|
+
<td
|
|
397
|
+
key={cat}
|
|
398
|
+
className="px-3 py-2.5 align-top w-px whitespace-nowrap"
|
|
399
|
+
>
|
|
400
|
+
<PolicyVerdictBadge verdict={bank.verdict} />
|
|
401
|
+
</td>
|
|
402
|
+
))}
|
|
403
|
+
</tr>
|
|
404
|
+
))
|
|
405
|
+
)}
|
|
406
|
+
</tbody>
|
|
407
|
+
</table>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
// Internal: verdict → progress bar colour map (track + indicator)
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
|
|
417
|
+
const VERDICT_SCORE_COLORS: Record<
|
|
418
|
+
string,
|
|
419
|
+
{ track: string; indicator: string }
|
|
420
|
+
> = {
|
|
421
|
+
yes: { track: "bg-success/20", indicator: "bg-success" },
|
|
422
|
+
soft_no: { track: "bg-warning/20", indicator: "bg-warning" },
|
|
423
|
+
no: { track: "bg-destructive/20", indicator: "bg-destructive" },
|
|
424
|
+
unknown: { track: "bg-muted", indicator: "bg-muted-foreground" },
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
// PolicyRankedList (Molecule) — Type C ranking response
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Displays a TOPSIS-ranked list of banks (Type C retrieval).
|
|
433
|
+
* Each bank row shows rank number, name, a colour-coded score progress bar,
|
|
434
|
+
* percentage, verdict badge, and key policy highlights as bullet points.
|
|
435
|
+
* A collapsible citation panel appears at the bottom.
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* <PolicyRankedList
|
|
439
|
+
* categories={["Bonus Income", "Add Backs"]}
|
|
440
|
+
* banks={rankedBanks}
|
|
441
|
+
* citations={citations}
|
|
442
|
+
* />
|
|
443
|
+
*/
|
|
444
|
+
export function PolicyRankedList({
|
|
445
|
+
categories,
|
|
446
|
+
banks,
|
|
447
|
+
citations,
|
|
448
|
+
queryContext,
|
|
449
|
+
className,
|
|
450
|
+
}: PolicyRankedListProps) {
|
|
451
|
+
return (
|
|
452
|
+
<div
|
|
453
|
+
data-slot="policy-ranked-list"
|
|
454
|
+
className={cn("border border-border bg-card flex flex-col", className)}
|
|
455
|
+
>
|
|
456
|
+
{/* Header */}
|
|
457
|
+
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-border bg-muted/30">
|
|
458
|
+
<span className="text-sm text-muted-foreground shrink-0">Ranked:</span>
|
|
459
|
+
<div className="flex items-center gap-1 flex-wrap flex-1 min-w-0">
|
|
460
|
+
{categories.map((cat, i) => (
|
|
461
|
+
<React.Fragment key={cat}>
|
|
462
|
+
{i > 0 && (
|
|
463
|
+
<span className="text-sm text-muted-foreground">+</span>
|
|
464
|
+
)}
|
|
465
|
+
<Badge variant="secondary" className="text-xs px-1.5 py-0">
|
|
466
|
+
{cat}
|
|
467
|
+
</Badge>
|
|
468
|
+
</React.Fragment>
|
|
469
|
+
))}
|
|
470
|
+
</div>
|
|
471
|
+
<span className="text-sm text-muted-foreground shrink-0">
|
|
472
|
+
{banks.length} banks
|
|
473
|
+
</span>
|
|
474
|
+
{queryContext && (
|
|
475
|
+
<QueryContextInfo label={queryContextLabel(queryContext)} />
|
|
476
|
+
)}
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
{/* Ranked rows */}
|
|
480
|
+
<div className="divide-y divide-border">
|
|
481
|
+
{banks.map((bank) => {
|
|
482
|
+
const scorePercent = Math.round(bank.score * 100);
|
|
483
|
+
|
|
484
|
+
return (
|
|
485
|
+
<div key={bank.bankName} className="flex flex-col">
|
|
486
|
+
<div className="flex items-center gap-3 px-4 py-3">
|
|
487
|
+
<span className="shrink-0 w-6 text-center text-sm font-bold text-muted-foreground">
|
|
488
|
+
#{bank.rank}
|
|
489
|
+
</span>
|
|
490
|
+
|
|
491
|
+
<div className="flex-1 min-w-0 flex flex-col gap-1.5">
|
|
492
|
+
<div className="flex items-center justify-between gap-2">
|
|
493
|
+
<span className="text-sm font-medium text-foreground truncate">
|
|
494
|
+
{bank.bankName}
|
|
495
|
+
</span>
|
|
496
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
497
|
+
<span className="text-sm text-muted-foreground font-mono">
|
|
498
|
+
{scorePercent}%
|
|
499
|
+
</span>
|
|
500
|
+
<PolicyVerdictBadge verdict={bank.verdict} />
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
<Progress
|
|
504
|
+
value={scorePercent}
|
|
505
|
+
className={cn(
|
|
506
|
+
"h-1.5",
|
|
507
|
+
(
|
|
508
|
+
VERDICT_SCORE_COLORS[bank.verdict] ??
|
|
509
|
+
VERDICT_SCORE_COLORS.unknown
|
|
510
|
+
).track,
|
|
511
|
+
)}
|
|
512
|
+
indicatorClassName={
|
|
513
|
+
(
|
|
514
|
+
VERDICT_SCORE_COLORS[bank.verdict] ??
|
|
515
|
+
VERDICT_SCORE_COLORS.unknown
|
|
516
|
+
).indicator
|
|
517
|
+
}
|
|
518
|
+
aria-label={`${bank.bankName} policy score: ${scorePercent}%`}
|
|
519
|
+
/>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
{bank.highlights.length > 0 && (
|
|
524
|
+
<div className="px-4 pb-3 pl-[52px]">
|
|
525
|
+
<ul className="flex flex-col gap-0.5">
|
|
526
|
+
{bank.highlights.map((h, i) => (
|
|
527
|
+
<li
|
|
528
|
+
key={i}
|
|
529
|
+
className="text-sm text-muted-foreground leading-relaxed list-disc list-inside"
|
|
530
|
+
>
|
|
531
|
+
{h}
|
|
532
|
+
</li>
|
|
533
|
+
))}
|
|
534
|
+
</ul>
|
|
535
|
+
</div>
|
|
536
|
+
)}
|
|
537
|
+
</div>
|
|
538
|
+
);
|
|
539
|
+
})}
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
);
|
|
543
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type ReactElement } from "react"
|
|
2
|
-
import * as React from "react"
|
|
3
|
-
import { Progress as ProgressPrimitive } from "@base-ui/react/progress"
|
|
4
|
-
import { cn } from "@/lib/utils"
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { Progress as ProgressPrimitive } from "@base-ui/react/progress";
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Progress — WealthX Design System
|
|
@@ -10,31 +10,34 @@ import { cn } from "@/lib/utils"
|
|
|
10
10
|
* Base: official shadcn progress (npx shadcn\@latest add progress)
|
|
11
11
|
* WealthX overrides: sharp corners (no rounded-full), track bg-muted instead of bg-primary/20
|
|
12
12
|
*/
|
|
13
|
-
export type ProgressProps = React.ComponentProps<
|
|
13
|
+
export type ProgressProps = React.ComponentProps<
|
|
14
|
+
typeof ProgressPrimitive.Root
|
|
15
|
+
> & {
|
|
16
|
+
/** Additional classes applied to the indicator (filled bar). Use to override the default bg-primary colour. */
|
|
17
|
+
indicatorClassName?: string;
|
|
18
|
+
};
|
|
14
19
|
|
|
15
20
|
function Progress({
|
|
16
21
|
className,
|
|
17
22
|
value,
|
|
23
|
+
indicatorClassName,
|
|
18
24
|
...props
|
|
19
25
|
}: ProgressProps): ReactElement {
|
|
20
26
|
return (
|
|
21
27
|
<ProgressPrimitive.Root
|
|
22
|
-
className={cn(
|
|
23
|
-
"relative h-2 w-full overflow-hidden bg-muted",
|
|
24
|
-
className
|
|
25
|
-
)}
|
|
28
|
+
className={cn("relative h-2 w-full overflow-hidden bg-muted", className)}
|
|
26
29
|
data-slot="progress"
|
|
27
30
|
value={value}
|
|
28
31
|
{...props}
|
|
29
32
|
>
|
|
30
33
|
<ProgressPrimitive.Track className="h-full">
|
|
31
34
|
<ProgressPrimitive.Indicator
|
|
32
|
-
className="h-full bg-primary transition-all"
|
|
35
|
+
className={cn("h-full bg-primary transition-all", indicatorClassName)}
|
|
33
36
|
data-slot="progress-indicator"
|
|
34
37
|
/>
|
|
35
38
|
</ProgressPrimitive.Track>
|
|
36
39
|
</ProgressPrimitive.Root>
|
|
37
|
-
)
|
|
40
|
+
);
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
export { Progress }
|
|
43
|
+
export { Progress };
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import ReactMarkdown from "react-markdown";
|
|
3
|
+
import rehypeRaw from "rehype-raw";
|
|
4
|
+
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
|
|
2
5
|
import { Bot, ChevronLeft, ChevronRight, SquarePen, X } from "lucide-react";
|
|
3
6
|
import { cn } from "@/lib/utils";
|
|
4
7
|
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
|
@@ -66,6 +69,12 @@ export interface SupportAgentMessage {
|
|
|
66
69
|
richContent?: SupportAgentRichContent;
|
|
67
70
|
/** True while the assistant response is streaming in. */
|
|
68
71
|
isStreaming?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Status text shown beside the animated dots while the response streams
|
|
74
|
+
* (e.g. "Checking the help center"). Rendered only while the bubble is empty
|
|
75
|
+
* and streaming. The trailing "…" animates on its own — do not include dots here.
|
|
76
|
+
*/
|
|
77
|
+
streamingLabel?: string;
|
|
69
78
|
/** True if the message errored. */
|
|
70
79
|
isErrored?: boolean;
|
|
71
80
|
}
|
|
@@ -153,6 +162,28 @@ function SupportTypingIndicator() {
|
|
|
153
162
|
);
|
|
154
163
|
}
|
|
155
164
|
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// StreamingStatus — optional status label with an animated "…" ellipsis
|
|
167
|
+
// (e.g. "Checking the help center" + animated dots), shown while streaming.
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
function StreamingStatus({ label }: { label?: string }) {
|
|
171
|
+
// No label → the standard DS typing indicator (3 bouncing dots).
|
|
172
|
+
if (!label) return <SupportTypingIndicator />;
|
|
173
|
+
|
|
174
|
+
// With label → status text + the same DS typing dots: "Checking the help center • • •"
|
|
175
|
+
return (
|
|
176
|
+
<span
|
|
177
|
+
className="flex items-center gap-1.5 text-sm text-muted-foreground"
|
|
178
|
+
role="status"
|
|
179
|
+
aria-label={`${label}…`}
|
|
180
|
+
>
|
|
181
|
+
<span>{label}</span>
|
|
182
|
+
<SupportTypingIndicator />
|
|
183
|
+
</span>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
156
187
|
// ---------------------------------------------------------------------------
|
|
157
188
|
// MessageBubble — renders a single message with optional rich content
|
|
158
189
|
// ---------------------------------------------------------------------------
|
|
@@ -185,11 +216,24 @@ function MessageBubble({ message }: MessageBubbleProps) {
|
|
|
185
216
|
data-role={message.role}
|
|
186
217
|
>
|
|
187
218
|
{isEmpty && message.isStreaming ? (
|
|
188
|
-
<
|
|
189
|
-
) : (
|
|
219
|
+
<StreamingStatus label={message.streamingLabel} />
|
|
220
|
+
) : isUser ? (
|
|
190
221
|
<span className="whitespace-pre-wrap break-words leading-relaxed">
|
|
191
222
|
{message.content}
|
|
192
223
|
</span>
|
|
224
|
+
) : (
|
|
225
|
+
<div className="break-words text-sm leading-relaxed [&_a]:text-primary [&_a]:underline [&_p]:m-0 [&_p:not(:last-child)]:mb-2 [&_ul]:my-1 [&_ul]:list-disc [&_ul]:pl-4 [&_ol]:my-1 [&_ol]:list-decimal [&_ol]:pl-4 [&_li]:mb-0.5">
|
|
226
|
+
<ReactMarkdown
|
|
227
|
+
rehypePlugins={[rehypeRaw, [rehypeSanitize, defaultSchema]]}
|
|
228
|
+
components={{
|
|
229
|
+
a: ({ node, ...props }) => (
|
|
230
|
+
<a {...props} target="_blank" rel="noopener noreferrer" />
|
|
231
|
+
),
|
|
232
|
+
}}
|
|
233
|
+
>
|
|
234
|
+
{message.content}
|
|
235
|
+
</ReactMarkdown>
|
|
236
|
+
</div>
|
|
193
237
|
)}
|
|
194
238
|
{message.isErrored && (
|
|
195
239
|
<p className="mt-1 text-xs opacity-70">
|