(false);
+
+ const availableExportOptions = EXPORT_OPTIONS.filter((option) =>
+ option.displayTypes.includes(interfaceDisplayType),
+ );
+
+ useEffect(() => {
+ const getAvailableOptions = () => {
+ return EXPORT_OPTIONS.filter((option) => option.displayTypes.includes(interfaceDisplayType));
+ };
+ const apiExportConfig = interfaceConfig?.export_config;
+ const availableOptions = getAvailableOptions();
+ const displayTypeChanged = previousDisplayType.current !== interfaceDisplayType;
+
+ // Helper to get embeddable assistant flag for current display type
+ const getEmbeddableFlag = (method: string): boolean => {
+ return interfaceDisplayType === INTERFACE_DISPLAY_TYPE.FULL_PAGE && method === 'embed';
+ };
+
+ // Get current config from store directly to avoid stale closures
+ const getCurrentConfig = () => useAgentStore.getState().exportConfig;
+ const updateConfig = useAgentStore.getState().setExportConfig;
+
+ // Helper to update config if values differ
+ const updateConfigIfNeeded = (method: string, embeddableAssistant: boolean) => {
+ const currentConfig = getCurrentConfig();
+ if (
+ currentConfig.method !== method ||
+ currentConfig.embeddable_assistant !== embeddableAssistant
+ ) {
+ updateConfig({
+ ...currentConfig,
+ method,
+ embeddable_assistant: embeddableAssistant,
+ });
+ }
+ };
+
+ // Initialization logic
+ if (!isInitialized.current) {
+ const hasDataAppId = !!interfaceConfig?.dataAppId;
+
+ if (!hasDataAppId && !apiExportConfig) return;
+
+ const currentConfig = getCurrentConfig();
+
+ // Get initial method and flag from API or existing config
+ let method: string;
+ let embeddableAssistant: boolean;
+
+ if (apiExportConfig) {
+ embeddableAssistant = apiExportConfig.embeddable_assistant ?? false;
+ const currentMethod = apiExportConfig.method ?? 'embed';
+ method = embeddableAssistant ? 'embed' : currentMethod;
+ } else if (currentConfig.method && currentConfig.embeddable_assistant !== undefined) {
+ embeddableAssistant = currentConfig.embeddable_assistant;
+ method = embeddableAssistant ? 'embed' : currentConfig.method;
+ } else {
+ method = 'embed';
+ embeddableAssistant = false;
+ }
+
+ // Validate method is available for current display type
+ const currentOption = availableOptions.find((option) => option.value === method);
+ if (!currentOption) {
+ const firstOption = availableOptions[0];
+ if (!firstOption) return;
+
+ method = firstOption.value;
+ embeddableAssistant = getEmbeddableFlag(method);
+ }
+
+ methodPerDisplayType.current[interfaceDisplayType] = method;
+ updateConfigIfNeeded(method, embeddableAssistant);
+ isInitialized.current = true;
+ return;
+ }
+
+ // Handle display type change
+ if (displayTypeChanged) {
+ const currentConfig = getCurrentConfig();
+
+ // Save previous method if it was valid
+ const previousMethodWasValid = EXPORT_OPTIONS.some(
+ (option) =>
+ option.value === currentConfig.method &&
+ option.displayTypes.includes(previousDisplayType.current),
+ );
+
+ if (previousMethodWasValid) {
+ methodPerDisplayType.current[previousDisplayType.current] = currentConfig.method;
+ }
+
+ previousDisplayType.current = interfaceDisplayType;
+
+ // Try to restore saved method for new display type
+ const savedMethod = methodPerDisplayType.current[interfaceDisplayType];
+ const savedMethodIsValid =
+ savedMethod && availableOptions.some((option) => option.value === savedMethod);
+
+ if (savedMethodIsValid) {
+ const embeddableAssistant = getEmbeddableFlag(savedMethod);
+ updateConfigIfNeeded(savedMethod, embeddableAssistant);
+ return;
+ }
+
+ // Check if API value should be used
+ const shouldUseApiValue =
+ apiExportConfig?.embeddable_assistant &&
+ interfaceDisplayType === INTERFACE_DISPLAY_TYPE.FULL_PAGE &&
+ !savedMethod;
+
+ if (shouldUseApiValue) {
+ updateConfigIfNeeded('embed', true);
+ methodPerDisplayType.current[interfaceDisplayType] = 'embed';
+ return;
+ }
+
+ // Fall back to first available option
+ const firstAvailableOption = availableOptions[0];
+ if (!firstAvailableOption) return;
+
+ const embeddableAssistant = getEmbeddableFlag(firstAvailableOption.value);
+ updateConfigIfNeeded(firstAvailableOption.value, embeddableAssistant);
+ methodPerDisplayType.current[interfaceDisplayType] = firstAvailableOption.value;
+ return;
+ }
+
+ // Validate and update method (no display type change)
+ const currentConfig = getCurrentConfig();
+
+ if (currentConfig.embeddable_assistant && currentConfig.method !== 'embed') {
+ updateConfig({ ...currentConfig, method: 'embed' });
+ methodPerDisplayType.current[interfaceDisplayType] = 'embed';
+ return;
+ }
+
+ const currentOption = availableOptions.find((option) => option.value === currentConfig.method);
+
+ if (!currentOption) {
+ const firstAvailableOption = availableOptions[0];
+ if (!firstAvailableOption) return;
+
+ const embeddableAssistant = getEmbeddableFlag(firstAvailableOption.value);
+ updateConfig({
+ ...currentConfig,
+ method: firstAvailableOption.value,
+ embeddable_assistant: embeddableAssistant,
+ });
+ methodPerDisplayType.current[interfaceDisplayType] = firstAvailableOption.value;
+ return;
+ }
+
+ methodPerDisplayType.current[interfaceDisplayType] = currentConfig.method;
+
+ const expectedEmbeddableAssistant = getEmbeddableFlag(currentConfig.method);
+
+ if (currentConfig.embeddable_assistant !== expectedEmbeddableAssistant) {
+ updateConfig({
+ ...currentConfig,
+ embeddable_assistant: expectedEmbeddableAssistant,
+ });
+ }
+ }, [interfaceDisplayType, interfaceConfig?.dataAppId, interfaceConfig?.export_config]);
+
+ return (
+
+
+
+
+ Method
+
+ {
+ const isEmbeddableAssistant =
+ interfaceDisplayType === INTERFACE_DISPLAY_TYPE.FULL_PAGE && value === 'embed';
+ const newMethod = value as string;
+
+ methodPerDisplayType.current[interfaceDisplayType] = newMethod;
+
+ setExportConfig({
+ ...exportConfig,
+ method: newMethod,
+ embeddable_assistant: isEmbeddableAssistant,
+ });
+ }}
+ placeholder='Select a method'
+ >
+ {EXPORT_OPTIONS.filter((option) =>
+ option.displayTypes.includes(interfaceDisplayType),
+ ).map((option, index) => (
+
+ ))}
+
+
+ {
+ availableExportOptions.find((option) => option.value === exportConfig.method)
+ ?.helperText
+ }
+
+
+
+ {/* Dynamic content based on selected method */}
+ {exportConfig.method === 'embed' && (
+
+ )}
+ {exportConfig.method === 'no_code' && (
+
+ )}
+ {exportConfig.method === 'assistant' && (
+
+ )}
+ {exportConfig.method === 'embed' &&
+ interfaceDisplayType === INTERFACE_DISPLAY_TYPE.FULL_PAGE && (
+
+
+ Query Selector
+
+
+ {
+ setExportConfig({
+ ...exportConfig,
+ query_selector: e.target.value,
+ });
+ }}
+ value={exportConfig.query_selector}
+ borderStyle='solid'
+ borderWidth='1px'
+ borderColor='gray.400'
+ fontSize='14px'
+ borderRadius='6px'
+ _focusVisible={{ border: 'gray.400' }}
+ _hover={{ border: 'gray.400' }}
+ name='query_selector'
+ />
+
+ Container is injected at the body of the content by default. Use query selectors to
+ inject in specific elements.
+
+
+ )}
+ {(exportConfig.method === 'no_code' || exportConfig.method === 'embed') &&
+ !exportConfig.embeddable_assistant &&
+ interfaceDisplayType === INTERFACE_DISPLAY_TYPE.MOBILE && (
+ <>
+
+
+
+ Interface Position
+
+
+
+
+
+
+
+ {
+ setExportConfig({
+ ...exportConfig,
+ interface_position: value as string,
+ });
+ }}
+ >
+ {INTERFACE_POSITION_OPTIONS.map((option, index) => (
+
+ ))}
+
+
+ >
+ )}
+ {exportConfig.method === 'no_code' && (
+ {
+ setExportConfig({
+ ...exportConfig,
+ whitelist_urls: value.target.value.split(',').map((url) => url.trim()),
+ });
+ }}
+ isTooltip
+ tooltipLabel={
+ 'Specify which URLs the chatbot can be embedded on. Enter comma-separated URLs to restrict where the interface appears.'
+ }
+ placeholder='Enter one or more URLs separated by commas.'
+ />
+ )}
+
+
+ );
+};
+
+export default ChatbotExportConfig;
diff --git a/ui/src/enterprise/views/Agents/Interface/ConfigWrapper.tsx b/ui/src/enterprise/views/Agents/Interface/ConfigWrapper.tsx
new file mode 100644
index 000000000..eef2e31c5
--- /dev/null
+++ b/ui/src/enterprise/views/Agents/Interface/ConfigWrapper.tsx
@@ -0,0 +1,362 @@
+import {
+ Stack,
+ Button,
+ HStack,
+ Icon,
+ Text,
+ Collapse,
+ Box,
+ Divider,
+ Switch,
+} from '@chakra-ui/react';
+import { useState } from 'react';
+import { FiChevronDown, FiChevronUp } from 'react-icons/fi';
+import { INTERFACE_TYPE } from '../types';
+import FeedbackConfigForm from '../../DataApps/DataAppsForm/BuildDataApp/FeedbackConfigForm';
+import { FEEDBACK_METHODS } from '@/enterprise/dataApps/feedbackTypes';
+import { WorkflowInterfaceConfig } from '@/enterprise/services/types';
+
+import { useParams } from 'react-router-dom';
+
+import ChatGeneralConfig from './ChatGeneralConfig';
+import ApiGeneralConfig from './ApiInterface/ApiGeneralConfig';
+import SlackGeneralConfig from './SlackInterface/SlackGeneralConfig';
+import SlackExportConfig from './SlackInterface/SlackExportConfig';
+import ChatbotExportConfig from './ChatbotExportConfig';
+import Security from './Security/Security';
+
+const ConfigWrapper = ({
+ selectedInterfaceType,
+ interfaceComponentConfig,
+ setInterfaceComponentConfig,
+}: {
+ selectedInterfaceType: INTERFACE_TYPE;
+ interfaceComponentConfig: WorkflowInterfaceConfig;
+ setInterfaceComponentConfig: (config: WorkflowInterfaceConfig) => void;
+}) => {
+ const [openState, setOpenState] = useState({
+ isGeneralOpen: true,
+ isFeedbackOpen: false,
+ isSecurityOpen: false,
+ isExportOpen: false,
+ });
+
+ const params = useParams();
+
+ const agentId = params.id ?? '';
+
+ return (
+
+
+
+
+ {selectedInterfaceType === INTERFACE_TYPE.WEBSITE_CHATBOT && (
+
+ )}
+ {selectedInterfaceType === INTERFACE_TYPE.API_INTERFACE && }
+ {selectedInterfaceType === INTERFACE_TYPE.SLACK_APP && }
+
+
+ {(selectedInterfaceType === INTERFACE_TYPE.WEBSITE_CHATBOT ||
+ selectedInterfaceType === INTERFACE_TYPE.SLACK_APP) && (
+ <>
+
+
+
+
+
+
+
+
+ Enable Feedback
+
+ {
+ setInterfaceComponentConfig({
+ ...interfaceComponentConfig,
+ feedback_config: {
+ ...interfaceComponentConfig.feedback_config,
+ feedback_enabled: event.target.checked,
+ },
+ });
+ }}
+ isChecked={interfaceComponentConfig.feedback_config.feedback_enabled}
+ />
+
+ {interfaceComponentConfig.feedback_config.feedback_enabled && (
+ {
+ setInterfaceComponentConfig({
+ ...interfaceComponentConfig,
+ feedback_config: {
+ ...interfaceComponentConfig.feedback_config,
+ feedback_method:
+ INTERFACE_TYPE.SLACK_APP === selectedInterfaceType
+ ? FEEDBACK_METHODS.THUMBS_RATING
+ : (value as FEEDBACK_METHODS),
+ },
+ });
+ }}
+ setFeedbackTitle={(value) => {
+ setInterfaceComponentConfig({
+ ...interfaceComponentConfig,
+ feedback_config: {
+ ...interfaceComponentConfig.feedback_config,
+ feedback_title: value,
+ },
+ });
+ }}
+ setFeedbackDescription={(value) => {
+ setInterfaceComponentConfig({
+ ...interfaceComponentConfig,
+ feedback_config: {
+ ...interfaceComponentConfig.feedback_config,
+ feedback_description: value,
+ },
+ });
+ }}
+ isAdditionalRemarks={
+ interfaceComponentConfig.feedback_config.additional_remarks?.enabled || false
+ }
+ setIsAdditionalRemarks={(value) => {
+ setInterfaceComponentConfig({
+ ...interfaceComponentConfig,
+ feedback_config: {
+ ...interfaceComponentConfig.feedback_config,
+ additional_remarks: {
+ ...interfaceComponentConfig.feedback_config.additional_remarks,
+ enabled: value,
+ },
+ },
+ });
+ }}
+ isAdditionalRemarksRequired={
+ interfaceComponentConfig.feedback_config.additional_remarks?.required || false
+ }
+ setIsAdditionalRemarksRequired={(value) => {
+ setInterfaceComponentConfig({
+ ...interfaceComponentConfig,
+ feedback_config: {
+ ...interfaceComponentConfig.feedback_config,
+ additional_remarks: {
+ ...interfaceComponentConfig.feedback_config.additional_remarks,
+ required: value,
+ },
+ },
+ });
+ }}
+ additionalRemarksTitle={
+ interfaceComponentConfig.feedback_config.additional_remarks?.title || ''
+ }
+ setAdditionalRemarksTitle={(value) => {
+ setInterfaceComponentConfig({
+ ...interfaceComponentConfig,
+ feedback_config: {
+ ...interfaceComponentConfig.feedback_config,
+ additional_remarks: {
+ ...interfaceComponentConfig.feedback_config.additional_remarks,
+ title: value,
+ },
+ },
+ });
+ }}
+ additionalRemarksDescription={
+ interfaceComponentConfig.feedback_config.additional_remarks?.description || ''
+ }
+ setAdditionalRemarksDescription={(value) => {
+ setInterfaceComponentConfig({
+ ...interfaceComponentConfig,
+ feedback_config: {
+ ...interfaceComponentConfig.feedback_config,
+ additional_remarks: {
+ ...interfaceComponentConfig.feedback_config.additional_remarks,
+ description: value,
+ },
+ },
+ });
+ }}
+ multipleChoiceConfig={{
+ type: interfaceComponentConfig.feedback_config.multiple_choice.type,
+ choices: interfaceComponentConfig.feedback_config.multiple_choice.choices,
+ }}
+ setMultipleChoiceConfig={(value) => {
+ setInterfaceComponentConfig({
+ ...interfaceComponentConfig,
+ feedback_config: {
+ ...interfaceComponentConfig.feedback_config,
+ multiple_choice: value,
+ },
+ });
+ }}
+ />
+ )}
+
+
+
+ >
+ )}
+ {(selectedInterfaceType === INTERFACE_TYPE.WEBSITE_CHATBOT ||
+ selectedInterfaceType === INTERFACE_TYPE.CHAT_ASSISTANT) && (
+ <>
+
+
+
+
+
+
+
+ >
+ )}
+ {selectedInterfaceType !== INTERFACE_TYPE.API_INTERFACE && (
+ <>
+
+
+
+ {selectedInterfaceType === INTERFACE_TYPE.WEBSITE_CHATBOT && (
+
+ )}
+ {selectedInterfaceType === INTERFACE_TYPE.SLACK_APP && (
+
+
+
+ )}
+
+ >
+ )}
+
+ );
+};
+
+export default ConfigWrapper;
diff --git a/ui/src/enterprise/views/Agents/Interface/__tests__/ChatGeneralConfig.test.tsx b/ui/src/enterprise/views/Agents/Interface/__tests__/ChatGeneralConfig.test.tsx
new file mode 100644
index 000000000..25c5c043b
--- /dev/null
+++ b/ui/src/enterprise/views/Agents/Interface/__tests__/ChatGeneralConfig.test.tsx
@@ -0,0 +1,427 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import { expect } from '@jest/globals';
+import '@testing-library/jest-dom/jest-globals';
+import '@testing-library/jest-dom';
+
+import ChatGeneralConfig from '../ChatGeneralConfig';
+import { Avatar, WorkflowInterfaceConfig } from '@/enterprise/services/types';
+import { INTERFACE_DISPLAY_TYPE } from '../../types';
+import { MULTIPLE_CHOICE_TYPES } from '@/enterprise/dataApps/feedbackTypes';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ChakraProvider, Tabs } from '@chakra-ui/react';
+import { mockSetInterfaceConfig } from '../../../../../../__mocks__/agentStoreMocks';
+
+// Mock useAgentStore
+const mockSetInterfaceDisplayType = jest.fn();
+let mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.FULL_PAGE;
+
+jest.mock('@/enterprise/store/useAgentStore', () => ({
+ __esModule: true,
+ default: (selector: (state: unknown) => unknown) =>
+ selector({
+ setInterfaceConfig: mockSetInterfaceConfig,
+ setInterfaceDisplayType: mockSetInterfaceDisplayType,
+ get interfaceDisplayType() {
+ return mockInterfaceDisplayType;
+ },
+ }),
+}));
+
+// Mock UploadChatbotAvatar
+jest.mock('@/enterprise/dataApps/components/ChatBot/UploadChatbotAvatar', () => ({
+ __esModule: true,
+ default: ({
+ label,
+ avatar,
+ visualColor,
+ setAvatar,
+ }: {
+ label: string;
+ avatar?: Avatar;
+ visualColor: string;
+ setAvatar: (avatar: Avatar | undefined) => void;
+ }) => (
+
+
{label}
+
{avatar ? JSON.stringify(avatar) : 'no-avatar'}
+
{visualColor}
+
+
+ ),
+}));
+
+// Mock InputField
+jest.mock('@/components/InputField', () => ({
+ __esModule: true,
+ default: ({
+ label,
+ name,
+ value,
+ onChange,
+ placeholder,
+ isTooltip,
+ tooltipLabel,
+ testId,
+ }: {
+ label: string;
+ name: string;
+ value: string;
+ onChange: (e: { target: { value: string } }) => void;
+ placeholder?: string;
+ isTooltip?: boolean;
+ tooltipLabel?: string;
+ testId?: string;
+ }) => (
+
+
+
+ {isTooltip && tooltipLabel &&
{tooltipLabel}
}
+
+ ),
+}));
+
+// Mock ColourPicker
+jest.mock('@/components/ColourPicker', () => ({
+ __esModule: true,
+ default: ({
+ label,
+ visualColor,
+ setVisualColor,
+ }: {
+ label: string;
+ visualColor: string;
+ setVisualColor: (value: string) => void;
+ }) => (
+
+
+ setVisualColor(e.target.value)}
+ />
+
+ ),
+}));
+
+// Mock TabItem and TabsWrapper
+jest.mock('@/components/TabItem', () => ({
+ __esModule: true,
+ default: ({
+ text,
+ action,
+ icon,
+ testId,
+ }: {
+ text: string;
+ action: () => void;
+ icon?: React.ReactNode;
+ testId?: string;
+ }) => (
+
+ ),
+}));
+
+jest.mock('@/components/TabsWrapper', () => {
+ return {
+ __esModule: true,
+ default: ({ children, index }: { children: React.ReactNode; index: number }) => (
+
+ {children}
+
+ ),
+ };
+});
+
+describe('ChatGeneralConfig', () => {
+ const baseConfig: WorkflowInterfaceConfig = {
+ component_type: 'chat_bot',
+ configurable_id: '',
+ configurable_type: 'workflow',
+ properties: {
+ field_group: '',
+ measure_value: '',
+ file_id: '',
+ card_title: 'Test Chat',
+ visual_color: '#FF5733',
+ chat_bot: {
+ welcome_message: 'Welcome!',
+ responder_name: 'Bot',
+ },
+ },
+ feedback_config: {
+ feedback_enabled: false,
+ feedback_method: null,
+ feedback_title: '',
+ feedback_description: '',
+ additional_remarks: {
+ enabled: false,
+ required: false,
+ title: '',
+ description: '',
+ },
+ multiple_choice: {
+ type: MULTIPLE_CHOICE_TYPES.SINGLE_SELECT,
+ choices: [],
+ },
+ },
+ export_config: {
+ method: 'embed',
+ interface_position: 'bottom_right',
+ whitelist_urls: [],
+ embeddable_assistant: false,
+ },
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.FULL_PAGE;
+ });
+ const queryClient = new QueryClient();
+
+ const renderComponent = (config: WorkflowInterfaceConfig) => {
+ return render(
+
+
+
+
+ ,
+ );
+ };
+
+ it('renders Chat Display section with tooltip', () => {
+ renderComponent(baseConfig);
+
+ expect(screen.getByText('Chat Display')).toBeInTheDocument();
+ });
+
+ it('uses stable test ids for display tabs and chat name', () => {
+ renderComponent(baseConfig);
+ expect(screen.getByTestId('interface-chat-display-tab-desktop')).toBeInTheDocument();
+ expect(screen.getByTestId('interface-chat-display-tab-mobile')).toBeInTheDocument();
+ expect(screen.getByTestId('interface-chat-name-input')).toBeInTheDocument();
+ });
+
+ it('renders Chat Name input field', () => {
+ renderComponent(baseConfig);
+
+ const input = screen.getByTestId('interface-chat-name-input');
+ expect(input).toBeInTheDocument();
+ expect(input).toHaveValue('Test Chat');
+ });
+
+ it('updates card_title when Chat Name input changes', () => {
+ renderComponent(baseConfig);
+
+ const input = screen.getByTestId('interface-chat-name-input');
+ fireEvent.change(input, { target: { value: 'New Chat Name' } });
+
+ expect(mockSetInterfaceConfig).toHaveBeenCalledWith({
+ ...baseConfig,
+ properties: {
+ ...baseConfig.properties,
+ card_title: 'New Chat Name',
+ },
+ });
+ });
+
+ it('renders Welcome Message input field', () => {
+ renderComponent(baseConfig);
+
+ const input = screen.getByTestId('input-welcomeMessage');
+ expect(input).toBeInTheDocument();
+ expect(input).toHaveValue('Welcome!');
+ });
+
+ it('updates welcome_message when Welcome Message input changes', () => {
+ renderComponent(baseConfig);
+
+ const input = screen.getByTestId('input-welcomeMessage');
+ fireEvent.change(input, { target: { value: 'New Welcome Message' } });
+
+ expect(mockSetInterfaceConfig).toHaveBeenCalledWith({
+ ...baseConfig,
+ properties: {
+ ...baseConfig.properties,
+ chat_bot: {
+ ...baseConfig.properties.chat_bot,
+ welcome_message: 'New Welcome Message',
+ },
+ },
+ });
+ });
+
+ it('renders ColourPicker for Chat Color', () => {
+ renderComponent(baseConfig);
+
+ expect(screen.getByTestId('colour-picker')).toBeInTheDocument();
+ const colorInput = screen.getByTestId('colour-input');
+ // HTML color inputs normalize to lowercase
+ expect(colorInput).toHaveValue('#ff5733');
+ });
+
+ it('updates visual_color when Chat Color changes', () => {
+ renderComponent(baseConfig);
+
+ const colorInput = screen.getByTestId('colour-input');
+ fireEvent.change(colorInput, { target: { value: '#123456' } });
+
+ expect(mockSetInterfaceConfig).toHaveBeenCalledWith({
+ ...baseConfig,
+ properties: {
+ ...baseConfig.properties,
+ visual_color: '#123456',
+ },
+ });
+ });
+
+ it('renders Responder Name input when interfaceDisplayType is MOBILE', () => {
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ renderComponent(baseConfig);
+
+ const input = screen.getByTestId('input-responderName');
+ expect(input).toBeInTheDocument();
+ expect(input).toHaveValue('Bot');
+ });
+
+ it('does not render Responder Name input when interfaceDisplayType is FULL_PAGE', () => {
+ renderComponent(baseConfig);
+
+ expect(screen.queryByTestId('input-responderName')).not.toBeInTheDocument();
+ });
+
+ it('updates responder_name when Responder Name input changes', () => {
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ renderComponent(baseConfig);
+
+ const input = screen.getByTestId('input-responderName');
+ fireEvent.change(input, { target: { value: 'New Bot Name' } });
+
+ expect(mockSetInterfaceConfig).toHaveBeenCalledWith({
+ ...baseConfig,
+ properties: {
+ ...baseConfig.properties,
+ chat_bot: {
+ ...baseConfig.properties.chat_bot,
+ responder_name: 'New Bot Name',
+ },
+ },
+ });
+ });
+
+ it('renders UploadChatbotAvatar component', () => {
+ renderComponent(baseConfig);
+
+ expect(screen.getByTestId('upload-chatbot-avatar')).toBeInTheDocument();
+ expect(screen.getByTestId('avatar-label')).toHaveTextContent('Avatar');
+ });
+
+ it('passes avatar prop to UploadChatbotAvatar when avatar exists', () => {
+ const avatar: Avatar = { type: 'image', value: 'http://example.com/avatar.png' };
+ const configWithAvatar: WorkflowInterfaceConfig = {
+ ...baseConfig,
+ properties: {
+ ...baseConfig.properties,
+ chat_bot: {
+ ...baseConfig.properties.chat_bot,
+ avatar,
+ },
+ },
+ };
+
+ renderComponent(configWithAvatar);
+
+ const avatarValue = screen.getByTestId('avatar-value');
+ expect(avatarValue).toHaveTextContent(JSON.stringify(avatar));
+ });
+
+ it('passes undefined avatar to UploadChatbotAvatar when avatar does not exist', () => {
+ renderComponent(baseConfig);
+
+ const avatarValue = screen.getByTestId('avatar-value');
+ expect(avatarValue).toHaveTextContent('no-avatar');
+ });
+
+ it('passes visual_color to UploadChatbotAvatar', () => {
+ renderComponent(baseConfig);
+
+ const visualColor = screen.getByTestId('avatar-visual-color');
+ expect(visualColor).toHaveTextContent('#FF5733');
+ });
+
+ it('updates avatar when setAvatar is called from UploadChatbotAvatar', () => {
+ renderComponent(baseConfig);
+
+ const setAvatarBtn = screen.getByTestId('set-avatar-btn');
+ fireEvent.click(setAvatarBtn);
+
+ expect(mockSetInterfaceConfig).toHaveBeenCalledWith({
+ ...baseConfig,
+ properties: {
+ ...baseConfig.properties,
+ chat_bot: {
+ ...baseConfig.properties.chat_bot,
+ avatar: { type: 'image', value: 'http://example.com/new-avatar.png' },
+ },
+ },
+ });
+ });
+
+ it('handles empty welcome_message gracefully', () => {
+ const configWithoutWelcome: WorkflowInterfaceConfig = {
+ ...baseConfig,
+ properties: {
+ ...baseConfig.properties,
+ chat_bot: {
+ ...baseConfig.properties.chat_bot,
+ welcome_message: undefined,
+ },
+ },
+ };
+
+ renderComponent(configWithoutWelcome);
+
+ const input = screen.getByTestId('input-welcomeMessage');
+ expect(input).toHaveValue('');
+ });
+
+ it('handles empty responder_name gracefully', () => {
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ const configWithoutResponder: WorkflowInterfaceConfig = {
+ ...baseConfig,
+ properties: {
+ ...baseConfig.properties,
+ chat_bot: {
+ ...baseConfig.properties.chat_bot,
+ responder_name: undefined,
+ },
+ },
+ };
+
+ renderComponent(configWithoutResponder);
+
+ const input = screen.getByTestId('input-responderName');
+ expect(input).toHaveValue('');
+ });
+
+ it('calls setInterfaceDisplayType when tab is clicked', () => {
+ renderComponent(baseConfig);
+
+ fireEvent.click(screen.getByTestId('interface-chat-display-tab-mobile'));
+
+ expect(mockSetInterfaceDisplayType).toHaveBeenCalledWith(INTERFACE_DISPLAY_TYPE.MOBILE);
+ });
+});
diff --git a/ui/src/enterprise/views/Agents/Interface/__tests__/ChatbotExportConfig.test.tsx b/ui/src/enterprise/views/Agents/Interface/__tests__/ChatbotExportConfig.test.tsx
new file mode 100644
index 000000000..01ec0df57
--- /dev/null
+++ b/ui/src/enterprise/views/Agents/Interface/__tests__/ChatbotExportConfig.test.tsx
@@ -0,0 +1,942 @@
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { expect, describe, it, beforeEach, jest } from '@jest/globals';
+import '@testing-library/jest-dom/jest-globals';
+import '@testing-library/jest-dom';
+import ChatbotExportConfig from '../ChatbotExportConfig';
+import { ChakraProvider } from '@chakra-ui/react';
+import { INTERFACE_DISPLAY_TYPE } from '../../types';
+import { WorkflowExportConfig, WorkflowInterfaceConfig } from '@/enterprise/services/types';
+
+// Mock useAgentStore
+const mockSetExportConfig = jest.fn();
+let mockExportConfig: WorkflowExportConfig = {
+ method: 'embed',
+ interface_position: 'bottom_right',
+ whitelist_urls: [],
+ embeddable_assistant: false,
+ query_selector: '',
+};
+let mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+let mockInterfaceConfig: WorkflowInterfaceConfig | null = null;
+let mockWorkflowDataApp: any = null;
+
+jest.mock('@/enterprise/store/useAgentStore', () => {
+ const mockFn = jest.fn((selector: (state: any) => any) => {
+ const state = {
+ exportConfig: mockExportConfig,
+ setExportConfig: mockSetExportConfig,
+ interfaceDisplayType: mockInterfaceDisplayType,
+ interfaceConfig: mockInterfaceConfig,
+ workflowDataApp: mockWorkflowDataApp,
+ };
+ return selector(state);
+ });
+ (mockFn as any).getState = jest.fn(() => ({
+ exportConfig: mockExportConfig,
+ setExportConfig: mockSetExportConfig,
+ }));
+ return {
+ __esModule: true,
+ default: mockFn,
+ };
+});
+
+// Mock useConfigStore
+jest.mock('@/enterprise/store/useConfigStore', () => ({
+ useConfigStore: {
+ getState: () => ({
+ configs: {
+ apiHost: 'https://api.example.com',
+ },
+ }),
+ subscribe: jest.fn(),
+ },
+}));
+
+// Mock hooks
+const mockShowToast = jest.fn();
+const mockShowError = jest.fn();
+
+jest.mock('@/hooks/useCustomToast', () => ({
+ __esModule: true,
+ default: () => mockShowToast,
+}));
+
+jest.mock('@/hooks/useErrorToast', () => ({
+ __esModule: true,
+ useErrorToast: () => mockShowError,
+}));
+
+// Mock copy-to-clipboard
+const mockCopyFn = jest.fn();
+jest.mock('copy-to-clipboard', () => ({
+ __esModule: true,
+ default: (...args: any[]) => mockCopyFn(...args),
+}));
+
+// Mock window.open
+const mockWindowOpen = jest.fn();
+Object.defineProperty(window, 'open', {
+ writable: true,
+ value: mockWindowOpen,
+});
+
+// Mock location
+Object.defineProperty(window, 'location', {
+ writable: true,
+ value: {
+ origin: 'https://app.example.com',
+ },
+});
+
+// Mock CustomSelect
+jest.mock('@/components/CustomSelect/CustomSelect', () => ({
+ __esModule: true,
+ CustomSelect: ({
+ name,
+ value,
+ onChange,
+ placeholder,
+ children,
+ 'data-testid': dataTestId,
+ }: {
+ name: string;
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+ children: React.ReactNode;
+ 'data-testid'?: string;
+ }) => (
+
+
+
+ ),
+}));
+
+// Mock Option
+jest.mock('@/components/CustomSelect/Option', () => ({
+ __esModule: true,
+ Option: ({ value, children }: { value: string; children: React.ReactNode }) => (
+
+ ),
+}));
+
+// Mock InputField
+jest.mock('@/components/InputField', () => ({
+ __esModule: true,
+ default: ({
+ label,
+ name,
+ value,
+ onChange,
+ placeholder,
+ isTooltip,
+ tooltipLabel,
+ }: {
+ label: string;
+ name: string;
+ value: string;
+ onChange: (e: { target: { value: string } }) => void;
+ placeholder?: string;
+ isTooltip?: boolean;
+ tooltipLabel?: string;
+ }) => (
+
+
+
+ {isTooltip && tooltipLabel &&
{tooltipLabel}
}
+
+ ),
+}));
+
+// Mock ExportComponents
+jest.mock('../ExportComponents', () => ({
+ __esModule: true,
+ ExportSection: ({
+ title,
+ children,
+ actions,
+ }: {
+ title: string;
+ children: React.ReactNode;
+ actions?: React.ReactNode;
+ }) => (
+
+
{title}
+ {actions &&
{actions}
}
+
{children}
+
+ ),
+ ExportButton: ({
+ icon,
+ onClick,
+ children,
+ isDisabled,
+ }: {
+ icon?: React.ReactNode;
+ onClick?: () => void;
+ children: React.ReactNode;
+ isDisabled?: boolean;
+ }) => (
+
+ ),
+}));
+
+// Mock separated components
+jest.mock('../EmbeddableCodeSection', () => ({
+ __esModule: true,
+ EmbeddableCodeSection: () => (
+ EmbeddableCodeSection
+ ),
+}));
+
+jest.mock('../ChromeExtensionSection', () => ({
+ __esModule: true,
+ ChromeExtensionSection: () => (
+ ChromeExtensionSection
+ ),
+}));
+
+jest.mock('../StandaloneAppSection', () => ({
+ __esModule: true,
+ StandaloneAppSection: () => StandaloneAppSection
,
+}));
+
+// Mock CHROME_EXTENSION_URL
+jest.mock('@/enterprise/app-constants', () => ({
+ __esModule: true,
+ CHROME_EXTENSION_URL: 'https://chrome-extension.example.com',
+}));
+
+// Mock react-icons/fi
+jest.mock('react-icons/fi', () => ({
+ FiCopy: () => CopyIcon,
+ FiExternalLink: () => ExternalLinkIcon,
+ FiInfo: () => InfoIcon,
+}));
+
+describe('ChatbotExportConfig', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockCopyFn.mockClear();
+ mockExportConfig = {
+ method: 'embed',
+ interface_position: 'bottom_right',
+ whitelist_urls: [],
+ embeddable_assistant: false,
+ query_selector: '',
+ };
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ mockInterfaceConfig = null;
+ mockWorkflowDataApp = null;
+ });
+
+ const renderComponent = (props = {}) => {
+ return render(
+
+
+ ,
+ );
+ };
+
+ describe('Component Rendering', () => {
+ it('renders method selector when isExportOpen is true', () => {
+ renderComponent();
+ expect(screen.getByText('Method')).toBeInTheDocument();
+ expect(screen.getByTestId('select-method')).toBeInTheDocument();
+ expect(screen.getByTestId('interface-export-method-select')).toBeInTheDocument();
+ });
+
+ it('does not render content when isExportOpen is false', async () => {
+ renderComponent({ isExportOpen: false });
+ await waitFor(
+ () => {
+ const methodText = screen.queryByText('Method');
+ if (methodText) {
+ const style = window.getComputedStyle(methodText);
+ expect(
+ style.display === 'none' || style.visibility === 'hidden' || !methodText.offsetParent,
+ ).toBe(true);
+ } else {
+ expect(methodText).not.toBeInTheDocument();
+ }
+ },
+ { timeout: 1000 },
+ );
+ });
+
+ it('renders helper text for selected method', () => {
+ renderComponent();
+ const helperText = screen.getByText(/Get a code snippet to embed your interface/i);
+ expect(helperText).toBeInTheDocument();
+ });
+
+ it('renders EmbeddableCodeSection when method is embed', () => {
+ mockExportConfig.method = 'embed';
+ renderComponent();
+ expect(screen.getByTestId('embeddable-code-section')).toBeInTheDocument();
+ });
+
+ it('renders ChromeExtensionSection when method is no_code', () => {
+ mockExportConfig.method = 'no_code';
+ renderComponent();
+ expect(screen.getByTestId('chrome-extension-section')).toBeInTheDocument();
+ });
+
+ it('renders StandaloneAppSection when method is assistant', () => {
+ mockExportConfig.method = 'assistant';
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.FULL_PAGE;
+ renderComponent();
+ expect(screen.getByTestId('standalone-app-section')).toBeInTheDocument();
+ });
+ });
+
+ describe('Method Selection', () => {
+ it('updates export config when method changes', () => {
+ renderComponent();
+ const select = screen.getByTestId('select-method');
+ fireEvent.change(select, { target: { value: 'no_code' } });
+
+ expect(mockSetExportConfig).toHaveBeenCalledWith({
+ ...mockExportConfig,
+ method: 'no_code',
+ embeddable_assistant: false,
+ });
+ });
+
+ it('sets embeddable_assistant to true when method is embed and display type is FULL_PAGE', () => {
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.FULL_PAGE;
+ renderComponent();
+ const select = screen.getByTestId('select-method');
+ fireEvent.change(select, { target: { value: 'embed' } });
+
+ expect(mockSetExportConfig).toHaveBeenCalledWith({
+ ...mockExportConfig,
+ method: 'embed',
+ embeddable_assistant: true,
+ });
+ });
+ });
+
+ describe('Query Selector Input', () => {
+ it('renders query selector input for embed method and full page', () => {
+ mockExportConfig.method = 'embed';
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.FULL_PAGE;
+ renderComponent();
+
+ expect(screen.getByText('Query Selector')).toBeInTheDocument();
+ expect(screen.getByPlaceholderText('[data-id = "tabpanel-general"]')).toBeInTheDocument();
+ });
+
+ it('does not render query selector for mobile display type', () => {
+ mockExportConfig.method = 'embed';
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ renderComponent();
+
+ expect(screen.queryByText('Query Selector')).not.toBeInTheDocument();
+ });
+
+ it('updates query_selector when input changes', () => {
+ mockExportConfig.method = 'embed';
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.FULL_PAGE;
+ renderComponent();
+
+ const input = screen.getByPlaceholderText('[data-id = "tabpanel-general"]');
+ fireEvent.change(input, { target: { value: '[data-id="test"]' } });
+
+ expect(mockSetExportConfig).toHaveBeenCalledWith({
+ ...mockExportConfig,
+ query_selector: '[data-id="test"]',
+ });
+ });
+ });
+
+ describe('Interface Position Selector', () => {
+ it('renders interface position selector for mobile and non-embeddable assistant', () => {
+ mockExportConfig.method = 'embed';
+ mockExportConfig.embeddable_assistant = false;
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ renderComponent();
+
+ expect(screen.getByText('Interface Position')).toBeInTheDocument();
+ expect(screen.getByTestId('select-interface_position')).toBeInTheDocument();
+ });
+
+ it('renders interface position selector for no_code method and mobile', () => {
+ mockExportConfig.method = 'no_code';
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ renderComponent();
+
+ expect(screen.getByText('Interface Position')).toBeInTheDocument();
+ });
+
+ it('does not render interface position for full page', () => {
+ mockExportConfig.method = 'embed';
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.FULL_PAGE;
+ renderComponent();
+
+ expect(screen.queryByText('Interface Position')).not.toBeInTheDocument();
+ });
+
+ it('does not render interface position for embeddable assistant', () => {
+ mockExportConfig.method = 'embed';
+ mockExportConfig.embeddable_assistant = true;
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ renderComponent();
+
+ expect(screen.queryByText('Interface Position')).not.toBeInTheDocument();
+ });
+
+ it('updates interface_position when selector changes', () => {
+ mockExportConfig.method = 'embed';
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ renderComponent();
+
+ const select = screen.getByTestId('select-interface_position');
+ fireEvent.change(select, { target: { value: 'bottom_left' } });
+
+ expect(mockSetExportConfig).toHaveBeenCalledWith({
+ ...mockExportConfig,
+ interface_position: 'bottom_left',
+ });
+ });
+ });
+
+ describe('Whitelist URLs Input', () => {
+ it('renders whitelist URLs input for no_code method', () => {
+ mockExportConfig.method = 'no_code';
+ renderComponent();
+
+ expect(screen.getByText('Whitelist URLs')).toBeInTheDocument();
+ expect(screen.getByTestId('input-whitelist_urls')).toBeInTheDocument();
+ });
+
+ it('does not render whitelist URLs for other methods', () => {
+ mockExportConfig.method = 'embed';
+ renderComponent();
+
+ expect(screen.queryByText('Whitelist URLs')).not.toBeInTheDocument();
+ });
+
+ it('updates whitelist_urls when input changes', () => {
+ mockExportConfig.method = 'no_code';
+ renderComponent();
+
+ const input = screen.getByTestId('input-whitelist_urls');
+ fireEvent.change(input, { target: { value: 'https://example.com, https://test.com' } });
+
+ expect(mockSetExportConfig).toHaveBeenCalledWith({
+ ...mockExportConfig,
+ whitelist_urls: ['https://example.com', 'https://test.com'],
+ });
+ });
+
+ it('handles empty whitelist_urls', () => {
+ mockExportConfig.method = 'no_code';
+ mockExportConfig.whitelist_urls = [];
+ renderComponent();
+
+ const input = screen.getByTestId('input-whitelist_urls');
+ expect(input).toHaveValue('');
+ });
+ });
+
+ describe('Initialization Logic', () => {
+ it('initializes with API export config when available', async () => {
+ mockExportConfig.method = 'no_code';
+ mockExportConfig.embeddable_assistant = false;
+ mockInterfaceConfig = {
+ dataAppId: 'app-123',
+ export_config: {
+ method: 'assistant',
+ embeddable_assistant: false,
+ },
+ } as any;
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.FULL_PAGE;
+ mockSetExportConfig.mockClear();
+
+ renderComponent();
+
+ await waitFor(
+ () => {
+ expect(mockSetExportConfig).toHaveBeenCalled();
+ },
+ { timeout: 2000 },
+ );
+ });
+
+ it('initializes with embed method when API config has embeddable_assistant true', async () => {
+ mockInterfaceConfig = {
+ dataAppId: 'app-123',
+ export_config: {
+ method: 'embed',
+ embeddable_assistant: true,
+ },
+ } as any;
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.FULL_PAGE;
+
+ renderComponent();
+
+ await waitFor(() => {
+ expect(mockSetExportConfig).toHaveBeenCalled();
+ });
+ });
+
+ it('uses first available option when current method is not available for display type', async () => {
+ mockExportConfig.method = 'assistant';
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ mockInterfaceConfig = {
+ dataAppId: 'app-123',
+ } as any;
+
+ renderComponent();
+
+ await waitFor(() => {
+ expect(mockSetExportConfig).toHaveBeenCalled();
+ });
+ });
+
+ it('initializes with default values when no API config and no current config', async () => {
+ mockExportConfig.method = '';
+ mockExportConfig.embeddable_assistant = undefined;
+ mockInterfaceConfig = {
+ dataAppId: 'app-123',
+ } as any;
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+
+ renderComponent();
+
+ await waitFor(() => {
+ expect(mockSetExportConfig).toHaveBeenCalled();
+ const calls = mockSetExportConfig.mock.calls;
+ const lastCall = calls[calls.length - 1];
+ if (lastCall) {
+ const config = lastCall[0] as WorkflowExportConfig;
+ expect(config.method).toBe('embed');
+ expect(config.embeddable_assistant).toBe(false);
+ }
+ });
+ });
+ });
+
+ describe('Display Type Changes', () => {
+ it('handles display type change from MOBILE to FULL_PAGE', async () => {
+ mockInterfaceConfig = {
+ dataAppId: 'app-123',
+ } as any;
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ mockExportConfig.method = 'embed';
+
+ const { rerender } = renderComponent();
+
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.FULL_PAGE;
+ rerender(
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(mockSetExportConfig).toHaveBeenCalled();
+ });
+ });
+
+ it('restores saved method when switching back to a display type', async () => {
+ mockInterfaceConfig = {
+ dataAppId: 'app-123',
+ } as any;
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ mockExportConfig.method = 'embed';
+
+ const { rerender } = renderComponent();
+
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.FULL_PAGE;
+ rerender(
+
+
+ ,
+ );
+
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ rerender(
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(mockSetExportConfig).toHaveBeenCalled();
+ });
+ });
+
+ it('uses API value when switching to FULL_PAGE with embeddable_assistant true', async () => {
+ mockInterfaceConfig = {
+ dataAppId: 'app-123',
+ export_config: {
+ method: 'embed',
+ embeddable_assistant: true,
+ },
+ } as any;
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ mockExportConfig.method = 'embed';
+ mockExportConfig.embeddable_assistant = false;
+
+ const { rerender } = renderComponent();
+
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.FULL_PAGE;
+ rerender(
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ const calls = mockSetExportConfig.mock.calls;
+ const hasEmbeddableTrue = calls.some((call) => {
+ const config = call[0] as WorkflowExportConfig;
+ return config.embeddable_assistant === true && config.method === 'embed';
+ });
+ expect(hasEmbeddableTrue).toBe(true);
+ });
+ });
+ });
+
+ describe('Validation Logic', () => {
+ it('forces method to embed when embeddable_assistant is true but method is not embed', async () => {
+ mockExportConfig.method = 'no_code';
+ mockExportConfig.embeddable_assistant = true;
+ mockInterfaceConfig = {
+ dataAppId: 'app-123',
+ } as any;
+
+ renderComponent();
+
+ await waitFor(() => {
+ expect(mockSetExportConfig).toHaveBeenCalledWith(
+ expect.objectContaining({
+ method: 'embed',
+ }),
+ );
+ });
+ });
+
+ it('validates embeddable_assistant flag matches display type and method', async () => {
+ mockExportConfig.method = 'no_code';
+ mockExportConfig.embeddable_assistant = false;
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.FULL_PAGE;
+ mockInterfaceConfig = {
+ dataAppId: 'app-123',
+ } as any;
+
+ renderComponent();
+
+ await waitFor(
+ () => {
+ expect(mockSetExportConfig).toHaveBeenCalled();
+ },
+ { timeout: 2000 },
+ );
+
+ const calls = mockSetExportConfig.mock.calls;
+ const lastCall = calls[calls.length - 1];
+ if (lastCall) {
+ const config = lastCall[0] as WorkflowExportConfig;
+ expect(['assistant', 'embed']).toContain(config.method);
+ }
+ });
+
+ it('corrects embeddable_assistant when method is embed but flag is wrong for MOBILE', async () => {
+ // Set up valid initialized state
+ mockExportConfig.method = 'embed';
+ mockExportConfig.embeddable_assistant = true; // Wrong for MOBILE - should be false
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ mockInterfaceConfig = {
+ dataAppId: 'app-123',
+ export_config: {
+ method: 'embed',
+ embeddable_assistant: false,
+ },
+ } as any;
+
+ renderComponent();
+
+ await waitFor(
+ () => {
+ const calls = mockSetExportConfig.mock.calls;
+ const hasCorrectedFlag = calls.some((call) => {
+ const config = call[0] as WorkflowExportConfig;
+ return config.method === 'embed' && config.embeddable_assistant === false;
+ });
+ expect(hasCorrectedFlag).toBe(true);
+ },
+ { timeout: 2000 },
+ );
+ });
+
+ it('corrects embeddable_assistant when method is embed but flag is wrong for FULL_PAGE', async () => {
+ // Set up valid initialized state
+ mockExportConfig.method = 'embed';
+ mockExportConfig.embeddable_assistant = false; // Wrong for FULL_PAGE - should be true
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.FULL_PAGE;
+ mockInterfaceConfig = {
+ dataAppId: 'app-123',
+ export_config: {
+ method: 'embed',
+ embeddable_assistant: true,
+ },
+ } as any;
+
+ renderComponent();
+
+ await waitFor(
+ () => {
+ const calls = mockSetExportConfig.mock.calls;
+ const hasCorrectedFlag = calls.some((call) => {
+ const config = call[0] as WorkflowExportConfig;
+ return config.method === 'embed' && config.embeddable_assistant === true;
+ });
+ expect(hasCorrectedFlag).toBe(true);
+ },
+ { timeout: 2000 },
+ );
+ });
+
+ it('updates method to first available when current method is invalid for display type after init', async () => {
+ // First render with valid config to initialize
+ mockExportConfig.method = 'embed';
+ mockExportConfig.embeddable_assistant = false;
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ mockInterfaceConfig = {
+ dataAppId: 'app-123',
+ export_config: {
+ method: 'embed',
+ embeddable_assistant: false,
+ },
+ } as any;
+
+ const { rerender } = renderComponent();
+
+ // Wait for initialization
+ await waitFor(() => {
+ expect(screen.getByText('Method')).toBeInTheDocument();
+ });
+
+ // Now set an invalid method and trigger a re-render with dataAppId change
+ mockSetExportConfig.mockClear();
+ mockExportConfig.method = 'assistant'; // Invalid for MOBILE
+ mockInterfaceConfig = {
+ dataAppId: 'app-456', // Changed to trigger useEffect
+ export_config: {
+ method: 'embed',
+ embeddable_assistant: false,
+ },
+ } as any;
+
+ rerender(
+
+
+ ,
+ );
+
+ await waitFor(
+ () => {
+ const calls = mockSetExportConfig.mock.calls;
+ const hasValidMethod = calls.some((call) => {
+ const config = call[0] as WorkflowExportConfig;
+ return config.method === 'embed' || config.method === 'no_code';
+ });
+ expect(hasValidMethod).toBe(true);
+ },
+ { timeout: 2000 },
+ );
+ });
+
+ it('stores method in ref after validation', async () => {
+ mockExportConfig.method = 'embed';
+ mockExportConfig.embeddable_assistant = false;
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ mockInterfaceConfig = {
+ dataAppId: 'app-123',
+ export_config: {
+ method: 'embed',
+ embeddable_assistant: false,
+ },
+ } as any;
+
+ renderComponent();
+
+ await waitFor(() => {
+ expect(screen.getByText('Method')).toBeInTheDocument();
+ });
+
+ // Verify component renders correctly with validated state
+ expect(screen.getByTestId('embeddable-code-section')).toBeInTheDocument();
+ });
+
+ it('forces method to embed during validation when embeddable_assistant is true with wrong method', async () => {
+ // Initialize with valid config first
+ mockExportConfig.method = 'embed';
+ mockExportConfig.embeddable_assistant = false;
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ mockInterfaceConfig = {
+ dataAppId: 'app-123',
+ export_config: {
+ method: 'embed',
+ embeddable_assistant: false,
+ },
+ } as any;
+
+ const { rerender } = renderComponent();
+
+ await waitFor(() => {
+ expect(screen.getByText('Method')).toBeInTheDocument();
+ });
+
+ // Now change to invalid state where embeddable_assistant is true but method isn't embed
+ mockSetExportConfig.mockClear();
+ mockExportConfig.method = 'no_code';
+ mockExportConfig.embeddable_assistant = true;
+ mockInterfaceConfig = {
+ dataAppId: 'app-456', // Changed to trigger useEffect
+ export_config: {
+ method: 'no_code',
+ embeddable_assistant: true,
+ },
+ } as any;
+
+ rerender(
+
+
+ ,
+ );
+
+ await waitFor(
+ () => {
+ const calls = mockSetExportConfig.mock.calls;
+ const hasForcedEmbed = calls.some((call) => {
+ const config = call[0] as WorkflowExportConfig;
+ return config.method === 'embed';
+ });
+ expect(hasForcedEmbed).toBe(true);
+ },
+ { timeout: 2000 },
+ );
+ });
+
+ it('corrects embeddable_assistant flag when it mismatches expected value after validation', async () => {
+ // Initialize with valid config for FULL_PAGE
+ mockExportConfig.method = 'embed';
+ mockExportConfig.embeddable_assistant = true;
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.FULL_PAGE;
+ mockInterfaceConfig = {
+ dataAppId: 'app-123',
+ export_config: {
+ method: 'embed',
+ embeddable_assistant: true,
+ },
+ } as any;
+
+ const { rerender } = renderComponent();
+
+ await waitFor(() => {
+ expect(screen.getByText('Method')).toBeInTheDocument();
+ });
+
+ // Now change the flag to wrong value while keeping same display type
+ mockSetExportConfig.mockClear();
+ mockExportConfig.method = 'embed';
+ mockExportConfig.embeddable_assistant = false; // Wrong for FULL_PAGE embed
+ mockInterfaceConfig = {
+ dataAppId: 'app-456', // Changed to trigger useEffect
+ export_config: {
+ method: 'embed',
+ embeddable_assistant: false,
+ },
+ } as any;
+
+ rerender(
+
+
+ ,
+ );
+
+ await waitFor(
+ () => {
+ const calls = mockSetExportConfig.mock.calls;
+ const hasCorrectedFlag = calls.some((call) => {
+ const config = call[0] as WorkflowExportConfig;
+ return config.embeddable_assistant === true;
+ });
+ expect(hasCorrectedFlag).toBe(true);
+ },
+ { timeout: 2000 },
+ );
+ });
+
+ it('updates to first available option when current method becomes invalid', async () => {
+ // Initialize with valid config
+ mockExportConfig.method = 'embed';
+ mockExportConfig.embeddable_assistant = false;
+ mockInterfaceDisplayType = INTERFACE_DISPLAY_TYPE.MOBILE;
+ mockInterfaceConfig = {
+ dataAppId: 'app-123',
+ export_config: {
+ method: 'embed',
+ embeddable_assistant: false,
+ },
+ } as any;
+
+ const { rerender } = renderComponent();
+
+ await waitFor(() => {
+ expect(screen.getByText('Method')).toBeInTheDocument();
+ });
+
+ // Change method to invalid value for MOBILE
+ mockSetExportConfig.mockClear();
+ mockExportConfig.method = 'assistant'; // Not valid for MOBILE
+ mockExportConfig.embeddable_assistant = false;
+ mockInterfaceConfig = {
+ dataAppId: 'app-789',
+ export_config: {
+ method: 'assistant',
+ embeddable_assistant: false,
+ },
+ } as any;
+
+ rerender(
+
+
+ ,
+ );
+
+ await waitFor(
+ () => {
+ const calls = mockSetExportConfig.mock.calls;
+ const hasValidMethod = calls.some((call) => {
+ const config = call[0] as WorkflowExportConfig;
+ // embed or no_code are valid for MOBILE
+ return config.method === 'embed' || config.method === 'no_code';
+ });
+ expect(hasValidMethod).toBe(true);
+ },
+ { timeout: 2000 },
+ );
+ });
+ });
+});
diff --git a/ui/src/enterprise/views/Agents/Interface/__tests__/ConfigWrapper.test.tsx b/ui/src/enterprise/views/Agents/Interface/__tests__/ConfigWrapper.test.tsx
new file mode 100644
index 000000000..6100f6aaa
--- /dev/null
+++ b/ui/src/enterprise/views/Agents/Interface/__tests__/ConfigWrapper.test.tsx
@@ -0,0 +1,288 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import { expect, describe, it, beforeEach } from '@jest/globals';
+import '@testing-library/jest-dom/jest-globals';
+import '@testing-library/jest-dom';
+import ConfigWrapper from '../ConfigWrapper';
+import { ChakraProvider } from '@chakra-ui/react';
+import { INTERFACE_TYPE } from '../../types';
+import { WorkflowInterfaceConfig } from '@/enterprise/services/types';
+
+// Mock react-router-dom
+jest.mock('react-router-dom', () => ({
+ useParams: () => ({ id: 'workflow-123' }),
+}));
+
+// Mock ChatGeneralConfig
+jest.mock('../ChatGeneralConfig', () => ({
+ __esModule: true,
+ default: () => Chat General Config
,
+}));
+
+// Mock ApiGeneralConfig
+jest.mock('../ApiInterface/ApiGeneralConfig', () => ({
+ __esModule: true,
+ default: () => API General Config
,
+}));
+
+// Mock SlackGeneralConfig
+jest.mock('../SlackInterface/SlackGeneralConfig', () => ({
+ __esModule: true,
+ default: () => Slack General Config
,
+}));
+
+// Mock SlackExportConfig
+jest.mock('../SlackInterface/SlackExportConfig', () => ({
+ __esModule: true,
+ default: () => Slack Export Config
,
+}));
+
+// Mock ChatbotExportConfig
+jest.mock('../ChatbotExportConfig', () => ({
+ __esModule: true,
+ default: ({ isExportOpen }: { isExportOpen: boolean; agentId: string }) =>
+ isExportOpen ? Chatbot Export Config
: null,
+}));
+
+// Mock Security
+jest.mock('../Security/Security', () => ({
+ __esModule: true,
+ default: () => Security Config
,
+}));
+
+// Mock FeedbackConfigForm
+jest.mock('@/enterprise/views/DataApps/DataAppsForm/BuildDataApp/FeedbackConfigForm', () => ({
+ __esModule: true,
+ default: () => Feedback Config Form
,
+}));
+
+// Mock react-icons
+jest.mock('react-icons/fi');
+
+describe('ConfigWrapper', () => {
+ const mockSetInterfaceComponentConfig = jest.fn();
+
+ const defaultInterfaceConfig = {
+ component_type: 'chatbot',
+ configurable_id: '123',
+ configurable_type: 'agent',
+ properties: {
+ field_group: '',
+ measure_value: '',
+ card_title: 'Test Chat',
+ visual_color: '#808080',
+ file_id: '',
+ chat_bot: {
+ welcome_message: 'Hello!',
+ responder_name: 'Bot',
+ },
+ },
+ feedback_config: {
+ feedback_enabled: false,
+ feedback_method: null,
+ feedback_title: '',
+ feedback_description: '',
+ additional_remarks: {
+ enabled: false,
+ required: false,
+ title: '',
+ description: '',
+ },
+ multiple_choice: {
+ type: 'single',
+ choices: [],
+ },
+ },
+ export_config: {
+ method: 'embed',
+ interface_position: 'bottom_right',
+ },
+ } as unknown as WorkflowInterfaceConfig;
+
+ const defaultProps = {
+ selectedInterfaceType: INTERFACE_TYPE.WEBSITE_CHATBOT,
+ interfaceComponentConfig: defaultInterfaceConfig,
+ setInterfaceComponentConfig: mockSetInterfaceComponentConfig,
+ };
+
+ const renderComponent = (props = defaultProps) => {
+ return render(
+
+
+ ,
+ );
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('Rendering', () => {
+ it('should render GENERAL section', () => {
+ renderComponent();
+ expect(screen.getByText('GENERAL')).toBeInTheDocument();
+ });
+
+ it('should render export section toggle with test id', () => {
+ renderComponent();
+ expect(screen.getByTestId('interface-export-section-toggle')).toBeInTheDocument();
+ });
+
+ it('should render ChatGeneralConfig for Website Chatbot', () => {
+ renderComponent({
+ ...defaultProps,
+ selectedInterfaceType: INTERFACE_TYPE.WEBSITE_CHATBOT,
+ });
+ expect(screen.getByTestId('chat-general-config')).toBeInTheDocument();
+ });
+
+ it('should render ApiGeneralConfig for API Interface', () => {
+ renderComponent({
+ ...defaultProps,
+ selectedInterfaceType: INTERFACE_TYPE.API_INTERFACE,
+ });
+ expect(screen.getByTestId('api-general-config')).toBeInTheDocument();
+ });
+
+ it('should render SlackGeneralConfig for Slack App', () => {
+ renderComponent({
+ ...defaultProps,
+ selectedInterfaceType: INTERFACE_TYPE.SLACK_APP,
+ });
+ expect(screen.getByTestId('slack-general-config')).toBeInTheDocument();
+ });
+ });
+
+ describe('Feedback Section', () => {
+ it('should render FEEDBACK section for Website Chatbot', () => {
+ renderComponent({
+ ...defaultProps,
+ selectedInterfaceType: INTERFACE_TYPE.WEBSITE_CHATBOT,
+ });
+ expect(screen.getByText('FEEDBACK')).toBeInTheDocument();
+ });
+
+ it('should render FEEDBACK section for Slack App', () => {
+ renderComponent({
+ ...defaultProps,
+ selectedInterfaceType: INTERFACE_TYPE.SLACK_APP,
+ });
+ expect(screen.getByText('FEEDBACK')).toBeInTheDocument();
+ });
+
+ it('should not render FEEDBACK section for API Interface', () => {
+ renderComponent({
+ ...defaultProps,
+ selectedInterfaceType: INTERFACE_TYPE.API_INTERFACE,
+ });
+ expect(screen.queryByText('FEEDBACK')).not.toBeInTheDocument();
+ });
+
+ it('should toggle feedback section when clicked', () => {
+ renderComponent({
+ ...defaultProps,
+ selectedInterfaceType: INTERFACE_TYPE.WEBSITE_CHATBOT,
+ });
+
+ const feedbackButton = screen.getByText('FEEDBACK').closest('button');
+ fireEvent.click(feedbackButton!);
+
+ // After clicking, feedback section should expand
+ expect(screen.getByText('Enable Feedback')).toBeInTheDocument();
+ });
+ });
+
+ describe('Security Section', () => {
+ it('should render SECURITY section for Website Chatbot', () => {
+ renderComponent({
+ ...defaultProps,
+ selectedInterfaceType: INTERFACE_TYPE.WEBSITE_CHATBOT,
+ });
+ expect(screen.getByText('SECURITY')).toBeInTheDocument();
+ });
+
+ it('should not render SECURITY section for API Interface', () => {
+ renderComponent({
+ ...defaultProps,
+ selectedInterfaceType: INTERFACE_TYPE.API_INTERFACE,
+ });
+ expect(screen.queryByText('SECURITY')).not.toBeInTheDocument();
+ });
+
+ it('should not render SECURITY section for Slack App', () => {
+ renderComponent({
+ ...defaultProps,
+ selectedInterfaceType: INTERFACE_TYPE.SLACK_APP,
+ });
+ expect(screen.queryByText('SECURITY')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('Export Section', () => {
+ it('should render EXPORT section for Website Chatbot', () => {
+ renderComponent({
+ ...defaultProps,
+ selectedInterfaceType: INTERFACE_TYPE.WEBSITE_CHATBOT,
+ });
+ expect(screen.getByText('EXPORT')).toBeInTheDocument();
+ });
+
+ it('should render EXPORT section for Slack App', () => {
+ renderComponent({
+ ...defaultProps,
+ selectedInterfaceType: INTERFACE_TYPE.SLACK_APP,
+ });
+ expect(screen.getByText('EXPORT')).toBeInTheDocument();
+ });
+
+ it('should not render EXPORT section for API Interface', () => {
+ renderComponent({
+ ...defaultProps,
+ selectedInterfaceType: INTERFACE_TYPE.API_INTERFACE,
+ });
+ expect(screen.queryByText('EXPORT')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('Collapsible Sections', () => {
+ it('should have GENERAL section open by default', () => {
+ renderComponent({
+ ...defaultProps,
+ selectedInterfaceType: INTERFACE_TYPE.WEBSITE_CHATBOT,
+ });
+ // Chat General Config should be visible by default
+ expect(screen.getByTestId('chat-general-config')).toBeInTheDocument();
+ });
+
+ it('should close GENERAL and open FEEDBACK when FEEDBACK is clicked', () => {
+ renderComponent({
+ ...defaultProps,
+ selectedInterfaceType: INTERFACE_TYPE.WEBSITE_CHATBOT,
+ });
+
+ const feedbackButton = screen.getByText('FEEDBACK').closest('button');
+ fireEvent.click(feedbackButton!);
+
+ // Feedback section should now be visible
+ expect(screen.getByText('Enable Feedback')).toBeInTheDocument();
+ });
+ });
+
+ describe('Feedback Toggle', () => {
+ it('should call setInterfaceComponentConfig when feedback switch is toggled', () => {
+ renderComponent({
+ ...defaultProps,
+ selectedInterfaceType: INTERFACE_TYPE.WEBSITE_CHATBOT,
+ });
+
+ // Open feedback section
+ const feedbackButton = screen.getByText('FEEDBACK').closest('button');
+ fireEvent.click(feedbackButton!);
+
+ // Toggle feedback switch
+ const feedbackSwitch = screen.getByRole('checkbox');
+ fireEvent.click(feedbackSwitch);
+
+ expect(mockSetInterfaceComponentConfig).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/ui/src/enterprise/views/Assistant/ListAssistants/AssistantsList.tsx b/ui/src/enterprise/views/Assistant/ListAssistants/AssistantsList.tsx
new file mode 100644
index 000000000..6ffdca776
--- /dev/null
+++ b/ui/src/enterprise/views/Assistant/ListAssistants/AssistantsList.tsx
@@ -0,0 +1,98 @@
+import { Box } from '@chakra-ui/react';
+import ContentContainer from '@/components/ContentContainer';
+import TopBar from '@/components/TopBar';
+
+import { useAPIErrorsToast } from '@/hooks/useErrorToast';
+import DataTable from '@/components/DataTable';
+import Loader from '@/components/Loader';
+import useProtectedNavigate from '@/enterprise/hooks/useProtectedNavigate';
+import { UserActions } from '@/enterprise/types';
+import Pagination from '@/components/EnhancedPagination';
+import useFilters from '@/hooks/useFilters';
+import useDataAppQueries from '@/enterprise/hooks/queries/useDataAppQueries';
+import NoAssistants from './NoAssistants';
+import { RENDERING_OPTION_TYPE } from '../../DataApps/DataAppsForm/types';
+import { AssistantColumns } from './AssistantColumns';
+import { DataAppsResponse } from '@/enterprise/services/types';
+
+const AssistantsList = () => {
+ const { useGetDataApps } = useDataAppQueries();
+ const { filters, updateFilters } = useFilters({
+ page: '1',
+ rendering_type: 'assistant',
+ });
+ const navigate = useProtectedNavigate();
+ const apiErrorToast = useAPIErrorsToast();
+
+ const { data: dataApps, isLoading: isDataAppsFetching } = useGetDataApps(filters);
+
+ const onPageSelect = (page: number) => {
+ updateFilters({
+ page: page.toString(),
+ rendering_type: 'assistant',
+ });
+ };
+
+ if (dataApps?.errors) {
+ apiErrorToast(dataApps.errors);
+ return;
+ }
+
+ const assistantsDataApps = dataApps?.data?.filter(
+ (app) => app.attributes.rendering_type === RENDERING_OPTION_TYPE.ASSISTANT,
+ );
+
+ if (isDataAppsFetching) {
+ return ;
+ }
+
+ return (
+
+
+
+ {assistantsDataApps?.length === 0 ? (
+
+ ) : (
+
+
+ {
+ const app = row.original as DataAppsResponse;
+ const title =
+ app.attributes?.visual_components?.[0]?.properties?.card_title ?? '';
+ return {
+ 'data-testid': `assistant-list-row-${app.id}`,
+ 'data-assistant-card-title': title,
+ };
+ }}
+ onRowClick={(row) =>
+ navigate({
+ to: `${row.original.id}`,
+ location: 'data_app',
+ action: UserActions.Read,
+ })
+ }
+ />
+
+
+ {dataApps?.links && (
+
+ )}
+
+
+ )}
+
+
+ );
+};
+
+export default AssistantsList;
diff --git a/ui/src/enterprise/views/Assistant/ListAssistants/__tests__/AssistantsList.test.tsx b/ui/src/enterprise/views/Assistant/ListAssistants/__tests__/AssistantsList.test.tsx
new file mode 100644
index 000000000..9b771c58b
--- /dev/null
+++ b/ui/src/enterprise/views/Assistant/ListAssistants/__tests__/AssistantsList.test.tsx
@@ -0,0 +1,322 @@
+import { fireEvent, screen } from '@testing-library/react';
+import { expect, describe, it, jest, beforeEach } from '@jest/globals';
+import '@testing-library/jest-dom/jest-globals';
+import '@testing-library/jest-dom';
+import AssistantsList from '../AssistantsList';
+import useDataAppQueries from '@/enterprise/hooks/queries/useDataAppQueries';
+import { useAPIErrorsToast } from '@/hooks/useErrorToast';
+import useFilters from '@/hooks/useFilters';
+import useProtectedNavigate from '@/enterprise/hooks/useProtectedNavigate';
+import { renderWithProviders } from '@/utils/testUtils';
+import { RENDERING_OPTION_TYPE } from '@/enterprise/views/DataApps/DataAppsForm/types';
+import { mockApiErrorToast } from '../../../../../../__mocks__/commonMocks';
+
+jest.mock('@/enterprise/hooks/queries/useDataAppQueries');
+jest.mock('@/hooks/useErrorToast', () => ({
+ useAPIErrorsToast: jest.fn(),
+}));
+jest.mock('@/hooks/useFilters');
+jest.mock('@/enterprise/hooks/useProtectedNavigate');
+jest.mock('../NoAssistants', () => ({
+ __esModule: true,
+ default: () => No Assistants
,
+}));
+jest.mock('@/components/DataTable', () => ({
+ __esModule: true,
+ default: ({ data, onRowClick, getRowProps }: any) => (
+
+ {data.map((item: any) => {
+ const row = { original: item, id: String(item.id) };
+ const rowProps = getRowProps ? getRowProps(row) : {};
+ return (
+
onRowClick({ original: item })}>
+ {item.attributes.visual_components[0]?.properties?.card_title || item.id}
+
+ );
+ })}
+
+ ),
+}));
+jest.mock('@/components/EnhancedPagination', () => ({
+ __esModule: true,
+ default: ({ currentPage, handlePageChange }: any) => (
+
+
+
+ ),
+}));
+jest.mock('@/components/Loader', () => ({
+ __esModule: true,
+ default: () => Loading...
,
+}));
+
+const mockUseDataAppQueries = useDataAppQueries as jest.MockedFunction;
+const mockUseAPIErrorsToast = useAPIErrorsToast as jest.MockedFunction;
+const mockUseFilters = useFilters as jest.MockedFunction;
+const mockUseProtectedNavigate = useProtectedNavigate as jest.MockedFunction<
+ typeof useProtectedNavigate
+>;
+
+const mockAssistantDataApp = {
+ id: 1,
+ type: 'data-apps',
+ attributes: {
+ name: 'Test Assistant',
+ description: '',
+ status: 'active',
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ rendering_type: RENDERING_OPTION_TYPE.ASSISTANT,
+ visual_components: [
+ {
+ properties: {
+ field_group: '',
+ measure_value: '',
+ card_title: 'Test Assistant',
+ visual_color: '#000000',
+ file_id: 'file-1',
+ },
+ feedback_config: {
+ feedback_enabled: false,
+ feedback_method: null,
+ feedback_title: '',
+ },
+ },
+ ],
+ meta_data: {
+ rendering_type: 'assistant',
+ },
+ data_app_token: 'token-123',
+ },
+};
+
+const mockDataApp = {
+ id: 2,
+ type: 'data-apps',
+ attributes: {
+ name: 'Test Data App',
+ description: '',
+ status: 'active',
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ rendering_type: RENDERING_OPTION_TYPE.NO_CODE,
+ visual_components: [
+ {
+ properties: {
+ field_group: '',
+ measure_value: '',
+ card_title: 'Test Data App',
+ visual_color: '#000000',
+ file_id: 'file-2',
+ },
+ feedback_config: {
+ feedback_enabled: false,
+ feedback_method: null,
+ feedback_title: '',
+ },
+ },
+ ],
+ meta_data: {
+ rendering_type: 'data_app',
+ },
+ data_app_token: 'token-456',
+ },
+};
+
+describe('AssistantsList', () => {
+ const mockNavigate = jest.fn();
+ const mockUpdateFilters = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ mockUseAPIErrorsToast.mockReturnValue(mockApiErrorToast);
+ mockUseProtectedNavigate.mockReturnValue(mockNavigate);
+ mockUseFilters.mockReturnValue({
+ filters: {
+ page: '1',
+ rendering_type: 'assistant',
+ },
+ updateFilters: mockUpdateFilters,
+ } as any);
+
+ mockUseDataAppQueries.mockReturnValue({
+ useGetDataApps: jest.fn().mockReturnValue({
+ data: {
+ data: [mockAssistantDataApp],
+ links: {
+ first: 'http://localhost?page=1',
+ last: 'http://localhost?page=5',
+ prev: null,
+ next: 'http://localhost?page=2',
+ },
+ },
+ isLoading: false,
+ }),
+ } as any);
+ });
+
+ describe('Component Rendering', () => {
+ it('should render AssistantsList with correct title and description', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('Chat Assistants')).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ 'Access and interact with your standalone chat assistants created using AI workflows and Data Apps.',
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it('should render data table when assistants exist', () => {
+ renderWithProviders();
+
+ expect(screen.getByTestId('data-table')).toBeInTheDocument();
+ const row = screen.getByTestId('assistant-list-row-1');
+ expect(row).toBeInTheDocument();
+ expect(row).toHaveAttribute('data-assistant-card-title', 'Test Assistant');
+ });
+
+ it('should render NoAssistants when no assistants exist', () => {
+ mockUseDataAppQueries.mockReturnValue({
+ useGetDataApps: jest.fn().mockReturnValue({
+ data: {
+ data: [],
+ },
+ isLoading: false,
+ }),
+ } as any);
+
+ renderWithProviders();
+
+ expect(screen.getByTestId('no-assistants')).toBeInTheDocument();
+ });
+
+ it('should show loader when data is fetching', () => {
+ mockUseDataAppQueries.mockReturnValue({
+ useGetDataApps: jest.fn().mockReturnValue({
+ data: null,
+ isLoading: true,
+ }),
+ } as any);
+
+ renderWithProviders();
+
+ expect(screen.getByTestId('loader')).toBeInTheDocument();
+ });
+ });
+
+ describe('Data Filtering', () => {
+ it('should filter only assistant rendering type', () => {
+ mockUseDataAppQueries.mockReturnValue({
+ useGetDataApps: jest.fn().mockReturnValue({
+ data: {
+ data: [mockAssistantDataApp, mockDataApp],
+ },
+ isLoading: false,
+ }),
+ } as any);
+
+ renderWithProviders();
+
+ expect(screen.getByTestId('assistant-list-row-1')).toBeInTheDocument();
+ expect(screen.queryByTestId('assistant-list-row-2')).not.toBeInTheDocument();
+ });
+
+ it('should handle empty data array', () => {
+ mockUseDataAppQueries.mockReturnValue({
+ useGetDataApps: jest.fn().mockReturnValue({
+ data: {
+ data: [],
+ },
+ isLoading: false,
+ }),
+ } as any);
+
+ renderWithProviders();
+
+ expect(screen.getByTestId('no-assistants')).toBeInTheDocument();
+ });
+ });
+
+ describe('Error Handling', () => {
+ it('should show error toast when data has errors', () => {
+ const errorData = {
+ errors: [{ detail: 'Error message' }],
+ };
+
+ mockUseDataAppQueries.mockReturnValue({
+ useGetDataApps: jest.fn().mockReturnValue({
+ data: errorData,
+ isLoading: false,
+ }),
+ } as any);
+
+ renderWithProviders();
+
+ expect(mockApiErrorToast).toHaveBeenCalledWith(errorData.errors);
+ });
+ });
+
+ describe('Row Click Navigation', () => {
+ it('should navigate to assistant detail when row is clicked', () => {
+ renderWithProviders();
+
+ const row = screen.getByTestId('assistant-list-row-1');
+ fireEvent.click(row);
+
+ expect(mockNavigate).toHaveBeenCalledWith({
+ to: '1',
+ location: 'data_app',
+ action: expect.any(String),
+ });
+ });
+ });
+
+ describe('Pagination', () => {
+ it('should render pagination when links exist', () => {
+ renderWithProviders();
+
+ expect(screen.getByTestId('pagination')).toBeInTheDocument();
+ });
+
+ it('should not render pagination when links do not exist', () => {
+ mockUseDataAppQueries.mockReturnValue({
+ useGetDataApps: jest.fn().mockReturnValue({
+ data: {
+ data: [mockAssistantDataApp],
+ links: null,
+ },
+ isLoading: false,
+ }),
+ } as any);
+
+ renderWithProviders();
+
+ expect(screen.queryByTestId('pagination')).not.toBeInTheDocument();
+ });
+
+ it('should update filters when page changes', () => {
+ renderWithProviders();
+
+ const nextButton = screen.getByText('Next');
+ fireEvent.click(nextButton);
+
+ expect(mockUpdateFilters).toHaveBeenCalledWith({
+ page: '2',
+ rendering_type: 'assistant',
+ });
+ });
+ });
+
+ describe('Filters', () => {
+ it('should initialize with correct filters', () => {
+ renderWithProviders();
+
+ expect(mockUseFilters).toHaveBeenCalledWith({
+ page: '1',
+ rendering_type: 'assistant',
+ });
+ });
+ });
+});
diff --git a/ui/src/enterprise/views/Tools/ToolsForm/SelectToolType.tsx b/ui/src/enterprise/views/Tools/ToolsForm/SelectToolType.tsx
new file mode 100644
index 000000000..24db1e5eb
--- /dev/null
+++ b/ui/src/enterprise/views/Tools/ToolsForm/SelectToolType.tsx
@@ -0,0 +1,174 @@
+import { Box, Text, Button, HStack, Grid } from '@chakra-ui/react';
+import { useState, useMemo } from 'react';
+import ContentContainer from '@/components/ContentContainer';
+import useSteppedForm from '@/stores/useSteppedForm';
+import useQueryWrapper from '@/hooks/useQueryWrapper';
+import { getToolDefinitions, getToolDefinition } from '@/enterprise/services/tools';
+import { ToolDefinitionTemplate } from '../ToolsList/types';
+import { ApiResponse } from '@/services/common';
+import Loader from '@/components/Loader';
+import ConnectorsGridItem from '@/components/ConnectorsGridItem';
+import { Connector } from '@/views/Connectors/types';
+import useCustomToast from '@/hooks/useCustomToast';
+import { CustomToastStatus } from '@/components/Toast';
+
+type TabType = 'all' | 'ai_squared' | 'custom';
+
+const TABS: { value: TabType; label: string }[] = [
+ { value: 'all', label: 'All Tools' },
+ { value: 'ai_squared', label: 'AI Squared' },
+ { value: 'custom', label: 'Custom' },
+];
+
+const convertToConnectors = (toolDefinitions: ToolDefinitionTemplate[]): Connector[] => {
+ return toolDefinitions.map((tool) => ({
+ icon: tool.icon || '',
+ name: tool.$id,
+ title: tool.title,
+ category: tool.category || 'custom',
+ connector_spec: {
+ connection_specification: tool.properties || {},
+ },
+ }));
+};
+
+const filterConnectors = (connectors: Connector[], activeTab: TabType): Connector[] => {
+ if (activeTab === 'custom') {
+ return connectors.filter((connector) => connector.category === 'custom');
+ }
+ if (activeTab === 'ai_squared') {
+ return connectors.filter((connector) => connector.category !== 'custom');
+ }
+ return connectors;
+};
+
+const saveToolMetadata = async (
+ connector: Connector,
+ saveConnectorFormData: any,
+ showToast: any,
+) => {
+ if (!connector?.name) {
+ showToast({
+ status: CustomToastStatus.Error,
+ title: 'Error',
+ description: 'Invalid tool selected. Please select a valid tool.',
+ position: 'bottom-right',
+ isClosable: true,
+ });
+ return false;
+ }
+
+ try {
+ const definitionResponse = await getToolDefinition(connector.name);
+ const definition = definitionResponse.data;
+
+ const metadata = {
+ category: definition?.category ?? 'custom',
+ icon: definition?.icon,
+ definition_id: connector.name,
+ };
+
+ saveConnectorFormData(connector.name, 'metadata', metadata);
+ return true;
+ } catch (error) {
+ const defaultMetadata = {
+ category: 'custom',
+ definition_id: connector.name,
+ };
+ saveConnectorFormData(connector.name, 'metadata', defaultMetadata);
+
+ showToast({
+ status: CustomToastStatus.Error,
+ title: 'Error',
+ description: 'Failed to fetch tool definition. Using default values.',
+ position: 'bottom-right',
+ isClosable: true,
+ });
+ return true;
+ }
+};
+
+const SelectToolType = (): JSX.Element => {
+ const { handleMoveForward, stepInfo, saveConnectorFormData } = useSteppedForm();
+ const [activeTab, setActiveTab] = useState('all');
+ const showToast = useCustomToast();
+
+ const { data, isLoading } = useQueryWrapper>(
+ ['tool-definitions'],
+ () => getToolDefinitions(),
+ {
+ refetchOnWindowFocus: false,
+ refetchOnMount: true,
+ },
+ );
+
+ const toolDefinitions = data?.data ?? [];
+ const connectors: Connector[] = useMemo(
+ () => convertToConnectors(toolDefinitions),
+ [toolDefinitions],
+ );
+ const filteredConnectors = useMemo(
+ () => filterConnectors(connectors, activeTab),
+ [connectors, activeTab],
+ );
+
+ const handleOnClick = async (connector: Connector) => {
+ const success = await saveToolMetadata(connector, saveConnectorFormData, showToast);
+
+ if (success && stepInfo?.formKey) {
+ handleMoveForward(stepInfo.formKey, connector.name);
+ }
+ };
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+ {TABS.map((tab) => (
+
+ ))}
+
+
+ {filteredConnectors.length > 0 ? (
+
+ {filteredConnectors.map((connector) => (
+
+ ))}
+
+ ) : (
+
+
+ No tools available yet
+
+
+ )}
+
+
+ );
+};
+
+export default SelectToolType;
diff --git a/ui/src/enterprise/views/Tools/ToolsForm/__tests__/SelectToolType.test.tsx b/ui/src/enterprise/views/Tools/ToolsForm/__tests__/SelectToolType.test.tsx
new file mode 100644
index 000000000..be994932a
--- /dev/null
+++ b/ui/src/enterprise/views/Tools/ToolsForm/__tests__/SelectToolType.test.tsx
@@ -0,0 +1,282 @@
+import { screen, fireEvent, waitFor } from '@testing-library/react';
+import { expect, describe, it, jest, beforeEach } from '@jest/globals';
+import '@testing-library/jest-dom/jest-globals';
+import '@testing-library/jest-dom';
+import SelectToolType from '../SelectToolType';
+import { getToolDefinition } from '@/enterprise/services/tools';
+import useSteppedForm from '@/stores/useSteppedForm';
+import useCustomToast from '@/hooks/useCustomToast';
+import useQueryWrapper from '@/hooks/useQueryWrapper';
+import { ToolDefinitionTemplate } from '../../ToolsList/types';
+import { ApiResponse } from '@/services/common';
+import { CustomToastStatus } from '@/components/Toast';
+import {
+ mockHandleMoveForward,
+ mockSaveConnectorFormData,
+ mockShowToast,
+ mockToolDefinitionsResponse,
+} from '../../../../../../__mocks__/toolMocks';
+import { renderWithProviders, createTestQueryClient } from '@/utils/testUtils';
+
+jest.mock('@/enterprise/services/tools', () => ({
+ getToolDefinitions: jest.fn(),
+ getToolDefinition: jest.fn(),
+}));
+
+jest.mock('@/stores/useSteppedForm');
+jest.mock('@/hooks/useCustomToast');
+jest.mock('@/hooks/useQueryWrapper');
+
+const mockGetToolDefinition = getToolDefinition as jest.MockedFunction;
+const mockUseSteppedForm = useSteppedForm as jest.MockedFunction;
+const mockUseCustomToast = useCustomToast as jest.MockedFunction;
+const mockUseQueryWrapper = useQueryWrapper as jest.MockedFunction;
+
+const queryClient = createTestQueryClient();
+
+const renderSelectToolType = () => {
+ return renderWithProviders(, { queryClient });
+};
+
+describe('SelectToolType', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ mockUseSteppedForm.mockReturnValue({
+ handleMoveForward: mockHandleMoveForward,
+ stepInfo: { formKey: 'tool' },
+ saveConnectorFormData: mockSaveConnectorFormData,
+ } as any);
+
+ mockUseCustomToast.mockReturnValue(mockShowToast as any);
+
+ mockUseQueryWrapper.mockReturnValue({
+ data: mockToolDefinitionsResponse,
+ isLoading: false,
+ error: null,
+ refetch: jest.fn(),
+ } as any);
+ });
+
+ it('should render loading state', () => {
+ mockUseQueryWrapper.mockReturnValue({
+ data: undefined,
+ isLoading: true,
+ error: null,
+ refetch: jest.fn(),
+ } as any);
+
+ renderSelectToolType();
+
+ // When loading, the component should render Loader
+ // The query wrapper should have been called with getToolDefinitions
+ expect(mockUseQueryWrapper).toHaveBeenCalledWith(
+ ['tool-definitions'],
+ expect.any(Function),
+ expect.objectContaining({
+ refetchOnMount: true,
+ refetchOnWindowFocus: false,
+ }),
+ );
+ });
+
+ it('should render all tabs', () => {
+ renderSelectToolType();
+
+ expect(screen.getByText('All Tools')).toBeInTheDocument();
+ expect(screen.getByText('AI Squared')).toBeInTheDocument();
+ expect(screen.getByText('Custom')).toBeInTheDocument();
+ });
+
+ it('passes tools-select-definition test ids to connector tiles', () => {
+ renderSelectToolType();
+ expect(screen.getByTestId('tools-select-definition-tool-1')).toBeInTheDocument();
+ expect(screen.getByTestId('tools-select-definition-tool-2')).toBeInTheDocument();
+ expect(screen.getByTestId('tools-select-definition-tool-3')).toBeInTheDocument();
+ });
+
+ it('should filter connectors by custom category when Custom tab is clicked', () => {
+ renderSelectToolType();
+
+ const customTab = screen.getByText('Custom');
+ fireEvent.click(customTab);
+
+ // Should show only custom tools
+ expect(screen.getByText('Tool 2')).toBeInTheDocument();
+ });
+
+ it('should filter connectors by ai_squared category when AI Squared tab is clicked', () => {
+ renderSelectToolType();
+
+ const aiSquaredTab = screen.getByText('AI Squared');
+ fireEvent.click(aiSquaredTab);
+
+ // Should show only AI Squared tools
+ expect(screen.getByText('Tool 1')).toBeInTheDocument();
+ expect(screen.getByText('Tool 3')).toBeInTheDocument();
+ });
+
+ it('should show all tools when All Tools tab is clicked', () => {
+ renderSelectToolType();
+
+ const allTab = screen.getByText('All Tools');
+ fireEvent.click(allTab);
+
+ // Should show all tools
+ expect(screen.getByText('Tool 1')).toBeInTheDocument();
+ expect(screen.getByText('Tool 2')).toBeInTheDocument();
+ expect(screen.getByText('Tool 3')).toBeInTheDocument();
+ });
+
+ it('should show empty state when no tools are available', () => {
+ mockUseQueryWrapper.mockReturnValue({
+ data: { data: [], status: 200 } as ApiResponse,
+ isLoading: false,
+ error: null,
+ refetch: jest.fn(),
+ } as any);
+
+ renderSelectToolType();
+
+ expect(screen.getByText('No tools available yet')).toBeInTheDocument();
+ });
+
+ it('should save tool metadata and move forward when tool is selected', async () => {
+ const mockDefinition = {
+ $id: 'tool-1',
+ title: 'Tool 1',
+ category: 'ai_squared',
+ icon: 'icon1',
+ };
+
+ mockGetToolDefinition.mockResolvedValue({
+ data: mockDefinition,
+ } as ApiResponse);
+
+ renderSelectToolType();
+
+ // Wait for tools to render, then click on a tool
+ await waitFor(() => {
+ expect(screen.getByText('Tool 1')).toBeInTheDocument();
+ });
+
+ // Find and click the connector item (this may need adjustment based on ConnectorsGridItem implementation)
+ const tool1 = screen.getByText('Tool 1');
+ fireEvent.click(tool1);
+
+ await waitFor(() => {
+ expect(mockGetToolDefinition).toHaveBeenCalledWith('tool-1');
+ expect(mockSaveConnectorFormData).toHaveBeenCalledWith('tool-1', 'metadata', {
+ category: 'ai_squared',
+ icon: 'icon1',
+ definition_id: 'tool-1',
+ });
+ expect(mockHandleMoveForward).toHaveBeenCalledWith('tool', 'tool-1');
+ });
+ });
+
+ it('should show error toast when tool definition fetch fails', async () => {
+ mockGetToolDefinition.mockRejectedValue(new Error('Failed to fetch'));
+
+ renderSelectToolType();
+
+ await waitFor(() => {
+ expect(screen.getByText('Tool 1')).toBeInTheDocument();
+ });
+
+ const tool1 = screen.getByText('Tool 1');
+ fireEvent.click(tool1);
+
+ await waitFor(() => {
+ expect(mockShowToast).toHaveBeenCalledWith({
+ status: CustomToastStatus.Error,
+ title: 'Error',
+ description: 'Failed to fetch tool definition. Using default values.',
+ position: 'bottom-right',
+ isClosable: true,
+ });
+ });
+ });
+
+ it('should use default category when tool definition has no category', async () => {
+ const mockDefinition = {
+ $id: 'tool-1',
+ title: 'Tool 1',
+ icon: 'icon1',
+ };
+
+ mockGetToolDefinition.mockResolvedValue({
+ data: mockDefinition,
+ } as ApiResponse);
+
+ renderSelectToolType();
+
+ await waitFor(() => {
+ expect(screen.getByText('Tool 1')).toBeInTheDocument();
+ });
+
+ const tool1 = screen.getByText('Tool 1');
+ fireEvent.click(tool1);
+
+ await waitFor(() => {
+ expect(mockSaveConnectorFormData).toHaveBeenCalledWith('tool-1', 'metadata', {
+ category: 'custom',
+ icon: 'icon1',
+ definition_id: 'tool-1',
+ });
+ });
+ });
+
+ it('should not move forward if stepInfo formKey is not available', async () => {
+ mockUseSteppedForm.mockReturnValue({
+ handleMoveForward: mockHandleMoveForward,
+ stepInfo: null,
+ saveConnectorFormData: mockSaveConnectorFormData,
+ } as any);
+
+ const mockDefinition = {
+ $id: 'tool-1',
+ title: 'Tool 1',
+ category: 'ai_squared',
+ icon: 'icon1',
+ };
+
+ mockGetToolDefinition.mockResolvedValue({
+ data: mockDefinition,
+ } as ApiResponse);
+
+ renderSelectToolType();
+
+ await waitFor(() => {
+ expect(screen.getByText('Tool 1')).toBeInTheDocument();
+ });
+
+ const tool1 = screen.getByText('Tool 1');
+ fireEvent.click(tool1);
+
+ await waitFor(() => {
+ expect(mockGetToolDefinition).toHaveBeenCalled();
+ expect(mockSaveConnectorFormData).toHaveBeenCalled();
+ });
+
+ expect(mockHandleMoveForward).not.toHaveBeenCalled();
+ });
+
+ it('should call getToolDefinitions when query executes', async () => {
+ // The query wrapper should call getToolDefinitions
+ // This is verified through the useQueryWrapper mock setup
+ renderSelectToolType();
+
+ // Verify that useQueryWrapper was called with getToolDefinitions
+ await waitFor(() => {
+ expect(mockUseQueryWrapper).toHaveBeenCalledWith(
+ ['tool-definitions'],
+ expect.any(Function),
+ expect.objectContaining({
+ refetchOnMount: true,
+ refetchOnWindowFocus: false,
+ }),
+ );
+ });
+ });
+});
diff --git a/ui/src/views/Models/__tests__/DefineSQL.test.tsx b/ui/src/views/Models/__tests__/DefineSQL.test.tsx
new file mode 100644
index 000000000..4c42e6641
--- /dev/null
+++ b/ui/src/views/Models/__tests__/DefineSQL.test.tsx
@@ -0,0 +1,453 @@
+import '@testing-library/jest-dom/jest-globals';
+import '@testing-library/jest-dom';
+import { screen, fireEvent, waitFor } from '@testing-library/react';
+import { renderWithProviders } from '@/utils/testUtils';
+import DefineSQL from '../ModelsForm/DefineModel/DefineSQL/DefineSQL';
+import {
+ mockNavigate,
+ mockShowToast,
+ mockPutModelById,
+ mockHandleMoveForward,
+ mockDynamicQueryStoreState,
+ mockApiErrorsToast,
+ mockErrorToast,
+ mockGetPreview,
+ mockUseModelPreviewReturn,
+ mockPrefillValues,
+} from '../__mocks__/modelsMocks';
+
+// ── Mocks ───────────────────────────────────────────────────────────
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useNavigate: () => mockNavigate,
+}));
+
+jest.mock('@/hooks/useCustomToast', () => ({
+ __esModule: true,
+ default: () => mockShowToast,
+}));
+
+jest.mock('@/hooks/useErrorToast', () => ({
+ useErrorToast: () => mockErrorToast,
+ useAPIErrorsToast: () => mockApiErrorsToast,
+}));
+
+jest.mock('@/services/models', () => ({
+ putModelById: (...args: unknown[]) => mockPutModelById(...args),
+}));
+
+jest.mock('@/stores/useSteppedForm', () => ({
+ __esModule: true,
+ default: () => ({
+ stepInfo: { formKey: 'defineModel' },
+ handleMoveForward: mockHandleMoveForward,
+ }),
+}));
+
+const mockDynamicQueryStore = jest.fn();
+jest.mock('@/enterprise/store/useDynamicQueryStore', () => ({
+ useDynamicQueryStore: (selector: (state: unknown) => unknown) =>
+ selector(mockDynamicQueryStore()),
+}));
+
+jest.mock('@/hooks/models/useModelPreview', () => ({
+ useModelPreview: () => mockUseModelPreviewReturn,
+}));
+
+// Mock Monaco editor
+const mockEditorRef = { current: { getValue: jest.fn(() => 'SELECT * FROM users') } };
+jest.mock('@monaco-editor/react', () => ({
+ __esModule: true,
+ default: ({
+ value,
+ onMount,
+ onChange,
+ }: {
+ value: string;
+ onMount?: (editor: unknown) => void;
+ onChange?: (value: string) => void;
+ }) => {
+ // Simulate editor mount
+ if (onMount) {
+ onMount(mockEditorRef.current);
+ }
+ return (
+