@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,139 @@
1
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
2
+ import { useRef, useEffect, useMemo, useCallback } from "react";
3
+ import { useVirtualizer } from "@tanstack/react-virtual";
4
+ import type { Message } from "@ai-sdk/react";
5
+ import { MessageBubble } from "./MessageBubble.js";
6
+ import { StreamingIndicator } from "./StreamingIndicator.js";
7
+ import { ErrorNote } from "./ErrorNote.js";
8
+ import type { DisplayRendererMap } from "../display/registry.js";
9
+ import { ScrollBar } from "../ui/scroll-area.js";
10
+ import { cn } from "../lib/utils.js";
11
+
12
+ export interface MessageListProps {
13
+ messages: Message[];
14
+ isLoading?: boolean;
15
+ displayRenderers?: DisplayRendererMap;
16
+ attachmentUrl?: (ref: string) => string;
17
+ className?: string;
18
+ error?: Error;
19
+ onRetry?: () => void;
20
+ }
21
+
22
+ export function MessageList({ messages, isLoading, displayRenderers, attachmentUrl, className, error, onRetry }: MessageListProps) {
23
+ const viewportRef = useRef<HTMLDivElement>(null);
24
+ const isFollowingRef = useRef(true);
25
+
26
+ const lastAssistantIndex = useMemo(() =>
27
+ messages.reduceRight((found, msg, i) => {
28
+ if (found !== -1) return found;
29
+ return msg.role === "assistant" ? i : -1;
30
+ }, -1),
31
+ [messages]
32
+ );
33
+
34
+ const virtualizer = useVirtualizer({
35
+ count: messages.length,
36
+ getScrollElement: () => viewportRef.current,
37
+ estimateSize: () => 80,
38
+ overscan: 5,
39
+ paddingStart: 16,
40
+ });
41
+
42
+ // Track scroll position to detect if user is following
43
+ const handleScroll = useCallback(() => {
44
+ const viewport = viewportRef.current;
45
+ if (!viewport) return;
46
+ const distanceFromBottom =
47
+ viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
48
+ isFollowingRef.current = distanceFromBottom <= 100;
49
+ }, []);
50
+
51
+ useEffect(() => {
52
+ const viewport = viewportRef.current;
53
+ if (!viewport) return;
54
+ viewport.addEventListener("scroll", handleScroll, { passive: true });
55
+ return () => viewport.removeEventListener("scroll", handleScroll);
56
+ }, [handleScroll]);
57
+
58
+ // Auto-scroll to bottom when following
59
+ useEffect(() => {
60
+ if (messages.length > 0 && isFollowingRef.current) {
61
+ virtualizer.scrollToIndex(messages.length - 1, { align: "end" });
62
+ }
63
+ }, [messages, virtualizer]);
64
+
65
+ // Auto-scroll when error appears
66
+ useEffect(() => {
67
+ if (error && isFollowingRef.current) {
68
+ const viewport = viewportRef.current;
69
+ if (viewport) viewport.scrollTop = viewport.scrollHeight;
70
+ }
71
+ }, [error]);
72
+
73
+ const virtualItems = virtualizer.getVirtualItems();
74
+
75
+ if (messages.length === 0) {
76
+ return (
77
+ <ScrollAreaPrimitive.Root className={cn("flex-1 relative overflow-hidden", className)}>
78
+ <ScrollAreaPrimitive.Viewport ref={viewportRef} className="h-full w-full rounded-[inherit]">
79
+ <div className="flex items-center justify-center text-muted-foreground text-sm py-8">
80
+ Envie uma mensagem para comecar
81
+ </div>
82
+ </ScrollAreaPrimitive.Viewport>
83
+ <ScrollBar />
84
+ <ScrollAreaPrimitive.Corner />
85
+ </ScrollAreaPrimitive.Root>
86
+ );
87
+ }
88
+
89
+ return (
90
+ <ScrollAreaPrimitive.Root className={cn("flex-1 relative overflow-hidden", className)}>
91
+ <ScrollAreaPrimitive.Viewport ref={viewportRef} className="h-full w-full rounded-[inherit]">
92
+ <div
93
+ className="relative w-full"
94
+ style={{ height: virtualizer.getTotalSize() + 16 }}
95
+ >
96
+ <div>
97
+ {virtualItems.map((virtualRow) => {
98
+ const message = messages[virtualRow.index];
99
+ return (
100
+ <div
101
+ key={message.id ?? virtualRow.index}
102
+ data-index={virtualRow.index}
103
+ ref={virtualizer.measureElement}
104
+ className="pb-3 px-4"
105
+ style={{
106
+ position: "absolute",
107
+ top: 0,
108
+ left: 0,
109
+ right: 0,
110
+ transform: `translateY(${virtualRow.start}px)`,
111
+ }}
112
+ >
113
+ <MessageBubble
114
+ message={message}
115
+ isStreaming={virtualRow.index === lastAssistantIndex && isLoading && messages[messages.length - 1]?.role === "assistant"}
116
+ displayRenderers={displayRenderers}
117
+ attachmentUrl={attachmentUrl}
118
+ />
119
+ </div>
120
+ );
121
+ })}
122
+ </div>
123
+ </div>
124
+ {isLoading && messages[messages.length - 1]?.role !== "assistant" && (
125
+ <div className="px-4 pb-3">
126
+ <StreamingIndicator />
127
+ </div>
128
+ )}
129
+ {!isLoading && error && messages.length > 0 && messages[messages.length - 1]?.role !== "assistant" && (
130
+ <div className="px-4 pb-3">
131
+ <ErrorNote onRetry={onRetry} />
132
+ </div>
133
+ )}
134
+ </ScrollAreaPrimitive.Viewport>
135
+ <ScrollBar />
136
+ <ScrollAreaPrimitive.Corner />
137
+ </ScrollAreaPrimitive.Root>
138
+ );
139
+ }
@@ -0,0 +1,19 @@
1
+ export function StreamingIndicator() {
2
+ return (
3
+ <span
4
+ className="inline-flex items-end gap-1.5 py-1 mt-4"
5
+ aria-label="Gerando resposta..."
6
+ role="status"
7
+ >
8
+ <span className="size-1 rounded-full bg-primary animate-[streaming-bounce_1.2s_ease-in-out_infinite_0ms]" />
9
+ <span className="size-1 rounded-full bg-primary animate-[streaming-bounce_1.2s_ease-in-out_infinite_150ms]" />
10
+ <span className="size-1 rounded-full bg-primary animate-[streaming-bounce_1.2s_ease-in-out_infinite_300ms]" />
11
+ <style>{`
12
+ @keyframes streaming-bounce {
13
+ 0%, 60%, 100% { transform: translateY(0); }
14
+ 30% { transform: translateY(-4px); }
15
+ }
16
+ `}</style>
17
+ </span>
18
+ );
19
+ }
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import { ChevronRight } from "lucide-react";
4
+ import * as React from "react";
5
+
6
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
7
+ import { cn } from "../lib/utils.js";
8
+
9
+ interface CollapsibleGroupProps {
10
+ label: string;
11
+ icon?: React.ReactNode;
12
+ open: boolean;
13
+ onToggle: () => void;
14
+ children: React.ReactNode;
15
+ }
16
+
17
+ function CollapsibleGroup({ label, icon, open, onToggle, children }: CollapsibleGroupProps) {
18
+ return (
19
+ <Collapsible open={open} onOpenChange={onToggle}>
20
+ <CollapsibleTrigger asChild>
21
+ <button
22
+ className="flex w-full items-center gap-1 px-2 py-1 hover:bg-accent rounded-md"
23
+ aria-expanded={open}
24
+ >
25
+ <ChevronRight
26
+ className={cn(
27
+ "size-3 shrink-0 text-muted-foreground transition-transform duration-200",
28
+ open && "rotate-90",
29
+ )}
30
+ />
31
+ {icon && <span className="shrink-0">{icon}</span>}
32
+ <span className="text-xs font-medium text-muted-foreground">{label}</span>
33
+ </button>
34
+ </CollapsibleTrigger>
35
+ <CollapsibleContent>{children}</CollapsibleContent>
36
+ </Collapsible>
37
+ );
38
+ }
39
+
40
+ export { CollapsibleGroup };
41
+ export type { CollapsibleGroupProps };
@@ -0,0 +1,200 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ArrowLeft, Download, MoreHorizontal, Pencil, Trash2 } from "lucide-react";
5
+
6
+ import { Badge } from "../ui/badge.js";
7
+ import { Button } from "../ui/button.js";
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuSeparator,
13
+ DropdownMenuTrigger,
14
+ } from "../ui/dropdown-menu.js";
15
+ import { Skeleton } from "../ui/skeleton.js";
16
+ import { cn } from "../lib/utils.js";
17
+ import { DeleteDialog } from "./DeleteDialog.js";
18
+ import { RenameDialog } from "./RenameDialog.js";
19
+
20
+ interface ConversationBarProps {
21
+ // Data
22
+ title?: string;
23
+ agentLabel?: string;
24
+ isLoading?: boolean;
25
+
26
+ // Actions
27
+ onRename?: (title: string) => void;
28
+ onExport?: () => void;
29
+ onDelete?: () => void;
30
+ onBack?: () => void;
31
+
32
+ // Controlled dialog state (optional — uncontrolled if not provided)
33
+ renameOpen?: boolean;
34
+ onRenameOpenChange?: (open: boolean) => void;
35
+ deleteOpen?: boolean;
36
+ onDeleteOpenChange?: (open: boolean) => void;
37
+
38
+ // Pending states
39
+ isPendingRename?: boolean;
40
+ isPendingDelete?: boolean;
41
+
42
+ // Labels
43
+ renameLabel?: string;
44
+ exportLabel?: string;
45
+ deleteLabel?: string;
46
+ untitledLabel?: string;
47
+
48
+ // Extension slots
49
+ actionsExtra?: React.ReactNode;
50
+ menuItemsExtra?: React.ReactNode;
51
+ afterBar?: React.ReactNode;
52
+
53
+ // Layout
54
+ className?: string;
55
+ }
56
+
57
+ function ConversationBar({
58
+ title,
59
+ agentLabel,
60
+ isLoading,
61
+ onRename,
62
+ onExport,
63
+ onDelete,
64
+ onBack,
65
+ renameOpen: renameOpenProp,
66
+ onRenameOpenChange,
67
+ deleteOpen: deleteOpenProp,
68
+ onDeleteOpenChange,
69
+ isPendingRename,
70
+ isPendingDelete,
71
+ renameLabel = "Rename",
72
+ exportLabel = "Export",
73
+ deleteLabel = "Delete",
74
+ untitledLabel = "Untitled",
75
+ actionsExtra,
76
+ menuItemsExtra,
77
+ afterBar,
78
+ className,
79
+ }: ConversationBarProps) {
80
+ // Internal dialog state for uncontrolled mode
81
+ const [internalRenameOpen, setInternalRenameOpen] = React.useState(false);
82
+ const [internalDeleteOpen, setInternalDeleteOpen] = React.useState(false);
83
+ const [renameValue, setRenameValue] = React.useState("");
84
+
85
+ const isControlledRename = renameOpenProp !== undefined && onRenameOpenChange !== undefined;
86
+ const isControlledDelete = deleteOpenProp !== undefined && onDeleteOpenChange !== undefined;
87
+
88
+ const renameOpen = isControlledRename ? renameOpenProp : internalRenameOpen;
89
+ const deleteOpen = isControlledDelete ? deleteOpenProp : internalDeleteOpen;
90
+
91
+ const setRenameOpen = (open: boolean) => {
92
+ if (isControlledRename) {
93
+ onRenameOpenChange(open);
94
+ } else {
95
+ setInternalRenameOpen(open);
96
+ }
97
+ };
98
+
99
+ const setDeleteOpen = (open: boolean) => {
100
+ if (isControlledDelete) {
101
+ onDeleteOpenChange(open);
102
+ } else {
103
+ setInternalDeleteOpen(open);
104
+ }
105
+ };
106
+
107
+ const handleRenameClick = () => {
108
+ setRenameValue(title ?? "");
109
+ setRenameOpen(true);
110
+ };
111
+
112
+ const handleRenameConfirm = () => {
113
+ if (renameValue.trim()) {
114
+ onRename?.(renameValue.trim());
115
+ }
116
+ setRenameOpen(false);
117
+ };
118
+
119
+ const handleDeleteConfirm = () => {
120
+ onDelete?.();
121
+ setDeleteOpen(false);
122
+ };
123
+
124
+ return (
125
+ <div className={cn("flex flex-col", className)}>
126
+ <div className="flex items-center gap-2 border-b bg-background px-3 py-2">
127
+ {onBack && (
128
+ <Button variant="ghost" size="icon" className="size-8 shrink-0" onClick={onBack}>
129
+ <ArrowLeft className="size-4" />
130
+ </Button>
131
+ )}
132
+
133
+ <div className="flex min-w-0 flex-1 items-center gap-2">
134
+ {isLoading ? (
135
+ <Skeleton className="h-5 w-40" />
136
+ ) : (
137
+ <span className="truncate text-sm font-medium">{title ?? untitledLabel}</span>
138
+ )}
139
+ {agentLabel && (
140
+ <Badge variant="secondary" className="shrink-0 text-xs">
141
+ {agentLabel}
142
+ </Badge>
143
+ )}
144
+ </div>
145
+
146
+ <div className="flex shrink-0 items-center gap-1">
147
+ {actionsExtra}
148
+
149
+ <DropdownMenu>
150
+ <DropdownMenuTrigger asChild>
151
+ <Button variant="ghost" size="icon" className="size-8">
152
+ <MoreHorizontal className="size-4" />
153
+ </Button>
154
+ </DropdownMenuTrigger>
155
+ <DropdownMenuContent align="end">
156
+ <DropdownMenuItem onClick={handleRenameClick}>
157
+ <Pencil className="mr-2 size-4" />
158
+ {renameLabel}
159
+ </DropdownMenuItem>
160
+ <DropdownMenuItem onClick={onExport}>
161
+ <Download className="mr-2 size-4" />
162
+ {exportLabel}
163
+ </DropdownMenuItem>
164
+ {menuItemsExtra}
165
+ <DropdownMenuSeparator />
166
+ <DropdownMenuItem
167
+ className="text-destructive focus:text-destructive"
168
+ onClick={() => setDeleteOpen(true)}
169
+ >
170
+ <Trash2 className="mr-2 size-4" />
171
+ {deleteLabel}
172
+ </DropdownMenuItem>
173
+ </DropdownMenuContent>
174
+ </DropdownMenu>
175
+ </div>
176
+ </div>
177
+
178
+ {afterBar}
179
+
180
+ <RenameDialog
181
+ open={renameOpen}
182
+ onOpenChange={setRenameOpen}
183
+ value={renameValue}
184
+ onValueChange={setRenameValue}
185
+ onConfirm={handleRenameConfirm}
186
+ isPending={isPendingRename}
187
+ />
188
+
189
+ <DeleteDialog
190
+ open={deleteOpen}
191
+ onOpenChange={setDeleteOpen}
192
+ onConfirm={handleDeleteConfirm}
193
+ isPending={isPendingDelete}
194
+ />
195
+ </div>
196
+ );
197
+ }
198
+
199
+ export { ConversationBar };
200
+ export type { ConversationBarProps };
@@ -0,0 +1,234 @@
1
+ "use client";
2
+
3
+ import { MessageSquare, Plus, Search } from "lucide-react";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "../lib/utils.js";
7
+ import { Button } from "../ui/button.js";
8
+ import { Input } from "../ui/input.js";
9
+ import { ScrollArea } from "../ui/scroll-area.js";
10
+ import { Skeleton } from "../ui/skeleton.js";
11
+ import { CollapsibleGroup } from "./CollapsibleGroup.js";
12
+ import { ConversationListItem } from "./ConversationListItem.js";
13
+ import type { Conversation } from "./types.js";
14
+ import { groupConversations } from "./utils.js";
15
+
16
+ interface ConversationListProps {
17
+ conversations: Conversation[];
18
+ activeId?: string;
19
+ isLoading?: boolean;
20
+
21
+ search?: string;
22
+ onSearchChange?: (value: string) => void;
23
+ searchPlaceholder?: string;
24
+
25
+ favorites?: Conversation[];
26
+ history?: Conversation[];
27
+ favoritesLabel?: string;
28
+ historyLabel?: string;
29
+
30
+ hasMore?: boolean;
31
+ onLoadMore?: () => void;
32
+ loadMoreLabel?: string;
33
+ remainingCount?: number;
34
+
35
+ onSelect?: (id: string) => void;
36
+ onRename?: (id: string, title: string) => void;
37
+ onStar?: (id: string, starred: boolean) => void;
38
+ onCreateRequest?: () => void;
39
+
40
+ getAgentLabel?: (agentId: string) => string;
41
+
42
+ headerExtra?: React.ReactNode;
43
+ filterExtra?: React.ReactNode;
44
+ itemBadgesExtra?: (conv: Conversation) => React.ReactNode;
45
+
46
+ emptyIcon?: React.ReactNode;
47
+ emptyTitle?: string;
48
+ emptyDescription?: string;
49
+
50
+ className?: string;
51
+ }
52
+
53
+ function ConversationList({
54
+ conversations,
55
+ activeId,
56
+ isLoading = false,
57
+ search = "",
58
+ onSearchChange,
59
+ searchPlaceholder = "Search...",
60
+ favorites: favoritesProp,
61
+ history: historyProp,
62
+ favoritesLabel = "Favorites",
63
+ historyLabel = "History",
64
+ hasMore = false,
65
+ onLoadMore,
66
+ loadMoreLabel = "Load more",
67
+ remainingCount,
68
+ onSelect,
69
+ onRename,
70
+ onStar,
71
+ onCreateRequest,
72
+ getAgentLabel,
73
+ headerExtra,
74
+ filterExtra,
75
+ itemBadgesExtra,
76
+ emptyIcon,
77
+ emptyTitle = "No conversations",
78
+ emptyDescription = "Start a conversation to begin.",
79
+ className,
80
+ }: ConversationListProps) {
81
+ const [renamingId, setRenamingId] = React.useState<string | null>(null);
82
+ const [renameValue, setRenameValue] = React.useState("");
83
+ const [favoritesOpen, setFavoritesOpen] = React.useState(true);
84
+ const [historyOpen, setHistoryOpen] = React.useState(true);
85
+
86
+ const derived = React.useMemo(
87
+ () => groupConversations(conversations),
88
+ [conversations],
89
+ );
90
+ const favorites = favoritesProp ?? derived.favorites;
91
+ const history = historyProp ?? derived.history;
92
+
93
+ function handleStartRename(conv: Conversation) {
94
+ return (e: React.MouseEvent) => {
95
+ e.stopPropagation();
96
+ setRenamingId(conv.id);
97
+ setRenameValue(conv.title ?? "");
98
+ };
99
+ }
100
+
101
+ function handleRenameCommit(id: string) {
102
+ if (renameValue.trim()) {
103
+ onRename?.(id, renameValue.trim());
104
+ }
105
+ setRenamingId(null);
106
+ setRenameValue("");
107
+ }
108
+
109
+ function handleRenameCancel() {
110
+ setRenamingId(null);
111
+ setRenameValue("");
112
+ }
113
+
114
+ function renderItem(conv: Conversation) {
115
+ return (
116
+ <ConversationListItem
117
+ key={conv.id}
118
+ conversation={conv}
119
+ agentLabel={getAgentLabel?.(conv.agentId)}
120
+ isActive={conv.id === activeId}
121
+ isRenaming={renamingId === conv.id}
122
+ renameValue={renameValue}
123
+ onRenameChange={setRenameValue}
124
+ onRenameCommit={() => handleRenameCommit(conv.id)}
125
+ onRenameCancel={handleRenameCancel}
126
+ onStartRename={handleStartRename(conv)}
127
+ onToggleStar={(e) => {
128
+ e.stopPropagation();
129
+ onStar?.(conv.id, !conv.starred);
130
+ }}
131
+ onClick={() => onSelect?.(conv.id)}
132
+ badgesExtra={itemBadgesExtra?.(conv)}
133
+ />
134
+ );
135
+ }
136
+
137
+ const isEmpty = !isLoading && conversations.length === 0;
138
+
139
+ return (
140
+ <div className={cn("flex h-full flex-col", className)}>
141
+ {/* Header row */}
142
+ <div className="flex items-center gap-1 px-2 py-2">
143
+ <div className="relative flex-1">
144
+ <Search className="absolute left-2 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
145
+ <Input
146
+ className="h-8 pl-7 text-sm"
147
+ placeholder={searchPlaceholder}
148
+ value={search}
149
+ onChange={(e) => onSearchChange?.(e.target.value)}
150
+ />
151
+ </div>
152
+ {headerExtra}
153
+ <Button
154
+ variant="ghost"
155
+ size="icon"
156
+ className="size-8 shrink-0"
157
+ onClick={onCreateRequest}
158
+ aria-label="New conversation"
159
+ >
160
+ <Plus className="size-4" />
161
+ </Button>
162
+ </div>
163
+
164
+ {/* Filter row */}
165
+ {filterExtra && <div className="px-2 pb-2">{filterExtra}</div>}
166
+
167
+ {/* Content */}
168
+ <ScrollArea className="flex-1">
169
+ <div className="px-2 pb-2">
170
+ {isLoading && (
171
+ <div className="flex flex-col gap-2 pt-1">
172
+ {Array.from({ length: 8 }).map((_, i) => (
173
+ <Skeleton key={i} className="h-16 w-full rounded-lg" />
174
+ ))}
175
+ </div>
176
+ )}
177
+
178
+ {isEmpty && (
179
+ <div className="flex flex-col items-center justify-center gap-2 py-12 text-center">
180
+ {emptyIcon ?? <MessageSquare className="size-10 text-muted-foreground/50" />}
181
+ <p className="text-sm font-medium text-foreground">{emptyTitle}</p>
182
+ <p className="text-xs text-muted-foreground">{emptyDescription}</p>
183
+ </div>
184
+ )}
185
+
186
+ {!isLoading && !isEmpty && (
187
+ <>
188
+ {favorites.length > 0 && (
189
+ <CollapsibleGroup
190
+ label={favoritesLabel}
191
+ open={favoritesOpen}
192
+ onToggle={() => setFavoritesOpen((v) => !v)}
193
+ >
194
+ <div className="mt-0.5 flex flex-col gap-0.5">
195
+ {favorites.map(renderItem)}
196
+ </div>
197
+ </CollapsibleGroup>
198
+ )}
199
+
200
+ {history.length > 0 && (
201
+ <CollapsibleGroup
202
+ label={historyLabel}
203
+ open={historyOpen}
204
+ onToggle={() => setHistoryOpen((v) => !v)}
205
+ >
206
+ <div className="mt-0.5 flex flex-col gap-0.5">
207
+ {history.map(renderItem)}
208
+ </div>
209
+
210
+ {hasMore && (
211
+ <button
212
+ className="mt-1 w-full rounded-md py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground"
213
+ onClick={onLoadMore}
214
+ >
215
+ {loadMoreLabel}
216
+ {remainingCount != null && remainingCount > 0 && (
217
+ <span className="ml-1 text-muted-foreground">
218
+ ({remainingCount} remaining)
219
+ </span>
220
+ )}
221
+ </button>
222
+ )}
223
+ </CollapsibleGroup>
224
+ )}
225
+ </>
226
+ )}
227
+ </div>
228
+ </ScrollArea>
229
+ </div>
230
+ );
231
+ }
232
+
233
+ export { ConversationList };
234
+ export type { ConversationListProps };