@iloveagents/foundry-web-ui 0.11.1 → 0.12.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.
@@ -4,6 +4,7 @@ import { AssistantRuntimeProvider, useLocalRuntime } from "@assistant-ui/react";
4
4
  import { TooltipProvider } from "../ui/tooltip.js";
5
5
  import { AGUIAdapterSDK } from "../lib/ag-ui-adapter.js";
6
6
  import { FileAttachmentAdapter } from "../lib/attachment-adapter.js";
7
+ import { useAttachmentAdapterStore } from "../lib/attachment-registry.js";
7
8
  import { ShowDocumentToolUI } from "./show-document-tool-ui.js";
8
9
  import { ClientToolExecutor } from "./client-tool-executor.js";
9
10
  const AGUIAdapterContext = createContext(null);
@@ -33,7 +34,10 @@ function AGUIRuntimeInner({ children, fetchFn, threadId, urlMatch, historyAdapte
33
34
  // Only pass options if at least one is set, mirroring the prior
34
35
  // single-arg semantics for the "fresh chat, no overrides" case.
35
36
  fetchFn || threadId ? { fetchFn, threadId } : undefined), [fetchFn, threadId]);
36
- const attachmentAdapter = useMemo(() => new FileAttachmentAdapter(), []);
37
+ // Hosts may register a custom adapter (see ``registerAttachmentAdapter``);
38
+ // the shell's inline-base64 adapter is the default.
39
+ const registeredAdapter = useAttachmentAdapterStore((s) => s.adapter);
40
+ const attachmentAdapter = useMemo(() => registeredAdapter ?? new FileAttachmentAdapter(), [registeredAdapter]);
37
41
  // Build the history adapter ONCE per inner mount, after the AG-UI
38
42
  // adapter exists so we know its thread id. The keyed-remount in the
39
43
  // outer wrapper ensures threadId changes trigger a fresh adapter +
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useState, useRef, useCallback } from "react";
2
+ import { useState, useRef, useCallback, useEffect } from "react";
3
3
  import { ThreadPrimitive, ComposerPrimitive, MessagePrimitive, ActionBarPrimitive, BranchPickerPrimitive, useAui, useAuiState, useThreadViewport, } from "@assistant-ui/react";
4
4
  import { ArrowUp, Copy, Check, RefreshCw, PencilIcon, ChevronLeft, ChevronRight, ChevronDown, Square, Bot, Paperclip, Minimize2, PanelRightClose, PanelRightOpen, } from "lucide-react";
5
5
  import { cn } from "@iloveagents/foundry-web-primitives";
@@ -53,8 +53,22 @@ const ScrollToBottomButton = () => {
53
53
  };
54
54
  const DropZone = ({ children }) => {
55
55
  const [isDragging, setIsDragging] = useState(false);
56
+ const [dropError, setDropError] = useState(null);
57
+ const dropErrorTimer = useRef(null);
56
58
  const dragCounter = useRef(0);
57
59
  const aui = useAui();
60
+ const showDropError = useCallback((message) => {
61
+ setDropError(message);
62
+ if (dropErrorTimer.current)
63
+ clearTimeout(dropErrorTimer.current);
64
+ dropErrorTimer.current = setTimeout(() => setDropError(null), 4000);
65
+ }, []);
66
+ useEffect(() => {
67
+ return () => {
68
+ if (dropErrorTimer.current)
69
+ clearTimeout(dropErrorTimer.current);
70
+ };
71
+ }, []);
58
72
  const handleDragEnter = useCallback((e) => {
59
73
  e.preventDefault();
60
74
  dragCounter.current++;
@@ -75,11 +89,25 @@ const DropZone = ({ children }) => {
75
89
  dragCounter.current = 0;
76
90
  setIsDragging(false);
77
91
  const files = Array.from(e.dataTransfer.files);
92
+ // Drops bypass the file picker's ``accept`` filter, so the adapter's
93
+ // add() is the real gate here — surface its rejections instead of
94
+ // leaving an unhandled promise rejection and a silently missing chip.
95
+ const rejected = [];
78
96
  for (const file of files) {
79
- await aui.composer.addAttachment(file);
97
+ try {
98
+ await aui.composer.addAttachment(file);
99
+ }
100
+ catch (err) {
101
+ rejected.push(`${file.name}: ${err instanceof Error ? err.message : "unsupported file type"}`);
102
+ }
103
+ }
104
+ if (rejected.length > 0) {
105
+ showDropError(rejected.length === 1
106
+ ? `Couldn't attach ${rejected[0]}`
107
+ : `Couldn't attach ${rejected.length} files (${rejected.join("; ")})`);
80
108
  }
81
- }, [aui]);
82
- return (_jsxs("div", { className: "relative flex flex-col flex-1 overflow-hidden", onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, children: [children, isDragging && (_jsx("div", { className: cn("absolute inset-0 z-50 flex items-center justify-center pointer-events-none", "bg-background/80 backdrop-blur-sm", "border-2 border-dashed border-primary rounded-xl"), children: _jsx("p", { className: "text-lg text-primary font-medium", children: "Drop files here" }) }))] }));
109
+ }, [aui, showDropError]);
110
+ return (_jsxs("div", { className: "relative flex flex-col flex-1 overflow-hidden", onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, children: [children, isDragging && (_jsx("div", { className: cn("absolute inset-0 z-50 flex items-center justify-center pointer-events-none", "bg-background/80 backdrop-blur-sm", "border-2 border-dashed border-primary rounded-xl"), children: _jsx("p", { className: "text-lg text-primary font-medium", children: "Drop files here" }) })), dropError && (_jsx("div", { role: "alert", className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 z-50 max-w-[90%]", "rounded-md border border-destructive/50 bg-background px-3 py-2", "text-sm text-destructive shadow-md pointer-events-none truncate"), children: dropError }))] }));
83
111
  };
84
112
  /**
85
113
  * Reusable chat content — used by ChatPage and ChatBubble.
package/dist/index.d.ts CHANGED
@@ -65,7 +65,8 @@ export type { NavItem, NavItemAction, NavItemDnd, NavGroup, NavGroupCreateAction
65
65
  export { usePageTools } from "./lib/use-page-tools.js";
66
66
  export { useNewConversation } from "./lib/use-new-conversation.js";
67
67
  export { AGUIAdapterSDK } from "./lib/ag-ui-adapter.js";
68
- export { FileAttachmentAdapter } from "./lib/attachment-adapter.js";
68
+ export { FileAttachmentAdapter, resolveMimeType } from "./lib/attachment-adapter.js";
69
+ export { registerAttachmentAdapter, useAttachmentAdapterStore, } from "./lib/attachment-registry.js";
69
70
  export { ReasoningPart, ReasoningMessagePartComponent } from "./components/reasoning-part.js";
70
71
  export { ReasoningEffortPicker } from "./components/reasoning-effort-picker.js";
71
72
  export { AuthProvider } from "./lib/auth-provider.js";
package/dist/index.js CHANGED
@@ -73,7 +73,8 @@ export { usePageTools } from "./lib/use-page-tools.js";
73
73
  export { useNewConversation } from "./lib/use-new-conversation.js";
74
74
  // --- Adapters ---
75
75
  export { AGUIAdapterSDK } from "./lib/ag-ui-adapter.js";
76
- export { FileAttachmentAdapter } from "./lib/attachment-adapter.js";
76
+ export { FileAttachmentAdapter, resolveMimeType } from "./lib/attachment-adapter.js";
77
+ export { registerAttachmentAdapter, useAttachmentAdapterStore, } from "./lib/attachment-registry.js";
77
78
  export { ReasoningPart, ReasoningMessagePartComponent } from "./components/reasoning-part.js";
78
79
  export { ReasoningEffortPicker } from "./components/reasoning-effort-picker.js";
79
80
  // --- Auth ---
@@ -20,6 +20,10 @@
20
20
  * lastspace's `_elide_inline_media` for the pattern.
21
21
  */
22
22
  import type { AttachmentAdapter, PendingAttachment, CompleteAttachment, Attachment } from "@assistant-ui/react";
23
+ /** Resolve a file's MIME type, falling back to extension inference when
24
+ * the browser reports an empty ``file.type`` (common for Office files).
25
+ * Exported for host attachment adapters that need the same resolution. */
26
+ export declare function resolveMimeType(file: File): string;
23
27
  export interface FileAttachmentAdapterOptions {
24
28
  /**
25
29
  * How non-image documents (PDF, DOCX) are sent.
@@ -20,17 +20,22 @@
20
20
  * lastspace's `_elide_inline_media` for the pattern.
21
21
  */
22
22
  const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20 MB
23
- /** Infer MIME type from extension when browser reports empty file.type (common for .docx) */
23
+ /** Infer MIME type from extension when browser reports empty file.type (common for Office files) */
24
24
  const EXTENSION_MIME = {
25
25
  ".pdf": "application/pdf",
26
26
  ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
27
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
28
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
27
29
  ".png": "image/png",
28
30
  ".jpg": "image/jpeg",
29
31
  ".jpeg": "image/jpeg",
30
32
  ".gif": "image/gif",
31
33
  ".webp": "image/webp",
32
34
  };
33
- function resolveMimeType(file) {
35
+ /** Resolve a file's MIME type, falling back to extension inference when
36
+ * the browser reports an empty ``file.type`` (common for Office files).
37
+ * Exported for host attachment adapters that need the same resolution. */
38
+ export function resolveMimeType(file) {
34
39
  if (file.type)
35
40
  return file.type;
36
41
  const ext = file.name.slice(file.name.lastIndexOf(".")).toLowerCase();
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Attachment adapter registry — host-injectable composer attachment handling.
3
+ *
4
+ * The shell's AG-UI runtime builds its attachment adapter internally
5
+ * (``FileAttachmentAdapter``), which reads files client-side and inlines
6
+ * them as base64 message parts. Hosts sometimes need a different
7
+ * strategy — e.g. uploading Office documents into a backend workspace
8
+ * and handing the agent a reference instead of bytes. Threading an
9
+ * adapter through every chat mount would couple the shell to feature
10
+ * modules, so this is a tiny registry instead — the exact pattern of
11
+ * ``registerChatSlots``: a module calls :func:`registerAttachmentAdapter`
12
+ * once (import time or ``useInit``) and the runtime provider picks it up
13
+ * on its next mount.
14
+ *
15
+ * Compose, don't replace: hosts typically wrap their special-case
16
+ * adapter together with the default in assistant-ui's
17
+ * ``CompositeAttachmentAdapter`` so images/PDF keep the stock inline
18
+ * behavior while selected types get the custom path. The composite also
19
+ * closes the drag-and-drop loophole — its ``add()`` rejects files no
20
+ * member adapter accepts, whereas drops bypass the file picker's
21
+ * ``accept`` filter.
22
+ */
23
+ import type { AttachmentAdapter } from "@assistant-ui/react";
24
+ interface AttachmentAdapterState {
25
+ adapter: AttachmentAdapter | null;
26
+ }
27
+ export declare const useAttachmentAdapterStore: import("zustand").UseBoundStore<import("zustand").StoreApi<AttachmentAdapterState>>;
28
+ /**
29
+ * Register the composer attachment adapter. ``null`` restores the
30
+ * shell's default (``FileAttachmentAdapter``). Later calls overwrite
31
+ * earlier ones — last registration wins.
32
+ */
33
+ export declare function registerAttachmentAdapter(adapter: AttachmentAdapter | null): void;
34
+ export {};
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Attachment adapter registry — host-injectable composer attachment handling.
3
+ *
4
+ * The shell's AG-UI runtime builds its attachment adapter internally
5
+ * (``FileAttachmentAdapter``), which reads files client-side and inlines
6
+ * them as base64 message parts. Hosts sometimes need a different
7
+ * strategy — e.g. uploading Office documents into a backend workspace
8
+ * and handing the agent a reference instead of bytes. Threading an
9
+ * adapter through every chat mount would couple the shell to feature
10
+ * modules, so this is a tiny registry instead — the exact pattern of
11
+ * ``registerChatSlots``: a module calls :func:`registerAttachmentAdapter`
12
+ * once (import time or ``useInit``) and the runtime provider picks it up
13
+ * on its next mount.
14
+ *
15
+ * Compose, don't replace: hosts typically wrap their special-case
16
+ * adapter together with the default in assistant-ui's
17
+ * ``CompositeAttachmentAdapter`` so images/PDF keep the stock inline
18
+ * behavior while selected types get the custom path. The composite also
19
+ * closes the drag-and-drop loophole — its ``add()`` rejects files no
20
+ * member adapter accepts, whereas drops bypass the file picker's
21
+ * ``accept`` filter.
22
+ */
23
+ import { create } from "zustand";
24
+ export const useAttachmentAdapterStore = create(() => ({
25
+ adapter: null,
26
+ }));
27
+ /**
28
+ * Register the composer attachment adapter. ``null`` restores the
29
+ * shell's default (``FileAttachmentAdapter``). Later calls overwrite
30
+ * earlier ones — last registration wins.
31
+ */
32
+ export function registerAttachmentAdapter(adapter) {
33
+ useAttachmentAdapterStore.setState({ adapter });
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iloveagents/foundry-web-ui",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "license": "MIT",
5
5
  "description": "React agent UI core for Foundry UI — chat, composer, AG-UI adapter for assistant-ui, tool cards, panels, sidebar, theme runtime, and UI stores.",
6
6
  "keywords": [
@@ -70,8 +70,8 @@
70
70
  "tailwind-merge": "^3.5.0",
71
71
  "react-markdown": "^10.0.0",
72
72
  "remark-gfm": "^4.0.0",
73
- "@iloveagents/foundry-agent": "^0.11.1",
74
- "@iloveagents/foundry-web-primitives": "^0.11.1"
73
+ "@iloveagents/foundry-agent": "^0.12.0",
74
+ "@iloveagents/foundry-web-primitives": "^0.12.0"
75
75
  },
76
76
  "devDependencies": {
77
77
  "@ag-ui/client": "^0.0.52",