@iloveagents/foundry-web-ui 0.7.0 → 0.8.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 +1 -1
- package/dist/components/app-brand.d.ts +1 -1
- package/dist/components/app-brand.js +1 -1
- package/dist/components/assistant-chat.d.ts +2 -2
- package/dist/components/assistant-chat.js +8 -5
- package/dist/components/chat-bubble.js +2 -0
- package/dist/components/composer-submit-bridge.js +7 -5
- package/dist/components/reasoning-effort-picker.d.ts +11 -0
- package/dist/components/reasoning-effort-picker.js +25 -0
- package/dist/components/reasoning-part.d.ts +15 -0
- package/dist/components/reasoning-part.js +21 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/lib/ag-ui-adapter.js +21 -0
- package/dist/lib/attachment-adapter.d.ts +30 -6
- package/dist/lib/attachment-adapter.js +22 -9
- package/package.json +22 -5
package/README.md
CHANGED
|
@@ -47,4 +47,4 @@ Feature modules (like SPACES) plug into `@iloveagents/foundry-web-ui` via regist
|
|
|
47
47
|
- **Citation handler** — handles `[n]` citation clicks in markdown
|
|
48
48
|
- **Tool UI** — custom rendering for agent tools via `makeAssistantToolUI`
|
|
49
49
|
|
|
50
|
-
See [AGENTS.md](
|
|
50
|
+
See [AGENTS.md](https://github.com/iLoveAgents/foundry-ui/blob/main/packages/web-ui/AGENTS.md) for architecture details and import boundary rules.
|
|
@@ -14,7 +14,7 @@ export interface BrandLockup {
|
|
|
14
14
|
*
|
|
15
15
|
* The rule worth stating: whether the name is printed depends on the *shape*
|
|
16
16
|
* of the brand image. A wordmark already spells the brand out, so pairing it
|
|
17
|
-
* with `appName` stutters ("
|
|
17
|
+
* with `appName` stutters ("Acme Acme"). An icon says nothing on its
|
|
18
18
|
* own, so it needs the name to complete the lockup. Hence an icon wins over a
|
|
19
19
|
* wordmark when both are set — supplying an icon is an explicit request for
|
|
20
20
|
* "<mark> Product Name", which is what a tenant whose product name differs
|
|
@@ -9,7 +9,7 @@ import { useThemeRuntime } from "./theme-runtime-provider.js";
|
|
|
9
9
|
*
|
|
10
10
|
* The rule worth stating: whether the name is printed depends on the *shape*
|
|
11
11
|
* of the brand image. A wordmark already spells the brand out, so pairing it
|
|
12
|
-
* with `appName` stutters ("
|
|
12
|
+
* with `appName` stutters ("Acme Acme"). An icon says nothing on its
|
|
13
13
|
* own, so it needs the name to complete the lockup. Hence an icon wins over a
|
|
14
14
|
* wordmark when both are set — supplying an icon is an explicit request for
|
|
15
15
|
* "<mark> Product Name", which is what a tenant whose product name differs
|
|
@@ -8,8 +8,8 @@ export interface ChatContentProps {
|
|
|
8
8
|
/**
|
|
9
9
|
* Starter prompts shown when the thread is empty. Defaults to
|
|
10
10
|
* `DEFAULT_STARTER_SUGGESTIONS`. Pass an empty array to hide the
|
|
11
|
-
* suggestion buttons entirely.
|
|
12
|
-
*
|
|
11
|
+
* suggestion buttons entirely. Downstream apps can pass a
|
|
12
|
+
* workspace-aware list derived from app context.
|
|
13
13
|
*/
|
|
14
14
|
starterSuggestions?: string[];
|
|
15
15
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useRef, useCallback } from "react";
|
|
3
|
-
import { ThreadPrimitive, ComposerPrimitive, MessagePrimitive, ActionBarPrimitive, BranchPickerPrimitive,
|
|
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";
|
|
6
6
|
import { Button } from "@iloveagents/foundry-web-primitives";
|
|
@@ -12,6 +12,8 @@ import { ComposerSubmitBridge } from "./composer-submit-bridge.js";
|
|
|
12
12
|
import { ChatComposerBannerSlot } from "./chat-slots.js";
|
|
13
13
|
import { useChatBubbleStore } from "../lib/chat-bubble-store.js";
|
|
14
14
|
import { MarkdownText } from "./markdown-text.js";
|
|
15
|
+
import { ReasoningMessagePartComponent } from "./reasoning-part.js";
|
|
16
|
+
import { ReasoningEffortPicker } from "./reasoning-effort-picker.js";
|
|
15
17
|
import { ToolFallback } from "./tool-fallback.js";
|
|
16
18
|
import { LoadingIndicator, LoadingBar } from "./loading-indicator.js";
|
|
17
19
|
import { MessageTimestamp } from "./message-timestamp.js";
|
|
@@ -24,12 +26,13 @@ const UserMessage = () => {
|
|
|
24
26
|
const UserActionBar = () => (_jsx(ActionBarPrimitive.Root, { hideWhenRunning: true, className: "flex items-center opacity-0 group-hover:opacity-100 transition-opacity", children: _jsx(ActionBarPrimitive.Edit, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Edit", size: "icon", children: _jsx(PencilIcon, { className: "size-4" }) }) }) }));
|
|
25
27
|
const AssistantMessage = () => (_jsx(MessagePrimitive.Root, { className: "w-full mb-2 group", children: _jsxs("div", { className: "mx-auto max-w-3xl px-4", children: [_jsx("div", { className: "text-foreground leading-relaxed flex flex-col gap-4", children: _jsx(MessagePrimitive.Parts, { components: {
|
|
26
28
|
Text: MarkdownText,
|
|
29
|
+
Reasoning: ReasoningMessagePartComponent,
|
|
27
30
|
tools: { Fallback: ToolFallback },
|
|
28
31
|
} }) }), _jsx(MessageError, {}), _jsxs("div", { className: "flex items-center gap-2 mt-1", children: [_jsx("div", { className: "flex h-8 w-[4.25rem] items-center", children: _jsx(AssistantActionBar, {}) }), _jsx(BranchPicker, {}), _jsx(MessageTimestamp, {})] })] }) }));
|
|
29
32
|
const AssistantActionBar = () => (_jsxs(ActionBarPrimitive.Root, { hideWhenRunning: true, className: cn("pointer-events-none flex items-center gap-1 -ml-2 opacity-0 transition-opacity", "group-hover:pointer-events-auto group-hover:opacity-100", "group-focus-within:pointer-events-auto group-focus-within:opacity-100"), children: [_jsx(ActionBarPrimitive.Copy, { asChild: true, children: _jsxs(TooltipIconButton, { tooltip: "Copy", size: "icon", children: [_jsx(MessagePrimitive.If, { copied: false, children: _jsx(Copy, { className: "size-4" }) }), _jsx(MessagePrimitive.If, { copied: true, children: _jsx(Check, { className: "size-4 text-primary" }) })] }) }), _jsx(ActionBarPrimitive.Reload, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Refresh", size: "icon", children: _jsx(RefreshCw, { className: "size-4" }) }) })] }));
|
|
30
33
|
const EditComposer = () => (_jsx(MessagePrimitive.Root, { className: "w-full mb-2", children: _jsx("div", { className: "flex items-start justify-end", children: _jsx("div", { className: "w-full max-w-[80%]", children: _jsxs(ComposerPrimitive.Root, { className: cn("flex flex-col gap-2", "rounded-2xl border border-input", "bg-background shadow-sm", "px-4 py-3"), children: [_jsx(ComposerPrimitive.Input, { autoFocus: true, className: cn("w-full bg-transparent", "text-base text-foreground", "outline-none resize-none", "placeholder:text-muted-foreground") }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx(ComposerPrimitive.Cancel, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "sm", children: "Cancel" }) }), _jsx(ComposerPrimitive.Send, { asChild: true, children: _jsx(Button, { size: "sm", children: "Update" }) })] })] }) }) }) }));
|
|
31
34
|
const ComposerActionButton = () => {
|
|
32
|
-
return (_jsxs(_Fragment, { children: [_jsx(ThreadPrimitive.If, { running: true, children: _jsx(ComposerPrimitive.Cancel, { asChild: true, children: _jsx(Button, { variant: "secondary", size: "icon", className: "rounded-full size-10 flex-shrink-0", "aria-label": "Stop generating", children: _jsx(Square, { className: "size-4" }) }) }) }), _jsx(ThreadPrimitive.If, { running: false, children: _jsx(ComposerPrimitive.Send, { asChild: true, children: _jsx(Button, { size: "icon", className: "rounded-full size-10 flex-shrink-0", "aria-label": "Send message", children: _jsx(ArrowUp, { className: "size-5" }) }) }) })] }));
|
|
35
|
+
return (_jsxs(_Fragment, { children: [_jsx(ThreadPrimitive.If, { running: false, children: _jsx(ReasoningEffortPicker, { className: "mr-1" }) }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(ComposerPrimitive.Cancel, { asChild: true, children: _jsx(Button, { variant: "secondary", size: "icon", className: "rounded-full size-10 flex-shrink-0", "aria-label": "Stop generating", children: _jsx(Square, { className: "size-4" }) }) }) }), _jsx(ThreadPrimitive.If, { running: false, children: _jsx(ComposerPrimitive.Send, { asChild: true, children: _jsx(Button, { size: "icon", className: "rounded-full size-10 flex-shrink-0", "aria-label": "Send message", children: _jsx(ArrowUp, { className: "size-5" }) }) }) })] }));
|
|
33
36
|
};
|
|
34
37
|
/**
|
|
35
38
|
* Default starter prompts shown on empty state. Override per-app by passing
|
|
@@ -51,7 +54,7 @@ const ScrollToBottomButton = () => {
|
|
|
51
54
|
const DropZone = ({ children }) => {
|
|
52
55
|
const [isDragging, setIsDragging] = useState(false);
|
|
53
56
|
const dragCounter = useRef(0);
|
|
54
|
-
const
|
|
57
|
+
const aui = useAui();
|
|
55
58
|
const handleDragEnter = useCallback((e) => {
|
|
56
59
|
e.preventDefault();
|
|
57
60
|
dragCounter.current++;
|
|
@@ -73,9 +76,9 @@ const DropZone = ({ children }) => {
|
|
|
73
76
|
setIsDragging(false);
|
|
74
77
|
const files = Array.from(e.dataTransfer.files);
|
|
75
78
|
for (const file of files) {
|
|
76
|
-
await
|
|
79
|
+
await aui.composer.addAttachment(file);
|
|
77
80
|
}
|
|
78
|
-
}, [
|
|
81
|
+
}, [aui]);
|
|
79
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" }) }))] }));
|
|
80
83
|
};
|
|
81
84
|
/**
|
|
@@ -9,6 +9,7 @@ import { ContextPins } from "./context-bar.js";
|
|
|
9
9
|
import { ComposerContextBadges, SentContextBadges } from "./context-badges.js";
|
|
10
10
|
import { ComposerSubmitBridge } from "./composer-submit-bridge.js";
|
|
11
11
|
import { ChatComposerBannerSlot, ChatLauncherBadgeSlot } from "./chat-slots.js";
|
|
12
|
+
import { ReasoningMessagePartComponent } from "./reasoning-part.js";
|
|
12
13
|
import { MarkdownText } from "./markdown-text.js";
|
|
13
14
|
import { LoadingIndicator } from "./loading-indicator.js";
|
|
14
15
|
import { MessageTimestamp } from "./message-timestamp.js";
|
|
@@ -21,6 +22,7 @@ const CHAT_BUBBLE_INSET = 8;
|
|
|
21
22
|
function BubbleAssistantMessage() {
|
|
22
23
|
return (_jsxs(MessagePrimitive.Root, { className: "w-full mb-2 group", children: [_jsx("div", { className: "text-foreground text-sm leading-relaxed flex flex-col gap-2", children: _jsx(MessagePrimitive.Parts, { components: {
|
|
23
24
|
Text: MarkdownText,
|
|
25
|
+
Reasoning: ReasoningMessagePartComponent,
|
|
24
26
|
} }) }), _jsx(MessageError, { compact: true }), _jsxs("div", { className: "flex h-7 items-center gap-2 mt-1", children: [_jsxs(ActionBarPrimitive.Root, { hideWhenRunning: true, className: cn("pointer-events-none flex items-center gap-1 opacity-0 transition-opacity", "group-hover:pointer-events-auto group-hover:opacity-100", "group-focus-within:pointer-events-auto group-focus-within:opacity-100"), children: [_jsx(ActionBarPrimitive.Copy, { asChild: true, children: _jsxs(TooltipIconButton, { tooltip: "Copy", size: "icon", className: "size-7", children: [_jsx(MessagePrimitive.If, { copied: false, children: _jsx(Copy, { className: "size-3" }) }), _jsx(MessagePrimitive.If, { copied: true, children: _jsx(Check, { className: "size-3 text-primary" }) })] }) }), _jsx(ActionBarPrimitive.Reload, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Retry", size: "icon", className: "size-7", children: _jsx(RefreshCw, { className: "size-3" }) }) })] }), _jsx(MessageTimestamp, {})] })] }));
|
|
25
27
|
}
|
|
26
28
|
function BubbleUserMessage() {
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { useEffect } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { useAui } from "@assistant-ui/react";
|
|
3
3
|
import { useComposerSubmitStore } from "../lib/composer-submit-store.js";
|
|
4
4
|
export function ComposerSubmitBridge() {
|
|
5
|
-
const
|
|
5
|
+
const aui = useAui();
|
|
6
6
|
const pending = useComposerSubmitStore((s) => s.pending);
|
|
7
7
|
useEffect(() => {
|
|
8
8
|
if (!pending)
|
|
9
9
|
return;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// send() starts the run for the queued text; steering/queueing semantics
|
|
11
|
+
// are handled by the composer client (assistant-ui >= 0.15).
|
|
12
|
+
aui.composer.setText(pending.text);
|
|
13
|
+
aui.composer.send();
|
|
12
14
|
useComposerSubmitStore.getState().consume(pending.id);
|
|
13
|
-
}, [
|
|
15
|
+
}, [aui, pending]);
|
|
14
16
|
return null;
|
|
15
17
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lets the user choose how hard the model thinks, per conversation.
|
|
3
|
+
*
|
|
4
|
+
* Sits in the composer next to Send. The choice rides on every request as
|
|
5
|
+
* an AG-UI forwarded prop and is read at send time, so switching it
|
|
6
|
+
* mid-conversation applies from the next message on. `Auto` sends nothing
|
|
7
|
+
* and leaves the backend's configured level in place.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ReasoningEffortPicker({ className }: {
|
|
10
|
+
className?: string;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useStore } from "zustand";
|
|
3
|
+
import { Check, ChevronDown } from "lucide-react";
|
|
4
|
+
import { reasoningEffortStore, REASONING_EFFORT_LABELS, } from "@iloveagents/foundry-agent";
|
|
5
|
+
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
6
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "../ui/dropdown-menu.js";
|
|
7
|
+
const OPTIONS = [
|
|
8
|
+
{ value: "default", hint: "Let the app decide" },
|
|
9
|
+
{ value: "low", hint: "Answers sooner, thinks less" },
|
|
10
|
+
{ value: "medium", hint: "Good default for most questions" },
|
|
11
|
+
{ value: "high", hint: "Slower, best for hard problems" },
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Lets the user choose how hard the model thinks, per conversation.
|
|
15
|
+
*
|
|
16
|
+
* Sits in the composer next to Send. The choice rides on every request as
|
|
17
|
+
* an AG-UI forwarded prop and is read at send time, so switching it
|
|
18
|
+
* mid-conversation applies from the next message on. `Auto` sends nothing
|
|
19
|
+
* and leaves the backend's configured level in place.
|
|
20
|
+
*/
|
|
21
|
+
export function ReasoningEffortPicker({ className }) {
|
|
22
|
+
const effort = useStore(reasoningEffortStore, (s) => s.effort);
|
|
23
|
+
const setEffort = useStore(reasoningEffortStore, (s) => s.setEffort);
|
|
24
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: cn("flex items-center gap-1 rounded-md px-2 py-1", "text-xs text-muted-foreground hover:text-foreground hover:bg-muted", "transition-colors", className), "aria-label": `Thinking effort: ${REASONING_EFFORT_LABELS[effort]}`, children: [_jsx("span", { children: REASONING_EFFORT_LABELS[effort] }), _jsx(ChevronDown, { className: "size-3.5" })] }) }), _jsx(DropdownMenuContent, { align: "end", className: "w-56", children: OPTIONS.map((opt) => (_jsxs(DropdownMenuItem, { onSelect: () => setEffort(opt.value), className: "flex items-start gap-2", children: [_jsx(Check, { className: cn("size-4 mt-0.5", effort === opt.value ? "opacity-100" : "opacity-0") }), _jsxs("span", { className: "flex flex-col", children: [_jsx("span", { className: "text-sm", children: REASONING_EFFORT_LABELS[opt.value] }), _jsx("span", { className: "text-xs text-muted-foreground", children: opt.hint })] })] }, opt.value))) })] }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a `reasoning` message part — the model's own thinking, streamed
|
|
3
|
+
* over AG-UI's `REASONING_*` events and kept separate from the answer.
|
|
4
|
+
*
|
|
5
|
+
* Collapsed by default and deliberately understated: reasoning is context for
|
|
6
|
+
* a curious reader, not the response. It stays visible after the run so a
|
|
7
|
+
* reader can check *why* an answer came out the way it did.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ReasoningPart({ text }: {
|
|
10
|
+
text: string;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
12
|
+
/** Adapter for assistant-ui's `Reasoning` part slot. */
|
|
13
|
+
export declare const ReasoningMessagePartComponent: ({ text }: {
|
|
14
|
+
text: string;
|
|
15
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Brain, ChevronDown } from "lucide-react";
|
|
4
|
+
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
5
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible.js";
|
|
6
|
+
/**
|
|
7
|
+
* Renders a `reasoning` message part — the model's own thinking, streamed
|
|
8
|
+
* over AG-UI's `REASONING_*` events and kept separate from the answer.
|
|
9
|
+
*
|
|
10
|
+
* Collapsed by default and deliberately understated: reasoning is context for
|
|
11
|
+
* a curious reader, not the response. It stays visible after the run so a
|
|
12
|
+
* reader can check *why* an answer came out the way it did.
|
|
13
|
+
*/
|
|
14
|
+
export function ReasoningPart({ text }) {
|
|
15
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
16
|
+
if (!text.trim())
|
|
17
|
+
return null;
|
|
18
|
+
return (_jsxs(Collapsible, { open: isOpen, onOpenChange: setIsOpen, className: "w-full", children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: cn("flex items-center gap-1.5 text-xs text-muted-foreground", "hover:text-foreground transition-colors"), "aria-label": isOpen ? "Hide reasoning" : "Show reasoning", children: [_jsx(Brain, { className: "size-3.5" }), _jsx("span", { children: "Reasoning" }), _jsx(ChevronDown, { className: cn("size-3.5 transition-transform", isOpen && "rotate-180") })] }) }), _jsx(CollapsibleContent, { children: _jsx("div", { className: cn("mt-1.5 whitespace-pre-wrap border-l-2 border-border pl-3", "text-xs text-muted-foreground"), children: text }) })] }));
|
|
19
|
+
}
|
|
20
|
+
/** Adapter for assistant-ui's `Reasoning` part slot. */
|
|
21
|
+
export const ReasoningMessagePartComponent = ({ text }) => (_jsx(ReasoningPart, { text: text }));
|
package/dist/index.d.ts
CHANGED
|
@@ -40,8 +40,8 @@ export { FormDialog } from "./components/form-dialog.js";
|
|
|
40
40
|
export { ConfirmDialog } from "./components/confirm-dialog.js";
|
|
41
41
|
export { NameDialog } from "./components/name-dialog.js";
|
|
42
42
|
export { SurfaceCard, surfaceCardVariants } from "./components/surface-card.js";
|
|
43
|
-
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider
|
|
44
|
-
export { Collapsible, CollapsibleTrigger, CollapsibleContent
|
|
43
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "./ui/tooltip.js";
|
|
44
|
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent } from "./ui/collapsible.js";
|
|
45
45
|
export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel, } from "./ui/dropdown-menu.js";
|
|
46
46
|
export { Dialog, DialogPortal, DialogOverlay, DialogTrigger, DialogClose, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, } from "./ui/dialog.js";
|
|
47
47
|
export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent } from "./ui/popover.js";
|
|
@@ -66,4 +66,6 @@ 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
68
|
export { FileAttachmentAdapter } from "./lib/attachment-adapter.js";
|
|
69
|
+
export { ReasoningPart, ReasoningMessagePartComponent } from "./components/reasoning-part.js";
|
|
70
|
+
export { ReasoningEffortPicker } from "./components/reasoning-effort-picker.js";
|
|
69
71
|
export { AuthProvider } from "./lib/auth-provider.js";
|
package/dist/index.js
CHANGED
|
@@ -44,8 +44,8 @@ export { SurfaceCard, surfaceCardVariants } from "./components/surface-card.js";
|
|
|
44
44
|
// coupling to ThemeScope (theme-runtime-provider.tsx) — they need a
|
|
45
45
|
// theme-system refactor before they can move to the leaf primitives
|
|
46
46
|
// package. Tracked for a follow-up under #95/#96.
|
|
47
|
-
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider
|
|
48
|
-
export { Collapsible, CollapsibleTrigger, CollapsibleContent
|
|
47
|
+
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "./ui/tooltip.js";
|
|
48
|
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent } from "./ui/collapsible.js";
|
|
49
49
|
export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel, } from "./ui/dropdown-menu.js";
|
|
50
50
|
export { Dialog, DialogPortal, DialogOverlay, DialogTrigger, DialogClose, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, } from "./ui/dialog.js";
|
|
51
51
|
export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent } from "./ui/popover.js";
|
|
@@ -74,6 +74,8 @@ export { useNewConversation } from "./lib/use-new-conversation.js";
|
|
|
74
74
|
// --- Adapters ---
|
|
75
75
|
export { AGUIAdapterSDK } from "./lib/ag-ui-adapter.js";
|
|
76
76
|
export { FileAttachmentAdapter } from "./lib/attachment-adapter.js";
|
|
77
|
+
export { ReasoningPart, ReasoningMessagePartComponent } from "./components/reasoning-part.js";
|
|
78
|
+
export { ReasoningEffortPicker } from "./components/reasoning-effort-picker.js";
|
|
77
79
|
// --- Auth ---
|
|
78
80
|
// `AuthProvider` is the React MSAL bootstrap (uses `@azure/msal-react`).
|
|
79
81
|
// Token store, config, fetch wrapper all live in `@iloveagents/foundry-agent/msal`.
|
|
@@ -46,6 +46,10 @@ export class AGUIAdapterSDK {
|
|
|
46
46
|
useAppStore.getState().consumeContextForMessage(lastUserMsg.id);
|
|
47
47
|
}
|
|
48
48
|
let currentText = "";
|
|
49
|
+
// Reasoning accumulates across the WHOLE run, unlike `currentText` which
|
|
50
|
+
// is per-turn: the collapsed reasoning panel shouldn't lose what the model
|
|
51
|
+
// thought before a tool call just because the runner re-issued the turn.
|
|
52
|
+
let currentReasoning = "";
|
|
49
53
|
const toolCalls = new Map();
|
|
50
54
|
let runError = null;
|
|
51
55
|
// Liveness clock for the working indicator: every runner event (incl.
|
|
@@ -64,6 +68,11 @@ export class AGUIAdapterSDK {
|
|
|
64
68
|
*/
|
|
65
69
|
const buildResult = (statusOverride) => {
|
|
66
70
|
const content = [];
|
|
71
|
+
// Reasoning first: it precedes the answer chronologically, and
|
|
72
|
+
// assistant-ui renders parts in array order.
|
|
73
|
+
if (currentReasoning) {
|
|
74
|
+
content.push({ type: "reasoning", text: currentReasoning });
|
|
75
|
+
}
|
|
67
76
|
for (const [id, tc] of toolCalls) {
|
|
68
77
|
let parsedArgs = {};
|
|
69
78
|
try {
|
|
@@ -143,6 +152,18 @@ export class AGUIAdapterSDK {
|
|
|
143
152
|
currentText += event.delta;
|
|
144
153
|
shouldYield = true;
|
|
145
154
|
break;
|
|
155
|
+
case "reasoning-delta":
|
|
156
|
+
currentReasoning += event.delta;
|
|
157
|
+
shouldYield = true;
|
|
158
|
+
break;
|
|
159
|
+
case "reasoning-end":
|
|
160
|
+
shouldYield = true;
|
|
161
|
+
break;
|
|
162
|
+
case "step-started":
|
|
163
|
+
case "step-finished":
|
|
164
|
+
// Progress only — the status store already carries the step name
|
|
165
|
+
// (pushed by the runner alongside these events).
|
|
166
|
+
break;
|
|
146
167
|
case "text-message-end":
|
|
147
168
|
shouldYield = true;
|
|
148
169
|
break;
|
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File attachment adapter for assistant-ui.
|
|
3
3
|
*
|
|
4
|
-
* Reads
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Reads attachments client-side as base64 and produces FileMessagePart content
|
|
5
|
+
* for both images and documents. The AG-UI adapter turns those into `binary`
|
|
6
|
+
* message parts (mimeType + data + filename), which the protocol carries
|
|
7
|
+
* natively and model providers accept as file input.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Documents used to degrade to a text stub (`[Attached file: x.pdf; ...]`) so
|
|
10
|
+
* the starter wouldn't send payloads to providers lacking file input. That
|
|
11
|
+
* silently discarded the bytes: the user attaches a PDF, the model receives
|
|
12
|
+
* only its filename, and — having been told a file was attached — goes hunting
|
|
13
|
+
* for a matching document in the backend and reports that it can't read it.
|
|
14
|
+
* Losing user data silently is the worse failure, so documents are sent by
|
|
15
|
+
* default. Pass `{ documentMode: "metadata" }` to restore the stub for agents
|
|
16
|
+
* whose model genuinely can't accept file input.
|
|
17
|
+
*
|
|
18
|
+
* Note for hosts: the bytes ride along on every turn of the live conversation
|
|
19
|
+
* and land in persisted history unless the backend elides them — see
|
|
20
|
+
* lastspace's `_elide_inline_media` for the pattern.
|
|
11
21
|
*/
|
|
12
22
|
import type { AttachmentAdapter, PendingAttachment, CompleteAttachment, Attachment } from "@assistant-ui/react";
|
|
23
|
+
export interface FileAttachmentAdapterOptions {
|
|
24
|
+
/**
|
|
25
|
+
* How non-image documents (PDF, DOCX) are sent.
|
|
26
|
+
*
|
|
27
|
+
* - `"binary"` (default) — base64 bytes as a file part, so the model can
|
|
28
|
+
* actually read the document.
|
|
29
|
+
* - `"metadata"` — filename/type/size as text only. For agents whose model
|
|
30
|
+
* cannot accept file input; the agent sees that a file was attached but
|
|
31
|
+
* never its contents.
|
|
32
|
+
*/
|
|
33
|
+
documentMode?: "binary" | "metadata";
|
|
34
|
+
}
|
|
13
35
|
export declare class FileAttachmentAdapter implements AttachmentAdapter {
|
|
14
36
|
accept: string;
|
|
37
|
+
private readonly documentMode;
|
|
38
|
+
constructor(options?: FileAttachmentAdapterOptions);
|
|
15
39
|
add({ file }: {
|
|
16
40
|
file: File;
|
|
17
41
|
}): Promise<PendingAttachment>;
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File attachment adapter for assistant-ui.
|
|
3
3
|
*
|
|
4
|
-
* Reads
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Reads attachments client-side as base64 and produces FileMessagePart content
|
|
5
|
+
* for both images and documents. The AG-UI adapter turns those into `binary`
|
|
6
|
+
* message parts (mimeType + data + filename), which the protocol carries
|
|
7
|
+
* natively and model providers accept as file input.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Documents used to degrade to a text stub (`[Attached file: x.pdf; ...]`) so
|
|
10
|
+
* the starter wouldn't send payloads to providers lacking file input. That
|
|
11
|
+
* silently discarded the bytes: the user attaches a PDF, the model receives
|
|
12
|
+
* only its filename, and — having been told a file was attached — goes hunting
|
|
13
|
+
* for a matching document in the backend and reports that it can't read it.
|
|
14
|
+
* Losing user data silently is the worse failure, so documents are sent by
|
|
15
|
+
* default. Pass `{ documentMode: "metadata" }` to restore the stub for agents
|
|
16
|
+
* whose model genuinely can't accept file input.
|
|
17
|
+
*
|
|
18
|
+
* Note for hosts: the bytes ride along on every turn of the live conversation
|
|
19
|
+
* and land in persisted history unless the backend elides them — see
|
|
20
|
+
* lastspace's `_elide_inline_media` for the pattern.
|
|
11
21
|
*/
|
|
12
22
|
const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20 MB
|
|
13
23
|
/** Infer MIME type from extension when browser reports empty file.type (common for .docx) */
|
|
@@ -39,8 +49,9 @@ function fileToBase64(file) {
|
|
|
39
49
|
});
|
|
40
50
|
}
|
|
41
51
|
export class FileAttachmentAdapter {
|
|
42
|
-
constructor() {
|
|
52
|
+
constructor(options = {}) {
|
|
43
53
|
this.accept = "image/*,application/pdf,.pdf,.docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
54
|
+
this.documentMode = options.documentMode ?? "binary";
|
|
44
55
|
}
|
|
45
56
|
async add({ file }) {
|
|
46
57
|
if (file.size > MAX_FILE_SIZE) {
|
|
@@ -58,7 +69,7 @@ export class FileAttachmentAdapter {
|
|
|
58
69
|
}
|
|
59
70
|
async send(attachment) {
|
|
60
71
|
const mimeType = attachment.contentType ?? "application/octet-stream";
|
|
61
|
-
if (!mimeType.startsWith("image/")) {
|
|
72
|
+
if (!mimeType.startsWith("image/") && this.documentMode === "metadata") {
|
|
62
73
|
return {
|
|
63
74
|
...attachment,
|
|
64
75
|
status: { type: "complete" },
|
|
@@ -67,7 +78,9 @@ export class FileAttachmentAdapter {
|
|
|
67
78
|
type: "text",
|
|
68
79
|
text: `[Attached file: ${attachment.name}` +
|
|
69
80
|
`; type: ${mimeType}` +
|
|
70
|
-
`; size: ${formatFileSize(attachment.file.size)}
|
|
81
|
+
`; size: ${formatFileSize(attachment.file.size)}` +
|
|
82
|
+
`. Contents were not sent — this agent is configured for ` +
|
|
83
|
+
`metadata-only attachments, so tell the user you cannot read it.]`,
|
|
71
84
|
},
|
|
72
85
|
],
|
|
73
86
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"license": "MIT",
|
|
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
|
+
"keywords": [
|
|
7
|
+
"react",
|
|
8
|
+
"ag-ui",
|
|
9
|
+
"assistant-ui",
|
|
10
|
+
"chat",
|
|
11
|
+
"agent",
|
|
12
|
+
"foundry-ui",
|
|
13
|
+
"agent-framework"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/iLoveAgents/foundry-ui.git",
|
|
18
|
+
"directory": "packages/web-ui"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/iLoveAgents/foundry-ui/tree/main/packages/web-ui#readme",
|
|
21
|
+
"bugs": "https://github.com/iLoveAgents/foundry-ui/issues",
|
|
5
22
|
"type": "module",
|
|
6
23
|
"main": "./dist/index.js",
|
|
7
24
|
"types": "./dist/index.d.ts",
|
|
@@ -24,7 +41,7 @@
|
|
|
24
41
|
"react": "^19.0.0",
|
|
25
42
|
"react-dom": "^19.0.0",
|
|
26
43
|
"react-router": "^7.0.0",
|
|
27
|
-
"@assistant-ui/react": "^0.
|
|
44
|
+
"@assistant-ui/react": "^0.15.1",
|
|
28
45
|
"@ag-ui/client": "^0.0.52",
|
|
29
46
|
"@ag-ui/core": "^0.0.52",
|
|
30
47
|
"zustand": "^5.0.0",
|
|
@@ -53,13 +70,13 @@
|
|
|
53
70
|
"tailwind-merge": "^3.5.0",
|
|
54
71
|
"react-markdown": "^10.0.0",
|
|
55
72
|
"remark-gfm": "^4.0.0",
|
|
56
|
-
"@iloveagents/foundry-agent": "^0.
|
|
57
|
-
"@iloveagents/foundry-web-primitives": "^0.
|
|
73
|
+
"@iloveagents/foundry-agent": "^0.8.0",
|
|
74
|
+
"@iloveagents/foundry-web-primitives": "^0.8.0"
|
|
58
75
|
},
|
|
59
76
|
"devDependencies": {
|
|
60
77
|
"@ag-ui/client": "^0.0.52",
|
|
61
78
|
"@ag-ui/core": "^0.0.52",
|
|
62
|
-
"@assistant-ui/react": "^0.
|
|
79
|
+
"@assistant-ui/react": "^0.15.1",
|
|
63
80
|
"@azure/msal-browser": "^5.0.0",
|
|
64
81
|
"@azure/msal-react": "^5.0.0",
|
|
65
82
|
"@types/react": "^19.2.2",
|