|
'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 |
|
|