@iloveagents/foundry-web-ui 0.3.1 → 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/README.md +13 -4
- package/dist/components/ag-ui-runtime-provider.d.ts +64 -0
- package/dist/components/ag-ui-runtime-provider.js +71 -0
- package/dist/components/app-brand.d.ts +6 -0
- package/dist/components/app-brand.js +13 -0
- package/dist/components/assistant-chat.d.ts +26 -0
- package/dist/components/assistant-chat.js +94 -0
- package/dist/components/chat-attachments.d.ts +25 -0
- package/dist/components/chat-attachments.js +20 -0
- package/dist/components/chat-bubble.d.ts +1 -0
- package/dist/components/chat-bubble.js +105 -0
- package/dist/components/chat-header.d.ts +2 -0
- package/dist/components/chat-header.js +16 -0
- package/dist/components/client-tool-executor.d.ts +12 -0
- package/dist/components/client-tool-executor.js +108 -0
- package/dist/components/collection-empty-state.d.ts +9 -0
- package/dist/components/collection-empty-state.js +6 -0
- package/dist/components/collection-filter-bar.d.ts +19 -0
- package/dist/components/collection-filter-bar.js +28 -0
- package/dist/components/collection-search-input.d.ts +6 -0
- package/dist/components/collection-search-input.js +8 -0
- package/dist/components/collection-skeleton.d.ts +6 -0
- package/dist/components/collection-skeleton.js +10 -0
- package/dist/components/collection-sort-menu.d.ts +10 -0
- package/dist/components/collection-sort-menu.js +9 -0
- package/dist/components/collection-surface.d.ts +8 -0
- package/dist/components/collection-surface.js +5 -0
- package/dist/components/collection-toolbar.d.ts +9 -0
- package/dist/components/collection-toolbar.js +5 -0
- package/dist/components/collection-view-toggle.d.ts +7 -0
- package/dist/components/collection-view-toggle.js +17 -0
- package/dist/components/composer-submit-bridge.d.ts +1 -0
- package/dist/components/composer-submit-bridge.js +15 -0
- package/dist/components/confirm-dialog.d.ts +11 -0
- package/dist/components/confirm-dialog.js +6 -0
- package/dist/components/confirmation-card.d.ts +14 -0
- package/dist/components/confirmation-card.js +12 -0
- package/dist/components/context-badges.d.ts +16 -0
- package/dist/components/context-badges.js +56 -0
- package/dist/components/context-bar.d.ts +41 -0
- package/dist/components/context-bar.js +123 -0
- package/dist/components/form-dialog.d.ts +12 -0
- package/dist/components/form-dialog.js +13 -0
- package/dist/components/global-selection-popover.d.ts +7 -0
- package/dist/components/global-selection-popover.js +167 -0
- package/dist/components/infinite-scroll-sentinel.d.ts +6 -0
- package/dist/components/infinite-scroll-sentinel.js +25 -0
- package/dist/components/json-viewer.d.ts +10 -0
- package/dist/components/json-viewer.js +80 -0
- package/dist/components/loading-indicator.d.ts +13 -0
- package/dist/components/loading-indicator.js +35 -0
- package/dist/components/markdown-text.d.ts +13 -0
- package/dist/components/markdown-text.js +221 -0
- 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/components/name-dialog.d.ts +16 -0
- package/dist/components/name-dialog.js +23 -0
- package/dist/components/selection-popover.d.ts +12 -0
- package/dist/components/selection-popover.js +111 -0
- package/dist/components/show-document-tool-ui.d.ts +1 -0
- package/dist/components/show-document-tool-ui.js +53 -0
- package/dist/components/sidebar.d.ts +4 -0
- package/dist/components/sidebar.js +904 -0
- package/dist/components/surface-card.d.ts +9 -0
- package/dist/components/surface-card.js +33 -0
- package/dist/components/theme-runtime-provider.d.ts +15 -0
- package/dist/components/theme-runtime-provider.js +48 -0
- package/dist/components/theme-toggle.d.ts +2 -0
- package/dist/components/theme-toggle.js +19 -0
- package/dist/components/tool-call-card.d.ts +27 -0
- package/dist/components/tool-call-card.js +13 -0
- package/dist/components/tool-fallback.d.ts +26 -0
- package/dist/components/tool-fallback.js +129 -0
- package/dist/components/tool-panel-layout.d.ts +10 -0
- package/dist/components/tool-panel-layout.js +12 -0
- package/dist/components/tool-panel.d.ts +1 -0
- package/dist/components/tool-panel.js +89 -0
- package/dist/components/tooltip-icon-button.d.ts +7 -0
- package/dist/components/tooltip-icon-button.js +9 -0
- package/dist/components/user-menu.d.ts +2 -0
- package/dist/components/user-menu.js +14 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +83 -0
- package/dist/lib/ag-ui-adapter.d.ts +29 -0
- package/dist/lib/ag-ui-adapter.js +382 -0
- package/dist/lib/app-store.d.ts +115 -0
- package/dist/lib/app-store.js +199 -0
- package/dist/lib/attachment-adapter.d.ts +20 -0
- package/dist/lib/attachment-adapter.js +99 -0
- package/dist/lib/auth-provider.d.ts +16 -0
- package/dist/lib/auth-provider.js +88 -0
- package/dist/lib/chat-bubble-store.d.ts +30 -0
- package/dist/lib/chat-bubble-store.js +23 -0
- package/dist/lib/chat-lifecycle-store.d.ts +10 -0
- package/{src/lib/chat-lifecycle-store.ts → dist/lib/chat-lifecycle-store.js} +3 -13
- package/dist/lib/client-tools.d.ts +11 -0
- package/dist/lib/client-tools.js +274 -0
- package/dist/lib/composer-submit-store.d.ts +12 -0
- package/dist/lib/composer-submit-store.js +17 -0
- package/dist/lib/dev-store.d.ts +38 -0
- package/dist/lib/dev-store.js +32 -0
- package/dist/lib/nav-config.d.ts +159 -0
- package/dist/lib/nav-config.js +162 -0
- package/dist/lib/nav-dnd.d.ts +8 -0
- package/dist/lib/nav-dnd.js +9 -0
- package/dist/lib/nav-store.d.ts +45 -0
- package/dist/lib/nav-store.js +49 -0
- package/dist/lib/selection-context.d.ts +21 -0
- package/dist/lib/selection-context.js +34 -0
- package/dist/lib/sidebar-store.d.ts +23 -0
- package/dist/lib/sidebar-store.js +98 -0
- package/dist/lib/theme-runtime.d.ts +130 -0
- package/dist/lib/theme-runtime.js +479 -0
- package/dist/lib/theme-store.d.ts +15 -0
- package/dist/lib/theme-store.js +53 -0
- package/dist/lib/tool-panel-store.d.ts +48 -0
- package/dist/lib/tool-panel-store.js +69 -0
- package/dist/lib/use-new-conversation.d.ts +8 -0
- package/dist/lib/use-new-conversation.js +62 -0
- package/dist/lib/use-page-tools.d.ts +27 -0
- package/{src/lib/use-page-tools.ts → dist/lib/use-page-tools.js} +11 -13
- package/dist/styles.css +142 -0
- package/dist/ui/collapsible.d.ts +5 -0
- package/{src/ui/collapsible.tsx → dist/ui/collapsible.js} +0 -2
- package/dist/ui/dialog.d.ts +15 -0
- package/dist/ui/dialog.js +25 -0
- package/dist/ui/dropdown-menu.d.ts +7 -0
- package/dist/ui/dropdown-menu.js +19 -0
- package/dist/ui/popover.d.ts +7 -0
- package/dist/ui/popover.js +10 -0
- package/dist/ui/select.d.ts +4 -0
- package/dist/ui/select.js +9 -0
- package/dist/ui/tooltip.d.ts +7 -0
- package/dist/ui/tooltip.js +10 -0
- package/package.json +31 -12
- package/AGENTS.md +0 -59
- package/CHANGELOG.md +0 -211
- package/CLAUDE.md +0 -1
- package/src/__tests__/no-spaces-imports.test.ts +0 -59
- package/src/__tests__/open-core-guards.test.ts +0 -307
- package/src/__tests__/theme-runtime.test.ts +0 -81
- package/src/__tests__/tool-fallback.test.tsx +0 -109
- package/src/components/__tests__/context-bar.test.ts +0 -112
- package/src/components/ag-ui-runtime-provider.tsx +0 -170
- package/src/components/app-brand.tsx +0 -38
- package/src/components/assistant-chat.tsx +0 -461
- package/src/components/chat-attachments.tsx +0 -68
- package/src/components/chat-bubble.tsx +0 -373
- package/src/components/chat-header.tsx +0 -44
- package/src/components/client-tool-executor.tsx +0 -230
- package/src/components/collection-empty-state.tsx +0 -37
- package/src/components/collection-filter-bar.tsx +0 -168
- package/src/components/collection-search-input.tsx +0 -41
- package/src/components/collection-skeleton.tsx +0 -55
- package/src/components/collection-sort-menu.tsx +0 -62
- package/src/components/collection-surface.tsx +0 -46
- package/src/components/collection-toolbar.tsx +0 -33
- package/src/components/collection-view-toggle.tsx +0 -61
- package/src/components/composer-submit-bridge.tsx +0 -19
- package/src/components/confirm-dialog.tsx +0 -49
- package/src/components/confirmation-card.tsx +0 -32
- package/src/components/context-badges.tsx +0 -136
- package/src/components/context-bar.tsx +0 -238
- package/src/components/form-dialog.tsx +0 -56
- package/src/components/global-selection-popover.tsx +0 -266
- package/src/components/infinite-scroll-sentinel.tsx +0 -48
- package/src/components/json-viewer.tsx +0 -232
- package/src/components/loading-indicator.tsx +0 -40
- package/src/components/markdown-text-citations.test.tsx +0 -108
- package/src/components/markdown-text.tsx +0 -508
- package/src/components/name-dialog.tsx +0 -87
- package/src/components/selection-popover.tsx +0 -156
- package/src/components/show-document-tool-ui.tsx +0 -90
- package/src/components/sidebar.test.tsx +0 -611
- package/src/components/sidebar.tsx +0 -1689
- package/src/components/surface-card.tsx +0 -45
- package/src/components/theme-runtime-provider.tsx +0 -93
- package/src/components/theme-toggle.tsx +0 -27
- package/src/components/tool-call-card.tsx +0 -126
- package/src/components/tool-fallback.tsx +0 -169
- package/src/components/tool-panel-layout.tsx +0 -36
- package/src/components/tool-panel.tsx +0 -233
- package/src/components/tooltip-icon-button.tsx +0 -28
- package/src/components/user-menu.tsx +0 -60
- package/src/index.ts +0 -185
- package/src/lib/__tests__/ag-ui-adapter.test.ts +0 -279
- package/src/lib/__tests__/app-store.test.ts +0 -405
- package/src/lib/__tests__/attachment-adapter.test.ts +0 -48
- package/src/lib/__tests__/chat-bubble-store.test.ts +0 -67
- package/src/lib/__tests__/client-tools.test.ts +0 -124
- package/src/lib/__tests__/dev-store.test.ts +0 -78
- package/src/lib/__tests__/nav-config.test.ts +0 -135
- package/src/lib/__tests__/nav-dnd.test.ts +0 -24
- package/src/lib/__tests__/nav-store.test.ts +0 -59
- package/src/lib/__tests__/selection-context.test.ts +0 -114
- package/src/lib/__tests__/sidebar-store.test.ts +0 -144
- package/src/lib/__tests__/theme-store.test.ts +0 -83
- package/src/lib/__tests__/tool-panel-store.test.ts +0 -61
- package/src/lib/__tests__/use-page-context.test.ts +0 -58
- package/src/lib/ag-ui-adapter.ts +0 -426
- package/src/lib/app-store.ts +0 -361
- package/src/lib/attachment-adapter.ts +0 -92
- package/src/lib/auth-provider.tsx +0 -144
- package/src/lib/chat-bubble-store.ts +0 -46
- package/src/lib/client-tools.ts +0 -293
- package/src/lib/composer-submit-store.ts +0 -32
- package/src/lib/dev-store.ts +0 -69
- package/src/lib/nav-config.ts +0 -329
- package/src/lib/nav-dnd.ts +0 -14
- package/src/lib/nav-store.ts +0 -89
- package/src/lib/selection-context.ts +0 -65
- package/src/lib/sidebar-store.ts +0 -113
- package/src/lib/theme-runtime.ts +0 -660
- package/src/lib/theme-store.ts +0 -67
- package/src/lib/tool-panel-store.ts +0 -121
- package/src/lib/use-new-conversation.test.tsx +0 -168
- package/src/lib/use-new-conversation.ts +0 -64
- package/src/ui/dialog.tsx +0 -109
- package/src/ui/dropdown-menu.tsx +0 -75
- package/src/ui/popover.tsx +0 -34
- package/src/ui/select.tsx +0 -15
- package/src/ui/tooltip.tsx +0 -30
- package/src/vite-env.d.ts +0 -1
- package/tsconfig.json +0 -16
- package/vitest.config.ts +0 -8
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# @iloveagents/foundry-web-ui
|
|
2
2
|
|
|
3
|
-
React UI core for
|
|
3
|
+
React UI core for Foundry UI apps.
|
|
4
|
+
|
|
5
|
+
`web-ui` is the package that renders the actual agent experience: chat UI,
|
|
6
|
+
composer, AG-UI adapter, message rendering, tool-call cards, side panels,
|
|
7
|
+
sidebar, theme runtime, context pins, attachment handling, and UI stores. It
|
|
8
|
+
depends on `@iloveagents/foundry-web-primitives` for leaf primitives (Button,
|
|
9
|
+
Input, Field, Checkbox, Textarea, `cn`).
|
|
4
10
|
|
|
5
11
|
## Install
|
|
6
12
|
|
|
@@ -22,11 +28,14 @@ import {
|
|
|
22
28
|
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
23
29
|
```
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
This package publishes built ESM JavaScript and `.d.ts` declarations. Host apps
|
|
32
|
+
must include Tailwind v4 `@source` directives so classes used inside package
|
|
33
|
+
components are generated:
|
|
26
34
|
|
|
27
35
|
```css
|
|
28
|
-
@
|
|
29
|
-
@source "
|
|
36
|
+
@import "@iloveagents/foundry-web-ui/styles.css";
|
|
37
|
+
@source "../node_modules/@iloveagents/foundry-web-primitives/dist";
|
|
38
|
+
@source "../node_modules/@iloveagents/foundry-web-ui/dist";
|
|
30
39
|
```
|
|
31
40
|
|
|
32
41
|
## Extension Points
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ThreadHistoryAdapter } from "@assistant-ui/react";
|
|
2
|
+
import { AGUIAdapterSDK } from "../lib/ag-ui-adapter.js";
|
|
3
|
+
/**
|
|
4
|
+
* Per-mount inputs the host shell passes when it wants a chat
|
|
5
|
+
* conversation history wired to the runtime. Mirrors
|
|
6
|
+
* :type:`ChatConversationFactoryArgs` in ``foundry-web-shell`` —
|
|
7
|
+
* duplicated here (rather than imported) so the web-ui package stays
|
|
8
|
+
* independent of the shell package's dependency graph.
|
|
9
|
+
*/
|
|
10
|
+
export interface AGUIChatConversationFactoryArgs {
|
|
11
|
+
/** Conversation id captured from the URL (resumed chat) or undefined (fresh). */
|
|
12
|
+
urlMatch?: string;
|
|
13
|
+
/** Stable AG-UI adapter thread id — present in both cases. */
|
|
14
|
+
aguiThreadId: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Factory the consumer passes to build a :type:`ThreadHistoryAdapter`
|
|
18
|
+
* once the AG-UI adapter has minted its thread id. Called inside the
|
|
19
|
+
* keyed runtime mount; assistant-ui invokes ``load()``/``append()``
|
|
20
|
+
* via the returned adapter.
|
|
21
|
+
*/
|
|
22
|
+
export type AGUIHistoryAdapterFactory = (args: AGUIChatConversationFactoryArgs) => ThreadHistoryAdapter;
|
|
23
|
+
interface AGUIRuntimeProviderProps {
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
/** Custom fetch for agent requests. Handles URL rewriting + auth in production. */
|
|
26
|
+
fetchFn?: typeof fetch;
|
|
27
|
+
/**
|
|
28
|
+
* Optional stable thread id for the AG-UI adapter. When set, the
|
|
29
|
+
* agent's POST body carries this id so server-side persistence
|
|
30
|
+
* (Foundry thread, conversation row) targets the same conversation
|
|
31
|
+
* across reloads / resumes. Changing the id remounts the inner
|
|
32
|
+
* runtime — past messages from a different thread don't bleed
|
|
33
|
+
* into the new one.
|
|
34
|
+
*/
|
|
35
|
+
threadId?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Conversation id captured from the current URL when the user
|
|
38
|
+
* resumed an existing chat (e.g. ``/chat/<id>``), or ``undefined``
|
|
39
|
+
* for a brand-new chat. Distinct from ``threadId``: the host shell
|
|
40
|
+
* MAY set ``threadId`` from a non-URL source (a sticky active
|
|
41
|
+
* conversation, a freshly-minted UUID) so ``threadId`` alone can't
|
|
42
|
+
* tell a history adapter "we're resuming". ``urlMatch`` carries
|
|
43
|
+
* that signal verbatim into ``historyAdapterFactory`` so the
|
|
44
|
+
* adapter can decide whether to load history (resume) or
|
|
45
|
+
* initialise empty (fresh) without inferring from ``threadId``.
|
|
46
|
+
*/
|
|
47
|
+
urlMatch?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Factory that builds the assistant-ui ``ThreadHistoryAdapter`` for
|
|
50
|
+
* this mount. Called once after the AG-UI adapter exists so it can
|
|
51
|
+
* receive both the URL match (when resuming) and the adapter's own
|
|
52
|
+
* thread id (always set — covers fresh chats). Pass alongside
|
|
53
|
+
* ``threadId`` for resumed chats; pass without ``threadId`` to get
|
|
54
|
+
* a fresh-chat runtime that still persists.
|
|
55
|
+
*/
|
|
56
|
+
historyAdapterFactory?: AGUIHistoryAdapterFactory;
|
|
57
|
+
}
|
|
58
|
+
interface AGUIContextValue {
|
|
59
|
+
adapter: AGUIAdapterSDK;
|
|
60
|
+
resetThread: () => void;
|
|
61
|
+
}
|
|
62
|
+
export declare function useAGUIAdapter(): AGUIContextValue;
|
|
63
|
+
export declare function AGUIRuntimeProvider(props: AGUIRuntimeProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useState, useCallback, useMemo } from "react";
|
|
3
|
+
import { AssistantRuntimeProvider, useLocalRuntime } from "@assistant-ui/react";
|
|
4
|
+
import { TooltipProvider } from "../ui/tooltip.js";
|
|
5
|
+
import { AGUIAdapterSDK } from "../lib/ag-ui-adapter.js";
|
|
6
|
+
import { FileAttachmentAdapter } from "../lib/attachment-adapter.js";
|
|
7
|
+
import { ShowDocumentToolUI } from "./show-document-tool-ui.js";
|
|
8
|
+
import { ClientToolExecutor } from "./client-tool-executor.js";
|
|
9
|
+
const AGUIAdapterContext = createContext(null);
|
|
10
|
+
export function useAGUIAdapter() {
|
|
11
|
+
const context = useContext(AGUIAdapterContext);
|
|
12
|
+
if (!context) {
|
|
13
|
+
throw new Error("useAGUIAdapter must be used within AGUIRuntimeProvider");
|
|
14
|
+
}
|
|
15
|
+
return context;
|
|
16
|
+
}
|
|
17
|
+
export function AGUIRuntimeProvider(props) {
|
|
18
|
+
// Splitting render across an outer wrapper + a keyed inner is the
|
|
19
|
+
// canonical React idiom for "rebuild this stateful subtree when X
|
|
20
|
+
// changes". assistant-ui's runtime is created inside useLocalRuntime
|
|
21
|
+
// and tracks its messages internally; swapping threadId or history
|
|
22
|
+
// mid-life would leave stale state. Keying the inner subtree on
|
|
23
|
+
// threadId (+ the manual reset counter) guarantees a clean runtime
|
|
24
|
+
// per conversation, matching the ChatGPT-style "click → fresh chat
|
|
25
|
+
// backed by past messages" UX.
|
|
26
|
+
const [resetKey, setResetKey] = useState(0);
|
|
27
|
+
const resetThread = useCallback(() => setResetKey((p) => p + 1), []);
|
|
28
|
+
const innerKey = `${props.threadId ?? "fresh"}:${resetKey}`;
|
|
29
|
+
return (_jsx(AGUIRuntimeInner, { ...props, resetThread: resetThread }, innerKey));
|
|
30
|
+
}
|
|
31
|
+
function AGUIRuntimeInner({ children, fetchFn, threadId, urlMatch, historyAdapterFactory, resetThread, }) {
|
|
32
|
+
const adapter = useMemo(() => new AGUIAdapterSDK("/api/agent",
|
|
33
|
+
// Only pass options if at least one is set, mirroring the prior
|
|
34
|
+
// single-arg semantics for the "fresh chat, no overrides" case.
|
|
35
|
+
fetchFn || threadId ? { fetchFn, threadId } : undefined), [fetchFn, threadId]);
|
|
36
|
+
const attachmentAdapter = useMemo(() => new FileAttachmentAdapter(), []);
|
|
37
|
+
// Build the history adapter ONCE per inner mount, after the AG-UI
|
|
38
|
+
// adapter exists so we know its thread id. The keyed-remount in the
|
|
39
|
+
// outer wrapper ensures threadId changes trigger a fresh adapter +
|
|
40
|
+
// fresh history adapter together, so resumed chats hydrate from the
|
|
41
|
+
// right conversation and fresh chats persist under their assistant-
|
|
42
|
+
// ui-minted UUID.
|
|
43
|
+
const historyAdapter = useMemo(() => {
|
|
44
|
+
if (!historyAdapterFactory)
|
|
45
|
+
return undefined;
|
|
46
|
+
return historyAdapterFactory({
|
|
47
|
+
// ``urlMatch`` is the URL-derived resume signal (undefined for
|
|
48
|
+
// fresh chats). ``aguiThreadId`` is always defined — the AG-UI
|
|
49
|
+
// adapter mints it on construction. The factory uses ``urlMatch``
|
|
50
|
+
// to decide "load history" vs "initialise empty", and
|
|
51
|
+
// ``aguiThreadId`` to identify the row to write into.
|
|
52
|
+
urlMatch,
|
|
53
|
+
aguiThreadId: adapter.threadId,
|
|
54
|
+
});
|
|
55
|
+
// adapter is stable for the life of this inner mount (keyed remount
|
|
56
|
+
// covers the threadId change case), so it's OK to depend on the
|
|
57
|
+
// factory + urlMatch here.
|
|
58
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
59
|
+
}, [historyAdapterFactory, urlMatch]);
|
|
60
|
+
const runtime = useLocalRuntime(adapter, {
|
|
61
|
+
adapters: {
|
|
62
|
+
attachments: attachmentAdapter,
|
|
63
|
+
// history is optional — when undefined, the runtime acts as a
|
|
64
|
+
// pure in-memory thread (the original behaviour). When provided,
|
|
65
|
+
// assistant-ui calls .load() on mount to seed past messages and
|
|
66
|
+
// .append() after every turn.
|
|
67
|
+
...(historyAdapter ? { history: historyAdapter } : {}),
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
return (_jsx(AGUIAdapterContext.Provider, { value: { adapter, resetThread }, children: _jsx(AssistantRuntimeProvider, { runtime: runtime, children: _jsxs(TooltipProvider, { children: [_jsx(ShowDocumentToolUI, {}), _jsx(ClientToolExecutor, {}), children] }) }) }));
|
|
71
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Layers3 } from "lucide-react";
|
|
3
|
+
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
4
|
+
import { useThemeRuntime } from "./theme-runtime-provider.js";
|
|
5
|
+
export function AppBrand({ showName = true, className, iconClassName, labelClassName, }) {
|
|
6
|
+
const runtime = useThemeRuntime();
|
|
7
|
+
const brandName = runtime.branding.appName || "Foundry UI";
|
|
8
|
+
const logoUrl = runtime.mode === "dark"
|
|
9
|
+
? runtime.branding.darkLogoUrl || runtime.branding.logoUrl
|
|
10
|
+
: runtime.branding.logoUrl || runtime.branding.darkLogoUrl;
|
|
11
|
+
const shouldShowName = showName && !logoUrl;
|
|
12
|
+
return (_jsxs("span", { className: cn("flex items-center gap-2", className), children: [logoUrl ? (_jsx("img", { src: logoUrl, alt: brandName, className: cn("h-5 w-auto max-w-[7rem] shrink-0 object-contain", iconClassName) })) : (_jsx(Layers3, { className: cn("size-5 shrink-0 text-primary", iconClassName) })), shouldShowName && _jsx("span", { className: cn("truncate", labelClassName), children: brandName })] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default starter prompts shown on empty state. Override per-app by passing
|
|
3
|
+
* a `starterSuggestions` prop to `<ChatContent>`. An empty array (`[]`)
|
|
4
|
+
* suppresses the suggestion buttons entirely.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_STARTER_SUGGESTIONS: string[];
|
|
7
|
+
export interface ChatContentProps {
|
|
8
|
+
/**
|
|
9
|
+
* Starter prompts shown when the thread is empty. Defaults to
|
|
10
|
+
* `DEFAULT_STARTER_SUGGESTIONS`. Pass an empty array to hide the
|
|
11
|
+
* suggestion buttons entirely. Customer apps (e.g. ANDRITZ Contracts)
|
|
12
|
+
* can pass a workspace-aware list derived from app context.
|
|
13
|
+
*/
|
|
14
|
+
starterSuggestions?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Show the page pin button in the composer. Defaults to false so generic
|
|
17
|
+
* starter apps only show attachment, input, and send controls. Apps with
|
|
18
|
+
* page-aware workflows can opt in explicitly.
|
|
19
|
+
*/
|
|
20
|
+
showPinButton?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reusable chat content — used by ChatPage and ChatBubble.
|
|
24
|
+
* Renders inside a ThreadPrimitive.Root context.
|
|
25
|
+
*/
|
|
26
|
+
export declare function ChatContent({ starterSuggestions, showPinButton, }?: ChatContentProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useCallback } from "react";
|
|
3
|
+
import { ThreadPrimitive, ComposerPrimitive, MessagePrimitive, ActionBarPrimitive, BranchPickerPrimitive, useAuiState, useComposerRuntime, useThreadViewport, } from "@assistant-ui/react";
|
|
4
|
+
import { ArrowUp, Copy, Check, RefreshCw, PencilIcon, ChevronLeft, ChevronRight, ChevronDown, Square, Bot, Paperclip, Minimize2, PanelRightClose, PanelRightOpen, } from "lucide-react";
|
|
5
|
+
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
6
|
+
import { Button } from "@iloveagents/foundry-web-primitives";
|
|
7
|
+
import { TooltipIconButton } from "./tooltip-icon-button.js";
|
|
8
|
+
import { userAttachmentComponents, composerAttachmentComponents } from "./chat-attachments.js";
|
|
9
|
+
import { SentContextBadges, ComposerContextBadges } from "./context-badges.js";
|
|
10
|
+
import { ContextPins } from "./context-bar.js";
|
|
11
|
+
import { ComposerSubmitBridge } from "./composer-submit-bridge.js";
|
|
12
|
+
import { useChatBubbleStore } from "../lib/chat-bubble-store.js";
|
|
13
|
+
import { MarkdownText } from "./markdown-text.js";
|
|
14
|
+
import { ToolFallback } from "./tool-fallback.js";
|
|
15
|
+
import { LoadingIndicator, LoadingBar } from "./loading-indicator.js";
|
|
16
|
+
import { MessageTimestamp } from "./message-timestamp.js";
|
|
17
|
+
import { MessageError } from "./message-error.js";
|
|
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" }) }) })] }));
|
|
19
|
+
const UserMessage = () => {
|
|
20
|
+
const messageId = useAuiState((s) => s.message.id);
|
|
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, {}) })] })] }) }));
|
|
22
|
+
};
|
|
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" }) }) }) }));
|
|
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: {
|
|
25
|
+
Text: MarkdownText,
|
|
26
|
+
tools: { Fallback: ToolFallback },
|
|
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, {})] })] }) }));
|
|
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" }) }) })] }));
|
|
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" }) })] })] }) }) }) }));
|
|
30
|
+
const ComposerActionButton = () => {
|
|
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" }) }) }) })] }));
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Default starter prompts shown on empty state. Override per-app by passing
|
|
35
|
+
* a `starterSuggestions` prop to `<ChatContent>`. An empty array (`[]`)
|
|
36
|
+
* suppresses the suggestion buttons entirely.
|
|
37
|
+
*/
|
|
38
|
+
export const DEFAULT_STARTER_SUGGESTIONS = [
|
|
39
|
+
"Say hello world",
|
|
40
|
+
"What tools can you use?",
|
|
41
|
+
"Help me get started",
|
|
42
|
+
];
|
|
43
|
+
const ScrollToBottomButton = () => {
|
|
44
|
+
const isAtBottom = useThreadViewport((s) => s.isAtBottom);
|
|
45
|
+
const scrollToBottom = useThreadViewport((s) => s.scrollToBottom);
|
|
46
|
+
if (isAtBottom)
|
|
47
|
+
return null;
|
|
48
|
+
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" }) }));
|
|
49
|
+
};
|
|
50
|
+
const DropZone = ({ children }) => {
|
|
51
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
52
|
+
const dragCounter = useRef(0);
|
|
53
|
+
const composerRuntime = useComposerRuntime();
|
|
54
|
+
const handleDragEnter = useCallback((e) => {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
dragCounter.current++;
|
|
57
|
+
if (dragCounter.current === 1)
|
|
58
|
+
setIsDragging(true);
|
|
59
|
+
}, []);
|
|
60
|
+
const handleDragLeave = useCallback((e) => {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
dragCounter.current--;
|
|
63
|
+
if (dragCounter.current === 0)
|
|
64
|
+
setIsDragging(false);
|
|
65
|
+
}, []);
|
|
66
|
+
const handleDragOver = useCallback((e) => {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
}, []);
|
|
69
|
+
const handleDrop = useCallback(async (e) => {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
dragCounter.current = 0;
|
|
72
|
+
setIsDragging(false);
|
|
73
|
+
const files = Array.from(e.dataTransfer.files);
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
await composerRuntime.addAttachment(file);
|
|
76
|
+
}
|
|
77
|
+
}, [composerRuntime]);
|
|
78
|
+
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" }) }))] }));
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Reusable chat content — used by ChatPage and ChatBubble.
|
|
82
|
+
* Renders inside a ThreadPrimitive.Root context.
|
|
83
|
+
*/
|
|
84
|
+
export function ChatContent({ starterSuggestions = DEFAULT_STARTER_SUGGESTIONS, showPinButton = false, } = {}) {
|
|
85
|
+
const isExpanded = useChatBubbleStore((s) => s.isExpanded);
|
|
86
|
+
const collapseChat = useChatBubbleStore((s) => s.collapse);
|
|
87
|
+
const showPagePanel = useChatBubbleStore((s) => s.showPagePanel);
|
|
88
|
+
const togglePagePanel = useChatBubbleStore((s) => s.togglePagePanel);
|
|
89
|
+
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: [_jsxs(ThreadPrimitive.Viewport, { className: "absolute inset-0 overflow-y-auto bg-background", 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", children: [_jsx(ThreadPrimitive.Messages, { components: {
|
|
90
|
+
UserMessage,
|
|
91
|
+
EditComposer,
|
|
92
|
+
AssistantMessage,
|
|
93
|
+
} }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingIndicator, {}) })] }) })] }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsx(ScrollToBottomButton, {}) })] }), _jsxs("div", { className: "shrink-0 bg-background", children: [_jsx(ThreadPrimitive.If, { running: false, children: _jsx("div", { className: "border-t border-border" }) }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingBar, {}) }), _jsx("div", { className: "mx-auto max-w-3xl px-4 py-4", children: _jsxs(ComposerPrimitive.Root, { className: cn("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, {}), _jsx(ComposerPrimitive.Attachments, { components: composerAttachmentComponents }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "flex items-center -space-x-1 shrink-0", children: [_jsx(ComposerPrimitive.AddAttachment, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Attach file", variant: "ghost", className: "shrink-0", children: _jsx(Paperclip, { className: "size-5" }) }) }), _jsx(ContextPins, { showPinButton: showPinButton })] }), _jsx(ComposerPrimitive.Input, { autoFocus: true, placeholder: "Message Assistant...", rows: 1, className: cn("flex-1 bg-transparent", "py-2 text-base", "text-foreground outline-none", "placeholder:text-muted-foreground", "resize-none max-h-32") }), _jsx(ComposerActionButton, {})] })] }) })] })] })] }));
|
|
94
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment rendering components for chat messages and composer.
|
|
3
|
+
*
|
|
4
|
+
* Following the official assistant-ui pattern, attachment components
|
|
5
|
+
* live in a separate file from the main thread components.
|
|
6
|
+
*
|
|
7
|
+
* @see https://www.assistant-ui.com/ - assistant-ui Documentation
|
|
8
|
+
*/
|
|
9
|
+
import { type FC } from "react";
|
|
10
|
+
/** Renders a pending file attachment chip in the composer with a remove button. */
|
|
11
|
+
export declare const ComposerAttachment: FC;
|
|
12
|
+
/** Renders a file badge on sent user messages. */
|
|
13
|
+
export declare const UserMessageAttachment: FC;
|
|
14
|
+
/** Component map for MessagePrimitive.Attachments on user messages. */
|
|
15
|
+
export declare const userAttachmentComponents: {
|
|
16
|
+
Document: FC;
|
|
17
|
+
Image: FC;
|
|
18
|
+
File: FC;
|
|
19
|
+
};
|
|
20
|
+
/** Component map for ComposerPrimitive.Attachments in the composer. */
|
|
21
|
+
export declare const composerAttachmentComponents: {
|
|
22
|
+
Document: FC;
|
|
23
|
+
Image: FC;
|
|
24
|
+
File: FC;
|
|
25
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AttachmentPrimitive } from "@assistant-ui/react";
|
|
3
|
+
import { FileText, X } from "lucide-react";
|
|
4
|
+
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
5
|
+
/** Renders a pending file attachment chip in the composer with a remove button. */
|
|
6
|
+
export const ComposerAttachment = () => (_jsxs(AttachmentPrimitive.Root, { className: cn("inline-flex items-center gap-2 max-w-[250px]", "rounded-lg border border-border bg-muted/50", "px-3 py-2"), children: [_jsx(FileText, { className: "size-4 text-muted-foreground flex-shrink-0" }), _jsx("span", { className: "text-sm truncate", children: _jsx(AttachmentPrimitive.Name, {}) }), _jsx(AttachmentPrimitive.Remove, { asChild: true, children: _jsx("button", { type: "button", className: "p-0.5 rounded hover:bg-muted flex-shrink-0", "aria-label": "Remove attachment", children: _jsx(X, { className: "size-3.5" }) }) })] }));
|
|
7
|
+
/** Renders a file badge on sent user messages. */
|
|
8
|
+
export const UserMessageAttachment = () => (_jsxs(AttachmentPrimitive.Root, { className: cn("inline-flex items-center gap-2 max-w-[250px]", "rounded-lg border border-border/50 bg-muted/30", "px-3 py-1.5"), children: [_jsx(FileText, { className: "size-3.5 text-muted-foreground flex-shrink-0" }), _jsx("span", { className: "text-xs truncate", children: _jsx(AttachmentPrimitive.Name, {}) })] }));
|
|
9
|
+
/** Component map for MessagePrimitive.Attachments on user messages. */
|
|
10
|
+
export const userAttachmentComponents = {
|
|
11
|
+
Document: UserMessageAttachment,
|
|
12
|
+
Image: UserMessageAttachment,
|
|
13
|
+
File: UserMessageAttachment,
|
|
14
|
+
};
|
|
15
|
+
/** Component map for ComposerPrimitive.Attachments in the composer. */
|
|
16
|
+
export const composerAttachmentComponents = {
|
|
17
|
+
Document: ComposerAttachment,
|
|
18
|
+
Image: ComposerAttachment,
|
|
19
|
+
File: ComposerAttachment,
|
|
20
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ChatBubble(): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
3
|
+
import { ThreadPrimitive, ComposerPrimitive, MessagePrimitive, ActionBarPrimitive, useAuiState, useThreadViewport, } from "@assistant-ui/react";
|
|
4
|
+
import { MessageSquare, X, Maximize2, SquarePen, ArrowUp, Square, ChevronDown, Copy, Check, Paperclip, RefreshCw, } from "lucide-react";
|
|
5
|
+
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
6
|
+
import { Button } from "@iloveagents/foundry-web-primitives";
|
|
7
|
+
import { TooltipIconButton } from "./tooltip-icon-button.js";
|
|
8
|
+
import { ContextPins } from "./context-bar.js";
|
|
9
|
+
import { ComposerContextBadges, SentContextBadges } from "./context-badges.js";
|
|
10
|
+
import { ComposerSubmitBridge } from "./composer-submit-bridge.js";
|
|
11
|
+
import { MarkdownText } from "./markdown-text.js";
|
|
12
|
+
import { LoadingIndicator } from "./loading-indicator.js";
|
|
13
|
+
import { MessageTimestamp } from "./message-timestamp.js";
|
|
14
|
+
import { MessageError } from "./message-error.js";
|
|
15
|
+
import { useAppStore } from "../lib/app-store.js";
|
|
16
|
+
import { useChatBubbleStore } from "../lib/chat-bubble-store.js";
|
|
17
|
+
import { useNewConversation } from "../lib/use-new-conversation.js";
|
|
18
|
+
import { RAIL_WIDTH, useSidebarStore } from "../lib/sidebar-store.js";
|
|
19
|
+
const CHAT_BUBBLE_INSET = 8;
|
|
20
|
+
function BubbleAssistantMessage() {
|
|
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: {
|
|
22
|
+
Text: MarkdownText,
|
|
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, {})] })] }));
|
|
24
|
+
}
|
|
25
|
+
function BubbleUserMessage() {
|
|
26
|
+
const messageId = useAuiState((s) => s.message.id);
|
|
27
|
+
return (_jsx(MessagePrimitive.Root, { className: "w-full mb-2", children: _jsxs("div", { className: "flex flex-col items-end gap-1", children: [_jsx(SentContextBadges, { messageId: messageId }), _jsx("div", { className: "max-w-[85%] bg-muted rounded-2xl px-3 py-2 text-sm text-foreground", children: _jsx(MessagePrimitive.Content, {}) })] }) }));
|
|
28
|
+
}
|
|
29
|
+
function BubbleScrollToBottomButton() {
|
|
30
|
+
const isAtBottom = useThreadViewport((s) => s.isAtBottom);
|
|
31
|
+
const scrollToBottom = useThreadViewport((s) => s.scrollToBottom);
|
|
32
|
+
if (isAtBottom)
|
|
33
|
+
return null;
|
|
34
|
+
return (_jsx(Button, { variant: "outline", size: "icon", className: "absolute bottom-2 left-1/2 -translate-x-1/2 z-10 rounded-full shadow-md size-7", onClick: () => scrollToBottom({ behavior: "smooth" }), children: _jsx(ChevronDown, { className: "size-3" }) }));
|
|
35
|
+
}
|
|
36
|
+
export function ChatBubble() {
|
|
37
|
+
const isOpen = useChatBubbleStore((s) => s.isOpen);
|
|
38
|
+
const isExpanded = useChatBubbleStore((s) => s.isExpanded);
|
|
39
|
+
const openBubble = useChatBubbleStore((s) => s.open);
|
|
40
|
+
const closeBubble = useChatBubbleStore((s) => s.close);
|
|
41
|
+
const expandChat = useChatBubbleStore((s) => s.expand);
|
|
42
|
+
const startNewThread = useNewConversation({ navigateToChat: false, preserveNavigation: true });
|
|
43
|
+
const isSidebarOpen = useSidebarStore((s) => s.isOpen);
|
|
44
|
+
const sidebarWidth = useSidebarStore((s) => s.width);
|
|
45
|
+
const pageLabel = useAppStore((s) => s.navContext.label);
|
|
46
|
+
const currentPage = useAppStore((s) => s.currentPage);
|
|
47
|
+
const threadActive = useAppStore((s) => s.threadActive);
|
|
48
|
+
// Draggable position: null = use default CSS anchored with equal left/bottom inset.
|
|
49
|
+
const [dragPos, setDragPos] = useState(null);
|
|
50
|
+
const dragStart = useRef(null);
|
|
51
|
+
const panelRef = useRef(null);
|
|
52
|
+
// Close and reset drag position so it reopens at the default spot
|
|
53
|
+
const handleClose = () => {
|
|
54
|
+
setDragPos(null);
|
|
55
|
+
closeBubble();
|
|
56
|
+
};
|
|
57
|
+
const onHeaderPointerDown = (e) => {
|
|
58
|
+
// Don't start drag when clicking header buttons
|
|
59
|
+
if (e.target.closest("button"))
|
|
60
|
+
return;
|
|
61
|
+
const panel = panelRef.current;
|
|
62
|
+
if (!panel)
|
|
63
|
+
return;
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
const rect = panel.getBoundingClientRect();
|
|
66
|
+
dragStart.current = { mx: e.clientX, my: e.clientY, px: rect.left, py: rect.top };
|
|
67
|
+
// Capture the initial position so first pointer-move works correctly
|
|
68
|
+
setDragPos({ x: rect.left, y: rect.top });
|
|
69
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
70
|
+
};
|
|
71
|
+
const onHeaderPointerMove = (e) => {
|
|
72
|
+
if (!dragStart.current)
|
|
73
|
+
return;
|
|
74
|
+
const panel = panelRef.current;
|
|
75
|
+
const panelW = panel?.offsetWidth ?? 380;
|
|
76
|
+
const panelH = panel?.offsetHeight ?? 600;
|
|
77
|
+
const dx = e.clientX - dragStart.current.mx;
|
|
78
|
+
const dy = e.clientY - dragStart.current.my;
|
|
79
|
+
// Clamp within viewport so the panel can't be dragged fully off-screen
|
|
80
|
+
const x = Math.max(0, Math.min(window.innerWidth - panelW, dragStart.current.px + dx));
|
|
81
|
+
const y = Math.max(0, Math.min(window.innerHeight - panelH, dragStart.current.py + dy));
|
|
82
|
+
setDragPos({ x, y });
|
|
83
|
+
};
|
|
84
|
+
const onHeaderPointerUp = (e) => {
|
|
85
|
+
if (!dragStart.current)
|
|
86
|
+
return;
|
|
87
|
+
dragStart.current = null;
|
|
88
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
89
|
+
};
|
|
90
|
+
const onHeaderLostPointerCapture = () => {
|
|
91
|
+
dragStart.current = null;
|
|
92
|
+
};
|
|
93
|
+
// Hide bubble on chat page (already full-screen) and in expanded mode
|
|
94
|
+
if (currentPage === "/" || isExpanded)
|
|
95
|
+
return null;
|
|
96
|
+
return (_jsxs(_Fragment, { children: [!isOpen && (_jsx("div", { "aria-hidden": "true", className: cn("pointer-events-none fixed bottom-0 left-0 z-40 hidden md:block", "border-r border-sidebar-border", "bg-gradient-to-b from-sidebar/0 via-sidebar/88 to-sidebar/96", "supports-[backdrop-filter]:backdrop-blur"), style: {
|
|
97
|
+
width: isSidebarOpen ? sidebarWidth : RAIL_WIDTH,
|
|
98
|
+
height: 64,
|
|
99
|
+
} })), !isOpen && (_jsx("button", { type: "button", onClick: openBubble, className: cn("fixed bottom-4 z-50", "size-10 rounded-full", "bg-primary text-primary-foreground shadow-lg", "flex items-center justify-center", "hover:bg-primary/90 transition-all duration-200", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"), style: { left: CHAT_BUBBLE_INSET, bottom: CHAT_BUBBLE_INSET }, "aria-label": "Open chat", children: _jsx(MessageSquare, { className: "size-4" }) })), isOpen && (_jsxs("div", { ref: panelRef, className: cn("fixed z-50", "rounded-2xl border border-border", "bg-background shadow-2xl", "flex flex-col overflow-hidden", "w-95 h-150"), style: dragPos
|
|
100
|
+
? { left: dragPos.x, top: dragPos.y }
|
|
101
|
+
: { 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(MessageSquare, { 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: [_jsxs(ThreadPrimitive.Viewport, { className: "absolute inset-0 overflow-y-auto", 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", children: [_jsx(ThreadPrimitive.Messages, { components: {
|
|
102
|
+
UserMessage: BubbleUserMessage,
|
|
103
|
+
AssistantMessage: BubbleAssistantMessage,
|
|
104
|
+
} }), _jsx(ThreadPrimitive.If, { running: true, children: _jsx(LoadingIndicator, {}) })] }) })] }), _jsx(ThreadPrimitive.If, { empty: false, children: _jsx(BubbleScrollToBottomButton, {}) })] }), _jsx("div", { className: "shrink-0 border-t border-border px-3 py-3", children: _jsxs(ComposerPrimitive.Root, { className: cn("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(ComposerSubmitBridge, {}), _jsx(ComposerContextBadges, {}), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: "flex items-center -space-x-1 shrink-0", children: [_jsx(ComposerPrimitive.AddAttachment, { asChild: true, children: _jsx(TooltipIconButton, { tooltip: "Attach file", size: "icon", className: "size-7 shrink-0", children: _jsx(Paperclip, { className: "size-3.5" }) }) }), _jsx(ContextPins, { compact: true })] }), _jsx(ComposerPrimitive.Input, { autoFocus: true, placeholder: "Message...", 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: 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" }) }) }) }), _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" }) }) }) })] })] }) })] })] }))] }));
|
|
105
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Link } from "react-router";
|
|
3
|
+
import { SquarePen, Menu } from "lucide-react";
|
|
4
|
+
import { cn } from "@iloveagents/foundry-web-primitives";
|
|
5
|
+
import { AppBrand } from "./app-brand.js";
|
|
6
|
+
import { TooltipIconButton } from "./tooltip-icon-button.js";
|
|
7
|
+
import { ThemeToggle } from "./theme-toggle.js";
|
|
8
|
+
import { UserMenu } from "./user-menu.js";
|
|
9
|
+
import { useSidebarStore } from "../lib/sidebar-store.js";
|
|
10
|
+
import { useNewConversation } from "../lib/use-new-conversation.js";
|
|
11
|
+
export const ChatHeader = () => {
|
|
12
|
+
const handleNewConversation = useNewConversation();
|
|
13
|
+
const toggleMobile = useSidebarStore((s) => s.toggleMobile);
|
|
14
|
+
const isSidebarOpen = useSidebarStore((s) => s.isOpen);
|
|
15
|
+
return (_jsx("div", { className: cn("shrink-0", "bg-background border-b border-border"), children: _jsxs("div", { className: "px-4 py-3 flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(TooltipIconButton, { tooltip: "Open sidebar", onClick: toggleMobile, className: "md:hidden", children: _jsx(Menu, { className: "size-5" }) }), _jsx(Link, { to: "/", className: cn("flex items-center gap-2.5 hover:opacity-80 transition-opacity", isSidebarOpen && "md:hidden"), children: _jsx(AppBrand, { labelClassName: "text-sm font-semibold text-foreground" }) })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(ThemeToggle, {}), _jsx(TooltipIconButton, { tooltip: "New Thread", size: "icon", onClick: handleNewConversation, children: _jsx(SquarePen, { className: "size-4 text-primary" }) }), _jsx(UserMenu, {})] })] }) }));
|
|
16
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client Tool Executor — registers custom UIs for client-side tools.
|
|
3
|
+
*
|
|
4
|
+
* Tool execution happens in the AGUIRunner (client-side tools intercepted at
|
|
5
|
+
* `tool-call-end`). This component only registers the UI rendering and
|
|
6
|
+
* injects the navigate function (which requires React Router context).
|
|
7
|
+
*
|
|
8
|
+
* Modern assistant-ui v0.12 pattern: each tool UI is a top-level
|
|
9
|
+
* `makeAssistantToolUI(...)` component. We mount them as children of
|
|
10
|
+
* `<ClientToolExecutor>` inside the AssistantRuntimeProvider tree.
|
|
11
|
+
*/
|
|
12
|
+
export declare function ClientToolExecutor(): import("react/jsx-runtime").JSX.Element;
|