@codrstudio/openclaude-chat 0.1.0 → 0.2.0

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 (62) hide show
  1. package/dist/components/StreamingIndicator.js +5 -5
  2. package/dist/display/DisplayReactRenderer.js +12 -12
  3. package/dist/display/react-sandbox/bootstrap.js +150 -150
  4. package/dist/styles.css +1 -2
  5. package/package.json +64 -61
  6. package/src/components/Chat.tsx +107 -107
  7. package/src/components/ErrorNote.tsx +35 -35
  8. package/src/components/LazyRender.tsx +42 -42
  9. package/src/components/Markdown.tsx +114 -114
  10. package/src/components/MessageBubble.tsx +107 -107
  11. package/src/components/MessageInput.tsx +421 -421
  12. package/src/components/MessageList.tsx +153 -153
  13. package/src/components/StreamingIndicator.tsx +19 -19
  14. package/src/display/AlertRenderer.tsx +23 -23
  15. package/src/display/CarouselRenderer.tsx +141 -141
  16. package/src/display/ChartRenderer.tsx +195 -195
  17. package/src/display/ChoiceButtonsRenderer.tsx +114 -114
  18. package/src/display/CodeBlockRenderer.tsx +49 -49
  19. package/src/display/ComparisonTableRenderer.tsx +132 -132
  20. package/src/display/DataTableRenderer.tsx +144 -144
  21. package/src/display/DisplayReactRenderer.tsx +269 -269
  22. package/src/display/FileCardRenderer.tsx +55 -55
  23. package/src/display/GalleryRenderer.tsx +65 -65
  24. package/src/display/ImageViewerRenderer.tsx +114 -114
  25. package/src/display/LinkPreviewRenderer.tsx +74 -74
  26. package/src/display/MapViewRenderer.tsx +75 -75
  27. package/src/display/MetricCardRenderer.tsx +29 -29
  28. package/src/display/PriceHighlightRenderer.tsx +62 -62
  29. package/src/display/ProductCardRenderer.tsx +112 -112
  30. package/src/display/ProgressStepsRenderer.tsx +59 -59
  31. package/src/display/SourcesListRenderer.tsx +47 -47
  32. package/src/display/SpreadsheetRenderer.tsx +86 -86
  33. package/src/display/StepTimelineRenderer.tsx +75 -75
  34. package/src/display/index.ts +21 -21
  35. package/src/display/react-sandbox/bootstrap.ts +155 -155
  36. package/src/display/registry.ts +84 -84
  37. package/src/display/sdk-types.ts +217 -217
  38. package/src/hooks/ChatProvider.tsx +21 -21
  39. package/src/hooks/useIsMobile.ts +15 -15
  40. package/src/hooks/useOpenClaudeChat.ts +476 -476
  41. package/src/index.ts +76 -76
  42. package/src/lib/utils.ts +6 -6
  43. package/src/parts/PartErrorBoundary.tsx +51 -51
  44. package/src/parts/PartRenderer.tsx +145 -145
  45. package/src/parts/ReasoningBlock.tsx +41 -41
  46. package/src/parts/ToolActivity.tsx +78 -78
  47. package/src/parts/ToolResult.tsx +79 -79
  48. package/src/styles.css +2 -2
  49. package/src/types.ts +41 -41
  50. package/src/ui/alert.tsx +77 -77
  51. package/src/ui/badge.tsx +36 -36
  52. package/src/ui/button.tsx +54 -54
  53. package/src/ui/card.tsx +68 -68
  54. package/src/ui/collapsible.tsx +7 -7
  55. package/src/ui/dialog.tsx +122 -122
  56. package/src/ui/dropdown-menu.tsx +76 -76
  57. package/src/ui/input.tsx +24 -24
  58. package/src/ui/progress.tsx +36 -36
  59. package/src/ui/scroll-area.tsx +48 -48
  60. package/src/ui/separator.tsx +31 -31
  61. package/src/ui/skeleton.tsx +9 -9
  62. package/src/ui/table.tsx +114 -114
@@ -1,29 +1,29 @@
1
- import type { DisplayMetric } from "./sdk-types.js";
2
- import { ArrowDown, ArrowRight, ArrowUp } from "lucide-react";
3
- import { Card } from "../ui/card.js";
4
-
5
- const TREND_CONFIG = {
6
- up: { Icon: ArrowUp, colorClass: "text-primary" },
7
- down: { Icon: ArrowDown, colorClass: "text-destructive" },
8
- neutral: { Icon: ArrowRight, colorClass: "text-muted-foreground" },
9
- } as const;
10
-
11
- export function MetricCardRenderer({ label, value, unit, trend }: DisplayMetric) {
12
- const trendConfig = trend ? TREND_CONFIG[trend.direction] : null;
13
-
14
- return (
15
- <Card className="p-4 w-fit">
16
- <p className="text-sm text-muted-foreground">{label}</p>
17
- <div className="flex items-baseline gap-2 mt-1">
18
- <span className="text-2xl font-bold text-foreground">{value}</span>
19
- {unit && <span className="text-sm text-muted-foreground">{unit}</span>}
20
- </div>
21
- {trend && trendConfig && (
22
- <div className={`flex items-center gap-1 mt-1 text-sm ${trendConfig.colorClass}`}>
23
- <trendConfig.Icon size={14} aria-hidden="true" />
24
- <span>{trend.value}</span>
25
- </div>
26
- )}
27
- </Card>
28
- );
29
- }
1
+ import type { DisplayMetric } from "./sdk-types.js";
2
+ import { ArrowDown, ArrowRight, ArrowUp } from "lucide-react";
3
+ import { Card } from "../ui/card.js";
4
+
5
+ const TREND_CONFIG = {
6
+ up: { Icon: ArrowUp, colorClass: "text-primary" },
7
+ down: { Icon: ArrowDown, colorClass: "text-destructive" },
8
+ neutral: { Icon: ArrowRight, colorClass: "text-muted-foreground" },
9
+ } as const;
10
+
11
+ export function MetricCardRenderer({ label, value, unit, trend }: DisplayMetric) {
12
+ const trendConfig = trend ? TREND_CONFIG[trend.direction] : null;
13
+
14
+ return (
15
+ <Card className="p-4 w-fit">
16
+ <p className="text-sm text-muted-foreground">{label}</p>
17
+ <div className="flex items-baseline gap-2 mt-1">
18
+ <span className="text-2xl font-bold text-foreground">{value}</span>
19
+ {unit && <span className="text-sm text-muted-foreground">{unit}</span>}
20
+ </div>
21
+ {trend && trendConfig && (
22
+ <div className={`flex items-center gap-1 mt-1 text-sm ${trendConfig.colorClass}`}>
23
+ <trendConfig.Icon size={14} aria-hidden="true" />
24
+ <span>{trend.value}</span>
25
+ </div>
26
+ )}
27
+ </Card>
28
+ );
29
+ }
@@ -1,62 +1,62 @@
1
- import type { DisplayPrice } from "./sdk-types.js";
2
- import { ExternalLink } from "lucide-react";
3
- import { Badge } from "../ui/badge";
4
- import { Card } from "../ui/card";
5
-
6
- function formatPrice(value: number, currency?: string): string {
7
- // Fallbacks defensivos: se o modelo nao enviar currency ou enviar um valor
8
- // invalido (ex: string vazia), caimos num formato numerico simples em vez de
9
- // crashar a arvore React com "Currency code is required".
10
- try {
11
- return new Intl.NumberFormat("pt-BR", {
12
- style: "currency",
13
- currency: currency || "BRL",
14
- }).format(value);
15
- } catch {
16
- return new Intl.NumberFormat("pt-BR").format(value);
17
- }
18
- }
19
-
20
- export function PriceHighlightRenderer({ value, label, context, source, badge }: DisplayPrice) {
21
- // Tambem aceita value como numero flat (modelo pode nao seguir schema exato).
22
- const amount =
23
- typeof value === "number"
24
- ? value
25
- : typeof value === "object" && value !== null && "value" in value
26
- ? (value as { value: number }).value
27
- : 0;
28
- const currency =
29
- typeof value === "object" && value !== null && "currency" in value
30
- ? (value as { currency?: string }).currency
31
- : undefined;
32
- return (
33
- <Card className="p-4 space-y-1 w-fit">
34
- <p className="text-sm text-muted-foreground">{label}</p>
35
- <div className="flex items-baseline gap-2">
36
- <span className="text-2xl font-bold text-foreground">
37
- {formatPrice(amount, currency)}
38
- </span>
39
- {badge && (
40
- <Badge variant="destructive">
41
- {badge.label}
42
- </Badge>
43
- )}
44
- </div>
45
- {context && <p className="text-sm text-muted-foreground">{context}</p>}
46
- {source && (
47
- <a
48
- href={source.url}
49
- target="_blank"
50
- rel="noopener noreferrer"
51
- className="flex items-center gap-1.5 text-xs text-primary hover:underline"
52
- >
53
- {source.favicon && (
54
- <img src={source.favicon} alt="" width={14} height={14} aria-hidden="true" />
55
- )}
56
- <span>{source.name}</span>
57
- <ExternalLink size={12} aria-hidden="true" />
58
- </a>
59
- )}
60
- </Card>
61
- );
62
- }
1
+ import type { DisplayPrice } from "./sdk-types.js";
2
+ import { ExternalLink } from "lucide-react";
3
+ import { Badge } from "../ui/badge";
4
+ import { Card } from "../ui/card";
5
+
6
+ function formatPrice(value: number, currency?: string): string {
7
+ // Fallbacks defensivos: se o modelo nao enviar currency ou enviar um valor
8
+ // invalido (ex: string vazia), caimos num formato numerico simples em vez de
9
+ // crashar a arvore React com "Currency code is required".
10
+ try {
11
+ return new Intl.NumberFormat("pt-BR", {
12
+ style: "currency",
13
+ currency: currency || "BRL",
14
+ }).format(value);
15
+ } catch {
16
+ return new Intl.NumberFormat("pt-BR").format(value);
17
+ }
18
+ }
19
+
20
+ export function PriceHighlightRenderer({ value, label, context, source, badge }: DisplayPrice) {
21
+ // Tambem aceita value como numero flat (modelo pode nao seguir schema exato).
22
+ const amount =
23
+ typeof value === "number"
24
+ ? value
25
+ : typeof value === "object" && value !== null && "value" in value
26
+ ? (value as { value: number }).value
27
+ : 0;
28
+ const currency =
29
+ typeof value === "object" && value !== null && "currency" in value
30
+ ? (value as { currency?: string }).currency
31
+ : undefined;
32
+ return (
33
+ <Card className="p-4 space-y-1 w-fit">
34
+ <p className="text-sm text-muted-foreground">{label}</p>
35
+ <div className="flex items-baseline gap-2">
36
+ <span className="text-2xl font-bold text-foreground">
37
+ {formatPrice(amount, currency)}
38
+ </span>
39
+ {badge && (
40
+ <Badge variant="destructive">
41
+ {badge.label}
42
+ </Badge>
43
+ )}
44
+ </div>
45
+ {context && <p className="text-sm text-muted-foreground">{context}</p>}
46
+ {source && (
47
+ <a
48
+ href={source.url}
49
+ target="_blank"
50
+ rel="noopener noreferrer"
51
+ className="flex items-center gap-1.5 text-xs text-primary hover:underline"
52
+ >
53
+ {source.favicon && (
54
+ <img src={source.favicon} alt="" width={14} height={14} aria-hidden="true" />
55
+ )}
56
+ <span>{source.name}</span>
57
+ <ExternalLink size={12} aria-hidden="true" />
58
+ </a>
59
+ )}
60
+ </Card>
61
+ );
62
+ }
@@ -1,112 +1,112 @@
1
- import { useState } from "react";
2
- import type { DisplayProduct } from "./sdk-types.js";
3
- import { Star } from "lucide-react";
4
- import { Card, CardContent, CardTitle } from "../ui/card.js";
5
- import { Badge } from "../ui/badge.js";
6
- import { Button } from "../ui/button.js";
7
- import { cn } from "../lib/utils.js";
8
-
9
- function StarRating({ score, count }: { score: number; count: number }) {
10
- const fullStars = Math.floor(score);
11
- const hasHalf = score - fullStars >= 0.5;
12
- const emptyStars = 5 - fullStars - (hasHalf ? 1 : 0);
13
-
14
- return (
15
- <div
16
- className="flex items-center gap-0.5 text-primary"
17
- aria-label={`${score} de 5 estrelas (${count} avaliações)`}
18
- >
19
- {Array.from({ length: fullStars }).map((_, i) => (
20
- <Star key={`f${i}`} size={14} fill="currentColor" />
21
- ))}
22
- {hasHalf && <Star size={14} fill="none" />}
23
- {Array.from({ length: emptyStars }).map((_, i) => (
24
- <Star key={`e${i}`} size={14} fill="none" className="text-muted-foreground" />
25
- ))}
26
- <span className="text-xs text-muted-foreground ml-1">({count})</span>
27
- </div>
28
- );
29
- }
30
-
31
- function formatMoney(value: number, currency = "BRL") {
32
- return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
33
- }
34
-
35
- export function ProductCardRenderer({
36
- title,
37
- image,
38
- price,
39
- originalPrice,
40
- rating,
41
- badges,
42
- url,
43
- description,
44
- }: DisplayProduct) {
45
- const [imgError, setImgError] = useState(false);
46
-
47
- const discount =
48
- price && originalPrice && originalPrice.value > price.value
49
- ? Math.round(((originalPrice.value - price.value) / originalPrice.value) * 100)
50
- : null;
51
-
52
- return (
53
- <Card className="overflow-hidden w-fit max-w-sm">
54
- {image && !imgError && (
55
- <div className="relative">
56
- <img
57
- src={image}
58
- alt={title}
59
- className={cn("w-full aspect-video object-cover")}
60
- loading="lazy"
61
- decoding="async"
62
- onError={() => setImgError(true)}
63
- />
64
- {discount !== null && (
65
- <Badge variant="destructive" className="absolute top-2 right-2">
66
- -{discount}%
67
- </Badge>
68
- )}
69
- </div>
70
- )}
71
-
72
- <CardContent className="p-4 space-y-2">
73
- {badges && badges.length > 0 && (
74
- <div className="flex flex-wrap gap-1">
75
- {badges.map((b, i) => (
76
- <Badge key={i} variant="secondary">
77
- {b.label}
78
- </Badge>
79
- ))}
80
- </div>
81
- )}
82
-
83
- <CardTitle className="text-sm">{title}</CardTitle>
84
-
85
- {description && (
86
- <p className="text-xs text-muted-foreground line-clamp-2">{description}</p>
87
- )}
88
-
89
- {rating && <StarRating score={rating.score} count={rating.count} />}
90
-
91
- {price && (
92
- <div className="flex items-baseline gap-2">
93
- <span className="text-lg font-bold">{formatMoney(price.value, price.currency)}</span>
94
- {originalPrice && (
95
- <span className="text-sm text-muted-foreground line-through">
96
- {formatMoney(originalPrice.value, originalPrice.currency)}
97
- </span>
98
- )}
99
- </div>
100
- )}
101
-
102
- {url && (
103
- <Button className="w-full" size="sm" asChild>
104
- <a href={url} target="_blank" rel="noopener noreferrer">
105
- Ver produto
106
- </a>
107
- </Button>
108
- )}
109
- </CardContent>
110
- </Card>
111
- );
112
- }
1
+ import { useState } from "react";
2
+ import type { DisplayProduct } from "./sdk-types.js";
3
+ import { Star } from "lucide-react";
4
+ import { Card, CardContent, CardTitle } from "../ui/card.js";
5
+ import { Badge } from "../ui/badge.js";
6
+ import { Button } from "../ui/button.js";
7
+ import { cn } from "../lib/utils.js";
8
+
9
+ function StarRating({ score, count }: { score: number; count: number }) {
10
+ const fullStars = Math.floor(score);
11
+ const hasHalf = score - fullStars >= 0.5;
12
+ const emptyStars = 5 - fullStars - (hasHalf ? 1 : 0);
13
+
14
+ return (
15
+ <div
16
+ className="flex items-center gap-0.5 text-primary"
17
+ aria-label={`${score} de 5 estrelas (${count} avaliações)`}
18
+ >
19
+ {Array.from({ length: fullStars }).map((_, i) => (
20
+ <Star key={`f${i}`} size={14} fill="currentColor" />
21
+ ))}
22
+ {hasHalf && <Star size={14} fill="none" />}
23
+ {Array.from({ length: emptyStars }).map((_, i) => (
24
+ <Star key={`e${i}`} size={14} fill="none" className="text-muted-foreground" />
25
+ ))}
26
+ <span className="text-xs text-muted-foreground ml-1">({count})</span>
27
+ </div>
28
+ );
29
+ }
30
+
31
+ function formatMoney(value: number, currency = "BRL") {
32
+ return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
33
+ }
34
+
35
+ export function ProductCardRenderer({
36
+ title,
37
+ image,
38
+ price,
39
+ originalPrice,
40
+ rating,
41
+ badges,
42
+ url,
43
+ description,
44
+ }: DisplayProduct) {
45
+ const [imgError, setImgError] = useState(false);
46
+
47
+ const discount =
48
+ price && originalPrice && originalPrice.value > price.value
49
+ ? Math.round(((originalPrice.value - price.value) / originalPrice.value) * 100)
50
+ : null;
51
+
52
+ return (
53
+ <Card className="overflow-hidden w-fit max-w-sm">
54
+ {image && !imgError && (
55
+ <div className="relative">
56
+ <img
57
+ src={image}
58
+ alt={title}
59
+ className={cn("w-full aspect-video object-cover")}
60
+ loading="lazy"
61
+ decoding="async"
62
+ onError={() => setImgError(true)}
63
+ />
64
+ {discount !== null && (
65
+ <Badge variant="destructive" className="absolute top-2 right-2">
66
+ -{discount}%
67
+ </Badge>
68
+ )}
69
+ </div>
70
+ )}
71
+
72
+ <CardContent className="p-4 space-y-2">
73
+ {badges && badges.length > 0 && (
74
+ <div className="flex flex-wrap gap-1">
75
+ {badges.map((b, i) => (
76
+ <Badge key={i} variant="secondary">
77
+ {b.label}
78
+ </Badge>
79
+ ))}
80
+ </div>
81
+ )}
82
+
83
+ <CardTitle className="text-sm">{title}</CardTitle>
84
+
85
+ {description && (
86
+ <p className="text-xs text-muted-foreground line-clamp-2">{description}</p>
87
+ )}
88
+
89
+ {rating && <StarRating score={rating.score} count={rating.count} />}
90
+
91
+ {price && (
92
+ <div className="flex items-baseline gap-2">
93
+ <span className="text-lg font-bold">{formatMoney(price.value, price.currency)}</span>
94
+ {originalPrice && (
95
+ <span className="text-sm text-muted-foreground line-through">
96
+ {formatMoney(originalPrice.value, originalPrice.currency)}
97
+ </span>
98
+ )}
99
+ </div>
100
+ )}
101
+
102
+ {url && (
103
+ <Button className="w-full" size="sm" asChild>
104
+ <a href={url} target="_blank" rel="noopener noreferrer">
105
+ Ver produto
106
+ </a>
107
+ </Button>
108
+ )}
109
+ </CardContent>
110
+ </Card>
111
+ );
112
+ }
@@ -1,59 +1,59 @@
1
- import type { DisplayProgress } from "./sdk-types.js";
2
- import { Check, Circle, Clock } from "lucide-react";
3
- import { Progress } from "../ui/progress.js";
4
- import { Badge } from "../ui/badge.js";
5
- import { cn } from "../lib/utils.js";
6
-
7
- export function ProgressStepsRenderer({ title, steps }: DisplayProgress) {
8
- const completed = steps.filter((s) => s.status === "completed").length;
9
- const percentage = steps.length > 0 ? Math.round((completed / steps.length) * 100) : 0;
10
-
11
- return (
12
- <div className="space-y-3">
13
- {title && <p className="font-medium text-foreground">{title}</p>}
14
- <div className="flex items-center gap-3">
15
- <Progress value={percentage} className="flex-1" />
16
- <span className="text-sm text-muted-foreground tabular-nums">{percentage}%</span>
17
- </div>
18
- <p className="text-sm text-muted-foreground">
19
- {completed} de {steps.length} concluídos
20
- </p>
21
- <ol className="space-y-2">
22
- {steps.map((step, index) => {
23
- const isCompleted = step.status === "completed";
24
- const isPending = step.status === "pending";
25
- return (
26
- <li key={index} className="flex items-start gap-2">
27
- <span
28
- aria-hidden="true"
29
- className={cn(
30
- "mt-0.5 shrink-0",
31
- isCompleted ? "text-primary" : "text-muted-foreground"
32
- )}
33
- >
34
- {isCompleted ? (
35
- <Check size={16} />
36
- ) : isPending ? (
37
- <Circle size={16} />
38
- ) : (
39
- <Clock size={16} />
40
- )}
41
- </span>
42
- <div className="flex-1 min-w-0">
43
- <p className={cn("text-sm", isCompleted ? "text-foreground" : "text-muted-foreground")}>
44
- {step.label}
45
- </p>
46
- {step.description && (
47
- <p className="text-xs text-muted-foreground mt-0.5">{step.description}</p>
48
- )}
49
- </div>
50
- <Badge variant="secondary" className="shrink-0 text-xs">
51
- {index + 1}
52
- </Badge>
53
- </li>
54
- );
55
- })}
56
- </ol>
57
- </div>
58
- );
59
- }
1
+ import type { DisplayProgress } from "./sdk-types.js";
2
+ import { Check, Circle, Clock } from "lucide-react";
3
+ import { Progress } from "../ui/progress.js";
4
+ import { Badge } from "../ui/badge.js";
5
+ import { cn } from "../lib/utils.js";
6
+
7
+ export function ProgressStepsRenderer({ title, steps }: DisplayProgress) {
8
+ const completed = steps.filter((s) => s.status === "completed").length;
9
+ const percentage = steps.length > 0 ? Math.round((completed / steps.length) * 100) : 0;
10
+
11
+ return (
12
+ <div className="space-y-3">
13
+ {title && <p className="font-medium text-foreground">{title}</p>}
14
+ <div className="flex items-center gap-3">
15
+ <Progress value={percentage} className="flex-1" />
16
+ <span className="text-sm text-muted-foreground tabular-nums">{percentage}%</span>
17
+ </div>
18
+ <p className="text-sm text-muted-foreground">
19
+ {completed} de {steps.length} concluídos
20
+ </p>
21
+ <ol className="space-y-2">
22
+ {steps.map((step, index) => {
23
+ const isCompleted = step.status === "completed";
24
+ const isPending = step.status === "pending";
25
+ return (
26
+ <li key={index} className="flex items-start gap-2">
27
+ <span
28
+ aria-hidden="true"
29
+ className={cn(
30
+ "mt-0.5 shrink-0",
31
+ isCompleted ? "text-primary" : "text-muted-foreground"
32
+ )}
33
+ >
34
+ {isCompleted ? (
35
+ <Check size={16} />
36
+ ) : isPending ? (
37
+ <Circle size={16} />
38
+ ) : (
39
+ <Clock size={16} />
40
+ )}
41
+ </span>
42
+ <div className="flex-1 min-w-0">
43
+ <p className={cn("text-sm", isCompleted ? "text-foreground" : "text-muted-foreground")}>
44
+ {step.label}
45
+ </p>
46
+ {step.description && (
47
+ <p className="text-xs text-muted-foreground mt-0.5">{step.description}</p>
48
+ )}
49
+ </div>
50
+ <Badge variant="secondary" className="shrink-0 text-xs">
51
+ {index + 1}
52
+ </Badge>
53
+ </li>
54
+ );
55
+ })}
56
+ </ol>
57
+ </div>
58
+ );
59
+ }
@@ -1,47 +1,47 @@
1
- import type { DisplaySources } from "./sdk-types.js";
2
- import { ExternalLink, Globe } from "lucide-react";
3
-
4
- export function SourcesListRenderer({ label, sources }: DisplaySources) {
5
- return (
6
- <div className="space-y-2">
7
- <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">{label}</p>
8
- <ol className="space-y-1">
9
- {sources.map((source, index) => (
10
- <li key={index}>
11
- <a
12
- href={source.url}
13
- target="_blank"
14
- rel="noopener noreferrer"
15
- className="flex items-start gap-2 p-2 rounded-md hover:bg-muted text-sm"
16
- >
17
- <span className="text-xs text-muted-foreground font-mono w-5 shrink-0 pt-0.5">
18
- {index + 1}
19
- </span>
20
- <div className="min-w-0 flex-1">
21
- <div className="flex items-center gap-1.5">
22
- {source.favicon ? (
23
- <img
24
- src={source.favicon}
25
- alt=""
26
- width={14}
27
- height={14}
28
- className="shrink-0"
29
- aria-hidden="true"
30
- />
31
- ) : (
32
- <Globe size={14} className="shrink-0 text-muted-foreground" aria-hidden="true" />
33
- )}
34
- <span className="font-medium text-primary truncate">{source.title}</span>
35
- <ExternalLink size={12} className="shrink-0 text-muted-foreground" aria-hidden="true" />
36
- </div>
37
- {source.snippet && (
38
- <p className="text-xs text-muted-foreground line-clamp-2 mt-0.5">{source.snippet}</p>
39
- )}
40
- </div>
41
- </a>
42
- </li>
43
- ))}
44
- </ol>
45
- </div>
46
- );
47
- }
1
+ import type { DisplaySources } from "./sdk-types.js";
2
+ import { ExternalLink, Globe } from "lucide-react";
3
+
4
+ export function SourcesListRenderer({ label, sources }: DisplaySources) {
5
+ return (
6
+ <div className="space-y-2">
7
+ <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">{label}</p>
8
+ <ol className="space-y-1">
9
+ {sources.map((source, index) => (
10
+ <li key={index}>
11
+ <a
12
+ href={source.url}
13
+ target="_blank"
14
+ rel="noopener noreferrer"
15
+ className="flex items-start gap-2 p-2 rounded-md hover:bg-muted text-sm"
16
+ >
17
+ <span className="text-xs text-muted-foreground font-mono w-5 shrink-0 pt-0.5">
18
+ {index + 1}
19
+ </span>
20
+ <div className="min-w-0 flex-1">
21
+ <div className="flex items-center gap-1.5">
22
+ {source.favicon ? (
23
+ <img
24
+ src={source.favicon}
25
+ alt=""
26
+ width={14}
27
+ height={14}
28
+ className="shrink-0"
29
+ aria-hidden="true"
30
+ />
31
+ ) : (
32
+ <Globe size={14} className="shrink-0 text-muted-foreground" aria-hidden="true" />
33
+ )}
34
+ <span className="font-medium text-primary truncate">{source.title}</span>
35
+ <ExternalLink size={12} className="shrink-0 text-muted-foreground" aria-hidden="true" />
36
+ </div>
37
+ {source.snippet && (
38
+ <p className="text-xs text-muted-foreground line-clamp-2 mt-0.5">{source.snippet}</p>
39
+ )}
40
+ </div>
41
+ </a>
42
+ </li>
43
+ ))}
44
+ </ol>
45
+ </div>
46
+ );
47
+ }