| 'use client' |
| import React, { useState } from 'react' |
| import { useTranslation } from 'react-i18next' |
| import { RiCloseLine } from '@remixicon/react' |
| import AppIconPicker from '../../base/app-icon-picker' |
| import Modal from '@/app/components/base/modal' |
| import Button from '@/app/components/base/button' |
| import Input from '@/app/components/base/input' |
| import Textarea from '@/app/components/base/textarea' |
| import Switch from '@/app/components/base/switch' |
| import Toast from '@/app/components/base/toast' |
| import AppIcon from '@/app/components/base/app-icon' |
| import { useProviderContext } from '@/context/provider-context' |
| import AppsFull from '@/app/components/billing/apps-full-in-dialog' |
| import type { AppIconType } from '@/types/app' |
|
|
| export type CreateAppModalProps = { |
| show: boolean |
| isEditModal?: boolean |
| appName: string |
| appDescription: string |
| appIconType: AppIconType | null |
| appIcon: string |
| appIconBackground?: string | null |
| appIconUrl?: string | null |
| appMode?: string |
| appUseIconAsAnswerIcon?: boolean |
| onConfirm: (info: { |
| name: string |
| icon_type: AppIconType |
| icon: string |
| icon_background?: string |
| description: string |
| use_icon_as_answer_icon?: boolean |
| }) => Promise<void> |
| onHide: () => void |
| } |
|
|
| const CreateAppModal = ({ |
| show = false, |
| isEditModal = false, |
| appIconType, |
| appIcon: _appIcon, |
| appIconBackground, |
| appIconUrl, |
| appName, |
| appDescription, |
| appMode, |
| appUseIconAsAnswerIcon, |
| onConfirm, |
| onHide, |
| }: CreateAppModalProps) => { |
| const { t } = useTranslation() |
|
|
| const [name, setName] = React.useState(appName) |
| const [appIcon, setAppIcon] = useState( |
| () => appIconType === 'image' |
| ? { type: 'image' as const, fileId: _appIcon, url: appIconUrl } |
| : { type: 'emoji' as const, icon: _appIcon, background: appIconBackground }, |
| ) |
| const [showAppIconPicker, setShowAppIconPicker] = useState(false) |
| const [description, setDescription] = useState(appDescription || '') |
| const [useIconAsAnswerIcon, setUseIconAsAnswerIcon] = useState(appUseIconAsAnswerIcon || false) |
|
|
| const { plan, enableBilling } = useProviderContext() |
| const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps) |
|
|
| const submit = () => { |
| if (!name.trim()) { |
| Toast.notify({ type: 'error', message: t('explore.appCustomize.nameRequired') }) |
| return |
| } |
| onConfirm({ |
| name, |
| icon_type: appIcon.type, |
| icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId, |
| icon_background: appIcon.type === 'emoji' ? appIcon.background! : undefined, |
| description, |
| use_icon_as_answer_icon: useIconAsAnswerIcon, |
| }) |
| onHide() |
| } |
|
|
| return ( |
| <> |
| <Modal |
| isShow={show} |
| onClose={() => {}} |
| className='relative !max-w-[480px] px-8' |
| > |
| <div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onHide}> |
| <RiCloseLine className='w-4 h-4 text-gray-500' /> |
| </div> |
| {isEditModal && ( |
| <div className='mb-9 font-semibold text-xl leading-[30px] text-gray-900'>{t('app.editAppTitle')}</div> |
| )} |
| {!isEditModal && ( |
| <div className='mb-9 font-semibold text-xl leading-[30px] text-gray-900'>{t('explore.appCustomize.title', { name: appName })}</div> |
| )} |
| <div className='mb-9'> |
| {/* icon & name */} |
| <div className='pt-2'> |
| <div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.newApp.captionName')}</div> |
| <div className='flex items-center justify-between space-x-2'> |
| <AppIcon |
| size='large' |
| onClick={() => { setShowAppIconPicker(true) }} |
| className='cursor-pointer' |
| iconType={appIcon.type} |
| icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon} |
| background={appIcon.type === 'image' ? undefined : appIcon.background} |
| imageUrl={appIcon.type === 'image' ? appIcon.url : undefined} |
| /> |
| <Input |
| value={name} |
| onChange={e => setName(e.target.value)} |
| placeholder={t('app.newApp.appNamePlaceholder') || ''} |
| className='grow h-10' |
| /> |
| </div> |
| </div> |
| {/* description */} |
| <div className='pt-2'> |
| <div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.newApp.captionDescription')}</div> |
| <Textarea |
| className='resize-none' |
| placeholder={t('app.newApp.appDescriptionPlaceholder') || ''} |
| value={description} |
| onChange={e => setDescription(e.target.value)} |
| /> |
| </div> |
| {/* answer icon */} |
| {isEditModal && (appMode === 'chat' || appMode === 'advanced-chat' || appMode === 'agent-chat') && ( |
| <div className='pt-2'> |
| <div className='flex justify-between items-center'> |
| <div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.answerIcon.title')}</div> |
| <Switch |
| defaultValue={useIconAsAnswerIcon} |
| onChange={v => setUseIconAsAnswerIcon(v)} |
| /> |
| </div> |
| <p className='body-xs-regular text-gray-500'>{t('app.answerIcon.descriptionInExplore')}</p> |
| </div> |
| )} |
| {!isEditModal && isAppsFull && <AppsFull loc='app-explore-create' />} |
| </div> |
| <div className='flex flex-row-reverse'> |
| <Button disabled={!isEditModal && isAppsFull} className='w-24 ml-2' variant='primary' onClick={submit}>{!isEditModal ? t('common.operation.create') : t('common.operation.save')}</Button> |
| <Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button> |
| </div> |
| </Modal> |
| {showAppIconPicker && <AppIconPicker |
| onSelect={(payload) => { |
| setAppIcon(payload) |
| setShowAppIconPicker(false) |
| }} |
| onClose={() => { |
| setAppIcon(appIconType === 'image' |
| ? { type: 'image' as const, url: appIconUrl, fileId: _appIcon } |
| : { type: 'emoji' as const, icon: _appIcon, background: appIconBackground }) |
| setShowAppIconPicker(false) |
| }} |
| />} |
| </> |
| ) |
| } |
|
|
| export default CreateAppModal |
|
|