@object-ui/app-shell 6.0.0 → 6.0.2
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/CHANGELOG.md +43 -0
- package/dist/console/AppContent.js +1 -1
- package/dist/console/home/HomeLayout.d.ts +6 -1
- package/dist/console/home/HomeLayout.js +2 -2
- package/dist/console/marketplace/MarketplacePackagePage.js +23 -9
- package/dist/console/marketplace/marketplaceApi.d.ts +14 -0
- package/dist/console/marketplace/marketplaceApi.js +5 -17
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useChatConversation.d.ts +31 -0
- package/dist/hooks/useChatConversation.js +188 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/layout/ConsoleFloatingChatbot.d.ts +7 -1
- package/dist/layout/ConsoleFloatingChatbot.js +16 -7
- package/dist/layout/ConsoleLayout.d.ts +6 -1
- package/dist/layout/ConsoleLayout.js +2 -2
- package/dist/runtime-config.d.ts +62 -0
- package/dist/runtime-config.js +86 -0
- package/dist/views/RecordDetailView.js +2 -1
- package/package.json +25 -25
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# @object-ui/app-shell — Changelog
|
|
2
2
|
|
|
3
|
+
## 6.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d0e63f1: Migrate AI chat history from localStorage to the server-backed
|
|
8
|
+
`ai_conversations` / `ai_messages` REST API. The studio `AiChatPanel`,
|
|
9
|
+
the console `ConsoleFloatingChatbot`, and any other consumer of the new
|
|
10
|
+
`useChatConversation` hook (in `@object-ui/app-shell`) now resolve a
|
|
11
|
+
durable conversation id per signed-in user, hydrate prior messages on
|
|
12
|
+
mount, and rotate the conversation on reset. The previous
|
|
13
|
+
`objectstack:ai-chat-messages` localStorage entries are no longer read
|
|
14
|
+
or written.
|
|
15
|
+
- @object-ui/types@6.0.2
|
|
16
|
+
- @object-ui/core@6.0.2
|
|
17
|
+
- @object-ui/i18n@6.0.2
|
|
18
|
+
- @object-ui/react@6.0.2
|
|
19
|
+
- @object-ui/components@6.0.2
|
|
20
|
+
- @object-ui/fields@6.0.2
|
|
21
|
+
- @object-ui/layout@6.0.2
|
|
22
|
+
- @object-ui/data-objectstack@6.0.2
|
|
23
|
+
- @object-ui/auth@6.0.2
|
|
24
|
+
- @object-ui/permissions@6.0.2
|
|
25
|
+
- @object-ui/collaboration@6.0.2
|
|
26
|
+
- @object-ui/providers@6.0.2
|
|
27
|
+
|
|
28
|
+
## 6.0.1
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- dbb9a98: cloud
|
|
33
|
+
- @object-ui/types@6.0.1
|
|
34
|
+
- @object-ui/core@6.0.1
|
|
35
|
+
- @object-ui/i18n@6.0.1
|
|
36
|
+
- @object-ui/react@6.0.1
|
|
37
|
+
- @object-ui/components@6.0.1
|
|
38
|
+
- @object-ui/fields@6.0.1
|
|
39
|
+
- @object-ui/layout@6.0.1
|
|
40
|
+
- @object-ui/data-objectstack@6.0.1
|
|
41
|
+
- @object-ui/auth@6.0.1
|
|
42
|
+
- @object-ui/permissions@6.0.1
|
|
43
|
+
- @object-ui/collaboration@6.0.1
|
|
44
|
+
- @object-ui/providers@6.0.1
|
|
45
|
+
|
|
3
46
|
## 6.0.0
|
|
4
47
|
|
|
5
48
|
### Major Changes
|
|
@@ -237,7 +237,7 @@ export function AppContent({ extraRoutes, extraRoutesNoApp } = {}) {
|
|
|
237
237
|
const expressionUser = user
|
|
238
238
|
? { name: user.name, email: user.email, role: user.role ?? 'user' }
|
|
239
239
|
: { name: 'Anonymous', email: '', role: 'guest' };
|
|
240
|
-
return (_jsxs(ExpressionProvider, { user: expressionUser, app: activeApp, data: {}, children: [_jsx(NavigationSyncEffect, {}), _jsxs(ConsoleLayout, { activeAppName: activeApp.name, activeApp: activeApp, onAppChange: handleAppChange, objects: allObjects, connectionState: connectionState, children: [_jsx(CommandPalette, { apps: apps, activeApp: activeApp, objects: allObjects, onAppChange: handleAppChange, dataSource: dataSource }), _jsx(KeyboardShortcutsDialog, {}), _jsx(OnboardingWalkthrough, {}), _jsx(ErrorBoundary, { children: _jsx(Suspense, { fallback: _jsx(LoadingScreen, {}), children: _jsx(RouteFader, { children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(Navigate, { to: resolveLandingRoute(activeApp), replace: true }) }), _jsx(Route, { path: ":objectName", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/new", element: _jsx(RecordFormPage, { mode: "create" }) }), _jsx(Route, { path: ":objectName/view/:viewId", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/record/:recordId", element: _jsx(RecordDetailView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }, refreshKey) }), _jsx(Route, { path: ":objectName/record/:recordId/edit", element: _jsx(RecordFormPage, { mode: "edit" }) }), _jsx(Route, { path: "dashboard/:dashboardName", element: _jsx(DashboardView, { dataSource: dataSource }) }), _jsx(Route, { path: "report/:reportName", element: _jsx(ReportView, { dataSource: dataSource }) }), _jsx(Route, { path: "page/:pageName", element: _jsx(PageView, {}) }), _jsx(Route, { path: "design/page/:pageName", element: _jsx(PageDesignPage, {}) }), _jsx(Route, { path: "design/dashboard/:dashboardName", element: _jsx(DashboardDesignPage, {}) }), _jsx(Route, { path: "search", element: _jsx(SearchResultsPage, {}) }), _jsx(Route, { path: "create-app", element: _jsx(CreateAppPage, {}) }), _jsx(Route, { path: "edit-app/:editAppName", element: _jsx(EditAppPage, {}) }), _jsx(Route, { path: "system/marketplace", element: _jsx(MarketplacePage, {}) }), _jsx(Route, { path: "system/marketplace/installed", element: _jsx(MarketplaceInstalledPage, {}) }), _jsx(Route, { path: "system/marketplace/:packageId", element: _jsx(MarketplacePackagePage, {}) }), extraRoutes, _jsx(Route, { path: ":objectName/:maybeRecordId", element: _jsx(ShorthandRecordRedirect, {}) }), _jsx(Route, { path: "*", element: _jsx(RouteNotFound, {}) })] }) }) }) }), currentObjectDef && (_jsx(ModalForm, { schema: {
|
|
240
|
+
return (_jsxs(ExpressionProvider, { user: expressionUser, app: activeApp, data: {}, children: [_jsx(NavigationSyncEffect, {}), _jsxs(ConsoleLayout, { activeAppName: activeApp.name, activeApp: activeApp, onAppChange: handleAppChange, objects: allObjects, connectionState: connectionState, userId: user?.id, children: [_jsx(CommandPalette, { apps: apps, activeApp: activeApp, objects: allObjects, onAppChange: handleAppChange, dataSource: dataSource }), _jsx(KeyboardShortcutsDialog, {}), _jsx(OnboardingWalkthrough, {}), _jsx(ErrorBoundary, { children: _jsx(Suspense, { fallback: _jsx(LoadingScreen, {}), children: _jsx(RouteFader, { children: _jsxs(Routes, { children: [_jsx(Route, { path: "/", element: _jsx(Navigate, { to: resolveLandingRoute(activeApp), replace: true }) }), _jsx(Route, { path: ":objectName", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/new", element: _jsx(RecordFormPage, { mode: "create" }) }), _jsx(Route, { path: ":objectName/view/:viewId", element: _jsx(ObjectView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit, externalRefreshKey: refreshKey }) }), _jsx(Route, { path: ":objectName/record/:recordId", element: _jsx(RecordDetailView, { dataSource: dataSource, objects: allObjects, onEdit: handleEdit }, refreshKey) }), _jsx(Route, { path: ":objectName/record/:recordId/edit", element: _jsx(RecordFormPage, { mode: "edit" }) }), _jsx(Route, { path: "dashboard/:dashboardName", element: _jsx(DashboardView, { dataSource: dataSource }) }), _jsx(Route, { path: "report/:reportName", element: _jsx(ReportView, { dataSource: dataSource }) }), _jsx(Route, { path: "page/:pageName", element: _jsx(PageView, {}) }), _jsx(Route, { path: "design/page/:pageName", element: _jsx(PageDesignPage, {}) }), _jsx(Route, { path: "design/dashboard/:dashboardName", element: _jsx(DashboardDesignPage, {}) }), _jsx(Route, { path: "search", element: _jsx(SearchResultsPage, {}) }), _jsx(Route, { path: "create-app", element: _jsx(CreateAppPage, {}) }), _jsx(Route, { path: "edit-app/:editAppName", element: _jsx(EditAppPage, {}) }), _jsx(Route, { path: "system/marketplace", element: _jsx(MarketplacePage, {}) }), _jsx(Route, { path: "system/marketplace/installed", element: _jsx(MarketplaceInstalledPage, {}) }), _jsx(Route, { path: "system/marketplace/:packageId", element: _jsx(MarketplacePackagePage, {}) }), extraRoutes, _jsx(Route, { path: ":objectName/:maybeRecordId", element: _jsx(ShorthandRecordRedirect, {}) }), _jsx(Route, { path: "*", element: _jsx(RouteNotFound, {}) })] }) }) }) }), currentObjectDef && (_jsx(ModalForm, { schema: {
|
|
241
241
|
type: 'object-form',
|
|
242
242
|
formType: 'modal',
|
|
243
243
|
objectName: currentObjectDef.name,
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
interface HomeLayoutProps {
|
|
12
12
|
children: React.ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* Signed-in user id. Forwarded to the floating chatbot so it can hydrate
|
|
15
|
+
* server-backed conversation history.
|
|
16
|
+
*/
|
|
17
|
+
userId?: string;
|
|
13
18
|
}
|
|
14
|
-
export declare function HomeLayout({ children }: HomeLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export declare function HomeLayout({ children, userId }: HomeLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
15
20
|
export {};
|
|
@@ -15,7 +15,7 @@ import { useDiscovery } from '@object-ui/react';
|
|
|
15
15
|
// Lightweight FAB stub — the heavy chat chunk graph only downloads on
|
|
16
16
|
// first hover/click. See ../../layout/ConsoleChatbotFab.tsx.
|
|
17
17
|
import { ConsoleChatbotFab } from '../../layout/ConsoleChatbotFab';
|
|
18
|
-
export function HomeLayout({ children }) {
|
|
18
|
+
export function HomeLayout({ children, userId }) {
|
|
19
19
|
const { setContext } = useNavigationContext();
|
|
20
20
|
const { isAiEnabled } = useDiscovery();
|
|
21
21
|
// Render the chatbot whenever AI is reachable. If the developer has explicitly
|
|
@@ -26,5 +26,5 @@ export function HomeLayout({ children }) {
|
|
|
26
26
|
useEffect(() => {
|
|
27
27
|
setContext('home');
|
|
28
28
|
}, [setContext]);
|
|
29
|
-
return (_jsxs("div", { className: "flex min-h-svh w-full flex-col bg-background", "data-testid": "home-layout", children: [_jsx("header", { className: "sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4", children: _jsx(AppHeader, { variant: "home" }) }), _jsx("main", { className: "flex-1 min-w-0 overflow-auto pb-20 sm:pb-0", children: children }), showChatbot && _jsx(ConsoleChatbotFab, { appLabel: "Workspace", objects: [] })] }));
|
|
29
|
+
return (_jsxs("div", { className: "flex min-h-svh w-full flex-col bg-background", "data-testid": "home-layout", children: [_jsx("header", { className: "sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4", children: _jsx(AppHeader, { variant: "home" }) }), _jsx("main", { className: "flex-1 min-w-0 overflow-auto pb-20 sm:pb-0", children: children }), showChatbot && _jsx(ConsoleChatbotFab, { appLabel: "Workspace", objects: [], userId: userId })] }));
|
|
30
30
|
}
|
|
@@ -7,11 +7,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
*/
|
|
8
8
|
import { useEffect, useState } from 'react';
|
|
9
9
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
10
|
-
import { Button, Badge, Card, CardContent, CardHeader, CardTitle, Skeleton, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Label, Checkbox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
|
|
11
|
-
import { ArrowLeft, ExternalLink, Download, AlertCircle, Package, Trash2 } from 'lucide-react';
|
|
10
|
+
import { Button, Badge, Card, CardContent, CardHeader, CardTitle, Skeleton, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Label, Checkbox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, } from '@object-ui/components';
|
|
11
|
+
import { ArrowLeft, ExternalLink, Download, AlertCircle, Package, Trash2, MoreHorizontal, CheckCircle2 } from 'lucide-react';
|
|
12
12
|
import { PackageIcon } from './PackageIcon';
|
|
13
13
|
import { MarkdownText } from './MarkdownText';
|
|
14
14
|
import { getMarketplacePackage, installPackage, installLocal, uninstallLocal, listLocalInstalls, listCloudEnvironments, listInstallableOrgIds, cloudInstallDeepLink, } from './marketplaceApi';
|
|
15
|
+
import { getRuntimeConfig } from '../../runtime-config';
|
|
15
16
|
export function MarketplacePackagePage() {
|
|
16
17
|
const navigate = useNavigate();
|
|
17
18
|
const { packageId, appName } = useParams();
|
|
@@ -32,6 +33,8 @@ export function MarketplacePackagePage() {
|
|
|
32
33
|
const [installingLocal, setInstallingLocal] = useState(false);
|
|
33
34
|
const [localResult, setLocalResult] = useState(null);
|
|
34
35
|
useEffect(() => {
|
|
36
|
+
if (!getRuntimeConfig().features.installLocal)
|
|
37
|
+
return;
|
|
35
38
|
let cancelled = false;
|
|
36
39
|
(async () => {
|
|
37
40
|
const items = await listLocalInstalls();
|
|
@@ -183,18 +186,29 @@ export function MarketplacePackagePage() {
|
|
|
183
186
|
}
|
|
184
187
|
};
|
|
185
188
|
if (loading) {
|
|
186
|
-
return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6", children: [_jsx(Skeleton, { className: "h-8 w-32" }), _jsx(Skeleton, { className: "h-16 w-full" }), _jsx(Skeleton, { className: "h-64 w-full" })] }));
|
|
189
|
+
return (_jsxs("div", { className: "mx-auto w-full max-w-6xl flex flex-col gap-6 p-4 sm:p-6", children: [_jsx(Skeleton, { className: "h-8 w-32" }), _jsx(Skeleton, { className: "h-16 w-full" }), _jsx(Skeleton, { className: "h-64 w-full" })] }));
|
|
187
190
|
}
|
|
188
191
|
if (error || !data) {
|
|
189
|
-
return (_jsxs("div", { className: "flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Back to marketplace"] }), _jsxs("div", { className: "flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-destructive", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium text-destructive", children: "Failed to load package" }), _jsx("div", { className: "text-muted-foreground mt-1", children: error ?? 'Not found.' })] })] })] }));
|
|
192
|
+
return (_jsxs("div", { className: "mx-auto w-full max-w-6xl flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs(Button, { variant: "ghost", size: "sm", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Back to marketplace"] }), _jsxs("div", { className: "flex items-start gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-destructive", "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium text-destructive", children: "Failed to load package" }), _jsx("div", { className: "text-muted-foreground mt-1", children: error ?? 'Not found.' })] })] })] }));
|
|
190
193
|
}
|
|
191
194
|
const pkg = data.package;
|
|
192
195
|
const latestVersion = pkg.latest_version?.version ?? data.versions[0]?.version ?? null;
|
|
193
196
|
const localInstall = localInstalls.find((i) => i.manifestId === pkg.manifest_id) ?? null;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
const supportsLocal = getRuntimeConfig().features.installLocal;
|
|
198
|
+
const primaryDisabled = !latestVersion || installingLocal || installing;
|
|
199
|
+
const primaryAction = supportsLocal
|
|
200
|
+
? {
|
|
201
|
+
label: installingLocal
|
|
202
|
+
? 'Working…'
|
|
203
|
+
: localInstall
|
|
204
|
+
? 'Reinstall'
|
|
205
|
+
: 'Install',
|
|
206
|
+
onClick: doInstallLocal,
|
|
207
|
+
}
|
|
208
|
+
: {
|
|
209
|
+
label: installing ? 'Installing…' : 'Install to cloud…',
|
|
210
|
+
onClick: openInstall,
|
|
211
|
+
};
|
|
212
|
+
return (_jsxs("div", { className: "mx-auto w-full max-w-6xl flex flex-col gap-6 p-4 sm:p-6", children: [_jsxs(Button, { variant: "ghost", size: "sm", className: "self-start -ml-2 text-muted-foreground hover:text-foreground", onClick: () => navigate(`${basePath}/system/marketplace`), children: [_jsx(ArrowLeft, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Back to marketplace"] }), _jsxs("div", { className: "flex items-start gap-5 flex-wrap sm:flex-nowrap rounded-2xl border bg-gradient-to-br from-primary/5 via-background to-background p-6 sm:p-8", children: [_jsx(PackageIcon, { iconUrl: pkg.icon_url, displayName: pkg.display_name, manifestId: pkg.manifest_id, className: "h-20 w-20 rounded-2xl shadow-sm ring-1 ring-border shrink-0", initialClassName: "text-3xl font-bold" }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [_jsx("h1", { className: "text-2xl sm:text-3xl font-bold tracking-tight truncate", children: pkg.display_name || pkg.manifest_id }), pkg.homepage_url && (_jsxs("a", { href: pkg.homepage_url, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors", title: "Homepage", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), _jsx("span", { className: "hidden sm:inline", children: "Homepage" })] }))] }), _jsxs("div", { className: "text-sm text-muted-foreground mt-2 flex flex-wrap items-center gap-1.5", children: [_jsx("code", { className: "font-mono text-xs px-1.5 py-0.5 rounded bg-muted", children: pkg.manifest_id }), latestVersion && _jsxs(Badge, { variant: "outline", children: ["v", latestVersion] }), pkg.publisher && pkg.publisher !== 'private' && (_jsx(Badge, { variant: pkg.publisher === 'objectstack' ? 'default' : 'secondary', children: pkg.publisher })), pkg.category && _jsx(Badge, { variant: "outline", children: pkg.category }), pkg.license && _jsx(Badge, { variant: "outline", className: "font-normal", children: pkg.license }), localInstall && (_jsxs(Badge, { variant: "default", className: "bg-green-600 hover:bg-green-600 gap-1", children: [_jsx(CheckCircle2, { className: "h-3 w-3", "aria-hidden": "true" }), "Installed \u00B7 v", localInstall.version] }))] }), pkg.description && (_jsx("p", { className: "text-sm sm:text-base text-foreground/80 mt-3 max-w-2xl leading-relaxed", children: pkg.description }))] }), _jsxs("div", { className: "flex items-center gap-2 shrink-0 self-start", children: [_jsxs(Button, { onClick: primaryAction.onClick, disabled: primaryDisabled, size: "lg", className: "min-w-[8rem]", children: [_jsx(Download, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), primaryAction.label] }), localInstall && (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "lg", className: "px-2.5", "aria-label": "More install options", children: _jsx(MoreHorizontal, { className: "h-4 w-4", "aria-hidden": "true" }) }) }), _jsx(DropdownMenuContent, { align: "end", className: "w-56", children: _jsxs(DropdownMenuItem, { onSelect: doUninstallLocal, disabled: installingLocal, className: "text-destructive focus:text-destructive", children: [_jsx(Trash2, { className: "h-4 w-4 mr-2", "aria-hidden": "true" }), "Uninstall from this runtime"] }) })] }))] })] }), localResult && (_jsxs("div", { role: "status", className: `flex items-start gap-2 rounded-md border p-3 text-sm whitespace-pre-wrap ${localResult.ok ? 'border-green-500/30 bg-green-500/5 text-green-700 dark:text-green-400' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: [localResult.ok ? _jsx(CheckCircle2, { className: "h-4 w-4 mt-0.5 shrink-0", "aria-hidden": "true" }) : _jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 shrink-0", "aria-hidden": "true" }), _jsx("div", { className: "flex-1", children: localResult.message }), _jsx("button", { type: "button", className: "text-xs underline opacity-60 hover:opacity-100", onClick: () => setLocalResult(null), children: "Dismiss" })] })), _jsxs("div", { className: "grid gap-4 lg:grid-cols-3", children: [_jsx("div", { className: "lg:col-span-2 space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "About" }) }), _jsx(CardContent, { children: pkg.readme ? (_jsx(MarkdownText, { source: pkg.readme })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: "No readme provided." })) })] }) }), _jsx("div", { className: "space-y-4", children: _jsxs(Card, { children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-base", children: "Versions" }) }), _jsx(CardContent, { children: data.versions.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No approved versions." })) : (_jsx("ul", { className: "space-y-2", children: data.versions.map((v) => (_jsxs("li", { className: "flex items-center justify-between gap-2 text-sm", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Package, { className: "h-3.5 w-3.5 text-muted-foreground", "aria-hidden": "true" }), _jsxs("code", { className: "font-mono", children: ["v", v.version] }), v.is_prerelease && _jsx(Badge, { variant: "outline", className: "text-xs", children: "pre" })] }), _jsx("span", { className: "text-xs text-muted-foreground", children: v.published_at ? new Date(v.published_at).toLocaleDateString() : '—' })] }, v.id))) })) })] }) })] }), _jsx(Dialog, { open: installOpen, onOpenChange: (o) => { setInstallOpen(o); if (!o)
|
|
199
213
|
setInstallResult(null); }, children: _jsxs(DialogContent, { children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Install ", pkg.display_name || pkg.manifest_id] }), _jsx(DialogDescription, { children: "Choose an environment to install this app into. You need to be signed into ObjectStack Cloud." })] }), envsLoading ? (_jsx(Skeleton, { className: "h-10 w-full" })) : envsError ? (_jsxs("div", { className: "rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-sm space-y-2", children: [_jsxs("div", { className: "flex items-start gap-2", children: [_jsx(AlertCircle, { className: "h-4 w-4 mt-0.5 text-amber-600", "aria-hidden": "true" }), _jsx("div", { className: "flex-1", children: envsError })] }), _jsx("a", { href: cloudInstallDeepLink(pkg.id), target: "_blank", rel: "noopener noreferrer", children: _jsxs(Button, { variant: "outline", size: "sm", className: "w-full", children: [_jsx(ExternalLink, { className: "h-4 w-4 mr-1.5", "aria-hidden": "true" }), "Open on cloud"] }) })] })) : envs.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No environments found in your active organization." })) : (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "env-select", children: "Environment" }), _jsxs(Select, { value: selectedEnv, onValueChange: setSelectedEnv, children: [_jsx(SelectTrigger, { id: "env-select", children: _jsx(SelectValue, { placeholder: "Pick an environment" }) }), _jsx(SelectContent, { children: envs.map((e) => (_jsxs(SelectItem, { value: e.id, children: [e.display_name || e.hostname || e.id, e.plan && _jsxs("span", { className: "text-muted-foreground", children: [" \u00B7 ", e.plan] })] }, e.id))) })] })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: "seed", checked: seedSampleData, onCheckedChange: (c) => setSeedSampleData(c === true) }), _jsx(Label, { htmlFor: "seed", className: "text-sm font-normal cursor-pointer", children: "Include sample data" })] })] })), installResult && (_jsx("div", { className: `rounded-md border p-3 text-sm ${installResult.ok ? 'border-green-500/30 bg-green-500/5 text-green-700' : 'border-destructive/30 bg-destructive/5 text-destructive'}`, children: installResult.message })), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => setInstallOpen(false), children: "Close" }), !envsError && (_jsx(Button, { onClick: doInstall, disabled: !selectedEnv || installing || installResult?.ok === true, children: installing ? 'Installing…' : 'Install' }))] })] }) })] }));
|
|
200
214
|
}
|
|
@@ -57,6 +57,20 @@ export declare function listMarketplacePackages(params?: {
|
|
|
57
57
|
offset?: number;
|
|
58
58
|
}): Promise<MarketplaceListResponse>;
|
|
59
59
|
export declare function getMarketplacePackage(id: string): Promise<MarketplaceDetailResponse>;
|
|
60
|
+
/**
|
|
61
|
+
* Install a package into an environment by calling the cloud's
|
|
62
|
+
* `install_package` action **directly** (not via the proxy). Requires the
|
|
63
|
+
* caller's browser to have a valid cloud session cookie — typically
|
|
64
|
+
* because the user has signed into cloud at least once and the cookie
|
|
65
|
+
* domain covers this origin. When that's not the case the call fails
|
|
66
|
+
* with 401; the UI can then surface a "Sign in on cloud" link.
|
|
67
|
+
*
|
|
68
|
+
* `cloudBaseUrl` is supplied by the server at boot via
|
|
69
|
+
* `/api/v1/runtime/config` (see `runtime-config.ts`); we read it through
|
|
70
|
+
* `getCloudBase()` rather than hardcoding `VITE_CLOUD_URL` or sniffing
|
|
71
|
+
* `window.location.hostname`. When the runtime *is* the cloud,
|
|
72
|
+
* `getCloudBase()` returns `''` and we fall back to same-origin.
|
|
73
|
+
*/
|
|
60
74
|
export interface InstallResponse {
|
|
61
75
|
installation?: {
|
|
62
76
|
id: string;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* cloud/packages/service-cloud/src/routes/marketplace.ts
|
|
13
13
|
* framework/packages/runtime/src/cloud/marketplace-proxy-plugin.ts
|
|
14
14
|
*/
|
|
15
|
+
import { getCloudBase } from '../../runtime-config';
|
|
15
16
|
const SERVER_URL = (import.meta.env.VITE_SERVER_URL || '').replace(/\/$/, '');
|
|
16
17
|
const API_BASE = `${SERVER_URL}/api/v1/marketplace`;
|
|
17
18
|
async function call(path, init) {
|
|
@@ -52,21 +53,8 @@ export async function listMarketplacePackages(params = {}) {
|
|
|
52
53
|
export async function getMarketplacePackage(id) {
|
|
53
54
|
return call(`/packages/${encodeURIComponent(id)}`);
|
|
54
55
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Install a package into an environment by calling the cloud's
|
|
57
|
-
* `install_package` action **directly** (not via the proxy). Requires the
|
|
58
|
-
* caller's browser to have a valid cloud session cookie — typically
|
|
59
|
-
* because the user has signed into cloud at least once and the cookie
|
|
60
|
-
* domain covers this origin. When that's not the case the call fails
|
|
61
|
-
* with 401; the UI can then surface a "Sign in on cloud" link.
|
|
62
|
-
*
|
|
63
|
-
* `cloudBaseUrl` is read from `VITE_CLOUD_URL` if set; otherwise we fall
|
|
64
|
-
* back to relative (assumes same-origin, which is true for cloud-hosted
|
|
65
|
-
* consoles but generally not for tenant runtimes).
|
|
66
|
-
*/
|
|
67
|
-
const CLOUD_BASE = (import.meta.env.VITE_CLOUD_URL || '').replace(/\/$/, '');
|
|
68
56
|
export async function installPackage(input) {
|
|
69
|
-
const base =
|
|
57
|
+
const base = getCloudBase() || SERVER_URL;
|
|
70
58
|
const res = await fetch(`${base}/api/v1/actions/sys_package/install_package`, {
|
|
71
59
|
method: 'POST',
|
|
72
60
|
credentials: 'include',
|
|
@@ -95,7 +83,7 @@ export async function installPackage(input) {
|
|
|
95
83
|
return (payload?.data ?? payload);
|
|
96
84
|
}
|
|
97
85
|
export async function listCloudEnvironments() {
|
|
98
|
-
const base =
|
|
86
|
+
const base = getCloudBase() || SERVER_URL;
|
|
99
87
|
const res = await fetch(`${base}/api/v1/data/sys_environment?limit=200`, {
|
|
100
88
|
credentials: 'include',
|
|
101
89
|
headers: { 'Accept': 'application/json' },
|
|
@@ -119,7 +107,7 @@ export async function listCloudEnvironments() {
|
|
|
119
107
|
* can render a clean "no installable environments" state.
|
|
120
108
|
*/
|
|
121
109
|
export async function listInstallableOrgIds() {
|
|
122
|
-
const base =
|
|
110
|
+
const base = getCloudBase() || SERVER_URL;
|
|
123
111
|
// sys_member rows are scoped to the caller; better-auth-managed table.
|
|
124
112
|
const url = `${base}/api/v1/data/sys_member?limit=200`;
|
|
125
113
|
let payload = null;
|
|
@@ -149,7 +137,7 @@ export async function listInstallableOrgIds() {
|
|
|
149
137
|
return ids;
|
|
150
138
|
}
|
|
151
139
|
export function cloudInstallDeepLink(packageId) {
|
|
152
|
-
const base =
|
|
140
|
+
const base = getCloudBase() || 'https://cloud.objectos.app';
|
|
153
141
|
return `${base}/apps/cloud-control/sys_package/${encodeURIComponent(packageId)}`;
|
|
154
142
|
}
|
|
155
143
|
export async function installLocal(input) {
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export { useRecentItems, type RecentItem } from './useRecentItems';
|
|
|
7
7
|
export { useRecordApprovals, type ApprovalProcessLite, type ApprovalRequestLite } from './useRecordApprovals';
|
|
8
8
|
export { useResponsiveSidebar } from './useResponsiveSidebar';
|
|
9
9
|
export { useTrackRouteAsRecent, type UseTrackRouteAsRecentOptions } from './useTrackRouteAsRecent';
|
|
10
|
+
export { useChatConversation, type HydratedUIMessage, type UseChatConversationOptions, type UseChatConversationReturn, } from './useChatConversation';
|
package/dist/hooks/index.js
CHANGED
|
@@ -7,3 +7,4 @@ export { useRecentItems } from './useRecentItems';
|
|
|
7
7
|
export { useRecordApprovals } from './useRecordApprovals';
|
|
8
8
|
export { useResponsiveSidebar } from './useResponsiveSidebar';
|
|
9
9
|
export { useTrackRouteAsRecent } from './useTrackRouteAsRecent';
|
|
10
|
+
export { useChatConversation, } from './useChatConversation';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Minimal UIMessage shape compatible with `@ai-sdk/react`'s `useChat`. */
|
|
2
|
+
export interface HydratedUIMessage {
|
|
3
|
+
id: string;
|
|
4
|
+
role: 'user' | 'assistant' | 'system';
|
|
5
|
+
parts: Array<{
|
|
6
|
+
type: 'text';
|
|
7
|
+
text: string;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
export interface UseChatConversationOptions {
|
|
11
|
+
/** Authenticated user id; hook is inert until this is defined. */
|
|
12
|
+
userId: string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Optional scope (e.g. agent name) for keying separate conversations under
|
|
15
|
+
* the same user.
|
|
16
|
+
*/
|
|
17
|
+
scope?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Base URL of the AI service (no trailing slash). Hook calls
|
|
20
|
+
* `${apiBase}/conversations[/...]`. Required.
|
|
21
|
+
*/
|
|
22
|
+
apiBase: string;
|
|
23
|
+
}
|
|
24
|
+
export interface UseChatConversationReturn {
|
|
25
|
+
conversationId: string | undefined;
|
|
26
|
+
initialMessages: HydratedUIMessage[];
|
|
27
|
+
isLoading: boolean;
|
|
28
|
+
/** Delete the current conversation + start a fresh one. */
|
|
29
|
+
reset: () => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export declare function useChatConversation(options: UseChatConversationOptions): UseChatConversationReturn;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* Server-backed AI chat conversation lifecycle.
|
|
4
|
+
*
|
|
5
|
+
* Binds a chat UI to an `ai_conversations` row owned by the signed-in user.
|
|
6
|
+
* On mount it tries the cached id (per user, optionally per scope); falls
|
|
7
|
+
* back to creating a fresh conversation when the cached one is gone
|
|
8
|
+
* (404/403).
|
|
9
|
+
*/
|
|
10
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
11
|
+
const CACHE_PREFIX = 'objectstack:ai-chat-conversation-id';
|
|
12
|
+
function cacheKey(userId, scope) {
|
|
13
|
+
return scope ? `${CACHE_PREFIX}:${userId}:${scope}` : `${CACHE_PREFIX}:${userId}`;
|
|
14
|
+
}
|
|
15
|
+
function readCache(key) {
|
|
16
|
+
try {
|
|
17
|
+
return localStorage.getItem(key) ?? undefined;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function writeCache(key, value) {
|
|
24
|
+
try {
|
|
25
|
+
if (value)
|
|
26
|
+
localStorage.setItem(key, value);
|
|
27
|
+
else
|
|
28
|
+
localStorage.removeItem(key);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* ignore — private mode, quota, etc. */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function contentToText(content) {
|
|
35
|
+
if (typeof content === 'string')
|
|
36
|
+
return content;
|
|
37
|
+
if (Array.isArray(content)) {
|
|
38
|
+
return content
|
|
39
|
+
.map((part) => {
|
|
40
|
+
if (typeof part === 'string')
|
|
41
|
+
return part;
|
|
42
|
+
if (part &&
|
|
43
|
+
typeof part === 'object' &&
|
|
44
|
+
'text' in part &&
|
|
45
|
+
typeof part.text === 'string') {
|
|
46
|
+
return part.text;
|
|
47
|
+
}
|
|
48
|
+
return '';
|
|
49
|
+
})
|
|
50
|
+
.join('');
|
|
51
|
+
}
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
function toUIMessages(rows) {
|
|
55
|
+
if (!rows)
|
|
56
|
+
return [];
|
|
57
|
+
const out = [];
|
|
58
|
+
rows.forEach((row, idx) => {
|
|
59
|
+
const role = row.role;
|
|
60
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system')
|
|
61
|
+
return;
|
|
62
|
+
const text = contentToText(row.content);
|
|
63
|
+
if (!text)
|
|
64
|
+
return;
|
|
65
|
+
out.push({
|
|
66
|
+
id: row.id ?? `msg-${idx}`,
|
|
67
|
+
role,
|
|
68
|
+
parts: [{ type: 'text', text }],
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
async function fetchConversation(apiBase, id) {
|
|
74
|
+
const res = await fetch(`${apiBase}/conversations/${encodeURIComponent(id)}`, {
|
|
75
|
+
credentials: 'include',
|
|
76
|
+
});
|
|
77
|
+
if (res.status === 404 || res.status === 403)
|
|
78
|
+
return null;
|
|
79
|
+
if (!res.ok)
|
|
80
|
+
throw new Error(`GET conversation failed: ${res.status}`);
|
|
81
|
+
return (await res.json());
|
|
82
|
+
}
|
|
83
|
+
async function createConversation(apiBase) {
|
|
84
|
+
const res = await fetch(`${apiBase}/conversations`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
credentials: 'include',
|
|
87
|
+
headers: { 'Content-Type': 'application/json' },
|
|
88
|
+
body: '{}',
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok)
|
|
91
|
+
throw new Error(`POST conversation failed: ${res.status}`);
|
|
92
|
+
return (await res.json());
|
|
93
|
+
}
|
|
94
|
+
async function deleteConversation(apiBase, id) {
|
|
95
|
+
await fetch(`${apiBase}/conversations/${encodeURIComponent(id)}`, {
|
|
96
|
+
method: 'DELETE',
|
|
97
|
+
credentials: 'include',
|
|
98
|
+
}).catch(() => {
|
|
99
|
+
/* best-effort */
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export function useChatConversation(options) {
|
|
103
|
+
const { userId, scope, apiBase } = options;
|
|
104
|
+
const [conversationId, setConversationId] = useState(undefined);
|
|
105
|
+
const [initialMessages, setInitialMessages] = useState([]);
|
|
106
|
+
const [isLoading, setIsLoading] = useState(Boolean(userId));
|
|
107
|
+
const mountedRef = useRef(true);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
mountedRef.current = true;
|
|
110
|
+
return () => {
|
|
111
|
+
mountedRef.current = false;
|
|
112
|
+
};
|
|
113
|
+
}, []);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (!userId) {
|
|
116
|
+
setConversationId(undefined);
|
|
117
|
+
setInitialMessages([]);
|
|
118
|
+
setIsLoading(false);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
let cancelled = false;
|
|
122
|
+
const key = cacheKey(userId, scope);
|
|
123
|
+
setIsLoading(true);
|
|
124
|
+
(async () => {
|
|
125
|
+
try {
|
|
126
|
+
const cached = readCache(key);
|
|
127
|
+
if (cached) {
|
|
128
|
+
const existing = await fetchConversation(apiBase, cached);
|
|
129
|
+
if (cancelled)
|
|
130
|
+
return;
|
|
131
|
+
if (existing) {
|
|
132
|
+
setConversationId(existing.id);
|
|
133
|
+
setInitialMessages(toUIMessages(existing.messages));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
writeCache(key, undefined);
|
|
137
|
+
}
|
|
138
|
+
const fresh = await createConversation(apiBase);
|
|
139
|
+
if (cancelled)
|
|
140
|
+
return;
|
|
141
|
+
writeCache(key, fresh.id);
|
|
142
|
+
setConversationId(fresh.id);
|
|
143
|
+
setInitialMessages(toUIMessages(fresh.messages));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
if (!cancelled) {
|
|
147
|
+
setConversationId(undefined);
|
|
148
|
+
setInitialMessages([]);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
if (!cancelled)
|
|
153
|
+
setIsLoading(false);
|
|
154
|
+
}
|
|
155
|
+
})();
|
|
156
|
+
return () => {
|
|
157
|
+
cancelled = true;
|
|
158
|
+
};
|
|
159
|
+
}, [userId, scope, apiBase]);
|
|
160
|
+
const reset = useCallback(async () => {
|
|
161
|
+
if (!userId)
|
|
162
|
+
return;
|
|
163
|
+
const key = cacheKey(userId, scope);
|
|
164
|
+
setIsLoading(true);
|
|
165
|
+
try {
|
|
166
|
+
if (conversationId)
|
|
167
|
+
await deleteConversation(apiBase, conversationId);
|
|
168
|
+
writeCache(key, undefined);
|
|
169
|
+
const fresh = await createConversation(apiBase);
|
|
170
|
+
writeCache(key, fresh.id);
|
|
171
|
+
if (!mountedRef.current)
|
|
172
|
+
return;
|
|
173
|
+
setConversationId(fresh.id);
|
|
174
|
+
setInitialMessages([]);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
if (mountedRef.current) {
|
|
178
|
+
setConversationId(undefined);
|
|
179
|
+
setInitialMessages([]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
if (mountedRef.current)
|
|
184
|
+
setIsLoading(false);
|
|
185
|
+
}
|
|
186
|
+
}, [conversationId, userId, scope, apiBase]);
|
|
187
|
+
return { conversationId, initialMessages, isLoading, reset };
|
|
188
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export { ConsoleLayout, AppHeader, AppSidebar, UnifiedSidebar, AppSwitcher, Conn
|
|
|
19
19
|
export type { ActivityItem } from './layout';
|
|
20
20
|
export { CommandPalette, KeyboardShortcutsDialog, OnboardingWalkthrough, ConditionalAuthWrapper, ConsoleToaster, RouteFader, toastWithUndo, type ToastWithUndoOptions, ErrorBoundary, LoadingScreen, ThemeProvider, useTheme, } from './chrome';
|
|
21
21
|
export { initSentry, captureError, setSentryUser, getSentry } from './observability';
|
|
22
|
+
export { initRuntimeConfig, getRuntimeConfig, getCloudBase, isRuntimeConfigInitialised, resetRuntimeConfigForTesting, } from './runtime-config';
|
|
23
|
+
export type { RuntimeConfig, RuntimeFeatures } from './runtime-config';
|
|
22
24
|
export { ObjectView, RecordDetailView, RecordFormPage, DashboardView, PageView, ReportView, SearchResultsPage, ViewConfigPanel, } from './views';
|
|
23
25
|
export type { RecordFormPageProps } from './views';
|
|
24
26
|
export { useFavorites, useMetadataService, useNavPins, useNavigationSync, NavigationSyncEffect, addNavigationItem, removeNavigationItems, renameNavigationItems, navigationEqual, generateNavId, useResponsiveSidebar, } from './hooks';
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,10 @@ export { ConsoleLayout, AppHeader, AppSidebar, UnifiedSidebar, AppSwitcher, Conn
|
|
|
22
22
|
export { CommandPalette, KeyboardShortcutsDialog, OnboardingWalkthrough, ConditionalAuthWrapper, ConsoleToaster, RouteFader, toastWithUndo, ErrorBoundary, LoadingScreen, ThemeProvider, useTheme, } from './chrome';
|
|
23
23
|
// Observability — Sentry integration, opt-in via VITE_SENTRY_DSN
|
|
24
24
|
export { initSentry, captureError, setSentryUser, getSentry } from './observability';
|
|
25
|
+
// Runtime configuration pushed by the server at boot. Consumers fetch
|
|
26
|
+
// `/api/v1/runtime/config` via `initRuntimeConfig()` before first render
|
|
27
|
+
// and read upstream cloud URL + capability flags from `getRuntimeConfig()`.
|
|
28
|
+
export { initRuntimeConfig, getRuntimeConfig, getCloudBase, isRuntimeConfigInitialised, resetRuntimeConfigForTesting, } from './runtime-config';
|
|
25
29
|
// Standard inner-SPA views
|
|
26
30
|
export { ObjectView, RecordDetailView, RecordFormPage, DashboardView, PageView, ReportView, SearchResultsPage, ViewConfigPanel, } from './views';
|
|
27
31
|
// Hooks
|
|
@@ -14,6 +14,12 @@ export interface ConsoleFloatingChatbotProps {
|
|
|
14
14
|
defaultAgent?: string;
|
|
15
15
|
/** Whether the floating panel should open immediately on mount. */
|
|
16
16
|
defaultOpen?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Authenticated user id. When provided, the chat hydrates from (and writes
|
|
19
|
+
* to) a server-backed `ai_conversations` row keyed by `userId` + agent.
|
|
20
|
+
* Inert until defined — the floating panel still works in local-only mode.
|
|
21
|
+
*/
|
|
22
|
+
userId?: string;
|
|
17
23
|
}
|
|
18
|
-
export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: apiBaseProp, defaultAgent: defaultAgentProp, defaultOpen, }: ConsoleFloatingChatbotProps): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: apiBaseProp, defaultAgent: defaultAgentProp, defaultOpen, userId, }: ConsoleFloatingChatbotProps): import("react/jsx-runtime").JSX.Element;
|
|
19
25
|
export {};
|
|
@@ -14,6 +14,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
14
14
|
import React from 'react';
|
|
15
15
|
import { FloatingChatbot, useObjectChat, useAgents, useHitlInChat, } from '@object-ui/plugin-chatbot';
|
|
16
16
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@object-ui/components';
|
|
17
|
+
import { useChatConversation } from '../hooks';
|
|
17
18
|
const DEFAULT_AI_PATH = '/api/v1/ai';
|
|
18
19
|
function resolveApiBase(explicit) {
|
|
19
20
|
if (explicit)
|
|
@@ -25,7 +26,7 @@ function resolveApiBase(explicit) {
|
|
|
25
26
|
const serverUrl = env.VITE_SERVER_URL ?? '';
|
|
26
27
|
return `${serverUrl.replace(/\/$/, '')}${DEFAULT_AI_PATH}`;
|
|
27
28
|
}
|
|
28
|
-
function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, activeAgent, onAgentChange, chatApi, apiBase, defaultOpen = false, }) {
|
|
29
|
+
function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, activeAgent, onAgentChange, chatApi, apiBase, defaultOpen = false, conversationId, }) {
|
|
29
30
|
const objectNames = objects.map((o) => o.label || o.name).join(', ');
|
|
30
31
|
const activeAgentLabel = React.useMemo(() => {
|
|
31
32
|
const found = agents.find((a) => a.name === activeAgent);
|
|
@@ -36,7 +37,7 @@ function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, a
|
|
|
36
37
|
: `Hello! I'm your **${appLabel}** assistant. ${agentsError ? '(Backend unreachable — running in offline demo mode.)' : ''}`;
|
|
37
38
|
const { messages, isLoading, error, sendMessage, stop, reload, clear, } = useObjectChat({
|
|
38
39
|
api: chatApi,
|
|
39
|
-
conversationId
|
|
40
|
+
conversationId,
|
|
40
41
|
body: {
|
|
41
42
|
context: {
|
|
42
43
|
activeApp: appLabel,
|
|
@@ -85,7 +86,7 @@ function ChatbotInner({ appLabel, objects, agents, agentsLoading, agentsError, a
|
|
|
85
86
|
? 'Loading agents...'
|
|
86
87
|
: 'Ask anything...', onSendMessage: (content) => sendMessage(content), onClear: clear, onStop: isLoading ? stop : undefined, onReload: reload, isLoading: isLoading, error: error, enableMarkdown: true, onToolApprove: hitl.decide, toolDecisions: hitl.decisions, toolApproveLabel: "Approve & run", toolDenyLabel: "Reject", toolDenyReason: "Operator rejected from chat" }));
|
|
87
88
|
}
|
|
88
|
-
export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: apiBaseProp, defaultAgent: defaultAgentProp, defaultOpen = false, }) {
|
|
89
|
+
export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: apiBaseProp, defaultAgent: defaultAgentProp, defaultOpen = false, userId, }) {
|
|
89
90
|
const apiBase = React.useMemo(() => resolveApiBase(apiBaseProp), [apiBaseProp]);
|
|
90
91
|
const env = import.meta.env ?? {};
|
|
91
92
|
const envDefaultAgent = env.VITE_AI_DEFAULT_AGENT;
|
|
@@ -101,8 +102,16 @@ export default function ConsoleFloatingChatbot({ appLabel, objects, apiBase: api
|
|
|
101
102
|
const chatApi = activeAgent
|
|
102
103
|
? `${apiBase}/agents/${encodeURIComponent(activeAgent)}/chat`
|
|
103
104
|
: undefined;
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
105
|
+
// Server-backed conversation. Scoped by agent so each agent gets its own
|
|
106
|
+
// persistent history. Hook is inert until `userId` is provided; without it
|
|
107
|
+
// the FAB continues to work in local-only mode (no persistence).
|
|
108
|
+
const { conversationId } = useChatConversation({
|
|
109
|
+
userId,
|
|
110
|
+
scope: activeAgent,
|
|
111
|
+
apiBase,
|
|
112
|
+
});
|
|
113
|
+
// `key` forces a clean remount whenever the chat endpoint OR the resolved
|
|
114
|
+
// conversation id changes — required because `useObjectChat` locks its mode
|
|
115
|
+
// (api vs local) and its `conversationId` on first render.
|
|
116
|
+
return (_jsx(ChatbotInner, { appLabel: appLabel, objects: objects, agents: agents, agentsLoading: agentsLoading, agentsError: agentsError, activeAgent: activeAgent, onAgentChange: setActiveAgent, chatApi: chatApi, apiBase: apiBase, defaultOpen: defaultOpen, conversationId: conversationId }, `${chatApi ?? 'local'}:${conversationId ?? 'pending'}`));
|
|
108
117
|
}
|
|
@@ -16,7 +16,12 @@ interface ConsoleLayoutProps {
|
|
|
16
16
|
onAppChange: (name: string) => void;
|
|
17
17
|
objects: any[];
|
|
18
18
|
connectionState?: ConnectionState;
|
|
19
|
+
/**
|
|
20
|
+
* Signed-in user id. Forwarded to the floating chatbot so it can hydrate
|
|
21
|
+
* server-backed conversation history. Omit for unauthenticated/local-only.
|
|
22
|
+
*/
|
|
23
|
+
userId?: string;
|
|
19
24
|
}
|
|
20
25
|
/** Floating chatbot wired with useObjectChat for demo auto-response */
|
|
21
|
-
export declare function ConsoleLayout({ children, activeAppName, activeApp, onAppChange, objects, connectionState }: ConsoleLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
export declare function ConsoleLayout({ children, activeAppName, activeApp, onAppChange, objects, connectionState, userId, }: ConsoleLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
22
27
|
export {};
|
|
@@ -28,7 +28,7 @@ function ConsoleLayoutInner({ children }) {
|
|
|
28
28
|
}
|
|
29
29
|
/** Floating chatbot wired with useObjectChat for demo auto-response */
|
|
30
30
|
// (moved to ./ConsoleFloatingChatbot.tsx for code-splitting)
|
|
31
|
-
export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange, objects, connectionState }) {
|
|
31
|
+
export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange, objects, connectionState, userId, }) {
|
|
32
32
|
const appLabel = resolveI18nLabel(activeApp?.label) || activeAppName;
|
|
33
33
|
const { isAiEnabled } = useDiscovery();
|
|
34
34
|
// Trust an explicit `VITE_AI_BASE_URL` opt-in even when discovery reports
|
|
@@ -51,5 +51,5 @@ export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange,
|
|
|
51
51
|
? `${resolveI18nLabel(activeApp.label)} — ObjectStack Console`
|
|
52
52
|
: undefined,
|
|
53
53
|
}
|
|
54
|
-
: undefined, children: [_jsx(ConsoleLayoutInner, { children: children }), showChatbot && (_jsx(ConsoleChatbotFab, { appLabel: appLabel, objects: objects }))] }) }));
|
|
54
|
+
: undefined, children: [_jsx(ConsoleLayoutInner, { children: children }), showChatbot && (_jsx(ConsoleChatbotFab, { appLabel: appLabel, objects: objects, userId: userId }))] }) }));
|
|
55
55
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime configuration pushed by the server at boot.
|
|
3
|
+
*
|
|
4
|
+
* The SPA fetches `GET /api/v1/runtime/config` once before first paint
|
|
5
|
+
* (`initRuntimeConfig()`) and exposes the response through a module-level
|
|
6
|
+
* singleton (`config` + `getRuntimeConfig()`). Modules that need to know
|
|
7
|
+
* about the upstream cloud URL or capability flags read from here —
|
|
8
|
+
* NEVER from `window.location.hostname` or Vite-time env vars, since
|
|
9
|
+
* those don't reflect the runtime the SPA is actually attached to (e.g.
|
|
10
|
+
* a tenant ObjectOS runtime pointing at a separate cloud control plane).
|
|
11
|
+
*
|
|
12
|
+
* Mirrors `apps/studio/src/lib/config.ts` but lives in app-shell because
|
|
13
|
+
* the Console SPA in `apps/console` consumes app-shell code.
|
|
14
|
+
*
|
|
15
|
+
* Server-side: see
|
|
16
|
+
* cloud/packages/service-cloud/src/multi-environment-plugins.ts
|
|
17
|
+
* cloud/packages/service-cloud/src/single-environment-plugin.ts
|
|
18
|
+
* framework/packages/runtime/src/cloud/runtime-config-plugin.ts
|
|
19
|
+
*/
|
|
20
|
+
export interface RuntimeFeatures {
|
|
21
|
+
/** "Install to this runtime" button is meaningful on this runtime. */
|
|
22
|
+
installLocal: boolean;
|
|
23
|
+
/** `/api/v1/marketplace/*` is reachable from this runtime. */
|
|
24
|
+
marketplace: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface RuntimeConfig {
|
|
27
|
+
/**
|
|
28
|
+
* Upstream cloud base URL — the SPA dispatches install + env listing
|
|
29
|
+
* directly against this origin. Empty string ⇒ same-origin (i.e. the
|
|
30
|
+
* runtime we're attached to *is* the cloud).
|
|
31
|
+
*/
|
|
32
|
+
cloudUrl: string;
|
|
33
|
+
/** Single-environment runtime (CLI `os serve`, etc.). */
|
|
34
|
+
singleEnvironment: boolean;
|
|
35
|
+
defaultOrgId?: string | null;
|
|
36
|
+
defaultEnvironmentId?: string | null;
|
|
37
|
+
features: RuntimeFeatures;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Fetch the server-pushed runtime config and merge it into the singleton.
|
|
41
|
+
* Must be awaited before first render so consumers see definitive values
|
|
42
|
+
* on first paint. Safe to call more than once (subsequent calls re-fetch
|
|
43
|
+
* and re-merge).
|
|
44
|
+
*
|
|
45
|
+
* `baseUrl` lets callers in dev (Vite proxy) override the fetch origin.
|
|
46
|
+
* In production both Console SPA and tenant runtime share an origin so
|
|
47
|
+
* the default (relative `/api/v1/...`) works.
|
|
48
|
+
*/
|
|
49
|
+
export declare function initRuntimeConfig(baseUrl?: string): Promise<void>;
|
|
50
|
+
/** Read-only accessor. Returns the current snapshot. */
|
|
51
|
+
export declare function getRuntimeConfig(): RuntimeConfig;
|
|
52
|
+
/** Whether `initRuntimeConfig()` has run at least once. */
|
|
53
|
+
export declare function isRuntimeConfigInitialised(): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the upstream cloud base URL the SPA should target. When the
|
|
56
|
+
* runtime says it *is* the cloud (`cloudUrl: ''`) the SPA stays on the
|
|
57
|
+
* current origin. Otherwise this returns the server-supplied URL with no
|
|
58
|
+
* trailing slash.
|
|
59
|
+
*/
|
|
60
|
+
export declare function getCloudBase(): string;
|
|
61
|
+
/** Test/dev helper. */
|
|
62
|
+
export declare function resetRuntimeConfigForTesting(): void;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
const defaults = {
|
|
3
|
+
cloudUrl: '',
|
|
4
|
+
singleEnvironment: false,
|
|
5
|
+
defaultOrgId: null,
|
|
6
|
+
defaultEnvironmentId: null,
|
|
7
|
+
features: { installLocal: false, marketplace: true },
|
|
8
|
+
};
|
|
9
|
+
let current = { ...defaults };
|
|
10
|
+
let initialised = false;
|
|
11
|
+
/** Apply a partial update over the singleton. */
|
|
12
|
+
function applyUpdate(patch) {
|
|
13
|
+
current = {
|
|
14
|
+
...current,
|
|
15
|
+
...patch,
|
|
16
|
+
features: {
|
|
17
|
+
...current.features,
|
|
18
|
+
...(patch.features ?? {}),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Fetch the server-pushed runtime config and merge it into the singleton.
|
|
24
|
+
* Must be awaited before first render so consumers see definitive values
|
|
25
|
+
* on first paint. Safe to call more than once (subsequent calls re-fetch
|
|
26
|
+
* and re-merge).
|
|
27
|
+
*
|
|
28
|
+
* `baseUrl` lets callers in dev (Vite proxy) override the fetch origin.
|
|
29
|
+
* In production both Console SPA and tenant runtime share an origin so
|
|
30
|
+
* the default (relative `/api/v1/...`) works.
|
|
31
|
+
*/
|
|
32
|
+
export async function initRuntimeConfig(baseUrl = '') {
|
|
33
|
+
const base = (baseUrl || '').replace(/\/+$/, '');
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(`${base}/api/v1/runtime/config`, {
|
|
36
|
+
credentials: 'include',
|
|
37
|
+
headers: { Accept: 'application/json' },
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok)
|
|
40
|
+
return;
|
|
41
|
+
const body = (await res.json());
|
|
42
|
+
if (!body || typeof body !== 'object')
|
|
43
|
+
return;
|
|
44
|
+
applyUpdate({
|
|
45
|
+
cloudUrl: typeof body.cloudUrl === 'string' ? body.cloudUrl.replace(/\/+$/, '') : current.cloudUrl,
|
|
46
|
+
singleEnvironment: !!body.singleEnvironment,
|
|
47
|
+
defaultOrgId: body.defaultOrgId ?? current.defaultOrgId ?? null,
|
|
48
|
+
defaultEnvironmentId: body.defaultEnvironmentId ?? current.defaultEnvironmentId ?? null,
|
|
49
|
+
features: body.features
|
|
50
|
+
? {
|
|
51
|
+
installLocal: !!body.features.installLocal,
|
|
52
|
+
marketplace: body.features.marketplace !== false,
|
|
53
|
+
}
|
|
54
|
+
: current.features,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Endpoint missing or network failure ⇒ keep defaults. Older runtimes
|
|
59
|
+
// pre-dating this endpoint simply behave as before.
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
initialised = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Read-only accessor. Returns the current snapshot. */
|
|
66
|
+
export function getRuntimeConfig() {
|
|
67
|
+
return current;
|
|
68
|
+
}
|
|
69
|
+
/** Whether `initRuntimeConfig()` has run at least once. */
|
|
70
|
+
export function isRuntimeConfigInitialised() {
|
|
71
|
+
return initialised;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the upstream cloud base URL the SPA should target. When the
|
|
75
|
+
* runtime says it *is* the cloud (`cloudUrl: ''`) the SPA stays on the
|
|
76
|
+
* current origin. Otherwise this returns the server-supplied URL with no
|
|
77
|
+
* trailing slash.
|
|
78
|
+
*/
|
|
79
|
+
export function getCloudBase() {
|
|
80
|
+
return current.cloudUrl ?? '';
|
|
81
|
+
}
|
|
82
|
+
/** Test/dev helper. */
|
|
83
|
+
export function resetRuntimeConfigForTesting() {
|
|
84
|
+
current = { ...defaults, features: { ...defaults.features } };
|
|
85
|
+
initialised = false;
|
|
86
|
+
}
|
|
@@ -386,7 +386,8 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
|
|
|
386
386
|
const shouldRefresh = action.refreshAfter !== false;
|
|
387
387
|
if (shouldRefresh)
|
|
388
388
|
setActionRefreshKey(k => k + 1);
|
|
389
|
-
|
|
389
|
+
const result = json?.data;
|
|
390
|
+
return { success: true, data: result, reload: shouldRefresh };
|
|
390
391
|
}
|
|
391
392
|
catch (error) {
|
|
392
393
|
return { success: false, error: error.message };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/app-shell",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Minimal application shell for ObjectUI - framework-agnostic rendering engine",
|
|
@@ -28,35 +28,35 @@
|
|
|
28
28
|
"@sentry/react": "^8.55.2",
|
|
29
29
|
"lucide-react": "^1.16.0",
|
|
30
30
|
"sonner": "^2.0.7",
|
|
31
|
-
"@object-ui/auth": "6.0.
|
|
32
|
-
"@object-ui/collaboration": "6.0.
|
|
33
|
-
"@object-ui/components": "6.0.
|
|
34
|
-
"@object-ui/core": "6.0.
|
|
35
|
-
"@object-ui/data-objectstack": "6.0.
|
|
36
|
-
"@object-ui/fields": "6.0.
|
|
37
|
-
"@object-ui/i18n": "6.0.
|
|
38
|
-
"@object-ui/layout": "6.0.
|
|
39
|
-
"@object-ui/permissions": "6.0.
|
|
40
|
-
"@object-ui/providers": "6.0.
|
|
41
|
-
"@object-ui/react": "6.0.
|
|
42
|
-
"@object-ui/types": "6.0.
|
|
31
|
+
"@object-ui/auth": "6.0.2",
|
|
32
|
+
"@object-ui/collaboration": "6.0.2",
|
|
33
|
+
"@object-ui/components": "6.0.2",
|
|
34
|
+
"@object-ui/core": "6.0.2",
|
|
35
|
+
"@object-ui/data-objectstack": "6.0.2",
|
|
36
|
+
"@object-ui/fields": "6.0.2",
|
|
37
|
+
"@object-ui/i18n": "6.0.2",
|
|
38
|
+
"@object-ui/layout": "6.0.2",
|
|
39
|
+
"@object-ui/permissions": "6.0.2",
|
|
40
|
+
"@object-ui/providers": "6.0.2",
|
|
41
|
+
"@object-ui/react": "6.0.2",
|
|
42
|
+
"@object-ui/types": "6.0.2"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"react": "^18.0.0 || ^19.0.0",
|
|
46
46
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
47
47
|
"react-router-dom": "^6.0.0 || ^7.0.0",
|
|
48
|
-
"@object-ui/plugin-calendar": "^6.0.
|
|
49
|
-
"@object-ui/plugin-charts": "^6.0.
|
|
50
|
-
"@object-ui/plugin-chatbot": "^6.0.
|
|
51
|
-
"@object-ui/plugin-dashboard": "^6.0.
|
|
52
|
-
"@object-ui/plugin-designer": "^6.0.
|
|
53
|
-
"@object-ui/plugin-detail": "^6.0.
|
|
54
|
-
"@object-ui/plugin-form": "^6.0.
|
|
55
|
-
"@object-ui/plugin-grid": "^6.0.
|
|
56
|
-
"@object-ui/plugin-kanban": "^6.0.
|
|
57
|
-
"@object-ui/plugin-list": "^6.0.
|
|
58
|
-
"@object-ui/plugin-report": "^6.0.
|
|
59
|
-
"@object-ui/plugin-view": "^6.0.
|
|
48
|
+
"@object-ui/plugin-calendar": "^6.0.2",
|
|
49
|
+
"@object-ui/plugin-charts": "^6.0.2",
|
|
50
|
+
"@object-ui/plugin-chatbot": "^6.0.2",
|
|
51
|
+
"@object-ui/plugin-dashboard": "^6.0.2",
|
|
52
|
+
"@object-ui/plugin-designer": "^6.0.2",
|
|
53
|
+
"@object-ui/plugin-detail": "^6.0.2",
|
|
54
|
+
"@object-ui/plugin-form": "^6.0.2",
|
|
55
|
+
"@object-ui/plugin-grid": "^6.0.2",
|
|
56
|
+
"@object-ui/plugin-kanban": "^6.0.2",
|
|
57
|
+
"@object-ui/plugin-list": "^6.0.2",
|
|
58
|
+
"@object-ui/plugin-report": "^6.0.2",
|
|
59
|
+
"@object-ui/plugin-view": "^6.0.2"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/node": "^25.9.0",
|