harebear-ui 0.8.0 → 0.9.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/AGENTS.md +2 -2
- package/dist/components/admin/app-nav.d.ts +4 -0
- package/dist/components/admin/app-nav.js +5 -2
- package/dist/components/admin/avatar-upload.d.ts +32 -0
- package/dist/components/admin/avatar-upload.js +121 -0
- package/dist/components/admin/category.d.ts +17 -0
- package/dist/components/admin/category.js +35 -0
- package/dist/components/admin/celebration.d.ts +7 -0
- package/dist/components/admin/celebration.js +61 -0
- package/dist/components/admin/color-swatch-picker.d.ts +15 -0
- package/dist/components/admin/color-swatch-picker.js +68 -0
- package/dist/components/admin/date-time-picker.d.ts +20 -0
- package/dist/components/admin/date-time-picker.js +97 -0
- package/dist/components/admin/detail-sheet.d.ts +18 -0
- package/dist/components/admin/detail-sheet.js +80 -0
- package/dist/components/admin/markdown-editor.d.ts +15 -0
- package/dist/components/admin/markdown-editor.js +42 -0
- package/dist/components/admin/user-chip.d.ts +34 -3
- package/dist/components/admin/user-chip.js +66 -7
- package/dist/components/admin/user-menu.d.ts +4 -1
- package/dist/components/admin/user-menu.js +2 -1
- package/dist/components/ai/ask-box.d.ts +14 -2
- package/dist/components/ai/ask-box.js +90 -54
- package/dist/components/ai/assistant.d.ts +13 -3
- package/dist/components/ai/assistant.js +37 -7
- package/dist/components/ai/code-block.d.ts +10 -3
- package/dist/components/ai/code-block.js +54 -5
- package/dist/components/ai/conversation-list.d.ts +13 -3
- package/dist/components/ai/conversation-list.js +15 -26
- package/dist/components/auth/auth.js +4 -6
- package/dist/components/data/agenda-row.d.ts +26 -0
- package/dist/components/data/agenda-row.js +62 -0
- package/dist/components/data/day-strip.d.ts +22 -0
- package/dist/components/data/day-strip.js +68 -0
- package/dist/components/data/sectioned-list.d.ts +21 -6
- package/dist/components/data/sectioned-list.js +44 -21
- package/dist/components/data/task-row.d.ts +3 -0
- package/dist/components/data/task-row.js +6 -14
- package/dist/components/ui/avatar.js +1 -1
- package/dist/components/ui/kbd.js +1 -1
- package/dist/components/ui/password-input.d.ts +8 -0
- package/dist/components/ui/password-input.js +32 -0
- package/dist/index.d.ts +15 -5
- package/dist/index.js +13 -3
- package/dist/lib/date.js +8 -0
- package/dist/lib/toast-clearance.js +22 -0
- package/dist/styles.css +48 -0
- package/docs/COMPONENTS.md +165 -15
- package/docs/ROADMAP.md +12 -11
- package/llms.txt +8 -7
- package/package.json +1 -1
- package/skills/harebear-ui/SKILL.md +2 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface MarkdownEditorProps extends Omit<React.ComponentProps<"textarea">, "value" | "defaultValue" | "onChange"> {
|
|
3
|
+
value: string;
|
|
4
|
+
onValueChange: (value: string) => void;
|
|
5
|
+
/**
|
|
6
|
+
* Turns the Markdown into elements for the Preview tab; the result is wrapped in Prose.
|
|
7
|
+
* With react-markdown: `(md) => <Markdown components={{ blockquote: ProseBlockquote }}>{md}</Markdown>`.
|
|
8
|
+
*/
|
|
9
|
+
renderPreview: (markdown: string) => React.ReactNode;
|
|
10
|
+
/** Classes for the wrapping Tabs. `className` goes to the textarea. */
|
|
11
|
+
wrapperClassName?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Markdown field with Write and Preview tabs. Other props go to the Textarea (id, placeholder, aria-*). */
|
|
14
|
+
declare function MarkdownEditor({ value, onValueChange, renderPreview, className, wrapperClassName, ...props }: MarkdownEditorProps): React.JSX.Element;
|
|
15
|
+
export { MarkdownEditor };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Prose } from "../ai/code-block.js";
|
|
3
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs.js";
|
|
4
|
+
import { Textarea } from "../ui/textarea.js";
|
|
5
|
+
import { cn } from "../../lib/utils.js";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
// src/components/admin/markdown-editor.tsx
|
|
8
|
+
/** Markdown field with Write and Preview tabs. Other props go to the Textarea (id, placeholder, aria-*). */
|
|
9
|
+
function MarkdownEditor({ value, onValueChange, renderPreview, className, wrapperClassName, ...props }) {
|
|
10
|
+
return /* @__PURE__ */ jsxs(Tabs, {
|
|
11
|
+
"data-slot": "markdown-editor",
|
|
12
|
+
defaultValue: "write",
|
|
13
|
+
className: cn("gap-3", wrapperClassName),
|
|
14
|
+
children: [
|
|
15
|
+
/* @__PURE__ */ jsxs(TabsList, { children: [/* @__PURE__ */ jsx(TabsTrigger, {
|
|
16
|
+
value: "write",
|
|
17
|
+
children: "Write"
|
|
18
|
+
}), /* @__PURE__ */ jsx(TabsTrigger, {
|
|
19
|
+
value: "preview",
|
|
20
|
+
children: "Preview"
|
|
21
|
+
})] }),
|
|
22
|
+
/* @__PURE__ */ jsx(TabsContent, {
|
|
23
|
+
value: "write",
|
|
24
|
+
children: /* @__PURE__ */ jsx(Textarea, {
|
|
25
|
+
value,
|
|
26
|
+
onChange: (e) => onValueChange(e.target.value),
|
|
27
|
+
className: cn("min-h-48", className),
|
|
28
|
+
...props
|
|
29
|
+
})
|
|
30
|
+
}),
|
|
31
|
+
/* @__PURE__ */ jsx(TabsContent, {
|
|
32
|
+
value: "preview",
|
|
33
|
+
className: "min-h-48 rounded-md border px-3.5 py-3",
|
|
34
|
+
children: value.trim() ? /* @__PURE__ */ jsx(Prose, { children: renderPreview(value) }) : /* @__PURE__ */ jsx("p", {
|
|
35
|
+
className: "text-sm text-muted-foreground",
|
|
36
|
+
children: "Nothing to preview."
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
]
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export { MarkdownEditor };
|
|
@@ -1,14 +1,45 @@
|
|
|
1
|
+
import { CategoryColor } from "./category.js";
|
|
2
|
+
import { Avatar } from "../ui/avatar.js";
|
|
1
3
|
import * as React from "react";
|
|
2
4
|
/** Up to two initials from a name: "Maya Chen" -> "MC". */
|
|
3
5
|
export declare function initials(name: string): string;
|
|
4
|
-
export interface
|
|
6
|
+
export interface MemberAvatarProps extends Omit<React.ComponentProps<typeof Avatar>, "children" | "color"> {
|
|
7
|
+
name: string;
|
|
8
|
+
avatarUrl?: string;
|
|
9
|
+
/** Category colour behind the initials when there is no photo. Default: the soft primary tint. */
|
|
10
|
+
color?: CategoryColor;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A person's photo, or their initials on their category colour. It is an image named after the person; pass
|
|
14
|
+
* `aria-hidden` when the name is already shown next to it.
|
|
15
|
+
*/
|
|
16
|
+
declare function MemberAvatar({ name, avatarUrl, color, ...props }: MemberAvatarProps): React.JSX.Element;
|
|
17
|
+
export interface AvatarStackProps extends React.ComponentProps<"div"> {
|
|
18
|
+
members: Pick<MemberAvatarProps, "name" | "avatarUrl" | "color">[];
|
|
19
|
+
/** Total slots, default 4. Past it, `max - 1` faces and a "+N" chip. */
|
|
20
|
+
max?: number;
|
|
21
|
+
}
|
|
22
|
+
/** Overlapping small avatars, e.g. who a list is shared with. Each face is named; the overflow reads "N more". */
|
|
23
|
+
declare function AvatarStack({ members, max, className, ...props }: AvatarStackProps): React.JSX.Element;
|
|
24
|
+
export interface VisibilityBadgeProps extends React.ComponentProps<"div"> {
|
|
25
|
+
visibility: "shared" | "private";
|
|
26
|
+
/** Faces shown when shared. */
|
|
27
|
+
members?: AvatarStackProps["members"];
|
|
28
|
+
/** Default "Shared with everyone" or "Only you". */
|
|
29
|
+
label?: React.ReactNode;
|
|
30
|
+
}
|
|
31
|
+
/** Who can see something: faces and "Shared with everyone", or a lock and "Only you". */
|
|
32
|
+
declare function VisibilityBadge({ visibility, members, label, className, ...props }: VisibilityBadgeProps): React.JSX.Element;
|
|
33
|
+
export interface UserChipProps extends Omit<React.ComponentProps<"div">, "color"> {
|
|
5
34
|
name: string;
|
|
6
35
|
subtitle?: React.ReactNode;
|
|
7
36
|
avatarUrl?: string;
|
|
37
|
+
/** Category colour behind the initials when there is no photo. */
|
|
38
|
+
color?: CategoryColor;
|
|
8
39
|
/** Trailing control, e.g. a menu button. */
|
|
9
40
|
action?: React.ReactNode;
|
|
10
41
|
size?: "sm" | "default" | "lg";
|
|
11
42
|
}
|
|
12
43
|
/** Avatar + name + secondary line. In a sidebar collapsed to its icon rail, only the avatar shows. */
|
|
13
|
-
declare function UserChip({ name, subtitle, avatarUrl, action, size, className, ...props }: UserChipProps): React.JSX.Element;
|
|
14
|
-
export { UserChip };
|
|
44
|
+
declare function UserChip({ name, subtitle, avatarUrl, color, action, size, className, ...props }: UserChipProps): React.JSX.Element;
|
|
45
|
+
export { AvatarStack, MemberAvatar, UserChip, VisibilityBadge };
|
|
@@ -1,25 +1,84 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { categoryBg } from "./category.js";
|
|
2
3
|
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar.js";
|
|
3
4
|
import { cn } from "../../lib/utils.js";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { LockIcon } from "lucide-react";
|
|
5
7
|
// src/components/admin/user-chip.tsx
|
|
6
8
|
/** Up to two initials from a name: "Maya Chen" -> "MC". */
|
|
7
9
|
function initials(name) {
|
|
8
10
|
return name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]?.toUpperCase()).join("");
|
|
9
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* A person's photo, or their initials on their category colour. It is an image named after the person; pass
|
|
14
|
+
* `aria-hidden` when the name is already shown next to it.
|
|
15
|
+
*/
|
|
16
|
+
function MemberAvatar({ name, avatarUrl, color, ...props }) {
|
|
17
|
+
return /* @__PURE__ */ jsxs(Avatar, {
|
|
18
|
+
role: "img",
|
|
19
|
+
"aria-label": name,
|
|
20
|
+
"data-color": color,
|
|
21
|
+
...props,
|
|
22
|
+
children: [avatarUrl && /* @__PURE__ */ jsx(AvatarImage, {
|
|
23
|
+
src: avatarUrl,
|
|
24
|
+
alt: ""
|
|
25
|
+
}), /* @__PURE__ */ jsx(AvatarFallback, {
|
|
26
|
+
"aria-hidden": "true",
|
|
27
|
+
className: cn(color && [categoryBg[color], "text-category-foreground"]),
|
|
28
|
+
children: initials(name)
|
|
29
|
+
})]
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** Overlapping small avatars, e.g. who a list is shared with. Each face is named; the overflow reads "N more". */
|
|
33
|
+
function AvatarStack({ members, max = 4, className, ...props }) {
|
|
34
|
+
const shown = members.length > max ? members.slice(0, max - 1) : members;
|
|
35
|
+
const extra = members.length - shown.length;
|
|
36
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
37
|
+
"data-slot": "avatar-stack",
|
|
38
|
+
className: cn("flex -space-x-1.5", className),
|
|
39
|
+
...props,
|
|
40
|
+
children: [shown.map((m, i) => /* @__PURE__ */ jsx(MemberAvatar, {
|
|
41
|
+
size: "sm",
|
|
42
|
+
...m,
|
|
43
|
+
className: "ring-2 ring-background"
|
|
44
|
+
}, `${m.name}-${i}`)), extra > 0 && /* @__PURE__ */ jsxs("span", {
|
|
45
|
+
role: "img",
|
|
46
|
+
"aria-label": `${extra} more`,
|
|
47
|
+
className: "relative flex size-7 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold text-muted-foreground ring-2 ring-background",
|
|
48
|
+
children: ["+", extra]
|
|
49
|
+
})]
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Who can see something: faces and "Shared with everyone", or a lock and "Only you". */
|
|
53
|
+
function VisibilityBadge({ visibility, members, label, className, ...props }) {
|
|
54
|
+
const shared = visibility === "shared";
|
|
55
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
56
|
+
"data-slot": "visibility-badge",
|
|
57
|
+
"data-visibility": visibility,
|
|
58
|
+
className: cn("inline-flex items-center gap-2 text-[13px] text-muted-foreground", className),
|
|
59
|
+
...props,
|
|
60
|
+
children: [shared ? members?.length ? /* @__PURE__ */ jsx(AvatarStack, {
|
|
61
|
+
members,
|
|
62
|
+
"aria-hidden": "true"
|
|
63
|
+
}) : null : /* @__PURE__ */ jsx(LockIcon, {
|
|
64
|
+
"aria-hidden": "true",
|
|
65
|
+
className: "size-4 shrink-0"
|
|
66
|
+
}), /* @__PURE__ */ jsx("span", { children: label ?? (shared ? "Shared with everyone" : "Only you") })]
|
|
67
|
+
});
|
|
68
|
+
}
|
|
10
69
|
/** Avatar + name + secondary line. In a sidebar collapsed to its icon rail, only the avatar shows. */
|
|
11
|
-
function UserChip({ name, subtitle, avatarUrl, action, size = "default", className, ...props }) {
|
|
70
|
+
function UserChip({ name, subtitle, avatarUrl, color, action, size = "default", className, ...props }) {
|
|
12
71
|
return /* @__PURE__ */ jsxs("div", {
|
|
13
72
|
"data-slot": "user-chip",
|
|
14
73
|
className: cn("flex min-w-0 items-center gap-3 group-data-[collapsible=icon]:justify-center", className),
|
|
15
74
|
...props,
|
|
16
75
|
children: [
|
|
17
|
-
/* @__PURE__ */
|
|
76
|
+
/* @__PURE__ */ jsx(MemberAvatar, {
|
|
77
|
+
name,
|
|
78
|
+
avatarUrl,
|
|
79
|
+
color,
|
|
18
80
|
size,
|
|
19
|
-
|
|
20
|
-
src: avatarUrl,
|
|
21
|
-
alt: ""
|
|
22
|
-
}), /* @__PURE__ */ jsx(AvatarFallback, { children: initials(name) })]
|
|
81
|
+
"aria-hidden": "true"
|
|
23
82
|
}),
|
|
24
83
|
/* @__PURE__ */ jsxs("div", {
|
|
25
84
|
className: "min-w-0 flex-1 leading-snug group-data-[collapsible=icon]:hidden",
|
|
@@ -38,4 +97,4 @@ function UserChip({ name, subtitle, avatarUrl, action, size = "default", classNa
|
|
|
38
97
|
]
|
|
39
98
|
});
|
|
40
99
|
}
|
|
41
|
-
export { UserChip, initials };
|
|
100
|
+
export { AvatarStack, MemberAvatar, UserChip, VisibilityBadge, initials };
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { CategoryColor } from "./category.js";
|
|
1
2
|
import * as React from "react";
|
|
2
3
|
export interface UserMenuProps {
|
|
3
4
|
name: string;
|
|
4
5
|
/** Second line, e.g. the email or "Signed in". */
|
|
5
6
|
subtitle?: React.ReactNode;
|
|
6
7
|
avatarUrl?: string;
|
|
8
|
+
/** Category colour behind the initials when there is no photo. */
|
|
9
|
+
color?: CategoryColor;
|
|
7
10
|
/** Extra items between the header and the theme choice, e.g. a Settings DropdownMenuItem. */
|
|
8
11
|
children?: React.ReactNode;
|
|
9
12
|
/** Show the Light / Dark / System choice. Default true. Needs ThemeProvider. */
|
|
@@ -18,5 +21,5 @@ export interface UserMenuProps {
|
|
|
18
21
|
* The signed-in user as a menu button, for a sidebar footer or top bar: theme choice, app items such as
|
|
19
22
|
* Settings, and sign out. In a sidebar icon rail the trigger is just the avatar and the menu opens to the side.
|
|
20
23
|
*/
|
|
21
|
-
declare function UserMenu({ name, subtitle, avatarUrl, children, themeSwitch, onSignOut, signOutLabel, className }: UserMenuProps): React.JSX.Element;
|
|
24
|
+
declare function UserMenu({ name, subtitle, avatarUrl, color, children, themeSwitch, onSignOut, signOutLabel, className }: UserMenuProps): React.JSX.Element;
|
|
22
25
|
export { UserMenu };
|
|
@@ -28,7 +28,7 @@ const themes = [
|
|
|
28
28
|
* The signed-in user as a menu button, for a sidebar footer or top bar: theme choice, app items such as
|
|
29
29
|
* Settings, and sign out. In a sidebar icon rail the trigger is just the avatar and the menu opens to the side.
|
|
30
30
|
*/
|
|
31
|
-
function UserMenu({ name, subtitle, avatarUrl, children, themeSwitch = true, onSignOut, signOutLabel = "Sign out", className }) {
|
|
31
|
+
function UserMenu({ name, subtitle, avatarUrl, color, children, themeSwitch = true, onSignOut, signOutLabel = "Sign out", className }) {
|
|
32
32
|
const { mode, setMode } = useTheme();
|
|
33
33
|
const sidebar = useSidebarOptional();
|
|
34
34
|
const rail = sidebar?.state === "collapsed" && !sidebar.isMobile;
|
|
@@ -40,6 +40,7 @@ function UserMenu({ name, subtitle, avatarUrl, children, themeSwitch = true, onS
|
|
|
40
40
|
name,
|
|
41
41
|
subtitle,
|
|
42
42
|
avatarUrl,
|
|
43
|
+
color,
|
|
43
44
|
className: "flex-1"
|
|
44
45
|
}), /* @__PURE__ */ jsx(ChevronsUpDownIcon, { className: "size-4 shrink-0 text-muted-foreground group-data-[collapsible=icon]:hidden" })]
|
|
45
46
|
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
@@ -18,10 +18,22 @@ export interface AskBoxProps extends Omit<React.ComponentProps<"form">, "onSubmi
|
|
|
18
18
|
lang?: string;
|
|
19
19
|
disabled?: boolean;
|
|
20
20
|
autoFocus?: boolean;
|
|
21
|
+
/** The assistant is working: sending is blocked and, with `onStop`, the send button becomes Stop. */
|
|
22
|
+
pending?: boolean;
|
|
23
|
+
/** Stops the assistant. Shown as a Stop button while `pending`. */
|
|
24
|
+
onStop?: () => void;
|
|
25
|
+
/**
|
|
26
|
+
* The answer, shown under the box (text, Prose, ActionReceipts). Passing it, even `null`, turns the reply area on:
|
|
27
|
+
* while `pending` with no answer yet it shows a typing indicator. `useAssistant().reply` fits here.
|
|
28
|
+
*/
|
|
29
|
+
reply?: React.ReactNode;
|
|
30
|
+
/** Link under the reply to continue in the full chat, labelled "Open chat". */
|
|
31
|
+
chatHref?: string;
|
|
21
32
|
}
|
|
22
33
|
/**
|
|
23
34
|
* The large rounded input an assistant starts from: auto-growing text, chips while typing, suggestion pills and the
|
|
24
|
-
* DictationButton (mic, stop, send). The focus ring is on the whole box.
|
|
35
|
+
* DictationButton (mic, stop, send). The focus ring is on the whole box. With `reply` the answer shows under the box;
|
|
36
|
+
* with `pending` and `onStop` the send button becomes Stop.
|
|
25
37
|
*/
|
|
26
|
-
declare function AskBox({ value, onValueChange, onSubmit, placeholder, chips, suggestions, onSuggestion, dictation, lang, disabled, autoFocus, className, ...props }: AskBoxProps): React.JSX.Element;
|
|
38
|
+
declare function AskBox({ value, onValueChange, onSubmit, placeholder, chips, suggestions, onSuggestion, dictation, lang, disabled, autoFocus, pending, onStop, reply, chatHref, className, ...props }: AskBoxProps): React.JSX.Element;
|
|
27
39
|
export { AskBox };
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { TypingIndicator } from "./chat.js";
|
|
2
3
|
import { DictationButton } from "./dictation.js";
|
|
3
4
|
import { Button } from "../ui/button.js";
|
|
4
5
|
import { cn } from "../../lib/utils.js";
|
|
5
6
|
import * as React from "react";
|
|
6
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { ArrowRightIcon, SquareIcon } from "lucide-react";
|
|
7
9
|
// src/components/ai/ask-box.tsx
|
|
8
10
|
const noSpeech = {
|
|
9
11
|
isSupported: () => false,
|
|
@@ -11,15 +13,20 @@ const noSpeech = {
|
|
|
11
13
|
};
|
|
12
14
|
/**
|
|
13
15
|
* The large rounded input an assistant starts from: auto-growing text, chips while typing, suggestion pills and the
|
|
14
|
-
* DictationButton (mic, stop, send). The focus ring is on the whole box.
|
|
16
|
+
* DictationButton (mic, stop, send). The focus ring is on the whole box. With `reply` the answer shows under the box;
|
|
17
|
+
* with `pending` and `onStop` the send button becomes Stop.
|
|
15
18
|
*/
|
|
16
|
-
function AskBox({ value, onValueChange, onSubmit, placeholder = "Ask anything", chips, suggestions, onSuggestion, dictation, lang, disabled, autoFocus, className, ...props }) {
|
|
19
|
+
function AskBox({ value, onValueChange, onSubmit, placeholder = "Ask anything", chips, suggestions, onSuggestion, dictation, lang, disabled, autoFocus, pending, onStop, reply, chatHref, className, ...props }) {
|
|
17
20
|
const id = React.useId();
|
|
18
21
|
const submit = () => {
|
|
19
22
|
const text = value.trim();
|
|
20
|
-
if (text
|
|
23
|
+
if (!text || disabled || pending) return;
|
|
24
|
+
onSubmit(text);
|
|
25
|
+
document.getElementById(id)?.focus();
|
|
21
26
|
};
|
|
22
27
|
const hasText = value.trim().length > 0;
|
|
28
|
+
const hasReply = reply != null && reply !== false;
|
|
29
|
+
const showReply = hasReply || pending && reply !== void 0;
|
|
23
30
|
return /* @__PURE__ */ jsxs("form", {
|
|
24
31
|
"data-slot": "ask-box",
|
|
25
32
|
className: cn("flex w-full min-w-0 flex-col gap-3", className),
|
|
@@ -28,58 +35,87 @@ function AskBox({ value, onValueChange, onSubmit, placeholder = "Ask anything",
|
|
|
28
35
|
submit();
|
|
29
36
|
},
|
|
30
37
|
...props,
|
|
31
|
-
children: [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
e.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
38
|
+
children: [
|
|
39
|
+
/* @__PURE__ */ jsxs("div", {
|
|
40
|
+
className: "focus-box flex flex-col gap-2 rounded-3xl border border-transparent bg-muted py-2.5 pr-2.5 pl-5",
|
|
41
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
42
|
+
className: "flex items-end gap-2",
|
|
43
|
+
children: [
|
|
44
|
+
/* @__PURE__ */ jsx("label", {
|
|
45
|
+
htmlFor: id,
|
|
46
|
+
className: "sr-only",
|
|
47
|
+
children: placeholder
|
|
48
|
+
}),
|
|
49
|
+
/* @__PURE__ */ jsx("textarea", {
|
|
50
|
+
id,
|
|
51
|
+
rows: 1,
|
|
52
|
+
value,
|
|
53
|
+
disabled,
|
|
54
|
+
autoFocus,
|
|
55
|
+
placeholder,
|
|
56
|
+
onChange: (e) => onValueChange(e.target.value),
|
|
57
|
+
onKeyDown: (e) => {
|
|
58
|
+
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
submit();
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
className: "field-sizing-content max-h-48 min-h-10 min-w-0 flex-1 resize-none border-0 bg-transparent py-2 text-base leading-6 text-foreground outline-none placeholder:text-muted-foreground"
|
|
64
|
+
}),
|
|
65
|
+
pending && onStop ? /* @__PURE__ */ jsx(Button, {
|
|
66
|
+
type: "button",
|
|
67
|
+
size: "icon",
|
|
68
|
+
shape: "pill",
|
|
69
|
+
className: "size-10 shrink-0",
|
|
70
|
+
"aria-label": "Stop",
|
|
71
|
+
onClick: () => {
|
|
72
|
+
onStop();
|
|
73
|
+
document.getElementById(id)?.focus();
|
|
74
|
+
},
|
|
75
|
+
children: /* @__PURE__ */ jsx(SquareIcon, { className: "size-3.5 fill-current" })
|
|
76
|
+
}) : /* @__PURE__ */ jsx(DictationButton, {
|
|
77
|
+
value,
|
|
78
|
+
disabled: disabled || pending,
|
|
79
|
+
source: dictation === false ? noSpeech : dictation,
|
|
80
|
+
lang,
|
|
81
|
+
onTranscript: (text) => onValueChange(value ? `${value.replace(/\s+$/, "")} ${text}` : text)
|
|
82
|
+
})
|
|
83
|
+
]
|
|
84
|
+
}), hasText && chips && /* @__PURE__ */ jsx("div", {
|
|
85
|
+
"data-slot": "ask-box-chips",
|
|
86
|
+
className: "flex flex-wrap gap-1.5 pb-1",
|
|
87
|
+
children: chips
|
|
88
|
+
})]
|
|
89
|
+
}),
|
|
90
|
+
showReply && /* @__PURE__ */ jsxs("div", {
|
|
91
|
+
"data-slot": "ask-box-reply",
|
|
92
|
+
"aria-live": "polite",
|
|
93
|
+
"aria-busy": pending || void 0,
|
|
94
|
+
className: "flex min-w-0 flex-col gap-3 px-2 text-[15px] leading-relaxed",
|
|
95
|
+
children: [hasReply ? reply : /* @__PURE__ */ jsx(TypingIndicator, { label: "Working" }), chatHref && /* @__PURE__ */ jsx(Button, {
|
|
96
|
+
asChild: true,
|
|
97
|
+
variant: "link",
|
|
98
|
+
className: "self-start px-0 max-md:min-h-11",
|
|
99
|
+
children: /* @__PURE__ */ jsxs("a", {
|
|
100
|
+
href: chatHref,
|
|
101
|
+
children: ["Open chat", /* @__PURE__ */ jsx(ArrowRightIcon, {})]
|
|
63
102
|
})
|
|
64
|
-
]
|
|
65
|
-
}),
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
children: s
|
|
81
|
-
}, s))
|
|
82
|
-
})]
|
|
103
|
+
})]
|
|
104
|
+
}),
|
|
105
|
+
suggestions && suggestions.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
106
|
+
"data-slot": "ask-box-suggestions",
|
|
107
|
+
className: "flex flex-wrap gap-2",
|
|
108
|
+
children: suggestions.map((s) => /* @__PURE__ */ jsx(Button, {
|
|
109
|
+
type: "button",
|
|
110
|
+
variant: "outline",
|
|
111
|
+
size: "sm",
|
|
112
|
+
shape: "pill",
|
|
113
|
+
disabled,
|
|
114
|
+
onClick: () => onSuggestion ? onSuggestion(s) : onSubmit(s),
|
|
115
|
+
children: s
|
|
116
|
+
}, s))
|
|
117
|
+
})
|
|
118
|
+
]
|
|
83
119
|
});
|
|
84
120
|
}
|
|
85
121
|
export { AskBox };
|
|
@@ -25,6 +25,12 @@ export interface AssistantSendContext {
|
|
|
25
25
|
}) => string;
|
|
26
26
|
/** Replace a message's content, e.g. while a reply streams in. */
|
|
27
27
|
update: (id: string, content: React.ReactNode) => void;
|
|
28
|
+
/** Aborted when the user presses Stop. Pass it to `fetch`, or listen for `abort` to tell your server to stop. */
|
|
29
|
+
signal: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
export interface AssistantSendOptions {
|
|
32
|
+
/** Open the dock. Default true; false for an AskBox that shows the answer inline. */
|
|
33
|
+
open?: boolean;
|
|
28
34
|
}
|
|
29
35
|
export interface AssistantContextValue {
|
|
30
36
|
open: boolean;
|
|
@@ -33,10 +39,14 @@ export interface AssistantContextValue {
|
|
|
33
39
|
messages: AssistantMessage[];
|
|
34
40
|
/** True while the send handler runs. */
|
|
35
41
|
pending: boolean;
|
|
42
|
+
/** The assistant messages after the latest user message, ready for `AskBox` `reply`; null until there is one. */
|
|
43
|
+
reply: React.ReactNode;
|
|
36
44
|
page: AssistantPage | null;
|
|
37
45
|
setPage: (page: AssistantPage | null) => void;
|
|
38
|
-
/** Adds the user message, opens the dock and runs the app's onSend. */
|
|
39
|
-
send: (text: string) => Promise<void>;
|
|
46
|
+
/** Adds the user message, opens the dock (unless `{ open: false }`) and runs the app's onSend. */
|
|
47
|
+
send: (text: string, options?: AssistantSendOptions) => Promise<void>;
|
|
48
|
+
/** Stops the running send: aborts its `signal`, ends `pending` and drops its return value and later `append`/`update` calls. */
|
|
49
|
+
stop: () => void;
|
|
40
50
|
append: AssistantSendContext["append"];
|
|
41
51
|
update: AssistantSendContext["update"];
|
|
42
52
|
clear: () => void;
|
|
@@ -44,7 +54,7 @@ export interface AssistantContextValue {
|
|
|
44
54
|
export interface AssistantProviderProps {
|
|
45
55
|
/**
|
|
46
56
|
* The app's send handler. Return the reply (text or nodes) to append it, or call `append` / `update` yourself
|
|
47
|
-
* (for streaming or receipts) and return nothing. A thrown error is shown as the reply.
|
|
57
|
+
* (for streaming or receipts) and return nothing. A thrown error is shown as the reply. Stop aborts `signal`.
|
|
48
58
|
*/
|
|
49
59
|
onSend: (text: string, context: AssistantSendContext) => React.ReactNode | Promise<React.ReactNode>;
|
|
50
60
|
initialMessages?: AssistantMessage[];
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { AskBox } from "./ask-box.js";
|
|
3
3
|
import { AssistantAvatar, ChatMessage, ChatThread, TypingIndicator } from "./chat.js";
|
|
4
4
|
import { Button } from "../ui/button.js";
|
|
5
|
+
import { useToastClearance } from "../../lib/toast-clearance.js";
|
|
5
6
|
import { cn } from "../../lib/utils.js";
|
|
6
7
|
import * as React from "react";
|
|
7
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -25,6 +26,7 @@ function AssistantProvider({ onSend, initialMessages = [], defaultOpen = false,
|
|
|
25
26
|
pageRef.current = page;
|
|
26
27
|
const onSendRef = React.useRef(onSend);
|
|
27
28
|
onSendRef.current = onSend;
|
|
29
|
+
const runRef = React.useRef(null);
|
|
28
30
|
const append = React.useCallback((m) => {
|
|
29
31
|
const id = m.id ?? newId();
|
|
30
32
|
setMessages((list) => [...list, {
|
|
@@ -39,43 +41,66 @@ function AssistantProvider({ onSend, initialMessages = [], defaultOpen = false,
|
|
|
39
41
|
content
|
|
40
42
|
} : m));
|
|
41
43
|
}, []);
|
|
42
|
-
const send = React.useCallback(async (text) => {
|
|
44
|
+
const send = React.useCallback(async (text, options) => {
|
|
43
45
|
const before = messagesRef.current;
|
|
46
|
+
runRef.current?.abort();
|
|
47
|
+
const run = new AbortController();
|
|
48
|
+
runRef.current = run;
|
|
44
49
|
append({
|
|
45
50
|
role: "user",
|
|
46
51
|
content: text
|
|
47
52
|
});
|
|
48
|
-
setOpen(true);
|
|
53
|
+
if (options?.open !== false) setOpen(true);
|
|
49
54
|
setPending(true);
|
|
50
55
|
try {
|
|
51
56
|
const reply = await onSendRef.current(text, {
|
|
52
57
|
page: pageRef.current,
|
|
53
58
|
messages: before,
|
|
54
|
-
append,
|
|
55
|
-
update
|
|
59
|
+
append: (m) => run.signal.aborted ? "" : append(m),
|
|
60
|
+
update: (id, content) => {
|
|
61
|
+
if (!run.signal.aborted) update(id, content);
|
|
62
|
+
},
|
|
63
|
+
signal: run.signal
|
|
56
64
|
});
|
|
65
|
+
if (run.signal.aborted) return;
|
|
57
66
|
if (reply !== void 0 && reply !== null && reply !== false) append({
|
|
58
67
|
role: "assistant",
|
|
59
68
|
content: reply
|
|
60
69
|
});
|
|
61
70
|
} catch (e) {
|
|
71
|
+
if (run.signal.aborted) return;
|
|
62
72
|
append({
|
|
63
73
|
role: "assistant",
|
|
64
74
|
content: e instanceof Error ? e.message : "Something went wrong."
|
|
65
75
|
});
|
|
66
76
|
} finally {
|
|
67
|
-
|
|
77
|
+
if (runRef.current === run) {
|
|
78
|
+
runRef.current = null;
|
|
79
|
+
setPending(false);
|
|
80
|
+
}
|
|
68
81
|
}
|
|
69
82
|
}, [append, update]);
|
|
83
|
+
const stop = React.useCallback(() => {
|
|
84
|
+
runRef.current?.abort();
|
|
85
|
+
runRef.current = null;
|
|
86
|
+
setPending(false);
|
|
87
|
+
}, []);
|
|
88
|
+
const reply = React.useMemo(() => {
|
|
89
|
+
const start = messages.findLastIndex((m) => m.role === "user");
|
|
90
|
+
const answer = start === -1 ? [] : messages.slice(start + 1);
|
|
91
|
+
return answer.length ? answer.map((m) => /* @__PURE__ */ jsx("div", { children: m.content }, m.id)) : null;
|
|
92
|
+
}, [messages]);
|
|
70
93
|
const value = React.useMemo(() => ({
|
|
71
94
|
open,
|
|
72
95
|
setOpen,
|
|
73
96
|
toggle: () => setOpen((v) => !v),
|
|
74
97
|
messages,
|
|
75
98
|
pending,
|
|
99
|
+
reply,
|
|
76
100
|
page,
|
|
77
101
|
setPage,
|
|
78
102
|
send,
|
|
103
|
+
stop,
|
|
79
104
|
append,
|
|
80
105
|
update,
|
|
81
106
|
clear: () => setMessages([])
|
|
@@ -83,8 +108,10 @@ function AssistantProvider({ onSend, initialMessages = [], defaultOpen = false,
|
|
|
83
108
|
open,
|
|
84
109
|
messages,
|
|
85
110
|
pending,
|
|
111
|
+
reply,
|
|
86
112
|
page,
|
|
87
113
|
send,
|
|
114
|
+
stop,
|
|
88
115
|
append,
|
|
89
116
|
update
|
|
90
117
|
]);
|
|
@@ -118,7 +145,7 @@ function useAssistantPage(page) {
|
|
|
118
145
|
* Sits above AppNav's phone tab bar.
|
|
119
146
|
*/
|
|
120
147
|
function AssistantDock({ title = "Assistant", icon = /* @__PURE__ */ jsx(SparklesIcon, {}), fullScreenHref, emptyTitle = "How can I help?", emptyDescription, suggestions, placeholder, dictation, launcher = true, launcherLabel, className, ...props }) {
|
|
121
|
-
const { open, setOpen, messages, pending, page, send } = useAssistant();
|
|
148
|
+
const { open, setOpen, messages, pending, page, send, stop } = useAssistant();
|
|
122
149
|
const [draft, setDraft] = React.useState("");
|
|
123
150
|
const titleId = React.useId();
|
|
124
151
|
const panelRef = React.useRef(null);
|
|
@@ -136,6 +163,7 @@ function AssistantDock({ title = "Assistant", icon = /* @__PURE__ */ jsx(Sparkle
|
|
|
136
163
|
panelRef.current?.querySelector("textarea")?.focus();
|
|
137
164
|
send(text);
|
|
138
165
|
};
|
|
166
|
+
useToastClearance("--hb-toast-launcher", "3.5rem", !open && launcher);
|
|
139
167
|
if (!open) {
|
|
140
168
|
if (!launcher) return null;
|
|
141
169
|
return /* @__PURE__ */ jsxs("button", {
|
|
@@ -220,7 +248,9 @@ function AssistantDock({ title = "Assistant", icon = /* @__PURE__ */ jsx(Sparkle
|
|
|
220
248
|
onSubmit: submit,
|
|
221
249
|
placeholder: placeholder ?? `Ask ${title}`,
|
|
222
250
|
suggestions: messages.length === 0 ? pills : void 0,
|
|
223
|
-
dictation
|
|
251
|
+
dictation,
|
|
252
|
+
pending,
|
|
253
|
+
onStop: stop
|
|
224
254
|
})
|
|
225
255
|
})
|
|
226
256
|
]
|
|
@@ -22,10 +22,17 @@ export interface InlineCodeProps extends React.ComponentProps<"code"> {
|
|
|
22
22
|
truncate?: number;
|
|
23
23
|
}
|
|
24
24
|
declare function InlineCode({ truncate, className, children, title, ...props }: InlineCodeProps): React.JSX.Element;
|
|
25
|
+
/**
|
|
26
|
+
* A blockquote that becomes a `Callout` when its first word is "Tip:" (the word is dropped).
|
|
27
|
+
* Prose applies it to blockquote children; pass it to react-markdown as `components={{ blockquote: ProseBlockquote }}`.
|
|
28
|
+
*/
|
|
29
|
+
declare function ProseBlockquote({ children, node: _node, ...props }: React.ComponentProps<"blockquote"> & {
|
|
30
|
+
node?: unknown;
|
|
31
|
+
}): React.JSX.Element;
|
|
25
32
|
/**
|
|
26
33
|
* Typography for long-form and model output. Styles plain HTML children
|
|
27
34
|
* (p, ul, ol, li, strong, a, code, pre, h1-h4, blockquote, table), so it works
|
|
28
|
-
* with react-markdown or any HTML renderer.
|
|
35
|
+
* with react-markdown or any HTML renderer. A blockquote starting "Tip:" renders as a Callout.
|
|
29
36
|
*/
|
|
30
|
-
declare function Prose({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
31
|
-
export { CodeSnippet, InlineCode, Prose };
|
|
37
|
+
declare function Prose({ className, children, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
38
|
+
export { CodeSnippet, InlineCode, Prose, ProseBlockquote };
|