Skip to content

Commit 8d603e9

Browse files
authored
Merge pull request #1173 from objectstack-ai/copilot/console-chatbot-render-when-ai-available
2 parents bb718db + 185e195 commit 8d603e9

5 files changed

Lines changed: 127 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- **AI service discovery** (`@object-ui/react`): Added `ai` service type to `DiscoveryInfo.services` interface with `enabled`, `status`, and `route` fields. Added `isAiEnabled` convenience property to `useDiscovery()` hook return value — returns `true` only when `services.ai.enabled === true` and `services.ai.status === 'available'`, defaults to `false` otherwise.
13+
14+
- **Conditional chatbot rendering** (`@object-ui/console`): Console floating chatbot (FAB) now only renders when the AI service is detected as available via `useDiscovery().isAiEnabled`. Previously the chatbot was always visible; now it is hidden when the server has no AI plugin installed.
15+
1216
- **Home page user menu** (`@object-ui/console`): Added complete user menu dropdown (Profile, Settings, Sign Out) to the Home Dashboard via new `HomeLayout` shell component. Users can now access account actions directly from the `/home` page without navigating elsewhere.
1317

1418
- **"Return to Home" navigation** (`@object-ui/console`): Added a "Home" entry in the AppSidebar app switcher dropdown, allowing users to navigate back to `/home` from any application context. Previously, the only way to return to the Home Dashboard was to manually edit the URL.

apps/console/src/components/ConsoleLayout.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import React from 'react';
1111
import { AppShell } from '@object-ui/layout';
1212
import { FloatingChatbot, useObjectChat, type ChatMessage } from '@object-ui/plugin-chatbot';
13+
import { useDiscovery } from '@object-ui/react';
1314
import { AppSidebar } from './AppSidebar';
1415
import { AppHeader } from './AppHeader';
1516
import { useResponsiveSidebar } from '../hooks/useResponsiveSidebar';
@@ -96,6 +97,7 @@ export function ConsoleLayout({
9697
connectionState
9798
}: ConsoleLayoutProps) {
9899
const appLabel = resolveI18nLabel(activeApp?.label) || activeAppName;
100+
const { isAiEnabled } = useDiscovery();
99101

100102
return (
101103
<AppShell
@@ -131,8 +133,8 @@ export function ConsoleLayout({
131133
{children}
132134
</ConsoleLayoutInner>
133135

134-
{/* Global floating chatbot — available on every page */}
135-
<ConsoleFloatingChatbot appLabel={appLabel} objects={objects} />
136+
{/* Global floating chatbot — rendered only when AI service is available */}
137+
{isAiEnabled && <ConsoleFloatingChatbot appLabel={appLabel} objects={objects} />}
136138
</AppShell>
137139
);
138140
}

packages/react/README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,18 @@ Access server discovery information including preview mode detection:
120120
import { useDiscovery } from '@object-ui/react'
121121

122122
function MyComponent() {
123-
const { discovery, isLoading, isAuthEnabled } = useDiscovery()
123+
const { discovery, isLoading, isAuthEnabled, isAiEnabled } = useDiscovery()
124124

125125
// Check if the server is in preview mode
126126
if (discovery?.mode === 'preview') {
127127
console.log('Preview mode active:', discovery.previewMode)
128128
}
129129

130+
// Check if AI service is available
131+
if (isAiEnabled) {
132+
console.log('AI service route:', discovery?.services?.ai?.route)
133+
}
134+
130135
return <div>Server: {discovery?.name}</div>
131136
}
132137
```
@@ -139,7 +144,7 @@ function MyComponent() {
139144
| `version` | `string` | Server version |
140145
| `mode` | `string` | Runtime mode (e.g. `'development'`, `'production'`, `'preview'`) |
141146
| `previewMode` | `object` | Preview mode configuration (present when mode is `'preview'`) |
142-
| `services` | `object` | Service availability status (auth, data, metadata) |
147+
| `services` | `object` | Service availability status (auth, data, metadata, ai) |
143148
| `capabilities` | `string[]` | API capabilities |
144149

145150
The `previewMode` object contains:

packages/react/src/hooks/__tests__/useDiscovery.test.tsx

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,104 @@ describe('useDiscovery', () => {
122122
expect(result.current.isAuthEnabled).toBe(false);
123123
});
124124

125+
it('isAiEnabled defaults to false when no discovery', async () => {
126+
const { result } = renderHook(() => useDiscovery());
127+
128+
await waitFor(() => {
129+
expect(result.current.isLoading).toBe(false);
130+
});
131+
132+
expect(result.current.isAiEnabled).toBe(false);
133+
});
134+
135+
it('isAiEnabled is true when ai service is enabled and available', async () => {
136+
const discoveryData = {
137+
services: {
138+
ai: { enabled: true, status: 'available' as const, route: '/api/v1/ai' },
139+
},
140+
};
141+
142+
const dataSource = {
143+
getDiscovery: vi.fn().mockResolvedValue(discoveryData),
144+
};
145+
146+
const { result } = renderHook(() => useDiscovery(), {
147+
wrapper: createWrapper(dataSource),
148+
});
149+
150+
await waitFor(() => {
151+
expect(result.current.isLoading).toBe(false);
152+
});
153+
154+
expect(result.current.isAiEnabled).toBe(true);
155+
});
156+
157+
it('isAiEnabled is false when ai service is enabled but unavailable', async () => {
158+
const discoveryData = {
159+
services: {
160+
ai: { enabled: true, status: 'unavailable' as const },
161+
},
162+
};
163+
164+
const dataSource = {
165+
getDiscovery: vi.fn().mockResolvedValue(discoveryData),
166+
};
167+
168+
const { result } = renderHook(() => useDiscovery(), {
169+
wrapper: createWrapper(dataSource),
170+
});
171+
172+
await waitFor(() => {
173+
expect(result.current.isLoading).toBe(false);
174+
});
175+
176+
expect(result.current.isAiEnabled).toBe(false);
177+
});
178+
179+
it('isAiEnabled is false when ai service is disabled', async () => {
180+
const discoveryData = {
181+
services: {
182+
ai: { enabled: false, status: 'available' as const },
183+
},
184+
};
185+
186+
const dataSource = {
187+
getDiscovery: vi.fn().mockResolvedValue(discoveryData),
188+
};
189+
190+
const { result } = renderHook(() => useDiscovery(), {
191+
wrapper: createWrapper(dataSource),
192+
});
193+
194+
await waitFor(() => {
195+
expect(result.current.isLoading).toBe(false);
196+
});
197+
198+
expect(result.current.isAiEnabled).toBe(false);
199+
});
200+
201+
it('isAiEnabled is false when ai service has no status', async () => {
202+
const discoveryData = {
203+
services: {
204+
ai: { enabled: true },
205+
},
206+
};
207+
208+
const dataSource = {
209+
getDiscovery: vi.fn().mockResolvedValue(discoveryData),
210+
};
211+
212+
const { result } = renderHook(() => useDiscovery(), {
213+
wrapper: createWrapper(dataSource),
214+
});
215+
216+
await waitFor(() => {
217+
expect(result.current.isLoading).toBe(false);
218+
});
219+
220+
expect(result.current.isAiEnabled).toBe(false);
221+
});
222+
125223
it('cleans up on unmount (cancelled flag)', async () => {
126224
let resolveDiscovery: (value: any) => void;
127225
const discoveryPromise = new Promise((resolve) => {

packages/react/src/hooks/useDiscovery.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ export interface DiscoveryInfo {
4949
enabled: boolean;
5050
status?: 'available' | 'unavailable';
5151
};
52+
/** AI service configuration */
53+
ai?: {
54+
enabled: boolean;
55+
status?: 'available' | 'unavailable';
56+
/** AI service endpoint route (e.g. '/api/v1/ai') */
57+
route?: string;
58+
};
5259
[key: string]: any;
5360
};
5461

@@ -149,6 +156,13 @@ export function useDiscovery() {
149156
* Defaults to true if discovery data is not available.
150157
*/
151158
isAuthEnabled: discovery?.services?.auth?.enabled ?? true,
159+
/**
160+
* Check if AI service is enabled and available on the server.
161+
* Defaults to false if discovery data is not available.
162+
*/
163+
isAiEnabled:
164+
discovery?.services?.ai?.enabled === true &&
165+
discovery?.services?.ai?.status === 'available',
152166
};
153167
}
154168

0 commit comments

Comments
 (0)