Compare commits

..

5 Commits

Author SHA1 Message Date
JzoNg 30393043b1 add error handle for email unavailable 10 months ago
JzoNg ae49a2bdd9 add error message of email unavailable 10 months ago
Yansong Zhang c271c65c85 fix ruff liner 10 months ago
Yansong Zhang a1de4fa428 check is email is freeze user email 10 months ago
Yansong Zhang 952bce4196 check is email is freeze user email 10 months ago

@ -1,7 +1,7 @@
![cover-v5-optimized](./images/GitHub_README_if.png) ![cover-v5-optimized](./images/GitHub_README_if.png)
<p align="center"> <p align="center">
📌 <a href="https://dify.ai/blog/introducing-dify-workflow-file-upload-a-demo-on-ai-podcast">Introducing Dify Workflow File Upload: Recreate Google NotebookLM Podcast111</a> 📌 <a href="https://dify.ai/blog/introducing-dify-workflow-file-upload-a-demo-on-ai-podcast">Introducing Dify Workflow File Upload: Recreate Google NotebookLM Podcast</a>
</p> </p>
<p align="center"> <p align="center">

@ -511,6 +511,8 @@ class CheckEmailUnique(Resource):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json") parser.add_argument("email", type=email, required=True, location="json")
args = parser.parse_args() args = parser.parse_args()
if AccountService.is_account_in_freeze(args["new_email"]):
raise AccountInFreezeError()
if not AccountService.check_email_unique(args["email"]): if not AccountService.check_email_unique(args["email"]):
raise EmailAlreadyInUseError() raise EmailAlreadyInUseError()
return {"result": "success"} return {"result": "success"}

@ -1011,9 +1011,7 @@ class ToolManager:
if variable is None: if variable is None:
raise ToolParameterError(f"Variable {tool_input.value} does not exist") raise ToolParameterError(f"Variable {tool_input.value} does not exist")
parameter_value = variable.value parameter_value = variable.value
elif tool_input.type == "constant": elif tool_input.type in {"mixed", "constant"}:
parameter_value = tool_input.value
elif tool_input.type == "mixed":
segment_group = variable_pool.convert_template(str(tool_input.value)) segment_group = variable_pool.convert_template(str(tool_input.value))
parameter_value = segment_group.text parameter_value = segment_group.text
else: else:

@ -54,7 +54,7 @@ class ToolNodeData(BaseNodeData, ToolEntity):
for val in value: for val in value:
if not isinstance(val, str): if not isinstance(val, str):
raise ValueError("value must be a list of strings") raise ValueError("value must be a list of strings")
elif typ == "constant" and not isinstance(value, str | int | float | bool | dict): elif typ == "constant" and not isinstance(value, str | int | float | bool):
raise ValueError("value must be a string, int, float, or bool") raise ValueError("value must be a string, int, float, or bool")
return typ return typ

@ -32,35 +32,6 @@ export type IAppDetailLayoutProps = {
appId: string appId: string
} }
const useIframeHeader = () => {
const [show, setShow] = useState(true);
useEffect(() => {
// 监听父级指定操作
const handler = (event: MessageEvent) => {
// if (event.origin !== "https://gcgj.ngsk.tech:7001") return;
if (event.data.type === "HIDDEN") setShow(() => false);
};
// 初始化完成后提示iframe操作
window.parent.postMessage(
{
type: "SIDEBA",
},
"*"
);
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, []);
return {
show,
};
};
const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => { const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const { const {
children, children,
@ -85,7 +56,6 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
icon: NavIcon icon: NavIcon
selectedIcon: NavIcon selectedIcon: NavIcon
}>>([]) }>>([])
const { show } = useIframeHeader()
const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => { const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
const navs = [ const navs = [
@ -190,9 +160,9 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
return ( return (
<div className={cn(s.app, 'relative flex', 'overflow-hidden')}> <div className={cn(s.app, 'relative flex', 'overflow-hidden')}>
{show ? appDetail && ( {appDetail && (
<AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background as string} desc={appDetail.mode} navigation={navigation} /> <AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background as string} desc={appDetail.mode} navigation={navigation} />
): ''} )}
<div className="grow overflow-hidden bg-components-panel-bg"> <div className="grow overflow-hidden bg-components-panel-bg">
{children} {children}
</div> </div>

@ -1,5 +1,5 @@
'use client' 'use client'
import React, { useEffect, useState } from 'react' import React, { useState } from 'react'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear' import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -9,7 +9,6 @@ import type { Item } from '@/app/components/base/select'
import { SimpleSelect } from '@/app/components/base/select' import { SimpleSelect } from '@/app/components/base/select'
import { TIME_PERIOD_MAPPING } from '@/app/components/app/log/filter' import { TIME_PERIOD_MAPPING } from '@/app/components/app/log/filter'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import { useRouter } from 'next/navigation'
dayjs.extend(quarterOfYear) dayjs.extend(quarterOfYear)
@ -22,44 +21,12 @@ export type IChartViewProps = {
headerRight: React.ReactNode headerRight: React.ReactNode
} }
let state = 0
const useIframeRedirection = () => {
const router = useRouter()
useEffect(() => {
// 监听父级指定操作
const handler = (event: MessageEvent) => {
// if (event.origin !== "https://gcgj.ngsk.tech:7001") return;
if (
event.data.type === 'CHART_VIEW_REDIRECT'
&& event.data.redirectUrl
&& state < 3 // 防止无限重定向
) {
state++
router.replace(event.data.redirectUrl)
}
}
// 首页初始化完成后提示iframe操作
window.parent.postMessage(
{
type: 'CHART_VIEW',
},
'*',
)
window.addEventListener('message', handler)
return () => window.removeEventListener('message', handler)
}, [])
}
export default function ChartView({ appId, headerRight }: IChartViewProps) { export default function ChartView({ appId, headerRight }: IChartViewProps) {
const { t } = useTranslation() const { t } = useTranslation()
const appDetail = useAppStore(state => state.appDetail) const appDetail = useAppStore(state => state.appDetail)
const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow' const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow'
const isWorkflow = appDetail?.mode === 'workflow' const isWorkflow = appDetail?.mode === 'workflow'
const [period, setPeriod] = useState<PeriodParams>({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } }) const [period, setPeriod] = useState<PeriodParams>({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').startOf('day').format(queryDateFormat), end: today.endOf('day').format(queryDateFormat) } })
useIframeRedirection()
const onSelect = (item: Item) => { const onSelect = (item: Item) => {
if (item.value === -1) { if (item.value === -1) {

@ -1,5 +1,5 @@
'use client' 'use client'
import type { FC, JSX } from 'react' import type { FC } from 'react'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'

@ -15,6 +15,8 @@ import {
verifyEmail, verifyEmail,
} from '@/service/common' } from '@/service/common'
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import { asyncRunSafe } from '@/utils'
import type { ResponseError } from '@/service/fetch'
type Props = { type Props = {
show: boolean show: boolean
@ -39,6 +41,7 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
const [time, setTime] = useState<number>(0) const [time, setTime] = useState<number>(0)
const [stepToken, setStepToken] = useState<string>('') const [stepToken, setStepToken] = useState<string>('')
const [newEmailExited, setNewEmailExited] = useState<boolean>(false) const [newEmailExited, setNewEmailExited] = useState<boolean>(false)
const [unAvailableEmail, setUnAvailableEmail] = useState<boolean>(false)
const [isCheckingEmail, setIsCheckingEmail] = useState<boolean>(false) const [isCheckingEmail, setIsCheckingEmail] = useState<boolean>(false)
const startCount = () => { const startCount = () => {
@ -124,9 +127,17 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
email, email,
}) })
setNewEmailExited(false) setNewEmailExited(false)
setUnAvailableEmail(false)
} }
catch { catch (e: any) {
if (e.status === 400) {
const [, errRespData] = await asyncRunSafe<ResponseError>(e.json())
const { code } = errRespData || {}
if (code === 'email_already_in_use')
setNewEmailExited(true) setNewEmailExited(true)
if (code === 'account_in_freeze')
setUnAvailableEmail(true)
}
} }
finally { finally {
setIsCheckingEmail(false) setIsCheckingEmail(false)
@ -291,15 +302,18 @@ const EmailChangeModal = ({ onClose, email, show }: Props) => {
placeholder={t('common.account.changeEmail.emailPlaceholder')} placeholder={t('common.account.changeEmail.emailPlaceholder')}
value={mail} value={mail}
onChange={e => handleNewEmailValueChange(e.target.value)} onChange={e => handleNewEmailValueChange(e.target.value)}
destructive={newEmailExited} destructive={newEmailExited || unAvailableEmail}
/> />
{newEmailExited && ( {newEmailExited && (
<div className='body-xs-regular mt-1 py-0.5 text-text-destructive'>{t('common.account.changeEmail.existingEmail')}</div> <div className='body-xs-regular mt-1 py-0.5 text-text-destructive'>{t('common.account.changeEmail.existingEmail')}</div>
)} )}
{unAvailableEmail && (
<div className='body-xs-regular mt-1 py-0.5 text-text-destructive'>{t('common.account.changeEmail.unAvailableEmail')}</div>
)}
</div> </div>
<div className='mt-3 space-y-2'> <div className='mt-3 space-y-2'>
<Button <Button
disabled={!mail || newEmailExited || isCheckingEmail || !isValidEmail(mail)} disabled={!mail || newEmailExited || unAvailableEmail || isCheckingEmail || !isValidEmail(mail)}
className='!w-full' className='!w-full'
variant='primary' variant='primary'
onClick={sendCodeToNewEmail} onClick={sendCodeToNewEmail}

@ -1,4 +1,4 @@
import type { JSX } from 'react' import type { ReactElement } from 'react'
import { cloneElement, useCallback } from 'react' import { cloneElement, useCallback } from 'react'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -7,7 +7,7 @@ import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigge
import { RiMoreLine } from '@remixicon/react' import { RiMoreLine } from '@remixicon/react'
export type Operation = { export type Operation = {
id: string; title: string; icon: JSX.Element; onClick: () => void id: string; title: string; icon: ReactElement; onClick: () => void
} }
const AppOperations = ({ operations, gap }: { const AppOperations = ({ operations, gap }: {

@ -91,7 +91,7 @@ const PureSelect = ({
triggerPopupSameWidth={triggerPopupSameWidth} triggerPopupSameWidth={triggerPopupSameWidth}
> >
<PortalToFollowElemTrigger <PortalToFollowElemTrigger
onClick={() => !disabled && handleOpenChange(!mergedOpen)} onClick={() => handleOpenChange(!mergedOpen)}
asChild asChild
> >
<div <div
@ -116,7 +116,7 @@ const PureSelect = ({
</div> </div>
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent className={cn( <PortalToFollowElemContent className={cn(
'z-[9999]', 'z-10',
popupWrapperClassName, popupWrapperClassName,
)}> )}>
<div <div

@ -70,7 +70,7 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
const [isShowModifyRetrievalModal, setIsShowModifyRetrievalModal] = useState(false) const [isShowModifyRetrievalModal, setIsShowModifyRetrievalModal] = useState(false)
const [isShowRightPanel, { setTrue: showRightPanel, setFalse: hideRightPanel, set: setShowRightPanel }] = useBoolean(!isMobile) const [isShowRightPanel, { setTrue: showRightPanel, setFalse: hideRightPanel, set: setShowRightPanel }] = useBoolean(!isMobile)
const renderHitResults = (results: HitTesting[] | ExternalKnowledgeBaseHitTesting[]) => ( const renderHitResults = (results: HitTesting[] | ExternalKnowledgeBaseHitTesting[]) => (
<div className='flex h-full flex-col rounded-tl-2xl bg-background-body px-4 py-3'> <div className='flex h-full flex-col rounded-t-2xl bg-background-body px-4 py-3'>
<div className='mb-2 shrink-0 pl-2 font-semibold leading-6 text-text-primary'> <div className='mb-2 shrink-0 pl-2 font-semibold leading-6 text-text-primary'>
{t('datasetHitTesting.hit.title', { num: results.length })} {t('datasetHitTesting.hit.title', { num: results.length })}
</div> </div>
@ -93,7 +93,7 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
) )
const renderEmptyState = () => ( const renderEmptyState = () => (
<div className='flex h-full flex-col items-center justify-center rounded-tl-2xl bg-background-body px-4 py-3'> <div className='flex h-full flex-col items-center justify-center rounded-t-2xl bg-background-body px-4 py-3'>
<div className={cn(docStyle.commonIcon, docStyle.targetIcon, '!h-14 !w-14 !bg-text-quaternary')} /> <div className={cn(docStyle.commonIcon, docStyle.targetIcon, '!h-14 !w-14 !bg-text-quaternary')} />
<div className='mt-3 text-[13px] text-text-quaternary'> <div className='mt-3 text-[13px] text-text-quaternary'>
{t('datasetHitTesting.hit.emptyTip')} {t('datasetHitTesting.hit.emptyTip')}
@ -180,7 +180,7 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
<div className='flex flex-col pt-3'> <div className='flex flex-col pt-3'>
{/* {renderHitResults(generalResultData)} */} {/* {renderHitResults(generalResultData)} */}
{submitLoading {submitLoading
? <div className='flex h-full flex-col rounded-tl-2xl bg-background-body px-4 py-3'> ? <div className='flex h-full flex-col rounded-t-2xl bg-background-body px-4 py-3'>
<CardSkelton /> <CardSkelton />
</div> </div>
: ( : (

@ -1,5 +1,5 @@
'use client' 'use client'
import React, { useState, useEffect } from "react"; import React, { useState } from 'react'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import s from './index.module.css' import s from './index.module.css'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
@ -9,33 +9,6 @@ type HeaderWrapperProps = {
children: React.ReactNode children: React.ReactNode
} }
const useIframeHeader = () => {
const [show, setShow] = useState(true);
useEffect(() => {
// 监听父级指定操作
const handler = (event: MessageEvent) => {
// if (event.origin !== "https://gcgj.ngsk.tech:7001") return;
if (event.data.type === "HIDDEN") setShow(() => false);
};
// 初始化完成后提示iframe操作
window.parent.postMessage(
{
type: "HEADER",
},
"*"
);
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, []);
return {
show,
};
};
const HeaderWrapper = ({ const HeaderWrapper = ({
children, children,
}: HeaderWrapperProps) => { }: HeaderWrapperProps) => {
@ -46,7 +19,6 @@ const HeaderWrapper = ({
const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true' const workflowCanvasMaximize = localStorage.getItem('workflow-canvas-maximize') === 'true'
const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize) const [hideHeader, setHideHeader] = useState(workflowCanvasMaximize)
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
const { show } = useIframeHeader();
eventEmitter?.useSubscription((v: any) => { eventEmitter?.useSubscription((v: any) => {
if (v?.type === 'workflow-canvas-maximize') if (v?.type === 'workflow-canvas-maximize')
@ -54,22 +26,15 @@ const HeaderWrapper = ({
}) })
return ( return (
<> <div className={classNames(
{show ? ( 'sticky left-0 right-0 top-0 z-[15] flex min-h-[56px] shrink-0 grow-0 basis-auto flex-col',
<div
className={classNames(
"sticky left-0 right-0 top-0 z-[15] flex min-h-[56px] shrink-0 grow-0 basis-auto flex-col",
s.header, s.header,
isBordered ? "border-b border-divider-regular" : "", isBordered ? 'border-b border-divider-regular' : '',
hideHeader && inWorkflowCanvas && "hidden" hideHeader && inWorkflowCanvas && 'hidden',
)} )}
> >
{children} {children}
</div> </div>
) : ( )
""
)}
</>
);
} }
export default HeaderWrapper export default HeaderWrapper

@ -164,7 +164,7 @@ const FormInputItem: FC<Props> = ({
...value, ...value,
[variable]: { [variable]: {
...varInput, ...varInput,
value: newValue, ...newValue,
}, },
}) })
} }
@ -242,7 +242,7 @@ const FormInputItem: FC<Props> = ({
<AppSelector <AppSelector
disabled={readOnly} disabled={readOnly}
scope={scope || 'all'} scope={scope || 'all'}
value={varInput?.value} value={varInput as any}
onSelect={handleAppOrModelSelect} onSelect={handleAppOrModelSelect}
/> />
)} )}
@ -251,7 +251,7 @@ const FormInputItem: FC<Props> = ({
popupClassName='!w-[387px]' popupClassName='!w-[387px]'
isAdvancedMode isAdvancedMode
isInWorkflow isInWorkflow
value={varInput?.value} value={varInput}
setModel={handleAppOrModelSelect} setModel={handleAppOrModelSelect}
readonly={readOnly} readonly={readOnly}
scope={scope} scope={scope}

@ -15,37 +15,6 @@ import Toast from '@/app/components/base/toast'
import { IS_CE_EDITION } from '@/config' import { IS_CE_EDITION } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context' import { useGlobalPublicStore } from '@/context/global-public-context'
const useIframeToken = () => {
const router = useRouter();
useEffect(() => {
// 监听父级指定操作
const handler = (event: MessageEvent) => {
// if (event.origin !== "https://gcgj.ngsk.tech:7001") return;
if (
event.data.type === "LOGIN_AND_WORKFLOW_INTO" &&
event.data.tokens &&
event.data.workflowUrl
) {
localStorage.setItem("console_token", event.data.tokens.console_token);
localStorage.setItem("refresh_token", event.data.tokens.refresh_token);
router.replace(event.data.workflowUrl);
}
};
// 首页初始化完成后提示iframe操作
window.parent.postMessage(
{
type: "LOGIN_AND_WORKFLOW",
},
"*"
);
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
}, []);
};
const NormalForm = () => { const NormalForm = () => {
const { t } = useTranslation() const { t } = useTranslation()
const router = useRouter() const router = useRouter()
@ -60,7 +29,6 @@ const NormalForm = () => {
const [showORLine, setShowORLine] = useState(false) const [showORLine, setShowORLine] = useState(false)
const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false) const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)
const [workspaceName, setWorkSpaceName] = useState('') const [workspaceName, setWorkSpaceName] = useState('')
useIframeToken();
const isInviteLink = Boolean(invite_token && invite_token !== 'null') const isInviteLink = Boolean(invite_token && invite_token !== 'null')

@ -248,6 +248,7 @@ const translation = {
emailLabel: 'New email', emailLabel: 'New email',
emailPlaceholder: 'Enter a new email', emailPlaceholder: 'Enter a new email',
existingEmail: 'A user with this email already exists.', existingEmail: 'A user with this email already exists.',
unAvailableEmail: 'This email is temporarily unavailable.',
sendVerifyCode: 'Send verification code', sendVerifyCode: 'Send verification code',
continue: 'Continue', continue: 'Continue',
changeTo: 'Change to {{email}}', changeTo: 'Change to {{email}}',

@ -249,6 +249,7 @@ const translation = {
emailLabel: '新しいメール', emailLabel: '新しいメール',
emailPlaceholder: '新しいメールを入力してください', emailPlaceholder: '新しいメールを入力してください',
existingEmail: 'このメールアドレスのユーザーは既に存在します', existingEmail: 'このメールアドレスのユーザーは既に存在します',
unAvailableEmail: 'このメールアドレスは現在使用できません。',
sendVerifyCode: '確認コードを送信', sendVerifyCode: '確認コードを送信',
continue: '続行', continue: '続行',
changeTo: '{{email}} に変更', changeTo: '{{email}} に変更',

@ -248,6 +248,7 @@ const translation = {
emailLabel: '新邮箱', emailLabel: '新邮箱',
emailPlaceholder: '输入新邮箱', emailPlaceholder: '输入新邮箱',
existingEmail: '该邮箱已存在', existingEmail: '该邮箱已存在',
unAvailableEmail: '该邮箱暂时无法使用。',
sendVerifyCode: '发送验证码', sendVerifyCode: '发送验证码',
continue: '继续', continue: '继续',
changeTo: '更改为 {{email}}', changeTo: '更改为 {{email}}',

@ -43,25 +43,14 @@ const nextConfig = {
search: '', search: '',
})), })),
}, },
experimental: {}, experimental: {
},
// fix all before production. Now it slow the develop speed. // fix all before production. Now it slow the develop speed.
eslint: { eslint: {
// Warning: This allows production builds to successfully complete even if // Warning: This allows production builds to successfully complete even if
// your project has ESLint errors. // your project has ESLint errors.
ignoreDuringBuilds: true, ignoreDuringBuilds: true,
dirs: [ dirs: ['app', 'bin', 'config', 'context', 'hooks', 'i18n', 'models', 'service', 'test', 'types', 'utils'],
'app',
'bin',
'config',
'context',
'hooks',
'i18n',
'models',
'service',
'test',
'types',
'utils',
],
}, },
typescript: { typescript: {
// https://nextjs.org/docs/api-reference/next.config.js/ignoring-typescript-errors // https://nextjs.org/docs/api-reference/next.config.js/ignoring-typescript-errors
@ -78,23 +67,6 @@ const nextConfig = {
] ]
}, },
output: 'standalone', output: 'standalone',
async headers() {
return [
{
source: '/(.*)', // 匹配所有路由
headers: [
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN', // 或 ALLOWALL更宽松
},
{
key: 'Content-Security-Policy',
value: 'frame-ancestors "self" *', // 允许所有
},
],
},
]
},
} }
module.exports = withBundleAnalyzer(withMDX(nextConfig)) module.exports = withBundleAnalyzer(withMDX(nextConfig))

Loading…
Cancel
Save