| 'use client' | 'use client' | ||||
| import { useEffect, useRef } from 'react' | |||||
| import { useEffect, useRef, useState } from 'react' | |||||
| import { useRouter, useSearchParams } from 'next/navigation' | |||||
| import useSWRInfinite from 'swr/infinite' | import useSWRInfinite from 'swr/infinite' | ||||
| import { debounce } from 'lodash-es' | import { debounce } from 'lodash-es' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { fetchAppList } from '@/service/apps' | import { fetchAppList } from '@/service/apps' | ||||
| import { useAppContext, useSelector } from '@/context/app-context' | import { useAppContext, useSelector } from '@/context/app-context' | ||||
| import { NEED_REFRESH_APP_LIST_KEY } from '@/config' | import { NEED_REFRESH_APP_LIST_KEY } from '@/config' | ||||
| import { ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations' | |||||
| import Confirm from '@/app/components/base/confirm/common' | |||||
| const getKey = (pageIndex: number, previousPageData: AppListResponse) => { | const getKey = (pageIndex: number, previousPageData: AppListResponse) => { | ||||
| if (!pageIndex || previousPageData.has_more) | if (!pageIndex || previousPageData.has_more) | ||||
| const loadingStateRef = useRef(false) | const loadingStateRef = useRef(false) | ||||
| const pageContainerRef = useSelector(state => state.pageContainerRef) | const pageContainerRef = useSelector(state => state.pageContainerRef) | ||||
| const anchorRef = useRef<HTMLAnchorElement>(null) | const anchorRef = useRef<HTMLAnchorElement>(null) | ||||
| const searchParams = useSearchParams() | |||||
| const router = useRouter() | |||||
| const payProviderName = searchParams.get('provider_name') | |||||
| const payStatus = searchParams.get('payment_result') | |||||
| const [showPayStatusModal, setShowPayStatusModal] = useState(false) | |||||
| useEffect(() => { | useEffect(() => { | ||||
| document.title = `${t('app.title')} - Dify` | document.title = `${t('app.title')} - Dify` | ||||
| localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY) | localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY) | ||||
| mutate() | mutate() | ||||
| } | } | ||||
| if (payProviderName === ProviderEnum.anthropic && (payStatus === 'succeeded' || payStatus === 'cancelled')) | |||||
| setShowPayStatusModal(true) | |||||
| }, []) | }, []) | ||||
| useEffect(() => { | useEffect(() => { | ||||
| return () => pageContainerRef.current?.removeEventListener('scroll', onScroll) | return () => pageContainerRef.current?.removeEventListener('scroll', onScroll) | ||||
| }, []) | }, []) | ||||
| const handleCancelShowPayStatusModal = () => { | |||||
| setShowPayStatusModal(false) | |||||
| router.replace('/', { forceOptimisticNavigation: false }) | |||||
| } | |||||
| return ( | return ( | ||||
| <nav className='grid content-start grid-cols-1 gap-4 px-12 pt-8 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'> | <nav className='grid content-start grid-cols-1 gap-4 px-12 pt-8 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'> | ||||
| {data?.map(({ data: apps }) => apps.map(app => ( | {data?.map(({ data: apps }) => apps.map(app => ( | ||||
| )))} | )))} | ||||
| { isCurrentWorkspaceManager | { isCurrentWorkspaceManager | ||||
| && <NewAppCard ref={anchorRef} onSuccess={mutate} />} | && <NewAppCard ref={anchorRef} onSuccess={mutate} />} | ||||
| { | |||||
| showPayStatusModal && ( | |||||
| <Confirm | |||||
| isShow | |||||
| onCancel={handleCancelShowPayStatusModal} | |||||
| onConfirm={handleCancelShowPayStatusModal} | |||||
| type={ | |||||
| payStatus === 'succeeded' | |||||
| ? 'success' | |||||
| : 'danger' | |||||
| } | |||||
| title={ | |||||
| payStatus === 'succeeded' | |||||
| ? t('common.actionMsg.paySucceeded') | |||||
| : t('common.actionMsg.payCancelled') | |||||
| } | |||||
| showOperateCancel={false} | |||||
| confirmText={(payStatus === 'cancelled' && t('common.operation.ok')) || ''} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| </nav> | </nav> | ||||
| ) | ) | ||||
| } | } |
| .wrapper { | |||||
| .wrapper-danger { | |||||
| background: linear-gradient(180deg, rgba(217, 45, 32, 0.05) 0%, rgba(217, 45, 32, 0.00) 24.02%), #F9FAFB; | background: linear-gradient(180deg, rgba(217, 45, 32, 0.05) 0%, rgba(217, 45, 32, 0.00) 24.02%), #F9FAFB; | ||||
| } | |||||
| .wrapper-success { | |||||
| background: linear-gradient(180deg, rgba(3, 152, 85, 0.05) 0%, rgba(3, 152, 85, 0.00) 22.44%), #F9FAFB; | |||||
| } | } |
| import Modal from '@/app/components/base/modal' | import Modal from '@/app/components/base/modal' | ||||
| import { XClose } from '@/app/components/base/icons/src/vender/line/general' | import { XClose } from '@/app/components/base/icons/src/vender/line/general' | ||||
| import { AlertCircle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' | import { AlertCircle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' | ||||
| import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general' | |||||
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| type ConfirmCommonProps = { | type ConfirmCommonProps = { | ||||
| onCancel: () => void | onCancel: () => void | ||||
| title: string | title: string | ||||
| desc?: string | desc?: string | ||||
| onConfirm: () => void | |||||
| onConfirm?: () => void | |||||
| showOperate?: boolean | |||||
| showOperateCancel?: boolean | |||||
| confirmText?: string | |||||
| } | } | ||||
| const ConfirmCommon: FC<ConfirmCommonProps> = ({ | const ConfirmCommon: FC<ConfirmCommonProps> = ({ | ||||
| title, | title, | ||||
| desc, | desc, | ||||
| onConfirm, | onConfirm, | ||||
| showOperate = true, | |||||
| showOperateCancel = true, | |||||
| confirmText, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| icon: <AlertCircle className='w-6 h-6 text-[#D92D20]' />, | icon: <AlertCircle className='w-6 h-6 text-[#D92D20]' />, | ||||
| confirmText: t('common.operation.remove'), | confirmText: t('common.operation.remove'), | ||||
| }, | }, | ||||
| success: { | |||||
| icon: <CheckCircle className='w-6 h-6 text-[#039855]' />, | |||||
| confirmText: t('common.operation.ok'), | |||||
| }, | |||||
| } | } | ||||
| return ( | return ( | ||||
| <Modal isShow={isShow} onClose={() => {}} className='!w-[480px] !max-w-[480px] !p-0 !rounded-2xl'> | <Modal isShow={isShow} onClose={() => {}} className='!w-[480px] !max-w-[480px] !p-0 !rounded-2xl'> | ||||
| <div className={cn(s.wrapper, 'relative p-8')}> | |||||
| <div className={cn(s[`wrapper-${type}`], 'relative p-8')}> | |||||
| <div className='flex items-center justify-center absolute top-4 right-4 w-8 h-8 cursor-pointer' onClick={onCancel}> | <div className='flex items-center justify-center absolute top-4 right-4 w-8 h-8 cursor-pointer' onClick={onCancel}> | ||||
| <XClose className='w-4 h-4 text-gray-500' /> | <XClose className='w-4 h-4 text-gray-500' /> | ||||
| </div> | </div> | ||||
| { | { | ||||
| desc && <div className='mt-1 text-sm text-gray-500'>{desc}</div> | desc && <div className='mt-1 text-sm text-gray-500'>{desc}</div> | ||||
| } | } | ||||
| <div className='flex items-center justify-end mt-10'> | |||||
| <Button | |||||
| className='mr-2 min-w-24 text-sm font-medium !text-gray-700' | |||||
| onClick={onCancel} | |||||
| > | |||||
| {t('common.operation.cancel')} | |||||
| </Button> | |||||
| <Button | |||||
| type='primary' | |||||
| className='' | |||||
| onClick={onConfirm} | |||||
| > | |||||
| {CONFIRM_MAP[type].confirmText} | |||||
| </Button> | |||||
| </div> | |||||
| { | |||||
| showOperate && ( | |||||
| <div className='flex items-center justify-end mt-10'> | |||||
| { | |||||
| showOperateCancel && ( | |||||
| <Button | |||||
| className='mr-2 min-w-24 text-sm font-medium !text-gray-700' | |||||
| onClick={onCancel} | |||||
| > | |||||
| {t('common.operation.cancel')} | |||||
| </Button> | |||||
| ) | |||||
| } | |||||
| <Button | |||||
| type='primary' | |||||
| className='' | |||||
| onClick={onConfirm} | |||||
| > | |||||
| {confirmText || CONFIRM_MAP[type].confirmText} | |||||
| </Button> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| </div> | </div> | ||||
| </Modal> | </Modal> | ||||
| ) | ) |
| import { InfoCircle } from '@/app/components/base/icons/src/vender/line/general' | import { InfoCircle } from '@/app/components/base/icons/src/vender/line/general' | ||||
| import { getPayUrl } from '@/service/common' | import { getPayUrl } from '@/service/common' | ||||
| import Button from '@/app/components/base/button' | import Button from '@/app/components/base/button' | ||||
| import { formatNumber } from '@/utils/format' | |||||
| type QuotaProps = { | type QuotaProps = { | ||||
| currentProvider: Provider | currentProvider: Provider | ||||
| const renderQuota = () => { | const renderQuota = () => { | ||||
| if (systemPaid?.is_valid) | if (systemPaid?.is_valid) | ||||
| return systemPaid.quota_limit - systemPaid.quota_used | |||||
| return formatNumber(systemPaid.quota_limit - systemPaid.quota_used) | |||||
| if (systemTrial.is_valid) | if (systemTrial.is_valid) | ||||
| return systemTrial.quota_limit - systemTrial.quota_used | |||||
| return formatNumber(systemTrial.quota_limit - systemTrial.quota_used) | |||||
| } | } | ||||
| const renderUnit = () => { | const renderUnit = () => { | ||||
| if (systemPaid?.is_valid) | if (systemPaid?.is_valid) |
| setup: 'Setup', | setup: 'Setup', | ||||
| getForFree: 'Get for free', | getForFree: 'Get for free', | ||||
| reload: 'Reload', | reload: 'Reload', | ||||
| ok: 'OK', | |||||
| }, | }, | ||||
| placeholder: { | placeholder: { | ||||
| input: 'Please enter', | input: 'Please enter', | ||||
| modifiedSuccessfully: 'Modified successfully', | modifiedSuccessfully: 'Modified successfully', | ||||
| modificationFailed: 'Modification failed', | modificationFailed: 'Modification failed', | ||||
| copySuccessfully: 'Copied successfully', | copySuccessfully: 'Copied successfully', | ||||
| paySucceeded: 'Payment succeeded', | |||||
| payCancelled: 'Payment cancelled', | |||||
| }, | }, | ||||
| model: { | model: { | ||||
| params: { | params: { |
| setup: '设置', | setup: '设置', | ||||
| getForFree: '免费获取', | getForFree: '免费获取', | ||||
| reload: '刷新', | reload: '刷新', | ||||
| ok: '好的', | |||||
| }, | }, | ||||
| placeholder: { | placeholder: { | ||||
| input: '请输入', | input: '请输入', | ||||
| modifiedSuccessfully: '修改成功', | modifiedSuccessfully: '修改成功', | ||||
| modificationFailed: '修改失败', | modificationFailed: '修改失败', | ||||
| copySuccessfully: '复制成功', | copySuccessfully: '复制成功', | ||||
| paySucceeded: '已支付成功', | |||||
| payCancelled: '已取消支付', | |||||
| }, | }, | ||||
| model: { | model: { | ||||
| params: { | params: { |