@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.
- package/dist/components/Chat.d.ts +21 -0
- package/dist/components/Chat.js +13 -0
- package/dist/components/ErrorNote.d.ts +5 -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 +10 -0
- package/dist/components/MessageBubble.js +39 -0
- package/dist/components/MessageInput.d.ts +19 -0
- package/dist/components/MessageInput.js +214 -0
- package/dist/components/MessageList.d.ts +12 -0
- package/dist/components/MessageList.js +68 -0
- package/dist/components/StreamingIndicator.d.ts +1 -0
- package/dist/components/StreamingIndicator.js +9 -0
- package/dist/conversations/CollapsibleGroup.d.ts +11 -0
- package/dist/conversations/CollapsibleGroup.js +9 -0
- package/dist/conversations/ConversationBar.d.ts +27 -0
- package/dist/conversations/ConversationBar.js +53 -0
- package/dist/conversations/ConversationList.d.ts +33 -0
- package/dist/conversations/ConversationList.js +48 -0
- package/dist/conversations/ConversationListItem.d.ts +20 -0
- package/dist/conversations/ConversationListItem.js +22 -0
- package/dist/conversations/DeleteDialog.d.ts +13 -0
- package/dist/conversations/DeleteDialog.js +8 -0
- package/dist/conversations/RenameDialog.d.ts +15 -0
- package/dist/conversations/RenameDialog.js +15 -0
- package/dist/conversations/index.d.ts +9 -0
- package/dist/conversations/index.js +5 -0
- package/dist/conversations/types.d.ts +21 -0
- package/dist/conversations/types.js +1 -0
- package/dist/conversations/useConversations.d.ts +19 -0
- package/dist/conversations/useConversations.js +102 -0
- package/dist/conversations/utils.d.ts +8 -0
- package/dist/conversations/utils.js +134 -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/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 +13 -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/registry.d.ts +5 -0
- package/dist/display/registry.js +50 -0
- package/dist/hooks/ChatProvider.d.ts +10 -0
- package/dist/hooks/ChatProvider.js +14 -0
- package/dist/hooks/useBackboneChat.d.ts +37 -0
- package/dist/hooks/useBackboneChat.js +121 -0
- package/dist/hooks/useIsMobile.d.ts +1 -0
- package/dist/hooks/useIsMobile.js +12 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +40 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/parts/PartRenderer.d.ts +40 -0
- package/dist/parts/PartRenderer.js +97 -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/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 +53 -0
- package/src/components/Chat.tsx +80 -0
- package/src/components/ErrorNote.tsx +32 -0
- package/src/components/LazyRender.tsx +42 -0
- package/src/components/Markdown.tsx +114 -0
- package/src/components/MessageBubble.tsx +102 -0
- package/src/components/MessageInput.tsx +421 -0
- package/src/components/MessageList.tsx +139 -0
- package/src/components/StreamingIndicator.tsx +19 -0
- package/src/conversations/CollapsibleGroup.tsx +41 -0
- package/src/conversations/ConversationBar.tsx +200 -0
- package/src/conversations/ConversationList.tsx +234 -0
- package/src/conversations/ConversationListItem.tsx +123 -0
- package/src/conversations/DeleteDialog.tsx +55 -0
- package/src/conversations/RenameDialog.tsx +74 -0
- package/src/conversations/index.ts +14 -0
- package/src/conversations/types.ts +17 -0
- package/src/conversations/useConversations.ts +148 -0
- package/src/conversations/utils.ts +159 -0
- package/src/display/AlertRenderer.tsx +27 -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/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 +44 -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/registry.ts +81 -0
- package/src/hooks/ChatProvider.tsx +22 -0
- package/src/hooks/useBackboneChat.ts +148 -0
- package/src/hooks/useIsMobile.ts +15 -0
- package/src/index.ts +80 -0
- package/src/lib/utils.ts +6 -0
- package/src/parts/PartRenderer.tsx +198 -0
- package/src/parts/ReasoningBlock.tsx +41 -0
- package/src/parts/ToolActivity.tsx +79 -0
- package/src/parts/ToolResult.tsx +79 -0
- package/src/styles.css +2 -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
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { DisplayProduct } from "@gugacoder/agentic-sdk";
|
|
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 "@gugacoder/agentic-sdk";
|
|
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 "@gugacoder/agentic-sdk";
|
|
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 "@gugacoder/agentic-sdk";
|
|
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 "@gugacoder/agentic-sdk";
|
|
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";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
|
|
3
|
+
import { AlertRenderer } from "./AlertRenderer.js";
|
|
4
|
+
import { MetricCardRenderer } from "./MetricCardRenderer.js";
|
|
5
|
+
import { PriceHighlightRenderer } from "./PriceHighlightRenderer.js";
|
|
6
|
+
import { FileCardRenderer } from "./FileCardRenderer.js";
|
|
7
|
+
import { CodeBlockRenderer } from "./CodeBlockRenderer.js";
|
|
8
|
+
import { SourcesListRenderer } from "./SourcesListRenderer.js";
|
|
9
|
+
import { StepTimelineRenderer } from "./StepTimelineRenderer.js";
|
|
10
|
+
import { ProgressStepsRenderer } from "./ProgressStepsRenderer.js";
|
|
11
|
+
import { ChartRenderer } from "./ChartRenderer.js";
|
|
12
|
+
import { CarouselRenderer } from "./CarouselRenderer.js";
|
|
13
|
+
import { ProductCardRenderer } from "./ProductCardRenderer.js";
|
|
14
|
+
import { ComparisonTableRenderer } from "./ComparisonTableRenderer.js";
|
|
15
|
+
import { DataTableRenderer } from "./DataTableRenderer.js";
|
|
16
|
+
import { SpreadsheetRenderer } from "./SpreadsheetRenderer.js";
|
|
17
|
+
import { GalleryRenderer } from "./GalleryRenderer.js";
|
|
18
|
+
import { ImageViewerRenderer } from "./ImageViewerRenderer.js";
|
|
19
|
+
import { LinkPreviewRenderer } from "./LinkPreviewRenderer.js";
|
|
20
|
+
import { MapViewRenderer } from "./MapViewRenderer.js";
|
|
21
|
+
import { ChoiceButtonsRenderer } from "./ChoiceButtonsRenderer.js";
|
|
22
|
+
|
|
23
|
+
export type DisplayActionName =
|
|
24
|
+
| "metric"
|
|
25
|
+
| "price"
|
|
26
|
+
| "alert"
|
|
27
|
+
| "choices"
|
|
28
|
+
| "table"
|
|
29
|
+
| "spreadsheet"
|
|
30
|
+
| "comparison"
|
|
31
|
+
| "carousel"
|
|
32
|
+
| "gallery"
|
|
33
|
+
| "sources"
|
|
34
|
+
| "product"
|
|
35
|
+
| "link"
|
|
36
|
+
| "file"
|
|
37
|
+
| "image"
|
|
38
|
+
| "chart"
|
|
39
|
+
| "map"
|
|
40
|
+
| "code"
|
|
41
|
+
| "progress"
|
|
42
|
+
| "steps";
|
|
43
|
+
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
export type DisplayRendererMap = Partial<Record<string, ComponentType<any>>>;
|
|
46
|
+
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
|
+
export const defaultDisplayRenderers: Record<DisplayActionName, ComponentType<any>> = {
|
|
49
|
+
// highlight
|
|
50
|
+
metric: MetricCardRenderer,
|
|
51
|
+
price: PriceHighlightRenderer,
|
|
52
|
+
alert: AlertRenderer,
|
|
53
|
+
choices: ChoiceButtonsRenderer,
|
|
54
|
+
// collection
|
|
55
|
+
table: DataTableRenderer,
|
|
56
|
+
spreadsheet: SpreadsheetRenderer,
|
|
57
|
+
comparison: ComparisonTableRenderer,
|
|
58
|
+
carousel: CarouselRenderer,
|
|
59
|
+
gallery: GalleryRenderer,
|
|
60
|
+
sources: SourcesListRenderer,
|
|
61
|
+
// card
|
|
62
|
+
product: ProductCardRenderer,
|
|
63
|
+
link: LinkPreviewRenderer,
|
|
64
|
+
file: FileCardRenderer,
|
|
65
|
+
image: ImageViewerRenderer,
|
|
66
|
+
// visual
|
|
67
|
+
chart: ChartRenderer,
|
|
68
|
+
map: MapViewRenderer,
|
|
69
|
+
code: CodeBlockRenderer,
|
|
70
|
+
progress: ProgressStepsRenderer,
|
|
71
|
+
steps: StepTimelineRenderer,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export function resolveDisplayRenderer(
|
|
75
|
+
action: string,
|
|
76
|
+
overrides?: DisplayRendererMap,
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
+
): ComponentType<any> | null {
|
|
79
|
+
if (overrides?.[action]) return overrides[action]!;
|
|
80
|
+
return (defaultDisplayRenderers as Record<string, ComponentType<any>>)[action] ?? null;
|
|
81
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React, { createContext, useContext } from "react";
|
|
2
|
+
import { useBackboneChat, type UseBackboneChatOptions } from "./useBackboneChat.js";
|
|
3
|
+
|
|
4
|
+
type ChatContextValue = ReturnType<typeof useBackboneChat>;
|
|
5
|
+
|
|
6
|
+
const ChatContext = createContext<ChatContextValue | null>(null);
|
|
7
|
+
|
|
8
|
+
export interface ChatProviderProps extends Omit<UseBackboneChatOptions, "initialMessages"> {
|
|
9
|
+
initialMessages?: UseBackboneChatOptions["initialMessages"];
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ChatProvider({ endpoint, token, sessionId, initialMessages, children }: ChatProviderProps) {
|
|
14
|
+
const chat = useBackboneChat({ endpoint, token, sessionId, initialMessages });
|
|
15
|
+
return <ChatContext.Provider value={chat}>{children}</ChatContext.Provider>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useChatContext(): ChatContextValue {
|
|
19
|
+
const ctx = useContext(ChatContext);
|
|
20
|
+
if (!ctx) throw new Error("useChatContext must be used within ChatProvider");
|
|
21
|
+
return ctx;
|
|
22
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { useChat } from "@ai-sdk/react";
|
|
2
|
+
import { useRef, useEffect, useState, useCallback } from "react";
|
|
3
|
+
import type { Message } from "@ai-sdk/react";
|
|
4
|
+
import type React from "react";
|
|
5
|
+
|
|
6
|
+
export type { Message };
|
|
7
|
+
|
|
8
|
+
const RESPONSE_TIMEOUT_MS = 30_000;
|
|
9
|
+
|
|
10
|
+
export interface UseBackboneChatOptions {
|
|
11
|
+
endpoint: string;
|
|
12
|
+
token: string;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
initialMessages?: Message[];
|
|
15
|
+
enableRichContent?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useBackboneChat(options: UseBackboneChatOptions) {
|
|
19
|
+
const [syntheticError, setSyntheticError] = useState<Error | null>(null);
|
|
20
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
21
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
22
|
+
const prevLoadingRef = useRef(false);
|
|
23
|
+
const pendingFilesRef = useRef<File[]>([]);
|
|
24
|
+
|
|
25
|
+
const clearTimer = useCallback(() => {
|
|
26
|
+
if (timeoutRef.current) {
|
|
27
|
+
clearTimeout(timeoutRef.current);
|
|
28
|
+
timeoutRef.current = null;
|
|
29
|
+
}
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const rich = options.enableRichContent !== false;
|
|
33
|
+
const chat = useChat({
|
|
34
|
+
api: `${options.endpoint}/api/v1/ai/conversations/${options.sessionId}/messages?format=datastream${rich ? "&rich=true" : ""}`,
|
|
35
|
+
headers: { Authorization: `Bearer ${options.token}` },
|
|
36
|
+
initialMessages: options.initialMessages,
|
|
37
|
+
fetch: async (url: string | URL | Request, init?: RequestInit) => {
|
|
38
|
+
if (pendingFilesRef.current.length > 0) {
|
|
39
|
+
const files = pendingFilesRef.current;
|
|
40
|
+
pendingFilesRef.current = [];
|
|
41
|
+
|
|
42
|
+
const body = JSON.parse((init?.body as string) || "{}");
|
|
43
|
+
const messages: Array<{ role: string; content: unknown }> = body.messages ?? [];
|
|
44
|
+
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
45
|
+
const messageText = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "";
|
|
46
|
+
|
|
47
|
+
const formData = new FormData();
|
|
48
|
+
if (messageText) formData.append("message", messageText);
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
formData.append("files", file);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const initHeaders = new Headers((init?.headers ?? {}) as HeadersInit);
|
|
54
|
+
initHeaders.delete("Content-Type");
|
|
55
|
+
|
|
56
|
+
const res = await fetch(url, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: initHeaders,
|
|
59
|
+
body: formData,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
let errorMsg = `Erro ${res.status}`;
|
|
64
|
+
try {
|
|
65
|
+
const text = await res.text();
|
|
66
|
+
if (text) errorMsg = text;
|
|
67
|
+
} catch { /* ignore */ }
|
|
68
|
+
throw new Error(errorMsg);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return res;
|
|
72
|
+
}
|
|
73
|
+
return fetch(url, init);
|
|
74
|
+
},
|
|
75
|
+
onError: () => {
|
|
76
|
+
clearTimer();
|
|
77
|
+
setIsUploading(false);
|
|
78
|
+
setSyntheticError(null);
|
|
79
|
+
},
|
|
80
|
+
onFinish: () => {
|
|
81
|
+
clearTimer();
|
|
82
|
+
setIsUploading(false);
|
|
83
|
+
setSyntheticError(null);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const buildAttachmentUrl = useCallback((ref: string) => {
|
|
88
|
+
const base = `${options.endpoint}/api/v1/ai/conversations/${options.sessionId}/attachments/${encodeURIComponent(ref)}`;
|
|
89
|
+
return options.token ? `${base}?token=${encodeURIComponent(options.token)}` : base;
|
|
90
|
+
}, [options.endpoint, options.sessionId, options.token]);
|
|
91
|
+
|
|
92
|
+
const { isLoading, messages, stop } = chat;
|
|
93
|
+
|
|
94
|
+
// --- Camada 2: timeout de resposta ---
|
|
95
|
+
// Inicia timer quando começa a carregar; reseta a cada chunk recebido
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (isLoading) {
|
|
98
|
+
clearTimer();
|
|
99
|
+
timeoutRef.current = setTimeout(() => {
|
|
100
|
+
stop();
|
|
101
|
+
setSyntheticError(new Error("Tempo limite atingido. O servidor nao respondeu."));
|
|
102
|
+
}, RESPONSE_TIMEOUT_MS);
|
|
103
|
+
} else {
|
|
104
|
+
clearTimer();
|
|
105
|
+
}
|
|
106
|
+
return clearTimer;
|
|
107
|
+
}, [isLoading, stop, clearTimer]);
|
|
108
|
+
|
|
109
|
+
// Reset timeout quando mensagens mudam (dados chegando)
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (isLoading && messages.length > 0) {
|
|
112
|
+
clearTimer();
|
|
113
|
+
timeoutRef.current = setTimeout(() => {
|
|
114
|
+
stop();
|
|
115
|
+
setSyntheticError(new Error("Tempo limite atingido. O servidor nao respondeu."));
|
|
116
|
+
}, RESPONSE_TIMEOUT_MS);
|
|
117
|
+
}
|
|
118
|
+
}, [messages, isLoading, stop, clearTimer]);
|
|
119
|
+
|
|
120
|
+
// --- Camada 3: deteccao de resposta vazia ---
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (prevLoadingRef.current && !isLoading) {
|
|
123
|
+
const lastMessage = messages[messages.length - 1];
|
|
124
|
+
if (!chat.error && lastMessage?.role === "user") {
|
|
125
|
+
setSyntheticError(new Error("Nenhuma resposta recebida. Tente novamente."));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
prevLoadingRef.current = isLoading;
|
|
129
|
+
}, [isLoading, messages, chat.error]);
|
|
130
|
+
|
|
131
|
+
const error = chat.error ?? syntheticError;
|
|
132
|
+
|
|
133
|
+
const handleSubmit = useCallback((e: React.FormEvent, attachments?: Array<{ file: File }>) => {
|
|
134
|
+
setSyntheticError(null);
|
|
135
|
+
if (attachments && attachments.length > 0) {
|
|
136
|
+
pendingFilesRef.current = attachments.map((a) => a.file);
|
|
137
|
+
setIsUploading(true);
|
|
138
|
+
}
|
|
139
|
+
return chat.handleSubmit(e as React.FormEvent<HTMLFormElement>);
|
|
140
|
+
}, [chat.handleSubmit]);
|
|
141
|
+
|
|
142
|
+
const reload = useCallback((...args: Parameters<typeof chat.reload>) => {
|
|
143
|
+
setSyntheticError(null);
|
|
144
|
+
return chat.reload(...args);
|
|
145
|
+
}, [chat.reload]);
|
|
146
|
+
|
|
147
|
+
return { ...chat, error, handleSubmit, reload, isUploading, buildAttachmentUrl };
|
|
148
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
export function useIsMobile(breakpoint = 768): boolean {
|
|
4
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
5
|
+
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const mql = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
|
|
8
|
+
const onChange = () => setIsMobile(mql.matches);
|
|
9
|
+
onChange();
|
|
10
|
+
mql.addEventListener("change", onChange);
|
|
11
|
+
return () => mql.removeEventListener("change", onChange);
|
|
12
|
+
}, [breakpoint]);
|
|
13
|
+
|
|
14
|
+
return isMobile;
|
|
15
|
+
}
|