@iloveagents/foundry-web-ui 0.26.0 → 0.27.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.d.ts +12 -1
- package/dist/components/assistant-chat.js +18 -13
- package/dist/components/chat-bubble.js +9 -6
- package/dist/components/chat-context.d.ts +2 -0
- package/dist/components/chat-context.js +70 -0
- package/dist/components/chat-slots.d.ts +10 -0
- package/dist/components/chat-slots.js +7 -0
- package/dist/components/composer-add-menu.js +3 -2
- package/dist/components/composer-input.d.ts +4 -0
- package/dist/components/composer-input.js +23 -0
- package/dist/components/context-badges.d.ts +4 -1
- package/dist/components/context-badges.js +7 -4
- package/dist/components/context-bar.d.ts +1 -0
- package/dist/components/context-bar.js +11 -3
- package/dist/components/data-table/data-table-frame.d.ts +1 -22
- package/dist/components/data-table/data-table-frame.js +21 -48
- package/dist/components/data-table/data-table-overflow.d.ts +5 -0
- package/dist/components/data-table/data-table-overflow.js +42 -0
- package/dist/components/data-table/data-table.js +88 -87
- package/dist/components/data-table/focus-pane-resize.d.ts +8 -0
- package/dist/components/data-table/focus-pane-resize.js +88 -0
- package/dist/components/focus-chat-pane.d.ts +8 -0
- package/dist/components/focus-chat-pane.js +9 -0
- package/dist/components/focus-dialog.d.ts +8 -0
- package/dist/components/focus-dialog.js +103 -0
- package/dist/components/focus-source-pane.d.ts +7 -0
- package/dist/components/focus-source-pane.js +15 -0
- package/dist/components/loading-indicator.js +1 -1
- package/dist/components/message-timestamp.js +3 -2
- package/dist/components/pin-state-icon.d.ts +5 -0
- package/dist/components/pin-state-icon.js +7 -0
- package/dist/components/pin-toggle.d.ts +7 -0
- package/dist/components/pin-toggle.js +10 -0
- package/dist/components/sidebar.d.ts +18 -1
- package/dist/components/sidebar.js +124 -74
- package/dist/components/tool-panel-layout.d.ts +3 -1
- package/dist/components/tool-panel-layout.js +5 -5
- package/dist/components/tool-panel.d.ts +8 -1
- package/dist/components/tool-panel.js +12 -8
- package/dist/components/user-menu.js +19 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/app-store.d.ts +0 -8
- package/dist/lib/app-store.js +14 -4
- package/dist/lib/chat-context-summary.d.ts +11 -0
- package/dist/lib/chat-context-summary.js +37 -0
- package/dist/lib/chat-lifecycle-store.d.ts +2 -0
- package/dist/lib/chat-lifecycle-store.js +6 -1
- package/dist/lib/client-tools.js +7 -0
- package/dist/lib/create-ui-id.d.ts +2 -0
- package/dist/lib/create-ui-id.js +9 -0
- package/dist/lib/focus-context.d.ts +2 -0
- package/dist/lib/focus-context.js +78 -0
- package/dist/lib/nav-config.d.ts +7 -0
- package/dist/lib/tool-panel-store.d.ts +7 -6
- package/dist/lib/tool-panel-store.js +29 -3
- package/dist/lib/use-new-conversation.js +1 -1
- package/dist/workbench/chat-header.d.ts +14 -0
- package/dist/workbench/chat-header.js +12 -0
- package/dist/workbench/chat-resize-handle.d.ts +4 -0
- package/dist/workbench/chat-resize-handle.js +40 -0
- package/dist/workbench/content-pane.d.ts +14 -0
- package/dist/workbench/content-pane.js +14 -0
- package/dist/workbench/conversation-history.d.ts +11 -0
- package/dist/workbench/conversation-history.js +83 -0
- package/dist/workbench/navigation.d.ts +6 -0
- package/dist/workbench/navigation.js +6 -0
- package/dist/workbench/selection-bridge.d.ts +5 -0
- package/dist/workbench/selection-bridge.js +84 -0
- package/dist/workbench/use-chat-width.d.ts +14 -0
- package/dist/workbench/use-chat-width.js +56 -0
- package/dist/workbench/use-workbench-state.d.ts +50 -0
- package/dist/workbench/use-workbench-state.js +145 -0
- package/dist/workbench/welcome-intro.d.ts +6 -0
- package/dist/workbench/welcome-intro.js +10 -0
- package/dist/workbench/workbench-styles.d.ts +1 -0
- package/dist/workbench/workbench-styles.js +290 -0
- package/dist/workbench/workbench.d.ts +5 -0
- package/dist/workbench/workbench.js +32 -0
- package/dist/workbench.d.ts +2 -0
- package/dist/workbench.js +2 -0
- package/package.json +8 -4
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
1
2
|
/**
|
|
2
3
|
* Default starter prompts shown on empty state. Override per-app by passing
|
|
3
4
|
* a `starterSuggestions` prop to `<ChatContent>`. An empty array (`[]`)
|
|
@@ -5,6 +6,16 @@
|
|
|
5
6
|
*/
|
|
6
7
|
export declare const DEFAULT_STARTER_SUGGESTIONS: string[];
|
|
7
8
|
export interface ChatContentProps {
|
|
9
|
+
centerComposer?: boolean;
|
|
10
|
+
composerIntro?: ReactNode;
|
|
11
|
+
composerFooter?: ReactNode;
|
|
12
|
+
/** Compact two-row composer with an auto-growing input. */
|
|
13
|
+
compactComposer?: boolean;
|
|
14
|
+
hiddenPageLabels?: string[];
|
|
15
|
+
/** Hide the already-visible navigation page badge, without removing context. */
|
|
16
|
+
hideCurrentPageBadge?: boolean;
|
|
17
|
+
/** Optional empty conversation presentation; leaves the composer and runtime unchanged. */
|
|
18
|
+
emptyState?: ReactNode;
|
|
8
19
|
/**
|
|
9
20
|
* Starter prompts shown when the thread is empty. Defaults to
|
|
10
21
|
* `DEFAULT_STARTER_SUGGESTIONS`. Pass an empty array to hide the
|
|
@@ -38,4 +49,4 @@ export declare const DEFAULT_COMPOSER_PLACEHOLDER = "Ask anything";
|
|
|
38
49
|
* Reusable chat content — used by ChatPage and ChatBubble.
|
|
39
50
|
* Renders inside a ThreadPrimitive.Root context.
|
|
40
51
|
*/
|
|
41
|
-
export declare function ChatContent({ starterSuggestions, showPinButton, placeholder, }?: ChatContentProps): import("react/jsx-runtime").JSX.Element;
|
|
52
|
+
export declare function ChatContent({ centerComposer, composerIntro, composerFooter, compactComposer, hiddenPageLabels, hideCurrentPageBadge, emptyState, starterSuggestions, showPinButton, placeholder, }?: ChatContentProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { ComposerInput } from "./composer-input.js";
|
|
2
3
|
import { useState, useRef, useCallback, useEffect } from "react";
|
|
3
4
|
import { ThreadPrimitive, ComposerPrimitive, MessagePrimitive, ActionBarPrimitive, BranchPickerPrimitive, useAui, useAuiState, useThreadViewport, } from "@assistant-ui/react";
|
|
4
5
|
import { ArrowUp, Copy, Check, RefreshCw, PencilIcon, ChevronLeft, ChevronRight, ChevronDown, Square, Bot, Minimize2, PanelRightClose, PanelRightOpen, } from "lucide-react";
|
|
@@ -9,6 +10,7 @@ import { userAttachmentComponents, composerAttachmentComponents } from "./chat-a
|
|
|
9
10
|
import { SentContextBadges, ComposerContextBadges } from "./context-badges.js";
|
|
10
11
|
import { ComposerAddMenu } from "./composer-add-menu.js";
|
|
11
12
|
import { ContextPins } from "./context-bar.js";
|
|
13
|
+
import { ChatContext } from "./chat-context.js";
|
|
12
14
|
import { ComposerSubmitBridge } from "./composer-submit-bridge.js";
|
|
13
15
|
import { ChatComposerActionsSlot, ChatComposerBannerSlot, useChatThreadSurface, useComposerActionsOwnSend, } from "./chat-slots.js";
|
|
14
16
|
import { useChatBubbleStore } from "../lib/chat-bubble-store.js";
|
|
@@ -20,20 +22,21 @@ import { MessageError } from "./message-error.js";
|
|
|
20
22
|
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" }) }) })] }));
|
|
21
23
|
const UserMessage = () => {
|
|
22
24
|
const messageId = useAuiState((s) => s.message.id);
|
|
23
|
-
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-
|
|
25
|
+
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-sm leading-relaxed text-foreground"), children: _jsx(MessagePrimitive.Content, {}) }), _jsxs("div", { className: "flex h-6 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(ThreadPrimitive.If, { running: false, children: _jsx(UserActionBar, {}) }) })] })] }) }));
|
|
24
26
|
};
|
|
25
27
|
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" }) }) }) }));
|
|
26
28
|
const AssistantMessage = () => {
|
|
29
|
+
const running = useAuiState((s) => s.message.status?.type === "running");
|
|
27
30
|
// Until the message has parts there is nothing to copy, retry or branch.
|
|
28
31
|
// `AssistantActionBar` already hides its BUTTONS while running, but the
|
|
29
32
|
// row reserving their height did not — so an empty assistant message put
|
|
30
33
|
// a 36px hole between the question and the "Thinking" line, which read as
|
|
31
34
|
// the indicator floating far away from the message it belongs to.
|
|
32
35
|
const hasParts = useAuiState((s) => (s.message.parts?.length ?? 0) > 0);
|
|
33
|
-
return (_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-
|
|
36
|
+
return (_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-sm text-foreground leading-relaxed flex flex-col gap-3", children: _jsx(ChatMessageParts, {}) }), _jsx(MessageError, {}), hasParts && !running && (_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, {})] }))] }) }));
|
|
34
37
|
};
|
|
35
38
|
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" }) }) })] }));
|
|
36
|
-
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(
|
|
39
|
+
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(ComposerInput, { autoFocus: true, className: cn("w-full bg-transparent", "text-base sm:text-sm 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" }) })] })] }) }) }) }));
|
|
37
40
|
const ComposerActionButton = () => {
|
|
38
41
|
// With nothing typed, a module-supplied control (a voice button) may hold
|
|
39
42
|
// the send corner instead of a Send that has nothing to send.
|
|
@@ -57,7 +60,7 @@ const ScrollToBottomButton = () => {
|
|
|
57
60
|
return null;
|
|
58
61
|
return (_jsx(Button, { variant: "outline", size: "icon", className: "absolute bottom-4 left-1/2 -translate-x-1/2 z-10 rounded-full shadow-md", onClick: () => scrollToBottom({ behavior: "smooth" }), children: _jsx(ChevronDown, { className: "size-4" }) }));
|
|
59
62
|
};
|
|
60
|
-
const DropZone = ({ children }) => {
|
|
63
|
+
const DropZone = ({ children, centered, }) => {
|
|
61
64
|
const [isDragging, setIsDragging] = useState(false);
|
|
62
65
|
const [dropError, setDropError] = useState(null);
|
|
63
66
|
const dropErrorTimer = useRef(null);
|
|
@@ -113,7 +116,7 @@ const DropZone = ({ children }) => {
|
|
|
113
116
|
: `Couldn't attach ${rejected.length} files (${rejected.join("; ")})`);
|
|
114
117
|
}
|
|
115
118
|
}, [aui, showDropError]);
|
|
116
|
-
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 }))] }));
|
|
119
|
+
return (_jsxs("div", { className: cn("relative flex flex-col flex-1", centered ? "justify-center overflow-y-auto" : "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 }))] }));
|
|
117
120
|
};
|
|
118
121
|
/**
|
|
119
122
|
* "Message Assistant..." named the machine and told the user nothing. The
|
|
@@ -125,23 +128,25 @@ export const DEFAULT_COMPOSER_PLACEHOLDER = "Ask anything";
|
|
|
125
128
|
* Reusable chat content — used by ChatPage and ChatBubble.
|
|
126
129
|
* Renders inside a ThreadPrimitive.Root context.
|
|
127
130
|
*/
|
|
128
|
-
export function ChatContent({ starterSuggestions = DEFAULT_STARTER_SUGGESTIONS, showPinButton = true, placeholder = DEFAULT_COMPOSER_PLACEHOLDER, } = {}) {
|
|
131
|
+
export function ChatContent({ centerComposer = false, composerIntro, composerFooter, compactComposer = false, hiddenPageLabels = [], hideCurrentPageBadge = false, emptyState, starterSuggestions = DEFAULT_STARTER_SUGGESTIONS, showPinButton = true, placeholder = DEFAULT_COMPOSER_PLACEHOLDER, } = {}) {
|
|
129
132
|
const ThreadSurface = useChatThreadSurface();
|
|
130
133
|
const isExpanded = useChatBubbleStore((s) => s.isExpanded);
|
|
131
134
|
const collapseChat = useChatBubbleStore((s) => s.collapse);
|
|
132
135
|
const showPagePanel = useChatBubbleStore((s) => s.showPagePanel);
|
|
133
136
|
const togglePagePanel = useChatBubbleStore((s) => s.togglePagePanel);
|
|
134
|
-
return (_jsxs(ThreadPrimitive.Root, { className: cn("flex flex-col flex-1", "bg-background text-foreground", "overflow-hidden"), children: [isExpanded && (_jsxs("div", { className: "shrink-0 flex items-center justify-between px-4 py-2 border-b border-border", children: [_jsxs("span", { className: "text-sm font-medium text-foreground flex items-center gap-2", children: [_jsx(Bot, { className: "size-4 text-primary" }), "Chat"] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(TooltipIconButton, { tooltip: showPagePanel ? "Hide page panel" : "Show page panel", size: "icon", onClick: togglePagePanel, className: "hidden xl:inline-flex", children: showPagePanel ? (_jsx(PanelRightClose, { className: "size-4" })) : (_jsx(PanelRightOpen, { className: "size-4" })) }), _jsx(TooltipIconButton, { tooltip: "Minimize to bubble", size: "icon", onClick: collapseChat, children: _jsx(Minimize2, { className: "size-4" }) })] })] })), _jsxs(DropZone, { children: [_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [ThreadSurface && (
|
|
137
|
+
return (_jsxs(ThreadPrimitive.Root, { className: cn("flex flex-col flex-1", "bg-background text-foreground", "overflow-hidden"), children: [isExpanded && (_jsxs("div", { className: "shrink-0 flex items-center justify-between px-4 py-2 border-b border-border", children: [_jsxs("span", { className: "text-sm font-medium text-foreground flex items-center gap-2", children: [_jsx(Bot, { className: "size-4 text-primary" }), "Chat"] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(TooltipIconButton, { tooltip: showPagePanel ? "Hide page panel" : "Show page panel", size: "icon", onClick: togglePagePanel, className: "hidden xl:inline-flex", children: showPagePanel ? (_jsx(PanelRightClose, { className: "size-4" })) : (_jsx(PanelRightOpen, { className: "size-4" })) }), _jsx(TooltipIconButton, { tooltip: "Minimize to bubble", size: "icon", onClick: collapseChat, children: _jsx(Minimize2, { className: "size-4" }) })] })] })), _jsxs(DropZone, { centered: centerComposer, children: [_jsxs("div", { className: "relative flex-1 overflow-hidden", style: centerComposer && !ThreadSurface ? { display: "none" } : undefined, children: [ThreadSurface && (
|
|
135
138
|
// `overflow-hidden`, not scroll: a surface owns its own layout,
|
|
136
139
|
// and a full-bleed one (an avatar cropped to the frame) must be
|
|
137
140
|
// able to reach the edges rather than sit in a scroll box.
|
|
138
|
-
_jsx("div", { className: "absolute inset-0 z-10 overflow-hidden bg-background", children: _jsx(ThreadSurface, {}) })), _jsxs(ThreadPrimitive.Viewport, { className: cn("absolute inset-0 overflow-y-auto bg-background", ThreadSurface && "invisible"), "aria-hidden": ThreadSurface ? true : undefined, children: [_jsx(ThreadPrimitive.Empty, { children: _jsxs("div", { className: cn("flex min-h-full flex-col", "items-center justify-center", "px-4"), children: [_jsx(Bot, { className: "size-16 mb-6 text-primary/70" }), _jsx("h1", { className: "text-[2rem] font-normal text-foreground text-center mb-6", children: "How can I help you today?" }), starterSuggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-2 max-w-lg", children: starterSuggestions.map((prompt) => (_jsx(ThreadPrimitive.Suggestion, { prompt: prompt, send: true, asChild: true, children: _jsx(Button, { variant: "outline", className: "text-sm", children: prompt }) }, prompt))) }))] }) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "mx-auto max-w-3xl px-4 pt-8 pb-4", children: [_jsx(ThreadPrimitive.Messages, { components: {
|
|
141
|
+
_jsx("div", { className: "absolute inset-0 z-10 overflow-hidden bg-background", children: _jsx(ThreadSurface, {}) })), _jsxs(ThreadPrimitive.Viewport, { className: cn("absolute inset-0 overflow-y-auto bg-background", ThreadSurface && "invisible"), "aria-hidden": ThreadSurface ? true : undefined, children: [_jsx(ThreadPrimitive.Empty, { children: emptyState ?? (_jsxs("div", { className: cn("flex min-h-full flex-col", "items-center justify-center", "px-4"), children: [_jsx(Bot, { className: "size-16 mb-6 text-primary/70" }), _jsx("h1", { className: "text-[2rem] font-normal text-foreground text-center mb-6", children: "How can I help you today?" }), starterSuggestions.length > 0 && (_jsx("div", { className: "flex flex-wrap justify-center gap-2 max-w-lg", children: starterSuggestions.map((prompt) => (_jsx(ThreadPrimitive.Suggestion, { prompt: prompt, send: true, asChild: true, children: _jsx(Button, { variant: "outline", className: "text-sm", children: prompt }) }, prompt))) }))] })) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "mx-auto max-w-3xl px-4 pt-8 pb-4", children: [_jsx(ThreadPrimitive.Messages, { components: {
|
|
139
142
|
UserMessage,
|
|
140
143
|
EditComposer,
|
|
141
144
|
AssistantMessage,
|
|
142
|
-
} }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingIndicator, {}) })] }) })] }), !ThreadSurface && (_jsx(ThreadPrimitive.If, { empty: false, children: _jsx(ScrollToBottomButton, {}) }))] }), _jsxs("div", { className: "shrink-0 bg-background", children: [_jsx(ChatComposerBannerSlot, {}), _jsx(ThreadPrimitive.If, { running: false, children: _jsx("div", { className: "border-t border-border" }) }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingBar, {}) }),
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
} }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingIndicator, {}) })] }) })] }), !ThreadSurface && (_jsx(ThreadPrimitive.If, { empty: false, children: _jsx(ScrollToBottomButton, {}) }))] }), _jsxs("div", { className: "shrink-0 bg-background", children: [composerIntro && _jsx("div", { className: "mx-auto max-w-3xl px-4 pt-4", children: composerIntro }), _jsx(ChatComposerBannerSlot, {}), _jsx(ThreadPrimitive.If, { running: false, children: _jsx("div", { "data-chat-composer-divider": true, className: "border-t border-border" }) }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingBar, {}) }), _jsxs("div", { className: "mx-auto max-w-3xl px-4 py-4", children: [_jsx(ChatContext, {}), _jsxs(ComposerPrimitive.Root, { style: compactComposer ? { borderRadius: 16 } : undefined, className: cn(
|
|
146
|
+
// Query container for the controls inside, so they size
|
|
147
|
+
// against the composer rather than the viewport. This one
|
|
148
|
+
// is wide, so the effort picker keeps its full label.
|
|
149
|
+
"@container", "flex flex-col gap-2", "rounded-3xl border border-input", "bg-background shadow-sm", "px-4 py-2", "focus-within:border-primary/50 focus-within:shadow-md", "transition-all duration-300"), children: [_jsx(ComposerSubmitBridge, {}), _jsx(ComposerContextBadges, { hideCurrentPage: hideCurrentPageBadge, hiddenPageLabels: hiddenPageLabels }), _jsx(ComposerPrimitive.Attachments, { components: composerAttachmentComponents }), _jsxs("div", { className: cn("flex items-center gap-2", compactComposer ? "flex-wrap" : "flex-wrap @md:flex-nowrap"), children: [_jsxs("div", { className: cn("flex shrink-0 items-center gap-1", compactComposer ? "order-2" : "order-2 @md:order-1"), children: [_jsx(ComposerAddMenu, { showPin: showPinButton }), _jsx(ContextPins, { allowPageToggle: showPinButton })] }), _jsx(ComposerInput, { autoFocus: true, placeholder: placeholder, rows: 1, className: cn("bg-transparent", compactComposer
|
|
150
|
+
? "order-1 w-full"
|
|
151
|
+
: "order-1 w-full @md:order-2 @md:w-auto @md:flex-1", "py-2 text-base sm:text-sm leading-relaxed", "text-foreground outline-none", "placeholder:text-muted-foreground", "resize-none max-h-32") }), _jsx("div", { className: cn("order-3 ml-auto flex shrink-0 items-center gap-2", !compactComposer && "@md:ml-0"), children: _jsx(ComposerActionButton, {}) })] })] }), composerFooter] })] })] })] }));
|
|
147
152
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { ChatContext } from "./chat-context.js";
|
|
3
|
+
import { ComposerInput } from "./composer-input.js";
|
|
2
4
|
import { useRef, useState } from "react";
|
|
3
5
|
import { ThreadPrimitive, ComposerPrimitive, MessagePrimitive, ActionBarPrimitive, useAuiState, useThreadViewport, } from "@assistant-ui/react";
|
|
4
6
|
import { Sparkles, X, Maximize2, SquarePen, ArrowUp, Square, ChevronDown, Copy, Check, RefreshCw, } from "lucide-react";
|
|
@@ -22,7 +24,8 @@ import { useNewConversation } from "../lib/use-new-conversation.js";
|
|
|
22
24
|
import { RAIL_WIDTH, useSidebarStore } from "../lib/sidebar-store.js";
|
|
23
25
|
const CHAT_BUBBLE_INSET = 8;
|
|
24
26
|
function BubbleAssistantMessage() {
|
|
25
|
-
|
|
27
|
+
const running = useAuiState((s) => s.message.status?.type === "running");
|
|
28
|
+
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(ChatMessageParts, {}) }), _jsx(MessageError, { compact: true }), !running && (_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, {})] }))] }));
|
|
26
29
|
}
|
|
27
30
|
function BubbleUserMessage() {
|
|
28
31
|
const messageId = useAuiState((s) => s.message.id);
|
|
@@ -106,9 +109,9 @@ export function ChatBubble() {
|
|
|
106
109
|
: { left: CHAT_BUBBLE_INSET, bottom: CHAT_BUBBLE_INSET }, children: [_jsxs("div", { className: cn("flex items-center justify-between px-4 py-2.5 border-b border-border shrink-0", "cursor-grab active:cursor-grabbing select-none"), style: { touchAction: "none" }, onPointerDown: onHeaderPointerDown, onPointerMove: onHeaderPointerMove, onPointerUp: onHeaderPointerUp, onLostPointerCapture: onHeaderLostPointerCapture, children: [_jsxs("div", { className: "flex flex-col min-w-0", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-sm", children: [_jsx(Sparkles, { className: "size-3.5 text-primary shrink-0" }), _jsx("span", { className: "font-semibold text-foreground truncate", children: pageLabel })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: threadActive ? "Active thread" : "New conversation" })] }), _jsxs("div", { className: "flex items-center gap-0.5", children: [_jsx(TooltipIconButton, { tooltip: "New Thread", size: "icon", className: "size-7", onClick: startNewThread, children: _jsx(SquarePen, { className: "size-3.5" }) }), _jsx(TooltipIconButton, { tooltip: "Expand chat", size: "icon", className: "size-7", onClick: expandChat, children: _jsx(Maximize2, { className: "size-3.5" }) }), _jsx(TooltipIconButton, { tooltip: "Close", size: "icon", className: "size-7", onClick: handleClose, children: _jsx(X, { className: "size-4" }) })] })] }), _jsxs(ThreadPrimitive.Root, { className: "flex flex-col flex-1 overflow-hidden", children: [_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [ThreadSurface && (_jsx("div", { className: "absolute inset-0 z-10 overflow-hidden bg-background", children: _jsx(ThreadSurface, {}) })), _jsxs(ThreadPrimitive.Viewport, { className: cn("absolute inset-0 overflow-y-auto", ThreadSurface && "invisible"), "aria-hidden": ThreadSurface ? true : undefined, children: [_jsx(ThreadPrimitive.Empty, { children: _jsx("div", { className: "flex h-full items-center justify-center p-4", children: _jsx("p", { className: "text-sm text-muted-foreground text-center", children: "Ask the agent about this page" }) }) }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsxs("div", { className: "px-4 pt-4 pb-3", children: [_jsx(ThreadPrimitive.Messages, { components: {
|
|
107
110
|
UserMessage: BubbleUserMessage,
|
|
108
111
|
AssistantMessage: BubbleAssistantMessage,
|
|
109
|
-
} }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingIndicator, {}) })] }) })] }), _jsx(ThreadPrimitive.If, { empty: false, children: !ThreadSurface && _jsx(BubbleScrollToBottomButton, {}) })] }), _jsxs("div", { className: "shrink-0 border-t border-border", children: [_jsx(ChatComposerBannerSlot, {}),
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
} }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingIndicator, {}) })] }) })] }), _jsx(ThreadPrimitive.If, { empty: false, children: !ThreadSurface && _jsx(BubbleScrollToBottomButton, {}) })] }), _jsxs("div", { className: "shrink-0 border-t border-border", children: [_jsx(ChatComposerBannerSlot, {}), _jsxs("div", { className: "px-3 py-3", children: [_jsx(ChatContext, {}), _jsxs(ComposerPrimitive.Root, { className: cn(
|
|
113
|
+
// Query container for the controls inside: the bubble is
|
|
114
|
+
// a narrow panel in a wide window, so they have to size
|
|
115
|
+
// against THIS box, not the viewport.
|
|
116
|
+
"@container", "flex flex-col gap-1", "rounded-2xl border border-input", "bg-background shadow-sm", "px-3 py-2", "focus-within:border-primary/50", "transition-all duration-200"), children: [_jsx(ComposerContextBadges, {}), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "flex shrink-0 items-center gap-0.5", children: [_jsx(ComposerAddMenu, { compact: true }), _jsx(ContextPins, { compact: true })] }), _jsx(ComposerInput, { autoFocus: true, placeholder: DEFAULT_COMPOSER_PLACEHOLDER, rows: 1, className: cn("flex-1 bg-transparent", "py-1 text-sm", "text-foreground outline-none", "placeholder:text-muted-foreground", "resize-none max-h-20") }), _jsx(ThreadPrimitive.If, { running: false, children: _jsx(ReasoningEffortPicker, {}) }), _jsx(ChatComposerActionsSlot, {}), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(ComposerPrimitive.Cancel, { asChild: true, children: _jsx(Button, { variant: "secondary", size: "icon", className: "rounded-full size-8 shrink-0", "aria-label": "Stop", children: _jsx(Square, { className: "size-3" }) }) }) }), !actionsOwnSend && (_jsx(ThreadPrimitive.If, { running: false, children: _jsx(ComposerPrimitive.Send, { asChild: true, children: _jsx(Button, { size: "icon", className: "rounded-full size-8 shrink-0", "aria-label": "Send", children: _jsx(ArrowUp, { className: "size-4" }) }) }) }))] })] })] })] })] })] }))] })] }));
|
|
114
117
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { ChevronDown, FileText, Maximize2, TextCursorInput } from "lucide-react";
|
|
4
|
+
import { useAppStore } from "../lib/app-store.js";
|
|
5
|
+
import { summarizeChatContext } from "../lib/chat-context-summary.js";
|
|
6
|
+
import { Popover, PopoverTrigger, PopoverContent } from "../ui/popover.js";
|
|
7
|
+
import { ChatContextDetailsSlot } from "./chat-slots.js";
|
|
8
|
+
function readContext() {
|
|
9
|
+
return summarizeChatContext(useAppStore.getState().getAgentState());
|
|
10
|
+
}
|
|
11
|
+
/** The displayed context and the agent request use the same projection. */
|
|
12
|
+
export function ChatContext() {
|
|
13
|
+
const [open, setOpen] = useState(false);
|
|
14
|
+
const [context, setContext] = useState(readContext);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
let frame = 0;
|
|
17
|
+
const refresh = () => {
|
|
18
|
+
cancelAnimationFrame(frame);
|
|
19
|
+
frame = requestAnimationFrame(() => {
|
|
20
|
+
const next = readContext();
|
|
21
|
+
setContext((current) => JSON.stringify(current) === JSON.stringify(next) ? current : next);
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const unsubscribe = useAppStore.subscribe(refresh);
|
|
25
|
+
// Modules already track field/caret events. Read after those listeners,
|
|
26
|
+
// rather than keeping a competing focus state inside the chat UI.
|
|
27
|
+
const events = ["focusin", "pointerup", "input", "selectionchange"];
|
|
28
|
+
for (const event of events)
|
|
29
|
+
document.addEventListener(event, refresh);
|
|
30
|
+
const handles = "[data-focused], [data-context-ref-type='entity-field']";
|
|
31
|
+
const observer = typeof MutationObserver === "undefined"
|
|
32
|
+
? null
|
|
33
|
+
: new MutationObserver((records) => {
|
|
34
|
+
if (records.some((record) => record.type === "attributes" ||
|
|
35
|
+
(record.target instanceof Element && record.target.closest(handles)) ||
|
|
36
|
+
[...record.addedNodes, ...record.removedNodes].some((node) => node instanceof Element &&
|
|
37
|
+
(node.matches(handles) || node.querySelector(handles)))))
|
|
38
|
+
refresh();
|
|
39
|
+
});
|
|
40
|
+
observer?.observe(document.body, {
|
|
41
|
+
subtree: true,
|
|
42
|
+
childList: true,
|
|
43
|
+
attributes: true,
|
|
44
|
+
attributeFilter: [
|
|
45
|
+
"data-focused",
|
|
46
|
+
"data-focus-title",
|
|
47
|
+
"data-focus-kind",
|
|
48
|
+
"data-context-ref-type",
|
|
49
|
+
"data-context-ref-id",
|
|
50
|
+
"data-entity-field-key",
|
|
51
|
+
"data-entity-field-label",
|
|
52
|
+
"inert",
|
|
53
|
+
"hidden",
|
|
54
|
+
"aria-hidden",
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
refresh();
|
|
58
|
+
return () => {
|
|
59
|
+
cancelAnimationFrame(frame);
|
|
60
|
+
unsubscribe();
|
|
61
|
+
observer?.disconnect();
|
|
62
|
+
for (const event of events)
|
|
63
|
+
document.removeEventListener(event, refresh);
|
|
64
|
+
};
|
|
65
|
+
}, []);
|
|
66
|
+
const Icon = context.field ? TextCursorInput : context.focused ? Maximize2 : FileText;
|
|
67
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", "data-chat-context": true, "aria-label": `Message context: ${context.label}${context.detail ? `, ${context.detail}` : ""}`, className: "mb-2 flex min-h-9 w-full min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-left text-xs text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-2 focus-visible:outline-primary [@media(pointer:coarse)]:min-h-11", children: [_jsx(Icon, { className: "size-3.5 shrink-0" }), _jsxs("span", { className: "min-w-0 flex-1 truncate", "aria-live": "polite", children: ["With ", _jsx("span", { className: "font-medium text-foreground", children: context.label }), context.detail && (_jsxs(_Fragment, { children: [" ", _jsx("span", { "aria-hidden": true, children: "\u203A" }), " ", _jsx("span", { className: "font-medium text-foreground", children: context.detail })] }))] }), _jsx(ChevronDown, { className: "size-3.5 shrink-0" })] }) }), _jsxs(PopoverContent, { align: "start", side: "top", collisionPadding: 12, "aria-label": "Context and actions", className: "w-[22rem] max-w-[calc(100vw-1.5rem)] max-h-[min(36rem,var(--radix-popover-content-available-height))] overflow-y-auto overscroll-contain space-y-3 p-3 text-sm", children: [_jsxs("div", { className: "px-2", children: [_jsx("h2", { className: "text-xs font-medium text-muted-foreground", children: "Current context" }), _jsxs("p", { className: "mt-1 break-words font-medium", children: [context.label, context.detail && ` › ${context.detail}`] })] }), _jsx("p", { className: "px-2 text-xs leading-relaxed text-muted-foreground", children: context.detail
|
|
68
|
+
? "This selection stays available when you move into chat."
|
|
69
|
+
: "The current page follows your navigation. Select text or a field to work more specifically." }), context.selectedText && (_jsx("blockquote", { className: "max-h-32 overflow-y-auto border-l-2 border-border pl-3 text-xs", children: context.selectedText })), _jsx(ChatContextDetailsSlot, { onDismiss: () => setOpen(false) }), context.references.length > 0 && (_jsxs("div", { className: "border-t border-border px-2 pt-3 text-xs", children: [_jsx("h3", { className: "font-medium", children: "Also open" }), _jsx("ul", { className: "mt-1 space-y-1 text-muted-foreground", children: context.references.map((label, index) => (_jsx("li", { className: "truncate", children: label }, `${label}-${index}`))) })] }))] })] }));
|
|
70
|
+
}
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* chat (``ChatContent``) and the floating bubble panel. It renders
|
|
15
15
|
* inside a fixed (non-scrolling) footer, so it never scrolls away
|
|
16
16
|
* with the thread.
|
|
17
|
+
* - ``contextDetails`` — grouped capabilities beneath the current context
|
|
18
|
+
* in the composer flyout. Call ``onDismiss`` before opening action dialogs.
|
|
17
19
|
* - ``launcherBadge`` — inside the collapsed chat-bubble launcher's
|
|
18
20
|
* wrapper (a positioned ``group`` container around the round button),
|
|
19
21
|
* so a badge can position itself absolutely over the button and use
|
|
@@ -41,6 +43,11 @@
|
|
|
41
43
|
import type { ComponentType } from "react";
|
|
42
44
|
interface ChatSlotsState {
|
|
43
45
|
composerBanner: ComponentType | null;
|
|
46
|
+
/** Extra capabilities in the context flyout. Providers own section headings
|
|
47
|
+
* and can dismiss the flyout before opening a dialog or starting an action. */
|
|
48
|
+
contextDetails: ComponentType<{
|
|
49
|
+
onDismiss: () => void;
|
|
50
|
+
}> | null;
|
|
44
51
|
launcherBadge: ComponentType | null;
|
|
45
52
|
composerActions: ComponentType | null;
|
|
46
53
|
/** Let ``composerActions`` stand in for Send while the composer is empty. */
|
|
@@ -57,6 +64,9 @@ export declare const useChatSlotsStore: import("zustand").UseBoundStore<import("
|
|
|
57
64
|
*/
|
|
58
65
|
export declare function registerChatSlots(slots: Partial<ChatSlotsState>): void;
|
|
59
66
|
export declare function ChatComposerBannerSlot(): import("react/jsx-runtime").JSX.Element | null;
|
|
67
|
+
export declare function ChatContextDetailsSlot({ onDismiss }: {
|
|
68
|
+
onDismiss: () => void;
|
|
69
|
+
}): import("react/jsx-runtime").JSX.Element | null;
|
|
60
70
|
export declare function ChatLauncherBadgeSlot(): import("react/jsx-runtime").JSX.Element | null;
|
|
61
71
|
export declare function ChatComposerActionsSlot(): import("react/jsx-runtime").JSX.Element | null;
|
|
62
72
|
/**
|
|
@@ -3,6 +3,7 @@ import { create } from "zustand";
|
|
|
3
3
|
import { useAuiState } from "@assistant-ui/react";
|
|
4
4
|
export const useChatSlotsStore = create(() => ({
|
|
5
5
|
composerBanner: null,
|
|
6
|
+
contextDetails: null,
|
|
6
7
|
launcherBadge: null,
|
|
7
8
|
composerActions: null,
|
|
8
9
|
composerActionsReplaceSend: false,
|
|
@@ -18,6 +19,8 @@ export function registerChatSlots(slots) {
|
|
|
18
19
|
const next = {};
|
|
19
20
|
if (slots.composerBanner !== undefined)
|
|
20
21
|
next.composerBanner = slots.composerBanner;
|
|
22
|
+
if (slots.contextDetails !== undefined)
|
|
23
|
+
next.contextDetails = slots.contextDetails;
|
|
21
24
|
if (slots.launcherBadge !== undefined)
|
|
22
25
|
next.launcherBadge = slots.launcherBadge;
|
|
23
26
|
if (slots.composerActions !== undefined)
|
|
@@ -33,6 +36,10 @@ export function ChatComposerBannerSlot() {
|
|
|
33
36
|
const Banner = useChatSlotsStore((s) => s.composerBanner);
|
|
34
37
|
return Banner ? _jsx(Banner, {}) : null;
|
|
35
38
|
}
|
|
39
|
+
export function ChatContextDetailsSlot({ onDismiss }) {
|
|
40
|
+
const Details = useChatSlotsStore((s) => s.contextDetails);
|
|
41
|
+
return Details ? _jsx(Details, { onDismiss: onDismiss }) : null;
|
|
42
|
+
}
|
|
36
43
|
export function ChatLauncherBadgeSlot() {
|
|
37
44
|
const Badge = useChatSlotsStore((s) => s.launcherBadge);
|
|
38
45
|
return Badge ? _jsx(Badge, {}) : null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PinStateIcon } from "./pin-state-icon.js";
|
|
2
3
|
import { ComposerPrimitive } from "@assistant-ui/react";
|
|
3
|
-
import { Paperclip,
|
|
4
|
+
import { Paperclip, Plus } from "lucide-react";
|
|
4
5
|
import { Button, cn } from "@iloveagents/foundry-web-primitives";
|
|
5
6
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "../ui/dropdown-menu.js";
|
|
6
7
|
import { usePagePin } from "./context-bar.js";
|
|
@@ -12,5 +13,5 @@ export const ComposerAddMenu = ({ compact = false, showPin = true }) => {
|
|
|
12
13
|
// user sees the result of what they just clicked.
|
|
13
14
|
event.preventDefault();
|
|
14
15
|
toggle();
|
|
15
|
-
}, children: [
|
|
16
|
+
}, children: [_jsx(PinStateIcon, { pinned: isPinned, className: icon }), isPinned ? "Unpin this page" : "Pin this page"] }))] })] }));
|
|
16
17
|
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ComponentProps } from "react";
|
|
2
|
+
import { ComposerPrimitive } from "@assistant-ui/react";
|
|
3
|
+
/** Re-measure autosizing when a pane changes width, not just on window resize. */
|
|
4
|
+
export declare function ComposerInput(props: ComponentProps<typeof ComposerPrimitive.Input>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { ComposerPrimitive, useAuiState } from "@assistant-ui/react";
|
|
4
|
+
/** Re-measure autosizing when a pane changes width, not just on window resize. */
|
|
5
|
+
export function ComposerInput(props) {
|
|
6
|
+
const ref = useRef(null);
|
|
7
|
+
const [width, setWidth] = useState(0);
|
|
8
|
+
const hasText = useAuiState((state) => state.composer.text.length > 0);
|
|
9
|
+
useLayoutEffect(() => {
|
|
10
|
+
const input = ref.current;
|
|
11
|
+
if (!input || typeof ResizeObserver === "undefined")
|
|
12
|
+
return;
|
|
13
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
14
|
+
const next = entry?.contentRect.width ?? 0;
|
|
15
|
+
// Height changes are autosize's output. Only width is an input.
|
|
16
|
+
if (next > 0)
|
|
17
|
+
setWidth(next);
|
|
18
|
+
});
|
|
19
|
+
observer.observe(input);
|
|
20
|
+
return () => observer.disconnect();
|
|
21
|
+
}, []);
|
|
22
|
+
return (_jsx(ComposerPrimitive.Input, { ...props, ref: ref, "data-composer-width": width, minRows: props.minRows ?? 1, maxRows: hasText ? props.maxRows : 1 }));
|
|
23
|
+
}
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { type FC } from "react";
|
|
11
11
|
/** Renders ephemeral context badges in the composer (persistent items shown in ContextPins). */
|
|
12
|
-
export declare const ComposerContextBadges: FC
|
|
12
|
+
export declare const ComposerContextBadges: FC<{
|
|
13
|
+
hideCurrentPage?: boolean;
|
|
14
|
+
hiddenPageLabels?: string[];
|
|
15
|
+
}>;
|
|
13
16
|
/** Renders sent context badges on a user message (read-only). */
|
|
14
17
|
export declare const SentContextBadges: FC<{
|
|
15
18
|
messageId: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PinStateIcon } from "./pin-state-icon.js";
|
|
2
3
|
/**
|
|
3
4
|
* Context badge components for the composer and sent messages.
|
|
4
5
|
*
|
|
@@ -9,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
10
|
* - SentContextBadges: read-only badges on sent user messages
|
|
10
11
|
*/
|
|
11
12
|
import { createElement } from "react";
|
|
12
|
-
import {
|
|
13
|
+
import { TextSelect, X, FileText, Link2, StickyNote } from "lucide-react";
|
|
13
14
|
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
14
15
|
import { Tooltip, TooltipTrigger, TooltipContent } from "../ui/tooltip.js";
|
|
15
16
|
import { useAppStore } from "../lib/app-store.js";
|
|
@@ -32,7 +33,7 @@ const ComposerBadge = ({ item }) => {
|
|
|
32
33
|
const removeItem = useAppStore((s) => s.removeContextItem);
|
|
33
34
|
const promoteItem = useAppStore((s) => s.promoteContextItem);
|
|
34
35
|
const isEphemeral = item.persistence === "ephemeral";
|
|
35
|
-
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: cn("inline-flex items-center gap-1.5 max-w-70", "rounded-lg bg-primary/5", "px-2 py-1", isEphemeral ? "border border-dashed border-primary/40" : "border border-primary/20"), children: [_jsx(ContextIcon, { type: item.type, className: "size-3 text-primary shrink-0" }), _jsx("span", { className: "text-xs text-foreground truncate", children: item.label }), isEphemeral && (_jsx("button", { type: "button", onClick: () => promoteItem(item.id), className: "p-0.5 rounded hover:bg-primary/10 shrink-0", "aria-label": "Pin context", children: _jsx(
|
|
36
|
+
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: cn("inline-flex items-center gap-1.5 max-w-70", "rounded-lg bg-primary/5", "px-2 py-1", isEphemeral ? "border border-dashed border-primary/40" : "border border-primary/20"), children: [_jsx(ContextIcon, { type: item.type, className: "size-3 text-primary shrink-0" }), _jsx("span", { className: "text-xs text-foreground truncate", children: item.label }), isEphemeral && (_jsx("button", { type: "button", onClick: () => promoteItem(item.id), className: "p-0.5 rounded hover:bg-primary/10 shrink-0", "aria-label": "Pin context", children: _jsx(PinStateIcon, { pinned: false, className: "size-3" }) })), _jsx("button", { type: "button", onClick: () => removeItem(item.id), className: "p-0.5 rounded hover:bg-primary/10 shrink-0", "aria-label": "Remove context", children: _jsx(X, { className: "size-3" }) })] }) }), _jsx(TooltipContent, { side: "top", className: "whitespace-pre-line", children: getBadgeTooltip(item) })] }));
|
|
36
37
|
};
|
|
37
38
|
/** Read-only badge on a sent user message. Mirrors composer context chips. */
|
|
38
39
|
const SentBadge = ({ item }) => {
|
|
@@ -40,9 +41,11 @@ const SentBadge = ({ item }) => {
|
|
|
40
41
|
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: cn("inline-flex items-center gap-1.5 max-w-70", "rounded-lg bg-primary/5", "px-2 py-1", isEphemeral ? "border border-dashed border-primary/40" : "border border-primary/20"), children: [_jsx(ContextIcon, { type: item.type, className: "size-3 text-primary shrink-0" }), _jsx("span", { className: "text-[11px] leading-tight text-foreground truncate", children: item.label })] }) }), _jsx(TooltipContent, { side: "top", className: "whitespace-pre-line", children: getBadgeTooltip(item) })] }));
|
|
41
42
|
};
|
|
42
43
|
/** Renders ephemeral context badges in the composer (persistent items shown in ContextPins). */
|
|
43
|
-
export const ComposerContextBadges = () => {
|
|
44
|
+
export const ComposerContextBadges = ({ hideCurrentPage = false, hiddenPageLabels = [] }) => {
|
|
44
45
|
const items = useAppStore((s) => s.contextItems);
|
|
45
|
-
const ephemeral = items.filter((i) => i.persistence === "ephemeral"
|
|
46
|
+
const ephemeral = items.filter((i) => i.persistence === "ephemeral" &&
|
|
47
|
+
!(hideCurrentPage &&
|
|
48
|
+
(i.payload.kind === "page" || (i.type === "note" && hiddenPageLabels.includes(i.label)))));
|
|
46
49
|
if (ephemeral.length === 0)
|
|
47
50
|
return null;
|
|
48
51
|
return (_jsx("div", { className: "flex flex-wrap gap-1.5", children: ephemeral.map((item) => (_jsx(ComposerBadge, { item: item }, item.id))) }));
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { type FC } from "react";
|
|
8
8
|
import { type ContextItem } from "../lib/app-store.js";
|
|
9
9
|
interface ContextPinsProps {
|
|
10
|
+
allowPageToggle?: boolean;
|
|
10
11
|
/** Show the pin button to add current page */
|
|
11
12
|
showPinButton?: boolean;
|
|
12
13
|
/** Compact mode for small containers (mini chat bubble header) */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PinStateIcon } from "./pin-state-icon.js";
|
|
2
3
|
/**
|
|
3
4
|
* Context Pins — pin button with count badge and hover flyout.
|
|
4
5
|
*
|
|
@@ -7,7 +8,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
7
8
|
*/
|
|
8
9
|
import { useState, useRef, useEffect, createElement } from "react";
|
|
9
10
|
import { useNavigate } from "react-router";
|
|
10
|
-
import { X,
|
|
11
|
+
import { X, TextSelect, FileText, Link2, StickyNote } from "lucide-react";
|
|
11
12
|
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
12
13
|
import { TooltipIconButton } from "./tooltip-icon-button.js";
|
|
13
14
|
import { useAppStore } from "../lib/app-store.js";
|
|
@@ -90,7 +91,7 @@ export function usePagePin() {
|
|
|
90
91
|
},
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
|
-
export const ContextPins = ({ showPinButton = false, compact = false, flyoutDirection = "up", }) => {
|
|
94
|
+
export const ContextPins = ({ allowPageToggle = true, showPinButton = false, compact = false, flyoutDirection = "up", }) => {
|
|
94
95
|
const items = useAppStore((s) => s.contextItems);
|
|
95
96
|
const removeItem = useAppStore((s) => s.removeContextItem);
|
|
96
97
|
const currentPage = useAppStore((s) => s.currentPage);
|
|
@@ -122,7 +123,14 @@ export const ContextPins = ({ showPinButton = false, compact = false, flyoutDire
|
|
|
122
123
|
};
|
|
123
124
|
if (pinned.length === 0 && !showPinButton)
|
|
124
125
|
return null;
|
|
125
|
-
return (_jsxs("div", { className: "relative", ref: containerRef, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave,
|
|
126
|
+
return (_jsxs("div", { className: "relative", ref: containerRef, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, onFocus: handleMouseEnter, onBlur: (event) => {
|
|
127
|
+
if (!event.currentTarget.contains(event.relatedTarget))
|
|
128
|
+
setFlyoutOpen(false);
|
|
129
|
+
}, children: [_jsxs(TooltipIconButton, { tooltip: allowPageToggle
|
|
130
|
+
? isCurrentPagePinned
|
|
131
|
+
? "Unpin this page"
|
|
132
|
+
: "Pin this page"
|
|
133
|
+
: "Manage pinned context", size: "icon", className: cn("shrink-0 relative", compact ? "size-7" : "size-8", isCurrentPagePinned ? "text-foreground" : "text-muted-foreground"), "aria-pressed": allowPageToggle ? isCurrentPagePinned : undefined, "aria-expanded": !allowPageToggle ? flyoutOpen : undefined, onClick: allowPageToggle ? handleTogglePin : () => setFlyoutOpen(true), children: [_jsx(PinStateIcon, { pinned: isCurrentPagePinned, className: compact ? "size-3.5" : "size-4" }), pinned.length > 0 && (_jsx("span", { className: cn("absolute rounded-full bg-primary font-medium text-primary-foreground flex items-center justify-center", compact
|
|
126
134
|
? "-top-0.5 -right-0.5 size-3.5 text-[9px]"
|
|
127
135
|
: "top-0 right-0 size-3.5 text-[9px]"), children: pinned.length }))] }), flyoutOpen && pinned.length > 0 && (_jsxs("div", { className: cn("absolute z-50 w-64 rounded-lg border border-border bg-background shadow-xl p-2 space-y-1", flyoutDirection === "up" ? "bottom-full mb-2 left-0" : "top-9 right-0"), children: [_jsx("div", { className: "px-1 pb-1 border-b border-border/50", children: _jsx("span", { className: "text-[10px] font-medium text-muted-foreground uppercase tracking-wide", children: "Pinned Context" }) }), pinned.map((item) => {
|
|
128
136
|
const navPath = getNavigablePath(item);
|
|
@@ -1,35 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The frame around a list: normally a plain container, in focus mode a
|
|
3
|
-
* fullscreen layer (Escape leaves it). `DataTableToolbar` shows the focus
|
|
4
|
-
* button whenever it renders inside a frame — users with wide lists get a
|
|
5
|
-
* way out of a cramped page without the host doing anything.
|
|
6
|
-
*
|
|
7
|
-
* The focus layer stays where it is in the tree — only its classes change —
|
|
8
|
-
* so entering and leaving focus never remounts the list (its rows, drawer,
|
|
9
|
-
* selection and in-flight edits survive) and it keeps inheriting the host's
|
|
10
|
-
* theme scope. It never scrolls itself: it is a fixed flex column whose
|
|
11
|
-
* growing child — the table — owns the scrolling, which keeps the toolbar in
|
|
12
|
-
* place while the rows move under a sticky header (see `DataTable`, which
|
|
13
|
-
* reads this context).
|
|
14
|
-
*
|
|
15
|
-
* A `position: fixed` layer is relative to the viewport unless an ancestor
|
|
16
|
-
* establishes a containing block (`transform`, `filter`, `perspective`,
|
|
17
|
-
* `contain: paint`); a host that does that on a list's ancestor has to opt
|
|
18
|
-
* out of focus mode.
|
|
19
|
-
*/
|
|
20
1
|
import { type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
/** A stable portal host preserves filters, selection and drafts across focus transitions. */
|
|
21
3
|
export interface DataTableFrameContextValue {
|
|
22
4
|
focused: boolean;
|
|
23
5
|
setFocused: (focused: boolean) => void;
|
|
24
6
|
}
|
|
25
|
-
/** The enclosing frame's focus state, or `null` outside a frame. */
|
|
26
7
|
export declare function useDataTableFrame(): DataTableFrameContextValue | null;
|
|
27
8
|
export interface DataTableFrameProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
28
9
|
children: ReactNode;
|
|
29
|
-
/** Classes applied in focus mode on top of the fullscreen layer. */
|
|
30
10
|
focusClassName?: string;
|
|
31
11
|
defaultFocused?: boolean;
|
|
32
|
-
/** Told when focus mode turns on or off — a host with room to spare can show more. */
|
|
33
12
|
onFocusChange?: (focused: boolean) => void;
|
|
34
13
|
}
|
|
35
14
|
export declare const DataTableFrame: import("react").ForwardRefExoticComponent<DataTableFrameProps & import("react").RefAttributes<HTMLDivElement>>;
|