@object-ui/app-shell 7.0.0 → 7.2.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/CHANGELOG.md +560 -0
- package/dist/console/AppContent.js +23 -17
- package/dist/console/ConsoleShell.d.ts +16 -0
- package/dist/console/ConsoleShell.js +43 -2
- package/dist/console/ai/AiChatPage.js +47 -16
- package/dist/console/ai/LiveCanvas.d.ts +8 -2
- package/dist/console/ai/LiveCanvas.js +6 -4
- package/dist/console/home/HomeLayout.js +5 -7
- package/dist/console/home/HomePage.js +1 -9
- package/dist/console/organizations/CreateWorkspaceDialog.js +15 -1
- package/dist/console/organizations/OrganizationsPage.js +22 -3
- package/dist/console/organizations/provisionEnvironment.d.ts +53 -0
- package/dist/console/organizations/provisionEnvironment.js +64 -0
- package/dist/environment/EnvironmentEntitlementDialog.d.ts +34 -0
- package/dist/environment/EnvironmentEntitlementDialog.js +37 -0
- package/dist/environment/EnvironmentListToolbar.d.ts +33 -0
- package/dist/environment/EnvironmentListToolbar.js +59 -0
- package/dist/environment/entitlements.d.ts +90 -0
- package/dist/environment/entitlements.js +91 -0
- package/dist/environment/useEnvironmentEntitlements.d.ts +32 -0
- package/dist/environment/useEnvironmentEntitlements.js +108 -0
- package/dist/hooks/useActionModal.js +15 -1
- package/dist/hooks/useAiSurface.d.ts +59 -0
- package/dist/hooks/useAiSurface.js +78 -0
- package/dist/hooks/useChatConversation.d.ts +30 -0
- package/dist/hooks/useChatConversation.js +63 -0
- package/dist/hooks/useConsoleActionRuntime.d.ts +3 -0
- package/dist/hooks/useConsoleActionRuntime.js +42 -10
- package/dist/index.d.ts +5 -2
- package/dist/index.js +10 -2
- package/dist/layout/AppHeader.js +28 -4
- package/dist/layout/ConsoleFloatingChatbot.d.ts +6 -4
- package/dist/layout/ConsoleFloatingChatbot.js +41 -10
- package/dist/layout/ConsoleLayout.js +5 -6
- package/dist/layout/ContextSelectors.js +59 -35
- package/dist/layout/agentPicker.d.ts +56 -0
- package/dist/layout/agentPicker.js +40 -0
- package/dist/preview/CommitTimeline.d.ts +15 -0
- package/dist/preview/CommitTimeline.js +82 -0
- package/dist/preview/DraftPreviewBar.js +20 -7
- package/dist/preview/UnpublishedAppBar.js +11 -7
- package/dist/preview/commitHistory.d.ts +28 -0
- package/dist/preview/commitHistory.js +48 -0
- package/dist/providers/ExpressionProvider.js +9 -3
- package/dist/providers/MetadataProvider.js +9 -0
- package/dist/utils/index.d.ts +2 -2
- package/dist/utils/index.js +1 -1
- package/dist/utils/recordFormNavigation.d.ts +60 -0
- package/dist/utils/recordFormNavigation.js +35 -0
- package/dist/utils/resolvePageVarTokens.d.ts +31 -0
- package/dist/utils/resolvePageVarTokens.js +72 -0
- package/dist/views/CreateViewDialog.js +14 -1
- package/dist/views/FlowRunner.d.ts +2 -30
- package/dist/views/FlowRunner.js +18 -50
- package/dist/views/ObjectView.js +26 -12
- package/dist/views/ScreenView.d.ts +70 -0
- package/dist/views/ScreenView.js +73 -0
- package/dist/views/metadata-admin/AssignedUsersSection.d.ts +28 -0
- package/dist/views/metadata-admin/AssignedUsersSection.js +151 -0
- package/dist/views/metadata-admin/DirectoryPage.js +2 -14
- package/dist/views/metadata-admin/JsonSourceEditor.d.ts +3 -1
- package/dist/views/metadata-admin/JsonSourceEditor.js +21 -3
- package/dist/views/metadata-admin/PackagesPage.d.ts +5 -0
- package/dist/views/metadata-admin/PackagesPage.js +58 -5
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +2 -1
- package/dist/views/metadata-admin/ResourceEditPage.js +83 -24
- package/dist/views/metadata-admin/ResourceListPage.js +28 -19
- package/dist/views/metadata-admin/StudioHomePage.js +6 -14
- package/dist/views/metadata-admin/anchors.js +20 -2
- package/dist/views/metadata-admin/createBody.d.ts +26 -0
- package/dist/views/metadata-admin/createBody.js +30 -0
- package/dist/views/metadata-admin/i18n.js +108 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.d.ts +10 -2
- package/dist/views/metadata-admin/inspectors/DatasetDefaultInspector.js +136 -8
- package/dist/views/metadata-admin/inspectors/FlowEdgeInspector.js +99 -4
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.d.ts +21 -0
- package/dist/views/metadata-admin/inspectors/FlowExprIssue.js +13 -0
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.d.ts +20 -2
- package/dist/views/metadata-admin/inspectors/FlowKeyValueField.js +71 -28
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowNodeConfigField.js +24 -9
- package/dist/views/metadata-admin/inspectors/FlowNodeInspector.js +81 -4
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.d.ts +4 -1
- package/dist/views/metadata-admin/inspectors/FlowObjectListField.js +8 -3
- package/dist/views/metadata-admin/inspectors/ObjectDefaultInspector.js +5 -4
- package/dist/views/metadata-admin/inspectors/ObjectFieldInspector.js +47 -12
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.d.ts +1 -1
- package/dist/views/metadata-admin/inspectors/ReportDefaultInspector.js +60 -2
- package/dist/views/metadata-admin/inspectors/VariableTextInput.d.ts +47 -0
- package/dist/views/metadata-admin/inspectors/VariableTextInput.js +95 -0
- package/dist/views/metadata-admin/inspectors/_shared.d.ts +5 -1
- package/dist/views/metadata-admin/inspectors/_shared.js +2 -2
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.d.ts +24 -0
- package/dist/views/metadata-admin/inspectors/datasetFilterCondition.js +102 -0
- package/dist/views/metadata-admin/inspectors/flow-node-config.d.ts +16 -1
- package/dist/views/metadata-admin/inspectors/flow-node-config.js +67 -11
- package/dist/views/metadata-admin/inspectors/flow-ref-check.d.ts +39 -0
- package/dist/views/metadata-admin/inspectors/flow-ref-check.js +114 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.d.ts +109 -0
- package/dist/views/metadata-admin/inspectors/flow-scope.js +199 -0
- package/dist/views/metadata-admin/inspectors/useDatasetFields.d.ts +14 -3
- package/dist/views/metadata-admin/inspectors/useDatasetFields.js +0 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.d.ts +23 -0
- package/dist/views/metadata-admin/inspectors/useFlowScope.js +45 -0
- package/dist/views/metadata-admin/issuePath.d.ts +22 -0
- package/dist/views/metadata-admin/issuePath.js +65 -0
- package/dist/views/metadata-admin/package-scope.d.ts +41 -0
- package/dist/views/metadata-admin/package-scope.js +59 -0
- package/dist/views/metadata-admin/preview-registry.d.ts +12 -0
- package/dist/views/metadata-admin/previews/DatasetPreview.js +21 -5
- package/dist/views/metadata-admin/previews/FlowCanvas.d.ts +26 -1
- package/dist/views/metadata-admin/previews/FlowCanvas.js +143 -16
- package/dist/views/metadata-admin/previews/FlowPreview.d.ts +1 -1
- package/dist/views/metadata-admin/previews/FlowPreview.js +47 -7
- package/dist/views/metadata-admin/previews/FlowSimulatorPanel.js +37 -3
- package/dist/views/metadata-admin/previews/ObjectFormCanvas.js +9 -4
- package/dist/views/metadata-admin/previews/PagePreview.js +112 -3
- package/dist/views/metadata-admin/previews/ProblemsPanel.d.ts +18 -0
- package/dist/views/metadata-admin/previews/ProblemsPanel.js +27 -0
- package/dist/views/metadata-admin/previews/ReportPreview.d.ts +9 -8
- package/dist/views/metadata-admin/previews/ReportPreview.js +33 -16
- package/dist/views/metadata-admin/previews/ScreenPreview.d.ts +38 -0
- package/dist/views/metadata-admin/previews/ScreenPreview.js +61 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.d.ts +14 -0
- package/dist/views/metadata-admin/previews/flow-canvas-layout.js +37 -0
- package/dist/views/metadata-admin/previews/flow-canvas-parts.d.ts +17 -1
- package/dist/views/metadata-admin/previews/flow-canvas-parts.js +23 -6
- package/dist/views/metadata-admin/previews/flow-expr-problems.d.ts +19 -0
- package/dist/views/metadata-admin/previews/flow-expr-problems.js +97 -0
- package/dist/views/metadata-admin/previews/flow-problems.d.ts +84 -0
- package/dist/views/metadata-admin/previews/flow-problems.js +209 -0
- package/dist/views/metadata-admin/previews/object-fields-io.d.ts +21 -0
- package/dist/views/metadata-admin/previews/object-fields-io.js +37 -2
- package/dist/views/metadata-admin/previews/screen-spec.d.ts +43 -0
- package/dist/views/metadata-admin/previews/screen-spec.js +108 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-types.d.ts +20 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.d.ts +7 -0
- package/dist/views/metadata-admin/previews/simulator/flow-sim-validate.js +76 -2
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.d.ts +32 -3
- package/dist/views/metadata-admin/previews/simulator/flow-simulator.js +119 -9
- package/package.json +38 -38
|
@@ -136,6 +136,36 @@ interface ServerConversation {
|
|
|
136
136
|
messages?: ServerMessage[];
|
|
137
137
|
}
|
|
138
138
|
export declare function toUIMessages(rows: ServerMessage[] | undefined): HydratedUIMessage[];
|
|
139
|
+
/**
|
|
140
|
+
* A FLAT `ai_messages` row as returned by the public share endpoint
|
|
141
|
+
* (`GET /api/v1/share-links/:token/messages`), which streams the raw object
|
|
142
|
+
* rows rather than the reconstructed ModelMessage history that the
|
|
143
|
+
* authenticated `GET /conversations/:id` returns.
|
|
144
|
+
*/
|
|
145
|
+
export interface RawAiMessageRow {
|
|
146
|
+
id?: string;
|
|
147
|
+
role: string;
|
|
148
|
+
/** Persisted text (assistant/user/system) OR a JSON-stringified tool-result array (tool role). */
|
|
149
|
+
content?: unknown;
|
|
150
|
+
/** Assistant tool CALLS, JSON-stringified (`tool-call` parts). */
|
|
151
|
+
tool_calls?: string | null;
|
|
152
|
+
/** Tool RESULT's owning call id (legacy plain-string tool rows). */
|
|
153
|
+
tool_call_id?: string | null;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Reconstruct the ModelMessage-shaped `content` that {@link toUIMessages}
|
|
157
|
+
* expects from the FLAT columns the public share endpoint returns raw.
|
|
158
|
+
*
|
|
159
|
+
* The authenticated path gets this reconstruction server-side
|
|
160
|
+
* (`ObjqlConversationService.toMessage`): an assistant turn's tool CALLS live
|
|
161
|
+
* in the separate `tool_calls` column, and a `tool` row's RESULTS are a
|
|
162
|
+
* JSON-stringified array in `content`. The share endpoint skips that step and
|
|
163
|
+
* dumps the rows verbatim — so the shared transcript previously rendered the
|
|
164
|
+
* raw `{"type":"tool-result",…}` envelope as text instead of a card. Mirroring
|
|
165
|
+
* `toMessage` here lets the share page reuse the exact same hydrate → render
|
|
166
|
+
* pipeline as the live chat. Keep this in lockstep with `toMessage`.
|
|
167
|
+
*/
|
|
168
|
+
export declare function aiMessageRowsToServerMessages(rows: RawAiMessageRow[] | undefined): ServerMessage[];
|
|
139
169
|
export declare function fetchConversation(apiBase: string, id: string): Promise<ServerConversation | null>;
|
|
140
170
|
export declare function useChatConversation(options: UseChatConversationOptions): UseChatConversationReturn;
|
|
141
171
|
export {};
|
|
@@ -267,6 +267,69 @@ export function toUIMessages(rows) {
|
|
|
267
267
|
});
|
|
268
268
|
return out;
|
|
269
269
|
}
|
|
270
|
+
function safeParseArray(value) {
|
|
271
|
+
if (typeof value !== 'string' || !value)
|
|
272
|
+
return undefined;
|
|
273
|
+
try {
|
|
274
|
+
const parsed = JSON.parse(value);
|
|
275
|
+
return Array.isArray(parsed) ? parsed : undefined;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Reconstruct the ModelMessage-shaped `content` that {@link toUIMessages}
|
|
283
|
+
* expects from the FLAT columns the public share endpoint returns raw.
|
|
284
|
+
*
|
|
285
|
+
* The authenticated path gets this reconstruction server-side
|
|
286
|
+
* (`ObjqlConversationService.toMessage`): an assistant turn's tool CALLS live
|
|
287
|
+
* in the separate `tool_calls` column, and a `tool` row's RESULTS are a
|
|
288
|
+
* JSON-stringified array in `content`. The share endpoint skips that step and
|
|
289
|
+
* dumps the rows verbatim — so the shared transcript previously rendered the
|
|
290
|
+
* raw `{"type":"tool-result",…}` envelope as text instead of a card. Mirroring
|
|
291
|
+
* `toMessage` here lets the share page reuse the exact same hydrate → render
|
|
292
|
+
* pipeline as the live chat. Keep this in lockstep with `toMessage`.
|
|
293
|
+
*/
|
|
294
|
+
export function aiMessageRowsToServerMessages(rows) {
|
|
295
|
+
if (!rows)
|
|
296
|
+
return [];
|
|
297
|
+
return rows.map((row) => {
|
|
298
|
+
const id = row.id;
|
|
299
|
+
const text = typeof row.content === 'string' ? row.content : '';
|
|
300
|
+
if (row.role === 'assistant') {
|
|
301
|
+
const toolCalls = safeParseArray(row.tool_calls);
|
|
302
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
303
|
+
const content = [];
|
|
304
|
+
if (text)
|
|
305
|
+
content.push({ type: 'text', text });
|
|
306
|
+
content.push(...toolCalls);
|
|
307
|
+
return { id, role: 'assistant', content };
|
|
308
|
+
}
|
|
309
|
+
return { id, role: 'assistant', content: text };
|
|
310
|
+
}
|
|
311
|
+
if (row.role === 'tool') {
|
|
312
|
+
const results = safeParseArray(row.content);
|
|
313
|
+
if (results && results.length > 0 && results[0]?.type === 'tool-result') {
|
|
314
|
+
return { id, role: 'tool', content: results };
|
|
315
|
+
}
|
|
316
|
+
// Back-compat: pre-array tool rows persisted a plain string.
|
|
317
|
+
return {
|
|
318
|
+
id,
|
|
319
|
+
role: 'tool',
|
|
320
|
+
content: [
|
|
321
|
+
{
|
|
322
|
+
type: 'tool-result',
|
|
323
|
+
toolCallId: row.tool_call_id ?? '',
|
|
324
|
+
toolName: 'unknown',
|
|
325
|
+
output: { type: 'text', value: text },
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return { id, role: row.role, content: text };
|
|
331
|
+
});
|
|
332
|
+
}
|
|
270
333
|
export async function fetchConversation(apiBase, id) {
|
|
271
334
|
const res = await fetch(`${apiBase}/conversations/${encodeURIComponent(id)}`, {
|
|
272
335
|
credentials: 'include',
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import React from 'react';
|
|
24
24
|
import { createAuthenticatedFetch } from '@object-ui/auth';
|
|
25
25
|
import type { ActionContext, ActionDef, ActionResult, ConfirmationHandler, NavigationHandler, ParamCollectionHandler, ResultDialogHandler, ToastHandler } from '@object-ui/core';
|
|
26
|
+
import { type EntitlementDialogSpec } from '../environment/entitlements';
|
|
26
27
|
export interface ConsoleActionRuntimeOptions {
|
|
27
28
|
/** Adapter for generic CRUD / execute calls. */
|
|
28
29
|
dataSource: any;
|
|
@@ -45,6 +46,8 @@ export interface ConsoleActionRuntime {
|
|
|
45
46
|
serverActionHandler: (action: ActionDef, context?: ActionContext) => Promise<ActionResult>;
|
|
46
47
|
/** Authenticated fetch wrapper (Bearer + tenant + cookies). */
|
|
47
48
|
authFetch: ReturnType<typeof createAuthenticatedFetch>;
|
|
49
|
+
/** Open the shared environment entitlement (upgrade / limit) dialog. */
|
|
50
|
+
openEntitlementDialog: (spec: EntitlementDialogSpec) => void;
|
|
48
51
|
/** Props to spread onto `<ActionProvider>`. */
|
|
49
52
|
actionProviderProps: {
|
|
50
53
|
context: Record<string, any>;
|
|
@@ -24,6 +24,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
24
24
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
25
25
|
import { useNavigate } from 'react-router-dom';
|
|
26
26
|
import { useAuth, createAuthenticatedFetch } from '@object-ui/auth';
|
|
27
|
+
import { usePermissions } from '@object-ui/permissions';
|
|
27
28
|
import { useObjectLabel } from '@object-ui/i18n';
|
|
28
29
|
import { ActionProvider, useGlobalUndo } from '@object-ui/react';
|
|
29
30
|
import { toast } from 'sonner';
|
|
@@ -32,11 +33,17 @@ import { ActionParamDialog } from '../views/ActionParamDialog';
|
|
|
32
33
|
import { ActionResultDialog } from '../views/ActionResultDialog';
|
|
33
34
|
import { FlowRunner } from '../views/FlowRunner';
|
|
34
35
|
import { resolveActionParams } from '../utils/resolveActionParams';
|
|
36
|
+
import { EnvironmentEntitlementDialog } from '../environment/EnvironmentEntitlementDialog';
|
|
37
|
+
import { entitlementDialogFromError } from '../environment/entitlements';
|
|
38
|
+
import { resolvePageVarTokens } from '../utils/resolvePageVarTokens';
|
|
35
39
|
const FALLBACK_USER = { id: 'current-user', name: 'Demo User', isPlatformAdmin: false };
|
|
36
40
|
export function useConsoleActionRuntime(opts) {
|
|
37
41
|
const { dataSource, objects, objectName, onRefresh } = opts;
|
|
38
42
|
const navigate = useNavigate();
|
|
39
43
|
const { user, activeOrganization } = useAuth();
|
|
44
|
+
// [ADR-0066 D4] System capabilities for the action capability gate (fail-open
|
|
45
|
+
// when no PermissionProvider is mounted — usePermissions returns []).
|
|
46
|
+
const { systemPermissions } = usePermissions();
|
|
40
47
|
const { fieldLabel, fieldOptionLabel, actionParamText, actionParamOptionLabel, actionDescription } = useObjectLabel();
|
|
41
48
|
const objectDef = useMemo(() => (objectName ? objects?.find((o) => o.name === objectName) : undefined), [objects, objectName]);
|
|
42
49
|
// Object name used for API paths / generic CRUD. Falls back to the action's
|
|
@@ -56,6 +63,9 @@ export function useConsoleActionRuntime(opts) {
|
|
|
56
63
|
const [resultDialogState, setResultDialogState] = useState({ open: false });
|
|
57
64
|
// A paused `screen`-node flow awaiting user input.
|
|
58
65
|
const [screenFlow, setScreenFlow] = useState(null);
|
|
66
|
+
// Plan/capacity gate dialog (upgrade / limit), shared by the env-list toolbar
|
|
67
|
+
// (proactive) and the api-action error path below (reactive safety net).
|
|
68
|
+
const [entitlementDialog, setEntitlementDialog] = useState({ open: false });
|
|
59
69
|
// Guards against double-firing a server action (slow SSO handoff, etc.).
|
|
60
70
|
const serverActionInFlight = useRef(new Set());
|
|
61
71
|
const resultDialogHandler = useCallback((spec, data) => new Promise((resolve) => {
|
|
@@ -103,8 +113,8 @@ export function useConsoleActionRuntime(opts) {
|
|
|
103
113
|
});
|
|
104
114
|
}, [objectName, objectDef, objects, fieldLabel, fieldOptionLabel, actionParamText, actionParamOptionLabel]);
|
|
105
115
|
const currentUser = user
|
|
106
|
-
? { id: user.id, name: user.name, avatar: user.image, isPlatformAdmin: user?.isPlatformAdmin ?? false }
|
|
107
|
-
: FALLBACK_USER;
|
|
116
|
+
? { id: user.id, name: user.name, avatar: user.image, isPlatformAdmin: user?.isPlatformAdmin ?? false, systemPermissions: systemPermissions ?? [] }
|
|
117
|
+
: { ...FALLBACK_USER, systemPermissions: systemPermissions ?? [] };
|
|
108
118
|
const toastHandler = useCallback((message, options) => {
|
|
109
119
|
if (options?.type === 'error') {
|
|
110
120
|
toast.error(message);
|
|
@@ -129,7 +139,10 @@ export function useConsoleActionRuntime(opts) {
|
|
|
129
139
|
}, [navigate]);
|
|
130
140
|
// Authenticated fetch for direct backend calls. Declared before apiHandler.
|
|
131
141
|
const authFetch = useMemo(() => createAuthenticatedFetch(), []);
|
|
132
|
-
const
|
|
142
|
+
const openEntitlementDialog = useCallback((spec) => {
|
|
143
|
+
setEntitlementDialog({ open: true, spec });
|
|
144
|
+
}, []);
|
|
145
|
+
const apiHandler = useCallback(async (action, context) => {
|
|
133
146
|
try {
|
|
134
147
|
const target = action.target || action.name;
|
|
135
148
|
const params = action.params || {};
|
|
@@ -144,6 +157,12 @@ export function useConsoleActionRuntime(opts) {
|
|
|
144
157
|
const rawParams = { ...params };
|
|
145
158
|
const rowRecord = rawParams._rowRecord;
|
|
146
159
|
delete rawParams._rowRecord;
|
|
160
|
+
// Resolve `{{page.<var>}}` tokens against the live page-variable snapshot
|
|
161
|
+
// (published into the action context by PageVariableActionBridge). This is
|
|
162
|
+
// what lets a pure-SDUI form submit the values its inputs wrote into page
|
|
163
|
+
// variables; whole-value tokens preserve type. See resolvePageVarTokens.
|
|
164
|
+
const pageVars = (context?.pageVariables ?? undefined);
|
|
165
|
+
const resolvedParams = resolvePageVarTokens(rawParams, pageVars);
|
|
147
166
|
// Interpolate `{field}` tokens in the target URL from the row record.
|
|
148
167
|
let resolvedTarget = targetStr;
|
|
149
168
|
if (rowRecord && /\{[a-z_][a-z0-9_]*\}/i.test(resolvedTarget)) {
|
|
@@ -156,7 +175,7 @@ export function useConsoleActionRuntime(opts) {
|
|
|
156
175
|
const wrap = action.bodyShape && typeof action.bodyShape === 'object' && action.bodyShape.wrap
|
|
157
176
|
? action.bodyShape.wrap
|
|
158
177
|
: undefined;
|
|
159
|
-
const body = wrap ? { [wrap]:
|
|
178
|
+
const body = wrap ? { [wrap]: resolvedParams } : { ...resolvedParams };
|
|
160
179
|
if (rowRecord && action.recordIdParam) {
|
|
161
180
|
const rowField = action.recordIdField || 'id';
|
|
162
181
|
const rowValue = rowRecord[rowField];
|
|
@@ -168,7 +187,7 @@ export function useConsoleActionRuntime(opts) {
|
|
|
168
187
|
body.organizationId = activeOrganization.id;
|
|
169
188
|
}
|
|
170
189
|
if (action.bodyExtra && typeof action.bodyExtra === 'object') {
|
|
171
|
-
Object.assign(body, action.bodyExtra);
|
|
190
|
+
Object.assign(body, resolvePageVarTokens(action.bodyExtra, pageVars));
|
|
172
191
|
}
|
|
173
192
|
const method = (action.method || 'POST').toUpperCase();
|
|
174
193
|
const init = {
|
|
@@ -181,12 +200,23 @@ export function useConsoleActionRuntime(opts) {
|
|
|
181
200
|
}
|
|
182
201
|
const res = await authFetch(url, init);
|
|
183
202
|
if (!res.ok) {
|
|
184
|
-
let
|
|
203
|
+
let body = null;
|
|
185
204
|
try {
|
|
186
|
-
|
|
187
|
-
detail = j?.error || j?.message || detail;
|
|
205
|
+
body = await res.json();
|
|
188
206
|
}
|
|
189
207
|
catch { /* response body not JSON */ }
|
|
208
|
+
// Plan/capacity gates (e.g. creating an environment the org's plan
|
|
209
|
+
// doesn't include) come back as coded 403s. Surface them as a friendly
|
|
210
|
+
// upgrade/limit DIALOG with a CTA — never a generic red error toast.
|
|
211
|
+
// Returning success:false WITHOUT an `error` suppresses the runner's
|
|
212
|
+
// error toast (ActionRunner.handlePostExecution); the dialog owns the
|
|
213
|
+
// messaging.
|
|
214
|
+
const entitlementSpec = entitlementDialogFromError(body);
|
|
215
|
+
if (entitlementSpec) {
|
|
216
|
+
openEntitlementDialog(entitlementSpec);
|
|
217
|
+
return { success: false };
|
|
218
|
+
}
|
|
219
|
+
const detail = body?.error || body?.message || `HTTP ${res.status}`;
|
|
190
220
|
return { success: false, error: detail };
|
|
191
221
|
}
|
|
192
222
|
const data = await res.json().catch(() => ({}));
|
|
@@ -247,7 +277,7 @@ export function useConsoleActionRuntime(opts) {
|
|
|
247
277
|
catch (error) {
|
|
248
278
|
return { success: false, error: error.message };
|
|
249
279
|
}
|
|
250
|
-
}, [dataSource, objApiName, authFetch, activeOrganization, refresh]);
|
|
280
|
+
}, [dataSource, objApiName, authFetch, activeOrganization, refresh, openEntitlementDialog]);
|
|
251
281
|
// Flow action handler — POST to /api/v1/automation/{name}/trigger.
|
|
252
282
|
// `context` is the shared ActionRunner context (registered handlers are
|
|
253
283
|
// invoked as `handler(action, runnerContext)`).
|
|
@@ -533,7 +563,8 @@ export function useConsoleActionRuntime(opts) {
|
|
|
533
563
|
} }), _jsx(ActionResultDialog, { state: resultDialogState, onAcknowledge: () => {
|
|
534
564
|
resultDialogState.resolve?.();
|
|
535
565
|
setResultDialogState({ open: false });
|
|
536
|
-
} }), _jsx(FlowRunner, { state: screenFlow, authFetch: authFetch, baseUrl: import.meta.env.VITE_SERVER_URL || '', dataSource: dataSource, objects: objects, onClose: () => setScreenFlow(null), onComplete: () => { setScreenFlow(null); refresh(); } })
|
|
566
|
+
} }), _jsx(FlowRunner, { state: screenFlow, authFetch: authFetch, baseUrl: import.meta.env.VITE_SERVER_URL || '', dataSource: dataSource, objects: objects, onClose: () => setScreenFlow(null), onComplete: () => { setScreenFlow(null); refresh(); } }), _jsx(EnvironmentEntitlementDialog, { state: entitlementDialog, apiBase: import.meta.env.VITE_SERVER_URL || '', onOpenChange: (open) => { if (!open)
|
|
567
|
+
setEntitlementDialog({ open: false }); } })] }));
|
|
537
568
|
return {
|
|
538
569
|
confirmHandler,
|
|
539
570
|
toastHandler,
|
|
@@ -544,6 +575,7 @@ export function useConsoleActionRuntime(opts) {
|
|
|
544
575
|
flowHandler,
|
|
545
576
|
serverActionHandler,
|
|
546
577
|
authFetch,
|
|
578
|
+
openEntitlementDialog,
|
|
547
579
|
actionProviderProps,
|
|
548
580
|
dialogs,
|
|
549
581
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -19,7 +19,9 @@ export type { AppShellProps, } from './types';
|
|
|
19
19
|
export type { MetadataState, MetadataContextValue, MetadataTypeStatus, } from './providers/MetadataProvider';
|
|
20
20
|
export type { ExpressionContextValue, } from './providers/ExpressionProvider';
|
|
21
21
|
export type { RecentItem, } from './hooks/useRecentItems';
|
|
22
|
-
export { ConsoleShell, ConnectedShell, RequireOrganization, AuthenticatedRoute, RootRedirect, SystemRedirect, LoadingFallback, } from './console/ConsoleShell';
|
|
22
|
+
export { ConsoleShell, ConnectedShell, RequireOrganization, RequireAiSurface, AuthenticatedRoute, RootRedirect, SystemRedirect, LoadingFallback, } from './console/ConsoleShell';
|
|
23
|
+
export { useAiSurfaceEnabled } from './hooks/useAiSurface';
|
|
24
|
+
export type { AiSurfaceState } from './hooks/useAiSurface';
|
|
23
25
|
export { ConsoleLayout, AppHeader, AppSidebar, UnifiedSidebar, AppSwitcher, ConnectionStatus, ActivityFeed, LocaleSwitcher, ModeToggle, AuthPageLayout, } from './layout';
|
|
24
26
|
export type { ActivityItem } from './layout';
|
|
25
27
|
export { CommandPalette, KeyboardShortcutsDialog, OnboardingWalkthrough, ConditionalAuthWrapper, ConsoleToaster, RouteFader, toastWithUndo, type ToastWithUndoOptions, ErrorBoundary, LoadingScreen, ThemeProvider, useTheme, } from './chrome';
|
|
@@ -48,8 +50,9 @@ export { MembersPage as DefaultMembersPage } from './console/organizations/manag
|
|
|
48
50
|
export { InvitationsPage as DefaultInvitationsPage } from './console/organizations/manage/InvitationsPage';
|
|
49
51
|
export { SettingsPage as DefaultSettingsPage } from './console/organizations/manage/SettingsPage';
|
|
50
52
|
export { AcceptInvitationPage as DefaultAcceptInvitationPage } from './console/organizations/manage/AcceptInvitationPage';
|
|
51
|
-
export { AiChatPage as DefaultAiChatPage, AiChatPage } from './console/ai/AiChatPage';
|
|
53
|
+
export { AiChatPage as DefaultAiChatPage, AiChatPage, hydratedMessagesToChatMessages, } from './console/ai/AiChatPage';
|
|
52
54
|
export { ConversationsSidebar } from './console/ai/ConversationsSidebar';
|
|
55
|
+
export { toUIMessages, aiMessageRowsToServerMessages, type HydratedUIMessage, type RawAiMessageRow, } from './hooks/useChatConversation';
|
|
53
56
|
export { registerAppComponent, getAppComponent, listAppComponents, componentRefToUrlSegments, urlSegmentsToComponentRef, } from './services/componentRegistry';
|
|
54
57
|
export type { AppComponentRegistryEntry } from './services/componentRegistry';
|
|
55
58
|
import './services/builtinComponents';
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,11 @@ export { getPendingRequests, isIdle, subscribeSettle, whenIdle, withSettleSignal
|
|
|
18
18
|
export { useRecentItems } from './hooks/useRecentItems';
|
|
19
19
|
// Console building blocks — compose these in your App.tsx to build the console
|
|
20
20
|
// routing tree. See examples/console-starter/src/App.tsx for a minimal example.
|
|
21
|
-
export { ConsoleShell, ConnectedShell, RequireOrganization, AuthenticatedRoute, RootRedirect, SystemRedirect, LoadingFallback, } from './console/ConsoleShell';
|
|
21
|
+
export { ConsoleShell, ConnectedShell, RequireOrganization, RequireAiSurface, AuthenticatedRoute, RootRedirect, SystemRedirect, LoadingFallback, } from './console/ConsoleShell';
|
|
22
|
+
// Runtime AI-availability signal — the single source of truth every AI entry
|
|
23
|
+
// point gates on (FAB, /ai routes, designer "Ask AI"). Server-pushed, no
|
|
24
|
+
// build-time edition flag. See ./hooks/useAiSurface.
|
|
25
|
+
export { useAiSurfaceEnabled } from './hooks/useAiSurface';
|
|
22
26
|
// Layout chrome
|
|
23
27
|
export { ConsoleLayout, AppHeader, AppSidebar, UnifiedSidebar, AppSwitcher, ConnectionStatus, ActivityFeed, LocaleSwitcher, ModeToggle, AuthPageLayout, } from './layout';
|
|
24
28
|
// Top-level chrome (dialogs, providers, error boundaries)
|
|
@@ -52,8 +56,12 @@ export { MembersPage as DefaultMembersPage } from './console/organizations/manag
|
|
|
52
56
|
export { InvitationsPage as DefaultInvitationsPage } from './console/organizations/manage/InvitationsPage';
|
|
53
57
|
export { SettingsPage as DefaultSettingsPage } from './console/organizations/manage/SettingsPage';
|
|
54
58
|
export { AcceptInvitationPage as DefaultAcceptInvitationPage } from './console/organizations/manage/AcceptInvitationPage';
|
|
55
|
-
export { AiChatPage as DefaultAiChatPage, AiChatPage } from './console/ai/AiChatPage';
|
|
59
|
+
export { AiChatPage as DefaultAiChatPage, AiChatPage, hydratedMessagesToChatMessages, } from './console/ai/AiChatPage';
|
|
56
60
|
export { ConversationsSidebar } from './console/ai/ConversationsSidebar';
|
|
61
|
+
// Conversation-history hydration helpers — reused by the public read-only
|
|
62
|
+
// share page (`/s/:token`) so a shared transcript renders identically to the
|
|
63
|
+
// live chat (tool cards included) instead of dumping raw envelopes.
|
|
64
|
+
export { toUIMessages, aiMessageRowsToServerMessages, } from './hooks/useChatConversation';
|
|
57
65
|
// Phase 3b: Component nav registry — plugins use this to register
|
|
58
66
|
// admin/setup UI surfaces that are addressable from App metadata via
|
|
59
67
|
// `{ type: 'component', componentRef: 'ns:name' }` nav items.
|
package/dist/layout/AppHeader.js
CHANGED
|
@@ -20,7 +20,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
20
20
|
*/
|
|
21
21
|
import { useLocation, useParams, Link, useNavigate } from 'react-router-dom';
|
|
22
22
|
import { Button, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, Avatar, AvatarImage, AvatarFallback, cn, } from '@object-ui/components';
|
|
23
|
-
import { Search, HelpCircle, ChevronDown, Check, Lock, LogOut, Boxes, Layers, Bot, User, BookOpen, ExternalLink, Keyboard, } from 'lucide-react';
|
|
23
|
+
import { Search, HelpCircle, ChevronDown, Check, Lock, LogOut, Boxes, Plus, Layers, Bot, User, BookOpen, ExternalLink, Keyboard, } from 'lucide-react';
|
|
24
24
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
25
25
|
import { useOffline } from '@object-ui/react';
|
|
26
26
|
import { PresenceAvatars, useTenantPresence } from '@object-ui/collaboration';
|
|
@@ -39,6 +39,7 @@ import { useMobileViewSwitcher } from './MobileViewSwitcherContext';
|
|
|
39
39
|
import { useNavigationContext } from '../context/NavigationContext';
|
|
40
40
|
import { useCommandPalette } from '../context/CommandPaletteProvider';
|
|
41
41
|
import { useUrlOverlay } from '../hooks/useUrlOverlay';
|
|
42
|
+
import { useAiSurfaceEnabled } from '../hooks/useAiSurface';
|
|
42
43
|
import { getProductName } from '../runtime-config';
|
|
43
44
|
import { LocalizedSidebarTrigger } from './LocalizedSidebarTrigger';
|
|
44
45
|
function humanizeSlug(slug) {
|
|
@@ -64,8 +65,12 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
64
65
|
// Click-reachable entry for the keyboard-shortcuts dialog (was `?`-key only).
|
|
65
66
|
// Shares the `?shortcuts=1` URL param with KeyboardShortcutsDialog (C2/C3).
|
|
66
67
|
const { openOverlay: openShortcuts } = useUrlOverlay('shortcuts');
|
|
67
|
-
const { user, signOut, isAuthEnabled, organizations, activeOrganization, isOrganizationsLoading, } = useAuth();
|
|
68
|
+
const { user, signOut, isAuthEnabled, organizations, activeOrganization, isOrganizationsLoading, getAuthConfig, } = useAuth();
|
|
68
69
|
const dataSource = useAdapter();
|
|
70
|
+
// Runtime AI gating: hide the top-bar AI entry point when the server serves
|
|
71
|
+
// no AI (Community Edition) so it can't dead-end on a chat with no agent.
|
|
72
|
+
// Same signal as the FAB and the `/ai` route guard.
|
|
73
|
+
const { enabled: aiEnabled } = useAiSurfaceEnabled();
|
|
69
74
|
const { t } = useObjectTranslation();
|
|
70
75
|
const { objectLabel, dashboardLabel, pageLabel, reportLabel, viewLabel, appLabel } = useObjectLabel();
|
|
71
76
|
const { apps: metadataApps, dashboards: metadataDashboards, pages: metadataPages, reports: metadataReports } = useMetadata();
|
|
@@ -432,6 +437,25 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
432
437
|
const activeActivities = activities ?? apiActivities ?? [];
|
|
433
438
|
const orgList = organizations ?? [];
|
|
434
439
|
const hasOrgSection = isOrganizationsLoading || orgList.length > 0 || !!activeOrganization;
|
|
440
|
+
// Mirror the server's `beforeCreateOrganization` gate so the "Create
|
|
441
|
+
// workspace" entry only shows where multi-org self-service is enabled.
|
|
442
|
+
// Default to allowed until the config resolves (avoids hiding it on slow
|
|
443
|
+
// networks); the server still enforces.
|
|
444
|
+
const [multiOrgDisabled, setMultiOrgDisabled] = useState(false);
|
|
445
|
+
useEffect(() => {
|
|
446
|
+
let cancelled = false;
|
|
447
|
+
getAuthConfig?.()
|
|
448
|
+
.then((cfg) => {
|
|
449
|
+
if (!cancelled)
|
|
450
|
+
setMultiOrgDisabled(cfg?.features?.multiOrgEnabled === false);
|
|
451
|
+
})
|
|
452
|
+
.catch(() => {
|
|
453
|
+
/* leave default — server still enforces */
|
|
454
|
+
});
|
|
455
|
+
return () => {
|
|
456
|
+
cancelled = true;
|
|
457
|
+
};
|
|
458
|
+
}, [getAuthConfig]);
|
|
435
459
|
// Build path segments (only used in `app` variant)
|
|
436
460
|
const pathParts = location.pathname.split('/').filter(Boolean);
|
|
437
461
|
const appNameFromRoute = params.appName || pathParts[1];
|
|
@@ -545,10 +569,10 @@ export function AppHeader({ variant, appName, objects, connectionState, presence
|
|
|
545
569
|
if (!isActive)
|
|
546
570
|
mobileSwitcher.onChange(v.id);
|
|
547
571
|
}, className: "gap-2", children: [v.icon ? (_jsx("span", { className: "shrink-0 text-muted-foreground [&>svg]:h-4 [&>svg]:w-4", children: v.icon })) : null, _jsx("span", { className: "flex-1 truncate", children: v.label }), v.locked ? (_jsx(Lock, { className: "h-3 w-3 shrink-0 text-muted-foreground", "aria-hidden": true })) : null, isActive ? (_jsx(Check, { className: "h-4 w-4 shrink-0 text-foreground", "aria-hidden": true })) : null] }, v.id));
|
|
548
|
-
}) })] })) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: mobileSwitcher.triggerLabel ?? mobileSwitcher.views[0].label }))) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel }))] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), t('topbar.offline', { defaultValue: 'Offline' })] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: t('topbar.usersOnline', { defaultValue: 'Users currently online' }), children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { type: "button", "data-testid": "action:command-palette:open", "aria-label": t('console.search', { defaultValue: 'Search…' }), "aria-keyshortcuts": "Meta+K Control+K", onClick: openCommandPalette, className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search…' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", "data-testid": "action:command-palette:open-mobile", onClick: openCommandPalette, "aria-label": t('console.search', { defaultValue: 'Search…' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx(InboxPopover, { notifications: notifications, unreadCount: unreadCount, pendingApprovalsCount: pendingApprovalsCount, activities: activeActivities, onMarkAllRead: markAllRead, onMarkRead: markNotificationRead }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0", asChild: true, "aria-label": t('topbar.aiAssistant', { defaultValue: 'AI Assistant' }), children: _jsx(Link, { to: "/ai", children: _jsx(Bot, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenu, { onOpenChange: (open) => { if (open)
|
|
572
|
+
}) })] })) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: mobileSwitcher.triggerLabel ?? mobileSwitcher.views[0].label }))) : (_jsx("span", { className: "text-sm font-medium sm:hidden truncate min-w-0 ml-1", children: lastSegmentLabel }))] }))] }), _jsxs("div", { className: "flex items-center gap-0.5 sm:gap-1 shrink-0 [&>*+*[data-topbar-group]]:ml-1 [&>[data-topbar-group]+[data-topbar-group]]:border-l [&>[data-topbar-group]+[data-topbar-group]]:border-border/60 [&>[data-topbar-group]+[data-topbar-group]]:pl-1 sm:[&>[data-topbar-group]+[data-topbar-group]]:pl-2 sm:[&>[data-topbar-group]+[data-topbar-group]]:ml-2", children: [!isOnline && (_jsxs("div", { className: "flex items-center gap-1 px-2 py-1 rounded-full bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium", children: [_jsx("span", { className: "h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), t('topbar.offline', { defaultValue: 'Offline' })] })), isApp && connectionState && _jsx(ConnectionStatus, { state: connectionState }), isApp && activeUsers.length > 0 && (_jsx("div", { className: "hidden md:flex items-center shrink-0", title: t('topbar.usersOnline', { defaultValue: 'Users currently online' }), children: _jsx(PresenceAvatars, { users: activeUsers, size: "sm", maxVisible: 3, showStatus: true }) })), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 sm:gap-1 shrink-0", children: [_jsxs("button", { type: "button", "data-testid": "action:command-palette:open", "aria-label": t('console.search', { defaultValue: 'Search…' }), "aria-keyshortcuts": "Meta+K Control+K", onClick: openCommandPalette, className: "hidden lg:flex relative items-center gap-2 w-48 xl:w-64 h-8 px-3 text-sm rounded-md border bg-muted/50 text-muted-foreground hover:bg-muted transition-colors", children: [_jsx(Search, { className: "h-3.5 w-3.5 shrink-0" }), _jsx("span", { className: "flex-1 text-left text-xs", children: t('console.search', { defaultValue: 'Search…' }) }), _jsxs("kbd", { className: "pointer-events-none inline-flex h-5 items-center gap-0.5 rounded border bg-background px-1.5 text-[10px] font-medium text-muted-foreground", children: [_jsx("span", { className: "text-xs", children: "\u2318" }), "K"] })] }), _jsx(Button, { variant: "ghost", size: "icon", className: "lg:hidden h-8 w-8 shrink-0", "data-testid": "action:command-palette:open-mobile", onClick: openCommandPalette, "aria-label": t('console.search', { defaultValue: 'Search…' }), children: _jsx(Search, { className: "h-4 w-4" }) })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [_jsx(InboxPopover, { notifications: notifications, unreadCount: unreadCount, pendingApprovalsCount: pendingApprovalsCount, activities: activeActivities, onMarkAllRead: markAllRead, onMarkRead: markNotificationRead }), aiEnabled && (_jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0", asChild: true, "aria-label": t('topbar.aiAssistant', { defaultValue: 'AI Assistant' }), children: _jsx(Link, { to: "/ai", children: _jsx(Bot, { className: "h-4 w-4" }) }) })), _jsxs(DropdownMenu, { onOpenChange: (open) => { if (open)
|
|
549
573
|
void loadHelpDocs(); }, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 hidden md:flex shrink-0", "aria-label": t('sidebar.helpTooltip', { defaultValue: 'Help & Documentation' }), children: _jsx(HelpCircle, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-56 rounded-lg", sideOffset: 4, children: [currentAppDocs.length > 0 && currentAppPackageId ? (_jsxs(DropdownMenuItem, { className: "cursor-pointer", onClick: () => navigate(currentAppDocs.length === 1
|
|
550
574
|
? `/apps/${currentAppPackageId}/docs/${currentAppDocs[0].name}`
|
|
551
|
-
: `/apps/${currentAppPackageId}/docs`), children: [_jsx(BookOpen, { className: "mr-2 h-4 w-4" }), t('help.appDocs', { defaultValue: "This app's docs" })] })) : null, _jsxs(DropdownMenuItem, { className: "cursor-pointer", onClick: () => navigate('/docs'), children: [_jsx(Layers, { className: "mr-2 h-4 w-4" }), t('help.allDocs', { defaultValue: 'All documentation' })] }), isApp ? (_jsxs(DropdownMenuItem, { className: "cursor-pointer", "data-testid": "action:keyboard-shortcuts:open", onClick: openShortcuts, children: [_jsx(Keyboard, { className: "mr-2 h-4 w-4" }), t('help.keyboardShortcuts', { defaultValue: 'Keyboard shortcuts' })] })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { asChild: true, className: "cursor-pointer", children: _jsxs("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: [_jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), t('help.onlineDocs', { defaultValue: 'Online documentation' })] }) })] })] })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [" ", _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [_jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/account/component/account/profile_card'), className: "cursor-pointer", children: [_jsx(User, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), hasOrgSection && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations'), className: "cursor-pointer", children: [_jsx(Boxes, { className: "mr-2 h-4 w-4" }), t('organizations.mine', { defaultValue: 'My Organizations' })] })), (metadataApps || [])
|
|
575
|
+
: `/apps/${currentAppPackageId}/docs`), children: [_jsx(BookOpen, { className: "mr-2 h-4 w-4" }), t('help.appDocs', { defaultValue: "This app's docs" })] })) : null, _jsxs(DropdownMenuItem, { className: "cursor-pointer", onClick: () => navigate('/docs'), children: [_jsx(Layers, { className: "mr-2 h-4 w-4" }), t('help.allDocs', { defaultValue: 'All documentation' })] }), isApp ? (_jsxs(DropdownMenuItem, { className: "cursor-pointer", "data-testid": "action:keyboard-shortcuts:open", onClick: openShortcuts, children: [_jsx(Keyboard, { className: "mr-2 h-4 w-4" }), t('help.keyboardShortcuts', { defaultValue: 'Keyboard shortcuts' })] })) : null, _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { asChild: true, className: "cursor-pointer", children: _jsxs("a", { href: "https://docs.objectstack.ai", target: "_blank", rel: "noopener noreferrer", children: [_jsx(ExternalLink, { className: "mr-2 h-4 w-4" }), t('help.onlineDocs', { defaultValue: 'Online documentation' })] }) })] })] })] }), _jsxs("div", { "data-topbar-group": true, className: "flex items-center gap-0.5 shrink-0", children: [" ", _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 rounded-full", children: _jsxs(Avatar, { className: "h-7 w-7 rounded-full", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-full bg-primary text-primary-foreground text-xs", children: getUserInitials(user) })] }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-64 rounded-lg", sideOffset: 4, children: [_jsx(DropdownMenuLabel, { className: "p-0 font-normal", children: _jsxs("div", { className: "flex items-center gap-2 px-2 py-2", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg", children: [_jsx(AvatarImage, { src: user?.image, alt: user?.name ?? 'User' }), _jsx(AvatarFallback, { className: "rounded-lg bg-primary text-primary-foreground", children: getUserInitials(user) })] }), _jsxs("div", { className: "grid flex-1 text-left text-sm leading-tight", children: [_jsx("span", { className: "truncate font-semibold", children: user?.name ?? 'User' }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: user?.email ?? '' })] })] }) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [_jsxs(DropdownMenuItem, { onClick: () => navigate('/apps/account/component/account/profile_card'), className: "cursor-pointer", children: [_jsx(User, { className: "mr-2 h-4 w-4" }), t('user.profile', { defaultValue: 'Profile' })] }), hasOrgSection && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations?manage=1'), className: "cursor-pointer", children: [_jsx(Boxes, { className: "mr-2 h-4 w-4" }), t('organizations.mine', { defaultValue: 'My Organizations' })] })), hasOrgSection && !multiOrgDisabled && (_jsxs(DropdownMenuItem, { onClick: () => navigate('/organizations?create=1'), className: "cursor-pointer", "data-testid": "header-create-workspace", children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), t('organizations.create', { defaultValue: 'Create workspace' })] })), (metadataApps || [])
|
|
552
576
|
.filter((a) => a.active !== false && a.hidden === true && a.name !== 'account')
|
|
553
577
|
.map((app) => {
|
|
554
578
|
const AppIcon = getIcon(app.icon);
|
|
@@ -36,10 +36,12 @@ export interface ConsoleFloatingChatbotProps {
|
|
|
36
36
|
*/
|
|
37
37
|
defaultAgent?: string;
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* Force the in-header agent switcher on (`true`) or off (`false`),
|
|
40
|
+
* overriding the default. When left undefined the switcher auto-reveals
|
|
41
|
+
* only when AI development is unlocked for the viewer — the live catalog
|
|
42
|
+
* serves BOTH an `ask` and a `build` agent and `aiStudio` isn't disabled —
|
|
43
|
+
* so pure end-user apps (only `ask`) stay clean while builders can flip
|
|
44
|
+
* Ask↔Build inline. `VITE_AI_SHOW_AGENT_PICKER=true` also forces it on.
|
|
43
45
|
*/
|
|
44
46
|
showAgentPicker?: boolean;
|
|
45
47
|
/** Whether the floating panel should open immediately on mount. */
|
|
@@ -13,7 +13,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
13
13
|
*/
|
|
14
14
|
import React from 'react';
|
|
15
15
|
import { useReconcileOnError } from '../hooks/useReconcileOnError';
|
|
16
|
-
import { FloatingChatbot, useObjectChat, useAgents, useHitlInChat, resolveDefaultAgentName, uiMessagesToChatMessages, publishHealthFromResponse, agentRouteName, } from '@object-ui/plugin-chatbot';
|
|
16
|
+
import { FloatingChatbot, useObjectChat, useAgents, useAiModels, useHitlInChat, resolveDefaultAgentName, uiMessagesToChatMessages, publishHealthFromResponse, agentRouteName, } from '@object-ui/plugin-chatbot';
|
|
17
17
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Button, ShareDialog, } from '@object-ui/components';
|
|
18
18
|
import { Share2, SquarePen } from 'lucide-react';
|
|
19
19
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
@@ -24,6 +24,7 @@ import { useAssistant, requestAssistantReview, emitCanvasInvalidate } from '../a
|
|
|
24
24
|
import { fetchPendingDraftCount } from '../preview/draftStatus';
|
|
25
25
|
import { getRuntimeConfig } from '../runtime-config';
|
|
26
26
|
import { cloudPricingDeepLink } from '../console/marketplace/marketplaceApi';
|
|
27
|
+
import { shouldShowAgentPicker } from './agentPicker';
|
|
27
28
|
/**
|
|
28
29
|
* Display names for the two built-in platform agents (ADR-0063: `ask` / `build`,
|
|
29
30
|
* bound by surface). The backend ships English labels ("Assistant" / "Builder");
|
|
@@ -73,6 +74,9 @@ function buildChatLocale(language, appLabel, agentName, fallbackAgentLabel, obje
|
|
|
73
74
|
toolRunning: '运行中',
|
|
74
75
|
toolAwaitingApproval: '等待确认',
|
|
75
76
|
toolFailed: '失败',
|
|
77
|
+
connectionWaiting: '正在等待服务器响应…',
|
|
78
|
+
connectionStalledLabel: '仍在处理中…',
|
|
79
|
+
connectionOfflineLabel: '网络已断开,正在重连…',
|
|
76
80
|
toolDetailsHidden: '已隐藏工具参数和原始结果,仅保留过程摘要。',
|
|
77
81
|
copy: '复制',
|
|
78
82
|
copied: '已复制',
|
|
@@ -99,6 +103,7 @@ function buildChatLocale(language, appLabel, agentName, fallbackAgentLabel, obje
|
|
|
99
103
|
planApproveHint: '回复以确认或调整该方案。',
|
|
100
104
|
planApprove: '开始搭建',
|
|
101
105
|
planAdjust: '调整方案',
|
|
106
|
+
planBuilt: '已搭建',
|
|
102
107
|
planApproveMessage: '就按这个方案搭建吧。',
|
|
103
108
|
planApproveDefaultsMessage: '就按你的合理假设直接搭建,未决问题用默认即可。',
|
|
104
109
|
planAnswer: (question, option) => `关于「${question}」,我选择「${option}」。`,
|
|
@@ -128,6 +133,9 @@ function buildChatLocale(language, appLabel, agentName, fallbackAgentLabel, obje
|
|
|
128
133
|
toolRunning: 'Running',
|
|
129
134
|
toolAwaitingApproval: 'Awaiting approval',
|
|
130
135
|
toolFailed: 'Failed',
|
|
136
|
+
connectionWaiting: 'Waiting for server…',
|
|
137
|
+
connectionStalledLabel: 'Still working…',
|
|
138
|
+
connectionOfflineLabel: 'Connection lost — reconnecting…',
|
|
131
139
|
toolDetailsHidden: 'Tool inputs and raw results are hidden in this view.',
|
|
132
140
|
copy: 'Copy',
|
|
133
141
|
copied: 'Copied',
|
|
@@ -154,6 +162,7 @@ function buildChatLocale(language, appLabel, agentName, fallbackAgentLabel, obje
|
|
|
154
162
|
planApproveHint: 'Reply to approve or adjust this plan.',
|
|
155
163
|
planApprove: 'Build it',
|
|
156
164
|
planAdjust: 'Adjust',
|
|
165
|
+
planBuilt: 'Built',
|
|
157
166
|
planApproveMessage: 'Looks good — build it as proposed.',
|
|
158
167
|
planApproveDefaultsMessage: 'Build it with your best assumptions; use sensible defaults for the open questions.',
|
|
159
168
|
planAnswer: (question, option) => `For "${question}", go with: ${option}.`,
|
|
@@ -248,6 +257,14 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
|
|
|
248
257
|
// the agent context so "add a priority field" acts on the open object,
|
|
249
258
|
// and drives context-aware starter suggestions.
|
|
250
259
|
const { editor } = useAssistant();
|
|
260
|
+
// ADR-0028: the plan-filtered AI-model allowlist this env offers in the
|
|
261
|
+
// picker (free / single-model envs return one entry → the footer picker
|
|
262
|
+
// hides itself). The selected id is sent with each turn; the backend
|
|
263
|
+
// validates it against the same allowlist and weights the quota by the
|
|
264
|
+
// chosen model's cost_weight.
|
|
265
|
+
const { models: aiModels, defaultModelId } = useAiModels({ apiBase });
|
|
266
|
+
const [selectedModelId, setSelectedModelId] = React.useState(undefined);
|
|
267
|
+
const effectiveModelId = selectedModelId ?? defaultModelId;
|
|
251
268
|
// Replay persisted history when present.
|
|
252
269
|
const hydratedHistory = React.useMemo(() => {
|
|
253
270
|
if (!persistedMessages || persistedMessages.length === 0)
|
|
@@ -268,6 +285,9 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
|
|
|
268
285
|
const { messages, isLoading, error, sendMessage, stop, reload, clear, setMessages, } = useObjectChat({
|
|
269
286
|
api: chatApi,
|
|
270
287
|
conversationId,
|
|
288
|
+
// ADR-0028: the user's picked model (or the env default) — sent in each
|
|
289
|
+
// request body; the agent route validates it + routes the turn to it.
|
|
290
|
+
model: effectiveModelId,
|
|
271
291
|
onError: handleChatError,
|
|
272
292
|
body: {
|
|
273
293
|
context: {
|
|
@@ -321,10 +341,10 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
|
|
|
321
341
|
sendMessage(prompt);
|
|
322
342
|
},
|
|
323
343
|
});
|
|
324
|
-
// Agent switcher —
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
344
|
+
// Agent switcher — Ask ↔ Build (plus any custom agents). Restrained by
|
|
345
|
+
// design: end users bound to a single agent never see it. `showAgentPicker`
|
|
346
|
+
// is true when AI development is unlocked (catalog serves both ask & build)
|
|
347
|
+
// or forced on; it still needs more than one agent to be a real choice.
|
|
328
348
|
const headerExtra = showAgentPicker && agents.length > 1 ? (_jsxs(Select, { value: activeAgent, onValueChange: onAgentChange, disabled: agentsLoading, children: [_jsx(SelectTrigger, { className: "h-7 w-[180px] text-xs", "data-testid": "floating-chatbot-agent-picker", children: _jsx(SelectValue, { placeholder: "Choose agent..." }) }), _jsx(SelectContent, { align: "end", children: agents.map((agent) => (_jsxs(SelectItem, { value: agent.name, className: "text-xs", children: [_jsx("span", { className: "font-medium", children: agent.label }), agent.description ? (_jsx("span", { className: "block text-muted-foreground text-[10px] truncate max-w-[220px]", children: agent.description })) : null] }, agent.name))) })] })) : null;
|
|
329
349
|
// Share-link control. Sits to the left of the panel's built-in
|
|
330
350
|
// fullscreen / close buttons so users can mint a public link without
|
|
@@ -339,7 +359,10 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
|
|
|
339
359
|
panelHeight: 560,
|
|
340
360
|
title: locale.title,
|
|
341
361
|
triggerSize: 56,
|
|
342
|
-
}, headerExtra: headerExtra, headerActions: headerActions, messages: messages, labels: locale.labels,
|
|
362
|
+
}, headerExtra: headerExtra, headerActions: headerActions, messages: messages, labels: locale.labels,
|
|
363
|
+
// ADR-0028: model picker — ChatbotEnhanced renders the footer <select>
|
|
364
|
+
// only when 2+ models are offered, so free / single-model envs see none.
|
|
365
|
+
models: aiModels, selectedModelId: effectiveModelId, onModelChange: setSelectedModelId, showAvatars: true, hideClearBar: true, assistantAvatarFallback: locale.agentLabel, suggestions: messages.length === 0 ? (editorSuggestions ?? locale.suggestions) : undefined, placeholder: activeAgent
|
|
343
366
|
? locale.placeholder
|
|
344
367
|
: agentsLoading
|
|
345
368
|
? locale.loadingPlaceholder
|
|
@@ -423,7 +446,7 @@ function ChatbotInner({ appLabel, appName, objects, agents, agentsLoading, activ
|
|
|
423
446
|
});
|
|
424
447
|
return false;
|
|
425
448
|
}
|
|
426
|
-
}, publishDraftsLabel: locale.publishDrafts, nextStepsLabel: locale.nextSteps, planTitleLabel: locale.planTitle, planQuestionsLabel: locale.planQuestions, planAssumptionsLabel: locale.planAssumptions, planApproveHintLabel: locale.planApproveHint, planApproveLabel: locale.planApprove, planAdjustLabel: locale.planAdjust, planApproveMessage: locale.planApproveMessage, planApproveDefaultsMessage: locale.planApproveDefaultsMessage, planAnswerMessage: locale.planAnswer, publishedLabel: locale.published,
|
|
449
|
+
}, publishDraftsLabel: locale.publishDrafts, nextStepsLabel: locale.nextSteps, planTitleLabel: locale.planTitle, planQuestionsLabel: locale.planQuestions, planAssumptionsLabel: locale.planAssumptions, planApproveHintLabel: locale.planApproveHint, planApproveLabel: locale.planApprove, planAdjustLabel: locale.planAdjust, planBuiltLabel: locale.planBuilt, planApproveMessage: locale.planApproveMessage, planApproveDefaultsMessage: locale.planApproveDefaultsMessage, planAnswerMessage: locale.planAnswer, publishedLabel: locale.published,
|
|
427
450
|
// Self-use "magic moment": when the plan enables it, auto-publish the
|
|
428
451
|
// drafted app the instant the agent finishes — the success path above
|
|
429
452
|
// then navigates straight to the live app, so "build" lands the user on
|
|
@@ -434,10 +457,18 @@ export default function ConsoleFloatingChatbot({ appLabel, appName, objects, api
|
|
|
434
457
|
const apiBase = React.useMemo(() => resolveApiBase(apiBaseProp), [apiBaseProp]);
|
|
435
458
|
const env = import.meta.env ?? {};
|
|
436
459
|
const envDefaultAgent = env.VITE_AI_DEFAULT_AGENT;
|
|
437
|
-
// Power-user / admin escape hatch: force the picker on globally without
|
|
438
|
-
// touching app metadata.
|
|
439
|
-
const showAgentPicker = showAgentPickerProp ?? env.VITE_AI_SHOW_AGENT_PICKER === 'true';
|
|
440
460
|
const { agents, isLoading: agentsLoading, error: agentsError } = useAgents({ apiBase });
|
|
461
|
+
// Reveal the Build/Ask switcher only when AI development is unlocked for this
|
|
462
|
+
// viewer — the live catalog serves BOTH an `ask` and a `build` agent and
|
|
463
|
+
// authoring isn't deployment-disabled. Pure end-user apps (only `ask`) stay
|
|
464
|
+
// clean; builders can flip "ask about my data" ↔ "extend my app" inline. An
|
|
465
|
+
// explicit prop or `VITE_AI_SHOW_AGENT_PICKER` still forces it. See agentPicker.
|
|
466
|
+
const showAgentPicker = shouldShowAgentPicker({
|
|
467
|
+
agents,
|
|
468
|
+
showAgentPickerProp,
|
|
469
|
+
envOptIn: env.VITE_AI_SHOW_AGENT_PICKER === 'true',
|
|
470
|
+
aiStudioEnabled: getRuntimeConfig().features.aiStudio !== false,
|
|
471
|
+
});
|
|
441
472
|
const [activeAgent, setActiveAgent] = React.useState(undefined);
|
|
442
473
|
React.useEffect(() => {
|
|
443
474
|
if (!activeAgent && agents.length > 0) {
|
|
@@ -10,7 +10,6 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
10
10
|
*/
|
|
11
11
|
import { useEffect } from 'react';
|
|
12
12
|
import { AppShell } from '@object-ui/layout';
|
|
13
|
-
import { useDiscovery } from '@object-ui/react';
|
|
14
13
|
import { isBuildAgent } from '@object-ui/plugin-chatbot';
|
|
15
14
|
// Lightweight FAB stub — the heavy chat chunk graph (plugin-chatbot,
|
|
16
15
|
// shiki, streamdown, mermaid, @ai-sdk, ~20MB) only downloads on first
|
|
@@ -22,6 +21,7 @@ import { UnifiedSidebar } from './UnifiedSidebar';
|
|
|
22
21
|
import { AppHeader } from './AppHeader';
|
|
23
22
|
import { MobileViewSwitcherProvider } from './MobileViewSwitcherContext';
|
|
24
23
|
import { useResponsiveSidebar } from '../hooks/useResponsiveSidebar';
|
|
24
|
+
import { useAiSurfaceEnabled } from '../hooks/useAiSurface';
|
|
25
25
|
import { useNavigationContext } from '../context/NavigationContext';
|
|
26
26
|
import { CommandPaletteProvider } from '../context/CommandPaletteProvider';
|
|
27
27
|
import { resolveI18nLabel } from '../utils';
|
|
@@ -35,11 +35,10 @@ function ConsoleLayoutInner({ children }) {
|
|
|
35
35
|
// (moved to ./ConsoleFloatingChatbot.tsx for code-splitting)
|
|
36
36
|
export function ConsoleLayout({ children, activeAppName, activeApp, onAppChange, objects, connectionState, userId, }) {
|
|
37
37
|
const appLabel = resolveI18nLabel(activeApp?.label) || activeAppName;
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
const showChatbot = isAiEnabled || aiBaseUrlConfigured;
|
|
38
|
+
// Runtime, server-pushed AI gating (shared with the `/ai` route guard and the
|
|
39
|
+
// top-bar AI link): show the chatbot only when the server actually serves AI,
|
|
40
|
+
// or when an explicit `VITE_AI_BASE_URL` opt-in points at an external server.
|
|
41
|
+
const { enabled: showChatbot } = useAiSurfaceEnabled();
|
|
43
42
|
// AI Studio (AI-driven metadata authoring / "online development") can be
|
|
44
43
|
// turned off per deployment. When off, suppress the metadata-authoring
|
|
45
44
|
// assistant so the chatbot falls back to the generic data assistant — the
|