| import { EventEmitterContextProvider } from '@/context/event-emitter' | import { EventEmitterContextProvider } from '@/context/event-emitter' | ||||
| import { ProviderContextProvider } from '@/context/provider-context' | import { ProviderContextProvider } from '@/context/provider-context' | ||||
| import { ModalContextProvider } from '@/context/modal-context' | import { ModalContextProvider } from '@/context/modal-context' | ||||
| import { TanstackQueryIniter } from '@/context/query-client' | |||||
| const Layout = ({ children }: { children: ReactNode }) => { | const Layout = ({ children }: { children: ReactNode }) => { | ||||
| return ( | return ( | ||||
| <> | <> | ||||
| <GA gaType={GaType.admin} /> | <GA gaType={GaType.admin} /> | ||||
| <SwrInitor> | <SwrInitor> | ||||
| <TanstackQueryIniter> | |||||
| <AppContextProvider> | |||||
| <EventEmitterContextProvider> | |||||
| <ProviderContextProvider> | |||||
| <ModalContextProvider> | |||||
| <HeaderWrapper> | |||||
| <Header /> | |||||
| </HeaderWrapper> | |||||
| {children} | |||||
| </ModalContextProvider> | |||||
| </ProviderContextProvider> | |||||
| </EventEmitterContextProvider> | |||||
| </AppContextProvider> | |||||
| </TanstackQueryIniter> | |||||
| <AppContextProvider> | |||||
| <EventEmitterContextProvider> | |||||
| <ProviderContextProvider> | |||||
| <ModalContextProvider> | |||||
| <HeaderWrapper> | |||||
| <Header /> | |||||
| </HeaderWrapper> | |||||
| {children} | |||||
| </ModalContextProvider> | |||||
| </ProviderContextProvider> | |||||
| </EventEmitterContextProvider> | |||||
| </AppContextProvider> | |||||
| </SwrInitor> | </SwrInitor> | ||||
| </> | </> | ||||
| ) | ) |
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { useContext } from 'use-context-selector' | import { useContext } from 'use-context-selector' | ||||
| import DeleteAccount from '../delete-account' | |||||
| import s from './index.module.css' | import s from './index.module.css' | ||||
| import Collapse from '@/app/components/header/account-setting/collapse' | import Collapse from '@/app/components/header/account-setting/collapse' | ||||
| import type { IItem } from '@/app/components/header/account-setting/collapse' | import type { IItem } from '@/app/components/header/account-setting/collapse' | ||||
| import Modal from '@/app/components/base/modal' | import Modal from '@/app/components/base/modal' | ||||
| import Confirm from '@/app/components/base/confirm' | |||||
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| import { updateUserProfile } from '@/service/common' | import { updateUserProfile } from '@/service/common' | ||||
| import { useAppContext } from '@/context/app-context' | import { useAppContext } from '@/context/app-context' | ||||
| } | } | ||||
| { | { | ||||
| showDeleteAccountModal && ( | showDeleteAccountModal && ( | ||||
| <Confirm | |||||
| isShow | |||||
| <DeleteAccount | |||||
| onCancel={() => setShowDeleteAccountModal(false)} | onCancel={() => setShowDeleteAccountModal(false)} | ||||
| onConfirm={() => setShowDeleteAccountModal(false)} | onConfirm={() => setShowDeleteAccountModal(false)} | ||||
| showCancel={false} | |||||
| type='warning' | |||||
| title={t('common.account.delete')} | |||||
| content={ | |||||
| <> | |||||
| <div className='my-1 text-text-destructive body-md-medium'> | |||||
| {t('common.account.deleteTip')} | |||||
| </div> | |||||
| <div className='mt-3 text-sm leading-5'> | |||||
| <span>{t('common.account.deleteConfirmTip')}</span> | |||||
| <a | |||||
| className='text-text-accent cursor' | |||||
| href={`mailto:support@dify.ai?subject=Delete Account Request&body=Delete Account: ${userProfile.email}`} | |||||
| target='_blank' | |||||
| rel='noreferrer noopener' | |||||
| onClick={(e) => { | |||||
| e.preventDefault() | |||||
| window.location.href = e.currentTarget.href | |||||
| }} | |||||
| > | |||||
| support@dify.ai | |||||
| </a> | |||||
| </div> | |||||
| <div className='my-2 px-3 py-2 rounded-lg bg-components-input-bg-active border border-components-input-border-active system-sm-regular text-components-input-text-filled'>{`${t('common.account.delete')}: ${userProfile.email}`}</div> | |||||
| </> | |||||
| } | |||||
| confirmText={t('common.operation.ok') as string} | |||||
| /> | /> | ||||
| ) | ) | ||||
| } | } |
| 'use client' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { useCallback, useState } from 'react' | |||||
| import Link from 'next/link' | |||||
| import { useSendDeleteAccountEmail } from '../state' | |||||
| import { useAppContext } from '@/context/app-context' | |||||
| import Input from '@/app/components/base/input' | |||||
| import Button from '@/app/components/base/button' | |||||
| type DeleteAccountProps = { | |||||
| onCancel: () => void | |||||
| onConfirm: () => void | |||||
| } | |||||
| export default function CheckEmail(props: DeleteAccountProps) { | |||||
| const { t } = useTranslation() | |||||
| const { userProfile } = useAppContext() | |||||
| const [userInputEmail, setUserInputEmail] = useState('') | |||||
| const { isPending: isSendingEmail, mutateAsync: getDeleteEmailVerifyCode } = useSendDeleteAccountEmail() | |||||
| const handleConfirm = useCallback(async () => { | |||||
| try { | |||||
| const ret = await getDeleteEmailVerifyCode() | |||||
| if (ret.result === 'success') | |||||
| props.onConfirm() | |||||
| } | |||||
| catch (error) { console.error(error) } | |||||
| }, [getDeleteEmailVerifyCode, props]) | |||||
| return <> | |||||
| <div className='py-1 text-text-destructive body-md-medium'> | |||||
| {t('common.account.deleteTip')} | |||||
| </div> | |||||
| <div className='pt-1 pb-2 text-text-secondary body-md-regular'> | |||||
| {t('common.account.deletePrivacyLinkTip')} | |||||
| <Link href='https://dify.ai/privacy' className='text-text-accent'>{t('common.account.deletePrivacyLink')}</Link> | |||||
| </div> | |||||
| <label className='mt-3 mb-1 h-6 flex items-center system-sm-semibold text-text-secondary'>{t('common.account.deleteLabel')}</label> | |||||
| <Input placeholder={t('common.account.deletePlaceholder') as string} onChange={(e) => { | |||||
| setUserInputEmail(e.target.value) | |||||
| }} /> | |||||
| <div className='w-full flex flex-col mt-3 gap-2'> | |||||
| <Button className='w-full' disabled={userInputEmail !== userProfile.email || isSendingEmail} loading={isSendingEmail} variant='primary' onClick={handleConfirm}>{t('common.account.sendVerificationButton')}</Button> | |||||
| <Button className='w-full' onClick={props.onCancel}>{t('common.operation.cancel')}</Button> | |||||
| </div> | |||||
| </> | |||||
| } |
| 'use client' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { useCallback, useState } from 'react' | |||||
| import { useRouter } from 'next/navigation' | |||||
| import { useDeleteAccountFeedback } from '../state' | |||||
| import { useAppContext } from '@/context/app-context' | |||||
| import Button from '@/app/components/base/button' | |||||
| import CustomDialog from '@/app/components/base/dialog' | |||||
| import Textarea from '@/app/components/base/textarea' | |||||
| import Toast from '@/app/components/base/toast' | |||||
| import { logout } from '@/service/common' | |||||
| type DeleteAccountProps = { | |||||
| onCancel: () => void | |||||
| onConfirm: () => void | |||||
| } | |||||
| export default function FeedBack(props: DeleteAccountProps) { | |||||
| const { t } = useTranslation() | |||||
| const { userProfile } = useAppContext() | |||||
| const router = useRouter() | |||||
| const [userFeedback, setUserFeedback] = useState('') | |||||
| const { isPending, mutateAsync: sendFeedback } = useDeleteAccountFeedback() | |||||
| const handleSuccess = useCallback(async () => { | |||||
| try { | |||||
| await logout({ | |||||
| url: '/logout', | |||||
| params: {}, | |||||
| }) | |||||
| localStorage.removeItem('refresh_token') | |||||
| localStorage.removeItem('console_token') | |||||
| router.push('/signin') | |||||
| Toast.notify({ type: 'info', message: t('common.account.deleteSuccessTip') }) | |||||
| } | |||||
| catch (error) { console.error(error) } | |||||
| }, [router, t]) | |||||
| const handleSubmit = useCallback(async () => { | |||||
| try { | |||||
| await sendFeedback({ feedback: userFeedback, email: userProfile.email }) | |||||
| props.onConfirm() | |||||
| await handleSuccess() | |||||
| } | |||||
| catch (error) { console.error(error) } | |||||
| }, [handleSuccess, userFeedback, sendFeedback, userProfile, props]) | |||||
| const handleSkip = useCallback(() => { | |||||
| props.onCancel() | |||||
| handleSuccess() | |||||
| }, [handleSuccess, props]) | |||||
| return <CustomDialog | |||||
| show={true} | |||||
| onClose={props.onCancel} | |||||
| title={t('common.account.feedbackTitle')} | |||||
| className="max-w-[480px]" | |||||
| footer={false} | |||||
| > | |||||
| <label className='mt-3 mb-1 flex items-center system-sm-semibold text-text-secondary'>{t('common.account.feedbackLabel')}</label> | |||||
| <Textarea rows={6} value={userFeedback} placeholder={t('common.account.feedbackPlaceholder') as string} onChange={(e) => { | |||||
| setUserFeedback(e.target.value) | |||||
| }} /> | |||||
| <div className='w-full flex flex-col mt-3 gap-2'> | |||||
| <Button className='w-full' loading={isPending} variant='primary' onClick={handleSubmit}>{t('common.operation.submit')}</Button> | |||||
| <Button className='w-full' onClick={handleSkip}>{t('common.operation.skip')}</Button> | |||||
| </div> | |||||
| </CustomDialog> | |||||
| } |
| 'use client' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { useCallback, useEffect, useState } from 'react' | |||||
| import Link from 'next/link' | |||||
| import { useAccountDeleteStore, useConfirmDeleteAccount, useSendDeleteAccountEmail } from '../state' | |||||
| import Input from '@/app/components/base/input' | |||||
| import Button from '@/app/components/base/button' | |||||
| import Countdown from '@/app/components/signin/countdown' | |||||
| const CODE_EXP = /[A-Za-z\d]{6}/gi | |||||
| type DeleteAccountProps = { | |||||
| onCancel: () => void | |||||
| onConfirm: () => void | |||||
| } | |||||
| export default function VerifyEmail(props: DeleteAccountProps) { | |||||
| const { t } = useTranslation() | |||||
| const emailToken = useAccountDeleteStore(state => state.sendEmailToken) | |||||
| const [verificationCode, setVerificationCode] = useState<string>() | |||||
| const [shouldButtonDisabled, setShouldButtonDisabled] = useState(true) | |||||
| const { mutate: sendEmail } = useSendDeleteAccountEmail() | |||||
| const { isPending: isDeleting, mutateAsync: confirmDeleteAccount } = useConfirmDeleteAccount() | |||||
| useEffect(() => { | |||||
| setShouldButtonDisabled(!(verificationCode && CODE_EXP.test(verificationCode)) || isDeleting) | |||||
| }, [verificationCode, isDeleting]) | |||||
| const handleConfirm = useCallback(async () => { | |||||
| try { | |||||
| const ret = await confirmDeleteAccount({ code: verificationCode!, token: emailToken }) | |||||
| if (ret.result === 'success') | |||||
| props.onConfirm() | |||||
| } | |||||
| catch (error) { console.error(error) } | |||||
| }, [emailToken, verificationCode, confirmDeleteAccount, props]) | |||||
| return <> | |||||
| <div className='pt-1 text-text-destructive body-md-medium'> | |||||
| {t('common.account.deleteTip')} | |||||
| </div> | |||||
| <div className='pt-1 pb-2 text-text-secondary body-md-regular'> | |||||
| {t('common.account.deletePrivacyLinkTip')} | |||||
| <Link href='https://dify.ai/privacy' className='text-text-accent'>{t('common.account.deletePrivacyLink')}</Link> | |||||
| </div> | |||||
| <label className='mt-3 mb-1 h-6 flex items-center system-sm-semibold text-text-secondary'>{t('common.account.verificationLabel')}</label> | |||||
| <Input minLength={6} maxLength={6} placeholder={t('common.account.verificationPlaceholder') as string} onChange={(e) => { | |||||
| setVerificationCode(e.target.value) | |||||
| }} /> | |||||
| <div className='w-full flex flex-col mt-3 gap-2'> | |||||
| <Button className='w-full' disabled={shouldButtonDisabled} loading={isDeleting} variant='warning' onClick={handleConfirm}>{t('common.account.permanentlyDeleteButton')}</Button> | |||||
| <Button className='w-full' onClick={props.onCancel}>{t('common.operation.cancel')}</Button> | |||||
| <Countdown onResend={sendEmail} /> | |||||
| </div> | |||||
| </> | |||||
| } |
| 'use client' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { useCallback, useState } from 'react' | |||||
| import CheckEmail from './components/check-email' | |||||
| import VerifyEmail from './components/verify-email' | |||||
| import FeedBack from './components/feed-back' | |||||
| import CustomDialog from '@/app/components/base/dialog' | |||||
| import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown' | |||||
| type DeleteAccountProps = { | |||||
| onCancel: () => void | |||||
| onConfirm: () => void | |||||
| } | |||||
| export default function DeleteAccount(props: DeleteAccountProps) { | |||||
| const { t } = useTranslation() | |||||
| const [showVerifyEmail, setShowVerifyEmail] = useState(false) | |||||
| const [showFeedbackDialog, setShowFeedbackDialog] = useState(false) | |||||
| const handleEmailCheckSuccess = useCallback(async () => { | |||||
| try { | |||||
| setShowVerifyEmail(true) | |||||
| localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`) | |||||
| } | |||||
| catch (error) { console.error(error) } | |||||
| }, []) | |||||
| if (showFeedbackDialog) | |||||
| return <FeedBack onCancel={props.onCancel} onConfirm={props.onConfirm} /> | |||||
| return <CustomDialog | |||||
| show={true} | |||||
| onClose={props.onCancel} | |||||
| title={t('common.account.delete')} | |||||
| className="max-w-[480px]" | |||||
| footer={false} | |||||
| > | |||||
| {!showVerifyEmail && <CheckEmail onCancel={props.onCancel} onConfirm={handleEmailCheckSuccess} />} | |||||
| {showVerifyEmail && <VerifyEmail onCancel={props.onCancel} onConfirm={() => { | |||||
| setShowFeedbackDialog(true) | |||||
| }} />} | |||||
| </CustomDialog> | |||||
| } |
| import { useMutation } from '@tanstack/react-query' | |||||
| import { create } from 'zustand' | |||||
| import { sendDeleteAccountCode, submitDeleteAccountFeedback, verifyDeleteAccountCode } from '@/service/common' | |||||
| type State = { | |||||
| sendEmailToken: string | |||||
| setSendEmailToken: (token: string) => void | |||||
| } | |||||
| export const useAccountDeleteStore = create<State>(set => ({ | |||||
| sendEmailToken: '', | |||||
| setSendEmailToken: (token: string) => set({ sendEmailToken: token }), | |||||
| })) | |||||
| export function useSendDeleteAccountEmail() { | |||||
| const updateEmailToken = useAccountDeleteStore(state => state.setSendEmailToken) | |||||
| return useMutation({ | |||||
| mutationKey: ['delete-account'], | |||||
| mutationFn: sendDeleteAccountCode, | |||||
| onSuccess: (ret) => { | |||||
| if (ret.result === 'success') | |||||
| updateEmailToken(ret.data) | |||||
| }, | |||||
| }) | |||||
| } | |||||
| export function useConfirmDeleteAccount() { | |||||
| return useMutation({ | |||||
| mutationKey: ['confirm-delete-account'], | |||||
| mutationFn: verifyDeleteAccountCode, | |||||
| }) | |||||
| } | |||||
| export function useDeleteAccountFeedback() { | |||||
| return useMutation({ | |||||
| mutationKey: ['delete-account-feedback'], | |||||
| mutationFn: submitDeleteAccountFeedback, | |||||
| }) | |||||
| } |
| </Transition.Child> | </Transition.Child> | ||||
| <div className="fixed inset-0 overflow-y-auto"> | <div className="fixed inset-0 overflow-y-auto"> | ||||
| <div className="flex items-center justify-center min-h-full p-4 text-center"> | |||||
| <div className="flex items-center justify-center min-h-full"> | |||||
| <Transition.Child | <Transition.Child | ||||
| as={Fragment} | as={Fragment} | ||||
| enter="ease-out duration-300" | enter="ease-out duration-300" | ||||
| leaveFrom="opacity-100 scale-100" | leaveFrom="opacity-100 scale-100" | ||||
| leaveTo="opacity-0 scale-95" | leaveTo="opacity-0 scale-95" | ||||
| > | > | ||||
| <Dialog.Panel className={classNames('w-full max-w-[800px] p-0 overflow-hidden text-left text-gray-900 align-middle transition-all transform bg-white shadow-xl rounded-2xl', className)}> | |||||
| <Dialog.Panel className={classNames('w-full max-w-[800px] p-6 overflow-hidden transition-all transform bg-components-panel-bg border-[0.5px] border-components-panel-border shadow-xl rounded-2xl', className)}> | |||||
| {Boolean(title) && ( | {Boolean(title) && ( | ||||
| <Dialog.Title | <Dialog.Title | ||||
| as={titleAs || 'h3'} | as={titleAs || 'h3'} | ||||
| className={classNames('px-8 py-6 text-lg font-medium leading-6 text-gray-900', titleClassName)} | |||||
| className={classNames('pr-8 pb-3 title-2xl-semi-bold text-text-primary', titleClassName)} | |||||
| > | > | ||||
| {title} | {title} | ||||
| </Dialog.Title> | </Dialog.Title> | ||||
| )} | )} | ||||
| <div className={classNames('px-8 text-lg font-medium leading-6', bodyClassName)}> | |||||
| <div className={classNames(bodyClassName)}> | |||||
| {children} | {children} | ||||
| </div> | </div> | ||||
| {Boolean(footer) && ( | {Boolean(footer) && ( | ||||
| <div className={classNames('flex items-center justify-end gap-2 px-8 py-6', footerClassName)}> | |||||
| <div className={classNames('flex items-center justify-end gap-2 px-6 pb-6 pt-3', footerClassName)}> | |||||
| {footer} | {footer} | ||||
| </div> | </div> | ||||
| )} | )} |
| .modal { | |||||
| padding: 24px 32px !important; | |||||
| width: 400px !important; | |||||
| } | |||||
| .bg { | |||||
| background: linear-gradient(180deg, rgba(217, 45, 32, 0.05) 0%, rgba(217, 45, 32, 0.00) 24.02%), #F9FAFB; | |||||
| } | |||||
| 'use client' | |||||
| import { useState } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { useContext, useContextSelector } from 'use-context-selector' | |||||
| import Collapse from '../collapse' | |||||
| import type { IItem } from '../collapse' | |||||
| import s from './index.module.css' | |||||
| import classNames from '@/utils/classnames' | |||||
| import Modal from '@/app/components/base/modal' | |||||
| import Confirm from '@/app/components/base/confirm' | |||||
| import Button from '@/app/components/base/button' | |||||
| import { updateUserProfile } from '@/service/common' | |||||
| import AppContext, { useAppContext } from '@/context/app-context' | |||||
| import { ToastContext } from '@/app/components/base/toast' | |||||
| import AppIcon from '@/app/components/base/app-icon' | |||||
| import Avatar from '@/app/components/base/avatar' | |||||
| import { IS_CE_EDITION } from '@/config' | |||||
| const titleClassName = ` | |||||
| text-sm font-medium text-gray-900 | |||||
| ` | |||||
| const descriptionClassName = ` | |||||
| mt-1 text-xs font-normal text-gray-500 | |||||
| ` | |||||
| const inputClassName = ` | |||||
| mt-2 w-full px-3 py-2 bg-gray-100 rounded | |||||
| text-sm font-normal text-gray-800 | |||||
| ` | |||||
| const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ | |||||
| export default function AccountPage() { | |||||
| const { t } = useTranslation() | |||||
| const { mutateUserProfile, userProfile, apps } = useAppContext() | |||||
| const { notify } = useContext(ToastContext) | |||||
| const [editNameModalVisible, setEditNameModalVisible] = useState(false) | |||||
| const [editName, setEditName] = useState('') | |||||
| const [editing, setEditing] = useState(false) | |||||
| const [editPasswordModalVisible, setEditPasswordModalVisible] = useState(false) | |||||
| const [currentPassword, setCurrentPassword] = useState('') | |||||
| const [password, setPassword] = useState('') | |||||
| const [confirmPassword, setConfirmPassword] = useState('') | |||||
| const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false) | |||||
| const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures) | |||||
| const handleEditName = () => { | |||||
| setEditNameModalVisible(true) | |||||
| setEditName(userProfile.name) | |||||
| } | |||||
| const handleSaveName = async () => { | |||||
| try { | |||||
| setEditing(true) | |||||
| await updateUserProfile({ url: 'account/name', body: { name: editName } }) | |||||
| notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) | |||||
| mutateUserProfile() | |||||
| setEditNameModalVisible(false) | |||||
| setEditing(false) | |||||
| } | |||||
| catch (e) { | |||||
| notify({ type: 'error', message: (e as Error).message }) | |||||
| setEditNameModalVisible(false) | |||||
| setEditing(false) | |||||
| } | |||||
| } | |||||
| const showErrorMessage = (message: string) => { | |||||
| notify({ | |||||
| type: 'error', | |||||
| message, | |||||
| }) | |||||
| } | |||||
| const valid = () => { | |||||
| if (!password.trim()) { | |||||
| showErrorMessage(t('login.error.passwordEmpty')) | |||||
| return false | |||||
| } | |||||
| if (!validPassword.test(password)) { | |||||
| showErrorMessage(t('login.error.passwordInvalid')) | |||||
| return false | |||||
| } | |||||
| if (password !== confirmPassword) { | |||||
| showErrorMessage(t('common.account.notEqual')) | |||||
| return false | |||||
| } | |||||
| return true | |||||
| } | |||||
| const resetPasswordForm = () => { | |||||
| setCurrentPassword('') | |||||
| setPassword('') | |||||
| setConfirmPassword('') | |||||
| } | |||||
| const handleSavePassword = async () => { | |||||
| if (!valid()) | |||||
| return | |||||
| try { | |||||
| setEditing(true) | |||||
| await updateUserProfile({ | |||||
| url: 'account/password', | |||||
| body: { | |||||
| password: currentPassword, | |||||
| new_password: password, | |||||
| repeat_new_password: confirmPassword, | |||||
| }, | |||||
| }) | |||||
| notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) | |||||
| mutateUserProfile() | |||||
| setEditPasswordModalVisible(false) | |||||
| resetPasswordForm() | |||||
| setEditing(false) | |||||
| } | |||||
| catch (e) { | |||||
| notify({ type: 'error', message: (e as Error).message }) | |||||
| setEditPasswordModalVisible(false) | |||||
| setEditing(false) | |||||
| } | |||||
| } | |||||
| const renderAppItem = (item: IItem) => { | |||||
| return ( | |||||
| <div className='flex px-3 py-1'> | |||||
| <div className='mr-3'> | |||||
| <AppIcon size='tiny' /> | |||||
| </div> | |||||
| <div className='mt-[3px] text-xs font-medium text-gray-700 leading-[18px]'>{item.name}</div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| return ( | |||||
| <> | |||||
| <div className='mb-8'> | |||||
| <div className={titleClassName}>{t('common.account.avatar')}</div> | |||||
| <Avatar name={userProfile.name} size={64} className='mt-2' /> | |||||
| </div> | |||||
| <div className='mb-8'> | |||||
| <div className={titleClassName}>{t('common.account.name')}</div> | |||||
| <div className={classNames('flex items-center justify-between mt-2 w-full h-9 px-3 bg-gray-100 rounded text-sm font-normal text-gray-800 cursor-pointer group')}> | |||||
| {userProfile.name} | |||||
| <div className='items-center hidden h-6 px-2 text-xs font-normal bg-white border border-gray-200 rounded-md group-hover:flex' onClick={handleEditName}>{t('common.operation.edit')}</div> | |||||
| </div> | |||||
| </div> | |||||
| <div className='mb-8'> | |||||
| <div className={titleClassName}>{t('common.account.email')}</div> | |||||
| <div className={classNames(inputClassName, 'cursor-pointer')}>{userProfile.email}</div> | |||||
| </div> | |||||
| {systemFeatures.enable_email_password_login && ( | |||||
| <div className='mb-8'> | |||||
| <div className='mb-1 text-sm font-medium text-gray-900'>{t('common.account.password')}</div> | |||||
| <div className='mb-2 text-xs text-gray-500'>{t('common.account.passwordTip')}</div> | |||||
| <Button onClick={() => setEditPasswordModalVisible(true)}>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</Button> | |||||
| </div> | |||||
| )} | |||||
| <div className='mb-6 border-[0.5px] border-gray-100' /> | |||||
| <div className='mb-8'> | |||||
| <div className={titleClassName}>{t('common.account.langGeniusAccount')}</div> | |||||
| <div className={descriptionClassName}>{t('common.account.langGeniusAccountTip')}</div> | |||||
| {!!apps.length && ( | |||||
| <Collapse | |||||
| title={`${t('common.account.showAppLength', { length: apps.length })}`} | |||||
| items={apps.map(app => ({ key: app.id, name: app.name }))} | |||||
| renderItem={renderAppItem} | |||||
| wrapperClassName='mt-2' | |||||
| /> | |||||
| )} | |||||
| {!IS_CE_EDITION && <Button className='mt-2 text-[#D92D20]' onClick={() => setShowDeleteAccountModal(true)}>{t('common.account.delete')}</Button>} | |||||
| </div> | |||||
| {editNameModalVisible && ( | |||||
| <Modal | |||||
| isShow | |||||
| onClose={() => setEditNameModalVisible(false)} | |||||
| className={s.modal} | |||||
| > | |||||
| <div className='mb-6 text-lg font-medium text-gray-900'>{t('common.account.editName')}</div> | |||||
| <div className={titleClassName}>{t('common.account.name')}</div> | |||||
| <input | |||||
| className={inputClassName} | |||||
| value={editName} | |||||
| onChange={e => setEditName(e.target.value)} | |||||
| /> | |||||
| <div className='flex justify-end mt-10'> | |||||
| <Button className='mr-2' onClick={() => setEditNameModalVisible(false)}>{t('common.operation.cancel')}</Button> | |||||
| <Button | |||||
| disabled={editing || !editName} | |||||
| variant='primary' | |||||
| onClick={handleSaveName} | |||||
| > | |||||
| {t('common.operation.save')} | |||||
| </Button> | |||||
| </div> | |||||
| </Modal> | |||||
| )} | |||||
| {editPasswordModalVisible && ( | |||||
| <Modal | |||||
| isShow | |||||
| onClose={() => { | |||||
| setEditPasswordModalVisible(false) | |||||
| resetPasswordForm() | |||||
| }} | |||||
| className={s.modal} | |||||
| > | |||||
| <div className='mb-6 text-lg font-medium text-gray-900'>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</div> | |||||
| {userProfile.is_password_set && ( | |||||
| <> | |||||
| <div className={titleClassName}>{t('common.account.currentPassword')}</div> | |||||
| <input | |||||
| type="password" | |||||
| className={inputClassName} | |||||
| value={currentPassword} | |||||
| onChange={e => setCurrentPassword(e.target.value)} | |||||
| /> | |||||
| </> | |||||
| )} | |||||
| <div className='mt-8 text-sm font-medium text-gray-900'> | |||||
| {userProfile.is_password_set ? t('common.account.newPassword') : t('common.account.password')} | |||||
| </div> | |||||
| <input | |||||
| type="password" | |||||
| className={inputClassName} | |||||
| value={password} | |||||
| onChange={e => setPassword(e.target.value)} | |||||
| /> | |||||
| <div className='mt-8 text-sm font-medium text-gray-900'>{t('common.account.confirmPassword')}</div> | |||||
| <input | |||||
| type="password" | |||||
| className={inputClassName} | |||||
| value={confirmPassword} | |||||
| onChange={e => setConfirmPassword(e.target.value)} | |||||
| /> | |||||
| <div className='flex justify-end mt-10'> | |||||
| <Button className='mr-2' onClick={() => { | |||||
| setEditPasswordModalVisible(false) | |||||
| resetPasswordForm() | |||||
| }}>{t('common.operation.cancel')}</Button> | |||||
| <Button | |||||
| disabled={editing} | |||||
| variant='primary' | |||||
| onClick={handleSavePassword} | |||||
| > | |||||
| {userProfile.is_password_set ? t('common.operation.reset') : t('common.operation.save')} | |||||
| </Button> | |||||
| </div> | |||||
| </Modal> | |||||
| )} | |||||
| {showDeleteAccountModal && ( | |||||
| <Confirm | |||||
| isShow | |||||
| onCancel={() => setShowDeleteAccountModal(false)} | |||||
| onConfirm={() => setShowDeleteAccountModal(false)} | |||||
| showCancel={false} | |||||
| type='warning' | |||||
| title={t('common.account.delete')} | |||||
| content={ | |||||
| <> | |||||
| <div className='my-1 text-[#D92D20] text-sm leading-5'> | |||||
| {t('common.account.deleteTip')} | |||||
| </div> | |||||
| <div className='mt-3 text-sm leading-5'> | |||||
| <span>{t('common.account.deleteConfirmTip')}</span> | |||||
| <a | |||||
| className='text-primary-600 cursor' | |||||
| href={`mailto:support@dify.ai?subject=Delete Account Request&body=Delete Account: ${userProfile.email}`} | |||||
| target='_blank' | |||||
| rel='noreferrer noopener' | |||||
| onClick={(e) => { | |||||
| e.preventDefault() | |||||
| window.location.href = e.currentTarget.href | |||||
| }} | |||||
| > | |||||
| support@dify.ai | |||||
| </a> | |||||
| </div> | |||||
| <div className='my-2 px-3 py-2 rounded-lg bg-gray-100 text-sm font-medium leading-5 text-gray-800'>{`${t('common.account.delete')}: ${userProfile.email}`}</div> | |||||
| </> | |||||
| } | |||||
| confirmText={t('common.operation.ok') as string} | |||||
| /> | |||||
| )} | |||||
| </> | |||||
| ) | |||||
| } |
| import BrowserInitor from './components/browser-initor' | import BrowserInitor from './components/browser-initor' | ||||
| import SentryInitor from './components/sentry-initor' | import SentryInitor from './components/sentry-initor' | ||||
| import { getLocaleOnServer } from '@/i18n/server' | import { getLocaleOnServer } from '@/i18n/server' | ||||
| import { TanstackQueryIniter } from '@/context/query-client' | |||||
| import './styles/globals.css' | import './styles/globals.css' | ||||
| import './styles/markdown.scss' | import './styles/markdown.scss' | ||||
| > | > | ||||
| <BrowserInitor> | <BrowserInitor> | ||||
| <SentryInitor> | <SentryInitor> | ||||
| <I18nServer>{children}</I18nServer> | |||||
| <TanstackQueryIniter> | |||||
| <I18nServer>{children}</I18nServer> | |||||
| </TanstackQueryIniter> | |||||
| </SentryInitor> | </SentryInitor> | ||||
| </BrowserInitor> | </BrowserInitor> | ||||
| </body> | </body> |
| view: 'View', | view: 'View', | ||||
| viewMore: 'VIEW MORE', | viewMore: 'VIEW MORE', | ||||
| regenerate: 'Regenerate', | regenerate: 'Regenerate', | ||||
| submit: 'Submit', | |||||
| skip: 'Skip', | |||||
| }, | }, | ||||
| errorMsg: { | errorMsg: { | ||||
| fieldRequired: '{{field}} is required', | fieldRequired: '{{field}} is required', | ||||
| editName: 'Edit Name', | editName: 'Edit Name', | ||||
| showAppLength: 'Show {{length}} apps', | showAppLength: 'Show {{length}} apps', | ||||
| delete: 'Delete Account', | delete: 'Delete Account', | ||||
| deleteTip: 'Deleting your account will permanently erase all your data and it cannot be recovered.', | |||||
| deleteConfirmTip: 'To confirm, please send the following from your registered email to ', | |||||
| deleteTip: 'Please note, once confirmed, as the Owner of any Workspaces, your workspaces will be scheduled in a queue for permanent deletion, and all your user data will be queued for permanent deletion.', | |||||
| deletePrivacyLinkTip: 'For more information about how we handle your data, please see our ', | |||||
| deletePrivacyLink: 'Privacy Policy.', | |||||
| deleteSuccessTip: 'Your account needs time to finish deleting. We\'ll email you when it\'s all done.', | |||||
| deleteLabel: 'To confirm, please type in your email below', | |||||
| deletePlaceholder: 'Please enter your email', | |||||
| sendVerificationButton: 'Send Verification Code', | |||||
| verificationLabel: 'Verification Code', | |||||
| verificationPlaceholder: 'Paste the 6-digit code', | |||||
| permanentlyDeleteButton: 'Permanently Delete Account', | |||||
| feedbackTitle: 'Feedback', | |||||
| feedbackLabel: 'Tell us why you deleted your account?', | |||||
| feedbackPlaceholder: 'Optional', | |||||
| }, | }, | ||||
| members: { | members: { | ||||
| team: 'Team', | team: 'Team', |
| view: '查看', | view: '查看', | ||||
| viewMore: '查看更多', | viewMore: '查看更多', | ||||
| regenerate: '重新生成', | regenerate: '重新生成', | ||||
| submit: '提交', | |||||
| skip: '跳过', | |||||
| }, | }, | ||||
| errorMsg: { | errorMsg: { | ||||
| fieldRequired: '{{field}} 为必填项', | fieldRequired: '{{field}} 为必填项', | ||||
| editName: '编辑名字', | editName: '编辑名字', | ||||
| showAppLength: '显示 {{length}} 个应用', | showAppLength: '显示 {{length}} 个应用', | ||||
| delete: '删除账户', | delete: '删除账户', | ||||
| deleteTip: '删除账户后,所有数据将被永久删除且不可恢复。', | |||||
| deleteConfirmTip: '请将以下内容通过您的账户邮箱发送到 ', | |||||
| deleteTip: '请注意,一旦确认,作为任何空间的所有者,您的空间将被安排进入永久删除队列,您的所有用户数据也将被排入永久删除队列。', | |||||
| deletePrivacyLinkTip: '有关我们如何处理您的数据的更多信息,请参阅我们的', | |||||
| deletePrivacyLink: '隐私政策', | |||||
| deleteSuccessTip: '删除账户需要一些时间。完成后,我们会通过邮件通知您。', | |||||
| deleteLabel: '请输入您的邮箱以确认', | |||||
| deletePlaceholder: '输入您的邮箱...', | |||||
| sendVerificationButton: '发送验证码', | |||||
| verificationLabel: '验证码', | |||||
| verificationPlaceholder: '输入 6 位数字验证码', | |||||
| permanentlyDeleteButton: '永久删除', | |||||
| feedbackTitle: '反馈', | |||||
| feedbackLabel: '请告诉我们您为什么删除账户?', | |||||
| feedbackPlaceholder: '选填', | |||||
| }, | }, | ||||
| members: { | members: { | ||||
| team: '团队', | team: '团队', |
| export const verifyResetPasswordCode = (body: { email: string;code: string;token: string }) => | export const verifyResetPasswordCode = (body: { email: string;code: string;token: string }) => | ||||
| post<CommonResponse & { is_valid: boolean }>('/forgot-password/validity', { body }) | post<CommonResponse & { is_valid: boolean }>('/forgot-password/validity', { body }) | ||||
| export const sendDeleteAccountCode = () => | |||||
| get<CommonResponse & { data: string }>('/account/delete/verify') | |||||
| export const verifyDeleteAccountCode = (body: { code: string;token: string }) => | |||||
| post<CommonResponse & { is_valid: boolean }>('/account/delete', { body }) | |||||
| export const submitDeleteAccountFeedback = (body: { feedback: string;email: string }) => | |||||
| post<CommonResponse>('/account/delete/feedback', { body }) |