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