@burtson-labs/ui 0.11.0 → 0.12.1
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 +2 -2
- package/dist/components/audio-player.d.ts +39 -0
- package/dist/components/audio-player.js +250 -0
- package/dist/components/audio-player.js.map +1 -0
- package/dist/components/chat-history.d.ts +64 -0
- package/dist/components/chat-history.js +282 -0
- package/dist/components/chat-history.js.map +1 -0
- package/dist/components/chat-layout.d.ts +42 -0
- package/dist/components/chat-layout.js +217 -0
- package/dist/components/chat-layout.js.map +1 -0
- package/dist/components/combobox.js +1 -1
- package/dist/components/composer.d.ts +16 -1
- package/dist/components/composer.js +103 -20
- package/dist/components/composer.js.map +1 -1
- package/dist/components/data-table.js +1 -1
- package/dist/components/message-actions.d.ts +56 -0
- package/dist/components/message-actions.js +181 -0
- package/dist/components/message-actions.js.map +1 -0
- package/dist/components/secret-input.js +1 -1
- package/dist/components/select.js +1 -1
- package/dist/components/voice-recorder.d.ts +28 -0
- package/dist/components/voice-recorder.js +240 -0
- package/dist/components/voice-recorder.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +12 -7
- package/dist/styles.css +1 -1
- package/dist/tokens.js +1 -1
- package/dist/tokens.js.map +1 -1
- package/package.json +1 -1
|
@@ -27,12 +27,27 @@ export interface ComposerProps extends Omit<React.ComponentProps<'form'>, 'onSub
|
|
|
27
27
|
/** Extra controls left of the send button (model picker, tools). */
|
|
28
28
|
actions?: React.ReactNode;
|
|
29
29
|
label?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Files the person picked, dropped on the composer or pasted. Setting it
|
|
32
|
+
* shows a paperclip button. The composer only hands the files over: the
|
|
33
|
+
* app uploads them and renders their state in `attachments` (an
|
|
34
|
+
* AttachmentTray), with `attachmentCount` and `canSubmit` for sending.
|
|
35
|
+
*/
|
|
36
|
+
onAttach?: (files: File[]) => void;
|
|
37
|
+
/** File input `accept`, e.g. "image/*,.pdf". Dropped and pasted files are filtered too. */
|
|
38
|
+
accept?: string;
|
|
39
|
+
/** Allow several files at once. Default true. */
|
|
40
|
+
multiple?: boolean;
|
|
41
|
+
/** Accessible name of the attach button. */
|
|
42
|
+
attachLabel?: string;
|
|
30
43
|
}
|
|
44
|
+
/** Does `file` match an `accept` list ("image/*,.pdf,text/plain")? Empty accepts all. */
|
|
45
|
+
export declare function acceptsFile(file: Pick<File, 'name' | 'type'>, accept?: string): boolean;
|
|
31
46
|
/**
|
|
32
47
|
* The message box: grows with its text, Enter sends, Shift+Enter adds a
|
|
33
48
|
* line, and it never sends mid-composition (IME input).
|
|
34
49
|
*/
|
|
35
|
-
declare function Composer({ onSubmit, onSubmitError, submitErrorText, value: controlled, onValueChange, streaming, onStop, disabled, placeholder, attachments, attachmentCount, canSubmit, actions, label, className, ...props }: ComposerProps): React.JSX.Element;
|
|
50
|
+
declare function Composer({ onSubmit, onSubmitError, submitErrorText, value: controlled, onValueChange, streaming, onStop, disabled, placeholder, attachments, attachmentCount, canSubmit, actions, label, onAttach, accept, multiple, attachLabel, className, ...props }: ComposerProps): React.JSX.Element;
|
|
36
51
|
export interface SuggestionsProps extends Omit<React.ComponentProps<'div'>, 'onSelect'> {
|
|
37
52
|
items: string[];
|
|
38
53
|
onSelect: (item: string) => void;
|
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
import { cn } from "../lib/utils.js";
|
|
2
2
|
import { Button } from "./button.js";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import ArrowUp from "@burtson-labs/icons/react/arrow-up";
|
|
6
|
+
import Paperclip from "@burtson-labs/icons/react/paperclip";
|
|
6
7
|
import Square from "@burtson-labs/icons/react/square";
|
|
7
8
|
//#region src/components/composer.tsx
|
|
9
|
+
/** Does `file` match an `accept` list ("image/*,.pdf,text/plain")? Empty accepts all. */
|
|
10
|
+
function acceptsFile(file, accept) {
|
|
11
|
+
if (!accept?.trim()) return true;
|
|
12
|
+
const name = file.name.toLowerCase();
|
|
13
|
+
const type = (file.type || "").toLowerCase();
|
|
14
|
+
return accept.split(",").map((a) => a.trim().toLowerCase()).filter(Boolean).some((rule) => rule.startsWith(".") ? name.endsWith(rule) : rule.endsWith("/*") ? type.startsWith(rule.slice(0, -1)) : type === rule);
|
|
15
|
+
}
|
|
8
16
|
/**
|
|
9
17
|
* The message box: grows with its text, Enter sends, Shift+Enter adds a
|
|
10
18
|
* line, and it never sends mid-composition (IME input).
|
|
11
19
|
*/
|
|
12
|
-
function Composer({ onSubmit, onSubmitError, submitErrorText = "Message could not be sent. Your draft is saved here. Try again.", value: controlled, onValueChange, streaming = false, onStop, disabled = false, placeholder = "Ask anything…", attachments, attachmentCount = 0, canSubmit = true, actions, label = "Message", className, ...props }) {
|
|
20
|
+
function Composer({ onSubmit, onSubmitError, submitErrorText = "Message could not be sent. Your draft is saved here. Try again.", value: controlled, onValueChange, streaming = false, onStop, disabled = false, placeholder = "Ask anything…", attachments, attachmentCount = 0, canSubmit = true, actions, label = "Message", onAttach, accept, multiple = true, attachLabel = "Attach files", className, ...props }) {
|
|
21
|
+
const fileInput = React.useRef(null);
|
|
22
|
+
const [dragging, setDragging] = React.useState(false);
|
|
23
|
+
const dragDepth = React.useRef(0);
|
|
24
|
+
const attach = (list) => {
|
|
25
|
+
if (!onAttach || !list) return;
|
|
26
|
+
let files = Array.from(list).filter((f) => acceptsFile(f, accept));
|
|
27
|
+
if (!multiple) files = files.slice(0, 1);
|
|
28
|
+
if (files.length) onAttach(files);
|
|
29
|
+
};
|
|
30
|
+
const hasFiles = (e) => Array.from(e.dataTransfer?.types ?? []).includes("Files");
|
|
13
31
|
const [own, setOwn] = React.useState("");
|
|
14
32
|
const value = controlled ?? own;
|
|
15
33
|
const [pending, setPending] = React.useState(false);
|
|
@@ -51,13 +69,42 @@ function Composer({ onSubmit, onSubmitError, submitErrorText = "Message could no
|
|
|
51
69
|
};
|
|
52
70
|
return /* @__PURE__ */ jsxs("form", {
|
|
53
71
|
"data-slot": "composer",
|
|
72
|
+
"data-dragging": dragging || void 0,
|
|
54
73
|
onSubmit: (e) => {
|
|
55
74
|
e.preventDefault();
|
|
56
75
|
send();
|
|
57
76
|
},
|
|
58
|
-
|
|
77
|
+
onDragEnter: (e) => {
|
|
78
|
+
if (!onAttach || disabled || !hasFiles(e)) return;
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
dragDepth.current += 1;
|
|
81
|
+
setDragging(true);
|
|
82
|
+
},
|
|
83
|
+
onDragOver: (e) => {
|
|
84
|
+
if (!onAttach || disabled || !hasFiles(e)) return;
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
e.dataTransfer.dropEffect = "copy";
|
|
87
|
+
},
|
|
88
|
+
onDragLeave: () => {
|
|
89
|
+
if (!onAttach) return;
|
|
90
|
+
dragDepth.current = Math.max(0, dragDepth.current - 1);
|
|
91
|
+
if (dragDepth.current === 0) setDragging(false);
|
|
92
|
+
},
|
|
93
|
+
onDrop: (e) => {
|
|
94
|
+
if (!onAttach || disabled) return;
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
dragDepth.current = 0;
|
|
97
|
+
setDragging(false);
|
|
98
|
+
attach(e.dataTransfer?.files);
|
|
99
|
+
},
|
|
100
|
+
className: cn("relative grid gap-2 rounded-lg border border-input bg-surface p-2 shadow-xs transition-[border-color,box-shadow] focus-within:border-brand focus-within:ring-[3px] focus-within:ring-ring/15 dark:bg-surface-raised", "data-[dragging]:border-brand data-[dragging]:ring-[3px] data-[dragging]:ring-ring/20", disabled && "opacity-60", className),
|
|
59
101
|
...props,
|
|
60
102
|
children: [
|
|
103
|
+
dragging && /* @__PURE__ */ jsx("div", {
|
|
104
|
+
"aria-hidden": true,
|
|
105
|
+
className: "pointer-events-none absolute inset-1 z-10 grid place-items-center rounded-md border border-dashed border-brand/50 bg-brand-soft/80 text-sm font-medium text-brand-soft-foreground",
|
|
106
|
+
children: "Drop files to attach"
|
|
107
|
+
}),
|
|
61
108
|
/* @__PURE__ */ jsx("textarea", {
|
|
62
109
|
"aria-label": label,
|
|
63
110
|
rows: 1,
|
|
@@ -67,6 +114,14 @@ function Composer({ onSubmit, onSubmitError, submitErrorText = "Message could no
|
|
|
67
114
|
"aria-invalid": failed || void 0,
|
|
68
115
|
placeholder,
|
|
69
116
|
onChange: (e) => setValue(e.target.value),
|
|
117
|
+
onPaste: (e) => {
|
|
118
|
+
if (!onAttach) return;
|
|
119
|
+
const files = Array.from(e.clipboardData?.files ?? []);
|
|
120
|
+
if (files.length) {
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
attach(files);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
70
125
|
onKeyDown: (e) => {
|
|
71
126
|
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing && e.nativeEvent.keyCode !== 229) {
|
|
72
127
|
e.preventDefault();
|
|
@@ -87,25 +142,53 @@ function Composer({ onSubmit, onSubmitError, submitErrorText = "Message could no
|
|
|
87
142
|
}),
|
|
88
143
|
/* @__PURE__ */ jsxs("div", {
|
|
89
144
|
className: "flex items-center gap-1",
|
|
90
|
-
children: [
|
|
91
|
-
|
|
92
|
-
|
|
145
|
+
children: [
|
|
146
|
+
onAttach && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("input", {
|
|
147
|
+
ref: fileInput,
|
|
148
|
+
type: "file",
|
|
149
|
+
hidden: true,
|
|
150
|
+
tabIndex: -1,
|
|
151
|
+
accept,
|
|
152
|
+
multiple,
|
|
153
|
+
"data-slot": "composer-file-input",
|
|
154
|
+
onChange: (e) => {
|
|
155
|
+
attach(e.target.files);
|
|
156
|
+
e.target.value = "";
|
|
157
|
+
}
|
|
158
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
93
159
|
type: "button",
|
|
94
160
|
size: "icon-sm",
|
|
95
|
-
variant: "
|
|
96
|
-
"aria-label":
|
|
97
|
-
|
|
98
|
-
disabled
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
children: /* @__PURE__ */ jsx(
|
|
161
|
+
variant: "ghost",
|
|
162
|
+
"aria-label": attachLabel,
|
|
163
|
+
title: attachLabel,
|
|
164
|
+
disabled,
|
|
165
|
+
onClick: () => fileInput.current?.click(),
|
|
166
|
+
className: "pointer-coarse:size-11",
|
|
167
|
+
children: /* @__PURE__ */ jsx(Paperclip, {})
|
|
168
|
+
})] }),
|
|
169
|
+
actions,
|
|
170
|
+
/* @__PURE__ */ jsx("div", {
|
|
171
|
+
className: "ml-auto",
|
|
172
|
+
children: streaming ? /* @__PURE__ */ jsx(Button, {
|
|
173
|
+
type: "button",
|
|
174
|
+
size: "icon-sm",
|
|
175
|
+
variant: "secondary",
|
|
176
|
+
"aria-label": "Stop",
|
|
177
|
+
className: "pointer-coarse:size-11",
|
|
178
|
+
onClick: onStop,
|
|
179
|
+
disabled: !onStop || disabled,
|
|
180
|
+
children: /* @__PURE__ */ jsx(Square, { className: "fill-current" })
|
|
181
|
+
}) : /* @__PURE__ */ jsx(Button, {
|
|
182
|
+
type: "submit",
|
|
183
|
+
size: "icon-sm",
|
|
184
|
+
"aria-label": "Send",
|
|
185
|
+
className: "pointer-coarse:size-11",
|
|
186
|
+
loading: pending,
|
|
187
|
+
disabled: !canSend,
|
|
188
|
+
children: /* @__PURE__ */ jsx(ArrowUp, {})
|
|
189
|
+
})
|
|
107
190
|
})
|
|
108
|
-
|
|
191
|
+
]
|
|
109
192
|
})
|
|
110
193
|
]
|
|
111
194
|
});
|
|
@@ -127,6 +210,6 @@ function Suggestions({ items, onSelect, className, ...props }) {
|
|
|
127
210
|
});
|
|
128
211
|
}
|
|
129
212
|
//#endregion
|
|
130
|
-
export { Composer, Suggestions };
|
|
213
|
+
export { Composer, Suggestions, acceptsFile };
|
|
131
214
|
|
|
132
215
|
//# sourceMappingURL=composer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composer.js","names":[],"sources":["../../src/components/composer.tsx"],"sourcesContent":["import ArrowUp from '@burtson-labs/icons/react/arrow-up';\nimport Square from '@burtson-labs/icons/react/square';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Button } from './button';\n\nexport interface ComposerProps extends Omit<React.ComponentProps<'form'>, 'onSubmit' | 'onChange'> {\n /**\n * Clears after success. Reject a returned promise to retain the draft. The\n * text can be empty when `attachmentCount` allows an attachment-only send.\n */\n onSubmit: (text: string) => void | Promise<void>;\n onSubmitError?: (error: unknown) => void;\n submitErrorText?: string;\n /** Controlled value; leave out to let the composer keep its own. */\n value?: string;\n onValueChange?: (value: string) => void;\n /** While a reply streams, the send button becomes Stop. */\n streaming?: boolean;\n onStop?: () => void;\n disabled?: boolean;\n placeholder?: string;\n /** Attachment chips or a file button, shown under the text. */\n attachments?: React.ReactNode;\n /** Attachments ready to send. Above zero, an empty message can be sent. */\n attachmentCount?: number;\n /**\n * Extra gate on sending, e.g. false while an attachment is still parsing.\n * Unlike `disabled`, the person can keep typing.\n */\n canSubmit?: boolean;\n /** Extra controls left of the send button (model picker, tools). */\n actions?: React.ReactNode;\n label?: string;\n}\n\n/**\n * The message box: grows with its text, Enter sends, Shift+Enter adds a\n * line, and it never sends mid-composition (IME input).\n */\nfunction Composer({\n onSubmit,\n onSubmitError,\n submitErrorText = 'Message could not be sent. Your draft is saved here. Try again.',\n value: controlled,\n onValueChange,\n streaming = false,\n onStop,\n disabled = false,\n placeholder = 'Ask anything…',\n attachments,\n attachmentCount = 0,\n canSubmit = true,\n actions,\n label = 'Message',\n className,\n ...props\n}: ComposerProps) {\n const [own, setOwn] = React.useState('');\n const value = controlled ?? own;\n const [pending, setPending] = React.useState(false);\n const [failed, setFailed] = React.useState(false);\n const sending = React.useRef(false);\n const latest = React.useRef(value);\n const mounted = React.useRef(true);\n const errorId = React.useId();\n React.useEffect(() => {\n latest.current = value;\n }, [value]);\n React.useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n const setValue = (v: string) => {\n if (controlled === undefined) setOwn(v);\n onValueChange?.(v);\n };\n const canSend =\n canSubmit &&\n !disabled &&\n !streaming &&\n !pending &&\n (value.trim().length > 0 || attachmentCount > 0);\n const send = async () => {\n if (!canSend || sending.current) return;\n const draft = value;\n sending.current = true;\n setFailed(false);\n setPending(true);\n try {\n await onSubmit(draft.trim());\n if (mounted.current && latest.current === draft) setValue('');\n } catch (error) {\n if (mounted.current) setFailed(true);\n onSubmitError?.(error);\n } finally {\n sending.current = false;\n if (mounted.current) setPending(false);\n }\n };\n\n return (\n <form\n data-slot=\"composer\"\n onSubmit={(e) => {\n e.preventDefault();\n void send();\n }}\n className={cn(\n 'grid gap-2 rounded-lg border border-input bg-surface p-2 shadow-xs transition-[border-color,box-shadow] focus-within:border-brand focus-within:ring-[3px] focus-within:ring-ring/15 dark:bg-surface-raised',\n disabled && 'opacity-60',\n className,\n )}\n {...props}\n >\n <textarea\n aria-label={label}\n rows={1}\n value={value}\n disabled={disabled || pending}\n aria-describedby={failed ? errorId : undefined}\n aria-invalid={failed || undefined}\n placeholder={placeholder}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={(e) => {\n if (\n e.key === 'Enter' &&\n !e.shiftKey &&\n !e.nativeEvent.isComposing &&\n e.nativeEvent.keyCode !== 229\n ) {\n e.preventDefault();\n void send();\n }\n }}\n className=\"field-sizing-content max-h-48 min-h-9 w-full resize-none bg-transparent px-2 py-1.5 text-base sm:text-sm leading-6 outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n />\n {failed && (\n <p id={errorId} role=\"alert\" className=\"px-2 text-sm text-destructive\">\n {submitErrorText}\n </p>\n )}\n {attachments && <div className=\"flex flex-wrap gap-1.5 px-1\">{attachments}</div>}\n <div className=\"flex items-center gap-1\">\n {actions}\n <div className=\"ml-auto\">\n {streaming ? (\n <Button\n type=\"button\"\n size=\"icon-sm\"\n variant=\"secondary\"\n aria-label=\"Stop\"\n onClick={onStop}\n disabled={!onStop || disabled}\n >\n <Square className=\"fill-current\" />\n </Button>\n ) : (\n <Button\n type=\"submit\"\n size=\"icon-sm\"\n aria-label=\"Send\"\n loading={pending}\n disabled={!canSend}\n >\n <ArrowUp />\n </Button>\n )}\n </div>\n </div>\n </form>\n );\n}\n\nexport interface SuggestionsProps extends Omit<React.ComponentProps<'div'>, 'onSelect'> {\n items: string[];\n onSelect: (item: string) => void;\n}\n\n/** Starter prompts as chips. */\nfunction Suggestions({ items, onSelect, className, ...props }: SuggestionsProps) {\n return (\n <div\n data-slot=\"suggestions\"\n role=\"group\"\n aria-label=\"Suggestions\"\n className={cn('flex flex-wrap gap-2', className)}\n {...props}\n >\n {items.map((item) => (\n <button\n key={item}\n type=\"button\"\n onClick={() => onSelect(item)}\n className=\"animate-in rounded-full border border-border-strong bg-surface px-3 py-1.5 text-[13px] text-foreground outline-none transition-colors hover:border-brand/40 hover:bg-brand-soft hover:text-brand-soft-foreground focus-visible:ring-[3px] focus-visible:ring-ring/20\"\n >\n {item}\n </button>\n ))}\n </div>\n );\n}\n\nexport { Composer, Suggestions };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"composer.js","names":["_Fragment"],"sources":["../../src/components/composer.tsx"],"sourcesContent":["import ArrowUp from '@burtson-labs/icons/react/arrow-up';\nimport Paperclip from '@burtson-labs/icons/react/paperclip';\nimport Square from '@burtson-labs/icons/react/square';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { Button } from './button';\n\nexport interface ComposerProps extends Omit<React.ComponentProps<'form'>, 'onSubmit' | 'onChange'> {\n /**\n * Clears after success. Reject a returned promise to retain the draft. The\n * text can be empty when `attachmentCount` allows an attachment-only send.\n */\n onSubmit: (text: string) => void | Promise<void>;\n onSubmitError?: (error: unknown) => void;\n submitErrorText?: string;\n /** Controlled value; leave out to let the composer keep its own. */\n value?: string;\n onValueChange?: (value: string) => void;\n /** While a reply streams, the send button becomes Stop. */\n streaming?: boolean;\n onStop?: () => void;\n disabled?: boolean;\n placeholder?: string;\n /** Attachment chips or a file button, shown under the text. */\n attachments?: React.ReactNode;\n /** Attachments ready to send. Above zero, an empty message can be sent. */\n attachmentCount?: number;\n /**\n * Extra gate on sending, e.g. false while an attachment is still parsing.\n * Unlike `disabled`, the person can keep typing.\n */\n canSubmit?: boolean;\n /** Extra controls left of the send button (model picker, tools). */\n actions?: React.ReactNode;\n label?: string;\n /**\n * Files the person picked, dropped on the composer or pasted. Setting it\n * shows a paperclip button. The composer only hands the files over: the\n * app uploads them and renders their state in `attachments` (an\n * AttachmentTray), with `attachmentCount` and `canSubmit` for sending.\n */\n onAttach?: (files: File[]) => void;\n /** File input `accept`, e.g. \"image/*,.pdf\". Dropped and pasted files are filtered too. */\n accept?: string;\n /** Allow several files at once. Default true. */\n multiple?: boolean;\n /** Accessible name of the attach button. */\n attachLabel?: string;\n}\n\n/** Does `file` match an `accept` list (\"image/*,.pdf,text/plain\")? Empty accepts all. */\nexport function acceptsFile(file: Pick<File, 'name' | 'type'>, accept?: string): boolean {\n if (!accept?.trim()) return true;\n const name = file.name.toLowerCase();\n const type = (file.type || '').toLowerCase();\n return accept\n .split(',')\n .map((a) => a.trim().toLowerCase())\n .filter(Boolean)\n .some((rule) =>\n rule.startsWith('.')\n ? name.endsWith(rule)\n : rule.endsWith('/*')\n ? type.startsWith(rule.slice(0, -1))\n : type === rule,\n );\n}\n\n/**\n * The message box: grows with its text, Enter sends, Shift+Enter adds a\n * line, and it never sends mid-composition (IME input).\n */\nfunction Composer({\n onSubmit,\n onSubmitError,\n submitErrorText = 'Message could not be sent. Your draft is saved here. Try again.',\n value: controlled,\n onValueChange,\n streaming = false,\n onStop,\n disabled = false,\n placeholder = 'Ask anything…',\n attachments,\n attachmentCount = 0,\n canSubmit = true,\n actions,\n label = 'Message',\n onAttach,\n accept,\n multiple = true,\n attachLabel = 'Attach files',\n className,\n ...props\n}: ComposerProps) {\n const fileInput = React.useRef<HTMLInputElement>(null);\n const [dragging, setDragging] = React.useState(false);\n const dragDepth = React.useRef(0);\n const attach = (list: FileList | File[] | null | undefined) => {\n if (!onAttach || !list) return;\n let files = Array.from(list).filter((f) => acceptsFile(f, accept));\n if (!multiple) files = files.slice(0, 1);\n if (files.length) onAttach(files);\n };\n const hasFiles = (e: React.DragEvent) =>\n Array.from(e.dataTransfer?.types ?? []).includes('Files');\n const [own, setOwn] = React.useState('');\n const value = controlled ?? own;\n const [pending, setPending] = React.useState(false);\n const [failed, setFailed] = React.useState(false);\n const sending = React.useRef(false);\n const latest = React.useRef(value);\n const mounted = React.useRef(true);\n const errorId = React.useId();\n React.useEffect(() => {\n latest.current = value;\n }, [value]);\n React.useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n const setValue = (v: string) => {\n if (controlled === undefined) setOwn(v);\n onValueChange?.(v);\n };\n const canSend =\n canSubmit &&\n !disabled &&\n !streaming &&\n !pending &&\n (value.trim().length > 0 || attachmentCount > 0);\n const send = async () => {\n if (!canSend || sending.current) return;\n const draft = value;\n sending.current = true;\n setFailed(false);\n setPending(true);\n try {\n await onSubmit(draft.trim());\n if (mounted.current && latest.current === draft) setValue('');\n } catch (error) {\n if (mounted.current) setFailed(true);\n onSubmitError?.(error);\n } finally {\n sending.current = false;\n if (mounted.current) setPending(false);\n }\n };\n\n return (\n <form\n data-slot=\"composer\"\n data-dragging={dragging || undefined}\n onSubmit={(e) => {\n e.preventDefault();\n void send();\n }}\n onDragEnter={(e) => {\n if (!onAttach || disabled || !hasFiles(e)) return;\n e.preventDefault();\n dragDepth.current += 1;\n setDragging(true);\n }}\n onDragOver={(e) => {\n if (!onAttach || disabled || !hasFiles(e)) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = 'copy';\n }}\n onDragLeave={() => {\n if (!onAttach) return;\n dragDepth.current = Math.max(0, dragDepth.current - 1);\n if (dragDepth.current === 0) setDragging(false);\n }}\n onDrop={(e) => {\n if (!onAttach || disabled) return;\n e.preventDefault();\n dragDepth.current = 0;\n setDragging(false);\n attach(e.dataTransfer?.files);\n }}\n className={cn(\n 'relative grid gap-2 rounded-lg border border-input bg-surface p-2 shadow-xs transition-[border-color,box-shadow] focus-within:border-brand focus-within:ring-[3px] focus-within:ring-ring/15 dark:bg-surface-raised',\n 'data-[dragging]:border-brand data-[dragging]:ring-[3px] data-[dragging]:ring-ring/20',\n disabled && 'opacity-60',\n className,\n )}\n {...props}\n >\n {dragging && (\n <div\n aria-hidden\n className=\"pointer-events-none absolute inset-1 z-10 grid place-items-center rounded-md border border-dashed border-brand/50 bg-brand-soft/80 text-sm font-medium text-brand-soft-foreground\"\n >\n Drop files to attach\n </div>\n )}\n <textarea\n aria-label={label}\n rows={1}\n value={value}\n disabled={disabled || pending}\n aria-describedby={failed ? errorId : undefined}\n aria-invalid={failed || undefined}\n placeholder={placeholder}\n onChange={(e) => setValue(e.target.value)}\n onPaste={(e) => {\n if (!onAttach) return;\n const files = Array.from(e.clipboardData?.files ?? []);\n // Only take over the paste when it carries files; text pastes as text.\n if (files.length) {\n e.preventDefault();\n attach(files);\n }\n }}\n onKeyDown={(e) => {\n if (\n e.key === 'Enter' &&\n !e.shiftKey &&\n !e.nativeEvent.isComposing &&\n e.nativeEvent.keyCode !== 229\n ) {\n e.preventDefault();\n void send();\n }\n }}\n className=\"field-sizing-content max-h-48 min-h-9 w-full resize-none bg-transparent px-2 py-1.5 text-base sm:text-sm leading-6 outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n />\n {failed && (\n <p id={errorId} role=\"alert\" className=\"px-2 text-sm text-destructive\">\n {submitErrorText}\n </p>\n )}\n {attachments && <div className=\"flex flex-wrap gap-1.5 px-1\">{attachments}</div>}\n <div className=\"flex items-center gap-1\">\n {onAttach && (\n <>\n <input\n ref={fileInput}\n type=\"file\"\n hidden\n tabIndex={-1}\n accept={accept}\n multiple={multiple}\n data-slot=\"composer-file-input\"\n onChange={(e) => {\n attach(e.target.files);\n // Let the same file be picked again after a removal.\n e.target.value = '';\n }}\n />\n <Button\n type=\"button\"\n size=\"icon-sm\"\n variant=\"ghost\"\n aria-label={attachLabel}\n title={attachLabel}\n disabled={disabled}\n onClick={() => fileInput.current?.click()}\n className=\"pointer-coarse:size-11\"\n >\n <Paperclip />\n </Button>\n </>\n )}\n {actions}\n <div className=\"ml-auto\">\n {streaming ? (\n <Button\n type=\"button\"\n size=\"icon-sm\"\n variant=\"secondary\"\n aria-label=\"Stop\"\n className=\"pointer-coarse:size-11\"\n onClick={onStop}\n disabled={!onStop || disabled}\n >\n <Square className=\"fill-current\" />\n </Button>\n ) : (\n <Button\n type=\"submit\"\n size=\"icon-sm\"\n aria-label=\"Send\"\n className=\"pointer-coarse:size-11\"\n loading={pending}\n disabled={!canSend}\n >\n <ArrowUp />\n </Button>\n )}\n </div>\n </div>\n </form>\n );\n}\n\nexport interface SuggestionsProps extends Omit<React.ComponentProps<'div'>, 'onSelect'> {\n items: string[];\n onSelect: (item: string) => void;\n}\n\n/** Starter prompts as chips. */\nfunction Suggestions({ items, onSelect, className, ...props }: SuggestionsProps) {\n return (\n <div\n data-slot=\"suggestions\"\n role=\"group\"\n aria-label=\"Suggestions\"\n className={cn('flex flex-wrap gap-2', className)}\n {...props}\n >\n {items.map((item) => (\n <button\n key={item}\n type=\"button\"\n onClick={() => onSelect(item)}\n className=\"animate-in rounded-full border border-border-strong bg-surface px-3 py-1.5 text-[13px] text-foreground outline-none transition-colors hover:border-brand/40 hover:bg-brand-soft hover:text-brand-soft-foreground focus-visible:ring-[3px] focus-visible:ring-ring/20\"\n >\n {item}\n </button>\n ))}\n </div>\n );\n}\n\nexport { Composer, Suggestions };\n"],"mappings":"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"}
|
|
@@ -7,8 +7,8 @@ import { Skeleton } from "./skeleton.js";
|
|
|
7
7
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./table.js";
|
|
8
8
|
import * as React from "react";
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
-
import ChevronsUpDown from "@burtson-labs/icons/react/chevrons-up-down";
|
|
11
10
|
import Search from "@burtson-labs/icons/react/search";
|
|
11
|
+
import ChevronsUpDown from "@burtson-labs/icons/react/chevrons-up-down";
|
|
12
12
|
import ArrowUp from "@burtson-labs/icons/react/arrow-up";
|
|
13
13
|
import ArrowDown from "@burtson-labs/icons/react/arrow-down";
|
|
14
14
|
//#region src/components/data-table.tsx
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export type MessageFeedback = 'up' | 'down' | null;
|
|
3
|
+
export interface MessageActionsProps extends React.ComponentProps<'div'> {
|
|
4
|
+
/** Text for the copy button; leave out to hide it. */
|
|
5
|
+
copyText?: string;
|
|
6
|
+
onRegenerate?: () => void;
|
|
7
|
+
/** Edit a sent message (user turns). Pair with MessageEditor. */
|
|
8
|
+
onEdit?: () => void;
|
|
9
|
+
/** The current rating; controlled. */
|
|
10
|
+
feedback?: MessageFeedback;
|
|
11
|
+
/** Adds thumbs up and down. Clicking the chosen one again clears it (null). */
|
|
12
|
+
onFeedback?: (value: MessageFeedback) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The row under a message: copy, regenerate, edit and a thumbs rating, as
|
|
16
|
+
* small icon buttons. Pass it to Message's `actions`, which shows it on
|
|
17
|
+
* hover and focus (always on touch screens).
|
|
18
|
+
*/
|
|
19
|
+
declare function MessageActions({ copyText, onRegenerate, onEdit, feedback, onFeedback, className, ...props }: MessageActionsProps): React.JSX.Element;
|
|
20
|
+
export interface MessageEditorProps extends Omit<React.ComponentProps<'form'>, 'onSubmit'> {
|
|
21
|
+
/** The text being edited. */
|
|
22
|
+
defaultValue: string;
|
|
23
|
+
/** Save and send the edited message again. */
|
|
24
|
+
onSubmit: (text: string) => void;
|
|
25
|
+
onCancel: () => void;
|
|
26
|
+
submitLabel?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Edit-and-resend in place of a sent message. Enter sends (never mid-IME
|
|
30
|
+
* composition), Shift+Enter adds a line, Escape cancels.
|
|
31
|
+
*/
|
|
32
|
+
declare function MessageEditor({ defaultValue, onSubmit, onCancel, submitLabel, className, ...props }: MessageEditorProps): React.JSX.Element;
|
|
33
|
+
export interface MessageFile {
|
|
34
|
+
name: string;
|
|
35
|
+
/** Bytes. */
|
|
36
|
+
size?: number;
|
|
37
|
+
/** MIME type; audio renders a player, images a thumbnail. */
|
|
38
|
+
type?: string;
|
|
39
|
+
/** Where to open or play it. */
|
|
40
|
+
href?: string;
|
|
41
|
+
/** Waveform peaks for audio (see computePeaks). */
|
|
42
|
+
peaks?: number[];
|
|
43
|
+
/** Seconds, for audio. */
|
|
44
|
+
duration?: number;
|
|
45
|
+
/** Transcript for audio. */
|
|
46
|
+
transcript?: React.ReactNode;
|
|
47
|
+
}
|
|
48
|
+
export interface MessageAttachmentsProps extends React.ComponentProps<'div'> {
|
|
49
|
+
files: MessageFile[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Files sent with a message: voice notes play inline, images show a
|
|
53
|
+
* thumbnail, anything else is a named chip that opens the file.
|
|
54
|
+
*/
|
|
55
|
+
declare function MessageAttachments({ files, className, ...props }: MessageAttachmentsProps): React.JSX.Element | null;
|
|
56
|
+
export { MessageActions, MessageAttachments, MessageEditor };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { cn } from "../lib/utils.js";
|
|
2
|
+
import { Button } from "./button.js";
|
|
3
|
+
import { formatBytes } from "./attachment.js";
|
|
4
|
+
import { VoiceMessage } from "./audio-player.js";
|
|
5
|
+
import { IconButton } from "./icon-button.js";
|
|
6
|
+
import { CopyButton } from "./copy-button.js";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import FileText from "@burtson-labs/icons/react/file-text";
|
|
10
|
+
import Edit from "@burtson-labs/icons/react/edit";
|
|
11
|
+
import RotateCcw from "@burtson-labs/icons/react/rotate-ccw";
|
|
12
|
+
import ThumbsDown from "@burtson-labs/icons/react/thumbs-down";
|
|
13
|
+
import ThumbsUp from "@burtson-labs/icons/react/thumbs-up";
|
|
14
|
+
//#region src/components/message-actions.tsx
|
|
15
|
+
/**
|
|
16
|
+
* The row under a message: copy, regenerate, edit and a thumbs rating, as
|
|
17
|
+
* small icon buttons. Pass it to Message's `actions`, which shows it on
|
|
18
|
+
* hover and focus (always on touch screens).
|
|
19
|
+
*/
|
|
20
|
+
function MessageActions({ copyText, onRegenerate, onEdit, feedback = null, onFeedback, className, ...props }) {
|
|
21
|
+
const small = "size-7 text-muted-foreground hover:text-foreground pointer-coarse:size-10";
|
|
22
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
23
|
+
"data-slot": "message-actions",
|
|
24
|
+
role: "toolbar",
|
|
25
|
+
"aria-label": "Message actions",
|
|
26
|
+
className: cn("flex items-center gap-0.5", className),
|
|
27
|
+
...props,
|
|
28
|
+
children: [
|
|
29
|
+
copyText !== void 0 && /* @__PURE__ */ jsx(CopyButton, {
|
|
30
|
+
value: copyText,
|
|
31
|
+
label: "Copy message",
|
|
32
|
+
className: small
|
|
33
|
+
}),
|
|
34
|
+
onEdit && /* @__PURE__ */ jsx(IconButton, {
|
|
35
|
+
variant: "ghost",
|
|
36
|
+
size: "icon-sm",
|
|
37
|
+
label: "Edit message",
|
|
38
|
+
onClick: onEdit,
|
|
39
|
+
className: small,
|
|
40
|
+
children: /* @__PURE__ */ jsx(Edit, {})
|
|
41
|
+
}),
|
|
42
|
+
onRegenerate && /* @__PURE__ */ jsx(IconButton, {
|
|
43
|
+
variant: "ghost",
|
|
44
|
+
size: "icon-sm",
|
|
45
|
+
label: "Regenerate response",
|
|
46
|
+
onClick: onRegenerate,
|
|
47
|
+
className: small,
|
|
48
|
+
children: /* @__PURE__ */ jsx(RotateCcw, {})
|
|
49
|
+
}),
|
|
50
|
+
onFeedback && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IconButton, {
|
|
51
|
+
variant: "ghost",
|
|
52
|
+
size: "icon-sm",
|
|
53
|
+
label: "Good response",
|
|
54
|
+
"aria-pressed": feedback === "up",
|
|
55
|
+
onClick: () => onFeedback(feedback === "up" ? null : "up"),
|
|
56
|
+
className: cn(small, feedback === "up" && "text-brand hover:text-brand"),
|
|
57
|
+
children: /* @__PURE__ */ jsx(ThumbsUp, { className: cn(feedback === "up" && "fill-current") })
|
|
58
|
+
}), /* @__PURE__ */ jsx(IconButton, {
|
|
59
|
+
variant: "ghost",
|
|
60
|
+
size: "icon-sm",
|
|
61
|
+
label: "Bad response",
|
|
62
|
+
"aria-pressed": feedback === "down",
|
|
63
|
+
onClick: () => onFeedback(feedback === "down" ? null : "down"),
|
|
64
|
+
className: cn(small, feedback === "down" && "text-foreground"),
|
|
65
|
+
children: /* @__PURE__ */ jsx(ThumbsDown, { className: cn(feedback === "down" && "fill-current") })
|
|
66
|
+
})] })
|
|
67
|
+
]
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Edit-and-resend in place of a sent message. Enter sends (never mid-IME
|
|
72
|
+
* composition), Shift+Enter adds a line, Escape cancels.
|
|
73
|
+
*/
|
|
74
|
+
function MessageEditor({ defaultValue, onSubmit, onCancel, submitLabel = "Send", className, ...props }) {
|
|
75
|
+
const [value, setValue] = React.useState(defaultValue);
|
|
76
|
+
const send = () => {
|
|
77
|
+
if (value.trim()) onSubmit(value.trim());
|
|
78
|
+
};
|
|
79
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
80
|
+
"data-slot": "message-editor",
|
|
81
|
+
onSubmit: (e) => {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
send();
|
|
84
|
+
},
|
|
85
|
+
className: cn("grid w-full max-w-[85%] gap-2 justify-self-end", className),
|
|
86
|
+
...props,
|
|
87
|
+
children: [/* @__PURE__ */ jsx("textarea", {
|
|
88
|
+
"aria-label": "Edit message",
|
|
89
|
+
value,
|
|
90
|
+
autoFocus: true,
|
|
91
|
+
onChange: (e) => setValue(e.target.value),
|
|
92
|
+
onKeyDown: (e) => {
|
|
93
|
+
if (e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229) return;
|
|
94
|
+
if (e.key === "Escape") {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
onCancel();
|
|
97
|
+
} else if (e.key === "Enter" && !e.shiftKey) {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
send();
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
className: "field-sizing-content max-h-60 min-h-16 w-full resize-none rounded-lg border border-brand bg-surface px-3 py-2 text-base leading-6 ring-[3px] ring-ring/15 outline-none sm:text-sm"
|
|
103
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
104
|
+
className: "flex justify-end gap-2",
|
|
105
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
106
|
+
type: "button",
|
|
107
|
+
variant: "ghost",
|
|
108
|
+
size: "sm",
|
|
109
|
+
onClick: onCancel,
|
|
110
|
+
children: "Cancel"
|
|
111
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
112
|
+
type: "submit",
|
|
113
|
+
size: "sm",
|
|
114
|
+
disabled: !value.trim(),
|
|
115
|
+
children: submitLabel
|
|
116
|
+
})]
|
|
117
|
+
})]
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Files sent with a message: voice notes play inline, images show a
|
|
122
|
+
* thumbnail, anything else is a named chip that opens the file.
|
|
123
|
+
*/
|
|
124
|
+
function MessageAttachments({ files, className, ...props }) {
|
|
125
|
+
if (!files.length) return null;
|
|
126
|
+
return /* @__PURE__ */ jsx("div", {
|
|
127
|
+
"data-slot": "message-attachments",
|
|
128
|
+
className: cn("flex flex-wrap gap-2", className),
|
|
129
|
+
...props,
|
|
130
|
+
children: files.map((f, i) => {
|
|
131
|
+
const type = f.type ?? "";
|
|
132
|
+
if (type.startsWith("audio/") && f.href) return /* @__PURE__ */ jsx(VoiceMessage, {
|
|
133
|
+
src: f.href,
|
|
134
|
+
peaks: f.peaks,
|
|
135
|
+
duration: f.duration,
|
|
136
|
+
transcript: f.transcript,
|
|
137
|
+
title: f.name
|
|
138
|
+
}, `${f.name}-${i}`);
|
|
139
|
+
if (type.startsWith("image/") && f.href) return /* @__PURE__ */ jsx("a", {
|
|
140
|
+
href: f.href,
|
|
141
|
+
target: "_blank",
|
|
142
|
+
rel: "noreferrer",
|
|
143
|
+
className: "block overflow-hidden rounded-lg border outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20",
|
|
144
|
+
children: /* @__PURE__ */ jsx("img", {
|
|
145
|
+
src: f.href,
|
|
146
|
+
alt: f.name,
|
|
147
|
+
className: "h-28 w-auto max-w-56 object-cover"
|
|
148
|
+
})
|
|
149
|
+
}, `${f.name}-${i}`);
|
|
150
|
+
const chip = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
151
|
+
/* @__PURE__ */ jsx(FileText, {
|
|
152
|
+
"aria-hidden": true,
|
|
153
|
+
className: "size-4 shrink-0 text-muted-foreground"
|
|
154
|
+
}),
|
|
155
|
+
/* @__PURE__ */ jsx("span", {
|
|
156
|
+
className: "min-w-0 truncate",
|
|
157
|
+
children: f.name
|
|
158
|
+
}),
|
|
159
|
+
f.size !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
160
|
+
className: "shrink-0 text-xs text-muted-foreground",
|
|
161
|
+
children: formatBytes(f.size)
|
|
162
|
+
})
|
|
163
|
+
] });
|
|
164
|
+
const chipClass = "inline-flex max-w-64 items-center gap-2 rounded-md border bg-surface px-2.5 py-1.5 text-sm";
|
|
165
|
+
return f.href ? /* @__PURE__ */ jsx("a", {
|
|
166
|
+
href: f.href,
|
|
167
|
+
target: "_blank",
|
|
168
|
+
rel: "noreferrer",
|
|
169
|
+
className: cn(chipClass, "outline-none hover:bg-muted focus-visible:ring-[3px] focus-visible:ring-ring/20"),
|
|
170
|
+
children: chip
|
|
171
|
+
}, `${f.name}-${i}`) : /* @__PURE__ */ jsx("span", {
|
|
172
|
+
className: chipClass,
|
|
173
|
+
children: chip
|
|
174
|
+
}, `${f.name}-${i}`);
|
|
175
|
+
})
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
export { MessageActions, MessageAttachments, MessageEditor };
|
|
180
|
+
|
|
181
|
+
//# sourceMappingURL=message-actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message-actions.js","names":["_Fragment"],"sources":["../../src/components/message-actions.tsx"],"sourcesContent":["import Edit from '@burtson-labs/icons/react/edit';\nimport FileText from '@burtson-labs/icons/react/file-text';\nimport RotateCcw from '@burtson-labs/icons/react/rotate-ccw';\nimport ThumbsDown from '@burtson-labs/icons/react/thumbs-down';\nimport ThumbsUp from '@burtson-labs/icons/react/thumbs-up';\nimport * as React from 'react';\n\nimport { cn } from '../lib/utils';\n\nimport { formatBytes } from './attachment';\nimport { VoiceMessage } from './audio-player';\nimport { Button } from './button';\nimport { CopyButton } from './copy-button';\nimport { IconButton } from './icon-button';\n\nexport type MessageFeedback = 'up' | 'down' | null;\n\nexport interface MessageActionsProps extends React.ComponentProps<'div'> {\n /** Text for the copy button; leave out to hide it. */\n copyText?: string;\n onRegenerate?: () => void;\n /** Edit a sent message (user turns). Pair with MessageEditor. */\n onEdit?: () => void;\n /** The current rating; controlled. */\n feedback?: MessageFeedback;\n /** Adds thumbs up and down. Clicking the chosen one again clears it (null). */\n onFeedback?: (value: MessageFeedback) => void;\n}\n\n/**\n * The row under a message: copy, regenerate, edit and a thumbs rating, as\n * small icon buttons. Pass it to Message's `actions`, which shows it on\n * hover and focus (always on touch screens).\n */\nfunction MessageActions({\n copyText,\n onRegenerate,\n onEdit,\n feedback = null,\n onFeedback,\n className,\n ...props\n}: MessageActionsProps) {\n const small = 'size-7 text-muted-foreground hover:text-foreground pointer-coarse:size-10';\n return (\n <div\n data-slot=\"message-actions\"\n role=\"toolbar\"\n aria-label=\"Message actions\"\n className={cn('flex items-center gap-0.5', className)}\n {...props}\n >\n {copyText !== undefined && (\n <CopyButton value={copyText} label=\"Copy message\" className={small} />\n )}\n {onEdit && (\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label=\"Edit message\"\n onClick={onEdit}\n className={small}\n >\n <Edit />\n </IconButton>\n )}\n {onRegenerate && (\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label=\"Regenerate response\"\n onClick={onRegenerate}\n className={small}\n >\n <RotateCcw />\n </IconButton>\n )}\n {onFeedback && (\n <>\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label=\"Good response\"\n aria-pressed={feedback === 'up'}\n onClick={() => onFeedback(feedback === 'up' ? null : 'up')}\n className={cn(small, feedback === 'up' && 'text-brand hover:text-brand')}\n >\n <ThumbsUp className={cn(feedback === 'up' && 'fill-current')} />\n </IconButton>\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label=\"Bad response\"\n aria-pressed={feedback === 'down'}\n onClick={() => onFeedback(feedback === 'down' ? null : 'down')}\n className={cn(small, feedback === 'down' && 'text-foreground')}\n >\n <ThumbsDown className={cn(feedback === 'down' && 'fill-current')} />\n </IconButton>\n </>\n )}\n </div>\n );\n}\n\nexport interface MessageEditorProps extends Omit<React.ComponentProps<'form'>, 'onSubmit'> {\n /** The text being edited. */\n defaultValue: string;\n /** Save and send the edited message again. */\n onSubmit: (text: string) => void;\n onCancel: () => void;\n submitLabel?: string;\n}\n\n/**\n * Edit-and-resend in place of a sent message. Enter sends (never mid-IME\n * composition), Shift+Enter adds a line, Escape cancels.\n */\nfunction MessageEditor({\n defaultValue,\n onSubmit,\n onCancel,\n submitLabel = 'Send',\n className,\n ...props\n}: MessageEditorProps) {\n const [value, setValue] = React.useState(defaultValue);\n const send = () => {\n if (value.trim()) onSubmit(value.trim());\n };\n return (\n <form\n data-slot=\"message-editor\"\n onSubmit={(e) => {\n e.preventDefault();\n send();\n }}\n className={cn('grid w-full max-w-[85%] gap-2 justify-self-end', className)}\n {...props}\n >\n <textarea\n aria-label=\"Edit message\"\n value={value}\n // eslint-disable-next-line jsx-a11y/no-autofocus -- editing was just asked for\n autoFocus\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={(e) => {\n if (e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229) return;\n if (e.key === 'Escape') {\n e.preventDefault();\n onCancel();\n } else if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n send();\n }\n }}\n className=\"field-sizing-content max-h-60 min-h-16 w-full resize-none rounded-lg border border-brand bg-surface px-3 py-2 text-base leading-6 ring-[3px] ring-ring/15 outline-none sm:text-sm\"\n />\n <div className=\"flex justify-end gap-2\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={onCancel}>\n Cancel\n </Button>\n <Button type=\"submit\" size=\"sm\" disabled={!value.trim()}>\n {submitLabel}\n </Button>\n </div>\n </form>\n );\n}\n\nexport interface MessageFile {\n name: string;\n /** Bytes. */\n size?: number;\n /** MIME type; audio renders a player, images a thumbnail. */\n type?: string;\n /** Where to open or play it. */\n href?: string;\n /** Waveform peaks for audio (see computePeaks). */\n peaks?: number[];\n /** Seconds, for audio. */\n duration?: number;\n /** Transcript for audio. */\n transcript?: React.ReactNode;\n}\n\nexport interface MessageAttachmentsProps extends React.ComponentProps<'div'> {\n files: MessageFile[];\n}\n\n/**\n * Files sent with a message: voice notes play inline, images show a\n * thumbnail, anything else is a named chip that opens the file.\n */\nfunction MessageAttachments({ files, className, ...props }: MessageAttachmentsProps) {\n if (!files.length) return null;\n return (\n <div\n data-slot=\"message-attachments\"\n className={cn('flex flex-wrap gap-2', className)}\n {...props}\n >\n {files.map((f, i) => {\n const type = f.type ?? '';\n if (type.startsWith('audio/') && f.href) {\n return (\n <VoiceMessage\n key={`${f.name}-${i}`}\n src={f.href}\n peaks={f.peaks}\n duration={f.duration}\n transcript={f.transcript}\n title={f.name}\n />\n );\n }\n if (type.startsWith('image/') && f.href) {\n return (\n <a\n key={`${f.name}-${i}`}\n href={f.href}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"block overflow-hidden rounded-lg border outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20\"\n >\n <img src={f.href} alt={f.name} className=\"h-28 w-auto max-w-56 object-cover\" />\n </a>\n );\n }\n const chip = (\n <>\n <FileText aria-hidden className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 truncate\">{f.name}</span>\n {f.size !== undefined && (\n <span className=\"shrink-0 text-xs text-muted-foreground\">{formatBytes(f.size)}</span>\n )}\n </>\n );\n const chipClass =\n 'inline-flex max-w-64 items-center gap-2 rounded-md border bg-surface px-2.5 py-1.5 text-sm';\n return f.href ? (\n <a\n key={`${f.name}-${i}`}\n href={f.href}\n target=\"_blank\"\n rel=\"noreferrer\"\n className={cn(\n chipClass,\n 'outline-none hover:bg-muted focus-visible:ring-[3px] focus-visible:ring-ring/20',\n )}\n >\n {chip}\n </a>\n ) : (\n <span key={`${f.name}-${i}`} className={chipClass}>\n {chip}\n </span>\n );\n })}\n </div>\n );\n}\n\nexport { MessageActions, MessageAttachments, MessageEditor };\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cn } from "../lib/utils.js";
|
|
2
|
-
import { Input } from "./input.js";
|
|
3
2
|
import { IconButton } from "./icon-button.js";
|
|
3
|
+
import { Input } from "./input.js";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import Eye from "@burtson-labs/icons/react/eye";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cn } from "../lib/utils.js";
|
|
2
|
-
import { fieldClasses } from "./input.js";
|
|
3
2
|
import { surfaceClasses } from "./popover.js";
|
|
3
|
+
import { fieldClasses } from "./input.js";
|
|
4
4
|
import ChevronDown from "@burtson-labs/icons/react/chevron-down";
|
|
5
5
|
import { Select } from "radix-ui";
|
|
6
6
|
import "react";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export type VoiceRecorderState = 'idle' | 'requesting' | 'recording' | 'paused' | 'denied' | 'unsupported' | 'error';
|
|
3
|
+
export interface VoiceRecording {
|
|
4
|
+
blob: Blob;
|
|
5
|
+
mimeType: string;
|
|
6
|
+
durationMs: number;
|
|
7
|
+
}
|
|
8
|
+
export interface VoiceRecorderProps extends React.ComponentProps<'div'> {
|
|
9
|
+
/** The finished recording, after Stop. */
|
|
10
|
+
onRecorded: (recording: VoiceRecording) => void;
|
|
11
|
+
onCancel?: () => void;
|
|
12
|
+
onStateChange?: (state: VoiceRecorderState) => void;
|
|
13
|
+
/** Stops by itself at this length. Default 5 minutes. */
|
|
14
|
+
maxDurationMs?: number;
|
|
15
|
+
/** Preferred container, used when the browser supports it (e.g. "audio/webm"). */
|
|
16
|
+
mimeType?: string;
|
|
17
|
+
/** Accessible name of the record button. */
|
|
18
|
+
label?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A microphone control for a composer: tap to record, then pause, stop to
|
|
22
|
+
* keep the clip or discard it. While recording it shows a live level meter
|
|
23
|
+
* and the elapsed time, in the recording colour (a state, not an error).
|
|
24
|
+
* When the browser can't record or the microphone is blocked it says so and
|
|
25
|
+
* how to fix it.
|
|
26
|
+
*/
|
|
27
|
+
declare function VoiceRecorder({ onRecorded, onCancel, onStateChange, maxDurationMs, mimeType, label, className, ...props }: VoiceRecorderProps): React.JSX.Element;
|
|
28
|
+
export { VoiceRecorder };
|