@gugacoder/agentic-chat 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 (187) hide show
  1. package/dist/components/Chat.d.ts +21 -0
  2. package/dist/components/Chat.js +13 -0
  3. package/dist/components/ErrorNote.d.ts +5 -0
  4. package/dist/components/ErrorNote.js +6 -0
  5. package/dist/components/LazyRender.d.ts +8 -0
  6. package/dist/components/LazyRender.js +22 -0
  7. package/dist/components/Markdown.d.ts +5 -0
  8. package/dist/components/Markdown.js +65 -0
  9. package/dist/components/MessageBubble.d.ts +10 -0
  10. package/dist/components/MessageBubble.js +39 -0
  11. package/dist/components/MessageInput.d.ts +19 -0
  12. package/dist/components/MessageInput.js +214 -0
  13. package/dist/components/MessageList.d.ts +12 -0
  14. package/dist/components/MessageList.js +68 -0
  15. package/dist/components/StreamingIndicator.d.ts +1 -0
  16. package/dist/components/StreamingIndicator.js +9 -0
  17. package/dist/conversations/CollapsibleGroup.d.ts +11 -0
  18. package/dist/conversations/CollapsibleGroup.js +9 -0
  19. package/dist/conversations/ConversationBar.d.ts +27 -0
  20. package/dist/conversations/ConversationBar.js +53 -0
  21. package/dist/conversations/ConversationList.d.ts +33 -0
  22. package/dist/conversations/ConversationList.js +48 -0
  23. package/dist/conversations/ConversationListItem.d.ts +20 -0
  24. package/dist/conversations/ConversationListItem.js +22 -0
  25. package/dist/conversations/DeleteDialog.d.ts +13 -0
  26. package/dist/conversations/DeleteDialog.js +8 -0
  27. package/dist/conversations/RenameDialog.d.ts +15 -0
  28. package/dist/conversations/RenameDialog.js +15 -0
  29. package/dist/conversations/index.d.ts +9 -0
  30. package/dist/conversations/index.js +5 -0
  31. package/dist/conversations/types.d.ts +21 -0
  32. package/dist/conversations/types.js +1 -0
  33. package/dist/conversations/useConversations.d.ts +19 -0
  34. package/dist/conversations/useConversations.js +102 -0
  35. package/dist/conversations/utils.d.ts +8 -0
  36. package/dist/conversations/utils.js +134 -0
  37. package/dist/display/AlertRenderer.d.ts +2 -0
  38. package/dist/display/AlertRenderer.js +13 -0
  39. package/dist/display/CarouselRenderer.d.ts +2 -0
  40. package/dist/display/CarouselRenderer.js +41 -0
  41. package/dist/display/ChartRenderer.d.ts +2 -0
  42. package/dist/display/ChartRenderer.js +76 -0
  43. package/dist/display/ChoiceButtonsRenderer.d.ts +6 -0
  44. package/dist/display/ChoiceButtonsRenderer.js +23 -0
  45. package/dist/display/CodeBlockRenderer.d.ts +2 -0
  46. package/dist/display/CodeBlockRenderer.js +17 -0
  47. package/dist/display/ComparisonTableRenderer.d.ts +2 -0
  48. package/dist/display/ComparisonTableRenderer.js +26 -0
  49. package/dist/display/DataTableRenderer.d.ts +2 -0
  50. package/dist/display/DataTableRenderer.js +74 -0
  51. package/dist/display/FileCardRenderer.d.ts +2 -0
  52. package/dist/display/FileCardRenderer.js +31 -0
  53. package/dist/display/GalleryRenderer.d.ts +2 -0
  54. package/dist/display/GalleryRenderer.js +11 -0
  55. package/dist/display/ImageViewerRenderer.d.ts +2 -0
  56. package/dist/display/ImageViewerRenderer.js +15 -0
  57. package/dist/display/LinkPreviewRenderer.d.ts +2 -0
  58. package/dist/display/LinkPreviewRenderer.js +20 -0
  59. package/dist/display/MapViewRenderer.d.ts +2 -0
  60. package/dist/display/MapViewRenderer.js +20 -0
  61. package/dist/display/MetricCardRenderer.d.ts +2 -0
  62. package/dist/display/MetricCardRenderer.js +12 -0
  63. package/dist/display/PriceHighlightRenderer.d.ts +2 -0
  64. package/dist/display/PriceHighlightRenderer.js +13 -0
  65. package/dist/display/ProductCardRenderer.d.ts +2 -0
  66. package/dist/display/ProductCardRenderer.js +23 -0
  67. package/dist/display/ProgressStepsRenderer.d.ts +2 -0
  68. package/dist/display/ProgressStepsRenderer.js +14 -0
  69. package/dist/display/SourcesListRenderer.d.ts +2 -0
  70. package/dist/display/SourcesListRenderer.js +5 -0
  71. package/dist/display/SpreadsheetRenderer.d.ts +2 -0
  72. package/dist/display/SpreadsheetRenderer.js +32 -0
  73. package/dist/display/StepTimelineRenderer.d.ts +2 -0
  74. package/dist/display/StepTimelineRenderer.js +21 -0
  75. package/dist/display/index.d.ts +21 -0
  76. package/dist/display/index.js +20 -0
  77. package/dist/display/registry.d.ts +5 -0
  78. package/dist/display/registry.js +50 -0
  79. package/dist/hooks/ChatProvider.d.ts +10 -0
  80. package/dist/hooks/ChatProvider.js +14 -0
  81. package/dist/hooks/useBackboneChat.d.ts +37 -0
  82. package/dist/hooks/useBackboneChat.js +121 -0
  83. package/dist/hooks/useIsMobile.d.ts +1 -0
  84. package/dist/hooks/useIsMobile.js +12 -0
  85. package/dist/index.d.ts +47 -0
  86. package/dist/index.js +40 -0
  87. package/dist/lib/utils.d.ts +2 -0
  88. package/dist/lib/utils.js +5 -0
  89. package/dist/parts/PartRenderer.d.ts +40 -0
  90. package/dist/parts/PartRenderer.js +97 -0
  91. package/dist/parts/ReasoningBlock.d.ts +6 -0
  92. package/dist/parts/ReasoningBlock.js +18 -0
  93. package/dist/parts/ToolActivity.d.ts +11 -0
  94. package/dist/parts/ToolActivity.js +52 -0
  95. package/dist/parts/ToolResult.d.ts +7 -0
  96. package/dist/parts/ToolResult.js +38 -0
  97. package/dist/styles.css +2 -0
  98. package/dist/ui/alert.d.ts +12 -0
  99. package/dist/ui/alert.js +28 -0
  100. package/dist/ui/badge.d.ts +9 -0
  101. package/dist/ui/badge.js +20 -0
  102. package/dist/ui/button.d.ts +11 -0
  103. package/dist/ui/button.js +31 -0
  104. package/dist/ui/card.d.ts +8 -0
  105. package/dist/ui/card.js +21 -0
  106. package/dist/ui/collapsible.d.ts +1 -0
  107. package/dist/ui/collapsible.js +2 -0
  108. package/dist/ui/dialog.d.ts +19 -0
  109. package/dist/ui/dialog.js +23 -0
  110. package/dist/ui/dropdown-menu.d.ts +11 -0
  111. package/dist/ui/dropdown-menu.js +15 -0
  112. package/dist/ui/input.d.ts +3 -0
  113. package/dist/ui/input.js +6 -0
  114. package/dist/ui/progress.d.ts +7 -0
  115. package/dist/ui/progress.js +9 -0
  116. package/dist/ui/scroll-area.d.ts +5 -0
  117. package/dist/ui/scroll-area.js +12 -0
  118. package/dist/ui/separator.d.ts +4 -0
  119. package/dist/ui/separator.js +8 -0
  120. package/dist/ui/skeleton.d.ts +3 -0
  121. package/dist/ui/skeleton.js +6 -0
  122. package/dist/ui/table.d.ts +10 -0
  123. package/dist/ui/table.js +27 -0
  124. package/package.json +53 -0
  125. package/src/components/Chat.tsx +80 -0
  126. package/src/components/ErrorNote.tsx +32 -0
  127. package/src/components/LazyRender.tsx +42 -0
  128. package/src/components/Markdown.tsx +114 -0
  129. package/src/components/MessageBubble.tsx +102 -0
  130. package/src/components/MessageInput.tsx +421 -0
  131. package/src/components/MessageList.tsx +139 -0
  132. package/src/components/StreamingIndicator.tsx +19 -0
  133. package/src/conversations/CollapsibleGroup.tsx +41 -0
  134. package/src/conversations/ConversationBar.tsx +200 -0
  135. package/src/conversations/ConversationList.tsx +234 -0
  136. package/src/conversations/ConversationListItem.tsx +123 -0
  137. package/src/conversations/DeleteDialog.tsx +55 -0
  138. package/src/conversations/RenameDialog.tsx +74 -0
  139. package/src/conversations/index.ts +14 -0
  140. package/src/conversations/types.ts +17 -0
  141. package/src/conversations/useConversations.ts +148 -0
  142. package/src/conversations/utils.ts +159 -0
  143. package/src/display/AlertRenderer.tsx +27 -0
  144. package/src/display/CarouselRenderer.tsx +141 -0
  145. package/src/display/ChartRenderer.tsx +195 -0
  146. package/src/display/ChoiceButtonsRenderer.tsx +114 -0
  147. package/src/display/CodeBlockRenderer.tsx +49 -0
  148. package/src/display/ComparisonTableRenderer.tsx +132 -0
  149. package/src/display/DataTableRenderer.tsx +144 -0
  150. package/src/display/FileCardRenderer.tsx +55 -0
  151. package/src/display/GalleryRenderer.tsx +65 -0
  152. package/src/display/ImageViewerRenderer.tsx +114 -0
  153. package/src/display/LinkPreviewRenderer.tsx +74 -0
  154. package/src/display/MapViewRenderer.tsx +75 -0
  155. package/src/display/MetricCardRenderer.tsx +29 -0
  156. package/src/display/PriceHighlightRenderer.tsx +44 -0
  157. package/src/display/ProductCardRenderer.tsx +112 -0
  158. package/src/display/ProgressStepsRenderer.tsx +59 -0
  159. package/src/display/SourcesListRenderer.tsx +47 -0
  160. package/src/display/SpreadsheetRenderer.tsx +86 -0
  161. package/src/display/StepTimelineRenderer.tsx +75 -0
  162. package/src/display/index.ts +21 -0
  163. package/src/display/registry.ts +81 -0
  164. package/src/hooks/ChatProvider.tsx +22 -0
  165. package/src/hooks/useBackboneChat.ts +148 -0
  166. package/src/hooks/useIsMobile.ts +15 -0
  167. package/src/index.ts +80 -0
  168. package/src/lib/utils.ts +6 -0
  169. package/src/parts/PartRenderer.tsx +198 -0
  170. package/src/parts/ReasoningBlock.tsx +41 -0
  171. package/src/parts/ToolActivity.tsx +79 -0
  172. package/src/parts/ToolResult.tsx +79 -0
  173. package/src/styles.css +2 -0
  174. package/src/ui/alert.tsx +77 -0
  175. package/src/ui/badge.tsx +36 -0
  176. package/src/ui/button.tsx +54 -0
  177. package/src/ui/card.tsx +68 -0
  178. package/src/ui/collapsible.tsx +7 -0
  179. package/src/ui/dialog.tsx +122 -0
  180. package/src/ui/dropdown-menu.tsx +76 -0
  181. package/src/ui/input.tsx +24 -0
  182. package/src/ui/progress.tsx +36 -0
  183. package/src/ui/scroll-area.tsx +48 -0
  184. package/src/ui/separator.tsx +31 -0
  185. package/src/ui/skeleton.tsx +9 -0
  186. package/src/ui/table.tsx +114 -0
  187. package/tsconfig.json +17 -0
@@ -0,0 +1,144 @@
1
+ import { useState } from "react";
2
+ import type { DisplayTable } from "@gugacoder/agentic-sdk";
3
+ import { ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react";
4
+ import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "../ui/table.js";
5
+ import { ScrollArea, ScrollBar } from "../ui/scroll-area.js";
6
+ import { Button } from "../ui/button.js";
7
+ import { Badge } from "../ui/badge.js";
8
+
9
+ type SortDir = "asc" | "desc";
10
+
11
+ function formatMoney(value: number, currency = "BRL") {
12
+ return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
13
+ }
14
+
15
+ function renderCellValue(value: unknown, type: string): React.ReactNode {
16
+ if (value == null) return "—";
17
+ switch (type) {
18
+ case "money":
19
+ return typeof value === "number"
20
+ ? formatMoney(value)
21
+ : typeof value === "object" && value !== null && "value" in value
22
+ ? formatMoney((value as { value: number; currency?: string }).value, (value as { currency?: string }).currency)
23
+ : String(value);
24
+ case "image":
25
+ return typeof value === "string" ? (
26
+ <img src={value} alt="" className="rounded-sm max-h-12 object-cover" loading="lazy" decoding="async" />
27
+ ) : null;
28
+ case "link":
29
+ return typeof value === "string" ? (
30
+ <a href={value} target="_blank" rel="noopener noreferrer" className="text-primary underline underline-offset-2 hover:opacity-80">
31
+ {value}
32
+ </a>
33
+ ) : null;
34
+ case "badge":
35
+ return <Badge variant="secondary">{String(value)}</Badge>;
36
+ default:
37
+ return String(value);
38
+ }
39
+ }
40
+
41
+ function compareValues(a: unknown, b: unknown, type: string): number {
42
+ if (a == null && b == null) return 0;
43
+ if (a == null) return 1;
44
+ if (b == null) return -1;
45
+
46
+ if (type === "money") {
47
+ const av = typeof a === "number" ? a : typeof a === "object" && a !== null && "value" in a ? (a as { value: number }).value : 0;
48
+ const bv = typeof b === "number" ? b : typeof b === "object" && b !== null && "value" in b ? (b as { value: number }).value : 0;
49
+ return av - bv;
50
+ }
51
+ if (type === "number") {
52
+ return (Number(a) || 0) - (Number(b) || 0);
53
+ }
54
+ return String(a).localeCompare(String(b), "pt-BR");
55
+ }
56
+
57
+ export function DataTableRenderer({ title, columns, rows, sortable }: DisplayTable) {
58
+ const [sortKey, setSortKey] = useState<string | null>(null);
59
+ const [sortDir, setSortDir] = useState<SortDir>("asc");
60
+
61
+ function handleSort(key: string) {
62
+ if (!sortable) return;
63
+ if (sortKey === key) {
64
+ setSortDir((d) => (d === "asc" ? "desc" : "asc"));
65
+ } else {
66
+ setSortKey(key);
67
+ setSortDir("asc");
68
+ }
69
+ }
70
+
71
+ const sortedRows = sortKey
72
+ ? [...rows].sort((a, b) => {
73
+ const col = columns.find((c) => c.key === sortKey);
74
+ const dir = sortDir === "asc" ? 1 : -1;
75
+ return compareValues(a[sortKey], b[sortKey], col?.type ?? "text") * dir;
76
+ })
77
+ : rows;
78
+
79
+ return (
80
+ <div className="space-y-2">
81
+ {title && <h3 className="text-sm font-semibold text-foreground">{title}</h3>}
82
+
83
+ <ScrollArea className="w-full">
84
+ <Table>
85
+ <TableHeader>
86
+ <TableRow>
87
+ {columns.map((col) => (
88
+ <TableHead
89
+ key={col.key}
90
+ className={col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : "text-left"}
91
+ >
92
+ {sortable ? (
93
+ <Button
94
+ variant="ghost"
95
+ size="sm"
96
+ className="-ml-3 h-8 font-semibold"
97
+ onClick={() => handleSort(col.key)}
98
+ aria-sort={
99
+ sortKey === col.key
100
+ ? sortDir === "asc"
101
+ ? "ascending"
102
+ : "descending"
103
+ : undefined
104
+ }
105
+ >
106
+ {col.label}
107
+ {sortKey === col.key ? (
108
+ sortDir === "asc" ? (
109
+ <ArrowUp className="ml-1 h-3 w-3" />
110
+ ) : (
111
+ <ArrowDown className="ml-1 h-3 w-3" />
112
+ )
113
+ ) : (
114
+ <ArrowUpDown className="ml-1 h-3 w-3" />
115
+ )}
116
+ </Button>
117
+ ) : (
118
+ col.label
119
+ )}
120
+ </TableHead>
121
+ ))}
122
+ </TableRow>
123
+ </TableHeader>
124
+
125
+ <TableBody>
126
+ {sortedRows.map((row, ri) => (
127
+ <TableRow key={ri}>
128
+ {columns.map((col) => (
129
+ <TableCell
130
+ key={col.key}
131
+ className={col.align === "right" ? "text-right" : col.align === "center" ? "text-center" : "text-left"}
132
+ >
133
+ {renderCellValue(row[col.key], col.type)}
134
+ </TableCell>
135
+ ))}
136
+ </TableRow>
137
+ ))}
138
+ </TableBody>
139
+ </Table>
140
+ <ScrollBar orientation="horizontal" />
141
+ </ScrollArea>
142
+ </div>
143
+ );
144
+ }
@@ -0,0 +1,55 @@
1
+ import type { DisplayFile } from "@gugacoder/agentic-sdk";
2
+ import {
3
+ Download,
4
+ File,
5
+ FileCode,
6
+ FileImage,
7
+ FileMinus,
8
+ FileText,
9
+ FileVideo,
10
+ Music,
11
+ } from "lucide-react";
12
+ import { Button } from "../ui/button.js";
13
+ import { Card } from "../ui/card.js";
14
+
15
+ function getFileIcon(type: string) {
16
+ const mime = type.toLowerCase();
17
+ if (mime.startsWith("image/")) return FileImage;
18
+ if (mime.startsWith("video/")) return FileVideo;
19
+ if (mime.startsWith("audio/")) return Music;
20
+ if (mime === "application/pdf" || mime === "text/plain" || mime.includes("document")) return FileText;
21
+ if (mime.includes("spreadsheet") || mime.includes("csv")) return FileMinus;
22
+ if (mime.includes("javascript") || mime.includes("typescript") || mime.includes("json") || mime.includes("html") || mime.includes("css") || mime.includes("xml")) return FileCode;
23
+ return File;
24
+ }
25
+
26
+ function formatSize(bytes: number): string {
27
+ if (bytes < 1024) return `${bytes} B`;
28
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
29
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
30
+ }
31
+
32
+ export function FileCardRenderer({ name, type, size, url }: DisplayFile) {
33
+ const Icon = getFileIcon(type);
34
+
35
+ return (
36
+ <Card className="flex items-center gap-3 p-3">
37
+ <div className="shrink-0 text-primary">
38
+ <Icon className="h-8 w-8" />
39
+ </div>
40
+ <div className="flex-1 min-w-0">
41
+ <p className="font-medium text-sm truncate">{name}</p>
42
+ <p className="text-xs text-muted-foreground">
43
+ {type}{size !== undefined && ` · ${formatSize(size)}`}
44
+ </p>
45
+ </div>
46
+ {url && (
47
+ <Button variant="ghost" size="icon" asChild>
48
+ <a href={url} download={name} aria-label={`Baixar ${name}`}>
49
+ <Download className="h-4 w-4" />
50
+ </a>
51
+ </Button>
52
+ )}
53
+ </Card>
54
+ );
55
+ }
@@ -0,0 +1,65 @@
1
+ import { useState } from "react";
2
+ import type { DisplayGallery } from "@gugacoder/agentic-sdk";
3
+ import { ZoomIn } from "lucide-react";
4
+ import { cn } from "../lib/utils.js";
5
+ import { Dialog, DialogContent, DialogTitle } from "../ui/dialog.js";
6
+
7
+ export function GalleryRenderer({ title, images }: DisplayGallery) {
8
+ const [lightboxIdx, setLightboxIdx] = useState<number | null>(null);
9
+
10
+ const activeImage = lightboxIdx !== null ? images[lightboxIdx] : null;
11
+
12
+ return (
13
+ <div className="space-y-2">
14
+ {title && <h3 className="text-sm font-medium text-foreground">{title}</h3>}
15
+
16
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-2">
17
+ {images.map((img, i) => (
18
+ <button
19
+ key={i}
20
+ className="relative group cursor-pointer rounded-md overflow-hidden"
21
+ onClick={() => setLightboxIdx(i)}
22
+ aria-label={img.alt ?? `Imagem ${i + 1}`}
23
+ >
24
+ <img
25
+ src={img.url}
26
+ alt={img.alt ?? ""}
27
+ className="w-full h-full object-cover aspect-square"
28
+ loading="lazy"
29
+ />
30
+ <div className="absolute inset-0 bg-background/50 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity">
31
+ <ZoomIn className="h-5 w-5 text-foreground" />
32
+ </div>
33
+ {img.caption && (
34
+ <p className="absolute bottom-0 left-0 right-0 text-sm text-muted-foreground bg-background/80 px-2 py-1 truncate">
35
+ {img.caption}
36
+ </p>
37
+ )}
38
+ </button>
39
+ ))}
40
+ </div>
41
+
42
+ <Dialog open={lightboxIdx !== null} onOpenChange={(open) => { if (!open) setLightboxIdx(null); }}>
43
+ <DialogContent className={cn("max-w-4xl p-0 bg-background/95")}>
44
+ <DialogTitle className="sr-only">
45
+ {activeImage?.alt ?? "Visualizador de imagem"}
46
+ </DialogTitle>
47
+ {activeImage && (
48
+ <div className="flex flex-col">
49
+ <img
50
+ src={activeImage.url}
51
+ alt={activeImage.alt ?? ""}
52
+ className="w-full max-h-[80vh] object-contain"
53
+ />
54
+ {activeImage.caption && (
55
+ <p className="text-sm text-muted-foreground px-4 py-3">
56
+ {activeImage.caption}
57
+ </p>
58
+ )}
59
+ </div>
60
+ )}
61
+ </DialogContent>
62
+ </Dialog>
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,114 @@
1
+ import { useState } from "react";
2
+ import type { DisplayImage } from "@gugacoder/agentic-sdk";
3
+ import { ZoomIn, ZoomOut, RotateCcw, X } from "lucide-react";
4
+ import { Dialog, DialogContent, DialogTitle } from "../ui/dialog.js";
5
+ import { Button } from "../ui/button.js";
6
+ import { cn } from "../lib/utils.js";
7
+
8
+ export function ImageViewerRenderer({ url, alt, caption, width, height }: DisplayImage) {
9
+ const [dialogOpen, setDialogOpen] = useState(false);
10
+ const [zoom, setZoom] = useState(1);
11
+
12
+ function handleOpen() {
13
+ setZoom(1);
14
+ setDialogOpen(true);
15
+ }
16
+
17
+ return (
18
+ <div className="flex flex-col gap-1.5">
19
+ <button
20
+ className="relative group cursor-pointer rounded-md overflow-hidden inline-block"
21
+ onClick={handleOpen}
22
+ aria-label={`Ampliar imagem${alt ? `: ${alt}` : ""}`}
23
+ >
24
+ <img
25
+ src={url}
26
+ alt={alt ?? ""}
27
+ className="block max-w-full rounded-md"
28
+ loading="lazy"
29
+ decoding="async"
30
+ width={width}
31
+ height={height}
32
+ />
33
+ <div className="absolute inset-0 bg-background/50 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity">
34
+ <ZoomIn className="h-5 w-5 text-foreground" />
35
+ </div>
36
+ </button>
37
+
38
+ {caption && (
39
+ <p className="text-xs text-muted-foreground">{caption}</p>
40
+ )}
41
+
42
+ <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
43
+ <DialogContent className="max-w-4xl p-0 bg-background/95 overflow-hidden">
44
+ <DialogTitle className="sr-only">{alt ?? "Visualizador de imagem"}</DialogTitle>
45
+
46
+ <div className="flex flex-col">
47
+ <div className="flex items-center gap-1 p-2 border-b border-border">
48
+ <Button
49
+ variant="ghost"
50
+ size="icon"
51
+ onClick={() => setZoom((z) => Math.max(0.25, z - 0.25))}
52
+ aria-label="Reduzir zoom"
53
+ disabled={zoom <= 0.25}
54
+ >
55
+ <ZoomOut className="h-4 w-4" />
56
+ </Button>
57
+
58
+ <span className={cn("text-xs text-muted-foreground w-12 text-center tabular-nums")}>
59
+ {Math.round(zoom * 100)}%
60
+ </span>
61
+
62
+ <Button
63
+ variant="ghost"
64
+ size="icon"
65
+ onClick={() => setZoom((z) => Math.min(4, z + 0.25))}
66
+ aria-label="Aumentar zoom"
67
+ disabled={zoom >= 4}
68
+ >
69
+ <ZoomIn className="h-4 w-4" />
70
+ </Button>
71
+
72
+ <Button
73
+ variant="ghost"
74
+ size="icon"
75
+ onClick={() => setZoom(1)}
76
+ aria-label="Resetar zoom"
77
+ >
78
+ <RotateCcw className="h-4 w-4" />
79
+ </Button>
80
+
81
+ <div className="flex-1" />
82
+
83
+ <Button
84
+ variant="ghost"
85
+ size="icon"
86
+ onClick={() => setDialogOpen(false)}
87
+ aria-label="Fechar"
88
+ >
89
+ <X className="h-4 w-4" />
90
+ </Button>
91
+ </div>
92
+
93
+ <div className="overflow-auto max-h-[80vh] flex items-center justify-center p-4">
94
+ <img
95
+ src={url}
96
+ alt={alt ?? ""}
97
+ style={{ transform: `scale(${zoom})`, transformOrigin: "center" }}
98
+ className="max-w-full transition-transform"
99
+ width={width}
100
+ height={height}
101
+ />
102
+ </div>
103
+
104
+ {caption && (
105
+ <p className="text-xs text-muted-foreground text-center p-2 border-t border-border">
106
+ {caption}
107
+ </p>
108
+ )}
109
+ </div>
110
+ </DialogContent>
111
+ </Dialog>
112
+ </div>
113
+ );
114
+ }
@@ -0,0 +1,74 @@
1
+ import { useState } from "react";
2
+ import type { DisplayLink } from "@gugacoder/agentic-sdk";
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 "@gugacoder/agentic-sdk";
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 "@gugacoder/agentic-sdk";
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,44 @@
1
+ import type { DisplayPrice } from "@gugacoder/agentic-sdk";
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
+ return new Intl.NumberFormat("pt-BR", {
8
+ style: "currency",
9
+ currency,
10
+ }).format(value);
11
+ }
12
+
13
+ export function PriceHighlightRenderer({ value, label, context, source, badge }: DisplayPrice) {
14
+ return (
15
+ <Card className="p-4 space-y-1 w-fit">
16
+ <p className="text-sm text-muted-foreground">{label}</p>
17
+ <div className="flex items-baseline gap-2">
18
+ <span className="text-2xl font-bold text-foreground">
19
+ {formatPrice(value.value, value.currency)}
20
+ </span>
21
+ {badge && (
22
+ <Badge variant="destructive">
23
+ {badge.label}
24
+ </Badge>
25
+ )}
26
+ </div>
27
+ {context && <p className="text-sm text-muted-foreground">{context}</p>}
28
+ {source && (
29
+ <a
30
+ href={source.url}
31
+ target="_blank"
32
+ rel="noopener noreferrer"
33
+ className="flex items-center gap-1.5 text-xs text-primary hover:underline"
34
+ >
35
+ {source.favicon && (
36
+ <img src={source.favicon} alt="" width={14} height={14} aria-hidden="true" />
37
+ )}
38
+ <span>{source.name}</span>
39
+ <ExternalLink size={12} aria-hidden="true" />
40
+ </a>
41
+ )}
42
+ </Card>
43
+ );
44
+ }