Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **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.

- **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.

- **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.

- **"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.
Expand Down
6 changes: 4 additions & 2 deletions apps/console/src/components/ConsoleLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import React from 'react';
import { AppShell } from '@object-ui/layout';
import { FloatingChatbot, useObjectChat, type ChatMessage } from '@object-ui/plugin-chatbot';
import { useDiscovery } from '@object-ui/react';
import { AppSidebar } from './AppSidebar';
import { AppHeader } from './AppHeader';
import { useResponsiveSidebar } from '../hooks/useResponsiveSidebar';
Expand Down Expand Up @@ -96,6 +97,7 @@ export function ConsoleLayout({
connectionState
}: ConsoleLayoutProps) {
const appLabel = resolveI18nLabel(activeApp?.label) || activeAppName;
const { isAiEnabled } = useDiscovery();

Comment on lines 99 to 101
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useDiscovery() reads from SchemaRendererContext, but ConsoleLayout is rendered outside the SchemaRendererProvider in App.tsx (the provider is nested inside ConsoleLayout). As a result, useDiscovery() will always see dataSource as undefined here, so isAiEnabled will always be false and the chatbot FAB will never render even when AI is available. Consider moving SchemaRendererProvider to wrap ConsoleLayout, or pass the console dataSource into ConsoleLayout (and/or add a useDiscovery(dataSource) overload) so discovery can be fetched in this component.

Copilot uses AI. Check for mistakes.
return (
<AppShell
Expand Down Expand Up @@ -131,8 +133,8 @@ export function ConsoleLayout({
{children}
</ConsoleLayoutInner>

{/* Global floating chatbot — available on every page */}
<ConsoleFloatingChatbot appLabel={appLabel} objects={objects} />
{/* Global floating chatbot — rendered only when AI service is available */}
{isAiEnabled && <ConsoleFloatingChatbot appLabel={appLabel} objects={objects} />}
</AppShell>
);
}
9 changes: 7 additions & 2 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,18 @@ Access server discovery information including preview mode detection:
import { useDiscovery } from '@object-ui/react'

function MyComponent() {
const { discovery, isLoading, isAuthEnabled } = useDiscovery()
const { discovery, isLoading, isAuthEnabled, isAiEnabled } = useDiscovery()

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

// Check if AI service is available
if (isAiEnabled) {
console.log('AI service route:', discovery?.services?.ai?.route)
}

return <div>Server: {discovery?.name}</div>
}
```
Expand All @@ -139,7 +144,7 @@ function MyComponent() {
| `version` | `string` | Server version |
| `mode` | `string` | Runtime mode (e.g. `'development'`, `'production'`, `'preview'`) |
| `previewMode` | `object` | Preview mode configuration (present when mode is `'preview'`) |
| `services` | `object` | Service availability status (auth, data, metadata) |
| `services` | `object` | Service availability status (auth, data, metadata, ai) |
| `capabilities` | `string[]` | API capabilities |

The `previewMode` object contains:
Expand Down
98 changes: 98 additions & 0 deletions packages/react/src/hooks/__tests__/useDiscovery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,104 @@ describe('useDiscovery', () => {
expect(result.current.isAuthEnabled).toBe(false);
});

it('isAiEnabled defaults to false when no discovery', async () => {
const { result } = renderHook(() => useDiscovery());

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

expect(result.current.isAiEnabled).toBe(false);
});

it('isAiEnabled is true when ai service is enabled and available', async () => {
const discoveryData = {
services: {
ai: { enabled: true, status: 'available' as const, route: '/api/v1/ai' },
},
};

const dataSource = {
getDiscovery: vi.fn().mockResolvedValue(discoveryData),
};

const { result } = renderHook(() => useDiscovery(), {
wrapper: createWrapper(dataSource),
});

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

expect(result.current.isAiEnabled).toBe(true);
});

it('isAiEnabled is false when ai service is enabled but unavailable', async () => {
const discoveryData = {
services: {
ai: { enabled: true, status: 'unavailable' as const },
},
};

const dataSource = {
getDiscovery: vi.fn().mockResolvedValue(discoveryData),
};

const { result } = renderHook(() => useDiscovery(), {
wrapper: createWrapper(dataSource),
});

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

expect(result.current.isAiEnabled).toBe(false);
});

it('isAiEnabled is false when ai service is disabled', async () => {
const discoveryData = {
services: {
ai: { enabled: false, status: 'available' as const },
},
};

const dataSource = {
getDiscovery: vi.fn().mockResolvedValue(discoveryData),
};

const { result } = renderHook(() => useDiscovery(), {
wrapper: createWrapper(dataSource),
});

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

expect(result.current.isAiEnabled).toBe(false);
});

it('isAiEnabled is false when ai service has no status', async () => {
const discoveryData = {
services: {
ai: { enabled: true },
},
};

const dataSource = {
getDiscovery: vi.fn().mockResolvedValue(discoveryData),
};

const { result } = renderHook(() => useDiscovery(), {
wrapper: createWrapper(dataSource),
});

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

expect(result.current.isAiEnabled).toBe(false);
});

it('cleans up on unmount (cancelled flag)', async () => {
let resolveDiscovery: (value: any) => void;
const discoveryPromise = new Promise((resolve) => {
Expand Down
14 changes: 14 additions & 0 deletions packages/react/src/hooks/useDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ export interface DiscoveryInfo {
enabled: boolean;
status?: 'available' | 'unavailable';
};
/** AI service configuration */
ai?: {
enabled: boolean;
status?: 'available' | 'unavailable';
/** AI service endpoint route (e.g. '/api/v1/ai') */
route?: string;
};
[key: string]: any;
};

Expand Down Expand Up @@ -149,6 +156,13 @@ export function useDiscovery() {
* Defaults to true if discovery data is not available.
*/
isAuthEnabled: discovery?.services?.auth?.enabled ?? true,
/**
* Check if AI service is enabled and available on the server.
* Defaults to false if discovery data is not available.
*/
isAiEnabled:
discovery?.services?.ai?.enabled === true &&
discovery?.services?.ai?.status === 'available',
};
}

Loading