'use client' import type { FC } from 'react' import React, { useCallback, useEffect, useState } from 'react' import { RiArrowRightSLine, RiCloseLine } from '@remixicon/react' import Link from 'next/link' import { Trans, useTranslation } from 'react-i18next' import { SparklesSoft } from '@/app/components/base/icons/src/public/common' import Modal from '@/app/components/base/modal' import ActionButton from '@/app/components/base/action-button' import Button from '@/app/components/base/button' import Divider from '@/app/components/base/divider' import Input from '@/app/components/base/input' import Textarea from '@/app/components/base/textarea' import AppIcon from '@/app/components/base/app-icon' import Switch from '@/app/components/base/switch' import PremiumBadge from '@/app/components/base/premium-badge' import { SimpleSelect } from '@/app/components/base/select' import type { AppDetailResponse } from '@/models/app' import type { AppIconType, AppSSO, Language } from '@/types/app' import { useToastContext } from '@/app/components/base/toast' import { languages } from '@/i18n/language' import Tooltip from '@/app/components/base/tooltip' import { useProviderContext } from '@/context/provider-context' import { useModalContext } from '@/context/modal-context' import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import AppIconPicker from '@/app/components/base/app-icon-picker' import cn from '@/utils/classnames' import { useDocLink } from '@/context/i18n' export type ISettingsModalProps = { isChat: boolean appInfo: AppDetailResponse & Partial isShow: boolean defaultValue?: string onClose: () => void onSave?: (params: ConfigParams) => Promise } export type ConfigParams = { title: string description: string default_language: string chat_color_theme: string chat_color_theme_inverted: boolean prompt_public: boolean copyright: string privacy_policy: string custom_disclaimer: string icon_type: AppIconType icon: string icon_background?: string show_workflow_steps: boolean use_icon_as_answer_icon: boolean enable_sso?: boolean } const prefixSettings = 'appOverview.overview.appInfo.settings' const SettingsModal: FC = ({ isChat, appInfo, isShow = false, onClose, onSave, }) => { const { notify } = useToastContext() const [isShowMore, setIsShowMore] = useState(false) const { title, icon_type, icon, icon_background, icon_url, description, chat_color_theme, chat_color_theme_inverted, copyright, privacy_policy, custom_disclaimer, default_language, show_workflow_steps, use_icon_as_answer_icon, } = appInfo.site const [inputInfo, setInputInfo] = useState({ title, desc: description, chatColorTheme: chat_color_theme, chatColorThemeInverted: chat_color_theme_inverted, copyright, copyrightSwitchValue: !!copyright, privacyPolicy: privacy_policy, customDisclaimer: custom_disclaimer, show_workflow_steps, use_icon_as_answer_icon, enable_sso: appInfo.enable_sso, }) const [language, setLanguage] = useState(default_language) const [saveLoading, setSaveLoading] = useState(false) const { t } = useTranslation() const docLink = useDocLink() const [showAppIconPicker, setShowAppIconPicker] = useState(false) const [appIcon, setAppIcon] = useState( icon_type === 'image' ? { type: 'image', url: icon_url!, fileId: icon } : { type: 'emoji', icon, background: icon_background! }, ) const { enableBilling, plan, webappCopyrightEnabled } = useProviderContext() const { setShowPricingModal, setShowAccountSettingModal } = useModalContext() const isFreePlan = plan.type === 'sandbox' const handlePlanClick = useCallback(() => { if (isFreePlan) setShowPricingModal() else setShowAccountSettingModal({ payload: 'billing' }) }, [isFreePlan, setShowAccountSettingModal, setShowPricingModal]) useEffect(() => { setInputInfo({ title, desc: description, chatColorTheme: chat_color_theme, chatColorThemeInverted: chat_color_theme_inverted, copyright, copyrightSwitchValue: !!copyright, privacyPolicy: privacy_policy, customDisclaimer: custom_disclaimer, show_workflow_steps, use_icon_as_answer_icon, enable_sso: appInfo.enable_sso, }) setLanguage(default_language) setAppIcon(icon_type === 'image' ? { type: 'image', url: icon_url!, fileId: icon } : { type: 'emoji', icon, background: icon_background! }) }, [appInfo, chat_color_theme, chat_color_theme_inverted, copyright, custom_disclaimer, default_language, description, icon, icon_background, icon_type, icon_url, privacy_policy, show_workflow_steps, title, use_icon_as_answer_icon]) const onHide = () => { onClose() setTimeout(() => { setIsShowMore(false) }, 200) } const onClickSave = async () => { if (!inputInfo.title) { notify({ type: 'error', message: t('app.newApp.nameNotEmpty') }) return } const validateColorHex = (hex: string | null) => { if (hex === null || hex?.length === 0) return true const regex = /#([A-Fa-f0-9]{6})/ const check = regex.test(hex) return check } const validatePrivacyPolicy = (privacyPolicy: string | null) => { if (privacyPolicy === null || privacyPolicy?.length === 0) return true return privacyPolicy.startsWith('http://') || privacyPolicy.startsWith('https://') } if (inputInfo !== null) { if (!validateColorHex(inputInfo.chatColorTheme)) { notify({ type: 'error', message: t(`${prefixSettings}.invalidHexMessage`) }) return } if (!validatePrivacyPolicy(inputInfo.privacyPolicy)) { notify({ type: 'error', message: t(`${prefixSettings}.invalidPrivacyPolicy`) }) return } } setSaveLoading(true) const params = { title: inputInfo.title, description: inputInfo.desc, default_language: language, chat_color_theme: inputInfo.chatColorTheme, chat_color_theme_inverted: inputInfo.chatColorThemeInverted, prompt_public: false, copyright: !webappCopyrightEnabled ? '' : inputInfo.copyrightSwitchValue ? inputInfo.copyright : '', privacy_policy: inputInfo.privacyPolicy, custom_disclaimer: inputInfo.customDisclaimer, icon_type: appIcon.type, icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId, icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined, show_workflow_steps: inputInfo.show_workflow_steps, use_icon_as_answer_icon: inputInfo.use_icon_as_answer_icon, enable_sso: inputInfo.enable_sso, } await onSave?.(params) setSaveLoading(false) onHide() } const onChange = (field: string) => { return (e: React.ChangeEvent) => { let value: string | boolean if (e.target.type === 'checkbox') value = (e.target as HTMLInputElement).checked else value = e.target.value setInputInfo(item => ({ ...item, [field]: value })) } } const onDesChange = (value: string) => { setInputInfo(item => ({ ...item, desc: value })) } return ( <> {/* header */}
{t(`${prefixSettings}.title`)}
{t(`${prefixSettings}.modalTip`)} {t('common.operation.learnMore')}
{/* form body */}
{/* name & icon */}
{t(`${prefixSettings}.webName`)}
{ setShowAppIconPicker(true) }} className='mt-2 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} />
{/* description */}
{t(`${prefixSettings}.webDesc`)}