harebear-ui 0.2.3 → 0.4.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/admin/app-shell.d.ts +4 -1
- package/dist/components/admin/app-shell.js +49 -46
- package/dist/components/admin/confirm-dialog.d.ts +17 -0
- package/dist/components/admin/confirm-dialog.js +20 -0
- package/dist/components/admin/copy-field.d.ts +17 -0
- package/dist/components/admin/copy-field.js +37 -0
- package/dist/components/admin/search-input.js +1 -1
- package/dist/components/admin/status.d.ts +12 -6
- package/dist/components/admin/status.js +8 -5
- package/dist/components/admin/user-chip.d.ts +1 -1
- package/dist/components/admin/user-chip.js +7 -4
- package/dist/components/admin/user-menu.d.ts +22 -0
- package/dist/components/admin/user-menu.js +82 -0
- package/dist/components/ai/approval.d.ts +30 -0
- package/dist/components/ai/approval.js +81 -0
- package/dist/components/ai/chat-shell.d.ts +53 -0
- package/dist/components/ai/chat-shell.js +201 -0
- package/dist/components/ai/chat.d.ts +11 -3
- package/dist/components/ai/chat.js +21 -15
- package/dist/components/ai/code-block.js +1 -1
- package/dist/components/ai/conversation-list.d.ts +36 -3
- package/dist/components/ai/conversation-list.js +122 -13
- package/dist/components/ai/prompt-input.d.ts +28 -4
- package/dist/components/ai/prompt-input.js +52 -4
- package/dist/components/ai/tool-steps.d.ts +41 -0
- package/dist/components/ai/tool-steps.js +124 -0
- package/dist/components/ui/alert.d.ts +1 -1
- package/dist/components/ui/alert.js +5 -2
- package/dist/components/ui/badge.d.ts +2 -1
- package/dist/components/ui/badge.js +27 -14
- package/dist/components/ui/item.js +2 -2
- package/dist/components/ui/sidebar.d.ts +3 -1
- package/dist/components/ui/sidebar.js +7 -3
- package/dist/hooks/use-media-query.d.ts +2 -0
- package/dist/hooks/use-media-query.js +12 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.js +11 -4
- package/dist/styles.css +24 -0
- package/docs/COMPONENTS.md +154 -72
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/skills/harebear-ui/SKILL.md +4 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { ChatThread } from "./chat.js";
|
|
3
|
+
import { Alert, AlertDescription } from "../ui/alert.js";
|
|
4
|
+
import { Button } from "../ui/button.js";
|
|
5
|
+
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "../ui/sheet.js";
|
|
6
|
+
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarInset, SidebarProvider, SidebarTrigger, useSidebar, useSidebarOptional } from "../ui/sidebar.js";
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip.js";
|
|
8
|
+
import { useMediaQuery } from "../../hooks/use-media-query.js";
|
|
9
|
+
import { cn } from "../../lib/utils.js";
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
import { PanelLeftCloseIcon, PanelLeftOpenIcon, PanelRightIcon, TriangleAlertIcon } from "lucide-react";
|
|
13
|
+
// src/components/ai/chat-shell.tsx
|
|
14
|
+
const WIDE = "(min-width: 1280px)";
|
|
15
|
+
const ChatShellContext = React.createContext(null);
|
|
16
|
+
function readStored(key) {
|
|
17
|
+
try {
|
|
18
|
+
const v = window.localStorage.getItem(key);
|
|
19
|
+
return v === null ? null : v !== "collapsed";
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const useIsomorphicLayoutEffect = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
|
|
25
|
+
/**
|
|
26
|
+
* Full-height chat app layout: a sidebar that collapses to an icon rail (a sheet on mobile), the main chat
|
|
27
|
+
* column, and an optional details panel. Put a ChatPanel inside.
|
|
28
|
+
*/
|
|
29
|
+
function ChatShell({ brand, sidebarActions, sidebar, sidebarFooter, details, detailsTitle = "Details", defaultDetailsOpen = true, storageKey, defaultOpen = true, className, children }) {
|
|
30
|
+
const [open, setOpen] = React.useState(defaultOpen);
|
|
31
|
+
useIsomorphicLayoutEffect(() => {
|
|
32
|
+
if (!storageKey) return;
|
|
33
|
+
const stored = readStored(storageKey);
|
|
34
|
+
if (stored !== null) setOpen(stored);
|
|
35
|
+
}, [storageKey]);
|
|
36
|
+
const onOpenChange = (next) => {
|
|
37
|
+
setOpen(next);
|
|
38
|
+
if (!storageKey) return;
|
|
39
|
+
try {
|
|
40
|
+
window.localStorage.setItem(storageKey, next ? "open" : "collapsed");
|
|
41
|
+
} catch {}
|
|
42
|
+
};
|
|
43
|
+
const wide = useMediaQuery(WIDE);
|
|
44
|
+
const [inline, setInline] = React.useState(defaultDetailsOpen);
|
|
45
|
+
const [sheet, setSheet] = React.useState(false);
|
|
46
|
+
const hasDetails = details !== void 0 && details !== null && details !== false;
|
|
47
|
+
const ctx = React.useMemo(() => hasDetails ? {
|
|
48
|
+
detailsTitle,
|
|
49
|
+
detailsOpen: wide ? inline : sheet,
|
|
50
|
+
toggleDetails: () => wide ? setInline((v) => !v) : setSheet((v) => !v)
|
|
51
|
+
} : null, [
|
|
52
|
+
hasDetails,
|
|
53
|
+
detailsTitle,
|
|
54
|
+
wide,
|
|
55
|
+
inline,
|
|
56
|
+
sheet
|
|
57
|
+
]);
|
|
58
|
+
return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsx(SidebarProvider, {
|
|
59
|
+
open,
|
|
60
|
+
onOpenChange,
|
|
61
|
+
style: {
|
|
62
|
+
"--sidebar-width": "17rem",
|
|
63
|
+
"--sidebar-width-icon": "4.25rem"
|
|
64
|
+
},
|
|
65
|
+
children: /* @__PURE__ */ jsxs(ChatShellContext.Provider, {
|
|
66
|
+
value: ctx,
|
|
67
|
+
children: [/* @__PURE__ */ jsxs(Sidebar, {
|
|
68
|
+
collapsible: "icon",
|
|
69
|
+
children: [
|
|
70
|
+
/* @__PURE__ */ jsxs(SidebarHeader, {
|
|
71
|
+
className: "gap-5 px-3.5 pt-[max(1.25rem,env(safe-area-inset-top))] group-data-[collapsible=icon]:items-center group-data-[collapsible=icon]:gap-3 group-data-[collapsible=icon]:px-0",
|
|
72
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
73
|
+
className: "flex min-w-0 items-center gap-2 group-data-[collapsible=icon]:flex-col group-data-[collapsible=icon]:gap-3",
|
|
74
|
+
children: [brand && /* @__PURE__ */ jsx("div", {
|
|
75
|
+
className: "min-w-0 flex-1 pl-2 group-data-[collapsible=icon]:pl-0",
|
|
76
|
+
children: brand
|
|
77
|
+
}), /* @__PURE__ */ jsx(SidebarCollapseButton, {})]
|
|
78
|
+
}), sidebarActions && /* @__PURE__ */ jsx("div", {
|
|
79
|
+
className: "flex flex-col gap-2.5 group-data-[collapsible=icon]:items-center",
|
|
80
|
+
children: sidebarActions
|
|
81
|
+
})]
|
|
82
|
+
}),
|
|
83
|
+
/* @__PURE__ */ jsx(SidebarContent, {
|
|
84
|
+
className: "px-3.5 pt-2",
|
|
85
|
+
children: sidebar
|
|
86
|
+
}),
|
|
87
|
+
sidebarFooter && /* @__PURE__ */ jsx(SidebarFooter, {
|
|
88
|
+
className: "px-3.5 pb-[max(1.25rem,env(safe-area-inset-bottom))] group-data-[collapsible=icon]:px-0",
|
|
89
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
90
|
+
className: "px-2 group-data-[collapsible=icon]:px-0 has-data-[slot=user-menu-trigger]:px-0",
|
|
91
|
+
children: sidebarFooter
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
]
|
|
95
|
+
}), /* @__PURE__ */ jsxs(SidebarInset, {
|
|
96
|
+
"data-slot": "chat-shell",
|
|
97
|
+
className: cn("h-dvh min-w-0 overflow-hidden", className),
|
|
98
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
99
|
+
className: "flex h-full min-h-0 gap-3 p-2 sm:p-3 md:pl-0",
|
|
100
|
+
children: [children, hasDetails && wide && inline && /* @__PURE__ */ jsx("aside", {
|
|
101
|
+
"data-slot": "chat-shell-details",
|
|
102
|
+
"aria-label": detailsTitle,
|
|
103
|
+
className: "flex w-75 shrink-0 flex-col gap-3 overflow-y-auto",
|
|
104
|
+
children: details
|
|
105
|
+
})]
|
|
106
|
+
}), hasDetails && !wide && /* @__PURE__ */ jsx(Sheet, {
|
|
107
|
+
open: sheet,
|
|
108
|
+
onOpenChange: setSheet,
|
|
109
|
+
children: /* @__PURE__ */ jsxs(SheetContent, {
|
|
110
|
+
className: "overflow-y-auto bg-background p-4",
|
|
111
|
+
children: [/* @__PURE__ */ jsx(SheetHeader, {
|
|
112
|
+
className: "p-0",
|
|
113
|
+
children: /* @__PURE__ */ jsx(SheetTitle, { children: detailsTitle })
|
|
114
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
115
|
+
className: "flex flex-col gap-3",
|
|
116
|
+
children: details
|
|
117
|
+
})]
|
|
118
|
+
})
|
|
119
|
+
})]
|
|
120
|
+
})]
|
|
121
|
+
})
|
|
122
|
+
}) });
|
|
123
|
+
}
|
|
124
|
+
/** Collapse / expand control for the desktop sidebar, with a tooltip. Hidden on mobile, where the sidebar is a sheet. */
|
|
125
|
+
function SidebarCollapseButton() {
|
|
126
|
+
const { state, isMobile, toggleSidebar } = useSidebar();
|
|
127
|
+
if (isMobile) return null;
|
|
128
|
+
const label = state === "collapsed" ? "Expand sidebar" : "Collapse sidebar";
|
|
129
|
+
return /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
130
|
+
asChild: true,
|
|
131
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
132
|
+
variant: "ghost",
|
|
133
|
+
size: state === "collapsed" ? "icon" : "icon-sm",
|
|
134
|
+
"aria-label": label,
|
|
135
|
+
onClick: toggleSidebar,
|
|
136
|
+
children: state === "collapsed" ? /* @__PURE__ */ jsx(PanelLeftOpenIcon, {}) : /* @__PURE__ */ jsx(PanelLeftCloseIcon, {})
|
|
137
|
+
})
|
|
138
|
+
}), /* @__PURE__ */ jsx(TooltipContent, {
|
|
139
|
+
side: "right",
|
|
140
|
+
children: label
|
|
141
|
+
})] });
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The chat column as a card: header, optional notice, scrolling thread, composer. Inside ChatShell it adds the
|
|
145
|
+
* mobile sidebar button and the details toggle to the header by itself.
|
|
146
|
+
*/
|
|
147
|
+
function ChatPanel({ title, description, actions, notice, composer, conversationId, autoScroll, className, children, ...props }) {
|
|
148
|
+
const sidebar = useSidebarOptional();
|
|
149
|
+
const shell = React.useContext(ChatShellContext);
|
|
150
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
151
|
+
"data-slot": "chat-panel",
|
|
152
|
+
className: cn("flex min-h-0 min-w-0 flex-1 flex-col rounded-xl border border-border bg-card shadow-card", className),
|
|
153
|
+
...props,
|
|
154
|
+
children: [
|
|
155
|
+
/* @__PURE__ */ jsxs("header", {
|
|
156
|
+
className: "flex flex-wrap items-center gap-2.5 px-3.5 pt-[max(0.75rem,env(safe-area-inset-top))] pb-3 sm:pr-4 sm:pl-6",
|
|
157
|
+
children: [
|
|
158
|
+
sidebar && /* @__PURE__ */ jsx(SidebarTrigger, { className: "md:hidden" }),
|
|
159
|
+
/* @__PURE__ */ jsxs("div", {
|
|
160
|
+
className: "flex min-w-0 flex-1 basis-40 flex-col leading-snug",
|
|
161
|
+
children: [title && /* @__PURE__ */ jsx("h1", {
|
|
162
|
+
className: "truncate text-[15px] font-semibold",
|
|
163
|
+
children: title
|
|
164
|
+
}), description && /* @__PURE__ */ jsx("span", {
|
|
165
|
+
className: "truncate text-xs text-muted-foreground",
|
|
166
|
+
children: description
|
|
167
|
+
})]
|
|
168
|
+
}),
|
|
169
|
+
actions,
|
|
170
|
+
shell && /* @__PURE__ */ jsx(Button, {
|
|
171
|
+
variant: "ghost",
|
|
172
|
+
size: "icon",
|
|
173
|
+
"aria-label": `Toggle ${shell.detailsTitle.toLowerCase()}`,
|
|
174
|
+
"aria-pressed": shell.detailsOpen,
|
|
175
|
+
onClick: shell.toggleDetails,
|
|
176
|
+
children: /* @__PURE__ */ jsx(PanelRightIcon, {})
|
|
177
|
+
})
|
|
178
|
+
]
|
|
179
|
+
}),
|
|
180
|
+
notice && /* @__PURE__ */ jsx("div", {
|
|
181
|
+
className: "px-3.5 pb-1 sm:px-6",
|
|
182
|
+
children: /* @__PURE__ */ jsxs(Alert, {
|
|
183
|
+
variant: "warning",
|
|
184
|
+
role: "status",
|
|
185
|
+
children: [/* @__PURE__ */ jsx(TriangleAlertIcon, {}), /* @__PURE__ */ jsx(AlertDescription, { children: notice })]
|
|
186
|
+
})
|
|
187
|
+
}),
|
|
188
|
+
/* @__PURE__ */ jsx(ChatThread, {
|
|
189
|
+
autoScroll,
|
|
190
|
+
className: "px-3.5 pt-4 sm:px-6",
|
|
191
|
+
contentClassName: "gap-7",
|
|
192
|
+
children
|
|
193
|
+
}, conversationId),
|
|
194
|
+
composer && /* @__PURE__ */ jsx("div", {
|
|
195
|
+
className: "px-3.5 pt-3 pb-[max(1.25rem,env(safe-area-inset-bottom))] sm:px-6",
|
|
196
|
+
children: composer
|
|
197
|
+
})
|
|
198
|
+
]
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
export { ChatPanel, ChatShell };
|
|
@@ -14,9 +14,11 @@ export interface ChatMessageProps extends React.ComponentProps<"div"> {
|
|
|
14
14
|
avatar?: React.ReactNode;
|
|
15
15
|
/** Rendered under assistant content, usually <MessageActions />. */
|
|
16
16
|
actions?: React.ReactNode;
|
|
17
|
+
/** User bubble color: "muted" (gray, default) or "soft" (tinted with the accent). */
|
|
18
|
+
variant?: "muted" | "soft";
|
|
17
19
|
}
|
|
18
20
|
/** One chat turn. User: right-aligned bubble. Assistant: avatar + free-flowing content. */
|
|
19
|
-
declare function ChatMessage({ role, avatar, actions, className, children, ...props }: ChatMessageProps): React.JSX.Element;
|
|
21
|
+
declare function ChatMessage({ role, avatar, actions, variant, className, children, ...props }: ChatMessageProps): React.JSX.Element;
|
|
20
22
|
/** Tinted circle with an icon. Default assistant avatar. */
|
|
21
23
|
declare function AssistantAvatar({ icon, className }: {
|
|
22
24
|
icon?: React.ReactNode;
|
|
@@ -32,8 +34,14 @@ export interface MessageActionsProps extends React.ComponentProps<"div"> {
|
|
|
32
34
|
}
|
|
33
35
|
/** Copy / regenerate / thumbs row under an assistant message. Add more buttons as children. */
|
|
34
36
|
declare function MessageActions({ copyText, onRegenerate, onFeedback, feedback, className, children, ...props }: MessageActionsProps): React.JSX.Element;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
export interface TypingIndicatorProps extends React.ComponentProps<"div"> {
|
|
38
|
+
/** Text after the dots, e.g. "Working". Also used as the accessible label. */
|
|
39
|
+
label?: React.ReactNode;
|
|
40
|
+
/** Dot color: "muted" (default) or "primary". */
|
|
41
|
+
variant?: "muted" | "primary";
|
|
42
|
+
}
|
|
43
|
+
/** Three pulsing dots while the assistant is generating, with an optional label. */
|
|
44
|
+
declare function TypingIndicator({ label, variant, className, ...props }: TypingIndicatorProps): React.JSX.Element;
|
|
37
45
|
export interface ToolCallProps extends React.ComponentProps<typeof Badge> {
|
|
38
46
|
/** "running" shows a spinner; "error" tints red. */
|
|
39
47
|
status?: "running" | "done" | "error";
|
|
@@ -42,14 +42,14 @@ function ChatThread({ autoScroll = true, className, contentClassName, children,
|
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
/** One chat turn. User: right-aligned bubble. Assistant: avatar + free-flowing content. */
|
|
45
|
-
function ChatMessage({ role, avatar, actions, className, children, ...props }) {
|
|
45
|
+
function ChatMessage({ role, avatar, actions, variant = "muted", className, children, ...props }) {
|
|
46
46
|
if (role === "user") return /* @__PURE__ */ jsx("div", {
|
|
47
47
|
"data-slot": "chat-message",
|
|
48
48
|
"data-role": "user",
|
|
49
49
|
className: cn("flex justify-end", className),
|
|
50
50
|
...props,
|
|
51
51
|
children: /* @__PURE__ */ jsx("div", {
|
|
52
|
-
className: "max-w-[80%] rounded-[calc(var(--radius-xl)*1.2)] rounded-br-sm
|
|
52
|
+
className: cn("max-w-[80%] min-w-0 rounded-[calc(var(--radius-xl)*1.2)] rounded-br-sm px-4.5 py-3 text-[15px] whitespace-pre-wrap wrap-anywhere", variant === "soft" ? "bg-primary-soft" : "bg-muted"),
|
|
53
53
|
children
|
|
54
54
|
})
|
|
55
55
|
});
|
|
@@ -119,22 +119,28 @@ function MessageActions({ copyText, onRegenerate, onFeedback, feedback, classNam
|
|
|
119
119
|
]
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
-
/** Three pulsing dots while the assistant is generating. */
|
|
123
|
-
function TypingIndicator({ className, ...props }) {
|
|
124
|
-
return /* @__PURE__ */
|
|
122
|
+
/** Three pulsing dots while the assistant is generating, with an optional label. */
|
|
123
|
+
function TypingIndicator({ label, variant = "muted", className, ...props }) {
|
|
124
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
125
125
|
"data-slot": "typing-indicator",
|
|
126
126
|
role: "status",
|
|
127
|
-
"aria-label": "Assistant is typing",
|
|
128
|
-
className: cn("flex gap-
|
|
127
|
+
"aria-label": typeof label === "string" ? label : "Assistant is typing",
|
|
128
|
+
className: cn("flex items-center gap-2 py-1", className),
|
|
129
129
|
...props,
|
|
130
|
-
children: [
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
131
|
+
className: "flex gap-1.25",
|
|
132
|
+
children: [
|
|
133
|
+
0,
|
|
134
|
+
150,
|
|
135
|
+
300
|
|
136
|
+
].map((d) => /* @__PURE__ */ jsx("span", {
|
|
137
|
+
className: cn("inline-block size-1.5 animate-typing rounded-full", variant === "primary" ? "bg-primary" : "bg-muted-foreground"),
|
|
138
|
+
style: { animationDelay: `${d}ms` }
|
|
139
|
+
}, d))
|
|
140
|
+
}), label && /* @__PURE__ */ jsx("span", {
|
|
141
|
+
className: "text-xs text-muted-foreground",
|
|
142
|
+
children: label
|
|
143
|
+
})]
|
|
138
144
|
});
|
|
139
145
|
}
|
|
140
146
|
/** Outlined chip summarizing a tool call, e.g. "Read 2 notes from homelab/". */
|
|
@@ -56,7 +56,7 @@ function InlineCode({ className, ...props }) {
|
|
|
56
56
|
function Prose({ className, ...props }) {
|
|
57
57
|
return /* @__PURE__ */ jsx("div", {
|
|
58
58
|
"data-slot": "prose",
|
|
59
|
-
className: cn("flex flex-col gap-3.5 text-[15px] leading-[1.65] text-foreground", "[&_ol]:flex [&_ol]:list-decimal [&_ol]:flex-col [&_ol]:gap-2 [&_ol]:pl-5.5", "[&_ul]:flex [&_ul]:list-disc [&_ul]:flex-col [&_ul]:gap-2 [&_ul]:pl-5.5", "[&_strong]:font-semibold [&_a]:text-primary [&_a]:underline-offset-4 hover:[&_a]:underline", "[&_h1]:text-xl [&_h1]:font-semibold [&_h2]:text-lg [&_h2]:font-semibold [&_h3]:font-semibold [&_h4]:font-semibold", "[&_blockquote]:border-l-2 [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground", "[&_:not(pre)>code]:rounded-[6px] [&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-[12.5px]", "[&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-muted [&_pre]:p-4.5 [&_pre]:font-mono [&_pre]:text-[13px]", "[&_table]:w-full [&_table]:text-sm [&_td]:border-b [&_td]:py-2 [&_th]:border-b [&_th]:py-2 [&_th]:text-left [&_th]:font-medium", className),
|
|
59
|
+
className: cn("flex min-w-0 flex-col gap-3.5 text-[15px] leading-[1.65] wrap-anywhere text-foreground", "[&_ol]:flex [&_ol]:list-decimal [&_ol]:flex-col [&_ol]:gap-2 [&_ol]:pl-5.5", "[&_ul]:flex [&_ul]:list-disc [&_ul]:flex-col [&_ul]:gap-2 [&_ul]:pl-5.5", "[&_strong]:font-semibold [&_a]:text-primary [&_a]:underline-offset-4 hover:[&_a]:underline", "[&_h1]:text-xl [&_h1]:font-semibold [&_h2]:text-lg [&_h2]:font-semibold [&_h3]:font-semibold [&_h4]:font-semibold", "[&_blockquote]:border-l-2 [&_blockquote]:pl-4 [&_blockquote]:text-muted-foreground", "[&_:not(pre)>code]:rounded-[6px] [&_:not(pre)>code]:bg-muted [&_:not(pre)>code]:px-1.5 [&_:not(pre)>code]:py-0.5 [&_:not(pre)>code]:font-mono [&_:not(pre)>code]:text-[12.5px]", "[&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-muted [&_pre]:p-4.5 [&_pre]:font-mono [&_pre]:text-[13px]", "[&_table]:w-full [&_table]:text-sm [&_td]:border-b [&_td]:py-2 [&_th]:border-b [&_th]:py-2 [&_th]:text-left [&_th]:font-medium", className),
|
|
60
60
|
...props
|
|
61
61
|
});
|
|
62
62
|
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
import { StatusTone } from "../admin/status.js";
|
|
2
|
+
import { Button } from "../ui/button.js";
|
|
1
3
|
import * as React from "react";
|
|
2
4
|
export interface Conversation {
|
|
3
5
|
id: string;
|
|
4
6
|
title: string;
|
|
7
|
+
/** Second line under the title, e.g. the last-updated time or "Working". */
|
|
8
|
+
description?: React.ReactNode;
|
|
9
|
+
/** Shows a status dot before the description, for live states. */
|
|
10
|
+
tone?: StatusTone;
|
|
11
|
+
/** Pulse the status dot. */
|
|
12
|
+
pulse?: boolean;
|
|
5
13
|
}
|
|
6
14
|
export interface ConversationGroup {
|
|
7
15
|
/** e.g. "Today", "Yesterday", "Previous 7 days". */
|
|
@@ -12,7 +20,32 @@ export interface ConversationListProps extends Omit<React.ComponentProps<"nav">,
|
|
|
12
20
|
groups: ConversationGroup[];
|
|
13
21
|
activeId?: string;
|
|
14
22
|
onSelect?: (id: string) => void;
|
|
23
|
+
/** Trailing control per row, e.g. a delete button. On devices that can hover it shows on hover and focus; on touch it is always visible. */
|
|
24
|
+
renderAction?: (conversation: Conversation) => React.ReactNode;
|
|
25
|
+
/** Shown when there are no conversations. Default "No conversations yet". */
|
|
26
|
+
empty?: React.ReactNode;
|
|
15
27
|
}
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Grouped list of chat threads for a sidebar. Inside a Sidebar it closes the mobile sheet on select,
|
|
30
|
+
* and hides itself when the sidebar is collapsed to its icon rail.
|
|
31
|
+
*/
|
|
32
|
+
declare function ConversationList({ groups, activeId, onSelect, renderAction, empty, className, ...props }: ConversationListProps): React.JSX.Element;
|
|
33
|
+
/**
|
|
34
|
+
* Buckets items into Today / Yesterday / Previous 7 days / Earlier by a timestamp (ms or Date),
|
|
35
|
+
* newest first, and drops empty groups. Pass `now` for deterministic tests.
|
|
36
|
+
*/
|
|
37
|
+
declare function groupByDate<T>(items: T[], getDate: (item: T) => number | Date, toConversation: (item: T) => Conversation, now?: number | Date): ConversationGroup[];
|
|
38
|
+
export interface NewConversationButtonProps extends React.ComponentProps<typeof Button> {
|
|
39
|
+
/** Default "New conversation". */
|
|
40
|
+
label?: string;
|
|
41
|
+
/** Default a plus icon. */
|
|
42
|
+
icon?: React.ReactNode;
|
|
43
|
+
/** Trailing content in the expanded button, e.g. <Kbd>⌘N</Kbd>. */
|
|
44
|
+
shortcut?: React.ReactNode;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Full-width "New conversation" button for a sidebar header. In the icon rail it shrinks to an icon with a
|
|
48
|
+
* tooltip; on mobile it closes the sidebar sheet after the click.
|
|
49
|
+
*/
|
|
50
|
+
declare function NewConversationButton({ label, icon, shortcut, variant, onClick, className, ...props }: NewConversationButtonProps): React.JSX.Element;
|
|
51
|
+
export { ConversationList, NewConversationButton, groupByDate };
|
|
@@ -1,30 +1,139 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { StatusDot } from "../admin/status.js";
|
|
3
|
+
import { Button } from "../ui/button.js";
|
|
4
|
+
import { Empty, EmptyDescription } from "../ui/empty.js";
|
|
5
|
+
import { useSidebarOptional } from "../ui/sidebar.js";
|
|
6
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
|
|
2
7
|
import { cn } from "../../lib/utils.js";
|
|
3
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { PlusIcon } from "lucide-react";
|
|
4
10
|
// src/components/ai/conversation-list.tsx
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Grouped list of chat threads for a sidebar. Inside a Sidebar it closes the mobile sheet on select,
|
|
13
|
+
* and hides itself when the sidebar is collapsed to its icon rail.
|
|
14
|
+
*/
|
|
15
|
+
function ConversationList({ groups, activeId, onSelect, renderAction, empty = "No conversations yet", className, ...props }) {
|
|
16
|
+
const sidebar = useSidebarOptional();
|
|
17
|
+
const select = (id) => {
|
|
18
|
+
onSelect?.(id);
|
|
19
|
+
sidebar?.setOpenMobile(false);
|
|
20
|
+
};
|
|
21
|
+
const visible = groups.filter((g) => g.items.length);
|
|
22
|
+
return /* @__PURE__ */ jsxs("nav", {
|
|
8
23
|
"data-slot": "conversation-list",
|
|
9
24
|
"aria-label": "Conversations",
|
|
10
|
-
className: cn("flex flex-col gap-4.5", className),
|
|
25
|
+
className: cn("flex flex-col gap-4.5 group-data-[collapsible=icon]:hidden", className),
|
|
11
26
|
...props,
|
|
12
|
-
children:
|
|
27
|
+
children: [visible.map((g) => /* @__PURE__ */ jsxs("div", {
|
|
13
28
|
className: "flex flex-col gap-0.5",
|
|
14
29
|
children: [/* @__PURE__ */ jsx("div", {
|
|
15
30
|
className: "px-3 pb-1.5 text-xs font-medium text-muted-foreground",
|
|
16
31
|
children: g.label
|
|
17
32
|
}), g.items.map((c) => {
|
|
18
33
|
const active = c.id === activeId;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
className: cn("
|
|
24
|
-
children:
|
|
34
|
+
const action = renderAction?.(c);
|
|
35
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
36
|
+
"data-slot": "conversation-item",
|
|
37
|
+
"data-active": active || void 0,
|
|
38
|
+
className: cn("group/conversation flex min-w-0 items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", active && "bg-card text-foreground shadow-card hover:bg-card"),
|
|
39
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
40
|
+
type: "button",
|
|
41
|
+
"aria-current": active ? "page" : void 0,
|
|
42
|
+
onClick: () => select(c.id),
|
|
43
|
+
className: cn("flex min-w-0 flex-1 flex-col rounded-md py-2.25 pl-3 text-left font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring/40", action ? "pr-1" : "pr-3"),
|
|
44
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
45
|
+
className: "truncate",
|
|
46
|
+
children: c.title
|
|
47
|
+
}), c.description !== void 0 && /* @__PURE__ */ jsxs("span", {
|
|
48
|
+
className: "flex min-w-0 items-center gap-1.5 text-xs font-normal text-muted-foreground tabular-nums",
|
|
49
|
+
children: [c.tone && /* @__PURE__ */ jsx(StatusDot, {
|
|
50
|
+
tone: c.tone,
|
|
51
|
+
pulse: c.pulse
|
|
52
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
53
|
+
className: "truncate",
|
|
54
|
+
children: c.description
|
|
55
|
+
})]
|
|
56
|
+
})]
|
|
57
|
+
}), action && /* @__PURE__ */ jsx("div", {
|
|
58
|
+
className: "mr-1 flex shrink-0 items-center [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover/conversation:opacity-100 [@media(hover:hover)]:group-focus-within/conversation:opacity-100",
|
|
59
|
+
children: action
|
|
60
|
+
})]
|
|
25
61
|
}, c.id);
|
|
26
62
|
})]
|
|
27
|
-
}, g.label))
|
|
63
|
+
}, g.label)), !visible.length && empty && /* @__PURE__ */ jsx(Empty, { children: /* @__PURE__ */ jsx(EmptyDescription, { children: empty }) })]
|
|
28
64
|
});
|
|
29
65
|
}
|
|
30
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Buckets items into Today / Yesterday / Previous 7 days / Earlier by a timestamp (ms or Date),
|
|
68
|
+
* newest first, and drops empty groups. Pass `now` for deterministic tests.
|
|
69
|
+
*/
|
|
70
|
+
function groupByDate(items, getDate, toConversation, now = Date.now()) {
|
|
71
|
+
const d = new Date(now);
|
|
72
|
+
const midnight = (daysAgo) => new Date(d.getFullYear(), d.getMonth(), d.getDate() - daysAgo).getTime();
|
|
73
|
+
const [today, yesterday, weekAgo] = [
|
|
74
|
+
midnight(0),
|
|
75
|
+
midnight(1),
|
|
76
|
+
midnight(7)
|
|
77
|
+
];
|
|
78
|
+
const groups = [
|
|
79
|
+
{
|
|
80
|
+
label: "Today",
|
|
81
|
+
items: []
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
label: "Yesterday",
|
|
85
|
+
items: []
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
label: "Previous 7 days",
|
|
89
|
+
items: []
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
label: "Earlier",
|
|
93
|
+
items: []
|
|
94
|
+
}
|
|
95
|
+
];
|
|
96
|
+
const sorted = [...items].sort((a, b) => +getDate(b) - +getDate(a));
|
|
97
|
+
for (const item of sorted) {
|
|
98
|
+
const t = +getDate(item);
|
|
99
|
+
groups[t >= today ? 0 : t >= yesterday ? 1 : t >= weekAgo ? 2 : 3]?.items.push(toConversation(item));
|
|
100
|
+
}
|
|
101
|
+
return groups.filter((g) => g.items.length);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Full-width "New conversation" button for a sidebar header. In the icon rail it shrinks to an icon with a
|
|
105
|
+
* tooltip; on mobile it closes the sidebar sheet after the click.
|
|
106
|
+
*/
|
|
107
|
+
function NewConversationButton({ label = "New conversation", icon = /* @__PURE__ */ jsx(PlusIcon, {}), shortcut, variant = "secondary", onClick, className, ...props }) {
|
|
108
|
+
const sidebar = useSidebarOptional();
|
|
109
|
+
const rail = sidebar?.state === "collapsed" && !sidebar.isMobile;
|
|
110
|
+
const button = /* @__PURE__ */ jsxs(Button, {
|
|
111
|
+
"data-slot": "new-conversation-button",
|
|
112
|
+
variant,
|
|
113
|
+
size: rail ? "icon" : "default",
|
|
114
|
+
"aria-label": rail ? label : void 0,
|
|
115
|
+
className: cn(!rail && "w-full justify-start", className),
|
|
116
|
+
onClick: (e) => {
|
|
117
|
+
onClick?.(e);
|
|
118
|
+
sidebar?.setOpenMobile(false);
|
|
119
|
+
},
|
|
120
|
+
...props,
|
|
121
|
+
children: [
|
|
122
|
+
icon,
|
|
123
|
+
!rail && /* @__PURE__ */ jsx("span", {
|
|
124
|
+
className: "flex-1 truncate text-left",
|
|
125
|
+
children: label
|
|
126
|
+
}),
|
|
127
|
+
!rail && shortcut
|
|
128
|
+
]
|
|
129
|
+
});
|
|
130
|
+
if (!rail) return button;
|
|
131
|
+
return /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
132
|
+
asChild: true,
|
|
133
|
+
children: button
|
|
134
|
+
}), /* @__PURE__ */ jsx(TooltipContent, {
|
|
135
|
+
side: "right",
|
|
136
|
+
children: label
|
|
137
|
+
})] });
|
|
138
|
+
}
|
|
139
|
+
export { ConversationList, NewConversationButton, groupByDate };
|
|
@@ -20,8 +20,32 @@ export interface PromptInputProps extends Omit<React.ComponentProps<"form">, "on
|
|
|
20
20
|
}
|
|
21
21
|
/** Chat composer: auto-growing textarea with attach, tool chips, and send/stop. */
|
|
22
22
|
declare function PromptInput({ value, onValueChange, onSubmit, placeholder, loading, onStop, onAttach, tools, footer, disabled, autoFocus, className, ...props }: PromptInputProps): React.JSX.Element;
|
|
23
|
-
|
|
24
|
-
declare function PromptInputChip({ active, className, ...props }: React.ComponentProps<typeof Button> & {
|
|
23
|
+
export interface PromptInputChipProps extends React.ComponentProps<typeof Button> {
|
|
25
24
|
active?: boolean;
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
/** "warning" outlines the chip in the warning color, e.g. for a risky mode that is on. */
|
|
26
|
+
tone?: "default" | "warning";
|
|
27
|
+
}
|
|
28
|
+
/** Toggleable pill for the composer toolbar (web search, a knowledge source…). */
|
|
29
|
+
declare function PromptInputChip({ active, tone, className, ...props }: PromptInputChipProps): React.JSX.Element;
|
|
30
|
+
export interface PromptInputSelectOption {
|
|
31
|
+
value: string;
|
|
32
|
+
label: React.ReactNode;
|
|
33
|
+
description?: React.ReactNode;
|
|
34
|
+
icon?: React.ReactNode;
|
|
35
|
+
/** "warning" shows the chip in the warning color while this option is selected. */
|
|
36
|
+
tone?: "default" | "warning";
|
|
37
|
+
}
|
|
38
|
+
export interface PromptInputSelectProps {
|
|
39
|
+
options: PromptInputSelectOption[];
|
|
40
|
+
value: string;
|
|
41
|
+
onValueChange: (value: string) => void;
|
|
42
|
+
/** Heading inside the menu, e.g. "Permissions for this conversation". */
|
|
43
|
+
label?: React.ReactNode;
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
/** Tooltip text on the chip. */
|
|
46
|
+
title?: string;
|
|
47
|
+
className?: string;
|
|
48
|
+
}
|
|
49
|
+
/** Composer chip that picks one of a few modes, each with an icon and a one-line description. */
|
|
50
|
+
declare function PromptInputSelect({ options, value, onValueChange, label, disabled, title, className }: PromptInputSelectProps): React.JSX.Element;
|
|
51
|
+
export { PromptInput, PromptInputChip, PromptInputSelect };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Button } from "../ui/button.js";
|
|
3
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger } from "../ui/dropdown-menu.js";
|
|
3
4
|
import { cn } from "../../lib/utils.js";
|
|
4
5
|
import * as React from "react";
|
|
5
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
import { ArrowUpIcon, PaperclipIcon, SquareIcon } from "lucide-react";
|
|
7
|
+
import { ArrowUpIcon, ChevronDownIcon, PaperclipIcon, SquareIcon } from "lucide-react";
|
|
7
8
|
// src/components/ai/prompt-input.tsx
|
|
8
9
|
/** Chat composer: auto-growing textarea with attach, tool chips, and send/stop. */
|
|
9
10
|
function PromptInput({ value, onValueChange, onSubmit, placeholder = "Send a message…", loading, onStop, onAttach, tools, footer, disabled, autoFocus, className, ...props }) {
|
|
@@ -84,16 +85,63 @@ function PromptInput({ value, onValueChange, onSubmit, placeholder = "Send a mes
|
|
|
84
85
|
})]
|
|
85
86
|
});
|
|
86
87
|
}
|
|
88
|
+
const chipClass = (tone) => cn("h-8", tone === "warning" && "border-warning/50 bg-transparent text-warning");
|
|
87
89
|
/** Toggleable pill for the composer toolbar (web search, a knowledge source…). */
|
|
88
|
-
function PromptInputChip({ active, className, ...props }) {
|
|
90
|
+
function PromptInputChip({ active, tone = "default", className, ...props }) {
|
|
89
91
|
return /* @__PURE__ */ jsx(Button, {
|
|
90
92
|
type: "button",
|
|
91
93
|
size: "sm",
|
|
92
94
|
shape: "pill",
|
|
93
95
|
variant: active ? "soft" : "outline",
|
|
94
96
|
"aria-pressed": active,
|
|
95
|
-
|
|
97
|
+
"data-tone": tone,
|
|
98
|
+
className: cn(chipClass(tone), className),
|
|
96
99
|
...props
|
|
97
100
|
});
|
|
98
101
|
}
|
|
99
|
-
|
|
102
|
+
/** Composer chip that picks one of a few modes, each with an icon and a one-line description. */
|
|
103
|
+
function PromptInputSelect({ options, value, onValueChange, label, disabled, title, className }) {
|
|
104
|
+
const current = options.find((o) => o.value === value) ?? options[0];
|
|
105
|
+
const tone = current?.tone ?? "default";
|
|
106
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
107
|
+
asChild: true,
|
|
108
|
+
disabled,
|
|
109
|
+
children: /* @__PURE__ */ jsxs(Button, {
|
|
110
|
+
type: "button",
|
|
111
|
+
size: "sm",
|
|
112
|
+
shape: "pill",
|
|
113
|
+
variant: tone === "warning" ? "outline" : "soft",
|
|
114
|
+
"data-slot": "prompt-input-select",
|
|
115
|
+
"data-tone": tone,
|
|
116
|
+
title,
|
|
117
|
+
className: cn(chipClass(tone), className),
|
|
118
|
+
children: [
|
|
119
|
+
current?.icon,
|
|
120
|
+
current?.label,
|
|
121
|
+
/* @__PURE__ */ jsx(ChevronDownIcon, { className: "opacity-60" })
|
|
122
|
+
]
|
|
123
|
+
})
|
|
124
|
+
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
125
|
+
align: "start",
|
|
126
|
+
className: "w-72",
|
|
127
|
+
children: [label && /* @__PURE__ */ jsx(DropdownMenuLabel, { children: label }), /* @__PURE__ */ jsx(DropdownMenuRadioGroup, {
|
|
128
|
+
value,
|
|
129
|
+
onValueChange,
|
|
130
|
+
children: options.map((o) => /* @__PURE__ */ jsx(DropdownMenuRadioItem, {
|
|
131
|
+
value: o.value,
|
|
132
|
+
className: "items-start py-2",
|
|
133
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
134
|
+
className: "flex min-w-0 flex-col",
|
|
135
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
136
|
+
className: "font-medium",
|
|
137
|
+
children: o.label
|
|
138
|
+
}), o.description && /* @__PURE__ */ jsx("span", {
|
|
139
|
+
className: "text-xs text-muted-foreground",
|
|
140
|
+
children: o.description
|
|
141
|
+
})]
|
|
142
|
+
})
|
|
143
|
+
}, o.value))
|
|
144
|
+
})]
|
|
145
|
+
})] });
|
|
146
|
+
}
|
|
147
|
+
export { PromptInput, PromptInputChip, PromptInputSelect };
|