@codrstudio/openclaude-chat 0.1.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.
- package/dist/components/Chat.d.ts +23 -0
- package/dist/components/Chat.js +12 -0
- package/dist/components/ErrorNote.d.ts +6 -0
- package/dist/components/ErrorNote.js +6 -0
- package/dist/components/LazyRender.d.ts +8 -0
- package/dist/components/LazyRender.js +22 -0
- package/dist/components/Markdown.d.ts +5 -0
- package/dist/components/Markdown.js +65 -0
- package/dist/components/MessageBubble.d.ts +9 -0
- package/dist/components/MessageBubble.js +45 -0
- package/dist/components/MessageInput.d.ts +19 -0
- package/dist/components/MessageInput.js +214 -0
- package/dist/components/MessageList.d.ts +13 -0
- package/dist/components/MessageList.js +72 -0
- package/dist/components/StreamingIndicator.d.ts +1 -0
- package/dist/components/StreamingIndicator.js +9 -0
- package/dist/display/AlertRenderer.d.ts +2 -0
- package/dist/display/AlertRenderer.js +13 -0
- package/dist/display/CarouselRenderer.d.ts +2 -0
- package/dist/display/CarouselRenderer.js +41 -0
- package/dist/display/ChartRenderer.d.ts +2 -0
- package/dist/display/ChartRenderer.js +76 -0
- package/dist/display/ChoiceButtonsRenderer.d.ts +6 -0
- package/dist/display/ChoiceButtonsRenderer.js +23 -0
- package/dist/display/CodeBlockRenderer.d.ts +2 -0
- package/dist/display/CodeBlockRenderer.js +17 -0
- package/dist/display/ComparisonTableRenderer.d.ts +2 -0
- package/dist/display/ComparisonTableRenderer.js +26 -0
- package/dist/display/DataTableRenderer.d.ts +2 -0
- package/dist/display/DataTableRenderer.js +74 -0
- package/dist/display/DisplayReactRenderer.d.ts +26 -0
- package/dist/display/DisplayReactRenderer.js +192 -0
- package/dist/display/FileCardRenderer.d.ts +2 -0
- package/dist/display/FileCardRenderer.js +31 -0
- package/dist/display/GalleryRenderer.d.ts +2 -0
- package/dist/display/GalleryRenderer.js +11 -0
- package/dist/display/ImageViewerRenderer.d.ts +2 -0
- package/dist/display/ImageViewerRenderer.js +15 -0
- package/dist/display/LinkPreviewRenderer.d.ts +2 -0
- package/dist/display/LinkPreviewRenderer.js +20 -0
- package/dist/display/MapViewRenderer.d.ts +2 -0
- package/dist/display/MapViewRenderer.js +20 -0
- package/dist/display/MetricCardRenderer.d.ts +2 -0
- package/dist/display/MetricCardRenderer.js +12 -0
- package/dist/display/PriceHighlightRenderer.d.ts +2 -0
- package/dist/display/PriceHighlightRenderer.js +30 -0
- package/dist/display/ProductCardRenderer.d.ts +2 -0
- package/dist/display/ProductCardRenderer.js +23 -0
- package/dist/display/ProgressStepsRenderer.d.ts +2 -0
- package/dist/display/ProgressStepsRenderer.js +14 -0
- package/dist/display/SourcesListRenderer.d.ts +2 -0
- package/dist/display/SourcesListRenderer.js +5 -0
- package/dist/display/SpreadsheetRenderer.d.ts +2 -0
- package/dist/display/SpreadsheetRenderer.js +32 -0
- package/dist/display/StepTimelineRenderer.d.ts +2 -0
- package/dist/display/StepTimelineRenderer.js +21 -0
- package/dist/display/index.d.ts +21 -0
- package/dist/display/index.js +20 -0
- package/dist/display/react-sandbox/bootstrap.d.ts +1 -0
- package/dist/display/react-sandbox/bootstrap.js +154 -0
- package/dist/display/registry.d.ts +5 -0
- package/dist/display/registry.js +52 -0
- package/dist/display/sdk-types.d.ts +187 -0
- package/dist/display/sdk-types.js +4 -0
- package/dist/hooks/ChatProvider.d.ts +9 -0
- package/dist/hooks/ChatProvider.js +14 -0
- package/dist/hooks/useIsMobile.d.ts +1 -0
- package/dist/hooks/useIsMobile.js +12 -0
- package/dist/hooks/useOpenClaudeChat.d.ts +36 -0
- package/dist/hooks/useOpenClaudeChat.js +361 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +42 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/parts/PartErrorBoundary.d.ts +21 -0
- package/dist/parts/PartErrorBoundary.js +27 -0
- package/dist/parts/PartRenderer.d.ts +8 -0
- package/dist/parts/PartRenderer.js +99 -0
- package/dist/parts/ReasoningBlock.d.ts +6 -0
- package/dist/parts/ReasoningBlock.js +18 -0
- package/dist/parts/ToolActivity.d.ts +11 -0
- package/dist/parts/ToolActivity.js +52 -0
- package/dist/parts/ToolResult.d.ts +7 -0
- package/dist/parts/ToolResult.js +38 -0
- package/dist/styles.css +2 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.js +4 -0
- package/dist/ui/alert.d.ts +12 -0
- package/dist/ui/alert.js +28 -0
- package/dist/ui/badge.d.ts +9 -0
- package/dist/ui/badge.js +20 -0
- package/dist/ui/button.d.ts +11 -0
- package/dist/ui/button.js +31 -0
- package/dist/ui/card.d.ts +8 -0
- package/dist/ui/card.js +21 -0
- package/dist/ui/collapsible.d.ts +1 -0
- package/dist/ui/collapsible.js +2 -0
- package/dist/ui/dialog.d.ts +19 -0
- package/dist/ui/dialog.js +23 -0
- package/dist/ui/dropdown-menu.d.ts +11 -0
- package/dist/ui/dropdown-menu.js +15 -0
- package/dist/ui/input.d.ts +3 -0
- package/dist/ui/input.js +6 -0
- package/dist/ui/progress.d.ts +7 -0
- package/dist/ui/progress.js +9 -0
- package/dist/ui/scroll-area.d.ts +5 -0
- package/dist/ui/scroll-area.js +12 -0
- package/dist/ui/separator.d.ts +4 -0
- package/dist/ui/separator.js +8 -0
- package/dist/ui/skeleton.d.ts +3 -0
- package/dist/ui/skeleton.js +6 -0
- package/dist/ui/table.d.ts +10 -0
- package/dist/ui/table.js +27 -0
- package/package.json +61 -0
- package/src/components/Chat.tsx +107 -0
- package/src/components/ErrorNote.tsx +35 -0
- package/src/components/LazyRender.tsx +42 -0
- package/src/components/Markdown.tsx +114 -0
- package/src/components/MessageBubble.tsx +107 -0
- package/src/components/MessageInput.tsx +421 -0
- package/src/components/MessageList.tsx +153 -0
- package/src/components/StreamingIndicator.tsx +19 -0
- package/src/display/AlertRenderer.tsx +23 -0
- package/src/display/CarouselRenderer.tsx +141 -0
- package/src/display/ChartRenderer.tsx +195 -0
- package/src/display/ChoiceButtonsRenderer.tsx +114 -0
- package/src/display/CodeBlockRenderer.tsx +49 -0
- package/src/display/ComparisonTableRenderer.tsx +132 -0
- package/src/display/DataTableRenderer.tsx +144 -0
- package/src/display/DisplayReactRenderer.tsx +269 -0
- package/src/display/FileCardRenderer.tsx +55 -0
- package/src/display/GalleryRenderer.tsx +65 -0
- package/src/display/ImageViewerRenderer.tsx +114 -0
- package/src/display/LinkPreviewRenderer.tsx +74 -0
- package/src/display/MapViewRenderer.tsx +75 -0
- package/src/display/MetricCardRenderer.tsx +29 -0
- package/src/display/PriceHighlightRenderer.tsx +62 -0
- package/src/display/ProductCardRenderer.tsx +112 -0
- package/src/display/ProgressStepsRenderer.tsx +59 -0
- package/src/display/SourcesListRenderer.tsx +47 -0
- package/src/display/SpreadsheetRenderer.tsx +86 -0
- package/src/display/StepTimelineRenderer.tsx +75 -0
- package/src/display/index.ts +21 -0
- package/src/display/react-sandbox/bootstrap.ts +155 -0
- package/src/display/registry.ts +84 -0
- package/src/display/sdk-types.ts +217 -0
- package/src/hooks/ChatProvider.tsx +21 -0
- package/src/hooks/useIsMobile.ts +15 -0
- package/src/hooks/useOpenClaudeChat.ts +476 -0
- package/src/index.ts +76 -0
- package/src/lib/utils.ts +6 -0
- package/src/parts/PartErrorBoundary.tsx +51 -0
- package/src/parts/PartRenderer.tsx +145 -0
- package/src/parts/ReasoningBlock.tsx +41 -0
- package/src/parts/ToolActivity.tsx +78 -0
- package/src/parts/ToolResult.tsx +79 -0
- package/src/styles.css +2 -0
- package/src/types.ts +41 -0
- package/src/ui/alert.tsx +77 -0
- package/src/ui/badge.tsx +36 -0
- package/src/ui/button.tsx +54 -0
- package/src/ui/card.tsx +68 -0
- package/src/ui/collapsible.tsx +7 -0
- package/src/ui/dialog.tsx +122 -0
- package/src/ui/dropdown-menu.tsx +76 -0
- package/src/ui/input.tsx +24 -0
- package/src/ui/progress.tsx +36 -0
- package/src/ui/scroll-area.tsx +48 -0
- package/src/ui/separator.tsx +31 -0
- package/src/ui/skeleton.tsx +9 -0
- package/src/ui/table.tsx +114 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { DisplayLink } from "./sdk-types.js";
|
|
3
|
+
import { Globe } from "lucide-react";
|
|
4
|
+
import { Card } from "../ui/card";
|
|
5
|
+
|
|
6
|
+
function getDomain(url: string, domainProp?: string): string {
|
|
7
|
+
if (domainProp) return domainProp;
|
|
8
|
+
try {
|
|
9
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
10
|
+
} catch {
|
|
11
|
+
return url;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function LinkPreviewRenderer({
|
|
16
|
+
url,
|
|
17
|
+
title,
|
|
18
|
+
description,
|
|
19
|
+
image,
|
|
20
|
+
favicon,
|
|
21
|
+
domain,
|
|
22
|
+
}: DisplayLink) {
|
|
23
|
+
const [imgError, setImgError] = useState(false);
|
|
24
|
+
const [faviconError, setFaviconError] = useState(false);
|
|
25
|
+
|
|
26
|
+
const displayDomain = getDomain(url, domain);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<a
|
|
30
|
+
href={url}
|
|
31
|
+
target="_blank"
|
|
32
|
+
rel="noopener noreferrer"
|
|
33
|
+
aria-label={`Link: ${title}`}
|
|
34
|
+
>
|
|
35
|
+
<Card className="overflow-hidden hover:bg-muted/50 transition-colors">
|
|
36
|
+
{image && !imgError && (
|
|
37
|
+
<div className="aspect-video overflow-hidden">
|
|
38
|
+
<img
|
|
39
|
+
src={image}
|
|
40
|
+
alt={title}
|
|
41
|
+
className="w-full h-full object-cover"
|
|
42
|
+
loading="lazy"
|
|
43
|
+
decoding="async"
|
|
44
|
+
onError={() => setImgError(true)}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
<div className="p-3 space-y-1">
|
|
50
|
+
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
51
|
+
{favicon && !faviconError ? (
|
|
52
|
+
<img
|
|
53
|
+
src={favicon}
|
|
54
|
+
alt=""
|
|
55
|
+
className="w-3 h-3 rounded-sm"
|
|
56
|
+
onError={() => setFaviconError(true)}
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
/>
|
|
59
|
+
) : (
|
|
60
|
+
<Globe size={12} aria-hidden="true" className="shrink-0" />
|
|
61
|
+
)}
|
|
62
|
+
<span>{displayDomain}</span>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<p className="font-medium text-foreground text-sm">{title}</p>
|
|
66
|
+
|
|
67
|
+
{description && (
|
|
68
|
+
<p className="text-xs text-muted-foreground line-clamp-2">{description}</p>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
</Card>
|
|
72
|
+
</a>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { DisplayMap } from "./sdk-types.js";
|
|
2
|
+
import { MapPin } from "lucide-react";
|
|
3
|
+
import { Card } from "../ui/card.js";
|
|
4
|
+
import { Separator } from "../ui/separator.js";
|
|
5
|
+
|
|
6
|
+
function buildOsmUrl(pins: DisplayMap["pins"], zoom: number): string {
|
|
7
|
+
if (pins.length === 0) {
|
|
8
|
+
return `https://www.openstreetmap.org/export/embed.html?bbox=-180,-90,180,90&layer=mapnik`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const lat = pins.reduce((acc, p) => acc + p.lat, 0) / pins.length;
|
|
12
|
+
const lng = pins.reduce((acc, p) => acc + p.lng, 0) / pins.length;
|
|
13
|
+
|
|
14
|
+
const firstPin = pins[0]!;
|
|
15
|
+
const markerParam =
|
|
16
|
+
pins.length === 1
|
|
17
|
+
? `&mlat=${firstPin.lat}&mlon=${firstPin.lng}`
|
|
18
|
+
: "";
|
|
19
|
+
|
|
20
|
+
return `https://www.openstreetmap.org/export/embed.html?bbox=${lng - 0.05},${lat - 0.05},${lng + 0.05},${lat + 0.05}&layer=mapnik&zoom=${zoom}${markerParam}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function MapViewRenderer({ title, pins, zoom }: DisplayMap) {
|
|
24
|
+
const osmUrl = buildOsmUrl(pins, zoom);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Card className="overflow-hidden">
|
|
28
|
+
{title && (
|
|
29
|
+
<div className="px-4 py-3">
|
|
30
|
+
<h3 className="font-medium text-sm text-foreground">{title}</h3>
|
|
31
|
+
</div>
|
|
32
|
+
)}
|
|
33
|
+
|
|
34
|
+
<div className="relative aspect-video bg-muted text-muted-foreground overflow-hidden">
|
|
35
|
+
<iframe
|
|
36
|
+
src={osmUrl}
|
|
37
|
+
className="w-full h-full border-0"
|
|
38
|
+
title={title ?? "Mapa OpenStreetMap"}
|
|
39
|
+
loading="lazy"
|
|
40
|
+
referrerPolicy="no-referrer"
|
|
41
|
+
sandbox="allow-scripts allow-same-origin"
|
|
42
|
+
/>
|
|
43
|
+
<div className="absolute inset-0 pointer-events-none flex items-center justify-center opacity-10">
|
|
44
|
+
<MapPin className="h-10 w-10" />
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{pins.length > 0 && (
|
|
49
|
+
<>
|
|
50
|
+
<Separator />
|
|
51
|
+
<ul className="p-3 space-y-2" aria-label="Locais no mapa">
|
|
52
|
+
{pins.map((pin, i) => (
|
|
53
|
+
<li key={i} className="flex items-start gap-2">
|
|
54
|
+
<MapPin className="h-4 w-4 shrink-0 text-primary mt-0.5" aria-hidden="true" />
|
|
55
|
+
<span className="flex flex-col min-w-0">
|
|
56
|
+
{pin.label && (
|
|
57
|
+
<span className="font-medium text-sm text-foreground">{pin.label}</span>
|
|
58
|
+
)}
|
|
59
|
+
{pin.address && (
|
|
60
|
+
<span className="text-xs text-muted-foreground">{pin.address}</span>
|
|
61
|
+
)}
|
|
62
|
+
{!pin.label && !pin.address && (
|
|
63
|
+
<span className="text-xs text-muted-foreground font-mono">
|
|
64
|
+
{pin.lat.toFixed(4)}, {pin.lng.toFixed(4)}
|
|
65
|
+
</span>
|
|
66
|
+
)}
|
|
67
|
+
</span>
|
|
68
|
+
</li>
|
|
69
|
+
))}
|
|
70
|
+
</ul>
|
|
71
|
+
</>
|
|
72
|
+
)}
|
|
73
|
+
</Card>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { DisplaySpreadsheet } from "./sdk-types.js";
|
|
2
|
+
import { ScrollArea, ScrollBar } from "../ui/scroll-area.js";
|
|
3
|
+
import {
|
|
4
|
+
Table,
|
|
5
|
+
TableBody,
|
|
6
|
+
TableCell,
|
|
7
|
+
TableHead,
|
|
8
|
+
TableHeader,
|
|
9
|
+
TableRow,
|
|
10
|
+
} from "../ui/table.js";
|
|
11
|
+
import { cn } from "../lib/utils.js";
|
|
12
|
+
|
|
13
|
+
function formatCell(
|
|
14
|
+
value: string | number | null,
|
|
15
|
+
colIndex: number,
|
|
16
|
+
moneyColumns: number[] = [],
|
|
17
|
+
percentColumns: number[] = [],
|
|
18
|
+
): string {
|
|
19
|
+
if (value === null || value === undefined) return "";
|
|
20
|
+
if (typeof value === "number") {
|
|
21
|
+
if (moneyColumns.includes(colIndex)) {
|
|
22
|
+
return new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(value);
|
|
23
|
+
}
|
|
24
|
+
if (percentColumns.includes(colIndex)) {
|
|
25
|
+
return new Intl.NumberFormat("pt-BR", {
|
|
26
|
+
style: "percent",
|
|
27
|
+
minimumFractionDigits: 1,
|
|
28
|
+
maximumFractionDigits: 2,
|
|
29
|
+
}).format(value / 100);
|
|
30
|
+
}
|
|
31
|
+
return new Intl.NumberFormat("pt-BR").format(value);
|
|
32
|
+
}
|
|
33
|
+
return String(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function SpreadsheetRenderer({ title, headers, rows, format }: DisplaySpreadsheet) {
|
|
37
|
+
const moneyColumns = format?.moneyColumns ?? [];
|
|
38
|
+
const percentColumns = format?.percentColumns ?? [];
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="space-y-2">
|
|
42
|
+
{title && <h3 className="text-sm font-semibold text-foreground">{title}</h3>}
|
|
43
|
+
|
|
44
|
+
<ScrollArea className="w-full">
|
|
45
|
+
<Table aria-readonly="true">
|
|
46
|
+
<TableHeader>
|
|
47
|
+
<TableRow>
|
|
48
|
+
<TableHead className="text-muted-foreground font-normal text-center w-10" aria-label="Linha" />
|
|
49
|
+
{headers.map((h, i) => (
|
|
50
|
+
<TableHead key={i} className="font-semibold">
|
|
51
|
+
{h}
|
|
52
|
+
</TableHead>
|
|
53
|
+
))}
|
|
54
|
+
</TableRow>
|
|
55
|
+
</TableHeader>
|
|
56
|
+
|
|
57
|
+
<TableBody>
|
|
58
|
+
{rows.map((row, ri) => (
|
|
59
|
+
<TableRow key={ri}>
|
|
60
|
+
<TableCell className="text-center text-xs text-muted-foreground select-none">
|
|
61
|
+
{ri + 1}
|
|
62
|
+
</TableCell>
|
|
63
|
+
{row.map((cell, ci) => {
|
|
64
|
+
const isMoney = moneyColumns.includes(ci);
|
|
65
|
+
const isPercent = percentColumns.includes(ci);
|
|
66
|
+
const isNumber = typeof cell === "number";
|
|
67
|
+
return (
|
|
68
|
+
<TableCell
|
|
69
|
+
key={ci}
|
|
70
|
+
className={cn(
|
|
71
|
+
(isMoney || isPercent || isNumber) && "text-right font-mono text-sm"
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
{formatCell(cell, ci, moneyColumns, percentColumns)}
|
|
75
|
+
</TableCell>
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
</TableRow>
|
|
79
|
+
))}
|
|
80
|
+
</TableBody>
|
|
81
|
+
</Table>
|
|
82
|
+
<ScrollBar orientation="horizontal" />
|
|
83
|
+
</ScrollArea>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { DisplaySteps } from "./sdk-types.js";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { Badge } from "../ui/badge";
|
|
4
|
+
|
|
5
|
+
const STATUS_CIRCLE: Record<"completed" | "current" | "pending", string> = {
|
|
6
|
+
completed: "bg-primary text-primary-foreground",
|
|
7
|
+
current: "bg-primary/20 border border-primary",
|
|
8
|
+
pending: "bg-muted text-muted-foreground",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const STATUS_TITLE: Record<"completed" | "current" | "pending", string> = {
|
|
12
|
+
completed: "text-foreground",
|
|
13
|
+
current: "text-primary",
|
|
14
|
+
pending: "text-muted-foreground",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function StepTimelineRenderer({ title, steps, orientation }: DisplaySteps) {
|
|
18
|
+
const isVertical = orientation !== "horizontal";
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className={cn("flex", isVertical ? "flex-col gap-0" : "flex-row items-start gap-0")}>
|
|
22
|
+
{title && (
|
|
23
|
+
<p className="text-sm font-medium text-foreground mb-3">{title}</p>
|
|
24
|
+
)}
|
|
25
|
+
{steps.map((step, index) => {
|
|
26
|
+
const isLast = index === steps.length - 1;
|
|
27
|
+
const { status } = step;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
key={index}
|
|
32
|
+
className={cn(
|
|
33
|
+
"flex",
|
|
34
|
+
isVertical ? "flex-row gap-3" : "flex-col items-center gap-2 flex-1"
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
{/* Circle + connector column */}
|
|
38
|
+
<div className={cn("flex", isVertical ? "flex-col items-center" : "flex-row items-center")}>
|
|
39
|
+
<div
|
|
40
|
+
className={cn(
|
|
41
|
+
"w-6 h-6 rounded-full flex items-center justify-center shrink-0",
|
|
42
|
+
STATUS_CIRCLE[status]
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
<Badge
|
|
46
|
+
variant="secondary"
|
|
47
|
+
className="w-5 h-5 flex items-center justify-center rounded-full p-0 text-[10px] font-semibold border-0 bg-transparent text-inherit"
|
|
48
|
+
>
|
|
49
|
+
{index + 1}
|
|
50
|
+
</Badge>
|
|
51
|
+
</div>
|
|
52
|
+
{!isLast && (
|
|
53
|
+
<div
|
|
54
|
+
className={cn(
|
|
55
|
+
isVertical ? "w-px h-6 bg-border" : "h-px w-full bg-border flex-1"
|
|
56
|
+
)}
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{/* Content */}
|
|
62
|
+
<div className={cn("pb-4 flex-1 min-w-0", isLast && "pb-0", !isVertical && "text-center")}>
|
|
63
|
+
<p className={cn("text-sm font-medium leading-none", STATUS_TITLE[status])}>
|
|
64
|
+
{step.title}
|
|
65
|
+
</p>
|
|
66
|
+
{step.description && (
|
|
67
|
+
<p className="text-xs text-muted-foreground mt-1">{step.description}</p>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { AlertRenderer } from "./AlertRenderer.js";
|
|
2
|
+
export { MetricCardRenderer } from "./MetricCardRenderer.js";
|
|
3
|
+
export { PriceHighlightRenderer } from "./PriceHighlightRenderer.js";
|
|
4
|
+
export { FileCardRenderer } from "./FileCardRenderer.js";
|
|
5
|
+
export { CodeBlockRenderer } from "./CodeBlockRenderer.js";
|
|
6
|
+
export { SourcesListRenderer } from "./SourcesListRenderer.js";
|
|
7
|
+
export { StepTimelineRenderer } from "./StepTimelineRenderer.js";
|
|
8
|
+
export { ProgressStepsRenderer } from "./ProgressStepsRenderer.js";
|
|
9
|
+
export { ChartRenderer } from "./ChartRenderer.js";
|
|
10
|
+
export { CarouselRenderer } from "./CarouselRenderer.js";
|
|
11
|
+
export { ProductCardRenderer } from "./ProductCardRenderer.js";
|
|
12
|
+
export { ComparisonTableRenderer } from "./ComparisonTableRenderer.js";
|
|
13
|
+
export { DataTableRenderer } from "./DataTableRenderer.js";
|
|
14
|
+
export { SpreadsheetRenderer } from "./SpreadsheetRenderer.js";
|
|
15
|
+
export { GalleryRenderer } from "./GalleryRenderer.js";
|
|
16
|
+
export { ImageViewerRenderer } from "./ImageViewerRenderer.js";
|
|
17
|
+
export { LinkPreviewRenderer } from "./LinkPreviewRenderer.js";
|
|
18
|
+
export { MapViewRenderer } from "./MapViewRenderer.js";
|
|
19
|
+
export { ChoiceButtonsRenderer } from "./ChoiceButtonsRenderer.js";
|
|
20
|
+
export { defaultDisplayRenderers, resolveDisplayRenderer } from "./registry.js";
|
|
21
|
+
export type { DisplayRendererMap, DisplayActionName } from "./registry.js";
|