Co-authored-by: Joel <iamjoel007@gmail.com>tags/0.3.23
| @@ -1,7 +1,6 @@ | |||
| 'use client' | |||
| import { useCallback, useEffect, useRef, useState } from 'react' | |||
| import { useRouter } from 'next/navigation' | |||
| import { useEffect, useRef } from 'react' | |||
| import useSWRInfinite from 'swr/infinite' | |||
| import { useTranslation } from 'react-i18next' | |||
| import AppCard from './AppCard' | |||
| @@ -10,11 +9,7 @@ import type { AppListResponse } from '@/models/app' | |||
| import { fetchAppList } from '@/service/apps' | |||
| import { useAppContext } from '@/context/app-context' | |||
| import { NEED_REFRESH_APP_LIST_KEY } from '@/config' | |||
| import Confirm from '@/app/components/base/confirm/common' | |||
| import { | |||
| useAnthropicCheckPay, | |||
| useSparkCheckQuota, | |||
| } from '@/hooks/use-pay' | |||
| import { CheckModal } from '@/hooks/use-pay' | |||
| const getKey = (pageIndex: number, previousPageData: AppListResponse) => { | |||
| if (!pageIndex || previousPageData.has_more) | |||
| @@ -27,15 +22,6 @@ const Apps = () => { | |||
| const { isCurrentWorkspaceManager } = useAppContext() | |||
| const { data, isLoading, setSize, mutate } = useSWRInfinite(getKey, fetchAppList, { revalidateFirstPage: false }) | |||
| const anchorRef = useRef<HTMLDivElement>(null) | |||
| const router = useRouter() | |||
| const [showPayStatusModal, setShowPayStatusModal] = useState(true) | |||
| const anthropicConfirmInfo = useAnthropicCheckPay() | |||
| const sparkConfirmInfo = useSparkCheckQuota() | |||
| const handleCancelShowPayStatusModal = useCallback(() => { | |||
| setShowPayStatusModal(false) | |||
| router.replace('/', { forceOptimisticNavigation: false }) | |||
| }, [router]) | |||
| useEffect(() => { | |||
| document.title = `${t('app.title')} - Dify` | |||
| @@ -64,33 +50,7 @@ const Apps = () => { | |||
| {data?.map(({ data: apps }) => apps.map(app => ( | |||
| <AppCard key={app.id} app={app} onRefresh={mutate} /> | |||
| )))} | |||
| { | |||
| showPayStatusModal && anthropicConfirmInfo && ( | |||
| <Confirm | |||
| isShow | |||
| onCancel={handleCancelShowPayStatusModal} | |||
| onConfirm={handleCancelShowPayStatusModal} | |||
| type={anthropicConfirmInfo.type} | |||
| title={anthropicConfirmInfo.title} | |||
| showOperateCancel={false} | |||
| confirmText={(anthropicConfirmInfo.type === 'danger' && t('common.operation.ok')) || ''} | |||
| /> | |||
| ) | |||
| } | |||
| { | |||
| showPayStatusModal && sparkConfirmInfo && ( | |||
| <Confirm | |||
| isShow | |||
| onCancel={handleCancelShowPayStatusModal} | |||
| onConfirm={handleCancelShowPayStatusModal} | |||
| type={sparkConfirmInfo.type} | |||
| title={sparkConfirmInfo.title} | |||
| desc={sparkConfirmInfo.desc} | |||
| showOperateCancel={false} | |||
| confirmText={(sparkConfirmInfo.type === 'danger' && t('common.operation.ok')) || ''} | |||
| /> | |||
| ) | |||
| } | |||
| <CheckModal /> | |||
| </nav> | |||
| <div ref={anchorRef} className='h-0'> </div> | |||
| </> | |||
| @@ -213,7 +213,7 @@ const ConfigModel: FC<IConfigModelProps> = ({ | |||
| const handleParamChange = (key: string, value: number) => { | |||
| const currParamsRule = getAllParams()[provider]?.[modelId] | |||
| let notOutRangeValue = parseFloat(value.toFixed(2)) | |||
| let notOutRangeValue = parseFloat((value || 0).toFixed(2)) | |||
| notOutRangeValue = Math.max(currParamsRule[key].min, notOutRangeValue) | |||
| notOutRangeValue = Math.min(currParamsRule[key].max, notOutRangeValue) | |||
| @@ -1,9 +1,20 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import React, { useEffect } from 'react' | |||
| import Tooltip from '@/app/components/base/tooltip' | |||
| import Slider from '@/app/components/base/slider' | |||
| export const getFitPrecisionValue = (num: number, precision: number | null) => { | |||
| if (!precision || !(`${num}`).includes('.')) | |||
| return num | |||
| const currNumPrecision = (`${num}`).split('.')[1].length | |||
| if (currNumPrecision > precision) | |||
| return parseFloat(num.toFixed(precision)) | |||
| return num | |||
| } | |||
| export type IParamIteProps = { | |||
| id: string | |||
| name: string | |||
| @@ -12,10 +23,26 @@ export type IParamIteProps = { | |||
| step?: number | |||
| min?: number | |||
| max: number | |||
| precision: number | null | |||
| onChange: (key: string, value: number) => void | |||
| } | |||
| const ParamIte: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max, value, onChange }) => { | |||
| const TIMES_TEMPLATE = '1000000000000' | |||
| const ParamItem: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max, precision, value, onChange }) => { | |||
| const getToIntTimes = (num: number) => { | |||
| if (precision) | |||
| return parseInt(TIMES_TEMPLATE.slice(0, precision + 1), 10) | |||
| if (num < 5) | |||
| return 10 | |||
| return 1 | |||
| } | |||
| const times = getToIntTimes(max) | |||
| useEffect(() => { | |||
| if (precision) | |||
| onChange(id, getFitPrecisionValue(value, precision)) | |||
| }, [value, precision]) | |||
| return ( | |||
| <div className="flex items-center justify-between"> | |||
| <div className="flex items-center"> | |||
| @@ -29,17 +56,21 @@ const ParamIte: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max, | |||
| </div> | |||
| <div className="flex items-center"> | |||
| <div className="mr-4 w-[120px]"> | |||
| <Slider value={max < 5 ? value * 10 : value} min={min < 0 ? min * 10 : min} max={max < 5 ? max * 10 : max} onChange={value => onChange(id, value / (max < 5 ? 10 : 1))} /> | |||
| <Slider value={value * times} min={min * times} max={max * times} onChange={(value) => { | |||
| onChange(id, value / times) | |||
| }} /> | |||
| </div> | |||
| <input type="number" min={min} max={max} step={step} className="block w-[64px] h-9 leading-9 rounded-lg border-0 pl-1 pl py-1.5 bg-gray-50 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-primary-600" value={value} onChange={(e) => { | |||
| const value = parseFloat(e.target.value) | |||
| if (value < min || value > max) | |||
| return | |||
| let value = getFitPrecisionValue(isNaN(parseFloat(e.target.value)) ? min : parseFloat(e.target.value), precision) | |||
| if (value < min) | |||
| value = min | |||
| if (value > max) | |||
| value = max | |||
| onChange(id, value) | |||
| }} /> | |||
| </div> | |||
| </div> | |||
| ) | |||
| } | |||
| export default React.memo(ParamIte) | |||
| export default React.memo(ParamItem) | |||
| @@ -0,0 +1,16 @@ | |||
| // GENERATE BY script | |||
| // DON NOT EDIT IT MANUALLY | |||
| import * as React from 'react' | |||
| import data from './Zhipuai.json' | |||
| import IconBase from '@/app/components/base/icons/IconBase' | |||
| import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' | |||
| const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>(( | |||
| props, | |||
| ref, | |||
| ) => <IconBase {...props} ref={ref} data={data as IconData} />) | |||
| Icon.displayName = 'Zhipuai' | |||
| export default Icon | |||
| @@ -0,0 +1,16 @@ | |||
| // GENERATE BY script | |||
| // DON NOT EDIT IT MANUALLY | |||
| import * as React from 'react' | |||
| import data from './ZhipuaiText.json' | |||
| import IconBase from '@/app/components/base/icons/IconBase' | |||
| import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' | |||
| const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>(( | |||
| props, | |||
| ref, | |||
| ) => <IconBase {...props} ref={ref} data={data as IconData} />) | |||
| Icon.displayName = 'ZhipuaiText' | |||
| export default Icon | |||
| @@ -0,0 +1,16 @@ | |||
| // GENERATE BY script | |||
| // DON NOT EDIT IT MANUALLY | |||
| import * as React from 'react' | |||
| import data from './ZhipuaiTextCn.json' | |||
| import IconBase from '@/app/components/base/icons/IconBase' | |||
| import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' | |||
| const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>(( | |||
| props, | |||
| ref, | |||
| ) => <IconBase {...props} ref={ref} data={data as IconData} />) | |||
| Icon.displayName = 'ZhipuaiTextCn' | |||
| export default Icon | |||
| @@ -29,3 +29,6 @@ export { default as ReplicateText } from './ReplicateText' | |||
| export { default as Replicate } from './Replicate' | |||
| export { default as XorbitsInferenceText } from './XorbitsInferenceText' | |||
| export { default as XorbitsInference } from './XorbitsInference' | |||
| export { default as ZhipuaiTextCn } from './ZhipuaiTextCn' | |||
| export { default as ZhipuaiText } from './ZhipuaiText' | |||
| export { default as Zhipuai } from './Zhipuai' | |||
| @@ -11,6 +11,7 @@ import chatglm from './chatglm' | |||
| import xinference from './xinference' | |||
| import openllm from './openllm' | |||
| import localai from './localai' | |||
| import zhipuai from './zhipuai' | |||
| export default { | |||
| openai, | |||
| @@ -26,4 +27,5 @@ export default { | |||
| xinference, | |||
| openllm, | |||
| localai, | |||
| zhipuai, | |||
| } | |||
| @@ -0,0 +1,55 @@ | |||
| import { ProviderEnum } from '../declarations' | |||
| import type { ProviderConfig } from '../declarations' | |||
| import { Zhipuai, ZhipuaiText, ZhipuaiTextCn } from '@/app/components/base/icons/src/public/llm' | |||
| const config: ProviderConfig = { | |||
| selector: { | |||
| name: { | |||
| 'en': 'ZHIPU AI', | |||
| 'zh-Hans': '智谱 AI', | |||
| }, | |||
| icon: <Zhipuai className='w-full h-full' />, | |||
| }, | |||
| item: { | |||
| key: ProviderEnum.zhipuai, | |||
| titleIcon: { | |||
| 'en': <ZhipuaiText className='-ml-1 h-7' />, | |||
| 'zh-Hans': <ZhipuaiTextCn className='h-8' />, | |||
| }, | |||
| }, | |||
| modal: { | |||
| key: ProviderEnum.zhipuai, | |||
| title: { | |||
| 'en': 'ZHIPU AI', | |||
| 'zh-Hans': '智谱 AI', | |||
| }, | |||
| icon: <Zhipuai className='w-6 h-6' />, | |||
| link: { | |||
| href: 'https://open.bigmodel.cn/usercenter/apikeys', | |||
| label: { | |||
| 'en': 'Get your API key from ZHIPU AI', | |||
| 'zh-Hans': '从智谱 AI 获取 API Key', | |||
| }, | |||
| }, | |||
| validateKeys: [ | |||
| 'api_key', | |||
| ], | |||
| fields: [ | |||
| { | |||
| type: 'text', | |||
| key: 'api_key', | |||
| required: true, | |||
| label: { | |||
| 'en': 'APIKey', | |||
| 'zh-Hans': 'APIKey', | |||
| }, | |||
| placeholder: { | |||
| 'en': 'Enter your APIKey here', | |||
| 'zh-Hans': '在此输入您的 APIKey', | |||
| }, | |||
| }, | |||
| ], | |||
| }, | |||
| } | |||
| export default config | |||
| @@ -42,6 +42,7 @@ export enum ProviderEnum { | |||
| 'xinference' = 'xinference', | |||
| 'openllm' = 'openllm', | |||
| 'localai' = 'localai', | |||
| 'zhipuai' = 'zhipuai', | |||
| } | |||
| export type ProviderConfigItem = { | |||
| @@ -78,8 +78,9 @@ const ModelPage = () => { | |||
| config.azure_openai, | |||
| config.replicate, | |||
| config.huggingface_hub, | |||
| config.minimax, | |||
| config.zhipuai, | |||
| config.spark, | |||
| config.minimax, | |||
| config.tongyi, | |||
| config.wenxin, | |||
| config.chatglm, | |||
| @@ -91,8 +92,9 @@ const ModelPage = () => { | |||
| else { | |||
| modelList = [ | |||
| config.huggingface_hub, | |||
| config.minimax, | |||
| config.zhipuai, | |||
| config.spark, | |||
| config.minimax, | |||
| config.azure_openai, | |||
| config.replicate, | |||
| config.tongyi, | |||
| @@ -19,6 +19,10 @@ const TIP_MAP: { [k: string]: TypeWithI18N } = { | |||
| 'en': 'Earn 3 million tokens for free', | |||
| 'zh-Hans': '免费获取 300 万个 token', | |||
| }, | |||
| [ProviderEnumValue.zhipuai]: { | |||
| 'en': 'Earn 10 million tokens for free', | |||
| 'zh-Hans': '免费获取 1000 万个 token', | |||
| }, | |||
| } | |||
| type FreeQuotaProps = { | |||
| modelItem: ProviderConfigItem | |||
| @@ -34,7 +34,7 @@ const Setting: FC<SettingProps> = ({ | |||
| return ( | |||
| <div className='flex items-center'> | |||
| { | |||
| (modelItem.key === ProviderEnum.minimax || modelItem.key === ProviderEnum.spark) && systemFree && !systemFree?.is_valid && !IS_CE_EDITION && locale === 'zh-Hans' && ( | |||
| (modelItem.key === ProviderEnum.minimax || modelItem.key === ProviderEnum.spark || modelItem.key === ProviderEnum.zhipuai) && systemFree && !systemFree?.is_valid && !IS_CE_EDITION && locale === 'zh-Hans' && ( | |||
| <FreeQuota | |||
| modelItem={modelItem} | |||
| onUpdate={onUpdate} | |||
| @@ -1,71 +0,0 @@ | |||
| 'use client' | |||
| import { useEffect, useState } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import useSWR from 'swr' | |||
| import { useSearchParams } from 'next/navigation' | |||
| import { useContext } from 'use-context-selector' | |||
| import I18n from '@/context/i18n' | |||
| import { ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations' | |||
| import { fetchSparkFreeQuotaVerify } from '@/service/common' | |||
| import type { ConfirmCommonProps } from '@/app/components/base/confirm/common' | |||
| export type ConfirmType = Pick<ConfirmCommonProps, 'type' | 'title' | 'desc'> | |||
| export const useAnthropicCheckPay = () => { | |||
| const { t } = useTranslation() | |||
| const [confirm, setConfirm] = useState<ConfirmType | null>(null) | |||
| const searchParams = useSearchParams() | |||
| const providerName = searchParams.get('provider_name') | |||
| const paymentResult = searchParams.get('payment_result') | |||
| useEffect(() => { | |||
| if (providerName === ProviderEnum.anthropic && (paymentResult === 'succeeded' || paymentResult === 'cancelled')) { | |||
| setConfirm({ | |||
| type: paymentResult === 'succeeded' ? 'success' : 'danger', | |||
| title: paymentResult === 'succeeded' ? t('common.actionMsg.paySucceeded') : t('common.actionMsg.payCancelled'), | |||
| }) | |||
| } | |||
| }, [providerName, paymentResult, t]) | |||
| return confirm | |||
| } | |||
| const QUOTA_RECEIVE_STATUS = { | |||
| success: { | |||
| 'en': 'Anthropic', | |||
| 'zh-Hans': '领取成功,将在 5 分钟后自动增加配额', | |||
| }, | |||
| fail: { | |||
| 'en': 'Anthropic', | |||
| 'zh-Hans': '领取失败', | |||
| }, | |||
| } | |||
| export const useSparkCheckQuota = () => { | |||
| const { locale } = useContext(I18n) | |||
| const [shouldVerify, setShouldVerify] = useState(false) | |||
| const { data } = useSWR( | |||
| shouldVerify | |||
| ? `/workspaces/current/model-providers/${ProviderEnum.spark}/free-quota-qualification-verify` | |||
| : null, | |||
| fetchSparkFreeQuotaVerify, | |||
| ) | |||
| const searchParams = useSearchParams() | |||
| const type = searchParams.get('type') | |||
| const provider = searchParams.get('provider') | |||
| const result = searchParams.get('result') | |||
| useEffect(() => { | |||
| if (type === 'provider_apply_callback' && provider === ProviderEnum.spark && result === 'success') | |||
| setShouldVerify(true) | |||
| }, [type, provider, result]) | |||
| return data | |||
| ? { | |||
| type: data.flag ? 'success' : 'danger', | |||
| title: data.flag ? QUOTA_RECEIVE_STATUS.success[locale] : QUOTA_RECEIVE_STATUS.fail[locale], | |||
| desc: !data.flag ? data.reason : undefined, | |||
| } | |||
| : null | |||
| } | |||
| @@ -0,0 +1,124 @@ | |||
| 'use client' | |||
| import { useCallback, useEffect, useState } from 'react' | |||
| import { useRouter, useSearchParams } from 'next/navigation' | |||
| import { useTranslation } from 'react-i18next' | |||
| import useSWR from 'swr' | |||
| import { useContext } from 'use-context-selector' | |||
| import I18n from '@/context/i18n' | |||
| import { ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations' | |||
| import { fetchFreeQuotaVerify } from '@/service/common' | |||
| import type { ConfirmCommonProps } from '@/app/components/base/confirm/common' | |||
| import Confirm from '@/app/components/base/confirm/common' | |||
| export type ConfirmType = Pick<ConfirmCommonProps, 'type' | 'title' | 'desc'> | |||
| export const useAnthropicCheckPay = () => { | |||
| const { t } = useTranslation() | |||
| const [confirm, setConfirm] = useState<ConfirmType | null>(null) | |||
| const searchParams = useSearchParams() | |||
| const providerName = searchParams.get('provider_name') | |||
| const paymentResult = searchParams.get('payment_result') | |||
| useEffect(() => { | |||
| if (providerName === ProviderEnum.anthropic && (paymentResult === 'succeeded' || paymentResult === 'cancelled')) { | |||
| setConfirm({ | |||
| type: paymentResult === 'succeeded' ? 'success' : 'danger', | |||
| title: paymentResult === 'succeeded' ? t('common.actionMsg.paySucceeded') : t('common.actionMsg.payCancelled'), | |||
| }) | |||
| } | |||
| }, [providerName, paymentResult, t]) | |||
| return confirm | |||
| } | |||
| const QUOTA_RECEIVE_STATUS = { | |||
| [ProviderEnum.spark]: { | |||
| success: { | |||
| 'en': 'Successful collection, the quota will be automatically increased after 5 minutes.', | |||
| 'zh-Hans': '领取成功,将在 5 分钟后自动增加配额', | |||
| }, | |||
| fail: { | |||
| 'en': 'Failure to collect', | |||
| 'zh-Hans': '领取失败', | |||
| }, | |||
| }, | |||
| [ProviderEnum.zhipuai]: { | |||
| success: { | |||
| 'en': 'Successful collection', | |||
| 'zh-Hans': '领取成功', | |||
| }, | |||
| fail: { | |||
| 'en': 'Failure to collect', | |||
| 'zh-Hans': '领取失败', | |||
| }, | |||
| }, | |||
| } | |||
| const FREE_CHECK_PROVIDER = [ProviderEnum.spark, ProviderEnum.zhipuai] | |||
| export const useCheckFreeQuota = () => { | |||
| const { locale } = useContext(I18n) | |||
| const router = useRouter() | |||
| const [shouldVerify, setShouldVerify] = useState(false) | |||
| const searchParams = useSearchParams() | |||
| const type = searchParams.get('type') | |||
| const provider = searchParams.get('provider') as (ProviderEnum.spark | ProviderEnum.zhipuai) | |||
| const result = searchParams.get('result') | |||
| const token = searchParams.get('token') | |||
| const { data, error } = useSWR( | |||
| shouldVerify | |||
| ? `/workspaces/current/model-providers/${provider}/free-quota-qualification-verify?token=${token}` | |||
| : null, | |||
| fetchFreeQuotaVerify, | |||
| ) | |||
| useEffect(() => { | |||
| if (error) | |||
| router.replace('/', { forceOptimisticNavigation: false }) | |||
| }, [error, router]) | |||
| useEffect(() => { | |||
| if (type === 'provider_apply_callback' && FREE_CHECK_PROVIDER.includes(provider) && result === 'success') | |||
| setShouldVerify(true) | |||
| }, [type, provider, result]) | |||
| return (data && provider) | |||
| ? { | |||
| type: data.flag ? 'success' : 'danger', | |||
| title: data.flag ? QUOTA_RECEIVE_STATUS[provider].success[locale] : QUOTA_RECEIVE_STATUS[provider].fail[locale], | |||
| desc: !data.flag ? data.reason : undefined, | |||
| } | |||
| : null | |||
| } | |||
| export const CheckModal = () => { | |||
| const router = useRouter() | |||
| const { t } = useTranslation() | |||
| const [showPayStatusModal, setShowPayStatusModal] = useState(true) | |||
| const anthropicConfirmInfo = useAnthropicCheckPay() | |||
| const freeQuotaConfirmInfo = useCheckFreeQuota() | |||
| const handleCancelShowPayStatusModal = useCallback(() => { | |||
| setShowPayStatusModal(false) | |||
| router.replace('/', { forceOptimisticNavigation: false }) | |||
| }, [router]) | |||
| const confirmInfo = anthropicConfirmInfo || freeQuotaConfirmInfo | |||
| if (!confirmInfo || !showPayStatusModal) | |||
| return null | |||
| return ( | |||
| <Confirm | |||
| isShow | |||
| onCancel={handleCancelShowPayStatusModal} | |||
| onConfirm={handleCancelShowPayStatusModal} | |||
| type={confirmInfo.type} | |||
| title={confirmInfo.title} | |||
| desc={confirmInfo.desc} | |||
| showOperateCancel={false} | |||
| confirmText={(confirmInfo.type === 'danger' && t('common.operation.ok')) || ''} | |||
| /> | |||
| ) | |||
| } | |||
| @@ -185,6 +185,6 @@ export const fetchDocumentsLimit: Fetcher<DocumentsLimitResponse, string> = (url | |||
| return get<DocumentsLimitResponse>(url) | |||
| } | |||
| export const fetchSparkFreeQuotaVerify: Fetcher<{ result: string; flag: boolean; reason: string }, string> = (url) => { | |||
| export const fetchFreeQuotaVerify: Fetcher<{ result: string; flag: boolean; reason: string }, string> = (url) => { | |||
| return get(url) as Promise<{ result: string; flag: boolean; reason: string }> | |||
| } | |||