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: 3 additions & 1 deletion packages/apollo-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,9 @@
"dist/canvas/index.js",
"dist/canvas/index.cjs",
"dist/chat/index.js",
"dist/chat/index.cjs"
"dist/chat/index.cjs",
"dist/chat/root/ApChat.js",
"dist/chat/root/ApChat.cjs"
],
"files": [
"dist"
Expand Down
3 changes: 3 additions & 0 deletions packages/apollo-react/rslib.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export default defineConfig({
'!./src/**/*.test.{ts,tsx}',
'!./src/**/*.stories.{ts,tsx}',
'!./src/**/storybook-utils/**',
// Story harnesses and fixtures, imported only by stories.
'!./src/chat/stories/**',
'!./src/material/stories/**',
// Test-only, like `src/test/**`: fixtures are data for the suites, not API, and they
// reach for devDependencies (the dnd geometry helper needs `@testing-library/react`),
// so there is nothing to gain from publishing them.
Expand Down
11 changes: 9 additions & 2 deletions packages/apollo-react/src/chat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ The legacy `@uipath/apollo-react/ap-chat` and `@uipath/apollo-react/ap-chat/serv
| `service/` | `AutopilotChatService`, `EventBus`, models and constants. No React. |
| `headless/` | React providers, hooks and utilities with no visual output, plus the chat and message renderer registries. |
| `root/` | `ChatRoot`: the provider stack that resolves and mounts a renderer. |
| `wind/` | The Wind renderer (apollo-wind components and Tailwind classes). For now a placeholder that names the missing renderer. |
| `wind/` | The Wind renderer (apollo-wind components and Tailwind classes). Embedded and FullScreen are supported; SideBySide renders an explicit placeholder. |
| `stories/` | Storybook playground and the fixtures shared with the Material showcase. Not published. |
| `locales/` | Lingui catalogs for chat strings, shared by both renderers. |
| `styles/` | `tailwind.chat.css`, compiled to `dist/chat/styles/tailwind.chat.css`. |

Expand All @@ -24,10 +25,16 @@ Code under `src/chat` must not depend on Material UI, so a bundle that only uses
- No `@mui/*` or `@emotion/*` imports.
- No imports from `src/material`, either relative (`../material/...`) or via `@uipath/apollo-react/material*`.

The one exception is the lazy registration of the Material renderer in `index.ts`, a dynamic `import()` that is split into its own chunk and only loaded when `renderer: 'material'` is requested. `ApChat` from this entry defaults to the Wind renderer; `ApChat` from `@uipath/apollo-react/ap-chat` registers Material eagerly and defaults to it.
The one exception is the lazy registration of the Material renderer in `root/ApChat.tsx`, a dynamic `import()` that is split into its own chunk and only loaded when `renderer: 'material'` is requested. `ApChat` from this entry defaults to the Wind renderer; `ApChat` from `@uipath/apollo-react/ap-chat` registers Material eagerly and defaults to it.

The Material renderer depends on this folder, never the other way round.

## Wind renderer notes

- The root element carries the chat theme as a class (`light`, `dark`, `light-hc`, `dark-hc`), so it themes itself regardless of the host's body class. Menus and tooltips portal inside that root, or into a themed node inside `portalContainer` when the host passes one.
- `spacing.compactMode` sets `data-density="compact"` on the root.
- `icon` values on custom header actions and message actions are renderer-specific: Material takes Material Icons names, Wind takes lucide names (kebab-case such as `file-text`, or the PascalCase export name `FileText`). Wind loads each icon lazily through `lucide-react/dynamic` and shows a neutral box for names it cannot resolve.

## Styles

Consumers using the Wind renderer include the pre-compiled CSS:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
import type React from 'react';

/**
* The Autopilot mark shown next to the chat title in FullScreen. Colors follow the chat theme.
*/
export function AutopilotLogo(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
{...props}
>
<path
d="M10.9305 0.74464C11.2979 -0.248213 12.7021 -0.248213 13.0695 0.744639L14.2001 3.79992L17.2554 4.93047C18.2482 5.29786 18.2482 6.70214 17.2554 7.06953L14.2001 8.20008L13.0695 11.2554C12.7021 12.2482 11.2979 12.2482 10.9305 11.2554L9.79992 8.20008L6.74464 7.06953C5.75179 6.70214 5.75179 5.29786 6.74464 4.93047L9.79992 3.79992L10.9305 0.74464Z"
fill="var(--color-primary)" />
<path
fill="var(--color-primary)"
/>
<path
d="M3.28698 9.49643C3.53191 8.83452 4.46809 8.83452 4.71302 9.49643L5.46672 11.5333L7.50357 12.287C8.16547 12.5319 8.16547 13.4681 7.50357 13.713L5.46672 14.4667L4.71302 16.5036C4.46809 17.1655 3.53191 17.1655 3.28698 16.5036L2.53328 14.4667L0.496426 13.713C-0.165475 13.4681 -0.165476 12.5319 0.496426 12.287L2.53328 11.5333L3.28698 9.49643Z"
fill="var(--color-icon-default)" />
<path
fill="var(--color-icon-default)"
/>
<path
d="M15.4652 13.3723C15.6489 12.8759 16.3511 12.8759 16.5348 13.3723L17.1 14.9L18.6277 15.4652C19.1241 15.6489 19.1241 16.3511 18.6277 16.5348L17.1 17.1L16.5348 18.6277C16.3511 19.1241 15.6489 19.1241 15.4652 18.6277L14.9 17.1L13.3723 16.5348C12.8759 16.3511 12.8759 15.6489 13.3723 15.4652L14.9 14.9L15.4652 13.3723Z"
fill="var(--color-icon-default)" />
</svg>
fill="var(--color-icon-default)"
/>
</svg>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, expect, it } from 'vitest';

import { AutopilotChatMode } from '../../service';
import { getChatHeaderActionVisibility } from './use-chat-header-actions';

const visibility = (
chatMode: AutopilotChatMode,
disabledFeatures = {},
readOnly = false,
customActionCount = 0
) => getChatHeaderActionVisibility({ chatMode, disabledFeatures, readOnly, customActionCount });

describe('getChatHeaderActionVisibility', () => {
it('shows the built-ins outside Embedded', () => {
expect(visibility(AutopilotChatMode.FullScreen, {}, false, 1)).toEqual({
newChat: true,
settings: true,
history: true,
fullScreenToggle: true,
customActions: true,
close: true,
});
});

it('makes close opt-in and hides the full-screen toggle in Embedded', () => {
expect(visibility(AutopilotChatMode.Embedded)).toMatchObject({
close: false,
fullScreenToggle: false,
});
expect(visibility(AutopilotChatMode.Embedded, { close: false }).close).toBe(true);
});

it('hides editing actions when read-only but keeps close', () => {
expect(visibility(AutopilotChatMode.SideBySide, {}, true, 2)).toEqual({
newChat: false,
settings: false,
history: false,
fullScreenToggle: true,
customActions: false,
close: true,
});
});

it('honours each disabled feature', () => {
expect(
visibility(AutopilotChatMode.FullScreen, {
newChat: true,
settings: true,
history: true,
fullScreen: true,
close: true,
})
).toEqual({
newChat: false,
settings: false,
history: false,
fullScreenToggle: false,
customActions: false,
close: false,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import React from 'react';

import {
type AutopilotChatCustomHeaderAction,
type AutopilotChatDisabledFeatures,
AutopilotChatMode,
AutopilotChatPreHookAction,
} from '../../service';
import { useAttachments } from '../providers/attachments-provider';
import { useChatService } from '../providers/chat-service.provider';
import { useChatState } from '../providers/chat-state-provider';
import { usePicker } from '../providers/picker-provider';

export interface ChatHeaderActionVisibility {
newChat: boolean;
settings: boolean;
history: boolean;
fullScreenToggle: boolean;
customActions: boolean;
close: boolean;
}

export interface ChatHeaderActionVisibilityInput {
chatMode: AutopilotChatMode;
disabledFeatures: AutopilotChatDisabledFeatures;
readOnly: boolean;
customActionCount: number;
}

/**
* Which built-in header actions show. Close is opt-in for Embedded (`close: false`) and opt-out
* everywhere else.
*/
export function getChatHeaderActionVisibility({
chatMode,
disabledFeatures,
readOnly,
customActionCount,
}: ChatHeaderActionVisibilityInput): ChatHeaderActionVisibility {
const isEmbedded = chatMode === AutopilotChatMode.Embedded;

return {
newChat: !readOnly && !disabledFeatures.newChat,
settings: !readOnly && !disabledFeatures.settings,
history: !readOnly && !disabledFeatures.history,
fullScreenToggle: !disabledFeatures.fullScreen && !isEmbedded,
customActions: !readOnly && customActionCount > 0,
close: isEmbedded ? disabledFeatures.close === false : !disabledFeatures.close,
};
}

export interface ChatHeaderActions {
visibility: ChatHeaderActionVisibility;
isFullScreen: boolean;
customHeaderActions: AutopilotChatCustomHeaderAction[];
newChat: () => void;
toggleSettings: () => void;
toggleHistory: () => void;
toggleFullScreen: () => void;
close: () => void;
runCustomAction: (action: AutopilotChatCustomHeaderAction) => void;
}

export interface UseChatHeaderActionsOptions {
/**
* Mode to derive visibility from instead of the live one, for a renderer that keeps drawing its
* previous layout while it animates closed.
*/
mode?: AutopilotChatMode;
}

/**
* Header action visibility and handlers, each gated by its pre-hook, shared by every renderer.
*/
export function useChatHeaderActions({
mode,
}: UseChatHeaderActionsOptions = {}): ChatHeaderActions {
const chatService = useChatService();
const { disabledFeatures, chatMode, historyOpen, settingsOpen, readOnly } = useChatState();
const visibleMode = mode ?? chatMode;
const { clearAttachments } = useAttachments();
const { customHeaderActions, handleCustomHeaderAction } = usePicker();

const withPreHook = React.useCallback(
(action: AutopilotChatPreHookAction, data: unknown, run: () => void) => {
chatService
?.getPreHook(action)(data)
.then((proceed) => {
if (proceed) {
run();
}
});
},
[chatService]
);

const newChat = React.useCallback(() => {
withPreHook(AutopilotChatPreHookAction.NewChat, undefined, () => {
chatService.newChat();
chatService.stopResponse();
chatService.setPrompt('');
clearAttachments();
});
}, [chatService, clearAttachments, withPreHook]);

const toggleHistory = React.useCallback(() => {
withPreHook(AutopilotChatPreHookAction.ToggleHistory, { historyOpen }, () =>
chatService.toggleHistory()
);
}, [chatService, historyOpen, withPreHook]);

const toggleSettings = React.useCallback(() => {
withPreHook(AutopilotChatPreHookAction.ToggleSettings, { settingsOpen }, () =>
chatService.toggleSettings()
);
}, [chatService, settingsOpen, withPreHook]);

const toggleFullScreen = React.useCallback(() => {
withPreHook(AutopilotChatPreHookAction.ToggleChat, { chatMode }, () =>
chatService.setChatMode(
chatMode === AutopilotChatMode.FullScreen
? AutopilotChatMode.SideBySide
: AutopilotChatMode.FullScreen
)
);
}, [chatService, chatMode, withPreHook]);

const close = React.useCallback(() => {
withPreHook(AutopilotChatPreHookAction.CloseChat, undefined, () => chatService.close());
}, [chatService, withPreHook]);

const visibility = React.useMemo(
() =>
getChatHeaderActionVisibility({
chatMode: visibleMode,
disabledFeatures,
readOnly,
customActionCount: customHeaderActions.length,
}),
[visibleMode, disabledFeatures, readOnly, customHeaderActions.length]
);

return {
visibility,
isFullScreen: visibleMode === AutopilotChatMode.FullScreen,
customHeaderActions,
newChat,
toggleSettings,
toggleHistory,
toggleFullScreen,
close,
runCustomAction: handleCustomHeaderAction,
};
}
34 changes: 34 additions & 0 deletions packages/apollo-react/src/chat/headless/hooks/use-chat-labels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';

import { useChatState } from '../providers/chat-state-provider';

export interface ChatLabels {
title: string;
inputPlaceholder: string;
footerDisclaimer: string;
}

/**
* The service's `overrideLabels`, falling back to the localized defaults, for every renderer.
*/
export function useChatLabels(): ChatLabels {
const { _ } = useLingui();
const { overrideLabels } = useChatState();

return {
title:
overrideLabels.title ?? _(msg({ id: 'autopilot-chat.header.title', message: `Autopilot` })),
inputPlaceholder:
overrideLabels.inputPlaceholder ??
_(msg({ id: 'autopilot-chat.input.placeholder', message: `Type a message...` })),
footerDisclaimer:
overrideLabels.footerDisclaimer ??
_(
msg({
id: 'autopilot-chat.input.footer',
message: `AI can make mistakes. Check important info.`,
})
),
};
}
Loading
Loading