@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,123 @@
1
+ "use client";
2
+
3
+ import { Pencil, Star } from "lucide-react";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "../lib/utils.js";
7
+ import { Input } from "../ui/input.js";
8
+ import { formatRelativeTime } from "./utils.js";
9
+ import type { Conversation } from "./types.js";
10
+
11
+ interface ConversationListItemProps {
12
+ conversation: Conversation;
13
+ agentLabel?: string;
14
+ isActive?: boolean;
15
+ isRenaming?: boolean;
16
+ renameValue?: string;
17
+ onRenameChange?: (value: string) => void;
18
+ onRenameCommit?: () => void;
19
+ onRenameCancel?: () => void;
20
+ onStartRename?: (e: React.MouseEvent) => void;
21
+ onToggleStar?: (e: React.MouseEvent) => void;
22
+ onClick?: () => void;
23
+ badgesExtra?: React.ReactNode;
24
+ className?: string;
25
+ }
26
+
27
+ function ConversationListItem({
28
+ conversation,
29
+ agentLabel,
30
+ isActive,
31
+ isRenaming,
32
+ renameValue = "",
33
+ onRenameChange,
34
+ onRenameCommit,
35
+ onRenameCancel,
36
+ onStartRename,
37
+ onToggleStar,
38
+ onClick,
39
+ badgesExtra,
40
+ className,
41
+ }: ConversationListItemProps) {
42
+ const handleRenameKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
43
+ if (e.key === "Enter") {
44
+ e.preventDefault();
45
+ onRenameCommit?.();
46
+ } else if (e.key === "Escape") {
47
+ e.preventDefault();
48
+ onRenameCancel?.();
49
+ }
50
+ };
51
+
52
+ return (
53
+ <div
54
+ className={cn(
55
+ "group relative flex items-center gap-1 rounded-md px-1 py-1 hover:bg-accent",
56
+ isActive && "bg-accent",
57
+ className,
58
+ )}
59
+ >
60
+ {/* Star icon */}
61
+ <button
62
+ className={cn(
63
+ "shrink-0 rounded p-0.5 transition-colors",
64
+ conversation.starred
65
+ ? "text-yellow-500 hover:text-yellow-400"
66
+ : "text-muted-foreground opacity-0 group-hover:opacity-100 hover:text-yellow-500",
67
+ )}
68
+ onClick={onToggleStar}
69
+ aria-label={conversation.starred ? "Unstar conversation" : "Star conversation"}
70
+ tabIndex={-1}
71
+ >
72
+ <Star className={cn("size-3.5", conversation.starred && "fill-yellow-500")} />
73
+ </button>
74
+
75
+ {/* Main clickable area */}
76
+ {isRenaming ? (
77
+ <Input
78
+ className="h-6 flex-1 px-1 py-0 text-sm"
79
+ value={renameValue}
80
+ onChange={(e) => onRenameChange?.(e.target.value)}
81
+ onKeyDown={handleRenameKeyDown}
82
+ onBlur={onRenameCancel}
83
+ autoFocus
84
+ />
85
+ ) : (
86
+ <button
87
+ className="flex min-w-0 flex-1 flex-col items-start text-left"
88
+ onClick={onClick}
89
+ >
90
+ <div className="flex w-full items-center gap-1">
91
+ {agentLabel && (
92
+ <span className="shrink-0 rounded bg-secondary px-1 py-0 text-[10px] text-secondary-foreground">
93
+ {agentLabel}
94
+ </span>
95
+ )}
96
+ {badgesExtra}
97
+ <span className="ml-auto shrink-0 text-[10px] text-muted-foreground">
98
+ {formatRelativeTime(conversation.updatedAt)}
99
+ </span>
100
+ </div>
101
+ <span className="w-full truncate text-sm text-foreground">
102
+ {conversation.title ?? "Untitled"}
103
+ </span>
104
+ </button>
105
+ )}
106
+
107
+ {/* Pencil icon (hover) */}
108
+ {!isRenaming && (
109
+ <button
110
+ className="shrink-0 rounded p-0.5 text-muted-foreground opacity-0 transition-colors group-hover:opacity-100 hover:text-foreground"
111
+ onClick={onStartRename}
112
+ aria-label="Rename conversation"
113
+ tabIndex={-1}
114
+ >
115
+ <Pencil className="size-3.5" />
116
+ </button>
117
+ )}
118
+ </div>
119
+ );
120
+ }
121
+
122
+ export { ConversationListItem };
123
+ export type { ConversationListItemProps };
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import { Button } from "../ui/button.js";
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ } from "../ui/dialog.js";
12
+
13
+ interface DeleteDialogProps {
14
+ open: boolean;
15
+ onOpenChange: (open: boolean) => void;
16
+ onConfirm: () => void;
17
+ isPending?: boolean;
18
+ title?: string;
19
+ description?: string;
20
+ cancelLabel?: string;
21
+ confirmLabel?: string;
22
+ }
23
+
24
+ function DeleteDialog({
25
+ open,
26
+ onOpenChange,
27
+ onConfirm,
28
+ isPending,
29
+ title = "Delete conversation",
30
+ description = "This conversation will be permanently removed.",
31
+ cancelLabel = "Cancel",
32
+ confirmLabel = "Delete",
33
+ }: DeleteDialogProps) {
34
+ return (
35
+ <Dialog open={open} onOpenChange={onOpenChange}>
36
+ <DialogContent className="sm:max-w-sm">
37
+ <DialogHeader>
38
+ <DialogTitle>{title}</DialogTitle>
39
+ <DialogDescription>{description}</DialogDescription>
40
+ </DialogHeader>
41
+ <DialogFooter>
42
+ <Button variant="outline" onClick={() => onOpenChange(false)} disabled={isPending}>
43
+ {cancelLabel}
44
+ </Button>
45
+ <Button variant="destructive" onClick={onConfirm} disabled={isPending}>
46
+ {confirmLabel}
47
+ </Button>
48
+ </DialogFooter>
49
+ </DialogContent>
50
+ </Dialog>
51
+ );
52
+ }
53
+
54
+ export { DeleteDialog };
55
+ export type { DeleteDialogProps };
@@ -0,0 +1,74 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import { Button } from "../ui/button.js";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ } from "../ui/dialog.js";
13
+ import { Input } from "../ui/input.js";
14
+
15
+ interface RenameDialogProps {
16
+ open: boolean;
17
+ onOpenChange: (open: boolean) => void;
18
+ value: string;
19
+ onValueChange: (value: string) => void;
20
+ onConfirm: () => void;
21
+ isPending?: boolean;
22
+ title?: string;
23
+ placeholder?: string;
24
+ cancelLabel?: string;
25
+ confirmLabel?: string;
26
+ }
27
+
28
+ function RenameDialog({
29
+ open,
30
+ onOpenChange,
31
+ value,
32
+ onValueChange,
33
+ onConfirm,
34
+ isPending,
35
+ title = "Rename conversation",
36
+ placeholder = "Conversation title",
37
+ cancelLabel = "Cancel",
38
+ confirmLabel = "Save",
39
+ }: RenameDialogProps) {
40
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
41
+ if (e.key === "Enter" && value.trim() && !isPending) {
42
+ e.preventDefault();
43
+ onConfirm();
44
+ }
45
+ };
46
+
47
+ return (
48
+ <Dialog open={open} onOpenChange={onOpenChange}>
49
+ <DialogContent className="sm:max-w-sm">
50
+ <DialogHeader>
51
+ <DialogTitle>{title}</DialogTitle>
52
+ </DialogHeader>
53
+ <Input
54
+ value={value}
55
+ onChange={(e) => onValueChange(e.target.value)}
56
+ onKeyDown={handleKeyDown}
57
+ placeholder={placeholder}
58
+ autoFocus
59
+ />
60
+ <DialogFooter>
61
+ <Button variant="outline" onClick={() => onOpenChange(false)} disabled={isPending}>
62
+ {cancelLabel}
63
+ </Button>
64
+ <Button onClick={onConfirm} disabled={!value.trim() || isPending}>
65
+ {confirmLabel}
66
+ </Button>
67
+ </DialogFooter>
68
+ </DialogContent>
69
+ </Dialog>
70
+ );
71
+ }
72
+
73
+ export { RenameDialog };
74
+ export type { RenameDialogProps };
@@ -0,0 +1,14 @@
1
+ export { ConversationList } from "./ConversationList.js";
2
+ export type { ConversationListProps } from "./ConversationList.js";
3
+
4
+ export { ConversationBar } from "./ConversationBar.js";
5
+ export type { ConversationBarProps } from "./ConversationBar.js";
6
+
7
+ export { useConversations } from "./useConversations.js";
8
+ export type { UseConversationsOptions, UseConversationsReturn } from "./useConversations.js";
9
+
10
+ export { useIsMobile } from "../hooks/useIsMobile.js";
11
+
12
+ export { formatRelativeTime, buildInitialMessages, groupConversations } from "./utils.js";
13
+
14
+ export type { Conversation, BackendMessage } from "./types.js";
@@ -0,0 +1,17 @@
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
+
10
+ export interface BackendMessage {
11
+ id?: string;
12
+ role: string;
13
+ content: string | unknown[];
14
+ _meta?: { id?: string; ts?: string; userId?: string; metadata?: Record<string, unknown> };
15
+ timestamp?: string;
16
+ metadata?: Record<string, unknown>;
17
+ }
@@ -0,0 +1,148 @@
1
+ import { useState, useCallback, useEffect } from "react";
2
+ import type { Conversation } from "./types.js";
3
+
4
+ export interface UseConversationsOptions {
5
+ endpoint?: string;
6
+ token?: string;
7
+ fetcher?: (url: string, init?: RequestInit) => Promise<Response>;
8
+ autoFetch?: boolean;
9
+ }
10
+
11
+ export interface UseConversationsReturn {
12
+ conversations: Conversation[];
13
+ isLoading: boolean;
14
+ error: Error | null;
15
+ refresh: () => Promise<void>;
16
+ create: (agentId: string) => Promise<Conversation>;
17
+ rename: (id: string, title: string) => Promise<void>;
18
+ star: (id: string, starred: boolean) => Promise<void>;
19
+ remove: (id: string) => Promise<void>;
20
+ exportUrl: (id: string, format?: "json" | "markdown") => string;
21
+ }
22
+
23
+ export function useConversations(options: UseConversationsOptions = {}): UseConversationsReturn {
24
+ const { endpoint = "", token, fetcher, autoFetch = true } = options;
25
+
26
+ const [conversations, setConversations] = useState<Conversation[]>([]);
27
+ const [isLoading, setIsLoading] = useState(false);
28
+ const [error, setError] = useState<Error | null>(null);
29
+
30
+ const doFetch = useCallback(
31
+ (url: string, init?: RequestInit) => {
32
+ const fn = fetcher ?? fetch;
33
+ const headers: Record<string, string> = {
34
+ "Content-Type": "application/json",
35
+ ...(init?.headers as Record<string, string> | undefined),
36
+ };
37
+ if (token) {
38
+ headers["Authorization"] = `Bearer ${token}`;
39
+ }
40
+ return fn(url, { ...init, headers });
41
+ },
42
+ [fetcher, token],
43
+ );
44
+
45
+ const refresh = useCallback(async () => {
46
+ setIsLoading(true);
47
+ setError(null);
48
+ try {
49
+ const res = await doFetch(`${endpoint}/api/v1/ai/conversations`);
50
+ if (!res.ok) throw new Error(`Failed to fetch conversations: ${res.status}`);
51
+ const data = (await res.json()) as Conversation[];
52
+ setConversations(data);
53
+ } catch (err) {
54
+ setError(err instanceof Error ? err : new Error(String(err)));
55
+ } finally {
56
+ setIsLoading(false);
57
+ }
58
+ }, [doFetch, endpoint]);
59
+
60
+ useEffect(() => {
61
+ if (autoFetch) {
62
+ void refresh();
63
+ }
64
+ }, [autoFetch, refresh]);
65
+
66
+ const create = useCallback(
67
+ async (agentId: string): Promise<Conversation> => {
68
+ const res = await doFetch(`${endpoint}/api/v1/ai/conversations`, {
69
+ method: "POST",
70
+ body: JSON.stringify({ agentId }),
71
+ });
72
+ if (!res.ok) throw new Error(`Failed to create conversation: ${res.status}`);
73
+ const created = (await res.json()) as Conversation;
74
+ setConversations((prev) => [created, ...prev]);
75
+ return created;
76
+ },
77
+ [doFetch, endpoint],
78
+ );
79
+
80
+ const rename = useCallback(
81
+ async (id: string, title: string): Promise<void> => {
82
+ const res = await doFetch(`${endpoint}/api/v1/ai/conversations/${id}`, {
83
+ method: "PATCH",
84
+ body: JSON.stringify({ title }),
85
+ });
86
+ if (!res.ok) throw new Error(`Failed to rename conversation: ${res.status}`);
87
+ setConversations((prev) =>
88
+ prev.map((c) => (c.id === id ? { ...c, title } : c)),
89
+ );
90
+ },
91
+ [doFetch, endpoint],
92
+ );
93
+
94
+ const star = useCallback(
95
+ async (id: string, starred: boolean): Promise<void> => {
96
+ // Optimistic update
97
+ setConversations((prev) =>
98
+ prev.map((c) => (c.id === id ? { ...c, starred } : c)),
99
+ );
100
+ try {
101
+ const res = await doFetch(`${endpoint}/api/v1/ai/conversations/${id}`, {
102
+ method: "PATCH",
103
+ body: JSON.stringify({ starred }),
104
+ });
105
+ if (!res.ok) throw new Error(`Failed to star conversation: ${res.status}`);
106
+ } catch (err) {
107
+ // Rollback on error
108
+ setConversations((prev) =>
109
+ prev.map((c) => (c.id === id ? { ...c, starred: !starred } : c)),
110
+ );
111
+ throw err;
112
+ }
113
+ },
114
+ [doFetch, endpoint],
115
+ );
116
+
117
+ const remove = useCallback(
118
+ async (id: string): Promise<void> => {
119
+ const res = await doFetch(`${endpoint}/api/v1/ai/conversations/${id}`, {
120
+ method: "DELETE",
121
+ });
122
+ if (!res.ok) throw new Error(`Failed to delete conversation: ${res.status}`);
123
+ setConversations((prev) => prev.filter((c) => c.id !== id));
124
+ },
125
+ [doFetch, endpoint],
126
+ );
127
+
128
+ const exportUrl = useCallback(
129
+ (id: string, format: "json" | "markdown" = "json"): string => {
130
+ const params = new URLSearchParams({ format });
131
+ if (token) params.set("token", token);
132
+ return `${endpoint}/api/v1/ai/conversations/${id}/export?${params.toString()}`;
133
+ },
134
+ [endpoint, token],
135
+ );
136
+
137
+ return {
138
+ conversations,
139
+ isLoading,
140
+ error,
141
+ refresh,
142
+ create,
143
+ rename,
144
+ star,
145
+ remove,
146
+ exportUrl,
147
+ };
148
+ }
@@ -0,0 +1,159 @@
1
+ import type { Message } from "@ai-sdk/react";
2
+ import type { BackendMessage, Conversation } from "./types.js";
3
+
4
+ export function formatRelativeTime(dateStr: string): string {
5
+ const diff = Date.now() - new Date(dateStr).getTime();
6
+ const seconds = Math.floor(diff / 1000);
7
+ if (seconds < 60) return "now";
8
+ const minutes = Math.floor(seconds / 60);
9
+ if (minutes < 60) return `${minutes}m ago`;
10
+ const hours = Math.floor(minutes / 60);
11
+ if (hours < 24) return `${hours}h ago`;
12
+ const days = Math.floor(hours / 24);
13
+ return `${days}d ago`;
14
+ }
15
+
16
+ export function buildInitialMessages(messages?: BackendMessage[]): Message[] | undefined {
17
+ if (!messages) return undefined;
18
+
19
+ type Part = {
20
+ type?: string;
21
+ text?: string;
22
+ toolCallId?: string;
23
+ toolName?: string;
24
+ input?: Record<string, unknown>;
25
+ output?: unknown;
26
+ _ref?: string;
27
+ mimeType?: string;
28
+ };
29
+
30
+ // Index tool results by toolCallId
31
+ const toolResults = new Map<string, { toolName: string; result: unknown }>();
32
+ for (const m of messages) {
33
+ if (m.role === "tool" && Array.isArray(m.content)) {
34
+ for (const part of m.content as Part[]) {
35
+ if (part.type === "tool-result" && part.toolCallId) {
36
+ const raw = part.output as { type?: string; value?: unknown } | unknown;
37
+ const value =
38
+ typeof raw === "object" &&
39
+ raw !== null &&
40
+ "type" in (raw as Record<string, unknown>) &&
41
+ (raw as Record<string, unknown>).type === "json"
42
+ ? (raw as { value: unknown }).value
43
+ : raw;
44
+ toolResults.set(part.toolCallId, { toolName: part.toolName ?? "", result: value });
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ const result: Message[] = [];
51
+ let i = 0;
52
+
53
+ while (i < messages.length) {
54
+ const m = messages[i]!;
55
+
56
+ if (m.role === "user") {
57
+ let content = "";
58
+ const parts: unknown[] = [];
59
+
60
+ if (typeof m.content === "string") {
61
+ content = m.content;
62
+ } else if (Array.isArray(m.content)) {
63
+ for (const p of m.content as Record<string, unknown>[]) {
64
+ if (p["type"] === "text") {
65
+ const text = String(p["text"] ?? "");
66
+ if (!text.startsWith("[📎") && !content) content = text;
67
+ parts.push(p);
68
+ } else if (p["type"] === "image" || p["type"] === "file") {
69
+ parts.push({ type: p["type"], _ref: p["_ref"], mimeType: p["mimeType"] });
70
+ } else {
71
+ parts.push(p);
72
+ }
73
+ }
74
+ }
75
+
76
+ result.push({
77
+ id: m._meta?.id ?? m.id ?? `msg-${i}`,
78
+ role: "user",
79
+ content,
80
+ ...(parts.length > 0 ? { parts } : {}),
81
+ } as Message);
82
+ i++;
83
+ continue;
84
+ }
85
+
86
+ if (m.role === "tool") {
87
+ i++;
88
+ continue;
89
+ }
90
+
91
+ if (m.role === "assistant") {
92
+ const parts: unknown[] = [];
93
+ let textContent = "";
94
+ const id = m._meta?.id ?? m.id ?? `msg-${i}`;
95
+
96
+ while (
97
+ i < messages.length &&
98
+ (messages[i]!.role === "assistant" || messages[i]!.role === "tool")
99
+ ) {
100
+ const cur = messages[i]!;
101
+
102
+ if (cur.role === "tool") {
103
+ i++;
104
+ continue;
105
+ }
106
+
107
+ if (typeof cur.content === "string") {
108
+ if (cur.content) {
109
+ parts.push({ type: "text", text: cur.content });
110
+ textContent += cur.content;
111
+ }
112
+ } else if (Array.isArray(cur.content)) {
113
+ for (const p of cur.content as Part[]) {
114
+ if (p.type === "text" && p.text) {
115
+ parts.push({ type: "text", text: p.text });
116
+ textContent += p.text;
117
+ } else if (p.type === "tool-call" && p.toolCallId) {
118
+ const tr = toolResults.get(p.toolCallId);
119
+ parts.push({
120
+ type: "tool-invocation",
121
+ toolInvocation: {
122
+ toolName: p.toolName ?? "",
123
+ toolCallId: p.toolCallId,
124
+ state: tr ? "result" : "call",
125
+ args: p.input,
126
+ result: tr?.result,
127
+ },
128
+ });
129
+ }
130
+ }
131
+ }
132
+ i++;
133
+ }
134
+
135
+ result.push({ id, role: "assistant", content: textContent, parts } as Message);
136
+ continue;
137
+ }
138
+
139
+ i++;
140
+ }
141
+
142
+ return result;
143
+ }
144
+
145
+ export function groupConversations(conversations: Conversation[]): {
146
+ favorites: Conversation[];
147
+ history: Conversation[];
148
+ } {
149
+ const favorites: Conversation[] = [];
150
+ const history: Conversation[] = [];
151
+ for (const conv of conversations) {
152
+ if (conv.starred) {
153
+ favorites.push(conv);
154
+ } else {
155
+ history.push(conv);
156
+ }
157
+ }
158
+ return { favorites, history };
159
+ }
@@ -0,0 +1,27 @@
1
+ import type { DisplayAlert } from "@gugacoder/agentic-sdk";
2
+ import { AlertCircle, AlertTriangle, CheckCircle, Info } from "lucide-react";
3
+ import { Alert, AlertDescription, AlertTitle } from "../ui/alert.js";
4
+
5
+ type AlertVariant = "info" | "warning" | "error" | "success";
6
+
7
+ const VARIANT_CONFIG: Record<AlertVariant, { Icon: typeof Info; className?: string }> = {
8
+ info: { Icon: Info, className: "text-blue-600 dark:text-blue-400 *:[svg]:text-blue-600 dark:*:[svg]:text-blue-400 border-blue-500/50" },
9
+ warning: { Icon: AlertTriangle, className: "text-yellow-600 dark:text-yellow-400 *:[svg]:text-yellow-600 dark:*:[svg]:text-yellow-400 border-yellow-500/50" },
10
+ error: { Icon: AlertCircle },
11
+ success: { Icon: CheckCircle, className: "text-green-600 dark:text-green-400 *:[svg]:text-green-600 dark:*:[svg]:text-green-400 border-green-500/50" },
12
+ };
13
+
14
+ export function AlertRenderer({ variant = "info", title, message }: DisplayAlert) {
15
+ const { Icon, className } = VARIANT_CONFIG[variant as AlertVariant];
16
+
17
+ return (
18
+ <Alert
19
+ variant={variant === "error" ? "destructive" : "default"}
20
+ className={className}
21
+ >
22
+ <Icon />
23
+ {title && <AlertTitle>{title}</AlertTitle>}
24
+ <AlertDescription>{message}</AlertDescription>
25
+ </Alert>
26
+ );
27
+ }