diff --git a/apps/chat/.env.development b/apps/chat/.env.development index 96db8b602c..3c00fa102b 100644 --- a/apps/chat/.env.development +++ b/apps/chat/.env.development @@ -91,7 +91,7 @@ DIAL_ROLES_FIELD="dial_roles" # Application UI settings -ENABLED_FEATURES="conversations-section,prompts-section,top-settings,top-clear-conversation,top-chat-info,top-chat-model-settings,empty-chat-settings,header,footer,request-api-key,report-an-issue,likes,conversations-sharing,prompts-sharing,input-files,attachments-manager,conversations-publishing,prompts-publishing,custom-logo,input-links,custom-applications,message-templates,marketplace,quick-apps" +ENABLED_FEATURES="conversations-section,prompts-section,top-settings,top-clear-conversation,top-chat-info,top-chat-model-settings,empty-chat-settings,header,footer,request-api-key,report-an-issue,likes,conversations-sharing,prompts-sharing,input-files,attachments-manager,conversations-publishing,prompts-publishing,custom-logo,input-links,custom-applications,message-templates,marketplace,quick-apps,code-apps" NEXT_PUBLIC_APP_NAME="Local Development APP Name" NEXT_PUBLIC_DEFAULT_SYSTEM_PROMPT="" NEXT_PUBLIC_DEFAULT_TEMPERATURE="1" diff --git a/apps/chat/src/components/Chat/ModelList.tsx b/apps/chat/src/components/Chat/ModelList.tsx index d3e7bcfe3d..d2ba110b79 100644 --- a/apps/chat/src/components/Chat/ModelList.tsx +++ b/apps/chat/src/components/Chat/ModelList.tsx @@ -11,7 +11,11 @@ import { useTranslation } from 'next-i18next'; import classNames from 'classnames'; -import { getModelDescription, isQuickApp } from '@/src/utils/app/application'; +import { + getApplicationType, + getModelDescription, + isApplicationStatusUpdating, +} from '@/src/utils/app/application'; import { getOpenAIEntityFullName, groupModelsAndSaveOrder, @@ -37,10 +41,9 @@ import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import { DESCRIPTION_DELIMITER_REGEX } from '@/src/constants/chat'; -import { QuickAppDialog } from '@/src/components/Common/QuickAppDialog'; +import { ApplicationWizard } from '@/src/components/Common/ApplicationWizard/ApplicationWizard'; import { ModelIcon } from '../Chatbar/ModelIcon'; -import { ApplicationDialog } from '../Common/ApplicationDialog'; import { ConfirmDialog } from '../Common/ConfirmDialog'; import ContextMenu from '../Common/ContextMenu'; import { DisableOverlay } from '../Common/DisableOverlay'; @@ -113,6 +116,7 @@ const ModelGroup = ({ const description = getModelDescription(currentEntity); const isPublicEntity = isEntityPublic(currentEntity); + const isModifyDisabled = isApplicationStatusUpdating(currentEntity); const handleSelectVersion = useCallback( (entity: DialAIEntityModel) => onSelect(entity.id), @@ -125,6 +129,7 @@ const ModelGroup = ({ name: t('Edit'), dataQa: 'edit', display: !isPublicEntity, + disabled: isModifyDisabled, Icon: IconPencilMinus, onClick: (e: React.MouseEvent) => { e.stopPropagation(); @@ -157,6 +162,7 @@ const ModelGroup = ({ { name: t('Delete'), dataQa: 'delete', + disabled: isModifyDisabled && !isPublicEntity, display: !isPublicEntity, Icon: IconTrashX, onClick: (e: React.MouseEvent) => { @@ -174,6 +180,7 @@ const ModelGroup = ({ handleEdit, handlePublish, handleOpenDeleteConfirmModal, + isModifyDisabled, ], ); @@ -342,11 +349,7 @@ export const ModelList = ({ const handleEdit = useCallback( (currentEntity: DialAIEntityModel) => { dispatch(ApplicationActions.get(currentEntity.id)); - handleOpenApplicationModal( - isQuickApp(currentEntity) - ? ApplicationType.QUICK_APP - : ApplicationType.CUSTOM_APP, - ); + handleOpenApplicationModal(getApplicationType(currentEntity)); }, [dispatch, handleOpenApplicationModal], ); @@ -441,24 +444,15 @@ export const ModelList = ({ onClose={handleConfirmDialogClose} /> )} - {applicationModal && - applicationModal.type === ApplicationType.CUSTOM_APP && ( - - )} - {applicationModal && - applicationModal.type === ApplicationType.QUICK_APP && ( - - )} + {!!applicationModal && ( + + )} {publishAction && entityForPublish && entityForPublish.id && ( void; - isEdit?: boolean; - currentReference?: string; - selectedApplication?: CustomApplicationModel; -} - -const safeStringify = ( - featureData: DialAIEntityFeatures | Record | undefined, -) => { - if ( - !featureData || - (isObject(featureData) && !Object.keys(featureData).length) - ) { - return ''; - } - - return JSON.stringify(featureData, null, 2); -}; - -const getItemLabel = (item: string) => item; - -const attachmentTypeRegex = new RegExp( - '^([a-zA-Z0-9!*\\-.+]+|\\*)\\/([a-zA-Z0-9!*\\-.+]+|\\*)$', -); - -const ApplicationDialogView: React.FC = ({ - onClose, - isEdit, - currentReference, - selectedApplication, -}) => { - const { - register, - handleSubmit, - setValue, - clearErrors, - setError, - trigger, - control, - formState: { errors, isValid }, - } = useForm({ - mode: 'onChange', - reValidateMode: 'onChange', - }); - - const { t } = useTranslation(Translation.Chat); - - const dispatch = useAppDispatch(); - - const files = useAppSelector(FilesSelectors.selectFiles); - const allTopics = useAppSelector(SettingsSelectors.selectTopics); - - const [deleteLogo, setDeleteLogo] = useState(false); - const [localLogoFile, setLocalLogoFile] = useState(); - const [inputAttachmentTypes, setInputAttachmentTypes] = useState( - [], - ); - const [featuresInput, setFeaturesInput] = useState( - safeStringify(selectedApplication?.features), - ); - const [topics, setTopics] = useState([]); - // const [capabilities, setCapabilities] = useState([]); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [isPublishing, setIsPublishing] = useState(false); - const [maxInputAttachmentsValue, setMaxInputAttachmentsValue] = useState( - selectedApplication?.maxInputAttachments, - ); - - const topicOptions = useMemo( - () => - allTopics.map((value) => ({ - value, - label: value, - ...getTopicColors(value), - })), - [allTopics], - ); - - const selectedOptions = useMemo( - () => topicOptions.filter((op) => topics.includes(op.value)), - [topicOptions, topics], - ); - - const inputClassName = 'input-form input-invalid peer mx-0'; - const applicationToPublish = useMemo(() => { - if (!selectedApplication) { - return undefined; - } - - return { - name: selectedApplication.name, - id: ApiUtils.decodeApiUrl(selectedApplication.id), - folderId: getFolderIdFromEntityId(selectedApplication.id), - iconUrl: selectedApplication.iconUrl, - }; - }, [selectedApplication]); - - const onLogoSelect = useCallback( - (filesIds: string[]) => { - const selectedFileId = filesIds[0]; - const newFile = files.find((file) => file.id === selectedFileId); - - if (newFile) { - setDeleteLogo(false); - setLocalLogoFile(newFile.id); - setValue('iconUrl', newFile.id); - trigger('iconUrl'); - } else { - setLocalLogoFile(undefined); - setValue('iconUrl', ''); - trigger('iconUrl'); - } - }, - [files, setValue, trigger], - ); - - const onDeleteLocalLogoHandler = () => { - setLocalLogoFile(undefined); - setDeleteLogo(true); - setValue('iconUrl', ''); - trigger('iconUrl'); - }; - - const handlePublish = (e: React.FormEvent) => { - e.preventDefault(); - setIsPublishing(true); - }; - - const handlePublishClose = () => { - setIsPublishing(false); - }; - - const handleDelete = useCallback(() => { - if (selectedApplication) { - dispatch(ApplicationActions.delete(selectedApplication)); - } - - onClose(false); - }, [dispatch, onClose, selectedApplication]); - - const handleConfirmDialogOpen = useCallback( - (e: React.FormEvent) => { - e.preventDefault(); - setIsDeleteModalOpen(true); - }, - [setIsDeleteModalOpen], - ); - - const handleConfirmDialogClose = useCallback( - (result: boolean) => { - setIsDeleteModalOpen(false); - - if (result) { - handleDelete(); - } - }, - [handleDelete, setIsDeleteModalOpen], - ); - - const handleAttachmentTypesError = useCallback(() => { - setError('inputAttachmentTypes', { - type: 'manual', - message: t(`Please match the MIME format.`) || '', - }); - }, [setError, t]); - - const handleClearAttachmentTypesError = useCallback(() => { - clearErrors('inputAttachmentTypes'); - }, [clearErrors]); - - const handleAttachmentTypesChange = useCallback( - (selectedItems: string[]) => { - setInputAttachmentTypes(selectedItems); - setValue('inputAttachmentTypes', selectedItems); - if (inputAttachmentTypes.length < selectedItems.length) { - trigger('inputAttachmentTypes'); - } - }, - [inputAttachmentTypes, setValue, trigger], - ); - - useEffect(() => { - if (selectedApplication) { - if (selectedApplication.inputAttachmentTypes) { - setInputAttachmentTypes(selectedApplication.inputAttachmentTypes); - setValue( - 'inputAttachmentTypes', - selectedApplication.inputAttachmentTypes, - ); - } - if (selectedApplication.iconUrl) { - setLocalLogoFile(selectedApplication.iconUrl); - setValue('iconUrl', selectedApplication.iconUrl); - } - setTopics(selectedApplication.topics ?? []); - setValue('topics', selectedApplication.topics ?? []); - } else { - setInputAttachmentTypes([]); - setValue('inputAttachmentTypes', []); - setLocalLogoFile(undefined); - setValue('iconUrl', ''); - setTopics([]); - setValue('topics', []); - // setCapabilities([]); - // setValue('capabilities', []); - } - }, [isEdit, selectedApplication, setValue]); - - const validateFeaturesData = (data: string | null) => { - if (!data?.trim()) { - return true; - } - - try { - const object = JSON.parse(data); - - if (typeof object === 'object' && !!object && !Array.isArray(object)) { - for (const [key, value] of Object.entries(object)) { - if (!key.trim()) { - return t('Keys should not be empty'); - } - - const valueType = typeof value; - if (!(['boolean', 'number'].includes(valueType) || value === null)) { - if (typeof value === 'string' && !value.trim()) { - return t('String values should not be empty'); - } - - if (!['boolean', 'number', 'string'].includes(valueType)) { - return t('Values should be a string, number, boolean or null'); - } - } - } - } else { - return t('Data is not a valid JSON object'); - } - - return true; - } catch (error) { - return t('Invalid JSON string'); - } - }; - - const handleChangeTopics = useCallback( - (option: readonly DropdownSelectorOption[]) => { - const values = option.map((option) => option.value); - setTopics(values); - setValue('topics', values); - }, - [setValue], - ); - - // const handleChangeCapabilities = useCallback( - // (option: readonly DropdownSelectorOption[]) => { - // const values = option.map((option) => option.value); - // setCapabilities(values); - // setValue('capabilities', values); - // }, - // [setValue], - // ); - - const handleChangeHandlerAttachments = ( - event: React.ChangeEvent, - ) => { - const newValue = event.target.value.replace(/[^0-9]/g, ''); - - if (newValue === '') { - setValue('maxInputAttachments', undefined); - } else { - setValue('maxInputAttachments', Number(newValue)); - } - }; - - const onSubmit = (data: FormData) => { - const preparedData = { - ...data, - maxInputAttachments: maxInputAttachmentsValue, - name: data.name.trim(), - description: data.description.trim(), - features: featuresInput ? JSON.parse(featuresInput) : null, - type: EntityType.Application, - isDefault: false, - folderId: '', - topics, - // capabilities, - }; - - if ( - isEdit && - selectedApplication?.name && - currentReference && - selectedApplication.id - ) { - const applicationData: CustomApplicationModel = { - ...preparedData, - reference: currentReference, - id: selectedApplication.id, - }; - - dispatch( - ApplicationActions.update({ - oldApplicationId: selectedApplication.id, - applicationData, - }), - ); - } else { - dispatch(ApplicationActions.create(preparedData)); - } - - onClose(true); - }; - - return ( - <> -
- -
-

- {isEdit ? t('Edit application') : t('Add application')} -

-
-
-
- - - {errors.name && ( - - {errors.name.message} - - )} -
- -
- - - {errors.version && ( - - {errors.version.message} - - )} -
- -
- - ( - - )} - /> - {!localLogoFile && errors.iconUrl && ( - - {errors.iconUrl.message} - - )} -
- -
- - ( - - )} - /> -
- - {/*
- - ( - - )} - /> - {!localLogoFile && errors.iconUrl && ( - - {errors.iconUrl.message} - - )} -
*/} - -
- -