@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,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ArrowLeft, Download, MoreHorizontal, Pencil, Trash2 } from "lucide-react";
|
|
5
|
+
import { Badge } from "../ui/badge.js";
|
|
6
|
+
import { Button } from "../ui/button.js";
|
|
7
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "../ui/dropdown-menu.js";
|
|
8
|
+
import { Skeleton } from "../ui/skeleton.js";
|
|
9
|
+
import { cn } from "../lib/utils.js";
|
|
10
|
+
import { DeleteDialog } from "./DeleteDialog.js";
|
|
11
|
+
import { RenameDialog } from "./RenameDialog.js";
|
|
12
|
+
function ConversationBar({ title, agentLabel, isLoading, onRename, onExport, onDelete, onBack, renameOpen: renameOpenProp, onRenameOpenChange, deleteOpen: deleteOpenProp, onDeleteOpenChange, isPendingRename, isPendingDelete, renameLabel = "Rename", exportLabel = "Export", deleteLabel = "Delete", untitledLabel = "Untitled", actionsExtra, menuItemsExtra, afterBar, className, }) {
|
|
13
|
+
// Internal dialog state for uncontrolled mode
|
|
14
|
+
const [internalRenameOpen, setInternalRenameOpen] = React.useState(false);
|
|
15
|
+
const [internalDeleteOpen, setInternalDeleteOpen] = React.useState(false);
|
|
16
|
+
const [renameValue, setRenameValue] = React.useState("");
|
|
17
|
+
const isControlledRename = renameOpenProp !== undefined && onRenameOpenChange !== undefined;
|
|
18
|
+
const isControlledDelete = deleteOpenProp !== undefined && onDeleteOpenChange !== undefined;
|
|
19
|
+
const renameOpen = isControlledRename ? renameOpenProp : internalRenameOpen;
|
|
20
|
+
const deleteOpen = isControlledDelete ? deleteOpenProp : internalDeleteOpen;
|
|
21
|
+
const setRenameOpen = (open) => {
|
|
22
|
+
if (isControlledRename) {
|
|
23
|
+
onRenameOpenChange(open);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
setInternalRenameOpen(open);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const setDeleteOpen = (open) => {
|
|
30
|
+
if (isControlledDelete) {
|
|
31
|
+
onDeleteOpenChange(open);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
setInternalDeleteOpen(open);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const handleRenameClick = () => {
|
|
38
|
+
setRenameValue(title ?? "");
|
|
39
|
+
setRenameOpen(true);
|
|
40
|
+
};
|
|
41
|
+
const handleRenameConfirm = () => {
|
|
42
|
+
if (renameValue.trim()) {
|
|
43
|
+
onRename?.(renameValue.trim());
|
|
44
|
+
}
|
|
45
|
+
setRenameOpen(false);
|
|
46
|
+
};
|
|
47
|
+
const handleDeleteConfirm = () => {
|
|
48
|
+
onDelete?.();
|
|
49
|
+
setDeleteOpen(false);
|
|
50
|
+
};
|
|
51
|
+
return (_jsxs("div", { className: cn("flex flex-col", className), children: [_jsxs("div", { className: "flex items-center gap-2 border-b bg-background px-3 py-2", children: [onBack && (_jsx(Button, { variant: "ghost", size: "icon", className: "size-8 shrink-0", onClick: onBack, children: _jsx(ArrowLeft, { className: "size-4" }) })), _jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: [isLoading ? (_jsx(Skeleton, { className: "h-5 w-40" })) : (_jsx("span", { className: "truncate text-sm font-medium", children: title ?? untitledLabel })), agentLabel && (_jsx(Badge, { variant: "secondary", className: "shrink-0 text-xs", children: agentLabel }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [actionsExtra, _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "size-8", children: _jsx(MoreHorizontal, { className: "size-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { onClick: handleRenameClick, children: [_jsx(Pencil, { className: "mr-2 size-4" }), renameLabel] }), _jsxs(DropdownMenuItem, { onClick: onExport, children: [_jsx(Download, { className: "mr-2 size-4" }), exportLabel] }), menuItemsExtra, _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onClick: () => setDeleteOpen(true), children: [_jsx(Trash2, { className: "mr-2 size-4" }), deleteLabel] })] })] })] })] }), afterBar, _jsx(RenameDialog, { open: renameOpen, onOpenChange: setRenameOpen, value: renameValue, onValueChange: setRenameValue, onConfirm: handleRenameConfirm, isPending: isPendingRename }), _jsx(DeleteDialog, { open: deleteOpen, onOpenChange: setDeleteOpen, onConfirm: handleDeleteConfirm, isPending: isPendingDelete })] }));
|
|
52
|
+
}
|
|
53
|
+
export { ConversationBar };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { Conversation } from "./types.js";
|
|
3
|
+
interface ConversationListProps {
|
|
4
|
+
conversations: Conversation[];
|
|
5
|
+
activeId?: string;
|
|
6
|
+
isLoading?: boolean;
|
|
7
|
+
search?: string;
|
|
8
|
+
onSearchChange?: (value: string) => void;
|
|
9
|
+
searchPlaceholder?: string;
|
|
10
|
+
favorites?: Conversation[];
|
|
11
|
+
history?: Conversation[];
|
|
12
|
+
favoritesLabel?: string;
|
|
13
|
+
historyLabel?: string;
|
|
14
|
+
hasMore?: boolean;
|
|
15
|
+
onLoadMore?: () => void;
|
|
16
|
+
loadMoreLabel?: string;
|
|
17
|
+
remainingCount?: number;
|
|
18
|
+
onSelect?: (id: string) => void;
|
|
19
|
+
onRename?: (id: string, title: string) => void;
|
|
20
|
+
onStar?: (id: string, starred: boolean) => void;
|
|
21
|
+
onCreateRequest?: () => void;
|
|
22
|
+
getAgentLabel?: (agentId: string) => string;
|
|
23
|
+
headerExtra?: React.ReactNode;
|
|
24
|
+
filterExtra?: React.ReactNode;
|
|
25
|
+
itemBadgesExtra?: (conv: Conversation) => React.ReactNode;
|
|
26
|
+
emptyIcon?: React.ReactNode;
|
|
27
|
+
emptyTitle?: string;
|
|
28
|
+
emptyDescription?: string;
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
declare function ConversationList({ conversations, activeId, isLoading, search, onSearchChange, searchPlaceholder, favorites: favoritesProp, history: historyProp, favoritesLabel, historyLabel, hasMore, onLoadMore, loadMoreLabel, remainingCount, onSelect, onRename, onStar, onCreateRequest, getAgentLabel, headerExtra, filterExtra, itemBadgesExtra, emptyIcon, emptyTitle, emptyDescription, className, }: ConversationListProps): import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
export { ConversationList };
|
|
33
|
+
export type { ConversationListProps };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { MessageSquare, Plus, Search } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
import { Button } from "../ui/button.js";
|
|
7
|
+
import { Input } from "../ui/input.js";
|
|
8
|
+
import { ScrollArea } from "../ui/scroll-area.js";
|
|
9
|
+
import { Skeleton } from "../ui/skeleton.js";
|
|
10
|
+
import { CollapsibleGroup } from "./CollapsibleGroup.js";
|
|
11
|
+
import { ConversationListItem } from "./ConversationListItem.js";
|
|
12
|
+
import { groupConversations } from "./utils.js";
|
|
13
|
+
function ConversationList({ conversations, activeId, isLoading = false, search = "", onSearchChange, searchPlaceholder = "Search...", favorites: favoritesProp, history: historyProp, favoritesLabel = "Favorites", historyLabel = "History", hasMore = false, onLoadMore, loadMoreLabel = "Load more", remainingCount, onSelect, onRename, onStar, onCreateRequest, getAgentLabel, headerExtra, filterExtra, itemBadgesExtra, emptyIcon, emptyTitle = "No conversations", emptyDescription = "Start a conversation to begin.", className, }) {
|
|
14
|
+
const [renamingId, setRenamingId] = React.useState(null);
|
|
15
|
+
const [renameValue, setRenameValue] = React.useState("");
|
|
16
|
+
const [favoritesOpen, setFavoritesOpen] = React.useState(true);
|
|
17
|
+
const [historyOpen, setHistoryOpen] = React.useState(true);
|
|
18
|
+
const derived = React.useMemo(() => groupConversations(conversations), [conversations]);
|
|
19
|
+
const favorites = favoritesProp ?? derived.favorites;
|
|
20
|
+
const history = historyProp ?? derived.history;
|
|
21
|
+
function handleStartRename(conv) {
|
|
22
|
+
return (e) => {
|
|
23
|
+
e.stopPropagation();
|
|
24
|
+
setRenamingId(conv.id);
|
|
25
|
+
setRenameValue(conv.title ?? "");
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function handleRenameCommit(id) {
|
|
29
|
+
if (renameValue.trim()) {
|
|
30
|
+
onRename?.(id, renameValue.trim());
|
|
31
|
+
}
|
|
32
|
+
setRenamingId(null);
|
|
33
|
+
setRenameValue("");
|
|
34
|
+
}
|
|
35
|
+
function handleRenameCancel() {
|
|
36
|
+
setRenamingId(null);
|
|
37
|
+
setRenameValue("");
|
|
38
|
+
}
|
|
39
|
+
function renderItem(conv) {
|
|
40
|
+
return (_jsx(ConversationListItem, { conversation: conv, agentLabel: getAgentLabel?.(conv.agentId), isActive: conv.id === activeId, isRenaming: renamingId === conv.id, renameValue: renameValue, onRenameChange: setRenameValue, onRenameCommit: () => handleRenameCommit(conv.id), onRenameCancel: handleRenameCancel, onStartRename: handleStartRename(conv), onToggleStar: (e) => {
|
|
41
|
+
e.stopPropagation();
|
|
42
|
+
onStar?.(conv.id, !conv.starred);
|
|
43
|
+
}, onClick: () => onSelect?.(conv.id), badgesExtra: itemBadgesExtra?.(conv) }, conv.id));
|
|
44
|
+
}
|
|
45
|
+
const isEmpty = !isLoading && conversations.length === 0;
|
|
46
|
+
return (_jsxs("div", { className: cn("flex h-full flex-col", className), children: [_jsxs("div", { className: "flex items-center gap-1 px-2 py-2", children: [_jsxs("div", { className: "relative flex-1", children: [_jsx(Search, { className: "absolute left-2 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { className: "h-8 pl-7 text-sm", placeholder: searchPlaceholder, value: search, onChange: (e) => onSearchChange?.(e.target.value) })] }), headerExtra, _jsx(Button, { variant: "ghost", size: "icon", className: "size-8 shrink-0", onClick: onCreateRequest, "aria-label": "New conversation", children: _jsx(Plus, { className: "size-4" }) })] }), filterExtra && _jsx("div", { className: "px-2 pb-2", children: filterExtra }), _jsx(ScrollArea, { className: "flex-1", children: _jsxs("div", { className: "px-2 pb-2", children: [isLoading && (_jsx("div", { className: "flex flex-col gap-2 pt-1", children: Array.from({ length: 8 }).map((_, i) => (_jsx(Skeleton, { className: "h-16 w-full rounded-lg" }, i))) })), isEmpty && (_jsxs("div", { className: "flex flex-col items-center justify-center gap-2 py-12 text-center", children: [emptyIcon ?? _jsx(MessageSquare, { className: "size-10 text-muted-foreground/50" }), _jsx("p", { className: "text-sm font-medium text-foreground", children: emptyTitle }), _jsx("p", { className: "text-xs text-muted-foreground", children: emptyDescription })] })), !isLoading && !isEmpty && (_jsxs(_Fragment, { children: [favorites.length > 0 && (_jsx(CollapsibleGroup, { label: favoritesLabel, open: favoritesOpen, onToggle: () => setFavoritesOpen((v) => !v), children: _jsx("div", { className: "mt-0.5 flex flex-col gap-0.5", children: favorites.map(renderItem) }) })), history.length > 0 && (_jsxs(CollapsibleGroup, { label: historyLabel, open: historyOpen, onToggle: () => setHistoryOpen((v) => !v), children: [_jsx("div", { className: "mt-0.5 flex flex-col gap-0.5", children: history.map(renderItem) }), hasMore && (_jsxs("button", { className: "mt-1 w-full rounded-md py-1.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground", onClick: onLoadMore, children: [loadMoreLabel, remainingCount != null && remainingCount > 0 && (_jsxs("span", { className: "ml-1 text-muted-foreground", children: ["(", remainingCount, " remaining)"] }))] }))] }))] }))] }) })] }));
|
|
47
|
+
}
|
|
48
|
+
export { ConversationList };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { Conversation } from "./types.js";
|
|
3
|
+
interface ConversationListItemProps {
|
|
4
|
+
conversation: Conversation;
|
|
5
|
+
agentLabel?: string;
|
|
6
|
+
isActive?: boolean;
|
|
7
|
+
isRenaming?: boolean;
|
|
8
|
+
renameValue?: string;
|
|
9
|
+
onRenameChange?: (value: string) => void;
|
|
10
|
+
onRenameCommit?: () => void;
|
|
11
|
+
onRenameCancel?: () => void;
|
|
12
|
+
onStartRename?: (e: React.MouseEvent) => void;
|
|
13
|
+
onToggleStar?: (e: React.MouseEvent) => void;
|
|
14
|
+
onClick?: () => void;
|
|
15
|
+
badgesExtra?: React.ReactNode;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
declare function ConversationListItem({ conversation, agentLabel, isActive, isRenaming, renameValue, onRenameChange, onRenameCommit, onRenameCancel, onStartRename, onToggleStar, onClick, badgesExtra, className, }: ConversationListItemProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export { ConversationListItem };
|
|
20
|
+
export type { ConversationListItemProps };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Pencil, Star } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
import { Input } from "../ui/input.js";
|
|
6
|
+
import { formatRelativeTime } from "./utils.js";
|
|
7
|
+
function ConversationListItem({ conversation, agentLabel, isActive, isRenaming, renameValue = "", onRenameChange, onRenameCommit, onRenameCancel, onStartRename, onToggleStar, onClick, badgesExtra, className, }) {
|
|
8
|
+
const handleRenameKeyDown = (e) => {
|
|
9
|
+
if (e.key === "Enter") {
|
|
10
|
+
e.preventDefault();
|
|
11
|
+
onRenameCommit?.();
|
|
12
|
+
}
|
|
13
|
+
else if (e.key === "Escape") {
|
|
14
|
+
e.preventDefault();
|
|
15
|
+
onRenameCancel?.();
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
return (_jsxs("div", { className: cn("group relative flex items-center gap-1 rounded-md px-1 py-1 hover:bg-accent", isActive && "bg-accent", className), children: [_jsx("button", { className: cn("shrink-0 rounded p-0.5 transition-colors", conversation.starred
|
|
19
|
+
? "text-yellow-500 hover:text-yellow-400"
|
|
20
|
+
: "text-muted-foreground opacity-0 group-hover:opacity-100 hover:text-yellow-500"), onClick: onToggleStar, "aria-label": conversation.starred ? "Unstar conversation" : "Star conversation", tabIndex: -1, children: _jsx(Star, { className: cn("size-3.5", conversation.starred && "fill-yellow-500") }) }), isRenaming ? (_jsx(Input, { className: "h-6 flex-1 px-1 py-0 text-sm", value: renameValue, onChange: (e) => onRenameChange?.(e.target.value), onKeyDown: handleRenameKeyDown, onBlur: onRenameCancel, autoFocus: true })) : (_jsxs("button", { className: "flex min-w-0 flex-1 flex-col items-start text-left", onClick: onClick, children: [_jsxs("div", { className: "flex w-full items-center gap-1", children: [agentLabel && (_jsx("span", { className: "shrink-0 rounded bg-secondary px-1 py-0 text-[10px] text-secondary-foreground", children: agentLabel })), badgesExtra, _jsx("span", { className: "ml-auto shrink-0 text-[10px] text-muted-foreground", children: formatRelativeTime(conversation.updatedAt) })] }), _jsx("span", { className: "w-full truncate text-sm text-foreground", children: conversation.title ?? "Untitled" })] })), !isRenaming && (_jsx("button", { className: "shrink-0 rounded p-0.5 text-muted-foreground opacity-0 transition-colors group-hover:opacity-100 hover:text-foreground", onClick: onStartRename, "aria-label": "Rename conversation", tabIndex: -1, children: _jsx(Pencil, { className: "size-3.5" }) }))] }));
|
|
21
|
+
}
|
|
22
|
+
export { ConversationListItem };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface DeleteDialogProps {
|
|
2
|
+
open: boolean;
|
|
3
|
+
onOpenChange: (open: boolean) => void;
|
|
4
|
+
onConfirm: () => void;
|
|
5
|
+
isPending?: boolean;
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
cancelLabel?: string;
|
|
9
|
+
confirmLabel?: string;
|
|
10
|
+
}
|
|
11
|
+
declare function DeleteDialog({ open, onOpenChange, onConfirm, isPending, title, description, cancelLabel, confirmLabel, }: DeleteDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
export { DeleteDialog };
|
|
13
|
+
export type { DeleteDialogProps };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button } from "../ui/button.js";
|
|
4
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../ui/dialog.js";
|
|
5
|
+
function DeleteDialog({ open, onOpenChange, onConfirm, isPending, title = "Delete conversation", description = "This conversation will be permanently removed.", cancelLabel = "Cancel", confirmLabel = "Delete", }) {
|
|
6
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-sm", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: description })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: isPending, children: cancelLabel }), _jsx(Button, { variant: "destructive", onClick: onConfirm, disabled: isPending, children: confirmLabel })] })] }) }));
|
|
7
|
+
}
|
|
8
|
+
export { DeleteDialog };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface RenameDialogProps {
|
|
2
|
+
open: boolean;
|
|
3
|
+
onOpenChange: (open: boolean) => void;
|
|
4
|
+
value: string;
|
|
5
|
+
onValueChange: (value: string) => void;
|
|
6
|
+
onConfirm: () => void;
|
|
7
|
+
isPending?: boolean;
|
|
8
|
+
title?: string;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
cancelLabel?: string;
|
|
11
|
+
confirmLabel?: string;
|
|
12
|
+
}
|
|
13
|
+
declare function RenameDialog({ open, onOpenChange, value, onValueChange, onConfirm, isPending, title, placeholder, cancelLabel, confirmLabel, }: RenameDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export { RenameDialog };
|
|
15
|
+
export type { RenameDialogProps };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button } from "../ui/button.js";
|
|
4
|
+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "../ui/dialog.js";
|
|
5
|
+
import { Input } from "../ui/input.js";
|
|
6
|
+
function RenameDialog({ open, onOpenChange, value, onValueChange, onConfirm, isPending, title = "Rename conversation", placeholder = "Conversation title", cancelLabel = "Cancel", confirmLabel = "Save", }) {
|
|
7
|
+
const handleKeyDown = (e) => {
|
|
8
|
+
if (e.key === "Enter" && value.trim() && !isPending) {
|
|
9
|
+
e.preventDefault();
|
|
10
|
+
onConfirm();
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-sm", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: title }) }), _jsx(Input, { value: value, onChange: (e) => onValueChange(e.target.value), onKeyDown: handleKeyDown, placeholder: placeholder, autoFocus: true }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: isPending, children: cancelLabel }), _jsx(Button, { onClick: onConfirm, disabled: !value.trim() || isPending, children: confirmLabel })] })] }) }));
|
|
14
|
+
}
|
|
15
|
+
export { RenameDialog };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { ConversationList } from "./ConversationList.js";
|
|
2
|
+
export type { ConversationListProps } from "./ConversationList.js";
|
|
3
|
+
export { ConversationBar } from "./ConversationBar.js";
|
|
4
|
+
export type { ConversationBarProps } from "./ConversationBar.js";
|
|
5
|
+
export { useConversations } from "./useConversations.js";
|
|
6
|
+
export type { UseConversationsOptions, UseConversationsReturn } from "./useConversations.js";
|
|
7
|
+
export { useIsMobile } from "../hooks/useIsMobile.js";
|
|
8
|
+
export { formatRelativeTime, buildInitialMessages, groupConversations } from "./utils.js";
|
|
9
|
+
export type { Conversation, BackendMessage } from "./types.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ConversationList } from "./ConversationList.js";
|
|
2
|
+
export { ConversationBar } from "./ConversationBar.js";
|
|
3
|
+
export { useConversations } from "./useConversations.js";
|
|
4
|
+
export { useIsMobile } from "../hooks/useIsMobile.js";
|
|
5
|
+
export { formatRelativeTime, buildInitialMessages, groupConversations } from "./utils.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface Conversation {
|
|
2
|
+
id: string;
|
|
3
|
+
title?: string;
|
|
4
|
+
agentId: string;
|
|
5
|
+
updatedAt: string;
|
|
6
|
+
starred: boolean;
|
|
7
|
+
metadata?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface BackendMessage {
|
|
10
|
+
id?: string;
|
|
11
|
+
role: string;
|
|
12
|
+
content: string | unknown[];
|
|
13
|
+
_meta?: {
|
|
14
|
+
id?: string;
|
|
15
|
+
ts?: string;
|
|
16
|
+
userId?: string;
|
|
17
|
+
metadata?: Record<string, unknown>;
|
|
18
|
+
};
|
|
19
|
+
timestamp?: string;
|
|
20
|
+
metadata?: Record<string, unknown>;
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Conversation } from "./types.js";
|
|
2
|
+
export interface UseConversationsOptions {
|
|
3
|
+
endpoint?: string;
|
|
4
|
+
token?: string;
|
|
5
|
+
fetcher?: (url: string, init?: RequestInit) => Promise<Response>;
|
|
6
|
+
autoFetch?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface UseConversationsReturn {
|
|
9
|
+
conversations: Conversation[];
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
error: Error | null;
|
|
12
|
+
refresh: () => Promise<void>;
|
|
13
|
+
create: (agentId: string) => Promise<Conversation>;
|
|
14
|
+
rename: (id: string, title: string) => Promise<void>;
|
|
15
|
+
star: (id: string, starred: boolean) => Promise<void>;
|
|
16
|
+
remove: (id: string) => Promise<void>;
|
|
17
|
+
exportUrl: (id: string, format?: "json" | "markdown") => string;
|
|
18
|
+
}
|
|
19
|
+
export declare function useConversations(options?: UseConversationsOptions): UseConversationsReturn;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from "react";
|
|
2
|
+
export function useConversations(options = {}) {
|
|
3
|
+
const { endpoint = "", token, fetcher, autoFetch = true } = options;
|
|
4
|
+
const [conversations, setConversations] = useState([]);
|
|
5
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
6
|
+
const [error, setError] = useState(null);
|
|
7
|
+
const doFetch = useCallback((url, init) => {
|
|
8
|
+
const fn = fetcher ?? fetch;
|
|
9
|
+
const headers = {
|
|
10
|
+
"Content-Type": "application/json",
|
|
11
|
+
...init?.headers,
|
|
12
|
+
};
|
|
13
|
+
if (token) {
|
|
14
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
15
|
+
}
|
|
16
|
+
return fn(url, { ...init, headers });
|
|
17
|
+
}, [fetcher, token]);
|
|
18
|
+
const refresh = useCallback(async () => {
|
|
19
|
+
setIsLoading(true);
|
|
20
|
+
setError(null);
|
|
21
|
+
try {
|
|
22
|
+
const res = await doFetch(`${endpoint}/api/v1/ai/conversations`);
|
|
23
|
+
if (!res.ok)
|
|
24
|
+
throw new Error(`Failed to fetch conversations: ${res.status}`);
|
|
25
|
+
const data = (await res.json());
|
|
26
|
+
setConversations(data);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
setIsLoading(false);
|
|
33
|
+
}
|
|
34
|
+
}, [doFetch, endpoint]);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (autoFetch) {
|
|
37
|
+
void refresh();
|
|
38
|
+
}
|
|
39
|
+
}, [autoFetch, refresh]);
|
|
40
|
+
const create = useCallback(async (agentId) => {
|
|
41
|
+
const res = await doFetch(`${endpoint}/api/v1/ai/conversations`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
body: JSON.stringify({ agentId }),
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok)
|
|
46
|
+
throw new Error(`Failed to create conversation: ${res.status}`);
|
|
47
|
+
const created = (await res.json());
|
|
48
|
+
setConversations((prev) => [created, ...prev]);
|
|
49
|
+
return created;
|
|
50
|
+
}, [doFetch, endpoint]);
|
|
51
|
+
const rename = useCallback(async (id, title) => {
|
|
52
|
+
const res = await doFetch(`${endpoint}/api/v1/ai/conversations/${id}`, {
|
|
53
|
+
method: "PATCH",
|
|
54
|
+
body: JSON.stringify({ title }),
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok)
|
|
57
|
+
throw new Error(`Failed to rename conversation: ${res.status}`);
|
|
58
|
+
setConversations((prev) => prev.map((c) => (c.id === id ? { ...c, title } : c)));
|
|
59
|
+
}, [doFetch, endpoint]);
|
|
60
|
+
const star = useCallback(async (id, starred) => {
|
|
61
|
+
// Optimistic update
|
|
62
|
+
setConversations((prev) => prev.map((c) => (c.id === id ? { ...c, starred } : c)));
|
|
63
|
+
try {
|
|
64
|
+
const res = await doFetch(`${endpoint}/api/v1/ai/conversations/${id}`, {
|
|
65
|
+
method: "PATCH",
|
|
66
|
+
body: JSON.stringify({ starred }),
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok)
|
|
69
|
+
throw new Error(`Failed to star conversation: ${res.status}`);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
// Rollback on error
|
|
73
|
+
setConversations((prev) => prev.map((c) => (c.id === id ? { ...c, starred: !starred } : c)));
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
}, [doFetch, endpoint]);
|
|
77
|
+
const remove = useCallback(async (id) => {
|
|
78
|
+
const res = await doFetch(`${endpoint}/api/v1/ai/conversations/${id}`, {
|
|
79
|
+
method: "DELETE",
|
|
80
|
+
});
|
|
81
|
+
if (!res.ok)
|
|
82
|
+
throw new Error(`Failed to delete conversation: ${res.status}`);
|
|
83
|
+
setConversations((prev) => prev.filter((c) => c.id !== id));
|
|
84
|
+
}, [doFetch, endpoint]);
|
|
85
|
+
const exportUrl = useCallback((id, format = "json") => {
|
|
86
|
+
const params = new URLSearchParams({ format });
|
|
87
|
+
if (token)
|
|
88
|
+
params.set("token", token);
|
|
89
|
+
return `${endpoint}/api/v1/ai/conversations/${id}/export?${params.toString()}`;
|
|
90
|
+
}, [endpoint, token]);
|
|
91
|
+
return {
|
|
92
|
+
conversations,
|
|
93
|
+
isLoading,
|
|
94
|
+
error,
|
|
95
|
+
refresh,
|
|
96
|
+
create,
|
|
97
|
+
rename,
|
|
98
|
+
star,
|
|
99
|
+
remove,
|
|
100
|
+
exportUrl,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Message } from "@ai-sdk/react";
|
|
2
|
+
import type { BackendMessage, Conversation } from "./types.js";
|
|
3
|
+
export declare function formatRelativeTime(dateStr: string): string;
|
|
4
|
+
export declare function buildInitialMessages(messages?: BackendMessage[]): Message[] | undefined;
|
|
5
|
+
export declare function groupConversations(conversations: Conversation[]): {
|
|
6
|
+
favorites: Conversation[];
|
|
7
|
+
history: Conversation[];
|
|
8
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export function formatRelativeTime(dateStr) {
|
|
2
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
3
|
+
const seconds = Math.floor(diff / 1000);
|
|
4
|
+
if (seconds < 60)
|
|
5
|
+
return "now";
|
|
6
|
+
const minutes = Math.floor(seconds / 60);
|
|
7
|
+
if (minutes < 60)
|
|
8
|
+
return `${minutes}m ago`;
|
|
9
|
+
const hours = Math.floor(minutes / 60);
|
|
10
|
+
if (hours < 24)
|
|
11
|
+
return `${hours}h ago`;
|
|
12
|
+
const days = Math.floor(hours / 24);
|
|
13
|
+
return `${days}d ago`;
|
|
14
|
+
}
|
|
15
|
+
export function buildInitialMessages(messages) {
|
|
16
|
+
if (!messages)
|
|
17
|
+
return undefined;
|
|
18
|
+
// Index tool results by toolCallId
|
|
19
|
+
const toolResults = new Map();
|
|
20
|
+
for (const m of messages) {
|
|
21
|
+
if (m.role === "tool" && Array.isArray(m.content)) {
|
|
22
|
+
for (const part of m.content) {
|
|
23
|
+
if (part.type === "tool-result" && part.toolCallId) {
|
|
24
|
+
const raw = part.output;
|
|
25
|
+
const value = typeof raw === "object" &&
|
|
26
|
+
raw !== null &&
|
|
27
|
+
"type" in raw &&
|
|
28
|
+
raw.type === "json"
|
|
29
|
+
? raw.value
|
|
30
|
+
: raw;
|
|
31
|
+
toolResults.set(part.toolCallId, { toolName: part.toolName ?? "", result: value });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const result = [];
|
|
37
|
+
let i = 0;
|
|
38
|
+
while (i < messages.length) {
|
|
39
|
+
const m = messages[i];
|
|
40
|
+
if (m.role === "user") {
|
|
41
|
+
let content = "";
|
|
42
|
+
const parts = [];
|
|
43
|
+
if (typeof m.content === "string") {
|
|
44
|
+
content = m.content;
|
|
45
|
+
}
|
|
46
|
+
else if (Array.isArray(m.content)) {
|
|
47
|
+
for (const p of m.content) {
|
|
48
|
+
if (p["type"] === "text") {
|
|
49
|
+
const text = String(p["text"] ?? "");
|
|
50
|
+
if (!text.startsWith("[📎") && !content)
|
|
51
|
+
content = text;
|
|
52
|
+
parts.push(p);
|
|
53
|
+
}
|
|
54
|
+
else if (p["type"] === "image" || p["type"] === "file") {
|
|
55
|
+
parts.push({ type: p["type"], _ref: p["_ref"], mimeType: p["mimeType"] });
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
parts.push(p);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
result.push({
|
|
63
|
+
id: m._meta?.id ?? m.id ?? `msg-${i}`,
|
|
64
|
+
role: "user",
|
|
65
|
+
content,
|
|
66
|
+
...(parts.length > 0 ? { parts } : {}),
|
|
67
|
+
});
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (m.role === "tool") {
|
|
72
|
+
i++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (m.role === "assistant") {
|
|
76
|
+
const parts = [];
|
|
77
|
+
let textContent = "";
|
|
78
|
+
const id = m._meta?.id ?? m.id ?? `msg-${i}`;
|
|
79
|
+
while (i < messages.length &&
|
|
80
|
+
(messages[i].role === "assistant" || messages[i].role === "tool")) {
|
|
81
|
+
const cur = messages[i];
|
|
82
|
+
if (cur.role === "tool") {
|
|
83
|
+
i++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (typeof cur.content === "string") {
|
|
87
|
+
if (cur.content) {
|
|
88
|
+
parts.push({ type: "text", text: cur.content });
|
|
89
|
+
textContent += cur.content;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (Array.isArray(cur.content)) {
|
|
93
|
+
for (const p of cur.content) {
|
|
94
|
+
if (p.type === "text" && p.text) {
|
|
95
|
+
parts.push({ type: "text", text: p.text });
|
|
96
|
+
textContent += p.text;
|
|
97
|
+
}
|
|
98
|
+
else if (p.type === "tool-call" && p.toolCallId) {
|
|
99
|
+
const tr = toolResults.get(p.toolCallId);
|
|
100
|
+
parts.push({
|
|
101
|
+
type: "tool-invocation",
|
|
102
|
+
toolInvocation: {
|
|
103
|
+
toolName: p.toolName ?? "",
|
|
104
|
+
toolCallId: p.toolCallId,
|
|
105
|
+
state: tr ? "result" : "call",
|
|
106
|
+
args: p.input,
|
|
107
|
+
result: tr?.result,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
result.push({ id, role: "assistant", content: textContent, parts });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
i++;
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
export function groupConversations(conversations) {
|
|
123
|
+
const favorites = [];
|
|
124
|
+
const history = [];
|
|
125
|
+
for (const conv of conversations) {
|
|
126
|
+
if (conv.starred) {
|
|
127
|
+
favorites.push(conv);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
history.push(conv);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { favorites, history };
|
|
134
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AlertCircle, AlertTriangle, CheckCircle, Info } from "lucide-react";
|
|
3
|
+
import { Alert, AlertDescription, AlertTitle } from "../ui/alert.js";
|
|
4
|
+
const VARIANT_CONFIG = {
|
|
5
|
+
info: { Icon: Info, className: "text-blue-600 dark:text-blue-400 *:[svg]:text-blue-600 dark:*:[svg]:text-blue-400 border-blue-500/50" },
|
|
6
|
+
warning: { Icon: AlertTriangle, className: "text-yellow-600 dark:text-yellow-400 *:[svg]:text-yellow-600 dark:*:[svg]:text-yellow-400 border-yellow-500/50" },
|
|
7
|
+
error: { Icon: AlertCircle },
|
|
8
|
+
success: { Icon: CheckCircle, className: "text-green-600 dark:text-green-400 *:[svg]:text-green-600 dark:*:[svg]:text-green-400 border-green-500/50" },
|
|
9
|
+
};
|
|
10
|
+
export function AlertRenderer({ variant = "info", title, message }) {
|
|
11
|
+
const { Icon, className } = VARIANT_CONFIG[variant];
|
|
12
|
+
return (_jsxs(Alert, { variant: variant === "error" ? "destructive" : "default", className: className, children: [_jsx(Icon, {}), title && _jsx(AlertTitle, { children: title }), _jsx(AlertDescription, { children: message })] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import useEmblaCarousel from "embla-carousel-react";
|
|
3
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
|
+
import { useCallback, useEffect, useState } from "react";
|
|
5
|
+
import { Badge } from "../ui/badge";
|
|
6
|
+
import { Button } from "../ui/button";
|
|
7
|
+
import { Card, CardContent } from "../ui/card";
|
|
8
|
+
import { cn } from "../lib/utils";
|
|
9
|
+
function formatPrice(value, currency) {
|
|
10
|
+
return new Intl.NumberFormat("pt-BR", { style: "currency", currency }).format(value);
|
|
11
|
+
}
|
|
12
|
+
export function CarouselRenderer({ title, items }) {
|
|
13
|
+
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: false, dragFree: false });
|
|
14
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
15
|
+
const [canScrollPrev, setCanScrollPrev] = useState(false);
|
|
16
|
+
const [canScrollNext, setCanScrollNext] = useState(false);
|
|
17
|
+
const onSelect = useCallback(() => {
|
|
18
|
+
if (!emblaApi)
|
|
19
|
+
return;
|
|
20
|
+
setSelectedIndex(emblaApi.selectedScrollSnap());
|
|
21
|
+
setCanScrollPrev(emblaApi.canScrollPrev());
|
|
22
|
+
setCanScrollNext(emblaApi.canScrollNext());
|
|
23
|
+
}, [emblaApi]);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!emblaApi)
|
|
26
|
+
return;
|
|
27
|
+
onSelect();
|
|
28
|
+
emblaApi.on("select", onSelect);
|
|
29
|
+
emblaApi.on("reInit", onSelect);
|
|
30
|
+
return () => {
|
|
31
|
+
emblaApi.off("select", onSelect);
|
|
32
|
+
emblaApi.off("reInit", onSelect);
|
|
33
|
+
};
|
|
34
|
+
}, [emblaApi, onSelect]);
|
|
35
|
+
const scrollPrev = useCallback(() => emblaApi?.scrollPrev(), [emblaApi]);
|
|
36
|
+
const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi]);
|
|
37
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [title && _jsx("p", { className: "text-sm font-medium text-foreground", children: title }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "outline", size: "icon", className: "rounded-full shrink-0", onClick: scrollPrev, disabled: !canScrollPrev, "aria-label": "Anterior", type: "button", children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), _jsx("div", { className: "overflow-hidden flex-1", ref: emblaRef, children: _jsx("div", { className: "flex", children: items.map((item, index) => (_jsx("div", { className: "flex-[0_0_80%] min-w-0 pl-3 first:pl-0", children: item.url ? (_jsx("a", { href: item.url, target: "_blank", rel: "noopener noreferrer", className: "block", children: _jsx(CarouselCard, { item: item }) })) : (_jsx(CarouselCard, { item: item })) }, index))) }) }), _jsx(Button, { variant: "outline", size: "icon", className: "rounded-full shrink-0", onClick: scrollNext, disabled: !canScrollNext, "aria-label": "Pr\u00F3ximo", type: "button", children: _jsx(ChevronRight, { className: "h-4 w-4" }) })] }), items.length > 1 && (_jsx("div", { className: "flex items-center justify-center gap-1.5", role: "tablist", "aria-label": "Slides", children: items.map((_, index) => (_jsx("button", { className: cn("w-2 h-2 rounded-full transition-colors", index === selectedIndex ? "bg-primary" : "bg-muted"), onClick: () => emblaApi?.scrollTo(index), role: "tab", "aria-selected": index === selectedIndex, "aria-label": `Slide ${index + 1}`, type: "button" }, index))) }))] }));
|
|
38
|
+
}
|
|
39
|
+
function CarouselCard({ item }) {
|
|
40
|
+
return (_jsxs(Card, { className: "overflow-hidden", children: [item.image && (_jsx("div", { className: "aspect-video overflow-hidden", children: _jsx("img", { src: item.image, alt: item.title, loading: "lazy", className: "w-full h-full object-cover" }) })), _jsxs(CardContent, { className: "p-3 space-y-1", children: [_jsx("p", { className: "font-medium text-sm text-foreground", children: item.title }), item.subtitle && (_jsx("p", { className: "text-xs text-muted-foreground", children: item.subtitle })), item.price && (_jsx("p", { className: "text-sm font-bold text-foreground", children: formatPrice(item.price.value, item.price.currency) })), item.badges && item.badges.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1 pt-1", children: item.badges.map((badge, i) => (_jsx(Badge, { variant: badge.variant === "destructive" ? "destructive" : badge.variant === "secondary" ? "secondary" : "default", children: badge.label }, i))) }))] })] }));
|
|
41
|
+
}
|