@djangocfg/ui-tools 2.1.411 → 2.1.412
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/package.json +6 -6
- package/src/tools/Chat/composer/AttachContext.tsx +22 -0
- package/src/tools/Chat/composer/Composer.tsx +108 -6
- package/src/tools/Chat/composer/ComposerMenuButton.tsx +39 -2
- package/src/tools/Chat/composer/fileToAttachment.ts +53 -0
- package/src/tools/Chat/composer/index.ts +16 -1
- package/src/tools/Chat/composer/types.ts +71 -0
- package/src/tools/Chat/composer/useComposerAttach.tsx +218 -0
- package/src/tools/Chat/hooks/useChat.ts +32 -0
- package/src/tools/Chat/hooks/useChatComposer.ts +13 -0
- package/src/tools/Chat/messages/MessageBubble.tsx +1 -1
- package/src/tools/Chat/public.ts +1 -0
- package/src/tools/Chat/types/events.ts +50 -0
- package/src/tools/Chat/types/index.ts +1 -1
- package/src/tools/Chat/types/message.ts +5 -0
- package/src/tools/CronScheduler/CronScheduler.client.tsx +42 -15
- package/src/tools/CronScheduler/components/CustomInput.tsx +26 -7
- package/src/tools/CronScheduler/components/DayChips.tsx +20 -7
- package/src/tools/CronScheduler/components/MonthDayGrid.tsx +35 -10
- package/src/tools/CronScheduler/components/SchedulePreview.tsx +8 -5
- package/src/tools/CronScheduler/components/ScheduleTypeSelector.tsx +12 -3
- package/src/tools/CronScheduler/components/TimeSelector.tsx +36 -13
- package/src/tools/CronScheduler/context/CronSchedulerContext.tsx +4 -0
- package/src/tools/CronScheduler/context/hooks.ts +8 -0
- package/src/tools/CronScheduler/context/index.ts +1 -0
- package/src/tools/CronScheduler/index.tsx +2 -0
- package/src/tools/CronScheduler/lazy.tsx +1 -0
- package/src/tools/CronScheduler/types/index.ts +18 -1
- package/src/tools/Map/lazy.tsx +11 -4
- package/src/tools/Uploader/hooks/useClipboardPaste.ts +3 -1
- package/src/tools/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.412",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -154,8 +154,8 @@
|
|
|
154
154
|
"test:watch": "vitest"
|
|
155
155
|
},
|
|
156
156
|
"peerDependencies": {
|
|
157
|
-
"@djangocfg/i18n": "^2.1.
|
|
158
|
-
"@djangocfg/ui-core": "^2.1.
|
|
157
|
+
"@djangocfg/i18n": "^2.1.412",
|
|
158
|
+
"@djangocfg/ui-core": "^2.1.412",
|
|
159
159
|
"consola": "^3.4.2",
|
|
160
160
|
"lodash-es": "^4.18.1",
|
|
161
161
|
"lucide-react": "^0.545.0",
|
|
@@ -210,9 +210,9 @@
|
|
|
210
210
|
"@maplibre/maplibre-gl-geocoder": "^1.7.0"
|
|
211
211
|
},
|
|
212
212
|
"devDependencies": {
|
|
213
|
-
"@djangocfg/i18n": "^2.1.
|
|
214
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
215
|
-
"@djangocfg/ui-core": "^2.1.
|
|
213
|
+
"@djangocfg/i18n": "^2.1.412",
|
|
214
|
+
"@djangocfg/typescript-config": "^2.1.412",
|
|
215
|
+
"@djangocfg/ui-core": "^2.1.412",
|
|
216
216
|
"@types/lodash-es": "^4.17.12",
|
|
217
217
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
218
218
|
"@types/node": "^25.2.3",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { ComposerAttachHandle } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Exposes the composer's attach pipeline to descendants — chiefly
|
|
9
|
+
* `ComposerMenuButton`, so a `+`-menu "Attach" item reaches the same
|
|
10
|
+
* `openPicker` as the paperclip button without a prop drill.
|
|
11
|
+
*
|
|
12
|
+
* `null` when no attach pipeline is mounted (composer without
|
|
13
|
+
* `showAttachmentButton` / `attach` config).
|
|
14
|
+
*/
|
|
15
|
+
const AttachContext = createContext<ComposerAttachHandle | null>(null);
|
|
16
|
+
|
|
17
|
+
export const AttachProvider = AttachContext.Provider;
|
|
18
|
+
|
|
19
|
+
/** Read the attach pipeline. Returns `null` outside an attach-enabled composer. */
|
|
20
|
+
export function useComposerAttachContext(): ComposerAttachHandle | null {
|
|
21
|
+
return useContext(AttachContext);
|
|
22
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { forwardRef, useEffect, useRef } from 'react';
|
|
3
|
+
import { forwardRef, useEffect, useRef, useState } from 'react';
|
|
4
4
|
|
|
5
5
|
import { Textarea } from '@djangocfg/ui-core/components';
|
|
6
6
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
@@ -8,13 +8,16 @@ import { cn } from '@djangocfg/ui-core/lib';
|
|
|
8
8
|
import { useChatContextOptional } from '../context';
|
|
9
9
|
import type { UseChatComposerReturn } from '../hooks/useChatComposer';
|
|
10
10
|
import { Attachments } from '../messages/Attachments';
|
|
11
|
+
import { AttachProvider } from './AttachContext';
|
|
11
12
|
import { ComposerActionBar } from './ComposerActionBar';
|
|
12
13
|
import { ComposerButton } from './ComposerButton';
|
|
13
14
|
import { ComposerFooter } from './ComposerFooter';
|
|
14
15
|
import { ComposerSizeProvider } from './size-context';
|
|
15
16
|
import { useComposerActions } from './useComposerActions';
|
|
17
|
+
import { useComposerAttach } from './useComposerAttach';
|
|
16
18
|
import type {
|
|
17
19
|
ComposerAppearance,
|
|
20
|
+
ComposerAttachConfig,
|
|
18
21
|
ComposerFooterProps,
|
|
19
22
|
ComposerLayout,
|
|
20
23
|
ComposerSize,
|
|
@@ -29,7 +32,16 @@ export interface ComposerProps {
|
|
|
29
32
|
composer: UseChatComposerReturn;
|
|
30
33
|
placeholder?: string;
|
|
31
34
|
disabled?: boolean;
|
|
35
|
+
/** Render the paperclip attach button. The composer ships a built-in
|
|
36
|
+
* file picker — paperclip, `+` menu, drag-drop and Ctrl+V paste all
|
|
37
|
+
* funnel through one validated pipeline. */
|
|
32
38
|
showAttachmentButton?: boolean;
|
|
39
|
+
/** Tune the built-in attach pipeline — accepted types, size / count
|
|
40
|
+
* caps, paste, and the optional upload transport. */
|
|
41
|
+
attach?: ComposerAttachConfig;
|
|
42
|
+
/** Override the built-in file picker. When set, the paperclip and the
|
|
43
|
+
* `+`-menu attach item call this instead of opening the native picker
|
|
44
|
+
* — for hosts that drive their own (e.g. a Wails native dialog). */
|
|
33
45
|
onPickFiles?: () => void;
|
|
34
46
|
className?: string;
|
|
35
47
|
textareaClassName?: string;
|
|
@@ -130,6 +142,7 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
130
142
|
placeholder = 'Type a message...',
|
|
131
143
|
disabled,
|
|
132
144
|
showAttachmentButton = false,
|
|
145
|
+
attach,
|
|
133
146
|
onPickFiles,
|
|
134
147
|
className,
|
|
135
148
|
textareaClassName,
|
|
@@ -220,13 +233,60 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
220
233
|
editable.focus();
|
|
221
234
|
};
|
|
222
235
|
|
|
236
|
+
// ── Attach pipeline ────────────────────────────────────────────────────
|
|
237
|
+
// The built-in file picker is mounted whenever the host renders the
|
|
238
|
+
// paperclip OR passes an `attach` config. `onPickFiles` (if given)
|
|
239
|
+
// overrides it — a host driving its own picker (e.g. a Wails dialog).
|
|
240
|
+
const attachEnabled = showAttachmentButton || attach != null;
|
|
241
|
+
const attachHandle = useComposerAttach({
|
|
242
|
+
composer,
|
|
243
|
+
config: attach ?? {},
|
|
244
|
+
disabled: isDisabled,
|
|
245
|
+
pasteScopeRef: surfaceRef,
|
|
246
|
+
});
|
|
247
|
+
// The resolved click handler: host override wins, else the built-in
|
|
248
|
+
// picker. `undefined` when attaching is not enabled at all.
|
|
249
|
+
const pickFiles = !attachEnabled
|
|
250
|
+
? undefined
|
|
251
|
+
: (onPickFiles ?? attachHandle.openPicker);
|
|
252
|
+
|
|
253
|
+
// Drag-drop onto the input surface — same validated path as the picker.
|
|
254
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
255
|
+
const dragDepth = useRef(0);
|
|
256
|
+
const handleDragEnter = (e: React.DragEvent) => {
|
|
257
|
+
if (!attachEnabled || isDisabled) return;
|
|
258
|
+
if (!e.dataTransfer.types.includes('Files')) return;
|
|
259
|
+
e.preventDefault();
|
|
260
|
+
dragDepth.current += 1;
|
|
261
|
+
setIsDragging(true);
|
|
262
|
+
};
|
|
263
|
+
const handleDragOver = (e: React.DragEvent) => {
|
|
264
|
+
if (attachEnabled && !isDisabled && e.dataTransfer.types.includes('Files')) {
|
|
265
|
+
e.preventDefault();
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
const handleDragLeave = (e: React.DragEvent) => {
|
|
269
|
+
if (!attachEnabled || isDisabled) return;
|
|
270
|
+
e.preventDefault();
|
|
271
|
+
dragDepth.current = Math.max(0, dragDepth.current - 1);
|
|
272
|
+
if (dragDepth.current === 0) setIsDragging(false);
|
|
273
|
+
};
|
|
274
|
+
const handleDrop = (e: React.DragEvent) => {
|
|
275
|
+
if (!attachEnabled || isDisabled) return;
|
|
276
|
+
e.preventDefault();
|
|
277
|
+
dragDepth.current = 0;
|
|
278
|
+
setIsDragging(false);
|
|
279
|
+
const dropped = Array.from(e.dataTransfer.files ?? []);
|
|
280
|
+
if (dropped.length > 0) attachHandle.attachFiles(dropped);
|
|
281
|
+
};
|
|
282
|
+
|
|
223
283
|
// Merge built-in send/stop/attach descriptors with host arrays.
|
|
224
284
|
const { actionsStart, actionsEnd } = useComposerActions({
|
|
225
285
|
composer,
|
|
226
286
|
isStreaming,
|
|
227
287
|
isDisabled,
|
|
228
288
|
showAttachmentButton,
|
|
229
|
-
onPickFiles,
|
|
289
|
+
onPickFiles: pickFiles,
|
|
230
290
|
actionsStart: composerSlots?.actionsStart,
|
|
231
291
|
actionsEnd: composerSlots?.actionsEnd,
|
|
232
292
|
onSend: () => void composer.submit(),
|
|
@@ -311,11 +371,19 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
311
371
|
<AttachSlot
|
|
312
372
|
disabled={isDisabled}
|
|
313
373
|
size={size}
|
|
314
|
-
onClick={() =>
|
|
374
|
+
onClick={() => pickFiles?.()}
|
|
315
375
|
{...slotProps?.attach}
|
|
316
376
|
/>
|
|
317
377
|
) : null;
|
|
318
378
|
|
|
379
|
+
// Hidden native file input — the picker the paperclip + `+` menu open.
|
|
380
|
+
// Rendered once per composer; only present when the pipeline is active
|
|
381
|
+
// and no host override is in play (an override owns its own picker).
|
|
382
|
+
const hiddenFileInput =
|
|
383
|
+
attachEnabled && !onPickFiles ? (
|
|
384
|
+
<input {...attachHandle.inputProps} />
|
|
385
|
+
) : null;
|
|
386
|
+
|
|
319
387
|
// ── Footer ─────────────────────────────────────────────────────────────
|
|
320
388
|
const footerNode =
|
|
321
389
|
footer === false ? null : (
|
|
@@ -331,25 +399,53 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
331
399
|
(composer.attachments.length > 0 ? (
|
|
332
400
|
<Attachments
|
|
333
401
|
attachments={composer.attachments}
|
|
334
|
-
|
|
402
|
+
// Route through the attach pipeline so a removed attachment's
|
|
403
|
+
// object-URL is revoked; falls back to a plain remove when the
|
|
404
|
+
// pipeline is not mounted (host-managed attachments).
|
|
405
|
+
onRemove={(a) =>
|
|
406
|
+
attachEnabled
|
|
407
|
+
? attachHandle.removeAttachment(a.id)
|
|
408
|
+
: composer.removeAttachment(a.id)
|
|
409
|
+
}
|
|
335
410
|
/>
|
|
336
411
|
) : null);
|
|
337
412
|
|
|
338
413
|
// ── Inline (compact single-row) layout ─────────────────────────────────
|
|
414
|
+
// Drag-drop handlers shared by both layouts. No-op when attaching is
|
|
415
|
+
// disabled — the handlers themselves early-return.
|
|
416
|
+
const dragProps = {
|
|
417
|
+
onDragEnter: handleDragEnter,
|
|
418
|
+
onDragOver: handleDragOver,
|
|
419
|
+
onDragLeave: handleDragLeave,
|
|
420
|
+
onDrop: handleDrop,
|
|
421
|
+
};
|
|
422
|
+
// A faint accent wash + ring while a file is dragged over the composer.
|
|
423
|
+
const dragOverlay =
|
|
424
|
+
isDragging ? (
|
|
425
|
+
<div
|
|
426
|
+
aria-hidden
|
|
427
|
+
className="pointer-events-none absolute inset-0 z-10 rounded-[inherit] bg-primary/5 ring-2 ring-inset ring-primary/40"
|
|
428
|
+
/>
|
|
429
|
+
) : null;
|
|
430
|
+
|
|
339
431
|
// Start cluster sits left of the textarea, end cluster right of it —
|
|
340
432
|
// `[📎][actionsStart] [textarea] [actionsEnd][🎙][▶]` (§3.6).
|
|
341
433
|
if (layout === 'inline') {
|
|
342
434
|
return (
|
|
343
435
|
<ComposerSizeProvider value={size}>
|
|
436
|
+
<AttachProvider value={attachHandle}>
|
|
344
437
|
<div
|
|
345
438
|
ref={ref}
|
|
439
|
+
{...dragProps}
|
|
346
440
|
className={cn(
|
|
347
|
-
'border-t border-border bg-background/95',
|
|
441
|
+
'relative border-t border-border bg-background/95',
|
|
348
442
|
sz.containerPadding,
|
|
349
443
|
ap.containerPadding,
|
|
350
444
|
className,
|
|
351
445
|
)}
|
|
352
446
|
>
|
|
447
|
+
{hiddenFileInput}
|
|
448
|
+
{dragOverlay}
|
|
353
449
|
{trayNode ? <div className="mb-1.5">{trayNode}</div> : null}
|
|
354
450
|
<div
|
|
355
451
|
className={cn(
|
|
@@ -385,6 +481,7 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
385
481
|
</div>
|
|
386
482
|
{footerNode}
|
|
387
483
|
</div>
|
|
484
|
+
</AttachProvider>
|
|
388
485
|
</ComposerSizeProvider>
|
|
389
486
|
);
|
|
390
487
|
}
|
|
@@ -453,19 +550,24 @@ export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Compo
|
|
|
453
550
|
|
|
454
551
|
return (
|
|
455
552
|
<ComposerSizeProvider value={size}>
|
|
553
|
+
<AttachProvider value={attachHandle}>
|
|
456
554
|
<div
|
|
457
555
|
ref={ref}
|
|
556
|
+
{...dragProps}
|
|
458
557
|
className={cn(
|
|
459
|
-
'border-t border-border bg-background/95',
|
|
558
|
+
'relative border-t border-border bg-background/95',
|
|
460
559
|
sz.containerPadding,
|
|
461
560
|
ap.containerPadding,
|
|
462
561
|
className,
|
|
463
562
|
)}
|
|
464
563
|
>
|
|
564
|
+
{hiddenFileInput}
|
|
565
|
+
{dragOverlay}
|
|
465
566
|
{trayNode ? <div className="mb-2">{trayNode}</div> : null}
|
|
466
567
|
{surfaceWrapper}
|
|
467
568
|
{footerNode}
|
|
468
569
|
</div>
|
|
570
|
+
</AttachProvider>
|
|
469
571
|
</ComposerSizeProvider>
|
|
470
572
|
);
|
|
471
573
|
});
|
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Plus } from 'lucide-react';
|
|
3
|
+
import { Paperclip, Plus } from 'lucide-react';
|
|
4
4
|
|
|
5
5
|
import { MenuBuilder, type MenuItem } from '@djangocfg/ui-core/components';
|
|
6
6
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
7
7
|
|
|
8
|
+
import { useComposerAttachContext } from './AttachContext';
|
|
8
9
|
import { BUTTON_SIZE } from './ComposerButton';
|
|
9
10
|
import { useResolvedComposerSize } from './size-context';
|
|
10
11
|
import type { ComposerSize } from './types';
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Built-in "Attach" menu item config. When `attachItem` is set, the
|
|
15
|
+
* menu button prepends a row wired to the composer's attach pipeline —
|
|
16
|
+
* the SAME `openPicker` the paperclip button uses. Pass `true` for
|
|
17
|
+
* defaults, or an object to override the label / icon.
|
|
18
|
+
*/
|
|
19
|
+
export type ComposerMenuAttachItem =
|
|
20
|
+
| boolean
|
|
21
|
+
| { label?: string };
|
|
22
|
+
|
|
12
23
|
export interface ComposerMenuButtonProps {
|
|
13
24
|
/** Declarative menu tree (see `MenuBuilder`). */
|
|
14
25
|
items: MenuItem[];
|
|
26
|
+
/** Prepend a built-in "Attach" row wired to the composer's attach
|
|
27
|
+
* pipeline. Requires an attach-enabled `<Composer>` ancestor;
|
|
28
|
+
* silently omitted otherwise. */
|
|
29
|
+
attachItem?: ComposerMenuAttachItem;
|
|
15
30
|
/** Omit to inherit the composer's size. */
|
|
16
31
|
size?: ComposerSize;
|
|
17
32
|
disabled?: boolean;
|
|
@@ -30,15 +45,37 @@ export interface ComposerMenuButtonProps {
|
|
|
30
45
|
*/
|
|
31
46
|
export function ComposerMenuButton({
|
|
32
47
|
items,
|
|
48
|
+
attachItem,
|
|
33
49
|
size,
|
|
34
50
|
disabled,
|
|
35
51
|
label = 'Add files and more',
|
|
36
52
|
icon,
|
|
37
53
|
}: ComposerMenuButtonProps) {
|
|
38
54
|
const sz = BUTTON_SIZE[useResolvedComposerSize(size)];
|
|
55
|
+
const attach = useComposerAttachContext();
|
|
56
|
+
|
|
57
|
+
// Prepend the built-in "Attach" row when requested AND an attach
|
|
58
|
+
// pipeline is in scope — it reuses the exact same `openPicker` as the
|
|
59
|
+
// paperclip button, so both inputs are one unified path.
|
|
60
|
+
const attachLabel =
|
|
61
|
+
typeof attachItem === 'object' ? (attachItem.label ?? 'Attach files') : 'Attach files';
|
|
62
|
+
const resolvedItems: MenuItem[] =
|
|
63
|
+
attachItem && attach
|
|
64
|
+
? [
|
|
65
|
+
{
|
|
66
|
+
kind: 'item',
|
|
67
|
+
id: '__composer_attach',
|
|
68
|
+
label: attachLabel,
|
|
69
|
+
icon: Paperclip,
|
|
70
|
+
disabled: attach.disabled,
|
|
71
|
+
onSelect: () => attach.openPicker(),
|
|
72
|
+
},
|
|
73
|
+
...items,
|
|
74
|
+
]
|
|
75
|
+
: items;
|
|
39
76
|
|
|
40
77
|
return (
|
|
41
|
-
<MenuBuilder items={
|
|
78
|
+
<MenuBuilder items={resolvedItems} side="top" align="start">
|
|
42
79
|
<button
|
|
43
80
|
type="button"
|
|
44
81
|
disabled={disabled}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `File → ChatAttachment` — the engine-side producer that was missing.
|
|
3
|
+
*
|
|
4
|
+
* Before this, every host re-implemented the conversion (see the fake
|
|
5
|
+
* stub in `apps/storybook/.../SpeechAndAttachments`). It lives here so
|
|
6
|
+
* the picker, drag-drop and paste paths all mint identical attachments.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createId } from '../core/ids';
|
|
10
|
+
import { getAssetTypeFromMime } from '../../Uploader';
|
|
11
|
+
import type { ChatAttachment } from '../types';
|
|
12
|
+
|
|
13
|
+
/** Map a MIME type to the `ChatAttachment.type` discriminant. */
|
|
14
|
+
function attachmentTypeFromMime(mime: string): ChatAttachment['type'] {
|
|
15
|
+
if (mime.startsWith('image/')) return 'image';
|
|
16
|
+
if (mime.startsWith('audio/')) return 'audio';
|
|
17
|
+
if (mime.startsWith('video/')) return 'video';
|
|
18
|
+
// getAssetTypeFromMime returns 'document' for everything else; the
|
|
19
|
+
// ChatAttachment union has no 'document' member — collapse to 'file'.
|
|
20
|
+
const asset = getAssetTypeFromMime(mime);
|
|
21
|
+
return asset === 'document' ? 'file' : asset;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Convert a picked/dropped/pasted `File` into a `ChatAttachment`.
|
|
26
|
+
*
|
|
27
|
+
* The `url` is an object-URL (`URL.createObjectURL`) — instantly usable
|
|
28
|
+
* for previews. When a host uploads the file via `uploadFn`, it replaces
|
|
29
|
+
* `url` with the remote URL and flips `status` to `'ready'`.
|
|
30
|
+
*
|
|
31
|
+
* `status` defaults to `'ready'`: with no `uploadFn` the attachment is
|
|
32
|
+
* immediately usable (current behaviour). The attach pipeline overrides
|
|
33
|
+
* it to `'uploading'` when an upload is in flight.
|
|
34
|
+
*/
|
|
35
|
+
export function fileToAttachment(file: File): ChatAttachment {
|
|
36
|
+
const mime = file.type || 'application/octet-stream';
|
|
37
|
+
return {
|
|
38
|
+
id: createId('att'),
|
|
39
|
+
type: attachmentTypeFromMime(mime),
|
|
40
|
+
url: URL.createObjectURL(file),
|
|
41
|
+
name: file.name,
|
|
42
|
+
mimeType: mime,
|
|
43
|
+
sizeBytes: file.size,
|
|
44
|
+
status: 'ready',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Revoke an attachment's object-URL, if it holds one. Call on remove. */
|
|
49
|
+
export function revokeAttachmentUrl(attachment: Pick<ChatAttachment, 'url'>): void {
|
|
50
|
+
if (attachment.url.startsWith('blob:')) {
|
|
51
|
+
URL.revokeObjectURL(attachment.url);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -9,7 +9,11 @@ export {
|
|
|
9
9
|
type ComposerAppearance,
|
|
10
10
|
} from './Composer';
|
|
11
11
|
export { ComposerButton, BUTTON_SIZE, type ComposerButtonProps } from './ComposerButton';
|
|
12
|
-
export {
|
|
12
|
+
export {
|
|
13
|
+
ComposerMenuButton,
|
|
14
|
+
type ComposerMenuButtonProps,
|
|
15
|
+
type ComposerMenuAttachItem,
|
|
16
|
+
} from './ComposerMenuButton';
|
|
13
17
|
export { ComposerRichTextarea, type ComposerRichTextareaProps } from './ComposerRichTextarea';
|
|
14
18
|
export { ComposerActionBar } from './ComposerActionBar';
|
|
15
19
|
export { ComposerToolPill, type ComposerToolPillProps } from './ComposerToolPill';
|
|
@@ -30,6 +34,12 @@ export {
|
|
|
30
34
|
type UseComposerActionsParams,
|
|
31
35
|
type ComposerActionClusters,
|
|
32
36
|
} from './useComposerActions';
|
|
37
|
+
export {
|
|
38
|
+
useComposerAttach,
|
|
39
|
+
type UseComposerAttachParams,
|
|
40
|
+
} from './useComposerAttach';
|
|
41
|
+
export { useComposerAttachContext } from './AttachContext';
|
|
42
|
+
export { fileToAttachment, revokeAttachmentUrl } from './fileToAttachment';
|
|
33
43
|
export type {
|
|
34
44
|
ComposerAction,
|
|
35
45
|
ComposerActionVisibility,
|
|
@@ -42,4 +52,9 @@ export type {
|
|
|
42
52
|
AttachButtonProps,
|
|
43
53
|
ComposerTextareaProps,
|
|
44
54
|
ActionBarProps,
|
|
55
|
+
ComposerAcceptType,
|
|
56
|
+
ComposerAttachConfig,
|
|
57
|
+
ComposerAttachHandle,
|
|
58
|
+
ComposerUploadFn,
|
|
59
|
+
ComposerUploadResult,
|
|
45
60
|
} from './types';
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { ComponentType, ReactNode } from 'react';
|
|
4
4
|
|
|
5
5
|
import type { UseChatComposerReturn } from '../hooks/useChatComposer';
|
|
6
|
+
import type { ChatAttachment } from '../types';
|
|
6
7
|
|
|
7
8
|
/** Composer visual size — shared across the `<Composer>` API. */
|
|
8
9
|
export type ComposerSize = 'sm' | 'md' | 'lg';
|
|
@@ -141,3 +142,73 @@ export interface ComposerFooterProps {
|
|
|
141
142
|
size?: ComposerSize;
|
|
142
143
|
className?: string;
|
|
143
144
|
}
|
|
145
|
+
|
|
146
|
+
// ── Attach pipeline ─────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/** Asset categories the picker accepts. Maps to MIME prefixes / extensions. */
|
|
149
|
+
export type ComposerAcceptType = 'image' | 'audio' | 'video' | 'document';
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Result a host's `uploadFn` resolves to — the remote location the
|
|
153
|
+
* attachment's `url` is rewritten to once the upload completes.
|
|
154
|
+
*/
|
|
155
|
+
export interface ComposerUploadResult {
|
|
156
|
+
url: string;
|
|
157
|
+
thumbnailUrl?: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Optional upload transport. The single environment-specific seam:
|
|
162
|
+
* - omitted → attachments stay local (`blob:` object-URL, `status:'ready'`).
|
|
163
|
+
* - web → multipart POST to a CDN.
|
|
164
|
+
* - Wails → a Wails-bound upload method.
|
|
165
|
+
*
|
|
166
|
+
* `onProgress` receives 0..1; call it to drive the staging-tray progress.
|
|
167
|
+
*/
|
|
168
|
+
export type ComposerUploadFn = (
|
|
169
|
+
file: File,
|
|
170
|
+
onProgress: (fraction: number) => void,
|
|
171
|
+
) => Promise<ComposerUploadResult>;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Declarative config for the built-in attach pipeline. When present (or
|
|
175
|
+
* when `showAttachmentButton` is set), the composer mounts its own file
|
|
176
|
+
* picker — paperclip, `+` menu, drag-drop and paste all funnel through it.
|
|
177
|
+
*/
|
|
178
|
+
export interface ComposerAttachConfig {
|
|
179
|
+
/** Accepted asset categories. Default: all four. */
|
|
180
|
+
accept?: ComposerAcceptType[];
|
|
181
|
+
/** Max files per message. Falls back to the composer's `maxAttachments`. */
|
|
182
|
+
maxFiles?: number;
|
|
183
|
+
/** Per-file size cap in bytes. Default: unlimited. */
|
|
184
|
+
maxSizeBytes?: number;
|
|
185
|
+
/** Allow selecting more than one file at once. Default `true`. */
|
|
186
|
+
multiple?: boolean;
|
|
187
|
+
/** Upload transport — see {@link ComposerUploadFn}. */
|
|
188
|
+
uploadFn?: ComposerUploadFn;
|
|
189
|
+
/** Enable Ctrl+V / Cmd+V paste-to-attach. Default `true`. */
|
|
190
|
+
pasteEnabled?: boolean;
|
|
191
|
+
/** Called when a file is rejected (size / type / count). */
|
|
192
|
+
onReject?: (file: File, reason: 'size' | 'type' | 'count') => void;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Imperative handle the attach pipeline exposes to the composer + menu. */
|
|
196
|
+
export interface ComposerAttachHandle {
|
|
197
|
+
/** Open the native file picker. */
|
|
198
|
+
openPicker: () => void;
|
|
199
|
+
/** Funnel already-resolved files (drop / paste) through validation. */
|
|
200
|
+
attachFiles: (files: File[]) => void;
|
|
201
|
+
/** Remove a staged attachment, revoking its object-URL first. */
|
|
202
|
+
removeAttachment: (id: string) => void;
|
|
203
|
+
/** True when attaching is unavailable (disabled / at file cap). */
|
|
204
|
+
disabled: boolean;
|
|
205
|
+
/** Hidden `<input type=file>` props — the composer renders this node. */
|
|
206
|
+
inputProps: {
|
|
207
|
+
ref: React.RefObject<HTMLInputElement | null>;
|
|
208
|
+
type: 'file';
|
|
209
|
+
accept: string;
|
|
210
|
+
multiple: boolean;
|
|
211
|
+
hidden: true;
|
|
212
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
213
|
+
};
|
|
214
|
+
}
|