@iloveagents/foundry-web-ui 0.3.0 → 0.4.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 +14 -5
- 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 +93 -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 +103 -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 +4 -0
- package/dist/components/loading-indicator.js +22 -0
- package/dist/components/markdown-text.d.ts +13 -0
- package/dist/components/markdown-text.js +221 -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 +373 -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 -203
- 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 -273
- 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 -412
- 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
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Store — unified shared state for the AG-UI multi-page application.
|
|
3
|
+
*
|
|
4
|
+
* - Navigation context (synced from nav store via app-layout)
|
|
5
|
+
* - Persistent vs ephemeral context lifecycle
|
|
6
|
+
* - Shared UI state (selectedItemId, content, highlights)
|
|
7
|
+
* - Strict AG-UI state projection sent to the agent on every request
|
|
8
|
+
*/
|
|
9
|
+
import { create } from "zustand";
|
|
10
|
+
const MAX_ITEMS = 10;
|
|
11
|
+
const MAX_LABEL_LENGTH = 500;
|
|
12
|
+
const MAX_CONTEXT_VALUE_LENGTH = 2000;
|
|
13
|
+
const TRUNCATION_SUFFIX = "...";
|
|
14
|
+
const DEFAULT_NAV_CONTEXT = {
|
|
15
|
+
group: null,
|
|
16
|
+
groupDescription: null,
|
|
17
|
+
groupMeta: {},
|
|
18
|
+
label: "Chat",
|
|
19
|
+
description: null,
|
|
20
|
+
meta: {},
|
|
21
|
+
contextInstructions: null,
|
|
22
|
+
};
|
|
23
|
+
function refDedupKey(payload) {
|
|
24
|
+
const selectionScope = typeof payload.meta?.selectionScope === "string" ? payload.meta.selectionScope : "block";
|
|
25
|
+
const targetPath = typeof payload.target?.path === "string"
|
|
26
|
+
? payload.target.path
|
|
27
|
+
: typeof payload.meta?.targetPath === "string"
|
|
28
|
+
? payload.meta.targetPath
|
|
29
|
+
: undefined;
|
|
30
|
+
if (selectionScope === "atom" && targetPath) {
|
|
31
|
+
return `${payload.refType}:${payload.refId}:atom:${targetPath}`;
|
|
32
|
+
}
|
|
33
|
+
if (selectionScope === "text") {
|
|
34
|
+
return `${payload.refType}:${payload.refId}:text:${payload.preview ?? ""}`;
|
|
35
|
+
}
|
|
36
|
+
return `${payload.refType}:${payload.refId}:block`;
|
|
37
|
+
}
|
|
38
|
+
function compactContextValue(value) {
|
|
39
|
+
if (typeof value === "string" && value.length > MAX_CONTEXT_VALUE_LENGTH) {
|
|
40
|
+
return `${value.slice(0, MAX_CONTEXT_VALUE_LENGTH - TRUNCATION_SUFFIX.length)}${TRUNCATION_SUFFIX}`;
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
function compactReferenceTarget(target) {
|
|
45
|
+
if (!target)
|
|
46
|
+
return undefined;
|
|
47
|
+
return {
|
|
48
|
+
...target,
|
|
49
|
+
currentValue: compactContextValue(target.currentValue),
|
|
50
|
+
proposedValue: compactContextValue(target.proposedValue),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export const useAppStore = create((set, get) => ({
|
|
54
|
+
// Thread state
|
|
55
|
+
threadActive: false,
|
|
56
|
+
markThreadActive: () => set({ threadActive: true }),
|
|
57
|
+
// Current page
|
|
58
|
+
currentPage: "/chat",
|
|
59
|
+
setCurrentPage: (page) => {
|
|
60
|
+
const prev = get().currentPage;
|
|
61
|
+
if (prev === page)
|
|
62
|
+
return;
|
|
63
|
+
// Clear ephemeral context on navigation
|
|
64
|
+
set({
|
|
65
|
+
currentPage: page,
|
|
66
|
+
contextItems: get().contextItems.filter((i) => i.persistence !== "ephemeral"),
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
// Navigation context
|
|
70
|
+
navContext: DEFAULT_NAV_CONTEXT,
|
|
71
|
+
setNavContext: (ctx) => set({ navContext: ctx }),
|
|
72
|
+
// Shared UI state
|
|
73
|
+
selectedItemId: null,
|
|
74
|
+
setSelectedItemId: (id) => set({ selectedItemId: id }),
|
|
75
|
+
content: {},
|
|
76
|
+
updateContentField: (key, value) => set({ content: { ...get().content, [key]: value } }),
|
|
77
|
+
clearContent: () => set({ content: {} }),
|
|
78
|
+
highlights: [],
|
|
79
|
+
addHighlight: (id) => {
|
|
80
|
+
const { highlights } = get();
|
|
81
|
+
if (!highlights.includes(id)) {
|
|
82
|
+
set({ highlights: [...highlights, id] });
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
removeHighlight: (id) => set({ highlights: get().highlights.filter((h) => h !== id) }),
|
|
86
|
+
clearHighlights: () => set({ highlights: [] }),
|
|
87
|
+
// Context items
|
|
88
|
+
contextItems: [],
|
|
89
|
+
sentContext: {},
|
|
90
|
+
addContextItem: (item) => {
|
|
91
|
+
const { contextItems } = get();
|
|
92
|
+
// Deduplicate by type-specific key
|
|
93
|
+
const isDuplicate = contextItems.some((existing) => {
|
|
94
|
+
if (item.type === "page" && existing.type === "page") {
|
|
95
|
+
const a = item.payload;
|
|
96
|
+
const b = existing.payload;
|
|
97
|
+
return a.path === b.path;
|
|
98
|
+
}
|
|
99
|
+
if (item.type === "ref" && existing.type === "ref") {
|
|
100
|
+
const a = item.payload;
|
|
101
|
+
const b = existing.payload;
|
|
102
|
+
return refDedupKey(a) === refDedupKey(b);
|
|
103
|
+
}
|
|
104
|
+
// selection / note: dedup by label + sourcePage
|
|
105
|
+
return existing.label === item.label && existing.sourcePage === item.sourcePage;
|
|
106
|
+
});
|
|
107
|
+
if (isDuplicate)
|
|
108
|
+
return;
|
|
109
|
+
if (contextItems.length >= MAX_ITEMS)
|
|
110
|
+
return;
|
|
111
|
+
const label = item.label.length > MAX_LABEL_LENGTH
|
|
112
|
+
? item.label.slice(0, MAX_LABEL_LENGTH) + "..."
|
|
113
|
+
: item.label;
|
|
114
|
+
set({
|
|
115
|
+
contextItems: [
|
|
116
|
+
...contextItems,
|
|
117
|
+
{ ...item, label, id: crypto.randomUUID(), createdAt: Date.now() },
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
removeContextItem: (id) => set({ contextItems: get().contextItems.filter((i) => i.id !== id) }),
|
|
122
|
+
promoteContextItem: (id) => set({
|
|
123
|
+
contextItems: get().contextItems.map((i) => i.id === id ? { ...i, persistence: "persistent" } : i),
|
|
124
|
+
}),
|
|
125
|
+
clearAllContext: () => set({ contextItems: [], sentContext: {} }),
|
|
126
|
+
consumeContextForMessage: (messageId) => {
|
|
127
|
+
const { contextItems, sentContext } = get();
|
|
128
|
+
if (contextItems.length === 0)
|
|
129
|
+
return [];
|
|
130
|
+
const snapshot = [...contextItems];
|
|
131
|
+
// Keep persistent, remove ephemeral
|
|
132
|
+
set({
|
|
133
|
+
contextItems: contextItems.filter((i) => i.persistence === "persistent"),
|
|
134
|
+
sentContext: { ...sentContext, [messageId]: snapshot },
|
|
135
|
+
});
|
|
136
|
+
return snapshot;
|
|
137
|
+
},
|
|
138
|
+
// AG-UI state projection
|
|
139
|
+
getAgentState: () => {
|
|
140
|
+
const s = get();
|
|
141
|
+
return {
|
|
142
|
+
navigation: {
|
|
143
|
+
group: s.navContext.group,
|
|
144
|
+
groupDescription: s.navContext.groupDescription,
|
|
145
|
+
groupMeta: s.navContext.groupMeta,
|
|
146
|
+
page: s.currentPage,
|
|
147
|
+
label: s.navContext.label,
|
|
148
|
+
description: s.navContext.description,
|
|
149
|
+
meta: s.navContext.meta,
|
|
150
|
+
contextInstructions: s.navContext.contextInstructions,
|
|
151
|
+
},
|
|
152
|
+
selectedItemId: s.selectedItemId,
|
|
153
|
+
content: s.content,
|
|
154
|
+
highlights: s.highlights,
|
|
155
|
+
contextItems: s.contextItems.map(({ type, label, payload, sourcePage }) => {
|
|
156
|
+
switch (payload.kind) {
|
|
157
|
+
case "text":
|
|
158
|
+
return {
|
|
159
|
+
type,
|
|
160
|
+
label: label.slice(0, 200),
|
|
161
|
+
text: payload.text.slice(0, 500),
|
|
162
|
+
sourcePage,
|
|
163
|
+
};
|
|
164
|
+
case "page":
|
|
165
|
+
return { type, label, path: payload.path, sourcePage };
|
|
166
|
+
case "ref":
|
|
167
|
+
return {
|
|
168
|
+
type,
|
|
169
|
+
label: label.slice(0, 200),
|
|
170
|
+
refType: payload.refType,
|
|
171
|
+
refId: payload.refId,
|
|
172
|
+
preview: payload.preview?.slice(0, 100),
|
|
173
|
+
target: compactReferenceTarget(payload.target),
|
|
174
|
+
meta: payload.meta,
|
|
175
|
+
targetPath: typeof payload.target?.path === "string"
|
|
176
|
+
? payload.target.path
|
|
177
|
+
: typeof payload.meta?.targetPath === "string"
|
|
178
|
+
? payload.meta.targetPath
|
|
179
|
+
: undefined,
|
|
180
|
+
sourcePage,
|
|
181
|
+
};
|
|
182
|
+
default:
|
|
183
|
+
return { type, label, sourcePage };
|
|
184
|
+
}
|
|
185
|
+
}),
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
// Full reset
|
|
189
|
+
resetAll: () => set({
|
|
190
|
+
threadActive: false,
|
|
191
|
+
currentPage: "/chat",
|
|
192
|
+
navContext: DEFAULT_NAV_CONTEXT,
|
|
193
|
+
selectedItemId: null,
|
|
194
|
+
content: {},
|
|
195
|
+
highlights: [],
|
|
196
|
+
contextItems: [],
|
|
197
|
+
sentContext: {},
|
|
198
|
+
}),
|
|
199
|
+
}));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File attachment adapter for assistant-ui.
|
|
3
|
+
*
|
|
4
|
+
* Reads image files client-side as base64 and produces FileMessagePart content.
|
|
5
|
+
* Non-image documents are represented as text metadata so starter apps don't
|
|
6
|
+
* send malformed multimodal payloads to providers that require a separate file
|
|
7
|
+
* upload or document extraction pipeline.
|
|
8
|
+
*
|
|
9
|
+
* Supported: images (PNG, JPEG, WEBP, GIF). PDFs/DOCX can be attached, but the
|
|
10
|
+
* default starter only sends their metadata to the agent.
|
|
11
|
+
*/
|
|
12
|
+
import type { AttachmentAdapter, PendingAttachment, CompleteAttachment, Attachment } from "@assistant-ui/react";
|
|
13
|
+
export declare class FileAttachmentAdapter implements AttachmentAdapter {
|
|
14
|
+
accept: string;
|
|
15
|
+
add({ file }: {
|
|
16
|
+
file: File;
|
|
17
|
+
}): Promise<PendingAttachment>;
|
|
18
|
+
send(attachment: PendingAttachment): Promise<CompleteAttachment>;
|
|
19
|
+
remove(_attachment: Attachment): Promise<void>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File attachment adapter for assistant-ui.
|
|
3
|
+
*
|
|
4
|
+
* Reads image files client-side as base64 and produces FileMessagePart content.
|
|
5
|
+
* Non-image documents are represented as text metadata so starter apps don't
|
|
6
|
+
* send malformed multimodal payloads to providers that require a separate file
|
|
7
|
+
* upload or document extraction pipeline.
|
|
8
|
+
*
|
|
9
|
+
* Supported: images (PNG, JPEG, WEBP, GIF). PDFs/DOCX can be attached, but the
|
|
10
|
+
* default starter only sends their metadata to the agent.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20 MB
|
|
13
|
+
/** Infer MIME type from extension when browser reports empty file.type (common for .docx) */
|
|
14
|
+
const EXTENSION_MIME = {
|
|
15
|
+
".pdf": "application/pdf",
|
|
16
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
17
|
+
".png": "image/png",
|
|
18
|
+
".jpg": "image/jpeg",
|
|
19
|
+
".jpeg": "image/jpeg",
|
|
20
|
+
".gif": "image/gif",
|
|
21
|
+
".webp": "image/webp",
|
|
22
|
+
};
|
|
23
|
+
function resolveMimeType(file) {
|
|
24
|
+
if (file.type)
|
|
25
|
+
return file.type;
|
|
26
|
+
const ext = file.name.slice(file.name.lastIndexOf(".")).toLowerCase();
|
|
27
|
+
return EXTENSION_MIME[ext] ?? "application/octet-stream";
|
|
28
|
+
}
|
|
29
|
+
function fileToBase64(file) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const reader = new FileReader();
|
|
32
|
+
reader.onload = () => {
|
|
33
|
+
const result = reader.result;
|
|
34
|
+
// Strip the data URI prefix (e.g. "data:image/png;base64,"), keep raw base64
|
|
35
|
+
resolve(result.split(",")[1]);
|
|
36
|
+
};
|
|
37
|
+
reader.onerror = reject;
|
|
38
|
+
reader.readAsDataURL(file);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export class FileAttachmentAdapter {
|
|
42
|
+
constructor() {
|
|
43
|
+
this.accept = "image/*,application/pdf,.pdf,.docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
44
|
+
}
|
|
45
|
+
async add({ file }) {
|
|
46
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
47
|
+
throw new Error(`File too large (${Math.round(file.size / 1024 / 1024)} MB). Max 20 MB.`);
|
|
48
|
+
}
|
|
49
|
+
const mimeType = resolveMimeType(file);
|
|
50
|
+
return {
|
|
51
|
+
id: crypto.randomUUID(),
|
|
52
|
+
type: mimeType.startsWith("image/") ? "image" : "document",
|
|
53
|
+
name: file.name,
|
|
54
|
+
contentType: mimeType,
|
|
55
|
+
file,
|
|
56
|
+
status: { type: "requires-action", reason: "composer-send" },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async send(attachment) {
|
|
60
|
+
const mimeType = attachment.contentType ?? "application/octet-stream";
|
|
61
|
+
if (!mimeType.startsWith("image/")) {
|
|
62
|
+
return {
|
|
63
|
+
...attachment,
|
|
64
|
+
status: { type: "complete" },
|
|
65
|
+
content: [
|
|
66
|
+
{
|
|
67
|
+
type: "text",
|
|
68
|
+
text: `[Attached file: ${attachment.name}` +
|
|
69
|
+
`; type: ${mimeType}` +
|
|
70
|
+
`; size: ${formatFileSize(attachment.file.size)}]`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const base64 = await fileToBase64(attachment.file);
|
|
76
|
+
return {
|
|
77
|
+
...attachment,
|
|
78
|
+
status: { type: "complete" },
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: "file",
|
|
82
|
+
data: base64,
|
|
83
|
+
mimeType,
|
|
84
|
+
filename: attachment.name,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async remove(_attachment) {
|
|
90
|
+
// No-op -- files are read client-side, nothing to clean up
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function formatFileSize(bytes) {
|
|
94
|
+
if (bytes < 1024)
|
|
95
|
+
return `${bytes} B`;
|
|
96
|
+
if (bytes < 1024 * 1024)
|
|
97
|
+
return `${Math.round(bytes / 1024)} KB`;
|
|
98
|
+
return `${Math.round((bytes / 1024 / 1024) * 10) / 10} MB`;
|
|
99
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthProvider — wraps the app with MSAL authentication.
|
|
3
|
+
*
|
|
4
|
+
* Uses MsalProvider's built-in redirect handling (no manual handleRedirectPromise).
|
|
5
|
+
* AuthGuard uses useMsal() + inProgress to avoid redirect loops.
|
|
6
|
+
*
|
|
7
|
+
* The provider expects a fully resolved MSAL configuration.
|
|
8
|
+
* Missing configuration is treated as an application bootstrap error.
|
|
9
|
+
*/
|
|
10
|
+
import { type FC, type PropsWithChildren } from "react";
|
|
11
|
+
import { type MsalAuthConfig } from "@iloveagents/foundry-agent/msal";
|
|
12
|
+
interface AuthProviderProps {
|
|
13
|
+
config: MsalAuthConfig;
|
|
14
|
+
}
|
|
15
|
+
export declare const AuthProvider: FC<PropsWithChildren<AuthProviderProps>>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* AuthProvider — wraps the app with MSAL authentication.
|
|
4
|
+
*
|
|
5
|
+
* Uses MsalProvider's built-in redirect handling (no manual handleRedirectPromise).
|
|
6
|
+
* AuthGuard uses useMsal() + inProgress to avoid redirect loops.
|
|
7
|
+
*
|
|
8
|
+
* The provider expects a fully resolved MSAL configuration.
|
|
9
|
+
* Missing configuration is treated as an application bootstrap error.
|
|
10
|
+
*/
|
|
11
|
+
import { useEffect, useState, } from "react";
|
|
12
|
+
import { authStore, initializeMsal, getMsalInstance, } from "@iloveagents/foundry-agent/msal";
|
|
13
|
+
export const AuthProvider = ({ config, children, }) => {
|
|
14
|
+
const [ready, setReady] = useState(false);
|
|
15
|
+
const [initError, setInitError] = useState(null);
|
|
16
|
+
const [MsalProvider, setMsalProvider] = useState(null);
|
|
17
|
+
const [useMsalHook, setUseMsalHook] = useState(null);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
let cancelled = false;
|
|
20
|
+
(async () => {
|
|
21
|
+
try {
|
|
22
|
+
await initializeMsal(config);
|
|
23
|
+
const msalReact = await import("@azure/msal-react");
|
|
24
|
+
if (cancelled)
|
|
25
|
+
return;
|
|
26
|
+
setMsalProvider(() => msalReact.MsalProvider);
|
|
27
|
+
setUseMsalHook(() => msalReact.useMsal);
|
|
28
|
+
setReady(true);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error("[auth] Failed to initialize MSAL", error);
|
|
32
|
+
if (cancelled)
|
|
33
|
+
return;
|
|
34
|
+
setInitError(error instanceof Error ? error.message : "Failed to initialize authentication.");
|
|
35
|
+
setReady(true);
|
|
36
|
+
}
|
|
37
|
+
})();
|
|
38
|
+
return () => { cancelled = true; };
|
|
39
|
+
}, [config]);
|
|
40
|
+
if (!ready)
|
|
41
|
+
return null;
|
|
42
|
+
if (initError) {
|
|
43
|
+
return (_jsx("div", { className: "flex min-h-dvh items-center justify-center bg-background px-6 text-center", children: _jsxs("div", { className: "max-w-md rounded-2xl border border-border bg-card px-6 py-5 shadow-sm", children: [_jsx("h1", { className: "text-lg font-semibold text-foreground", children: "Authentication unavailable" }), _jsx("p", { className: "mt-2 text-sm text-muted-foreground", children: initError })] }) }));
|
|
44
|
+
}
|
|
45
|
+
if (!MsalProvider || !useMsalHook) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const instance = getMsalInstance();
|
|
49
|
+
return (_jsx(MsalProvider, { instance: instance, children: _jsx(AuthGuard, { useMsalHook: useMsalHook, children: children }) }));
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* AuthGuard — waits for MsalProvider to finish processing redirects,
|
|
53
|
+
* then either syncs the account or triggers loginRedirect.
|
|
54
|
+
*
|
|
55
|
+
* Uses inProgress === "none" to avoid redirect loops (best practice from
|
|
56
|
+
* Microsoft docs — never call loginRedirect while another interaction is active).
|
|
57
|
+
*/
|
|
58
|
+
const AuthGuard = ({ useMsalHook, children, }) => {
|
|
59
|
+
const { instance, accounts, inProgress } = useMsalHook();
|
|
60
|
+
const [authenticated, setAuthenticated] = useState(false);
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
// Wait until MsalProvider finishes processing any redirect or startup
|
|
63
|
+
if (inProgress !== "none")
|
|
64
|
+
return;
|
|
65
|
+
if (accounts.length > 0) {
|
|
66
|
+
const account = accounts[0];
|
|
67
|
+
authStore.getState().signIn({
|
|
68
|
+
name: account.name ?? account.username,
|
|
69
|
+
email: account.username,
|
|
70
|
+
oid: account.localAccountId,
|
|
71
|
+
});
|
|
72
|
+
instance.setActiveAccount(account);
|
|
73
|
+
setAuthenticated(true);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// No account and no interaction in progress — redirect to login.
|
|
77
|
+
// Only request OIDC scopes here. API/SPACES tokens are acquired
|
|
78
|
+
// silently later via acquireTokenSilent (consent is per-resource
|
|
79
|
+
// in Entra ID — including them here would show N consent screens).
|
|
80
|
+
instance.loginRedirect({
|
|
81
|
+
scopes: ["openid", "profile", "email"],
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}, [instance, accounts, inProgress]);
|
|
85
|
+
if (!authenticated)
|
|
86
|
+
return null;
|
|
87
|
+
return _jsx(_Fragment, { children: children });
|
|
88
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat Bubble Store — manages chat display mode as a layout state.
|
|
3
|
+
*
|
|
4
|
+
* Four modes: CLOSED → BUBBLE → EXPANDED → FULL
|
|
5
|
+
* - CLOSED: only trigger button visible
|
|
6
|
+
* - BUBBLE (isOpen): floating 380x600 chat panel
|
|
7
|
+
* - EXPANDED (isExpanded): chat fills main area, page goes to panel
|
|
8
|
+
* - FULL: expanded with page panel closed
|
|
9
|
+
*
|
|
10
|
+
* No route changes. The URL stays the same in all modes.
|
|
11
|
+
*/
|
|
12
|
+
interface ChatBubbleState {
|
|
13
|
+
/** Bubble floating panel is visible */
|
|
14
|
+
isOpen: boolean;
|
|
15
|
+
/** Chat is expanded into the main layout area */
|
|
16
|
+
isExpanded: boolean;
|
|
17
|
+
/** Page panel visible in expanded mode (false = FULL mode) */
|
|
18
|
+
showPagePanel: boolean;
|
|
19
|
+
open: () => void;
|
|
20
|
+
close: () => void;
|
|
21
|
+
toggle: () => void;
|
|
22
|
+
/** Expand: hide bubble, show chat in main layout */
|
|
23
|
+
expand: () => void;
|
|
24
|
+
/** Collapse: hide expanded chat, show bubble */
|
|
25
|
+
collapse: () => void;
|
|
26
|
+
/** Toggle page panel in expanded mode (EXPANDED ↔ FULL) */
|
|
27
|
+
togglePagePanel: () => void;
|
|
28
|
+
}
|
|
29
|
+
export declare const useChatBubbleStore: import("zustand").UseBoundStore<import("zustand").StoreApi<ChatBubbleState>>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat Bubble Store — manages chat display mode as a layout state.
|
|
3
|
+
*
|
|
4
|
+
* Four modes: CLOSED → BUBBLE → EXPANDED → FULL
|
|
5
|
+
* - CLOSED: only trigger button visible
|
|
6
|
+
* - BUBBLE (isOpen): floating 380x600 chat panel
|
|
7
|
+
* - EXPANDED (isExpanded): chat fills main area, page goes to panel
|
|
8
|
+
* - FULL: expanded with page panel closed
|
|
9
|
+
*
|
|
10
|
+
* No route changes. The URL stays the same in all modes.
|
|
11
|
+
*/
|
|
12
|
+
import { create } from "zustand";
|
|
13
|
+
export const useChatBubbleStore = create((set) => ({
|
|
14
|
+
isOpen: false,
|
|
15
|
+
isExpanded: false,
|
|
16
|
+
showPagePanel: true,
|
|
17
|
+
open: () => set({ isOpen: true }),
|
|
18
|
+
close: () => set({ isOpen: false, isExpanded: false }),
|
|
19
|
+
toggle: () => set((s) => ({ isOpen: !s.isOpen })),
|
|
20
|
+
expand: () => set({ isOpen: false, isExpanded: true, showPagePanel: true }),
|
|
21
|
+
collapse: () => set({ isOpen: true, isExpanded: false }),
|
|
22
|
+
togglePagePanel: () => set((s) => ({ showPagePanel: !s.showPagePanel })),
|
|
23
|
+
}));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface ChatLifecycleStore {
|
|
2
|
+
/**
|
|
3
|
+
* Called from :func:`useNewConversation` when the user starts a
|
|
4
|
+
* new thread. Spaces wires this to clear the active-chat sticky id.
|
|
5
|
+
*/
|
|
6
|
+
onNewConversation: (() => void) | null;
|
|
7
|
+
setOnNewConversation: (fn: (() => void) | null) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare const useChatLifecycleStore: import("zustand").UseBoundStore<import("zustand").StoreApi<ChatLifecycleStore>>;
|
|
10
|
+
export {};
|
|
@@ -21,17 +21,7 @@
|
|
|
21
21
|
* ``onNewConversation`` null — the call site no-ops gracefully.
|
|
22
22
|
*/
|
|
23
23
|
import { create } from "zustand";
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* Called from :func:`useNewConversation` when the user starts a
|
|
28
|
-
* new thread. Spaces wires this to clear the active-chat sticky id.
|
|
29
|
-
*/
|
|
30
|
-
onNewConversation: (() => void) | null;
|
|
31
|
-
setOnNewConversation: (fn: (() => void) | null) => void;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export const useChatLifecycleStore = create<ChatLifecycleStore>((set) => ({
|
|
35
|
-
onNewConversation: null,
|
|
36
|
-
setOnNewConversation: (fn) => set({ onNewConversation: fn }),
|
|
24
|
+
export const useChatLifecycleStore = create((set) => ({
|
|
25
|
+
onNewConversation: null,
|
|
26
|
+
setOnNewConversation: (fn) => set({ onNewConversation: fn }),
|
|
37
27
|
}));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Client Tools — registered into the dynamic tool registry at app init.
|
|
3
|
+
*
|
|
4
|
+
* Naming convention:
|
|
5
|
+
* ui_* — tools that affect the UI (navigate, theme, panel, content)
|
|
6
|
+
*
|
|
7
|
+
* Page-specific tools use their own prefix (e.g., page_search_documents).
|
|
8
|
+
* The adapter reads from the registry (not this file) on every request.
|
|
9
|
+
*/
|
|
10
|
+
export declare function setNavigateFunction(fn: (path: string) => void): void;
|
|
11
|
+
export declare function registerGlobalTools(): void;
|