@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,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
+ }