diff --git a/ui/src/components/CustomSelect/CustomSelect.tsx b/ui/src/components/CustomSelect/CustomSelect.tsx new file mode 100644 index 000000000..a270c44c4 --- /dev/null +++ b/ui/src/components/CustomSelect/CustomSelect.tsx @@ -0,0 +1,138 @@ +import { + chakra, + FormControlOptions, + HTMLChakraProps, + Icon, + omitThemingProps, + Text, + ThemingProps, + useFormControl, + useMergeRefs, + useMultiStyleConfig, + usePopper, +} from '@chakra-ui/react'; +import { mergeWith } from '@chakra-ui/utils'; +import { useSelect } from 'downshift'; +import { Children, cloneElement, forwardRef, isValidElement, ReactElement, useMemo } from 'react'; +import { SelectIcon } from './SelectIcon'; +import { IconType } from 'react-icons/lib'; + +export interface SelectProps + extends FormControlOptions, + ThemingProps<'Select'>, + Omit< + HTMLChakraProps<'button'>, + 'disabled' | 'required' | 'readOnly' | 'size' | 'value' | 'onChange' + > { + placeholder?: string; + value?: string | null | undefined; + buttonIcon?: IconType; + onChange?: (item: string | null | undefined) => void; +} + +export const CustomSelect = forwardRef((props, ownRef) => { + const themed = omitThemingProps(props) as SelectProps & { 'data-testid'?: string }; + const { + id, + value, + children, + placeholder, + buttonIcon, + onChange, + 'data-testid': dataTestId, + ...rest + } = themed; + const ownButtonProps = useFormControl(rest); + const styles = useMultiStyleConfig('CustomSelect', props); + + const validChildren = useMemo( + () => + Children.toArray(children) + .filter>(isValidElement) + .filter((child) => 'value' in child.props), + [children], + ); + + const items = validChildren.map((child) => child.props.value); + + const { isOpen, selectedItem, getToggleButtonProps, getMenuProps, getItemProps } = useSelect({ + id, + items, + selectedItem: value, + onSelectedItemChange: (val) => onChange?.(val.selectedItem), + }); + + const { referenceRef: popperRef, getPopperProps } = usePopper({ + enabled: isOpen, + gutter: 2, + }); + const { ref: useSelectToggleButtonRef, ...useSelectToggleButtonProps } = getToggleButtonProps(); + + const toggleButtonRef = useMergeRefs(ownRef, useSelectToggleButtonRef, popperRef); + const toggleButtonProps = mergeWith(ownButtonProps, useSelectToggleButtonProps); + + return ( + + + {buttonIcon && } + {validChildren.find((child) => child.props.value === selectedItem)?.props.children || + selectedItem || ( + + {placeholder} + + )} + + + + + {isOpen && + validChildren.map((item, index) => + cloneElement(item, { + __css: styles.option, + ...getItemProps({ item: item.props.value, index }), + }), + )} + + + + ); +}); + +CustomSelect.displayName = 'CustomSelect'; diff --git a/ui/src/components/FormFooter/FormFooter.tsx b/ui/src/components/FormFooter/FormFooter.tsx index 1a8db00d3..fc8470332 100644 --- a/ui/src/components/FormFooter/FormFooter.tsx +++ b/ui/src/components/FormFooter/FormFooter.tsx @@ -111,6 +111,7 @@ const FormFooter = ({ ) : null} {isContinueCtaRequired ? ( +<<<<<<< HEAD +======= + + + +>>>>>>> c14aaa594 (feat(CE): data test ids for connectors and models (#1831)) ) : null} diff --git a/ui/src/enterprise/dataApps/components/VisualConfig/CustomVisualConfig.tsx b/ui/src/enterprise/dataApps/components/VisualConfig/CustomVisualConfig.tsx new file mode 100644 index 000000000..171aadeb4 --- /dev/null +++ b/ui/src/enterprise/dataApps/components/VisualConfig/CustomVisualConfig.tsx @@ -0,0 +1,282 @@ +import DashedBox from '@/components/DashedBox'; +import { CustomToastStatus } from '@/components/Toast'; +import { uploadCustomComponent } from '@/enterprise/services/data-apps'; +import { SchemaFieldOptions } from '@/enterprise/views/AIMLSources/types/types'; +import useCustomToast from '@/hooks/useCustomToast'; +import { useAPIErrorsToast, useErrorToast } from '@/hooks/useErrorToast'; +import { Box, Text, Input, Button, Icon, Stack, Switch } from '@chakra-ui/react'; +import { Dispatch, SetStateAction, ChangeEvent, useState, useEffect } from 'react'; +import { FiCopy, FiFileText, FiKey, FiUpload, FiX } from 'react-icons/fi'; + +export type CustomVisualConfigProps = { + fieldGroup?: string | null; + fileId?: string | null; + fileName?: string | null; + sessionStorageKey?: string | null; + modelFields: SchemaFieldOptions[]; + showVisual?: boolean | null; + setFileId: Dispatch>; + setFileName: Dispatch>; + setSessionStorageKey: Dispatch>; + handleFieldGroupSelect: Dispatch>; + setShowVisual: Dispatch>; +}; + +const CustomVisualConfig = ({ + fileId, + fileName, + sessionStorageKey, + showVisual, + handleFieldGroupSelect, + setFileId, + setFileName, + setSessionStorageKey, + setShowVisual, +}: CustomVisualConfigProps) => { + const [file, setFile] = useState(); + const [isFileInServer, setIsFileInServer] = useState(fileId ? true : false); + const [isUploading, setIsUploading] = useState(false); + const [isUploadEnabled, setIsUploadEnabled] = useState(file ? file.size > 0 : false); + + const toast = useCustomToast(); + const errorToast = useErrorToast(); + const apiErrorToast = useAPIErrorsToast(); + + const handleUpload = async () => { + if (file) { + try { + setIsUploading(true); + if (file === undefined || file === null) { + errorToast('No file selected', true, null, true); + setIsUploading(false); + return; + } + const data = await uploadCustomComponent(file); + if (data.id) { + setFileId(data.id.toString()); + handleFieldGroupSelect('custom'); // Will update code in V2 to support filtering values by group. We will be passing full response currently to the component. + toast({ + title: 'Success!', + description: 'Custom Script File Uploaded Successfully!', + status: CustomToastStatus.Success, + isClosable: true, + position: 'bottom-right', + }); + } + if (data.errors) { + apiErrorToast(data.errors); + } + } catch { + errorToast('An Error Occurred, please try again.', true, null, true); + } finally { + setIsUploading(false); + } + } + }; + + async function setFileData(selectedFile: File) { + try { + const fileData = selectedFile; + if (fileData.size === 0 || (await fileData.text()).length === 0) { + errorToast('File is empty', true, null, true); + return; + } + setFile(fileData); + setIsUploadEnabled(true); + } catch { + setIsUploadEnabled(false); + errorToast('An Error Occurred, please try again.', true, null, true); + } + } + + const handleFileChange = (event: ChangeEvent) => { + const selectedFile = event.target.files?.[0]; + if (selectedFile === undefined) { + errorToast('No file selected', true, null, true); + setIsUploadEnabled(false); + return; + } + if (selectedFile) { + setFileData(selectedFile); + setFileName(selectedFile.name); + } + }; + + const handleCopyKey = () => { + if (sessionStorageKey) { + navigator.clipboard.writeText(sessionStorageKey); + toast({ + title: 'Success!', + description: 'Session Storage Key Copied Successfully!', + status: CustomToastStatus.Success, + isClosable: true, + position: 'bottom-right', + }); + } else { + errorToast('No Session Storage Key Found', true, null, true); + } + }; + + useEffect(() => { + if (!sessionStorageKey) { + const uuid = crypto.randomUUID(); + setSessionStorageKey(uuid); + toast({ + title: 'Success!', + description: 'Session Storage Key Generated Successfully!', + status: CustomToastStatus.Success, + isClosable: true, + position: 'bottom-right', + }); + } + }, [sessionStorageKey]); + + return ( + <> + + + + Show Visual + + setShowVisual(event.target.checked)} + isChecked={showVisual ?? true} + /> + + + + + + + Update Session Storage Key + + + + Update the session storage key to use a different key for the custom visual. + + + + + {sessionStorageKey} + + + + + + + + + Custom Script + + + {isFileInServer ? ( + + + + + {fileName || 'custom_script.js'} + + + setIsFileInServer(false)} + cursor='pointer' + data-testid='custom-visual-remove-uploaded-file' + aria-label='Remove uploaded file' + display='flex' + alignItems='center' + justifyContent='center' + background='transparent' + border='none' + p={0} + > + + + + ) : ( + + + + + + + Only .js build files are supported (Max 2MB) + + + )} + + + + + ); +}; + +export default CustomVisualConfig; diff --git a/ui/src/enterprise/dataApps/feedbacks/__tests__/ScaleFeedbackConfig.test.tsx b/ui/src/enterprise/dataApps/feedbacks/__tests__/ScaleFeedbackConfig.test.tsx new file mode 100644 index 000000000..d77bb58aa --- /dev/null +++ b/ui/src/enterprise/dataApps/feedbacks/__tests__/ScaleFeedbackConfig.test.tsx @@ -0,0 +1,13 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import { expect, describe, it } from '@jest/globals'; +import ScaleFeedbackConfig from '../ScaleFeedbackConfig'; + +describe('ScaleFeedbackConfig', () => { + it('renders ScaleFeedbackConfig component correclty', () => { + render(); + expect(screen.getByTestId('scale-feedback-container')).toBeTruthy(); + expect(screen.getByTestId('scale-type-select')).toBeTruthy(); + expect(screen.getByTestId('scale-type-select')).toBeDisabled(); + }); +}); diff --git a/ui/src/enterprise/views/Agents/AgentWorkflow/Configbar/FormComponents/PromptInput.tsx b/ui/src/enterprise/views/Agents/AgentWorkflow/Configbar/FormComponents/PromptInput.tsx new file mode 100644 index 000000000..6d902ff44 --- /dev/null +++ b/ui/src/enterprise/views/Agents/AgentWorkflow/Configbar/FormComponents/PromptInput.tsx @@ -0,0 +1,145 @@ +import ActionBadge from '@/components/ActionBadge/ActionBadge'; +import BaseModal from '@/components/BaseModal'; +import TextareaHighlighter from '@/components/TextareaHighlighter'; +import ToolTip from '@/components/ToolTip'; +import { Text, Flex, Box, Button, useDisclosure, Divider } from '@chakra-ui/react'; +import { WidgetProps } from '@rjsf/utils'; +import { useEffect, useState } from 'react'; +import { FiAlignLeft, FiInfo, FiPlus } from 'react-icons/fi'; + +const PromptInput = ({ value, required, onChange, label, options }: WidgetProps) => { + const { isOpen, onClose, onOpen } = useDisclosure(); + const [prompt, setPrompt] = useState(value ?? ''); + + useEffect(() => { + setPrompt(value); + }, [value]); + + return ( + + + + + + {label} + + {required && *} + + {options?.tooltip && ( + + + + + + )} + + onOpen()} + > + setPrompt(e.target.value)} + /> + + + + + + + } + openModal={isOpen} + setModalOpen={onClose} + > + + + + Prompt Templates + + + } + actionIcon={} + description='Text-to-SQL' + tooltipText='' + action={() => + setPrompt( + `You are given a database schema and a user question. Based on the schema, write a valid SQL query that answers the question.\nSchema: {db_schmea}\n\nQuestion: {user_input}\n\nOutput:\n Provide only the SQL query without any explanation.`, + ) + } + /> + + + + + + + Prompt + + {required && *} + + {options?.tooltip && ( + + + + + + )} + + + setPrompt(e.target.value)} + /> + + + + { + 'Insert dynamic variables to the prompt using curly braces, e.g., {variable_name}' + } + + + + + + + + ); +}; + +export default PromptInput; diff --git a/ui/src/enterprise/views/DataApps/DataAppsForm/BuildDataApp/AppConfiguration.tsx b/ui/src/enterprise/views/DataApps/DataAppsForm/BuildDataApp/AppConfiguration.tsx new file mode 100644 index 000000000..1b61b2f97 --- /dev/null +++ b/ui/src/enterprise/views/DataApps/DataAppsForm/BuildDataApp/AppConfiguration.tsx @@ -0,0 +1,276 @@ +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; + +import { Box, Divider, HStack, Image, Text } from '@chakra-ui/react'; +import { CustomSelect } from '@/components/CustomSelect/CustomSelect'; +import { Option } from '@/components/CustomSelect/Option'; + +import { useQuery } from '@tanstack/react-query'; +import { getAICatalog } from '@/services/syncs'; +import { GetAllModelsResponse } from '@/services/models.ts'; + +import { + BAR_CHART_PROPERTIES, + CUSTOM_VISUAL_PROPERTIES, + DONUT_CHART_PROPERTIES, + SCATTER_CHART_PROPERTIES, + TABLE_PROPERTIES, + CHATBOT_PROPERTIES, + TEXT_PROPERTIES, + VisualTypesProps, +} from '@/enterprise/dataApps/visualTypes.ts'; +import VisualTypesCarousel from './VisualTypesCarousel'; +import VisualComponentConfigRenderer from '@/enterprise/dataApps/components/VisualComponentConfigRenderer'; + +import FiAICard from '@/assets/icons/FiAICard.svg'; +import FiArray from '@/assets/icons/FiArray.svg'; +import FiBoolean from '@/assets/icons/FiBoolean.svg'; +import FiString from '@/assets/icons/FiString.svg'; +import FiInteger from '@/assets/icons/FiInteger.svg'; + +type AppConfigurationProps = { + selectedVisualComponent: string | null; + fieldGroup: string | null | undefined; + measureValue: string | null | undefined; + colorByField: string | null | undefined; + selectedModelId: string | null | undefined; + fileId: string | null | undefined; + fileName: string | null | undefined; + sessionStorageKey: string | null | undefined; + showVisual?: boolean; + setFieldGroup: Dispatch>; + setFileId: Dispatch>; + setFileName: Dispatch>; + setMeasureValue: Dispatch>; + setColorByField: Dispatch>; + setSelectedVisualComponent: Dispatch>; + setSelectedModelId: Dispatch>; + setSessionStorageKey: Dispatch>; + setShowVisual: Dispatch>; + aiMLModels: GetAllModelsResponse[]; + isEdit?: boolean; +}; + +const VISUAL_TYPES: VisualTypesProps[] = [ + BAR_CHART_PROPERTIES, + SCATTER_CHART_PROPERTIES, + DONUT_CHART_PROPERTIES, + TABLE_PROPERTIES, + CHATBOT_PROPERTIES, + TEXT_PROPERTIES, + CUSTOM_VISUAL_PROPERTIES, +]; + +const BadgeIcon = (type: string | undefined) => { + if (!type) return FiString; + switch (type.toLowerCase()) { + case 'string': + return FiString; + case 'number': + return FiInteger; + case 'array': + return FiArray; + case 'boolean': + return FiBoolean; + default: + return FiString; + } +}; + +const AppConfiguration = ({ + selectedVisualComponent, + fieldGroup, + measureValue, + colorByField, + selectedModelId, + fileId, + setFileId, + fileName, + setFileName, + sessionStorageKey, + showVisual, + setShowVisual, + setSessionStorageKey, + setSelectedModelId, + setFieldGroup, + setMeasureValue, + setColorByField, + setSelectedVisualComponent, + aiMLModels, + isEdit = false, +}: AppConfigurationProps) => { + const [selectedModelConnectorId, setSelectedModelConnectorId] = useState< + string | null | undefined + >(''); + const [selectedDynamicModel, setSelectedDynamicModel] = useState( + null, + ); + + const { data: connectorCatalog } = useQuery({ + queryKey: ['ai_catalog', selectedModelConnectorId], + queryFn: () => getAICatalog(selectedModelConnectorId as string), + refetchOnMount: true, + refetchOnWindowFocus: true, + enabled: !!selectedModelConnectorId, + gcTime: 0, + }); + + const modelFields = selectedDynamicModel + ? selectedDynamicModel?.attributes?.configuration?.json_schema?.output + : connectorCatalog?.data?.attributes?.catalog?.streams[0]?.json_schema?.output; + + useEffect(() => { + if (isEdit && aiMLModels?.length) { + setSelectedModelId(selectedModelId); + setSelectedModelConnectorId( + aiMLModels?.find((model) => model?.id === selectedModelId?.toString())?.attributes + ?.connector?.id, + ); + } + }, [aiMLModels]); + + useEffect(() => { + const selectedModel = aiMLModels?.find((model) => model?.id === selectedModelId?.toString()); + setSelectedModelConnectorId(selectedModel?.attributes?.connector?.id); + if (selectedModel?.attributes?.query_type === 'dynamic_sql') { + setSelectedDynamicModel(selectedModel); + } else { + setSelectedDynamicModel(null); + } + }, [selectedModelId]); + + return ( + + + + + Model + + {aiMLModels?.length && ( + { + setSelectedModelId(value); + const selectedModel = aiMLModels?.find((model) => model?.id === value?.toString()); + setSelectedModelConnectorId(selectedModel?.attributes?.connector?.id); + + // Update selectedDynamicModel only if query_type is dynamic_sql + if (selectedModel?.attributes?.query_type === 'dynamic_sql') { + setSelectedDynamicModel(selectedModel); + } else { + setSelectedDynamicModel(null); + } + setFieldGroup(null); + setMeasureValue(null); + setColorByField(null); + }} + maxH='150px' + overflowY='scroll' + placeholder='Select Model' + > + {aiMLModels?.map((AIModel) => ( + + ))} + + )} + + + + MODEL FIELDS + + {!selectedModelId && ( + + Select a model first to preview its fields and set up your data app. + + )} + {selectedModelId && selectedModelId > '' && ( + + {modelFields?.map((modelField, index) => ( + + + + {modelField?.name} + + + ))} + + )} + + + + + + + + Visual Type + + + + {selectedModelId && selectedModelId > '' && ( + + )} + + ); +}; + +export default AppConfiguration; diff --git a/ui/src/enterprise/views/DataApps/DataAppsForm/BuildDataApp/DataAppsProperties.tsx b/ui/src/enterprise/views/DataApps/DataAppsForm/BuildDataApp/DataAppsProperties.tsx new file mode 100644 index 000000000..c012d7a36 --- /dev/null +++ b/ui/src/enterprise/views/DataApps/DataAppsForm/BuildDataApp/DataAppsProperties.tsx @@ -0,0 +1,300 @@ +import TabItem from '@/components/TabItem'; +import TabsWrapper from '@/components/TabsWrapper'; +import { + Box, + Divider, + Input, + InputGroup, + InputLeftElement, + Switch, + TabList, + Text, +} from '@chakra-ui/react'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import FeedbackConfigForm from './FeedbackConfigForm'; +import { FEEDBACK_METHODS, MultipleChoiceConfig } from '@/enterprise/dataApps/feedbackTypes'; + +import UploadChatbotAvatar from '@/enterprise/dataApps/components/ChatBot/UploadChatbotAvatar'; +import { Avatar } from '@/enterprise/services/types'; + +enum DATA_APP_PROPERT_TABS { + PROPERTIES = 'Properties', + FEEDBACK = 'Feedback', +} + +type DataAppsPropertiesProps = { + canConfigureColor: boolean; + feedbackTitle: string; + feedbackDescription: string; + cardTitle: string; + visualColor: string; + feedbackMethod: FEEDBACK_METHODS | null; + isAdditionalRemarks: boolean; + isAdditionalRemarksRequired: boolean; + additionalRemarksTitle: string; + additionalRemarksDescription: string; + multipleChoiceConfig: MultipleChoiceConfig; + responderName: string; + welcomeMessage: string; + isFeedbackEnabled: boolean; + avatar?: Avatar; + setIsFeedbackEnabled: Dispatch>; + setMultipleChoiceConfig: Dispatch>; + setFeedbackMethod: Dispatch>; + setFeedbackTitle: Dispatch>; + setFeedbackDescription: Dispatch>; + setIsAdditionalRemarks: Dispatch>; + setIsAdditionalRemarksRequired: Dispatch>; + setAdditionalRemarksTitle: Dispatch>; + setAdditionalRemarksDescription: Dispatch>; + setCardTitle: Dispatch>; + setVisualColor: Dispatch>; + isChatbot?: boolean; + setResponderName: (args: string) => void; + setWelcomeMessage: (args: string) => void; + setAvatar: (args: Avatar | undefined) => void; +}; + +const DataAppsProperties = ({ + canConfigureColor = true, + feedbackTitle, + setFeedbackTitle, + feedbackMethod, + setFeedbackMethod, + feedbackDescription, + setFeedbackDescription, + cardTitle, + setCardTitle, + visualColor, + setVisualColor, + isAdditionalRemarks, + setIsAdditionalRemarks, + isAdditionalRemarksRequired, + setIsAdditionalRemarksRequired, + additionalRemarksTitle, + setAdditionalRemarksTitle, + additionalRemarksDescription, + setAdditionalRemarksDescription, + multipleChoiceConfig, + setMultipleChoiceConfig, + responderName, + setResponderName, + isChatbot = false, + welcomeMessage, + setWelcomeMessage, + isFeedbackEnabled, + setIsFeedbackEnabled, + avatar, + setAvatar, +}: DataAppsPropertiesProps) => { + const [activeTab, setActiveTab] = useState(DATA_APP_PROPERT_TABS.PROPERTIES); + + useEffect(() => { + if (isChatbot) { + setCardTitle('Chatbot'); + } + }, [isChatbot]); + + useEffect(() => { + if (feedbackMethod === FEEDBACK_METHODS.TEXT_INPUT) { + setIsAdditionalRemarks(false); + setIsAdditionalRemarksRequired(false); + } + }, [feedbackMethod]); + + return ( + + + + + setActiveTab(DATA_APP_PROPERT_TABS.PROPERTIES)} + flex={1} + /> + setActiveTab(DATA_APP_PROPERT_TABS.FEEDBACK)} + flex={1} + /> + + + {activeTab === DATA_APP_PROPERT_TABS.FEEDBACK && ( + <> + + + Enable Feedback + + setIsFeedbackEnabled(event.target.checked)} + isChecked={isFeedbackEnabled} + /> + + {isFeedbackEnabled && ( + + )} + + )} + {activeTab === DATA_APP_PROPERT_TABS.PROPERTIES && ( + <> + + + {isChatbot ? 'Chat Title' : 'Card Title'} + + setCardTitle(value)} + value={cardTitle || (isChatbot ? 'Chatbot' : '')} + borderStyle='solid' + borderWidth='1px' + borderColor='gray.400' + fontSize='14px' + /> + + {canConfigureColor && ( + + + {isChatbot ? 'Chat Color' : 'Visual Color'} + + + + + + + {/* Visible input showing the hex code */} + + {/* Hidden input for color picking */} + setVisualColor(value)} + cursor='pointer' + /> + + + + Shades of this color will be used in the visual. + + + + )} + {isChatbot && ( + <> + + + + + + Welcome Message + + + setWelcomeMessage(value)} + borderStyle='solid' + borderWidth='1px' + borderColor='gray.400' + fontSize='14px' + /> + + This will appear when you open the chat. + + + + + + Responder Name + + setResponderName(value)} + borderStyle='solid' + borderWidth='1px' + borderColor='gray.400' + fontSize='14px' + /> + + + + Avatar + + + This will appear as the avatar in the chat. + + + + + )} + + )} + + + ); +}; + +export default DataAppsProperties; diff --git a/ui/src/enterprise/views/DataApps/DataAppsForm/DataAppsFinaliseForm/DataAppsFinaliseForm.tsx b/ui/src/enterprise/views/DataApps/DataAppsForm/DataAppsFinaliseForm/DataAppsFinaliseForm.tsx new file mode 100644 index 000000000..d15c54a60 --- /dev/null +++ b/ui/src/enterprise/views/DataApps/DataAppsForm/DataAppsFinaliseForm/DataAppsFinaliseForm.tsx @@ -0,0 +1,513 @@ +import { + Box, + Image, + Input, + InputGroup, + InputRightElement, + Radio, + RadioGroup, + Select, + Stack, + Switch, + Text, + Textarea, + Tooltip, +} from '@chakra-ui/react'; +import ContentContainer from '@/components/ContentContainer'; +import { useState } from 'react'; +import { FiInfo } from 'react-icons/fi'; +import FormFooter from '@/components/FormFooter'; +import { useFormik } from 'formik'; +import CodePlugin from '@/assets/icons/code.svg'; +import NoCode from '@/assets/icons/connector-placeholder.svg'; +import Badge from '@/components/Badge'; +import { CreateDataAppPayload, DataAppsResponse } from '@/enterprise/services/types'; +import EmbedCodeModal from '@/enterprise/views/DataApps/EmbedCodeModal.tsx'; +import { RENDERING_OPTION_TYPE } from '@/enterprise/views/DataApps/DataAppsForm/types'; +import ChromeExtensionModal from '@/enterprise/views/DataApps/ChromeExtensionModal'; +import { CHATBOT_PROPERTIES } from '@/enterprise/dataApps/visualTypes'; +import useDataAppMutations from '@/enterprise/hooks/mutations/useDataAppMutations'; +import FiAIMessageCircle from '@/assets/icons/FiAIMessageCircle.svg'; +import PreviewDataApp from '../BuildDataApp/PreviewDataApp'; +import { useNavigate } from 'react-router-dom'; +import useSteppedForm from '@/stores/useSteppedForm'; + +type RenderingOptionProps = { + optionText: string; + optionDesc: string; + optionIcon: string; + optionValue: RENDERING_OPTION_TYPE; +}; + +const RenderingOption = ({ + optionText, + optionDesc, + optionIcon, + optionValue, +}: RenderingOptionProps) => ( + + + + rendering-option + + + {optionText} + + + {optionDesc} + + + + + +); + +const DataAppsFinaliseForm = ({ + isEdit = false, + dataAppDetails, + prefillDataAppProperties, +}: { + isEdit?: boolean; + dataAppDetails?: DataAppsResponse; + prefillDataAppProperties?: CreateDataAppPayload; +}) => { + const { createDataApp, updateDataApp } = useDataAppMutations(); + const [selectedRenderingOption, setSelectedRenderingOption] = useState( + (dataAppDetails?.attributes?.rendering_type as RENDERING_OPTION_TYPE) || + RENDERING_OPTION_TYPE.EMBED, + ); + const navigate = useNavigate(); + + const { forms, handleMoveBack, stepInfo } = useSteppedForm(); + + const dataAppProperties = forms.find(({ stepKey }) => stepKey === 'buildDataApp')?.data?.[ + 'buildDataApp' + ] as CreateDataAppPayload; + + const isChatBot = isEdit + ? dataAppDetails?.attributes?.visual_components?.[0]?.component_type === + CHATBOT_PROPERTIES.value + : dataAppProperties?.data_app?.visual_components?.[0]?.component_type === + CHATBOT_PROPERTIES.value; + + const getPayload = ( + payload: CreateDataAppPayload, + data: { + app_name: string; + description: string; + query_selector: string; + container_position: string; + whitelist_urls: string; + auto_refresh_enabled: boolean; + }, + ) => { + payload.data_app.rendering_type = selectedRenderingOption; + payload.data_app.meta_data = { + rendering_type: selectedRenderingOption, + }; + + payload.data_app.name = data.app_name; + payload.data_app.description = data.description; + + payload.data_app.meta_data = { + ...payload.data_app.meta_data, + query_selector: data.query_selector, + run_method: 'auto', + container_position: data.container_position, + whitelist_urls: data.whitelist_urls.split(','), + auto_refresh_enabled: data.auto_refresh_enabled, + }; + return payload; + }; + + const formik = useFormik({ + enableReinitialize: true, + initialValues: { + app_name: dataAppDetails?.attributes?.name ?? dataAppProperties?.data_app?.name ?? '', + description: + dataAppDetails?.attributes?.description ?? dataAppProperties?.data_app?.description ?? '', + query_selector: + dataAppDetails?.attributes?.meta_data?.query_selector ?? + dataAppProperties?.data_app?.meta_data?.query_selector ?? + '', + container_position: + dataAppDetails?.attributes?.meta_data?.container_position ?? + dataAppProperties?.data_app?.meta_data?.container_position ?? + (isChatBot ? 'bottom_right' : 'prepend'), + whitelist_urls: + dataAppDetails?.attributes?.meta_data?.whitelist_urls?.join(',') ?? + dataAppProperties?.data_app?.meta_data?.whitelist_urls?.join(',') ?? + '', + auto_refresh_enabled: + dataAppDetails?.attributes?.meta_data?.auto_refresh_enabled ?? + dataAppProperties?.data_app?.meta_data?.auto_refresh_enabled ?? + true, + }, + + onSubmit: async (data) => { + const dataAppPayload = getPayload( + isEdit ? (prefillDataAppProperties as CreateDataAppPayload) : dataAppProperties, + data, + ); + const response = + isEdit && dataAppDetails + ? await updateDataApp.mutateAsync({ + id: dataAppDetails.id.toString(), + data: dataAppPayload, + }) + : await createDataApp.mutateAsync(dataAppPayload); + + if (response?.data?.attributes) { + if (!isEdit) { + navigate(`/data-apps/list/${response.data.id}?showModal=true`); + } + } + }, + }); + + const isContinueEnabled = isChatBot + ? formik.values.app_name > '' && formik.values.container_position > '' + : formik.values.app_name > '' && + formik.values.container_position > '' && + formik.values.query_selector > ''; + + return ( + + +
+ {!isEdit && ( + + + Review + + + + )} + + + + Configure your rendering settings + + setSelectedRenderingOption(value)} + value={selectedRenderingOption} + > + + + + {isChatBot && ( + + )} + + + + {!isChatBot && ( + + + Query Selector + + + + + Container is injected at the body of the content by default. Use query selectors + to inject in specific elements. + + + )} + {selectedRenderingOption !== RENDERING_OPTION_TYPE.ASSISTANT && ( + + + + Container Position + + + + Container position defines where the container is placed in relation to the + target content. + + + {selectedRenderingOption === RENDERING_OPTION_TYPE.NO_CODE && ( + + + + Run Method + + + + + + )} + + )} + {selectedRenderingOption === RENDERING_OPTION_TYPE.NO_CODE && ( + + + + Whitelist URL + + + (Optional) + + + + + + Comma separated list of urls to whitelist + + + )} + + + + + Enable Auto Refresh + + + + + + + + formik.setFieldValue('auto_refresh_enabled', e.target.checked)} + /> + + + When enabled, the data app will automatically refresh its content periodically. + + + + + + {!isEdit && ( + + Finalize settings for this app + + )} + + + Data App Name + + + + + + + + + + + + + + Description + + + (Optional) + + +