@vvedantb/ui 0.38.0 → 0.39.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/README.md +27 -0
- package/package.json +1 -1
- package/src/ai-elements/message.tsx +230 -6
- package/src/ai-elements/prompt-input.tsx +62 -0
package/README.md
CHANGED
|
@@ -1633,6 +1633,33 @@ const config = {
|
|
|
1633
1633
|
|
|
1634
1634
|
Outside a `StickToBottom` (for example a page outline) use `JumpRail` with `items` (`{ id, title, preview?, level? }`), a `scrollRef` or `getScrollElement`, and `targetAttribute="data-toc-id"` or `resolveTarget`. `onReveal` runs when a target is not in the DOM; `onBeforeScroll` runs just before it. `onSelect(id)` runs after a click or Enter/Space pick, once the scroll has started or `onReveal` has run, so an app can move focus to the target for keyboard users; `ChatJumpRail` accepts it too. `observeKey` re-observes when the rendered window changes. The geometry helpers (`resolveJumpRailTopPercent`, `resolveJumpRailIndexFromPointer`, `resolveJumpRailHeightStyle`, `resolveJumpRailTickWidthClass`, `resolveJumpRailTickLevelStyle`) are exported.
|
|
1635
1635
|
- `Message`: `classes.root`, `classes.user`, `classes.assistant` (the role classes of `Message`), `classes.content` (all `MessageContent` classes), `classes.branchSelector`, `classes.branchIcon` (a class in place of the arrows' `size={14}`), and `lazyActionTooltips`: a `MessageAction` mounts its tooltip on first hover or focus (focus opens it), not with the button.
|
|
1636
|
+
- Quoted replies (0.39.0): `Message` takes `actions` (right-click menu plus hover toolbar) and `highlighted`; `MessageQuote` goes in `MessageContent` above the text; `ReplyQuoteChip` goes in `PromptInputHeader`.
|
|
1637
|
+
|
|
1638
|
+
```tsx
|
|
1639
|
+
<Message
|
|
1640
|
+
from="user"
|
|
1641
|
+
id={`msg-${m.id}`}
|
|
1642
|
+
highlighted={highlightId === m.id}
|
|
1643
|
+
actions={[
|
|
1644
|
+
{ key: "reply", label: "Reply", icon: <IconArrowBackUp size={14} />, onSelect: () => setReply(m) },
|
|
1645
|
+
{ key: "copy", label: "Copy", icon: <IconCopy size={14} />, onSelect: () => copy(m.text) },
|
|
1646
|
+
]}
|
|
1647
|
+
>
|
|
1648
|
+
<MessageContent>
|
|
1649
|
+
{m.quoted && <MessageQuote author={m.quoted.author} text={m.quoted.text} onPress={() => jumpTo(m.quoted.id)} />}
|
|
1650
|
+
{m.text}
|
|
1651
|
+
</MessageContent>
|
|
1652
|
+
</Message>
|
|
1653
|
+
|
|
1654
|
+
<PromptInput onSubmit={send}>
|
|
1655
|
+
<PromptInputHeader>
|
|
1656
|
+
{reply && <ReplyQuoteChip author={reply.author} text={reply.text} onDismiss={() => setReply(null)} />}
|
|
1657
|
+
</PromptInputHeader>
|
|
1658
|
+
{/* textarea, footer */}
|
|
1659
|
+
</PromptInput>
|
|
1660
|
+
```
|
|
1661
|
+
|
|
1662
|
+
The app owns the reply state and the scroll. `MessageHoverActions` needs a `relative` `Message` (the `actions` prop sets it).
|
|
1636
1663
|
- `PromptInput`: `classes.group` (the input group, default `overflow-hidden`), `classes.groupIdle` / `classes.groupDragOver` (added while no drag / an accepted drag is over the form), `classes.groupDefault` (added when no `containerClassName` is given), `classes.textarea`, `classes.footer`, `classes.submit`, `classes.tabItem`; `submitIcons.<send|submitted|streaming|error>`, `submitCrossfade` (cross-fade the submit icon on status change) and `menuTriggerAsChild` (default `true`: `PromptInputActionMenuTrigger` is the Radix trigger; `false` nests a button inside it). New `PromptInput` props: `dataDrop` (`{ type, onDrop }`: a non-file drag of that MIME type counts as a drag over the form and `onDrop` gets its string payload) and `inputGroupClassName` (same as `containerClassName`). The attachments context adds `replace(id, file)`; `accept` now matches extension patterns such as `.html` by file name; `useOptionalPromptInputController` is exported.
|
|
1637
1664
|
- `PromptInputSubmit`: new prop `queued` (0.30.0). Set it when a send would join a queue (the agent is still working). The send icon becomes `SendQueueIcon`, an up arrow that morphs into a stack of bars while `queued` is `true`. Leave it unset to keep `submitIcons.send`. `SendQueueIcon` (`queued`, `className` default `size-4`) is exported for other send buttons. It uses `m`, so `queued` needs the app's `LazyMotion`.
|
|
1638
1665
|
- `PromptInputSettings` (new): an overflow menu with a "Model" submenu (one radio list, or a submenu per provider). Entry: `providerLabel(provider)` (default the id), `providerIcon(provider)` (default none), `classes.trigger`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vvedantb/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Shared React 19 UI for web apps: one main JavaScript entry (primitives, shell pieces, app helpers including the Convex and Clerk provider, VITE env parsing and ensure-user, artifact viewers over the @vvedantb/server artifacts core, native tokens), third-party brand logos (also @vvedantb/ui/logos), motion-driven animated SVG icons (@vvedantb/ui/icons/animated), a client entry without @vvedantb/server, convex or Clerk for Chrome extensions, design-token CSS, shared Tailwind v4 CSS, a Tailwind preset and packaged Instrument Sans fonts.",
|
|
6
6
|
"type": "module",
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import type { UIMessage } from "ai";
|
|
3
|
-
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
|
|
3
|
+
import type { ComponentProps, HTMLAttributes, ReactElement, ReactNode } from "react";
|
|
4
4
|
import { Button } from "../ui/button";
|
|
5
5
|
import { ButtonGroup, ButtonGroupText } from "../ui/button-group";
|
|
6
|
+
import {
|
|
7
|
+
ContextMenu,
|
|
8
|
+
ContextMenuContent,
|
|
9
|
+
ContextMenuItem,
|
|
10
|
+
ContextMenuTrigger,
|
|
11
|
+
} from "../ui/context-menu";
|
|
6
12
|
import {
|
|
7
13
|
Tooltip,
|
|
8
14
|
TooltipContent,
|
|
@@ -30,23 +36,233 @@ export interface MessageConfig {
|
|
|
30
36
|
branchIcon?: string;
|
|
31
37
|
};
|
|
32
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* One action of a message: a row in the right-click menu and, with an `icon`,
|
|
41
|
+
* a button in the hover toolbar. Start with Reply.
|
|
42
|
+
*/
|
|
43
|
+
export type MessageMenuAction = {
|
|
44
|
+
key: string;
|
|
45
|
+
label: string;
|
|
46
|
+
/** Shows the action in the hover toolbar (an icon button). Without it: menu only. */
|
|
47
|
+
icon?: ReactNode;
|
|
48
|
+
destructive?: boolean;
|
|
49
|
+
disabled?: boolean;
|
|
50
|
+
onSelect: () => void;
|
|
51
|
+
/** Force the hover toolbar on (`true`) or off (`false`). Default: on when there is an `icon`. */
|
|
52
|
+
hover?: boolean;
|
|
53
|
+
};
|
|
33
54
|
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
|
34
55
|
from: UIMessage["role"];
|
|
56
|
+
/**
|
|
57
|
+
* Right-click menu and hover toolbar actions, such as Reply. Without it (or
|
|
58
|
+
* empty) `Message` renders the same `div` as before. With it a right click
|
|
59
|
+
* opens the menu and a hover toolbar shows beside the bubble. The toolbar is
|
|
60
|
+
* absolutely positioned and takes no layout space.
|
|
61
|
+
*/
|
|
62
|
+
actions?: MessageMenuAction[];
|
|
63
|
+
/**
|
|
64
|
+
* The target of a quote click: a soft primary wash and ring. Set it back to
|
|
65
|
+
* `false` about 1.5 seconds later and it fades out (`duration-1000`). Sets
|
|
66
|
+
* `data-highlighted`.
|
|
67
|
+
*/
|
|
68
|
+
highlighted?: boolean;
|
|
35
69
|
};
|
|
36
|
-
export const Message = ({
|
|
70
|
+
export const Message = ({
|
|
71
|
+
className,
|
|
72
|
+
from,
|
|
73
|
+
actions,
|
|
74
|
+
highlighted,
|
|
75
|
+
children,
|
|
76
|
+
...props
|
|
77
|
+
}: MessageProps) => {
|
|
37
78
|
const classes = useUiConfig("Message")?.classes;
|
|
38
79
|
const merge = useCn();
|
|
39
|
-
|
|
80
|
+
const hasActions = actions !== undefined && actions.length > 0;
|
|
81
|
+
const root = (
|
|
40
82
|
<div
|
|
41
83
|
className={merge(
|
|
42
84
|
classes?.root ?? "group flex w-full max-w-[95%] flex-col gap-2",
|
|
43
85
|
from === "user"
|
|
44
86
|
? (classes?.user ?? "is-user ml-auto justify-end")
|
|
45
87
|
: (classes?.assistant ?? "is-assistant"),
|
|
88
|
+
highlighted !== undefined &&
|
|
89
|
+
"rounded-xl transition-[background-color,box-shadow] duration-1000",
|
|
90
|
+
highlighted && "bg-primary/10 ring-2 ring-primary/30",
|
|
46
91
|
className,
|
|
47
92
|
)}
|
|
93
|
+
data-highlighted={highlighted ? "" : undefined}
|
|
48
94
|
{...props}
|
|
49
|
-
|
|
95
|
+
>
|
|
96
|
+
{hasActions ? (
|
|
97
|
+
<div
|
|
98
|
+
className={
|
|
99
|
+
from === "user"
|
|
100
|
+
? "relative ml-auto w-fit max-w-full"
|
|
101
|
+
: "relative w-fit max-w-full"
|
|
102
|
+
}
|
|
103
|
+
>
|
|
104
|
+
{children}
|
|
105
|
+
<MessageHoverActions actions={actions} from={from} />
|
|
106
|
+
</div>
|
|
107
|
+
) : (
|
|
108
|
+
children
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
return hasActions ? (
|
|
113
|
+
<MessageContextMenu actions={actions}>{root}</MessageContextMenu>
|
|
114
|
+
) : (
|
|
115
|
+
root
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
export type MessageContextMenuProps = {
|
|
119
|
+
actions: MessageMenuAction[];
|
|
120
|
+
/** One element, which becomes the right-click target (Radix `asChild`). */
|
|
121
|
+
children: ReactElement;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* The right-click menu of a message: `actions` as `ContextMenuItem`s,
|
|
125
|
+
* destructive ones in the danger colour. With no actions it returns `children`.
|
|
126
|
+
*/
|
|
127
|
+
export const MessageContextMenu = ({
|
|
128
|
+
actions,
|
|
129
|
+
children,
|
|
130
|
+
}: MessageContextMenuProps) => {
|
|
131
|
+
if (actions.length === 0) return children;
|
|
132
|
+
return (
|
|
133
|
+
<ContextMenu>
|
|
134
|
+
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
|
|
135
|
+
<ContextMenuContent aria-label="Message actions">
|
|
136
|
+
{actions.map((action) => (
|
|
137
|
+
<ContextMenuItem
|
|
138
|
+
key={action.key}
|
|
139
|
+
disabled={action.disabled}
|
|
140
|
+
startContent={action.icon}
|
|
141
|
+
className={action.destructive ? "text-danger" : undefined}
|
|
142
|
+
onSelect={action.onSelect}
|
|
143
|
+
>
|
|
144
|
+
{action.label}
|
|
145
|
+
</ContextMenuItem>
|
|
146
|
+
))}
|
|
147
|
+
</ContextMenuContent>
|
|
148
|
+
</ContextMenu>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
export type MessageHoverActionsProps = Omit<
|
|
152
|
+
ComponentProps<"div">,
|
|
153
|
+
"children"
|
|
154
|
+
> & {
|
|
155
|
+
actions: MessageMenuAction[];
|
|
156
|
+
/** Role of the message: user toolbars sit left of the bubble, others right. Default `assistant`. */
|
|
157
|
+
from?: UIMessage["role"];
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* A small toolbar of icon buttons that shows on hover or focus of the
|
|
161
|
+
* enclosing `Message` (the `group` class, `reveal-on-hover`; always visible on
|
|
162
|
+
* touch). It is absolutely positioned, so it takes no layout space: beside the
|
|
163
|
+
* bubble, top-aligned (left of a user bubble, right of an assistant message).
|
|
164
|
+
* On narrow screens it moves to the bubble's top edge. Draws the actions that
|
|
165
|
+
* have an `icon`. Place it inside a `relative w-fit` wrapper around the
|
|
166
|
+
* bubble; the `actions` prop on `Message` does this.
|
|
167
|
+
*/
|
|
168
|
+
export const MessageHoverActions = ({
|
|
169
|
+
actions,
|
|
170
|
+
from = "assistant",
|
|
171
|
+
className,
|
|
172
|
+
...props
|
|
173
|
+
}: MessageHoverActionsProps) => {
|
|
174
|
+
const merge = useCn();
|
|
175
|
+
const shown = actions.filter((action) =>
|
|
176
|
+
action.hover === undefined ? action.icon !== undefined : action.hover,
|
|
177
|
+
);
|
|
178
|
+
if (shown.length === 0) return null;
|
|
179
|
+
return (
|
|
180
|
+
<div
|
|
181
|
+
role="toolbar"
|
|
182
|
+
aria-label="Message actions"
|
|
183
|
+
className={merge(
|
|
184
|
+
"reveal-on-hover absolute top-0 z-10 flex w-fit items-center gap-0.5 rounded-lg border border-border bg-surface p-0.5 shadow-sm transition-opacity",
|
|
185
|
+
from === "user"
|
|
186
|
+
? "right-full mr-1 max-sm:right-2 max-sm:mr-0 max-sm:-translate-y-[62%] max-sm:[&>button]:size-6"
|
|
187
|
+
: "left-full ml-1 max-sm:left-auto max-sm:right-0 max-sm:ml-0 max-sm:-translate-y-full max-sm:[&>button]:size-6",
|
|
188
|
+
className,
|
|
189
|
+
)}
|
|
190
|
+
{...props}
|
|
191
|
+
>
|
|
192
|
+
{shown.map((action) => (
|
|
193
|
+
<MessageAction
|
|
194
|
+
key={action.key}
|
|
195
|
+
label={action.label}
|
|
196
|
+
tooltip={action.label}
|
|
197
|
+
tooltipSide="bottom"
|
|
198
|
+
// Narrow: the toolbar sits on the bubble corner and a tooltip below it would cover the text.
|
|
199
|
+
tooltipClassName="max-sm:hidden"
|
|
200
|
+
disabled={action.disabled}
|
|
201
|
+
className={action.destructive ? "text-danger" : undefined}
|
|
202
|
+
onClick={action.onSelect}
|
|
203
|
+
>
|
|
204
|
+
{action.icon}
|
|
205
|
+
</MessageAction>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
export type MessageQuoteProps = Omit<ComponentProps<"div">, "onClick"> & {
|
|
211
|
+
/** Who wrote the quoted message. */
|
|
212
|
+
author: string;
|
|
213
|
+
/** The quoted text, cut to two lines. */
|
|
214
|
+
text: string;
|
|
215
|
+
/** Makes the quote a button, so the app can scroll to and highlight the original. */
|
|
216
|
+
onPress?: () => void;
|
|
217
|
+
};
|
|
218
|
+
/**
|
|
219
|
+
* A compact quote inside a message bubble, above its text: a left border, the
|
|
220
|
+
* author and two lines of the text. It reads on the primary user bubble, the
|
|
221
|
+
* border and text take the bubble colour. With `onPress` it is a button.
|
|
222
|
+
*/
|
|
223
|
+
export const MessageQuote = ({
|
|
224
|
+
author,
|
|
225
|
+
text,
|
|
226
|
+
onPress,
|
|
227
|
+
className,
|
|
228
|
+
...props
|
|
229
|
+
}: MessageQuoteProps) => {
|
|
230
|
+
const merge = useCn();
|
|
231
|
+
const body = (
|
|
232
|
+
<>
|
|
233
|
+
<span className="block truncate text-xs font-semibold opacity-70">
|
|
234
|
+
{author}
|
|
235
|
+
</span>
|
|
236
|
+
<span className="line-clamp-2 block text-sm opacity-90">{text}</span>
|
|
237
|
+
</>
|
|
238
|
+
);
|
|
239
|
+
const base = "block min-w-0 border-l-2 border-current/40 pl-2.5 text-left";
|
|
240
|
+
if (onPress === undefined) {
|
|
241
|
+
return (
|
|
242
|
+
<div
|
|
243
|
+
role="group"
|
|
244
|
+
aria-label={`Quoted message from ${author}`}
|
|
245
|
+
className={merge(base, className)}
|
|
246
|
+
{...props}
|
|
247
|
+
>
|
|
248
|
+
{body}
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return (
|
|
253
|
+
<div
|
|
254
|
+
className={merge(base, className)}
|
|
255
|
+
{...props}
|
|
256
|
+
>
|
|
257
|
+
<button
|
|
258
|
+
type="button"
|
|
259
|
+
aria-label={`Go to quoted message from ${author}`}
|
|
260
|
+
onClick={onPress}
|
|
261
|
+
className="block w-full min-w-0 cursor-pointer text-left focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
262
|
+
>
|
|
263
|
+
{body}
|
|
264
|
+
</button>
|
|
265
|
+
</div>
|
|
50
266
|
);
|
|
51
267
|
};
|
|
52
268
|
export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
|
|
@@ -88,6 +304,10 @@ export const MessageActionsBar = ({
|
|
|
88
304
|
};
|
|
89
305
|
export type MessageActionProps = ComponentProps<typeof Button> & {
|
|
90
306
|
tooltip?: string;
|
|
307
|
+
/** Side of the tooltip. Default: the Radix default (`top`). */
|
|
308
|
+
tooltipSide?: "top" | "right" | "bottom" | "left";
|
|
309
|
+
/** Extra classes for the tooltip content. */
|
|
310
|
+
tooltipClassName?: string;
|
|
91
311
|
label?: string;
|
|
92
312
|
};
|
|
93
313
|
export const MessageAction = (props: MessageActionProps) => {
|
|
@@ -96,6 +316,8 @@ export const MessageAction = (props: MessageActionProps) => {
|
|
|
96
316
|
};
|
|
97
317
|
function EagerMessageAction({
|
|
98
318
|
tooltip,
|
|
319
|
+
tooltipSide,
|
|
320
|
+
tooltipClassName,
|
|
99
321
|
children,
|
|
100
322
|
label,
|
|
101
323
|
variant = "ghost",
|
|
@@ -113,7 +335,7 @@ function EagerMessageAction({
|
|
|
113
335
|
<TooltipProvider>
|
|
114
336
|
<Tooltip>
|
|
115
337
|
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
116
|
-
<TooltipContent>
|
|
338
|
+
<TooltipContent side={tooltipSide} className={tooltipClassName}>
|
|
117
339
|
<p>{tooltip}</p>
|
|
118
340
|
</TooltipContent>
|
|
119
341
|
</Tooltip>
|
|
@@ -129,6 +351,8 @@ function EagerMessageAction({
|
|
|
129
351
|
*/
|
|
130
352
|
function LazyMessageAction({
|
|
131
353
|
tooltip,
|
|
354
|
+
tooltipSide,
|
|
355
|
+
tooltipClassName,
|
|
132
356
|
children,
|
|
133
357
|
label,
|
|
134
358
|
variant = "ghost",
|
|
@@ -166,7 +390,7 @@ function LazyMessageAction({
|
|
|
166
390
|
{/* The focus that armed it landed before the tooltip existed. */}
|
|
167
391
|
<Tooltip defaultOpen={armedBy === "focus"}>
|
|
168
392
|
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
169
|
-
<TooltipContent>
|
|
393
|
+
<TooltipContent side={tooltipSide} className={tooltipClassName}>
|
|
170
394
|
<p>{tooltip}</p>
|
|
171
395
|
</TooltipContent>
|
|
172
396
|
</Tooltip>
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
SelectTrigger,
|
|
48
48
|
SelectValue,
|
|
49
49
|
} from "../ui/radix-select";
|
|
50
|
+
import { Button } from "../ui/button";
|
|
50
51
|
import { CrossfadeIconSlot } from "../ui/crossfade-icon";
|
|
51
52
|
import { SendQueueIcon } from "../ui/send-queue-icon";
|
|
52
53
|
import { Spinner } from "../ui/spinner";
|
|
@@ -811,6 +812,67 @@ export const PromptInputHeader = ({
|
|
|
811
812
|
/>
|
|
812
813
|
);
|
|
813
814
|
};
|
|
815
|
+
export type ReplyQuoteChipProps = Omit<ComponentProps<"div">, "children"> & {
|
|
816
|
+
/** Who wrote the message being replied to. */
|
|
817
|
+
author: string;
|
|
818
|
+
/** Its text, cut to two lines. */
|
|
819
|
+
text: string;
|
|
820
|
+
/** Called by the X button. Clear the reply in the app. */
|
|
821
|
+
onDismiss?: () => void;
|
|
822
|
+
/** Default "Dismiss reply quote". */
|
|
823
|
+
dismissLabel?: string;
|
|
824
|
+
};
|
|
825
|
+
/**
|
|
826
|
+
* The chip above the composer while a reply is pending: an accent bar, the
|
|
827
|
+
* author, two lines of the message and an X. Put it in `PromptInputHeader`:
|
|
828
|
+
*
|
|
829
|
+
* ```tsx
|
|
830
|
+
* <PromptInputHeader>
|
|
831
|
+
* {reply && <ReplyQuoteChip author={reply.author} text={reply.text} onDismiss={() => setReply(null)} />}
|
|
832
|
+
* </PromptInputHeader>
|
|
833
|
+
* ```
|
|
834
|
+
*/
|
|
835
|
+
export const ReplyQuoteChip = ({
|
|
836
|
+
author,
|
|
837
|
+
text,
|
|
838
|
+
onDismiss,
|
|
839
|
+
dismissLabel = "Dismiss reply quote",
|
|
840
|
+
className,
|
|
841
|
+
...props
|
|
842
|
+
}: ReplyQuoteChipProps) => {
|
|
843
|
+
const merge = useCn();
|
|
844
|
+
return (
|
|
845
|
+
<div
|
|
846
|
+
role="group"
|
|
847
|
+
aria-label={`Replying to ${author}`}
|
|
848
|
+
className={merge(
|
|
849
|
+
"flex w-full min-w-0 items-start gap-2 rounded-field border border-border bg-surface-secondary/60 py-1.5 pr-1 pl-3 text-sm",
|
|
850
|
+
className,
|
|
851
|
+
)}
|
|
852
|
+
{...props}
|
|
853
|
+
>
|
|
854
|
+
<span
|
|
855
|
+
aria-hidden="true"
|
|
856
|
+
className="w-0.5 shrink-0 self-stretch rounded-full bg-primary"
|
|
857
|
+
/>
|
|
858
|
+
<div className="min-w-0 flex-1">
|
|
859
|
+
<span className="block truncate text-xs font-semibold text-muted-foreground">
|
|
860
|
+
{author}
|
|
861
|
+
</span>
|
|
862
|
+
<span className="line-clamp-2 block text-foreground">{text}</span>
|
|
863
|
+
</div>
|
|
864
|
+
<Button
|
|
865
|
+
type="button"
|
|
866
|
+
variant="ghost"
|
|
867
|
+
size="icon-sm"
|
|
868
|
+
aria-label={dismissLabel}
|
|
869
|
+
onClick={onDismiss}
|
|
870
|
+
>
|
|
871
|
+
<IconX size={14} />
|
|
872
|
+
</Button>
|
|
873
|
+
</div>
|
|
874
|
+
);
|
|
875
|
+
};
|
|
814
876
|
export type PromptInputFooterProps = Omit<
|
|
815
877
|
ComponentProps<typeof InputGroupAddon>,
|
|
816
878
|
"align"
|