@iloveagents/foundry-web-ui 0.4.0 → 0.5.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/dist/components/assistant-chat.js +5 -4
- package/dist/components/chat-bubble.js +4 -2
- package/dist/components/loading-indicator.d.ts +10 -1
- package/dist/components/loading-indicator.js +16 -3
- package/dist/components/message-error.d.ts +10 -0
- package/dist/components/message-error.js +10 -0
- package/dist/components/message-timestamp.d.ts +10 -0
- package/dist/components/message-timestamp.js +18 -0
- package/dist/lib/ag-ui-adapter.js +9 -0
- package/package.json +3 -3
|
@@ -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, useAuiState, useComposerRuntime, 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";
|
|
@@ -13,18 +13,19 @@ import { useChatBubbleStore } from "../lib/chat-bubble-store.js";
|
|
|
13
13
|
import { MarkdownText } from "./markdown-text.js";
|
|
14
14
|
import { ToolFallback } from "./tool-fallback.js";
|
|
15
15
|
import { LoadingIndicator, LoadingBar } from "./loading-indicator.js";
|
|
16
|
+
import { MessageTimestamp } from "./message-timestamp.js";
|
|
17
|
+
import { MessageError } from "./message-error.js";
|
|
16
18
|
const BranchPicker = () => (_jsxs(BranchPickerPrimitive.Root, { hideWhenSingleBranch: true, className: "inline-flex items-center gap-1 text-xs", children: [_jsx(BranchPickerPrimitive.Previous, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Previous", size: "icon", children: _jsx(ChevronLeft, { className: "size-4" }) }) }), _jsxs("span", { className: "text-muted-foreground", children: [_jsx(BranchPickerPrimitive.Number, {}), " / ", _jsx(BranchPickerPrimitive.Count, {})] }), _jsx(BranchPickerPrimitive.Next, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Next", size: "icon", children: _jsx(ChevronRight, { className: "size-4" }) }) })] }));
|
|
17
19
|
const UserMessage = () => {
|
|
18
20
|
const messageId = useAuiState((s) => s.message.id);
|
|
19
|
-
return (_jsx(MessagePrimitive.Root, { className: "w-full mb-2 group", children: _jsxs("div", { className: "flex flex-col items-end gap-1", children: [_jsx(MessagePrimitive.Attachments, { components: userAttachmentComponents }), _jsx(SentContextBadges, { messageId: messageId }), _jsx("div", { className: cn("max-w-[80%]", "bg-muted rounded-2xl", "px-4 py-3", "text-foreground"), children: _jsx(MessagePrimitive.Content, {}) }), _jsxs("div", { className: "flex h-8 items-center justify-end gap-2 pr-2", children: [_jsx(BranchPicker, {}), _jsx("div", { className: "flex h-8 w-10 items-center justify-end", children: _jsx(UserActionBar, {}) })] })] }) }));
|
|
21
|
+
return (_jsx(MessagePrimitive.Root, { className: "w-full mb-2 group", children: _jsxs("div", { className: "flex flex-col items-end gap-1", children: [_jsx(MessagePrimitive.Attachments, { components: userAttachmentComponents }), _jsx(SentContextBadges, { messageId: messageId }), _jsx("div", { className: cn("max-w-[80%]", "bg-muted rounded-2xl", "px-4 py-3", "text-foreground"), children: _jsx(MessagePrimitive.Content, {}) }), _jsxs("div", { className: "flex h-8 items-center justify-end gap-2 pr-2", children: [_jsx(MessageTimestamp, {}), _jsx(BranchPicker, {}), _jsx("div", { className: "flex h-8 w-10 items-center justify-end", children: _jsx(UserActionBar, {}) })] })] }) }));
|
|
20
22
|
};
|
|
21
23
|
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" }) }) }) }));
|
|
22
24
|
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: {
|
|
23
25
|
Text: MarkdownText,
|
|
24
26
|
tools: { Fallback: ToolFallback },
|
|
25
|
-
} }) }), _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, {})] })] }) }));
|
|
27
|
+
} }) }), _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, {})] })] }) }));
|
|
26
28
|
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" }) }) })] }));
|
|
27
|
-
const MessageError = () => (_jsx(MessagePrimitive.Error, { children: _jsx(ErrorPrimitive.Root, { className: "mt-2 rounded-lg border border-destructive bg-destructive/10 p-3", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium text-destructive", children: "Error:" }), _jsx(ErrorPrimitive.Message, { className: "text-sm text-destructive" })] }) }) }));
|
|
28
29
|
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" }) })] })] }) }) }) }));
|
|
29
30
|
const ComposerActionButton = () => {
|
|
30
31
|
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" }) }) }) })] }));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useRef, useState } from "react";
|
|
3
3
|
import { ThreadPrimitive, ComposerPrimitive, MessagePrimitive, ActionBarPrimitive, useAuiState, useThreadViewport, } from "@assistant-ui/react";
|
|
4
|
-
import { MessageSquare, X, Maximize2, SquarePen, ArrowUp, Square, ChevronDown, Copy, Check, Paperclip, } from "lucide-react";
|
|
4
|
+
import { MessageSquare, X, Maximize2, SquarePen, ArrowUp, Square, ChevronDown, Copy, Check, Paperclip, RefreshCw, } from "lucide-react";
|
|
5
5
|
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
6
6
|
import { Button } from "@iloveagents/foundry-web-primitives";
|
|
7
7
|
import { TooltipIconButton } from "./tooltip-icon-button.js";
|
|
@@ -10,6 +10,8 @@ import { ComposerContextBadges, SentContextBadges } from "./context-badges.js";
|
|
|
10
10
|
import { ComposerSubmitBridge } from "./composer-submit-bridge.js";
|
|
11
11
|
import { MarkdownText } from "./markdown-text.js";
|
|
12
12
|
import { LoadingIndicator } from "./loading-indicator.js";
|
|
13
|
+
import { MessageTimestamp } from "./message-timestamp.js";
|
|
14
|
+
import { MessageError } from "./message-error.js";
|
|
13
15
|
import { useAppStore } from "../lib/app-store.js";
|
|
14
16
|
import { useChatBubbleStore } from "../lib/chat-bubble-store.js";
|
|
15
17
|
import { useNewConversation } from "../lib/use-new-conversation.js";
|
|
@@ -18,7 +20,7 @@ const CHAT_BUBBLE_INSET = 8;
|
|
|
18
20
|
function BubbleAssistantMessage() {
|
|
19
21
|
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: {
|
|
20
22
|
Text: MarkdownText,
|
|
21
|
-
} }) }), _jsx("div", { className: "flex h-7
|
|
23
|
+
} }) }), _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, {})] })] }));
|
|
22
24
|
}
|
|
23
25
|
function BubbleUserMessage() {
|
|
24
26
|
const messageId = useAuiState((s) => s.message.id);
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
/** Shimmer bar shown above the composer while the agent is running. */
|
|
2
2
|
export declare function LoadingBar(): import("react/jsx-runtime").JSX.Element;
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Inline status shown in the message area while the agent is running.
|
|
5
|
+
*
|
|
6
|
+
* Visible for the whole run (assistant-ui `running` gate) with the current
|
|
7
|
+
* phase, plus a distinct amber "stalled" treatment when no event or server
|
|
8
|
+
* heartbeat has arrived for a while — so a user can always tell "still
|
|
9
|
+
* working" from "connection is dead". A dead stream is additionally
|
|
10
|
+
* surfaced as an explicit run error by the runner; this indicator covers
|
|
11
|
+
* the in-flight phases.
|
|
12
|
+
*/
|
|
4
13
|
export declare function LoadingIndicator(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -8,15 +8,28 @@ function formatToolName(name) {
|
|
|
8
8
|
export function LoadingBar() {
|
|
9
9
|
return (_jsx("div", { className: "w-full h-px bg-primary/30 overflow-hidden", children: _jsx("div", { className: "h-full w-1/4 bg-primary animate-[shimmer_1.5s_ease-in-out_infinite]" }) }));
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Inline status shown in the message area while the agent is running.
|
|
13
|
+
*
|
|
14
|
+
* Visible for the whole run (assistant-ui `running` gate) with the current
|
|
15
|
+
* phase, plus a distinct amber "stalled" treatment when no event or server
|
|
16
|
+
* heartbeat has arrived for a while — so a user can always tell "still
|
|
17
|
+
* working" from "connection is dead". A dead stream is additionally
|
|
18
|
+
* surfaced as an explicit run error by the runner; this indicator covers
|
|
19
|
+
* the in-flight phases.
|
|
20
|
+
*/
|
|
12
21
|
export function LoadingIndicator() {
|
|
13
22
|
const streaming = useStore(streamingStatusStore, (s) => s.streamingStatus);
|
|
23
|
+
const stalled = streaming.status === "stalled";
|
|
14
24
|
let label = "Thinking";
|
|
15
|
-
if (
|
|
25
|
+
if (stalled) {
|
|
26
|
+
label = "Still working — no signal from the agent";
|
|
27
|
+
}
|
|
28
|
+
else if (streaming.status === "calling" && streaming.toolName) {
|
|
16
29
|
label = `Running ${formatToolName(streaming.toolName)}`;
|
|
17
30
|
}
|
|
18
31
|
else if (streaming.status === "streaming") {
|
|
19
32
|
label = "Generating response";
|
|
20
33
|
}
|
|
21
|
-
return (_jsx("div", { className: "h-10 px-4", children: _jsxs("div", { className: "flex items-center gap-2 py-2 text-foreground leading-relaxed animate-in fade-in duration-300", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: label }), _jsxs("div", { className: "flex gap-1", children: [_jsx("span", { className:
|
|
34
|
+
return (_jsx("div", { className: "h-10 px-4", children: _jsxs("div", { className: "flex items-center gap-2 py-2 text-foreground leading-relaxed animate-in fade-in duration-300", children: [_jsx("span", { className: stalled ? "text-sm text-warning" : "text-sm text-muted-foreground", children: label }), _jsxs("div", { className: "flex gap-1", children: [_jsx("span", { className: `size-1.5 rounded-full ${stalled ? "bg-warning/70" : "bg-primary/60"} animate-[pulse_1.4s_ease-in-out_infinite]` }), _jsx("span", { className: `size-1.5 rounded-full ${stalled ? "bg-warning/70" : "bg-primary/60"} animate-[pulse_1.4s_ease-in-out_0.2s_infinite]` }), _jsx("span", { className: `size-1.5 rounded-full ${stalled ? "bg-warning/70" : "bg-primary/60"} animate-[pulse_1.4s_ease-in-out_0.4s_infinite]` })] })] }) }));
|
|
22
35
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FC } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Visible, styled run-error for an assistant message — rendered whenever the
|
|
4
|
+
* message settled with an error status (agent RUN_ERROR, lost connection,
|
|
5
|
+
* transport failure). Shared by the full-page thread and the compact chat
|
|
6
|
+
* bubble so a failed run is NEVER silent in either surface.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MessageError: FC<{
|
|
9
|
+
compact?: boolean;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MessagePrimitive, ErrorPrimitive } from "@assistant-ui/react";
|
|
3
|
+
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
4
|
+
/**
|
|
5
|
+
* Visible, styled run-error for an assistant message — rendered whenever the
|
|
6
|
+
* message settled with an error status (agent RUN_ERROR, lost connection,
|
|
7
|
+
* transport failure). Shared by the full-page thread and the compact chat
|
|
8
|
+
* bubble so a failed run is NEVER silent in either surface.
|
|
9
|
+
*/
|
|
10
|
+
export const MessageError = ({ compact }) => (_jsx(MessagePrimitive.Error, { children: _jsx(ErrorPrimitive.Root, { className: cn("mt-2 rounded-lg border border-destructive bg-destructive/10", compact ? "p-2" : "p-3"), children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: cn("font-medium text-destructive", compact ? "text-xs" : "text-sm"), children: "Error:" }), _jsx(ErrorPrimitive.Message, { className: cn("text-destructive", compact ? "text-xs" : "text-sm") })] }) }) }));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FC } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Message send/receive time — revealed on hover in the same row as the
|
|
4
|
+
* message actions (edit / copy). The short time is shown; the full date
|
|
5
|
+
* lives in the native tooltip. Shared by the full-page thread and the
|
|
6
|
+
* compact chat bubble.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MessageTimestamp: FC<{
|
|
9
|
+
className?: string;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useAuiState } from "@assistant-ui/react";
|
|
3
|
+
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
4
|
+
/**
|
|
5
|
+
* Message send/receive time — revealed on hover in the same row as the
|
|
6
|
+
* message actions (edit / copy). The short time is shown; the full date
|
|
7
|
+
* lives in the native tooltip. Shared by the full-page thread and the
|
|
8
|
+
* compact chat bubble.
|
|
9
|
+
*/
|
|
10
|
+
export const MessageTimestamp = ({ className }) => {
|
|
11
|
+
const createdAt = useAuiState((s) => s.message.createdAt);
|
|
12
|
+
if (!createdAt)
|
|
13
|
+
return null;
|
|
14
|
+
const d = createdAt instanceof Date ? createdAt : new Date(createdAt);
|
|
15
|
+
if (Number.isNaN(d.getTime()))
|
|
16
|
+
return null;
|
|
17
|
+
return (_jsx("span", { title: d.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }), className: cn("text-xs text-muted-foreground/70 tabular-nums select-none", "opacity-0 transition-opacity group-hover:opacity-100", className), children: d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" }) }));
|
|
18
|
+
};
|
|
@@ -48,6 +48,10 @@ export class AGUIAdapterSDK {
|
|
|
48
48
|
let currentText = "";
|
|
49
49
|
const toolCalls = new Map();
|
|
50
50
|
let runError = null;
|
|
51
|
+
// Liveness clock for the working indicator: every runner event (incl.
|
|
52
|
+
// server heartbeats) proves the run is alive; the indicator derives
|
|
53
|
+
// elapsed time from runStartedAt and staleness from lastSignalAt.
|
|
54
|
+
streamingStatusStore.getState().markRunStarted();
|
|
51
55
|
/**
|
|
52
56
|
* Build a `ChatModelRunResult` from the accumulated state.
|
|
53
57
|
*
|
|
@@ -103,7 +107,11 @@ export class AGUIAdapterSDK {
|
|
|
103
107
|
abortSignal,
|
|
104
108
|
})) {
|
|
105
109
|
let shouldYield = false;
|
|
110
|
+
streamingStatusStore.getState().touchSignal();
|
|
106
111
|
switch (event.type) {
|
|
112
|
+
case "heartbeat":
|
|
113
|
+
// Server liveness ping — already recorded via touchSignal above.
|
|
114
|
+
break;
|
|
107
115
|
case "request-sent":
|
|
108
116
|
if (import.meta.env.DEV) {
|
|
109
117
|
useDevStore.getState().captureRequest(event.input);
|
|
@@ -226,6 +234,7 @@ export class AGUIAdapterSDK {
|
|
|
226
234
|
}
|
|
227
235
|
finally {
|
|
228
236
|
streamingStatusStore.getState().setStreamingStatus({ status: "idle" });
|
|
237
|
+
streamingStatusStore.getState().markRunEnded();
|
|
229
238
|
}
|
|
230
239
|
}
|
|
231
240
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iloveagents/foundry-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"tailwind-merge": "^3.5.0",
|
|
54
54
|
"react-markdown": "^10.0.0",
|
|
55
55
|
"remark-gfm": "^4.0.0",
|
|
56
|
-
"@iloveagents/foundry-agent": "^0.
|
|
57
|
-
"@iloveagents/foundry-web-primitives": "^0.
|
|
56
|
+
"@iloveagents/foundry-agent": "^0.5.0",
|
|
57
|
+
"@iloveagents/foundry-web-primitives": "^0.5.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@ag-ui/client": "^0.0.52",
|