@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.
Files changed (39) hide show
  1. package/.turbo/turbo-build.log +98 -95
  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-DSVKEVX6.mjs → chunk-CZOGJC76.mjs} +1 -1
  6. package/dist/{chunk-JPGL36WQ.mjs → chunk-FL7DEYUA.mjs} +6 -7
  7. package/dist/{chunk-2CHH5QOA.mjs → chunk-FQUT5XD6.mjs} +1 -1
  8. package/dist/chunk-MGIDYXOP.mjs +814 -0
  9. package/dist/{chunk-OG2VM34K.mjs → chunk-MHBQJVHE.mjs} +1 -1
  10. package/dist/{chunk-R7M657QL.mjs → chunk-STN5QIWN.mjs} +36 -1
  11. package/dist/components/ui/file-preview-dialog.js +6 -7
  12. package/dist/components/ui/file-preview-dialog.mjs +2 -2
  13. package/dist/components/ui/kanban-column.js +6 -7
  14. package/dist/components/ui/kanban-column.mjs +3 -3
  15. package/dist/components/ui/opportunity-card.js +6 -7
  16. package/dist/components/ui/opportunity-card.mjs +2 -2
  17. package/dist/components/ui/pipeline-board.js +6 -7
  18. package/dist/components/ui/pipeline-board.mjs +4 -4
  19. package/dist/components/ui/policy-ai/index.js +1636 -0
  20. package/dist/components/ui/policy-ai/index.mjs +36 -0
  21. package/dist/components/ui/progress.js +6 -7
  22. package/dist/components/ui/progress.mjs +1 -1
  23. package/dist/components/ui/stage-timeline.js +6 -7
  24. package/dist/components/ui/stage-timeline.mjs +2 -2
  25. package/dist/components/ui/support-agent/index.js +31 -1
  26. package/dist/components/ui/support-agent/index.mjs +1 -1
  27. package/dist/index.js +4105 -3299
  28. package/dist/index.mjs +25 -7
  29. package/dist/styles.css +1 -1
  30. package/package.json +10 -5
  31. package/src/components/index.tsx +30 -0
  32. package/src/components/ui/policy-ai/index.tsx +41 -0
  33. package/src/components/ui/policy-ai/policy-ai-panel.tsx +526 -0
  34. package/src/components/ui/policy-ai/policy-ai-primitives.tsx +332 -0
  35. package/src/components/ui/policy-ai/policy-ai-responses.tsx +543 -0
  36. package/src/components/ui/progress.tsx +15 -12
  37. package/src/components/ui/support-agent/support-agent-panel.tsx +46 -2
  38. package/src/styles/styles-css.ts +1 -1
  39. 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<typeof ProgressPrimitive.Root>
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
- <SupportTypingIndicator />
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">