@object-ui/app-shell 11.0.0 → 11.3.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 +144 -0
- package/dist/chrome/LoadingScreen.js +1 -1
- package/dist/console/ConsoleShell.js +2 -1
- package/dist/console/RemediationOverlay.d.ts +17 -0
- package/dist/console/RemediationOverlay.js +113 -0
- package/dist/console/ai/AiChatPage.d.ts +36 -0
- package/dist/console/ai/AiChatPage.js +104 -13
- package/dist/console/home/CloudOnboardingNext.d.ts +10 -0
- package/dist/console/home/CloudOnboardingNext.js +117 -0
- package/dist/console/home/HomePage.js +2 -1
- package/dist/console/organizations/OrganizationsPage.js +6 -2
- package/dist/console/organizations/resolveHomeUrl.d.ts +7 -0
- package/dist/console/organizations/resolveHomeUrl.js +19 -2
- package/dist/hooks/useChatConversation.d.ts +22 -0
- package/dist/hooks/useChatConversation.js +57 -10
- package/dist/hooks/useReconcileOnError.js +12 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/layout/ConsoleFloatingChatbot.js +54 -7
- package/dist/layout/WorkspaceSwitcher.d.ts +4 -2
- package/dist/layout/WorkspaceSwitcher.js +15 -10
- package/dist/preview/DraftPreviewBar.js +15 -11
- package/dist/utils/index.d.ts +2 -24
- package/dist/utils/index.js +14 -101
- package/dist/views/DashboardView.js +2 -3
- package/dist/views/InterfaceListPage.js +4 -1
- package/dist/views/ObjectView.js +4 -2
- package/dist/views/PageView.js +14 -5
- package/dist/views/RecordDetailView.js +1 -0
- package/dist/views/ReportView.js +2 -3
- package/dist/views/metadata-admin/clientValidation.js +8 -2
- package/dist/views/metadata-admin/color-variant-field.d.ts +1 -12
- package/dist/views/metadata-admin/color-variant-field.js +11 -0
- package/dist/views/metadata-admin/previews/OutlineStrip.d.ts +1 -13
- package/dist/views/metadata-admin/previews/OutlineStrip.js +12 -0
- package/dist/views/metadata-admin/previews/PagePreview.js +9 -0
- package/dist/views/metadata-admin/previews/SourcePageEditor.d.ts +28 -0
- package/dist/views/metadata-admin/previews/SourcePageEditor.js +83 -0
- package/dist/views/studio-design/StudioDesignSurface.d.ts +20 -0
- package/dist/views/studio-design/StudioDesignSurface.js +659 -0
- package/package.json +43 -43
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,149 @@
|
|
|
1
1
|
# @object-ui/app-shell — Changelog
|
|
2
2
|
|
|
3
|
+
## 11.3.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ca4a795: fix(app-shell): restore admin design surface gated on the removed `user.role='admin'` overwrite
|
|
8
|
+
|
|
9
|
+
ADR-0068 (a3a5abff8) stopped the server `customSession` from overwriting
|
|
10
|
+
`user.role = 'admin'` for workspace owners/admins — canonical roles now arrive
|
|
11
|
+
in `user.roles[]` (`org_owner` / `org_admin`) with `user.isPlatformAdmin` as a
|
|
12
|
+
derived alias, and `useIsWorkspaceAdmin()` was introduced to read them. Four
|
|
13
|
+
runtime views were missed in that migration and still gated their admin design
|
|
14
|
+
tools on the now-defunct `user?.role === 'admin'`, so workspace owners/admins
|
|
15
|
+
silently lost:
|
|
16
|
+
|
|
17
|
+
- **ObjectView** — the list "+ New view" button plus rename/delete/pin/
|
|
18
|
+
set-default/config/manage-views and the view config panel.
|
|
19
|
+
- **PageView / DashboardView / ReportView** — the inline "Edit"/config entry
|
|
20
|
+
points for the shared page / dashboard / report definitions.
|
|
21
|
+
|
|
22
|
+
All four now call `useIsWorkspaceAdmin()` (same helper already adopted by
|
|
23
|
+
AppSidebar, UnifiedSidebar, HomePage, Marketplace…). No behavior change for
|
|
24
|
+
genuine platform admins; restores the surface for org owners/admins.
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [d88c8ec]
|
|
27
|
+
- Updated dependencies [b7237bb]
|
|
28
|
+
- Updated dependencies [d23d6eb]
|
|
29
|
+
- @object-ui/components@11.3.0
|
|
30
|
+
- @object-ui/i18n@11.3.0
|
|
31
|
+
- @object-ui/core@11.3.0
|
|
32
|
+
- @object-ui/fields@11.3.0
|
|
33
|
+
- @object-ui/layout@11.3.0
|
|
34
|
+
- @object-ui/plugin-editor@11.3.0
|
|
35
|
+
- @object-ui/react@11.3.0
|
|
36
|
+
- @object-ui/data-objectstack@11.3.0
|
|
37
|
+
- @object-ui/types@11.3.0
|
|
38
|
+
- @object-ui/auth@11.3.0
|
|
39
|
+
- @object-ui/permissions@11.3.0
|
|
40
|
+
- @object-ui/collaboration@11.3.0
|
|
41
|
+
- @object-ui/providers@11.3.0
|
|
42
|
+
|
|
43
|
+
## 11.2.0
|
|
44
|
+
|
|
45
|
+
### Minor Changes
|
|
46
|
+
|
|
47
|
+
- 490ba55: feat(cloud): state-aware onboarding next-step widget for the Cloud Welcome page
|
|
48
|
+
|
|
49
|
+
The Cloud control-plane Welcome page is static SDUI, but the most useful thing it
|
|
50
|
+
can show — "what do I do next?" — depends on live state the metadata can't carry:
|
|
51
|
+
does the caller's org already have its production environment? New signups are
|
|
52
|
+
auto-provisioned one, so a static "Step 1: create an environment" is wrong for
|
|
53
|
+
most first-time users.
|
|
54
|
+
|
|
55
|
+
Add `cloud:onboarding-next`, a registered SDUI widget that resolves
|
|
56
|
+
`hasProductionEnv` from the same org-scoped `/cloud/environment-entitlements`
|
|
57
|
+
endpoint the environment list uses, and renders the right primary action:
|
|
58
|
+
|
|
59
|
+
- no production env → **Create your environment** (the real first step);
|
|
60
|
+
- has production env → **Open Production** (full-page nav that follows the SSO
|
|
61
|
+
302 into the env) + **Manage environments**;
|
|
62
|
+
- loading → a neutral skeleton (no CTA flash / layout jump);
|
|
63
|
+
- unknown / error → degrades to the open-production actions, so the button
|
|
64
|
+
always works.
|
|
65
|
+
|
|
66
|
+
Routes and the SSO endpoint come from the page metadata (`properties`), so the
|
|
67
|
+
Cloud app owns its URLs and copy; the widget owns only the state logic.
|
|
68
|
+
|
|
69
|
+
- 32dbd6a: feat(detail): `relatedLayout: 'tabs'` — surface related tables as peer tabs via config
|
|
70
|
+
|
|
71
|
+
Record detail pages can now show each related table as its own top-level tab
|
|
72
|
+
instead of stacking them all inside a single **Related** tab — no custom page
|
|
73
|
+
required. Set `detail.relatedLayout: 'tabs'` on the object; the synthesized
|
|
74
|
+
record page then emits one tab per related list (label = the related list's
|
|
75
|
+
`title`, falling back to its `objectName`, carrying its `icon`), slotted between
|
|
76
|
+
the **Details** tab and **Activity** / **History**.
|
|
77
|
+
|
|
78
|
+
- `buildDefaultPageSchema` (`@object-ui/plugin-detail`): new
|
|
79
|
+
`BuildPageOptions.relatedLayout?: 'stack' | 'tabs'` threaded through
|
|
80
|
+
`buildDefaultTabs` (the single choke point for the related-tab emission).
|
|
81
|
+
`'tabs'` fans the related children out into peer tabs; `'stack'` (default)
|
|
82
|
+
keeps the legacy single **Related** tab — **zero regression** when omitted.
|
|
83
|
+
Still honours `hideRelatedTab` (no related tabs emitted) in both modes.
|
|
84
|
+
- `RecordDetailView` (`@object-ui/app-shell`): reads
|
|
85
|
+
`objectDef.detail.relatedLayout` per object and forwards it to the synth.
|
|
86
|
+
|
|
87
|
+
### Patch Changes
|
|
88
|
+
|
|
89
|
+
- Updated dependencies [9e7a986]
|
|
90
|
+
- Updated dependencies [1311749]
|
|
91
|
+
- @object-ui/components@11.2.0
|
|
92
|
+
- @object-ui/core@11.2.0
|
|
93
|
+
- @object-ui/fields@11.2.0
|
|
94
|
+
- @object-ui/layout@11.2.0
|
|
95
|
+
- @object-ui/plugin-editor@11.2.0
|
|
96
|
+
- @object-ui/data-objectstack@11.2.0
|
|
97
|
+
- @object-ui/react@11.2.0
|
|
98
|
+
- @object-ui/types@11.2.0
|
|
99
|
+
- @object-ui/i18n@11.2.0
|
|
100
|
+
- @object-ui/auth@11.2.0
|
|
101
|
+
- @object-ui/permissions@11.2.0
|
|
102
|
+
- @object-ui/collaboration@11.2.0
|
|
103
|
+
- @object-ui/providers@11.2.0
|
|
104
|
+
|
|
105
|
+
## 11.1.0
|
|
106
|
+
|
|
107
|
+
### Minor Changes
|
|
108
|
+
|
|
109
|
+
- 6fb6738: Auth: remediation overlay for the ADR-0069 session gate (enforced MFA / password expiry)
|
|
110
|
+
|
|
111
|
+
The ObjectStack backend now blocks logged-in users from protected resources with `403 { error: { code: 'MFA_REQUIRED' | 'PASSWORD_EXPIRED' } }`. The Console now detects this on every API response and raises a full-screen, guided remediation flow instead of leaving the user on failing requests.
|
|
112
|
+
|
|
113
|
+
- `@object-ui/auth`: the authenticated fetch wrapper detects the gate and broadcasts it via a tiny module-level emitter; `AuthProvider` exposes `remediationRequired` + `setRemediationRequired`; the `twoFactorClient` plugin is enabled and `enrollTotp` / `verifyTotp` are added to the auth client (`changePassword` already existed).
|
|
114
|
+
- `@object-ui/app-shell`: a `RemediationOverlay` (mounted in `ConsoleShell`) renders the guided flow — change an expired password, or enrol an authenticator (password confirm → QR + backup codes → verify TOTP) — then reloads so the app re-fetches cleanly. Auth + metadata + `me/*` reads stay reachable (server allow-list), so the overlay renders above a normally-loading shell.
|
|
115
|
+
|
|
116
|
+
### Patch Changes
|
|
117
|
+
|
|
118
|
+
- e2c9b0d: fix(first-run): two first-time-user friction fixes found via a full ObjectOS Cloud signup walkthrough.
|
|
119
|
+
|
|
120
|
+
- **Page-load race**: an app whose landing is a `type:'page'` (SDUI page) flashed a false "page not found" / blank body on the very first render — `PageView` treated the lazily-loading (empty) `pages` array as "page doesn't exist". It now shows a loading state until the `page` metadata type is actually resolved (`getTypeStatus('page')`), then trusts the not-found. This is exactly the post-signup landing, where the app's home page is the first thing rendered.
|
|
121
|
+
- **Redundant launcher hop**: after creating/switching a workspace, the user was hard-reloaded to `/home` (the workspace launcher) even when the workspace has a single app — an extra, contentless layer. `OrganizationsPage` and `WorkspaceSwitcher` now reload to the console ROOT (`resolveRootUrl`), so `RootLandingRedirect` resolves the right landing: a single-app workspace lands straight IN that app; multi-app workspaces still fall back to `/home`.
|
|
122
|
+
|
|
123
|
+
- 6726a2b: First-run UX polish (objectstack-ai/objectui#2038) — copy improvements found via the ObjectOS Cloud signup walkthrough:
|
|
124
|
+
|
|
125
|
+
- **"Organization" → "Workspace"** across the org picker (`organizations.*` strings, en + zh). The create flow + WorkspaceSwitcher already say "workspace"; the picker ("Your Organizations / No organizations yet") was the lone holdout. Now consistent.
|
|
126
|
+
- **Non-admin empty state** — "There are no applications available to you yet. Please contact your workspace administrator." → "Your workspace is being set up — apps your admin shares with you will show up here." (less dead-end, en + zh).
|
|
127
|
+
- **Cold-start reassurance** — new `console.loadingHint` line under the LoadingScreen steps: "Setting up a new environment can take a few moments." (en + zh).
|
|
128
|
+
- **Signup value-prop** — register subtitle "Enter your information to get started" → "Create your account to start building." (en + zh).
|
|
129
|
+
|
|
130
|
+
- 8e2223c: fix(home): the workspace empty-state title hardcoded "Welcome to ObjectUI" — a stale brand a first-time user sees on their empty `/home`. Read the product name from the runtime-config branding (`getRuntimeConfig().branding.productName`, server-pushed, fallback "ObjectOS") like LoadingScreen does, so it shows the deployment's real product (e.g. "Welcome to ObjectOS Cloud").
|
|
131
|
+
- Updated dependencies [6fb6738]
|
|
132
|
+
- Updated dependencies [6726a2b]
|
|
133
|
+
- @object-ui/auth@11.1.0
|
|
134
|
+
- @object-ui/i18n@11.1.0
|
|
135
|
+
- @object-ui/components@11.1.0
|
|
136
|
+
- @object-ui/fields@11.1.0
|
|
137
|
+
- @object-ui/react@11.1.0
|
|
138
|
+
- @object-ui/layout@11.1.0
|
|
139
|
+
- @object-ui/plugin-editor@11.1.0
|
|
140
|
+
- @object-ui/types@11.1.0
|
|
141
|
+
- @object-ui/core@11.1.0
|
|
142
|
+
- @object-ui/data-objectstack@11.1.0
|
|
143
|
+
- @object-ui/permissions@11.1.0
|
|
144
|
+
- @object-ui/collaboration@11.1.0
|
|
145
|
+
- @object-ui/providers@11.1.0
|
|
146
|
+
|
|
3
147
|
## 7.3.0
|
|
4
148
|
|
|
5
149
|
### Patch Changes
|
|
@@ -36,5 +36,5 @@ export function LoadingScreen({ message, error, onRetry, retrying }) {
|
|
|
36
36
|
}, 1200);
|
|
37
37
|
return () => clearInterval(timer);
|
|
38
38
|
}, [message, error, loadingSteps.length]);
|
|
39
|
-
return (_jsx("div", { className: "flex flex-col items-center justify-center h-screen bg-background", children: _jsxs("div", { className: "flex flex-col items-center gap-6", children: [_jsxs("div", { className: "relative", children: [_jsx("div", { className: "absolute inset-0 bg-primary/20 rounded-2xl blur-xl animate-pulse" }), _jsx("div", { className: "relative bg-linear-to-br from-primary to-primary/80 p-4 rounded-2xl shadow-lg", children: _jsx(Database, { className: "h-10 w-10 text-primary-foreground" }) })] }), _jsxs("div", { className: "text-center space-y-2", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: getProductName() }), _jsx("p", { className: "text-sm text-muted-foreground", children: strings.initializing })] }), error ? (_jsxs("div", { className: "flex flex-col items-center gap-4 max-w-md w-full px-6", children: [_jsxs("div", { className: "flex flex-col items-center gap-3 p-5 rounded-lg border border-destructive/30 bg-destructive/5 w-full", children: [_jsxs("div", { className: "flex items-center gap-2 text-destructive", children: [_jsx(AlertCircle, { className: "h-5 w-5 shrink-0" }), _jsx("span", { className: "text-sm font-semibold", children: strings.error.connectionFailed })] }), _jsx("p", { className: "text-xs text-muted-foreground text-center break-words", children: error }), _jsx("p", { className: "text-xs text-muted-foreground text-center", children: strings.error.checkServer })] }), onRetry && (_jsx(Button, { onClick: onRetry, disabled: retrying, variant: "default", size: "sm", className: "gap-2", children: retrying ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), strings.actions.retrying] })) : (_jsxs(_Fragment, { children: [_jsx(RefreshCw, { className: "h-4 w-4" }), strings.actions.retry] })) }))] })) : message ? (_jsxs("div", { className: "flex items-center gap-3 px-4 py-2 bg-muted/50 rounded-full", children: [_jsx(Spinner, { className: "h-4 w-4 text-primary" }), _jsx("span", { className: "text-sm text-muted-foreground", children: message })] })) : (
|
|
39
|
+
return (_jsx("div", { className: "flex flex-col items-center justify-center h-screen bg-background", children: _jsxs("div", { className: "flex flex-col items-center gap-6", children: [_jsxs("div", { className: "relative", children: [_jsx("div", { className: "absolute inset-0 bg-primary/20 rounded-2xl blur-xl animate-pulse" }), _jsx("div", { className: "relative bg-linear-to-br from-primary to-primary/80 p-4 rounded-2xl shadow-lg", children: _jsx(Database, { className: "h-10 w-10 text-primary-foreground" }) })] }), _jsxs("div", { className: "text-center space-y-2", children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: getProductName() }), _jsx("p", { className: "text-sm text-muted-foreground", children: strings.initializing })] }), error ? (_jsxs("div", { className: "flex flex-col items-center gap-4 max-w-md w-full px-6", children: [_jsxs("div", { className: "flex flex-col items-center gap-3 p-5 rounded-lg border border-destructive/30 bg-destructive/5 w-full", children: [_jsxs("div", { className: "flex items-center gap-2 text-destructive", children: [_jsx(AlertCircle, { className: "h-5 w-5 shrink-0" }), _jsx("span", { className: "text-sm font-semibold", children: strings.error.connectionFailed })] }), _jsx("p", { className: "text-xs text-muted-foreground text-center break-words", children: error }), _jsx("p", { className: "text-xs text-muted-foreground text-center", children: strings.error.checkServer })] }), onRetry && (_jsx(Button, { onClick: onRetry, disabled: retrying, variant: "default", size: "sm", className: "gap-2", children: retrying ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), strings.actions.retrying] })) : (_jsxs(_Fragment, { children: [_jsx(RefreshCw, { className: "h-4 w-4" }), strings.actions.retry] })) }))] })) : message ? (_jsxs("div", { className: "flex items-center gap-3 px-4 py-2 bg-muted/50 rounded-full", children: [_jsx(Spinner, { className: "h-4 w-4 text-primary" }), _jsx("span", { className: "text-sm text-muted-foreground", children: message })] })) : (_jsxs("div", { className: "flex flex-col gap-2 w-64", children: [loadingSteps.map((step, index) => (_jsxs("div", { className: "flex items-center gap-2.5 text-sm transition-opacity duration-300", style: { opacity: index <= currentStep ? 1 : 0.3 }, children: [index < currentStep ? (_jsx(CheckCircle2, { className: "h-4 w-4 text-primary shrink-0" })) : index === currentStep ? (_jsx(Loader2, { className: "h-4 w-4 text-primary shrink-0 animate-spin" })) : (_jsx("div", { className: "h-4 w-4 rounded-full border border-muted-foreground/30 shrink-0" })), _jsx("span", { className: index <= currentStep ? 'text-foreground' : 'text-muted-foreground', children: step })] }, step))), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: strings.loadingHint })] }))] }) }));
|
|
40
40
|
}
|
|
@@ -25,6 +25,7 @@ import { FavoritesProvider } from '../context/FavoritesProvider';
|
|
|
25
25
|
import { RecentItemsProvider } from '../context/RecentItemsProvider';
|
|
26
26
|
import { UserStateAdaptersProvider, useAttachUserStateAdapters, } from '../context/UserStateAdapters';
|
|
27
27
|
import { ThemeProvider } from '../chrome/ThemeProvider';
|
|
28
|
+
import { RemediationOverlay } from './RemediationOverlay';
|
|
28
29
|
export function LoadingFallback() {
|
|
29
30
|
return (_jsx("div", { className: "h-screen flex items-center justify-center text-sm text-muted-foreground", children: "Loading\u2026" }));
|
|
30
31
|
}
|
|
@@ -43,7 +44,7 @@ export function LoadingFallback() {
|
|
|
43
44
|
* </BrowserRouter>
|
|
44
45
|
*/
|
|
45
46
|
export function ConsoleShell({ children }) {
|
|
46
|
-
return (_jsx(ThemeProvider, { defaultTheme: "system", storageKey: "object-ui-theme", children: _jsx(NavigationProvider, { children: _jsx(UserStateAdaptersProvider, { children: _jsx(FavoritesProvider, { children:
|
|
47
|
+
return (_jsx(ThemeProvider, { defaultTheme: "system", storageKey: "object-ui-theme", children: _jsx(NavigationProvider, { children: _jsx(UserStateAdaptersProvider, { children: _jsx(FavoritesProvider, { children: _jsxs(RecentItemsProvider, { children: [_jsx(Suspense, { fallback: _jsx(LoadingFallback, {}), children: children }), _jsx(RemediationOverlay, {})] }) }) }) }) }));
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
49
50
|
* ConnectedShell — mounts the data layer (AdapterProvider + MetadataProvider).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* ADR-0069 — full-screen remediation overlay.
|
|
10
|
+
*
|
|
11
|
+
* When the backend blocks a logged-in user with `403 PASSWORD_EXPIRED` /
|
|
12
|
+
* `MFA_REQUIRED`, the API fetch interceptor raises `remediationRequired` on the
|
|
13
|
+
* auth context. This overlay guides the user through the fix (change the
|
|
14
|
+
* expired password, or enrol an authenticator) instead of leaving them staring
|
|
15
|
+
* at failing requests. On success it reloads so the app re-fetches cleanly.
|
|
16
|
+
*/
|
|
17
|
+
export declare function RemediationOverlay(): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* ObjectUI
|
|
4
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*/
|
|
9
|
+
import { useState } from 'react';
|
|
10
|
+
import { useAuth } from '@object-ui/auth';
|
|
11
|
+
import { Button, Input, Label } from '@object-ui/components';
|
|
12
|
+
import { toCanvas } from 'qrcode';
|
|
13
|
+
/**
|
|
14
|
+
* ADR-0069 — full-screen remediation overlay.
|
|
15
|
+
*
|
|
16
|
+
* When the backend blocks a logged-in user with `403 PASSWORD_EXPIRED` /
|
|
17
|
+
* `MFA_REQUIRED`, the API fetch interceptor raises `remediationRequired` on the
|
|
18
|
+
* auth context. This overlay guides the user through the fix (change the
|
|
19
|
+
* expired password, or enrol an authenticator) instead of leaving them staring
|
|
20
|
+
* at failing requests. On success it reloads so the app re-fetches cleanly.
|
|
21
|
+
*/
|
|
22
|
+
export function RemediationOverlay() {
|
|
23
|
+
const { remediationRequired } = useAuth();
|
|
24
|
+
if (!remediationRequired)
|
|
25
|
+
return null;
|
|
26
|
+
return (_jsx("div", { className: "fixed inset-0 z-[200] flex items-center justify-center bg-background/95 p-4 backdrop-blur-sm", children: _jsx("div", { className: "w-full max-w-md rounded-xl border bg-card p-6 shadow-xl", children: remediationRequired.code === 'PASSWORD_EXPIRED' ? (_jsx(ExpiredPasswordForm, { message: remediationRequired.message })) : (_jsx(MfaEnrollForm, { message: remediationRequired.message })) }) }));
|
|
27
|
+
}
|
|
28
|
+
function SignOutLink() {
|
|
29
|
+
const { signOut } = useAuth();
|
|
30
|
+
return (_jsx("button", { type: "button", onClick: () => { void signOut(); }, className: "mt-4 w-full text-center text-xs text-muted-foreground hover:text-foreground hover:underline", children: "Sign out instead" }));
|
|
31
|
+
}
|
|
32
|
+
function ExpiredPasswordForm({ message }) {
|
|
33
|
+
const { changePassword, setRemediationRequired } = useAuth();
|
|
34
|
+
const [current, setCurrent] = useState('');
|
|
35
|
+
const [next, setNext] = useState('');
|
|
36
|
+
const [confirm, setConfirm] = useState('');
|
|
37
|
+
const [busy, setBusy] = useState(false);
|
|
38
|
+
const [error, setError] = useState(null);
|
|
39
|
+
const submit = async (e) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
setError(null);
|
|
42
|
+
if (next !== confirm) {
|
|
43
|
+
setError('New passwords do not match.');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
setBusy(true);
|
|
47
|
+
try {
|
|
48
|
+
await changePassword(current, next);
|
|
49
|
+
setRemediationRequired(null);
|
|
50
|
+
window.location.reload();
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
setError(err instanceof Error ? err.message : 'Could not change password.');
|
|
54
|
+
setBusy(false);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
return (_jsxs("form", { onSubmit: submit, className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold", children: "Your password has expired" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: message || 'Please set a new password to continue.' })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "rem-cur", children: "Current password" }), _jsx(Input, { id: "rem-cur", type: "password", autoComplete: "current-password", value: current, onChange: (e) => setCurrent(e.target.value), required: true })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "rem-new", children: "New password" }), _jsx(Input, { id: "rem-new", type: "password", autoComplete: "new-password", value: next, onChange: (e) => setNext(e.target.value), required: true })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "rem-conf", children: "Confirm new password" }), _jsx(Input, { id: "rem-conf", type: "password", autoComplete: "new-password", value: confirm, onChange: (e) => setConfirm(e.target.value), required: true })] }), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null, _jsx(Button, { type: "submit", className: "w-full", disabled: busy, children: busy ? 'Updating…' : 'Change password & continue' }), _jsx(SignOutLink, {})] }));
|
|
58
|
+
}
|
|
59
|
+
function TotpQr({ uri }) {
|
|
60
|
+
const [error, setError] = useState(null);
|
|
61
|
+
const ref = (node) => {
|
|
62
|
+
if (!node || !uri)
|
|
63
|
+
return;
|
|
64
|
+
toCanvas(node, uri, { width: 184, margin: 1 }, (err) => { if (err)
|
|
65
|
+
setError(err.message); });
|
|
66
|
+
};
|
|
67
|
+
return (_jsx("div", { className: "flex justify-center rounded-md border bg-white p-3", children: error ? _jsx("span", { className: "text-sm text-destructive", children: error }) : _jsx("canvas", { ref: ref }) }));
|
|
68
|
+
}
|
|
69
|
+
function MfaEnrollForm({ message }) {
|
|
70
|
+
const { enrollTotp, verifyTotp, setRemediationRequired } = useAuth();
|
|
71
|
+
const [step, setStep] = useState('password');
|
|
72
|
+
const [password, setPassword] = useState('');
|
|
73
|
+
const [code, setCode] = useState('');
|
|
74
|
+
const [totpUri, setTotpUri] = useState('');
|
|
75
|
+
const [backupCodes, setBackupCodes] = useState([]);
|
|
76
|
+
const [busy, setBusy] = useState(false);
|
|
77
|
+
const [error, setError] = useState(null);
|
|
78
|
+
const start = async (e) => {
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
setError(null);
|
|
81
|
+
setBusy(true);
|
|
82
|
+
try {
|
|
83
|
+
const { totpURI, backupCodes: codes } = await enrollTotp(password);
|
|
84
|
+
setTotpUri(totpURI);
|
|
85
|
+
setBackupCodes(codes ?? []);
|
|
86
|
+
setStep('verify');
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
setError(err instanceof Error ? err.message : 'Could not start enrollment.');
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
setBusy(false);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const verify = async (e) => {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
setError(null);
|
|
98
|
+
setBusy(true);
|
|
99
|
+
try {
|
|
100
|
+
await verifyTotp(code.trim());
|
|
101
|
+
setRemediationRequired(null);
|
|
102
|
+
window.location.reload();
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
setError(err instanceof Error ? err.message : 'Invalid code. Try again.');
|
|
106
|
+
setBusy(false);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
if (step === 'password') {
|
|
110
|
+
return (_jsxs("form", { onSubmit: start, className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold", children: "Set up two-factor authentication" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: message || 'Your organization requires an authenticator app to continue.' })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "rem-pw", children: "Confirm your password" }), _jsx(Input, { id: "rem-pw", type: "password", autoComplete: "current-password", value: password, onChange: (e) => setPassword(e.target.value), required: true })] }), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null, _jsx(Button, { type: "submit", className: "w-full", disabled: busy, children: busy ? 'Preparing…' : 'Continue' }), _jsx(SignOutLink, {})] }));
|
|
111
|
+
}
|
|
112
|
+
return (_jsxs("form", { onSubmit: verify, className: "space-y-4", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold", children: "Scan with your authenticator" }), _jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Scan this QR code with Google Authenticator, 1Password, Authy, etc., then enter the 6-digit code." })] }), totpUri ? _jsx(TotpQr, { uri: totpUri }) : null, backupCodes.length > 0 ? (_jsxs("details", { className: "rounded-md border bg-muted/40 p-3 text-xs", children: [_jsx("summary", { className: "cursor-pointer font-medium", children: "Save your backup codes" }), _jsx("p", { className: "mt-1 text-muted-foreground", children: "Store these somewhere safe \u2014 each can be used once if you lose your device." }), _jsx("div", { className: "mt-2 grid grid-cols-2 gap-1 font-mono", children: backupCodes.map((c) => _jsx("span", { children: c }, c)) })] })) : null, _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "rem-code", children: "6-digit code" }), _jsx(Input, { id: "rem-code", inputMode: "numeric", autoComplete: "one-time-code", maxLength: 8, placeholder: "123456", value: code, onChange: (e) => setCode(e.target.value), required: true })] }), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null, _jsx(Button, { type: "submit", className: "w-full", disabled: busy, children: busy ? 'Verifying…' : 'Verify & continue' }), _jsx(SignOutLink, {})] }));
|
|
113
|
+
}
|
|
@@ -91,5 +91,41 @@ export declare function matchAiChatShortcut(e: {
|
|
|
91
91
|
shiftKey: boolean;
|
|
92
92
|
altKey: boolean;
|
|
93
93
|
}): AiChatShortcut | null;
|
|
94
|
+
/** A composer submission held until the conversation id that will carry it exists. */
|
|
95
|
+
export interface PendingFirstMessage {
|
|
96
|
+
content: string;
|
|
97
|
+
files?: File[];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Guards the empty-state FIRST send against the conversation-id remount race.
|
|
101
|
+
*
|
|
102
|
+
* On a fresh `/ai/:agent` the composer (and its suggestion chips) go live the
|
|
103
|
+
* instant the agent resolves — BEFORE `POST /conversations` has minted the id.
|
|
104
|
+
* `<ChatPane>` is keyed on that id (`…:pending` → `…:<id>`), so the moment it
|
|
105
|
+
* resolves React UNMOUNTS the pane and mounts a fresh one. A first message
|
|
106
|
+
* submitted in that window lives inside the doomed pane: its optimistic bubble
|
|
107
|
+
* is discarded and the just-started `…/chat` request is aborted before it
|
|
108
|
+
* reaches the wire — so it vanishes silently (input cleared, no bubble, no
|
|
109
|
+
* error). Distinct from the #2047 path, where `…/chat` WAS sent and failed.
|
|
110
|
+
*
|
|
111
|
+
* The fix: when we have a server endpoint but no id yet, stash the send in a ref
|
|
112
|
+
* the PAGE owns (so it outlives the pane remount) and replay it the moment an id
|
|
113
|
+
* exists — in the freshly-mounted pane (or in place if no remount happened). A
|
|
114
|
+
* send made once the id is present (the normal path, and every send after the
|
|
115
|
+
* first) goes straight through. Local/echo mode (no `chatApi`) also sends
|
|
116
|
+
* immediately, so the offline-demo bot keeps responding.
|
|
117
|
+
*
|
|
118
|
+
* Exported for unit testing.
|
|
119
|
+
*/
|
|
120
|
+
export declare function useDeferredFirstSend(opts: {
|
|
121
|
+
/** The chat endpoint — defined once an agent is resolved (server-backed mode). */
|
|
122
|
+
chatApi: string | undefined;
|
|
123
|
+
/** The conversation id; undefined while `POST /conversations` is in flight. */
|
|
124
|
+
conversationId: string | undefined;
|
|
125
|
+
/** Page-owned stash that OUTLIVES the keyed `<ChatPane>` remount. */
|
|
126
|
+
pendingRef: React.MutableRefObject<PendingFirstMessage | null>;
|
|
127
|
+
/** The real send (resetSuppression + sendMessage + onSent). */
|
|
128
|
+
doSend: (content: string, files?: File[]) => void;
|
|
129
|
+
}): (content: string, files?: File[]) => void;
|
|
94
130
|
export declare function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentProp }?: AiChatPageProps): React.JSX.Element;
|
|
95
131
|
export default AiChatPage;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
3
3
|
/**
|
|
4
4
|
* AiChatPage — full-page ChatGPT-style AI surface.
|
|
@@ -17,7 +17,7 @@ import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
|
|
17
17
|
import { useAuth } from '@object-ui/auth';
|
|
18
18
|
import { useObjectTranslation } from '@object-ui/i18n';
|
|
19
19
|
import { toast } from 'sonner';
|
|
20
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Button, ShareDialog, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, Empty, EmptyTitle, EmptyDescription, cn, } from '@object-ui/components';
|
|
20
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Tabs, TabsList, TabsTrigger, Button, ShareDialog, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, Empty, EmptyTitle, EmptyDescription, cn, } from '@object-ui/components';
|
|
21
21
|
import { Bug, PanelLeft, PanelLeftClose, PanelLeftOpen, Share2 } from 'lucide-react';
|
|
22
22
|
import { ChatbotEnhanced, useAgents, useObjectChat, useAiModels, useHitlInChat, resolveDefaultAgentName, PLATFORM_DEFAULT_AGENT, agentRouteName, resolveAgentParam, isBuiltinAgentName, isBuildAgent, isAskAgent, publishHealthFromResponse, detectDraftResult, detectProposedPlan, buildProgressFromDraftReview, } from '@object-ui/plugin-chatbot';
|
|
23
23
|
import { AppHeader } from '../../layout/AppHeader';
|
|
@@ -347,6 +347,55 @@ export function matchAiChatShortcut(e) {
|
|
|
347
347
|
return null;
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Guards the empty-state FIRST send against the conversation-id remount race.
|
|
352
|
+
*
|
|
353
|
+
* On a fresh `/ai/:agent` the composer (and its suggestion chips) go live the
|
|
354
|
+
* instant the agent resolves — BEFORE `POST /conversations` has minted the id.
|
|
355
|
+
* `<ChatPane>` is keyed on that id (`…:pending` → `…:<id>`), so the moment it
|
|
356
|
+
* resolves React UNMOUNTS the pane and mounts a fresh one. A first message
|
|
357
|
+
* submitted in that window lives inside the doomed pane: its optimistic bubble
|
|
358
|
+
* is discarded and the just-started `…/chat` request is aborted before it
|
|
359
|
+
* reaches the wire — so it vanishes silently (input cleared, no bubble, no
|
|
360
|
+
* error). Distinct from the #2047 path, where `…/chat` WAS sent and failed.
|
|
361
|
+
*
|
|
362
|
+
* The fix: when we have a server endpoint but no id yet, stash the send in a ref
|
|
363
|
+
* the PAGE owns (so it outlives the pane remount) and replay it the moment an id
|
|
364
|
+
* exists — in the freshly-mounted pane (or in place if no remount happened). A
|
|
365
|
+
* send made once the id is present (the normal path, and every send after the
|
|
366
|
+
* first) goes straight through. Local/echo mode (no `chatApi`) also sends
|
|
367
|
+
* immediately, so the offline-demo bot keeps responding.
|
|
368
|
+
*
|
|
369
|
+
* Exported for unit testing.
|
|
370
|
+
*/
|
|
371
|
+
export function useDeferredFirstSend(opts) {
|
|
372
|
+
const { chatApi, conversationId, pendingRef, doSend } = opts;
|
|
373
|
+
const apiMode = Boolean(chatApi);
|
|
374
|
+
// Replay a deferred first message the instant a conversation id exists — in
|
|
375
|
+
// the freshly-mounted pane after the remount, or in place if none happened.
|
|
376
|
+
// Clearing the ref before sending makes the replay fire at most once even
|
|
377
|
+
// though `doSend` (and StrictMode's double-invoke) re-run this effect.
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
if (!apiMode || !conversationId)
|
|
380
|
+
return;
|
|
381
|
+
const pending = pendingRef.current;
|
|
382
|
+
if (!pending)
|
|
383
|
+
return;
|
|
384
|
+
pendingRef.current = null;
|
|
385
|
+
doSend(pending.content, pending.files);
|
|
386
|
+
}, [apiMode, conversationId, pendingRef, doSend]);
|
|
387
|
+
return useCallback((content, files) => {
|
|
388
|
+
// Server-backed, but the id is still being minted: sending now would be
|
|
389
|
+
// lost — a convId-less `/chat` the server won't persist, or a request torn
|
|
390
|
+
// down when the pane remounts as the id resolves. Stash it; the effect
|
|
391
|
+
// above replays it once the id lands.
|
|
392
|
+
if (apiMode && !conversationId) {
|
|
393
|
+
pendingRef.current = { content, files };
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
doSend(content, files);
|
|
397
|
+
}, [apiMode, conversationId, pendingRef, doSend]);
|
|
398
|
+
}
|
|
350
399
|
export function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentProp } = {}) {
|
|
351
400
|
const { user } = useAuth();
|
|
352
401
|
const { t } = useObjectTranslation();
|
|
@@ -587,6 +636,11 @@ export function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentPro
|
|
|
587
636
|
return;
|
|
588
637
|
setTitleHints((current) => current[conversationId] === hint ? current : { ...current, [conversationId]: hint });
|
|
589
638
|
}, [conversationId, initialMessages]);
|
|
639
|
+
// Holds an empty-state first message submitted before the conversation id was
|
|
640
|
+
// minted. Owned by the PAGE (not the keyed <ChatPane>) so it survives the
|
|
641
|
+
// remount that the id-resolution triggers; the freshly-mounted pane replays it
|
|
642
|
+
// via useDeferredFirstSend. See that hook for the full race.
|
|
643
|
+
const pendingFirstMessageRef = useRef(null);
|
|
590
644
|
const handleSent = useCallback((firstUserMessage) => {
|
|
591
645
|
// New user turn → bump sidebar list so the row's preview/timestamp refreshes.
|
|
592
646
|
setRefreshKey((k) => k + 1);
|
|
@@ -612,11 +666,11 @@ export function AiChatPage({ apiBase: apiBaseProp, defaultAgent: defaultAgentPro
|
|
|
612
666
|
void t1;
|
|
613
667
|
void t2;
|
|
614
668
|
}, [conversationId]);
|
|
615
|
-
return (_jsxs("div", { className: "flex h-svh w-full flex-col bg-background", "data-testid": "ai-chat-page", children: [_jsxs("header", { className: "sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background/95 px-2 backdrop-blur sm:px-4", children: [!noAgents && (
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
669
|
+
return (_jsxs("div", { className: "flex h-svh w-full flex-col bg-background", "data-testid": "ai-chat-page", children: [_jsxs("header", { className: "sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background/95 px-2 backdrop-blur sm:px-4", children: [!noAgents && (_jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 shrink-0 md:hidden", onClick: () => setMobileChatsOpen(true), "aria-label": t('console.ai.openChats'), "data-testid": "ai-chat-mobile-sidebar-trigger", children: _jsx(PanelLeft, { className: "h-4 w-4" }) })), _jsx("div", { className: "min-w-0 flex-1", children: _jsx(AppHeader, { variant: "home" }) })] }), noAgents ? (_jsx(AiUnavailable, { hasError: Boolean(agentsError), onRetry: refetchAgents, onHome: () => navigate('/home'), t: t })) : (_jsxs(_Fragment, { children: [_jsx(Sheet, { open: mobileChatsOpen, onOpenChange: setMobileChatsOpen, children: _jsxs(SheetContent, { side: "left", className: "w-[320px] p-0 sm:max-w-[360px]", "data-testid": "ai-chat-mobile-sidebar", children: [_jsxs(SheetHeader, { className: "sr-only", children: [_jsx(SheetTitle, { children: t('console.ai.chats') }), _jsx(SheetDescription, { children: t('console.ai.chatsDescription') })] }), _jsx(ConversationsSidebar, { userId: userId, apiBase: apiBase, activeAgent: activeAgent, refreshKey: refreshKey, titleHints: titleHints, className: "h-full border-r-0", onNavigate: () => setMobileChatsOpen(false) })] }) }), conversationId && (_jsx(ShareDialog, { open: shareOpen, onOpenChange: setShareOpen, objectName: "ai_conversations", recordId: conversationId, recordLabel: "this conversation", apiBase: restApiBase, publicBaseUrl: publicShareBase })), conversationId && isBuildAgent(activeAgent) && (_jsx(BuildDebugDrawer, { apiBase: apiBase, conversationId: conversationId, open: debugOpen, onOpenChange: setDebugOpen })), _jsxs("div", { className: "flex min-h-0 flex-1 w-full bg-background", children: [_jsxs("div", { className: "hidden shrink-0 flex-col border-r md:flex", children: [!chatsCollapsed && (_jsx(ConversationsSidebar, { userId: userId, apiBase: apiBase, activeAgent: activeAgent, refreshKey: refreshKey, titleHints: titleHints, className: "w-72 min-h-0 flex-1 border-r-0" })), _jsx("div", { className: cn('mt-auto p-2', chatsCollapsed ? 'w-12' : 'w-72 border-t'), children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8", onClick: toggleChatsCollapsed, "aria-label": chatsCollapsed
|
|
670
|
+
? t('console.ai.showChats', { defaultValue: 'Show chats' })
|
|
671
|
+
: t('console.ai.hideChats', { defaultValue: 'Hide chats' }), title: chatsCollapsed
|
|
672
|
+
? t('console.ai.showChats', { defaultValue: 'Show chats' })
|
|
673
|
+
: t('console.ai.hideChats', { defaultValue: 'Hide chats' }), "data-testid": "ai-chat-collapse-sidebar-trigger", "aria-pressed": chatsCollapsed, children: chatsCollapsed ? _jsx(PanelLeftOpen, { className: "h-4 w-4" }) : _jsx(PanelLeftClose, { className: "h-4 w-4" }) }) })] }), _jsx("main", { className: "flex min-w-0 flex-1 flex-col", children: _jsx(ChatPane, { agents: agents, agentsLoading: agentsLoading, agentsError: agentsError, activeAgent: activeAgent, chatApi: chatApi, apiBase: apiBase, conversationId: conversationId, editPackageId: editPackageId, initialMessages: initialMessages, pendingFirstMessageRef: pendingFirstMessageRef, onSent: handleSent, onShare: () => setShareOpen(true), onDebug: () => setDebugOpen(true), showDebug: isBuildAgent(activeAgent), onCanvasOpenChange: handleCanvasOpenChange }, `${chatApi ?? 'local'}:${conversationId ?? 'pending'}`) })] })] }))] }));
|
|
620
674
|
}
|
|
621
675
|
/**
|
|
622
676
|
* Graceful state for `/ai` when the agent catalog resolved empty — shown
|
|
@@ -634,7 +688,7 @@ function AiUnavailable({ hasError, onRetry, onHome, t, }) {
|
|
|
634
688
|
defaultValue: "This deployment doesn't have an AI assistant enabled. Everything else works as usual.",
|
|
635
689
|
}) }), _jsxs("div", { className: "mt-6 flex flex-col items-center gap-3 sm:flex-row", children: [hasError && (_jsx(Button, { variant: "outline", onClick: onRetry, "data-testid": "ai-unavailable-retry", children: t('console.ai.unavailableRetry', { defaultValue: 'Try again' }) })), _jsx(Button, { onClick: onHome, "data-testid": "ai-unavailable-home", children: t('console.ai.unavailableHome', { defaultValue: 'Back to home' }) })] })] }) }));
|
|
636
690
|
}
|
|
637
|
-
function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, apiBase, conversationId, editPackageId, initialMessages, onSent, onShare, onDebug, showDebug, onCanvasOpenChange, }) {
|
|
691
|
+
function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, apiBase, conversationId, editPackageId, initialMessages, pendingFirstMessageRef, onSent, onShare, onDebug, showDebug, onCanvasOpenChange, }) {
|
|
638
692
|
const { t } = useObjectTranslation();
|
|
639
693
|
const navigate = useNavigate();
|
|
640
694
|
// The agent dropdown is a LAUNCHER now (not an in-surface mode toggle): it
|
|
@@ -755,12 +809,24 @@ function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, ap
|
|
|
755
809
|
sendMessage(prompt);
|
|
756
810
|
},
|
|
757
811
|
});
|
|
758
|
-
|
|
812
|
+
// The real send, shared by a normal submit and by the deferred-first-message
|
|
813
|
+
// replay below (resetSuppression → send → onSent, in that order).
|
|
814
|
+
const doSend = useCallback((content, files) => {
|
|
759
815
|
resetSuppression();
|
|
760
816
|
sendMessage(content, files);
|
|
761
817
|
onSent(content);
|
|
762
|
-
}, [sendMessage, onSent]);
|
|
763
|
-
|
|
818
|
+
}, [resetSuppression, sendMessage, onSent]);
|
|
819
|
+
// Guards the empty-state first send against the conversation-id remount race:
|
|
820
|
+
// a send made before the id is minted is stashed in the page-owned ref and
|
|
821
|
+
// replayed once the id lands, so the magic-moment first message reliably
|
|
822
|
+
// reaches `…/chat` instead of being dropped. See useDeferredFirstSend.
|
|
823
|
+
const handleSend = useDeferredFirstSend({
|
|
824
|
+
chatApi,
|
|
825
|
+
conversationId,
|
|
826
|
+
pendingRef: pendingFirstMessageRef,
|
|
827
|
+
doSend,
|
|
828
|
+
});
|
|
829
|
+
const headerSlot = (_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 border-b border-border/50 px-4 pb-2 pt-3 sm:px-6", children: [_jsx("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: showAgentLauncher ? (agents.length <= 3 ? (_jsx(Tabs, { value: activeAgent, onValueChange: (name) => navigate(`/ai/${agentRouteName(name)}`), children: _jsx(TabsList, { className: "h-7 gap-0.5 p-0.5", "data-testid": "ai-chat-agent-picker", "aria-label": t('console.ai.switchAssistant', { defaultValue: 'Switch assistant' }), children: agents.map((agent) => (_jsx(TabsTrigger, { value: agent.name, disabled: agentsLoading, title: agent.description || undefined, className: "h-6 px-2.5 text-xs", children: localizeAgentLabel(t, agent.name, agent.label) }, agent.name))) }) })) : (_jsxs(Select, { value: activeAgent, onValueChange: (name) => navigate(`/ai/${agentRouteName(name)}`), disabled: agentsLoading, children: [_jsx(SelectTrigger, { className: "h-7 w-auto min-w-0 border-0 bg-transparent px-1.5 text-xs shadow-none hover:bg-accent focus:ring-0 focus:ring-offset-0 focus-visible:ring-1 focus-visible:ring-border/80 focus-visible:ring-offset-0 sm:min-w-[160px]", "data-testid": "ai-chat-agent-picker", "aria-label": t('console.ai.switchAssistant', { defaultValue: 'Switch assistant' }), children: _jsx(SelectValue, { placeholder: t('console.ai.chooseAgent', { defaultValue: 'Choose assistant…' }) }) }), _jsx(SelectContent, { align: "start", children: agents.map((agent) => (_jsxs(SelectItem, { value: agent.name, className: "text-xs", children: [_jsx("span", { className: "font-medium", children: localizeAgentLabel(t, agent.name, agent.label) }), agent.description ? (_jsx("span", { className: "block text-muted-foreground text-[10px] truncate max-w-[260px]", children: agent.description })) : null] }, agent.name))) })] }))) : (_jsx("span", { className: "truncate text-xs font-medium text-foreground/85", children: activeAgentLabel })) }), _jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [showDebug && onDebug ? (_jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7 text-muted-foreground hover:text-foreground", onClick: onDebug, disabled: !conversationId, "aria-label": "Build Doctor", "data-testid": "ai-chat-debug-button", title: conversationId ? 'Build Doctor — what actually landed?' : 'Send a message first', children: _jsx(Bug, { className: "h-3.5 w-3.5" }) })) : null, _jsx(Button, { variant: "ghost", size: "icon", className: "h-7 w-7 text-muted-foreground hover:text-foreground", onClick: onShare, disabled: !conversationId, "aria-label": t('console.ai.share'), "data-testid": "ai-chat-share-button", title: conversationId ? t('console.ai.shareTitle') : t('console.ai.shareDisabledTitle'), children: _jsx(Share2, { className: "h-3.5 w-3.5" }) })] }), agentsError ? (_jsx("span", { className: "basis-full text-[10px] text-amber-700 dark:text-amber-400", title: agentsError.message, children: t('console.ai.offlineDemoMode') })) : null] }));
|
|
764
830
|
return (_jsxs("div", { ref: split.containerRef, className: "relative flex min-h-0 flex-1 px-0", children: [_jsx("div", { "data-chat-column": true, className: canvasApp
|
|
765
831
|
? 'flex min-h-0 shrink-0 justify-center'
|
|
766
832
|
: 'flex min-h-0 flex-1 justify-center', style: canvasApp ? { width: split.width } : undefined, children: _jsx(ChatbotEnhanced, { className: "min-h-0 flex-1 bg-background md:max-w-5xl", onUpgrade: () => window.open(cloudPricingDeepLink(), '_blank', 'noopener,noreferrer'), surface: "plain", maxHeight: "100%", headerSlot: headerSlot, messages: messages, placeholder: activeAgent
|
|
@@ -784,6 +850,23 @@ function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, ap
|
|
|
784
850
|
connectionWaiting: t('console.ai.connectionWaiting', { defaultValue: 'Waiting for server…' }),
|
|
785
851
|
connectionStalledLabel: t('console.ai.connectionStalled', { defaultValue: 'Still working…' }),
|
|
786
852
|
connectionOfflineLabel: t('console.ai.connectionOffline', { defaultValue: 'Connection lost — reconnecting…' }),
|
|
853
|
+
// Friendly in-progress feedback for the long, atomic propose_blueprint
|
|
854
|
+
// call — a lead-in plus rotating hints so the wait reads as deliberate
|
|
855
|
+
// design work, not a hang. Each hint is translated individually (the
|
|
856
|
+
// established per-string pattern; avoids an i18n returnObjects array).
|
|
857
|
+
designingPlanLabel: t('console.ai.designingPlan', { defaultValue: 'Designing your app…' }),
|
|
858
|
+
designingPlanHints: [
|
|
859
|
+
t('console.ai.designingPlanHint.data', { defaultValue: 'Mapping out the data you’ll track…' }),
|
|
860
|
+
t('console.ai.designingPlanHint.objects', { defaultValue: 'Shaping objects and their fields…' }),
|
|
861
|
+
t('console.ai.designingPlanHint.relations', { defaultValue: 'Connecting related records…' }),
|
|
862
|
+
t('console.ai.designingPlanHint.lookups', { defaultValue: 'Setting up relationships and lookups…' }),
|
|
863
|
+
t('console.ai.designingPlanHint.views', { defaultValue: 'Planning the screens and views…' }),
|
|
864
|
+
t('console.ai.designingPlanHint.forms', { defaultValue: 'Laying out forms and lists…' }),
|
|
865
|
+
t('console.ai.designingPlanHint.defaults', { defaultValue: 'Adding sensible defaults and validations…' }),
|
|
866
|
+
t('console.ai.designingPlanHint.dashboard', { defaultValue: 'Sketching a dashboard to track it…' }),
|
|
867
|
+
t('console.ai.designingPlanHint.review', { defaultValue: 'Double-checking the structure hangs together…' }),
|
|
868
|
+
t('console.ai.designingPlanHint.finalize', { defaultValue: 'Pulling the plan together…' }),
|
|
869
|
+
],
|
|
787
870
|
toolDetailsHidden: t('console.ai.toolDetailsHidden'),
|
|
788
871
|
copy: t('console.ai.copy'),
|
|
789
872
|
copied: t('console.ai.copied'),
|
|
@@ -792,6 +875,12 @@ function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, ap
|
|
|
792
875
|
submit: t('console.ai.submit'),
|
|
793
876
|
uploadFiles: t('console.ai.uploadFiles'),
|
|
794
877
|
stopResponse: t('console.ai.stopResponse'),
|
|
878
|
+
sendFailedRateLimited: t('console.ai.sendFailedRateLimited', {
|
|
879
|
+
defaultValue: "You're sending messages too quickly. Your message is kept below — wait a moment and try again.",
|
|
880
|
+
}),
|
|
881
|
+
sendFailedGeneric: t('console.ai.sendFailedGeneric', {
|
|
882
|
+
defaultValue: "Couldn't send your message. It's kept below — please try again.",
|
|
883
|
+
}),
|
|
795
884
|
trace: t('console.ai.trace'),
|
|
796
885
|
viewTrace: t('console.ai.viewTrace'),
|
|
797
886
|
},
|
|
@@ -851,9 +940,11 @@ function ChatPane({ agents, agentsLoading, agentsError, activeAgent, chatApi, ap
|
|
|
851
940
|
});
|
|
852
941
|
return false;
|
|
853
942
|
}
|
|
854
|
-
}, publishDraftsLabel: t('console.ai.publishDrafts', { defaultValue: 'Publish' }), publishedLabel: t('console.ai.published', { defaultValue: 'Published' }), nextStepsLabel: t('console.ai.nextSteps', { defaultValue: "What's next" }), planTitleLabel: t('console.ai.planTitle', { defaultValue: 'Proposed plan' }), planQuestionsLabel: t('console.ai.planQuestions', { defaultValue: 'Confirm before building' }), planAssumptionsLabel: t('console.ai.planAssumptions', { defaultValue: 'Assumptions' }), planApproveHintLabel: t('console.ai.planApproveHint', {
|
|
943
|
+
}, publishDraftsLabel: t('console.ai.publishDrafts', { defaultValue: 'Publish' }), publishedLabel: t('console.ai.published', { defaultValue: 'Published' }), nextStepsLabel: t('console.ai.nextSteps', { defaultValue: "What's next" }), planTitleLabel: t('console.ai.planTitle', { defaultValue: 'Proposed plan' }), planQuestionsLabel: t('console.ai.planQuestions', { defaultValue: 'Confirm before building' }), planAssumptionsLabel: t('console.ai.planAssumptions', { defaultValue: 'Assumptions' }), planDeferredLabel: t('console.ai.planDeferred', { defaultValue: 'Not yet built' }), planApproveHintLabel: t('console.ai.planApproveHint', {
|
|
855
944
|
defaultValue: 'Reply to approve or adjust this plan.',
|
|
856
|
-
}), planApproveLabel: t('console.ai.planApprove', { defaultValue: 'Build it' }), planAdjustLabel: t('console.ai.planAdjust', { defaultValue: 'Adjust' }), planBuiltLabel: t('console.ai.planBuilt', { defaultValue: 'Built' }),
|
|
945
|
+
}), planApproveLabel: t('console.ai.planApprove', { defaultValue: 'Build it' }), planAdjustLabel: t('console.ai.planAdjust', { defaultValue: 'Adjust' }), planBuiltLabel: t('console.ai.planBuilt', { defaultValue: 'Built' }), planReadyLabel: t('console.ai.planReady', {
|
|
946
|
+
defaultValue: 'The plan is ready. Build it now, or tell me what to adjust.',
|
|
947
|
+
}), planApproveMessage: t('console.ai.planApproveMessage', {
|
|
857
948
|
defaultValue: 'Looks good — build it as proposed.',
|
|
858
949
|
}), planApproveDefaultsMessage: t('console.ai.planApproveDefaultsMessage', {
|
|
859
950
|
defaultValue: 'Build it with your best assumptions; use sensible defaults for the open questions.',
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface CloudOnboardingNextProps {
|
|
2
|
+
properties?: {
|
|
3
|
+
/** Backend SSO-open endpoint (full-page nav that 302s into the prod env). */
|
|
4
|
+
openProductionUrl?: string;
|
|
5
|
+
/** SPA route to the environments list (create / open / manage). */
|
|
6
|
+
environmentsRoute?: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export declare function CloudOnboardingNext({ properties }: CloudOnboardingNextProps): import("react").JSX.Element;
|
|
10
|
+
export {};
|