refactor: replace CustomDialog with Dialog component across multiple files

- Updated DeleteAccount, FeedBack, BatchModal, VersionInfoModal, GetAutomaticRes, GetCodeGeneratorRes, EditModal, ConfigModal, ParamsConfig, DSLConfirmModal, and CreateFromDSLModal to use the new Dialog component from @langgenius/dify-ui/dialog instead of the previous CustomDialog and Modal components.
- Enhanced dialog structure for better accessibility and styling consistency.
This commit is contained in:
CodingOnStar 2026-05-08 18:38:12 +08:00
parent 1b0d4637b3
commit 05481059ed
83 changed files with 3037 additions and 3422 deletions

View File

@ -1,9 +1,9 @@
'use client' 'use client'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import CustomDialog from '@/app/components/base/dialog'
import Textarea from '@/app/components/base/textarea' import Textarea from '@/app/components/base/textarea'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { useRouter } from '@/next/navigation' import { useRouter } from '@/next/navigation'
@ -47,26 +47,34 @@ export default function FeedBack(props: DeleteAccountProps) {
handleSuccess() handleSuccess()
}, [handleSuccess, props]) }, [handleSuccess, props])
return ( return (
<CustomDialog <Dialog
show={true} open
onClose={props.onCancel} onOpenChange={(open) => {
title={t('account.feedbackTitle', { ns: 'common' })} if (!open)
className="max-w-[480px]" props.onCancel()
footer={false} }}
> >
<label className="mt-3 mb-1 flex items-center system-sm-semibold text-text-secondary">{t('account.feedbackLabel', { ns: 'common' })}</label> <DialogContent
<Textarea className="max-w-[480px] overflow-hidden!"
rows={6} backdropClassName="bg-background-overlay-backdrop backdrop-blur-[6px]"
value={userFeedback} >
placeholder={t('account.feedbackPlaceholder', { ns: 'common' }) as string} <DialogTitle className="pr-8 pb-3 title-2xl-semi-bold text-text-primary">
onChange={(e) => { {t('account.feedbackTitle', { ns: 'common' })}
setUserFeedback(e.target.value) </DialogTitle>
}} <label className="mt-3 mb-1 flex items-center system-sm-semibold text-text-secondary">{t('account.feedbackLabel', { ns: 'common' })}</label>
/> <Textarea
<div className="mt-3 flex w-full flex-col gap-2"> rows={6}
<Button className="w-full" loading={isPending} variant="primary" onClick={handleSubmit}>{t('operation.submit', { ns: 'common' })}</Button> value={userFeedback}
<Button className="w-full" onClick={handleSkip}>{t('operation.skip', { ns: 'common' })}</Button> placeholder={t('account.feedbackPlaceholder', { ns: 'common' }) as string}
</div> onChange={(e) => {
</CustomDialog> setUserFeedback(e.target.value)
}}
/>
<div className="mt-3 flex w-full flex-col gap-2">
<Button className="w-full" loading={isPending} variant="primary" onClick={handleSubmit}>{t('operation.submit', { ns: 'common' })}</Button>
<Button className="w-full" onClick={handleSkip}>{t('operation.skip', { ns: 'common' })}</Button>
</div>
</DialogContent>
</Dialog>
) )
} }

View File

@ -1,7 +1,7 @@
'use client' 'use client'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import CustomDialog from '@/app/components/base/dialog'
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown' import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown'
import CheckEmail from './components/check-email' import CheckEmail from './components/check-email'
import FeedBack from './components/feed-back' import FeedBack from './components/feed-back'
@ -30,22 +30,30 @@ export default function DeleteAccount(props: DeleteAccountProps) {
return <FeedBack onCancel={props.onCancel} onConfirm={props.onConfirm} /> return <FeedBack onCancel={props.onCancel} onConfirm={props.onConfirm} />
return ( return (
<CustomDialog <Dialog
show={true} open
onClose={props.onCancel} onOpenChange={(open) => {
title={t('account.delete', { ns: 'common' })} if (!open)
className="max-w-[480px]" props.onCancel()
footer={false} }}
> >
{!showVerifyEmail && <CheckEmail onCancel={props.onCancel} onConfirm={handleEmailCheckSuccess} />} <DialogContent
{showVerifyEmail && ( className="max-w-[480px] overflow-hidden!"
<VerifyEmail backdropClassName="bg-background-overlay-backdrop backdrop-blur-[6px]"
onCancel={props.onCancel} >
onConfirm={() => { <DialogTitle className="pr-8 pb-3 title-2xl-semi-bold text-text-primary">
setShowFeedbackDialog(true) {t('account.delete', { ns: 'common' })}
}} </DialogTitle>
/> {!showVerifyEmail && <CheckEmail onCancel={props.onCancel} onConfirm={handleEmailCheckSuccess} />}
)} {showVerifyEmail && (
</CustomDialog> <VerifyEmail
onCancel={props.onCancel}
onConfirm={() => {
setShowFeedbackDialog(true)
}}
/>
)}
</DialogContent>
</Dialog>
) )
} }

View File

@ -1,13 +1,12 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import * as React from 'react' import * as React from 'react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import AnnotationFull from '@/app/components/billing/annotation-full' import AnnotationFull from '@/app/components/billing/annotation-full'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { annotationBatchImport, checkAnnotationBatchImportProgress } from '@/service/annotation' import { annotationBatchImport, checkAnnotationBatchImportProgress } from '@/service/annotation'
@ -88,37 +87,40 @@ const BatchModal: FC<IBatchModalProps> = ({
} }
return ( return (
<Modal isShow={isShow} onClose={noop} className="max-w-[520px]! rounded-xl! px-8 py-6"> <Dialog open={isShow}>
<div className="relative pb-1 system-xl-medium text-text-primary">{t('batchModal.title', { ns: 'appAnnotation' })}</div> <DialogContent className="w-full max-w-[520px]! overflow-hidden! rounded-xl! border-none px-8 py-6 text-left align-middle">
<div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onCancel}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div>
<CSVUploader
file={currentCSV}
updateFile={handleFile}
/>
<CSVDownloader />
{isAnnotationFull && ( <div className="relative pb-1 system-xl-medium text-text-primary">{t('batchModal.title', { ns: 'appAnnotation' })}</div>
<div className="mt-4"> <div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onCancel}>
<AnnotationFull /> <RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div> </div>
)} <CSVUploader
file={currentCSV}
updateFile={handleFile}
/>
<CSVDownloader />
<div className="mt-[28px] flex justify-end pt-6"> {isAnnotationFull && (
<Button className="mr-2 system-sm-medium text-text-tertiary" onClick={onCancel}> <div className="mt-4">
{t('batchModal.cancel', { ns: 'appAnnotation' })} <AnnotationFull />
</Button> </div>
<Button )}
variant="primary"
onClick={handleSend} <div className="mt-[28px] flex justify-end pt-6">
disabled={isAnnotationFull || !currentCSV} <Button className="mr-2 system-sm-medium text-text-tertiary" onClick={onCancel}>
loading={importStatus === ProcessStatus.PROCESSING || importStatus === ProcessStatus.WAITING} {t('batchModal.cancel', { ns: 'appAnnotation' })}
> </Button>
{t('batchModal.run', { ns: 'appAnnotation' })} <Button
</Button> variant="primary"
</div> onClick={handleSend}
</Modal> disabled={isAnnotationFull || !currentCSV}
loading={importStatus === ProcessStatus.PROCESSING || importStatus === ProcessStatus.WAITING}
>
{t('batchModal.run', { ns: 'appAnnotation' })}
</Button>
</div>
</DialogContent>
</Dialog>
) )
} }
export default React.memo(BatchModal) export default React.memo(BatchModal)

View File

@ -1,12 +1,12 @@
import type { FC } from 'react' import type { FC } from 'react'
import type { VersionHistory } from '@/types/workflow' import type { VersionHistory } from '@/types/workflow'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import * as React from 'react' import * as React from 'react'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import Input from '../../base/input' import Input from '../../base/input'
import Textarea from '../../base/textarea' import Textarea from '../../base/textarea'
@ -66,46 +66,55 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
}, []) }, [])
return ( return (
<Modal className="p-0" isShow={isOpen} onClose={onClose}> <Dialog
<div className="relative w-full p-6 pr-14 pb-4"> open={isOpen}
<div className="title-2xl-semi-bold text-text-primary first-letter:capitalize"> onOpenChange={(open) => {
{versionInfo?.marked_name ? t('versionHistory.editVersionInfo', { ns: 'workflow' }) : t('versionHistory.nameThisVersion', { ns: 'workflow' })} if (!open)
</div> onClose()
<div className="absolute top-5 right-5 flex h-8 w-8 cursor-pointer items-center justify-center p-1.5" onClick={onClose}> }}
<RiCloseLine className="h-[18px] w-[18px] text-text-tertiary" /> >
</div> <DialogContent className="w-full max-w-[480px] overflow-hidden! border-none p-0 text-left align-middle">
</div>
<div className="flex flex-col gap-y-4 px-6 py-3"> <div className="relative w-full p-6 pr-14 pb-4">
<div className="flex flex-col gap-y-1"> <div className="title-2xl-semi-bold text-text-primary first-letter:capitalize">
<div className="flex h-6 items-center system-sm-semibold text-text-secondary"> {versionInfo?.marked_name ? t('versionHistory.editVersionInfo', { ns: 'workflow' }) : t('versionHistory.nameThisVersion', { ns: 'workflow' })}
{t('versionHistory.editField.title', { ns: 'workflow' })}
</div> </div>
<Input <div className="absolute top-5 right-5 flex h-8 w-8 cursor-pointer items-center justify-center p-1.5" onClick={onClose}>
value={title} <RiCloseLine className="h-[18px] w-[18px] text-text-tertiary" />
placeholder={`${t('versionHistory.nameThisVersion', { ns: 'workflow' })}${t('panel.optional', { ns: 'workflow' })}`}
onChange={handleTitleChange}
destructive={titleError}
/>
</div>
<div className="flex flex-col gap-y-1">
<div className="flex h-6 items-center system-sm-semibold text-text-secondary">
{t('versionHistory.editField.releaseNotes', { ns: 'workflow' })}
</div> </div>
<Textarea
value={releaseNotes}
placeholder={`${t('versionHistory.releaseNotesPlaceholder', { ns: 'workflow' })}${t('panel.optional', { ns: 'workflow' })}`}
onChange={handleDescriptionChange}
destructive={releaseNotesError}
/>
</div> </div>
</div> <div className="flex flex-col gap-y-4 px-6 py-3">
<div className="flex justify-end p-6 pt-5"> <div className="flex flex-col gap-y-1">
<div className="flex items-center gap-x-3"> <div className="flex h-6 items-center system-sm-semibold text-text-secondary">
<Button onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button> {t('versionHistory.editField.title', { ns: 'workflow' })}
<Button variant="primary" onClick={handlePublish}>{t('common.publish', { ns: 'workflow' })}</Button> </div>
<Input
value={title}
placeholder={`${t('versionHistory.nameThisVersion', { ns: 'workflow' })}${t('panel.optional', { ns: 'workflow' })}`}
onChange={handleTitleChange}
destructive={titleError}
/>
</div>
<div className="flex flex-col gap-y-1">
<div className="flex h-6 items-center system-sm-semibold text-text-secondary">
{t('versionHistory.editField.releaseNotes', { ns: 'workflow' })}
</div>
<Textarea
value={releaseNotes}
placeholder={`${t('versionHistory.releaseNotesPlaceholder', { ns: 'workflow' })}${t('panel.optional', { ns: 'workflow' })}`}
onChange={handleDescriptionChange}
destructive={releaseNotesError}
/>
</div>
</div> </div>
</div> <div className="flex justify-end p-6 pt-5">
</Modal> <div className="flex items-center gap-x-3">
<Button nativeButton={false} onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button nativeButton={false} variant="primary" onClick={handlePublish}>{t('common.publish', { ns: 'workflow' })}</Button>
</div>
</div>
</DialogContent>
</Dialog>
) )
} }

View File

@ -3,8 +3,11 @@ import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react' import * as React from 'react'
import EditModal from '../edit-modal' import EditModal from '../edit-modal'
vi.mock('@/app/components/base/modal', () => ({ vi.mock('@langgenius/dify-ui/dialog', () => ({
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>, Dialog: ({ children, open }: { children: React.ReactNode, open?: boolean }) =>
open === false ? null : <>{children}</>,
DialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
})) }))
describe('Conversation history edit modal', () => { describe('Conversation history edit modal', () => {

View File

@ -2,10 +2,10 @@
import type { FC } from 'react' import type { FC } from 'react'
import type { ConversationHistoriesRole } from '@/models/debug' import type { ConversationHistoriesRole } from '@/models/debug'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import * as React from 'react' import * as React from 'react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
type Props = { type Props = {
isShow: boolean isShow: boolean
@ -25,37 +25,45 @@ const EditModal: FC<Props> = ({
const { t } = useTranslation() const { t } = useTranslation()
const [tempData, setTempData] = useState(data) const [tempData, setTempData] = useState(data)
return ( return (
<Modal <Dialog
title={t('feature.conversationHistory.editModal.title', { ns: 'appDebug' })} open={isShow}
isShow={isShow} onOpenChange={(open) => {
onClose={onClose} if (!open)
onClose()
}}
> >
<div className="mt-6 text-sm leading-[21px] font-medium text-text-primary">{t('feature.conversationHistory.editModal.userPrefix', { ns: 'appDebug' })}</div> <DialogContent className="w-full max-w-[480px] overflow-hidden! border-none p-6 text-left align-middle">
<input <DialogTitle className="title-2xl-semi-bold text-text-primary">
className="mt-2 box-border h-10 w-full rounded-lg bg-components-input-bg-normal px-3 text-sm leading-10" {t('feature.conversationHistory.editModal.title', { ns: 'appDebug' })}
value={tempData.user_prefix} </DialogTitle>
onChange={e => setTempData({
...tempData,
user_prefix: e.target.value,
})}
/>
<div className="mt-6 text-sm leading-[21px] font-medium text-text-primary">{t('feature.conversationHistory.editModal.assistantPrefix', { ns: 'appDebug' })}</div> <div className="mt-6 text-sm leading-[21px] font-medium text-text-primary">{t('feature.conversationHistory.editModal.userPrefix', { ns: 'appDebug' })}</div>
<input <input
className="mt-2 box-border h-10 w-full rounded-lg bg-components-input-bg-normal px-3 text-sm leading-10" className="mt-2 box-border h-10 w-full rounded-lg bg-components-input-bg-normal px-3 text-sm leading-10"
value={tempData.assistant_prefix} value={tempData.user_prefix}
onChange={e => setTempData({ onChange={e => setTempData({
...tempData, ...tempData,
assistant_prefix: e.target.value, user_prefix: e.target.value,
})} })}
placeholder={t('chat.conversationNamePlaceholder', { ns: 'common' }) || ''} />
/>
<div className="mt-10 flex justify-end"> <div className="mt-6 text-sm leading-[21px] font-medium text-text-primary">{t('feature.conversationHistory.editModal.assistantPrefix', { ns: 'appDebug' })}</div>
<Button className="mr-2 shrink-0" onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button> <input
<Button variant="primary" className="shrink-0" onClick={() => onSave(tempData)} loading={saveLoading}>{t('operation.save', { ns: 'common' })}</Button> className="mt-2 box-border h-10 w-full rounded-lg bg-components-input-bg-normal px-3 text-sm leading-10"
</div> value={tempData.assistant_prefix}
</Modal> onChange={e => setTempData({
...tempData,
assistant_prefix: e.target.value,
})}
placeholder={t('chat.conversationNamePlaceholder', { ns: 'common' }) || ''}
/>
<div className="mt-10 flex justify-end">
<Button className="mr-2 shrink-0" onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button variant="primary" className="shrink-0" onClick={() => onSave(tempData)} loading={saveLoading}>{t('operation.save', { ns: 'common' })}</Button>
</div>
</DialogContent>
</Dialog>
) )
} }

View File

@ -2,13 +2,13 @@
import type { ChangeEvent, FC } from 'react' import type { ChangeEvent, FC } from 'react'
import type { Item as SelectItem } from './type-select' import type { Item as SelectItem } from './type-select'
import type { InputVar, InputVarType, MoreInfo } from '@/app/components/workflow/types' import type { InputVar, InputVarType, MoreInfo } from '@/app/components/workflow/types'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react' import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import Modal from '@/app/components/base/modal'
import ConfigContext from '@/context/debug-configuration' import ConfigContext from '@/context/debug-configuration'
import { AppModeEnum } from '@/types/app' import { AppModeEnum } from '@/types/app'
import { checkKeys, getNewVarInWorkflow, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' import { checkKeys, getNewVarInWorkflow, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var'
@ -141,35 +141,43 @@ const ConfigModal: FC<IConfigModalProps> = ({
} }
return ( return (
<Modal <Dialog
title={t(`variableConfig.${isCreate ? 'addModalTitle' : 'editModalTitle'}`, { ns: 'appDebug' })} open={isShow}
isShow={isShow} onOpenChange={(open) => {
onClose={onClose} if (!open)
onClose()
}}
> >
<div className="mb-8" ref={modalRef} tabIndex={-1}> <DialogContent className="overflow-hidden! border-none text-left align-middle">
<ConfigModalFormFields <DialogTitle className="title-2xl-semi-bold text-text-primary">
checkboxDefaultSelectValue={checkboxDefaultSelectValue} {t(`variableConfig.${isCreate ? 'addModalTitle' : 'editModalTitle'}`, { ns: 'appDebug' })}
isStringInput={isStringInput} </DialogTitle>
jsonSchemaStr={jsonSchemaStr}
maxLength={max_length} <div className="mb-8" ref={modalRef} tabIndex={-1}>
modelId={modelConfig.model_id} <ConfigModalFormFields
onFilePayloadChange={payload => setTempPayload(payload as InputVar)} checkboxDefaultSelectValue={checkboxDefaultSelectValue}
onJSONSchemaChange={handleJSONSchemaChange} isStringInput={isStringInput}
onPayloadChange={handlePayloadChange} jsonSchemaStr={jsonSchemaStr}
onTypeChange={handleTypeChange} maxLength={max_length}
onVarKeyBlur={handleVarKeyBlur} modelId={modelConfig.model_id}
onVarNameChange={handleVarNameChange} onFilePayloadChange={payload => setTempPayload(payload as InputVar)}
options={options} onJSONSchemaChange={handleJSONSchemaChange}
selectOptions={selectOptions} onPayloadChange={handlePayloadChange}
tempPayload={tempPayload} onTypeChange={handleTypeChange}
t={t} onVarKeyBlur={handleVarKeyBlur}
onVarNameChange={handleVarNameChange}
options={options}
selectOptions={selectOptions}
tempPayload={tempPayload}
t={t}
/>
</div>
<ModalFoot
onConfirm={handleConfirm}
onCancel={onClose}
/> />
</div> </DialogContent>
<ModalFoot </Dialog>
onConfirm={handleConfirm}
onCancel={onClose}
/>
</Modal>
) )
} }
export default React.memo(ConfigModal) export default React.memo(ConfigModal)

View File

@ -14,6 +14,7 @@ import {
AlertDialogTitle, AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog' } from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { import {
RiDatabase2Line, RiDatabase2Line,
@ -32,9 +33,8 @@ import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Generator } from '@/app/components/base/icons/src/vender/other' import { Generator } from '@/app/components/base/icons/src/vender/other'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import { generateBasicAppFirstTimeRule, generateRule } from '@/service/debug' import { generateBasicAppFirstTimeRule, generateRule } from '@/service/debug'
@ -282,143 +282,148 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
} }
return ( return (
<Modal <Dialog
isShow={isShow} open={isShow}
onClose={onClose} onOpenChange={(open) => {
className="min-w-[1140px] p-0!" if (!open)
onClose()
}}
> >
<div className="flex h-[680px] flex-wrap"> <DialogContent className="max-h-none w-[1140px] max-w-none! min-w-[1140px] overflow-hidden! border-none p-0! text-left align-middle">
<div className="h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6">
<div className="mb-5"> <div className="flex h-[680px] flex-wrap">
<div className={`text-lg leading-[28px] font-bold ${s.textGradient}`}>{t('generate.title', { ns: 'appDebug' })}</div> <div className="h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6">
<div className="mt-1 text-[13px] font-normal text-text-tertiary">{t('generate.description', { ns: 'appDebug' })}</div> <div className="mb-5">
</div> <div className={`text-lg leading-[28px] font-bold ${s.textGradient}`}>{t('generate.title', { ns: 'appDebug' })}</div>
<div> <div className="mt-1 text-[13px] font-normal text-text-tertiary">{t('generate.description', { ns: 'appDebug' })}</div>
<ModelParameterModal </div>
popupClassName="w-[520px]!" <div>
isAdvancedMode={true} <ModelParameterModal
provider={model.provider} popupClassName="w-[520px]!"
completionParams={model.completion_params} isAdvancedMode={true}
modelId={model.name} provider={model.provider}
setModel={handleModelChange} completionParams={model.completion_params}
onCompletionParamsChange={handleCompletionParamsChange} modelId={model.name}
hideDebugWithMultipleModel setModel={handleModelChange}
/> onCompletionParamsChange={handleCompletionParamsChange}
</div> hideDebugWithMultipleModel
{isBasicMode && ( />
<div className="mt-4"> </div>
<div className="flex items-center"> {isBasicMode && (
<div className="mr-3 shrink-0 text-xs leading-[18px] font-semibold text-text-tertiary uppercase">{t('generate.tryIt', { ns: 'appDebug' })}</div> <div className="mt-4">
<div <div className="flex items-center">
className="h-px grow" <div className="mr-3 shrink-0 text-xs leading-[18px] font-semibold text-text-tertiary uppercase">{t('generate.tryIt', { ns: 'appDebug' })}</div>
style={{ <div
background: 'linear-gradient(to right, rgba(243, 244, 246, 1), rgba(243, 244, 246, 0))', className="h-px grow"
}} style={{
> background: 'linear-gradient(to right, rgba(243, 244, 246, 1), rgba(243, 244, 246, 0))',
}}
>
</div>
</div>
<div className="flex flex-wrap">
{tryList.map(item => (
<TryLabel
key={item.key}
Icon={item.icon}
text={t(`generate.template.${item.key}.name`, { ns: 'appDebug' })}
onClick={handleChooseTemplate(item.key)}
/>
))}
</div> </div>
</div> </div>
<div className="flex flex-wrap"> )}
{tryList.map(item => (
<TryLabel {/* inputs */}
key={item.key} <div className="mt-4">
Icon={item.icon} <div>
text={t(`generate.template.${item.key}.name`, { ns: 'appDebug' })} <div className="mb-1.5 system-sm-semibold-uppercase text-text-secondary">{t('generate.instruction', { ns: 'appDebug' })}</div>
onClick={handleChooseTemplate(item.key)} {isBasicMode
/> ? (
))} <InstructionEditorInBasic
editorKey={editorKey}
generatorType={GeneratorType.prompt}
value={instruction}
onChange={setInstruction}
availableVars={[]}
availableNodes={[]}
isShowCurrentBlock={!!currentPrompt}
isShowLastRunBlock={false}
/>
)
: (
<InstructionEditorInWorkflow
editorKey={editorKey}
generatorType={GeneratorType.prompt}
value={instruction}
onChange={setInstruction}
nodeId={nodeId || ''}
isShowCurrentBlock={!!currentPrompt}
/>
)}
</div>
<IdeaOutput
value={ideaOutput}
onChange={setIdeaOutput}
/>
<div className="mt-7 flex justify-end space-x-2">
<Button onClick={onClose}>{t(`${i18nPrefix}.dismiss`, { ns: 'appDebug' })}</Button>
<Button
className="flex space-x-1"
variant="primary"
onClick={onGenerate}
disabled={isLoading}
>
<Generator className="h-4 w-4" />
<span className="text-xs font-semibold">{t('generate.generate', { ns: 'appDebug' })}</span>
</Button>
</div> </div>
</div> </div>
</div>
{(!isLoading && current) && (
<div className="h-full w-0 grow bg-background-default-subtle p-6 pb-0">
<Result
current={current!}
isBasicMode={isBasicMode}
nodeId={nodeId!}
currentVersionIndex={currentVersionIndex || 0}
setCurrentVersionIndex={setCurrentVersionIndex}
versions={versions || []}
onApply={showConfirmOverwrite}
generatorType={GeneratorType.prompt}
/>
</div>
)} )}
{isLoading && renderLoading}
{/* inputs */} {isShowAutoPromptResPlaceholder() && <ResPlaceholder />}
<div className="mt-4"> <AlertDialog open={isShowConfirmOverwrite} onOpenChange={open => !open && hideShowConfirmOverwrite()}>
<div> <AlertDialogContent>
<div className="mb-1.5 system-sm-semibold-uppercase text-text-secondary">{t('generate.instruction', { ns: 'appDebug' })}</div> <div className="flex flex-col gap-2 px-6 pt-6 pb-4">
{isBasicMode <AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
? ( {t('generate.overwriteTitle', { ns: 'appDebug' })}
<InstructionEditorInBasic </AlertDialogTitle>
editorKey={editorKey} <AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
generatorType={GeneratorType.prompt} {t('generate.overwriteMessage', { ns: 'appDebug' })}
value={instruction} </AlertDialogDescription>
onChange={setInstruction} </div>
availableVars={[]} <AlertDialogActions>
availableNodes={[]} <AlertDialogCancelButton>{t('operation.cancel', { ns: 'common' })}</AlertDialogCancelButton>
isShowCurrentBlock={!!currentPrompt} <AlertDialogConfirmButton
isShowLastRunBlock={false} onClick={() => {
/> hideShowConfirmOverwrite()
) onFinished(current!)
: ( }}
<InstructionEditorInWorkflow >
editorKey={editorKey} {t('operation.confirm', { ns: 'common' })}
generatorType={GeneratorType.prompt} </AlertDialogConfirmButton>
value={instruction} </AlertDialogActions>
onChange={setInstruction} </AlertDialogContent>
nodeId={nodeId || ''} </AlertDialog>
isShowCurrentBlock={!!currentPrompt}
/>
)}
</div>
<IdeaOutput
value={ideaOutput}
onChange={setIdeaOutput}
/>
<div className="mt-7 flex justify-end space-x-2">
<Button onClick={onClose}>{t(`${i18nPrefix}.dismiss`, { ns: 'appDebug' })}</Button>
<Button
className="flex space-x-1"
variant="primary"
onClick={onGenerate}
disabled={isLoading}
>
<Generator className="h-4 w-4" />
<span className="text-xs font-semibold">{t('generate.generate', { ns: 'appDebug' })}</span>
</Button>
</div>
</div>
</div> </div>
</DialogContent>
{(!isLoading && current) && ( </Dialog>
<div className="h-full w-0 grow bg-background-default-subtle p-6 pb-0">
<Result
current={current!}
isBasicMode={isBasicMode}
nodeId={nodeId!}
currentVersionIndex={currentVersionIndex || 0}
setCurrentVersionIndex={setCurrentVersionIndex}
versions={versions || []}
onApply={showConfirmOverwrite}
generatorType={GeneratorType.prompt}
/>
</div>
)}
{isLoading && renderLoading}
{isShowAutoPromptResPlaceholder() && <ResPlaceholder />}
<AlertDialog open={isShowConfirmOverwrite} onOpenChange={open => !open && hideShowConfirmOverwrite()}>
<AlertDialogContent>
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
{t('generate.overwriteTitle', { ns: 'appDebug' })}
</AlertDialogTitle>
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
{t('generate.overwriteMessage', { ns: 'appDebug' })}
</AlertDialogDescription>
</div>
<AlertDialogActions>
<AlertDialogCancelButton>{t('operation.cancel', { ns: 'common' })}</AlertDialogCancelButton>
<AlertDialogConfirmButton
onClick={() => {
hideShowConfirmOverwrite()
onFinished(current!)
}}
>
{t('operation.confirm', { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
</div>
</Modal>
) )
} }
export default React.memo(GetAutomaticRes) export default React.memo(GetAutomaticRes)

View File

@ -13,6 +13,7 @@ import {
AlertDialogTitle, AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog' } from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { import {
useBoolean, useBoolean,
@ -23,7 +24,6 @@ import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Generator } from '@/app/components/base/icons/src/vender/other' import { Generator } from '@/app/components/base/icons/src/vender/other'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
@ -202,99 +202,104 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
) )
return ( return (
<Modal <Dialog
isShow={isShow} open={isShow}
onClose={onClose} onOpenChange={(open) => {
className="min-w-[1140px] p-0!" if (!open)
onClose()
}}
> >
<div className="relative flex h-[680px] flex-wrap"> <DialogContent className="max-h-none w-full min-w-[1140px] overflow-hidden! border-none p-0! text-left align-middle">
<div className="h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6">
<div className="mb-5"> <div className="relative flex h-[680px] flex-wrap">
<div className={`text-lg leading-[28px] font-bold ${s.textGradient}`}>{t('codegen.title', { ns: 'appDebug' })}</div> <div className="h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6">
<div className="mt-1 text-[13px] font-normal text-text-tertiary">{t('codegen.description', { ns: 'appDebug' })}</div> <div className="mb-5">
</div> <div className={`text-lg leading-[28px] font-bold ${s.textGradient}`}>{t('codegen.title', { ns: 'appDebug' })}</div>
<div className="mb-4"> <div className="mt-1 text-[13px] font-normal text-text-tertiary">{t('codegen.description', { ns: 'appDebug' })}</div>
<ModelParameterModal </div>
popupClassName="w-[520px]!" <div className="mb-4">
isAdvancedMode={true} <ModelParameterModal
provider={model.provider} popupClassName="w-[520px]!"
completionParams={model.completion_params} isAdvancedMode={true}
modelId={model.name} provider={model.provider}
setModel={handleModelChange} completionParams={model.completion_params}
onCompletionParamsChange={handleCompletionParamsChange} modelId={model.name}
hideDebugWithMultipleModel setModel={handleModelChange}
/> onCompletionParamsChange={handleCompletionParamsChange}
</div> hideDebugWithMultipleModel
<div>
<div className="text-[0px]">
<div className="mb-1.5 system-sm-semibold-uppercase text-text-secondary">{t('codegen.instruction', { ns: 'appDebug' })}</div>
<InstructionEditor
editorKey={editorKey}
value={instruction}
onChange={setInstruction}
nodeId={nodeId}
generatorType={GeneratorType.code}
isShowCurrentBlock={!!currentCode}
/> />
</div> </div>
<IdeaOutput <div>
value={ideaOutput} <div className="text-[0px]">
onChange={setIdeaOutput} <div className="mb-1.5 system-sm-semibold-uppercase text-text-secondary">{t('codegen.instruction', { ns: 'appDebug' })}</div>
/> <InstructionEditor
editorKey={editorKey}
value={instruction}
onChange={setInstruction}
nodeId={nodeId}
generatorType={GeneratorType.code}
isShowCurrentBlock={!!currentCode}
/>
</div>
<IdeaOutput
value={ideaOutput}
onChange={setIdeaOutput}
/>
<div className="mt-7 flex justify-end space-x-2"> <div className="mt-7 flex justify-end space-x-2">
<Button onClick={onClose}>{t(`${i18nPrefix}.dismiss`, { ns: 'appDebug' })}</Button> <Button onClick={onClose}>{t(`${i18nPrefix}.dismiss`, { ns: 'appDebug' })}</Button>
<Button <Button
className="flex space-x-1" className="flex space-x-1"
variant="primary" variant="primary"
onClick={onGenerate} onClick={onGenerate}
disabled={isLoading} disabled={isLoading}
> >
<Generator className="h-4 w-4" /> <Generator className="h-4 w-4" />
<span className="text-xs font-semibold">{t('codegen.generate', { ns: 'appDebug' })}</span> <span className="text-xs font-semibold">{t('codegen.generate', { ns: 'appDebug' })}</span>
</Button> </Button>
</div>
</div> </div>
</div> </div>
{isLoading && renderLoading}
{!isLoading && !current && <ResPlaceholder />}
{(!isLoading && current) && (
<div className="h-full w-0 grow bg-background-default-subtle p-6 pb-0">
<Result
current={current!}
currentVersionIndex={currentVersionIndex || 0}
setCurrentVersionIndex={setCurrentVersionIndex}
versions={versions || []}
onApply={showConfirmOverwrite}
generatorType={GeneratorType.code}
/>
</div>
)}
</div> </div>
{isLoading && renderLoading} <AlertDialog open={isShowConfirmOverwrite} onOpenChange={open => !open && hideShowConfirmOverwrite()}>
{!isLoading && !current && <ResPlaceholder />} <AlertDialogContent>
{(!isLoading && current) && ( <div className="flex flex-col gap-2 px-6 pt-6 pb-4">
<div className="h-full w-0 grow bg-background-default-subtle p-6 pb-0"> <AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
<Result {t('codegen.overwriteConfirmTitle', { ns: 'appDebug' })}
current={current!} </AlertDialogTitle>
currentVersionIndex={currentVersionIndex || 0} <AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
setCurrentVersionIndex={setCurrentVersionIndex} {t('codegen.overwriteConfirmMessage', { ns: 'appDebug' })}
versions={versions || []} </AlertDialogDescription>
onApply={showConfirmOverwrite} </div>
generatorType={GeneratorType.code} <AlertDialogActions>
/> <AlertDialogCancelButton>{t('operation.cancel', { ns: 'common' })}</AlertDialogCancelButton>
</div> <AlertDialogConfirmButton
)} onClick={() => {
</div> hideShowConfirmOverwrite()
<AlertDialog open={isShowConfirmOverwrite} onOpenChange={open => !open && hideShowConfirmOverwrite()}> onFinished(current!)
<AlertDialogContent> }}
<div className="flex flex-col gap-2 px-6 pt-6 pb-4"> >
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary"> {t('operation.confirm', { ns: 'common' })}
{t('codegen.overwriteConfirmTitle', { ns: 'appDebug' })} </AlertDialogConfirmButton>
</AlertDialogTitle> </AlertDialogActions>
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary"> </AlertDialogContent>
{t('codegen.overwriteConfirmMessage', { ns: 'appDebug' })} </AlertDialog>
</AlertDialogDescription> </DialogContent>
</div> </Dialog>
<AlertDialogActions>
<AlertDialogCancelButton>{t('operation.cancel', { ns: 'common' })}</AlertDialogCancelButton>
<AlertDialogConfirmButton
onClick={() => {
hideShowConfirmOverwrite()
onFinished(current!)
}}
>
{t('operation.confirm', { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
</Modal>
) )
} }

View File

@ -3,12 +3,12 @@ import type { DataSet } from '@/models/datasets'
import type { DatasetConfigs } from '@/models/debug' import type { DatasetConfigs } from '@/models/debug'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { RiEqualizer2Line } from '@remixicon/react' import { RiEqualizer2Line } from '@remixicon/react'
import { memo, useEffect, useState } from 'react' import { memo, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import Modal from '@/app/components/base/modal'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { useCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { import {
@ -123,32 +123,36 @@ const ParamsConfig = ({
</Button> </Button>
{ {
rerankSettingModalOpen && ( rerankSettingModalOpen && (
<Modal <Dialog
isShow={rerankSettingModalOpen} open={rerankSettingModalOpen}
onClose={() => { onOpenChange={(open) => {
setRerankSettingModalOpen(false) if (!open) {
setRerankSettingModalOpen(false)
}
}} }}
className="sm:min-w-[528px]"
> >
<ConfigContent <DialogContent className="w-full max-w-[480px] overflow-hidden! border-none text-left align-middle sm:min-w-[528px]">
datasetConfigs={tempDataSetConfigs}
onChange={handleSetTempDataSetConfigs}
selectedDatasets={selectedDatasets}
/>
<div className="mt-6 flex justify-end"> <ConfigContent
<Button datasetConfigs={tempDataSetConfigs}
className="mr-2 shrink-0" onChange={handleSetTempDataSetConfigs}
onClick={() => { selectedDatasets={selectedDatasets}
setTempDataSetConfigs(datasetConfigs) />
setRerankSettingModalOpen(false)
}} <div className="mt-6 flex justify-end">
> <Button
{t('operation.cancel', { ns: 'common' })} className="mr-2 shrink-0"
</Button> onClick={() => {
<Button variant="primary" className="shrink-0" onClick={handleSave}>{t('operation.save', { ns: 'common' })}</Button> setTempDataSetConfigs(datasetConfigs)
</div> setRerankSettingModalOpen(false)
</Modal> }}
>
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button variant="primary" className="shrink-0" onClick={handleSave}>{t('operation.save', { ns: 'common' })}</Button>
</div>
</DialogContent>
</Dialog>
) )
} }

View File

@ -1,6 +1,13 @@
import { Button } from '@langgenius/dify-ui/button' import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
type DSLConfirmModalProps = { type DSLConfirmModalProps = {
versions?: { versions?: {
@ -20,32 +27,36 @@ const DSLConfirmModal = ({
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Modal <AlertDialog
isShow open
onClose={() => onCancel()} onOpenChange={(open) => {
className="w-[480px]" if (!open)
onCancel()
}}
> >
<div className="flex flex-col items-start gap-2 self-stretch pb-4"> <AlertDialogContent className="w-[480px] overflow-hidden! border-none text-left align-middle shadow-xl">
<div className="title-2xl-semi-bold text-text-primary">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</div> <div className="flex flex-col items-start gap-2 self-stretch p-6 pb-4">
<div className="flex grow flex-col system-md-regular text-text-secondary"> <AlertDialogTitle className="title-2xl-semi-bold text-text-primary">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</AlertDialogTitle>
<div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div> <AlertDialogDescription render={<div />} className="flex grow flex-col system-md-regular text-text-secondary">
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div> <div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
<br /> <div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
<div> <br />
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })} <div>
<span className="system-md-medium">{versions.importedVersion}</span> {t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
</div> <span className="system-md-medium">{versions.importedVersion}</span>
<div> </div>
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })} <div>
<span className="system-md-medium">{versions.systemVersion}</span> {t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
</div> <span className="system-md-medium">{versions.systemVersion}</span>
</div>
</AlertDialogDescription>
</div> </div>
</div> <AlertDialogActions>
<div className="flex items-start justify-end gap-2 self-stretch pt-6"> <AlertDialogCancelButton variant="secondary">{t('newApp.Cancel', { ns: 'app' })}</AlertDialogCancelButton>
<Button variant="secondary" onClick={() => onCancel()}>{t('newApp.Cancel', { ns: 'app' })}</Button> <AlertDialogConfirmButton onClick={onConfirm} disabled={confirmDisabled}>{t('newApp.Confirm', { ns: 'app' })}</AlertDialogConfirmButton>
<Button variant="primary" tone="destructive" onClick={onConfirm} disabled={confirmDisabled}>{t('newApp.Confirm', { ns: 'app' })}</Button> </AlertDialogActions>
</div> </AlertDialogContent>
</Modal> </AlertDialog>
) )
} }

View File

@ -3,14 +3,13 @@
import type { MouseEventHandler } from 'react' import type { MouseEventHandler } from 'react'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import { useDebounceFn, useKeyPress } from 'ahooks' import { useDebounceFn, useKeyPress } from 'ahooks'
import { noop } from 'es-toolkit/function'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import AppsFull from '@/app/components/billing/apps-full-in-dialog' import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
@ -219,108 +218,112 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
return ( return (
<> <>
<Modal <Dialog open={show}>
className="w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0 shadow-xl" <DialogContent className="w-full max-w-[480px]! overflow-hidden! rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0! text-left align-middle shadow-xl">
isShow={show}
onClose={noop} <div className="flex items-center justify-between pt-6 pr-5 pb-3 pl-6 title-2xl-semi-bold text-text-primary">
> {t('importApp', { ns: 'app' })}
<div className="flex items-center justify-between pt-6 pr-5 pb-3 pl-6 title-2xl-semi-bold text-text-primary"> <div
{t('importApp', { ns: 'app' })} className="flex h-8 w-8 cursor-pointer items-center"
<div onClick={() => onClose()}
className="flex h-8 w-8 cursor-pointer items-center" >
onClick={() => onClose()} <RiCloseLine className="h-5 w-5 text-text-tertiary" />
> </div>
<RiCloseLine className="h-5 w-5 text-text-tertiary" />
</div> </div>
</div> <div className="flex h-9 items-center space-x-6 border-b border-divider-subtle px-6 system-md-semibold text-text-tertiary">
<div className="flex h-9 items-center space-x-6 border-b border-divider-subtle px-6 system-md-semibold text-text-tertiary"> {
{ tabs.map(tab => (
tabs.map(tab => ( <div
<div key={tab.key}
key={tab.key} className={cn(
className={cn( 'relative flex h-full cursor-pointer items-center',
'relative flex h-full cursor-pointer items-center', currentTab === tab.key && 'text-text-primary',
currentTab === tab.key && 'text-text-primary', )}
)} onClick={() => setCurrentTab(tab.key)}
onClick={() => setCurrentTab(tab.key)} >
> {tab.label}
{tab.label} {
{ currentTab === tab.key && (
currentTab === tab.key && ( <div className="absolute bottom-0 h-[2px] w-full bg-util-colors-blue-brand-blue-brand-600"></div>
<div className="absolute bottom-0 h-[2px] w-full bg-util-colors-blue-brand-blue-brand-600"></div> )
) }
} </div>
</div> ))
)) }
} </div>
</div> <div className="px-6 py-4">
<div className="px-6 py-4"> {
{ currentTab === CreateFromDSLModalTab.FROM_FILE && (
currentTab === CreateFromDSLModalTab.FROM_FILE && ( <Uploader
<Uploader className="mt-0"
className="mt-0" file={currentFile}
file={currentFile} updateFile={handleFile}
updateFile={handleFile}
/>
)
}
{
currentTab === CreateFromDSLModalTab.FROM_URL && (
<div>
<div className="mb-1 system-md-semibold text-text-secondary">DSL URL</div>
<Input
placeholder={t('importFromDSLUrlPlaceholder', { ns: 'app' }) || ''}
value={dslUrlValue}
onChange={e => setDslUrlValue(e.target.value)}
/> />
</div> )
) }
} {
</div> currentTab === CreateFromDSLModalTab.FROM_URL && (
{isAppsFull && ( <div>
<div className="px-6"> <div className="mb-1 system-md-semibold text-text-secondary">DSL URL</div>
<AppsFull className="mt-0" loc="app-create-dsl" /> <Input
placeholder={t('importFromDSLUrlPlaceholder', { ns: 'app' }) || ''}
value={dslUrlValue}
onChange={e => setDslUrlValue(e.target.value)}
/>
</div>
)
}
</div> </div>
)} {isAppsFull && (
<div className="flex justify-end px-6 py-5"> <div className="px-6">
<Button className="mr-2" onClick={onClose}>{t('newApp.Cancel', { ns: 'app' })}</Button> <AppsFull className="mt-0" loc="app-create-dsl" />
<Button </div>
disabled={buttonDisabled} )}
variant="primary" <div className="flex justify-end px-6 py-5">
onClick={handleCreateApp} <Button className="mr-2" onClick={onClose}>{t('newApp.Cancel', { ns: 'app' })}</Button>
className="gap-1" <Button
> disabled={buttonDisabled}
<span>{t('newApp.Create', { ns: 'app' })}</span> variant="primary"
<ShortcutsName keys={['ctrl', '↵']} bgColor="white" /> onClick={handleCreateApp}
</Button> className="gap-1"
</div> >
</Modal> <span>{t('newApp.Create', { ns: 'app' })}</span>
<Modal <ShortcutsName keys={['ctrl', '↵']} bgColor="white" />
isShow={showErrorModal} </Button>
onClose={() => setShowErrorModal(false)} </div>
className="w-[480px]" </DialogContent>
</Dialog>
<Dialog
open={showErrorModal}
onOpenChange={(open) => {
if (!open)
setShowErrorModal(false)
}}
> >
<div className="flex flex-col items-start gap-2 self-stretch pb-4"> <DialogContent className="w-full max-w-[480px]! overflow-hidden! border-none text-left align-middle">
<div className="title-2xl-semi-bold text-text-primary">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</div>
<div className="flex grow flex-col system-md-regular text-text-secondary"> <div className="flex flex-col items-start gap-2 self-stretch pb-4">
<div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div> <div className="title-2xl-semi-bold text-text-primary">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</div>
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div> <div className="flex grow flex-col system-md-regular text-text-secondary">
<br /> <div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
<div> <div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })} <br />
<span className="system-md-medium">{versions?.importedVersion}</span> <div>
</div> {t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
<div> <span className="system-md-medium">{versions?.importedVersion}</span>
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })} </div>
<span className="system-md-medium">{versions?.systemVersion}</span> <div>
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
<span className="system-md-medium">{versions?.systemVersion}</span>
</div>
</div> </div>
</div> </div>
</div> <div className="flex items-start justify-end gap-2 self-stretch pt-6">
<div className="flex items-start justify-end gap-2 self-stretch pt-6"> <Button variant="secondary" onClick={() => setShowErrorModal(false)}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button variant="secondary" onClick={() => setShowErrorModal(false)}>{t('newApp.Cancel', { ns: 'app' })}</Button> <Button variant="primary" tone="destructive" onClick={onDSLConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button>
<Button variant="primary" tone="destructive" onClick={onDSLConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button> </div>
</div> </DialogContent>
</Modal> </Dialog>
</> </>
) )
} }

View File

@ -1,16 +1,14 @@
'use client' 'use client'
import type { AppIconType } from '@/types/app' import type { AppIconType } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import * as React from 'react' import * as React from 'react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import AppsFull from '@/app/components/billing/apps-full-in-dialog' import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import AppIconPicker from '../../base/app-icon-picker' import AppIconPicker from '../../base/app-icon-picker'
@ -71,40 +69,39 @@ const DuplicateAppModal = ({
return ( return (
<> <>
<Modal <Dialog open={show}>
isShow={show} <DialogContent className="w-full max-w-[480px]! overflow-hidden! border-none px-8 text-left align-middle">
onClose={noop}
className={cn('relative max-w-[480px]!', 'px-8')} <div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onHide}>
> <RiCloseLine className="h-4 w-4 text-text-tertiary" />
<div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onHide}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div>
<div className="relative mt-3 mb-9 text-xl leading-[30px] font-semibold text-text-primary">{t('duplicateTitle', { ns: 'app' })}</div>
<div className="mb-9 system-sm-regular text-text-secondary">
<div className="mb-2 system-md-medium">{t('appCustomize.subTitle', { ns: 'explore' })}</div>
<div className="flex items-center justify-between space-x-2">
<AppIcon
size="large"
onClick={() => { setShowAppIconPicker(true) }}
className="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}
/>
<Input
value={name}
onChange={e => setName(e.target.value)}
className="h-10"
/>
</div> </div>
{isAppsFull && <AppsFull className="mt-4" loc="app-duplicate-create" />} <div className="relative mt-3 mb-9 text-xl leading-[30px] font-semibold text-text-primary">{t('duplicateTitle', { ns: 'app' })}</div>
</div> <div className="mb-9 system-sm-regular text-text-secondary">
<div className="flex flex-row-reverse"> <div className="mb-2 system-md-medium">{t('appCustomize.subTitle', { ns: 'explore' })}</div>
<Button disabled={isAppsFull} className="ml-2 w-24" variant="primary" onClick={submit}>{t('duplicate', { ns: 'app' })}</Button> <div className="flex items-center justify-between space-x-2">
<Button className="w-24" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button> <AppIcon
</div> size="large"
</Modal> onClick={() => { setShowAppIconPicker(true) }}
className="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}
/>
<Input
value={name}
onChange={e => setName(e.target.value)}
className="h-10"
/>
</div>
{isAppsFull && <AppsFull className="mt-4" loc="app-duplicate-create" />}
</div>
<div className="flex flex-row-reverse">
<Button disabled={isAppsFull} className="ml-2 w-24" variant="primary" onClick={submit}>{t('duplicate', { ns: 'app' })}</Button>
<Button className="w-24" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
</div>
</DialogContent>
</Dialog>
{showAppIconPicker && ( {showAppIconPicker && (
<AppIconPicker <AppIconPicker
onSelect={(payload) => { onSelect={(payload) => {

View File

@ -12,9 +12,9 @@ import {
} from '@langgenius/dify-ui/alert-dialog' } from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
@ -22,7 +22,6 @@ import AppIcon from '@/app/components/base/app-icon'
import Checkbox from '@/app/components/base/checkbox' import Checkbox from '@/app/components/base/checkbox'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import AppsFull from '@/app/components/billing/apps-full-in-dialog' import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
@ -109,69 +108,68 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
return ( return (
<> <>
<Modal <Dialog open={show}>
className={cn('w-[600px] max-w-[600px] p-8')} <DialogContent className={cn('w-full overflow-hidden! border-none text-left align-middle', cn('w-[600px] max-w-[600px] p-8'))}>
isShow={show}
onClose={noop} <div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onClose}>
> <RiCloseLine className="h-4 w-4 text-text-tertiary" />
<div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onClose}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div>
<div className="h-12 w-12 rounded-xl border-[0.5px] border-divider-regular bg-background-default-burn p-3 shadow-xl">
<AlertTriangle className="h-6 w-6 text-[rgb(247,144,9)]" />
</div>
<div className="relative mt-3 text-xl leading-[30px] font-semibold text-text-primary">{t('switch', { ns: 'app' })}</div>
<div className="my-1 text-sm leading-5 text-text-tertiary">
<span>{t('switchTipStart', { ns: 'app' })}</span>
<span className="font-medium text-text-secondary">{t('switchTip', { ns: 'app' })}</span>
<span>{t('switchTipEnd', { ns: 'app' })}</span>
</div>
<div className="pb-4">
<div className="py-2 text-sm leading-[20px] font-medium text-text-primary">{t('switchLabel', { ns: 'app' })}</div>
<div className="flex items-center justify-between space-x-2">
<AppIcon
size="large"
onClick={() => { setShowAppIconPicker(true) }}
className="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}
/>
<Input
value={name}
onChange={e => setName(e.target.value)}
placeholder={t('newApp.appNamePlaceholder', { ns: 'app' }) || ''}
className="h-10 grow"
/>
</div> </div>
{showAppIconPicker && ( <div className="h-12 w-12 rounded-xl border-[0.5px] border-divider-regular bg-background-default-burn p-3 shadow-xl">
<AppIconPicker <AlertTriangle className="h-6 w-6 text-[rgb(247,144,9)]" />
onSelect={(payload) => {
setAppIcon(payload)
setShowAppIconPicker(false)
}}
onClose={() => {
setAppIcon(appDetail.icon_type === 'image'
? { type: 'image' as const, url: appDetail.icon_url, fileId: appDetail.icon }
: { type: 'emoji' as const, icon: appDetail.icon, background: appDetail.icon_background })
setShowAppIconPicker(false)
}}
/>
)}
</div>
{isAppsFull && <AppsFull loc="app-switch" />}
<div className="flex items-center justify-between pt-6">
<div className="flex items-center">
<Checkbox className="shrink-0" checked={removeOriginal} onCheck={() => setRemoveOriginal(!removeOriginal)} />
<div className="ml-2 cursor-pointer text-sm leading-5 text-text-secondary" onClick={() => setRemoveOriginal(!removeOriginal)}>{t('removeOriginal', { ns: 'app' })}</div>
</div> </div>
<div className="flex items-center"> <div className="relative mt-3 text-xl leading-[30px] font-semibold text-text-primary">{t('switch', { ns: 'app' })}</div>
<Button className="mr-2" onClick={onClose}>{t('newApp.Cancel', { ns: 'app' })}</Button> <div className="my-1 text-sm leading-5 text-text-tertiary">
<Button className="border-red-700" disabled={isAppsFull || !name} variant="primary" tone="destructive" onClick={goStart}>{t('switchStart', { ns: 'app' })}</Button> <span>{t('switchTipStart', { ns: 'app' })}</span>
<span className="font-medium text-text-secondary">{t('switchTip', { ns: 'app' })}</span>
<span>{t('switchTipEnd', { ns: 'app' })}</span>
</div> </div>
</div> <div className="pb-4">
</Modal> <div className="py-2 text-sm leading-[20px] font-medium text-text-primary">{t('switchLabel', { ns: 'app' })}</div>
<div className="flex items-center justify-between space-x-2">
<AppIcon
size="large"
onClick={() => { setShowAppIconPicker(true) }}
className="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}
/>
<Input
value={name}
onChange={e => setName(e.target.value)}
placeholder={t('newApp.appNamePlaceholder', { ns: 'app' }) || ''}
className="h-10 grow"
/>
</div>
{showAppIconPicker && (
<AppIconPicker
onSelect={(payload) => {
setAppIcon(payload)
setShowAppIconPicker(false)
}}
onClose={() => {
setAppIcon(appDetail.icon_type === 'image'
? { type: 'image' as const, url: appDetail.icon_url, fileId: appDetail.icon }
: { type: 'emoji' as const, icon: appDetail.icon, background: appDetail.icon_background })
setShowAppIconPicker(false)
}}
/>
)}
</div>
{isAppsFull && <AppsFull loc="app-switch" />}
<div className="flex items-center justify-between pt-6">
<div className="flex items-center">
<Checkbox className="shrink-0" checked={removeOriginal} onCheck={() => setRemoveOriginal(!removeOriginal)} />
<div className="ml-2 cursor-pointer text-sm leading-5 text-text-secondary" onClick={() => setRemoveOriginal(!removeOriginal)}>{t('removeOriginal', { ns: 'app' })}</div>
</div>
<div className="flex items-center">
<Button className="mr-2" onClick={onClose}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button className="border-red-700" disabled={isAppsFull || !name} variant="primary" tone="destructive" onClick={goStart}>{t('switchStart', { ns: 'app' })}</Button>
</div>
</div>
</DialogContent>
</Dialog>
<AlertDialog <AlertDialog
open={showConfirmDelete} open={showConfirmDelete}
onOpenChange={handleConfirmDeleteOpenChange} onOpenChange={handleConfirmDeleteOpenChange}

View File

@ -4,15 +4,14 @@ import type { OnImageInput } from './ImageInput'
import type { AppIconType, ImageFile } from '@/types/app' import type { AppIconType, ImageFile } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { RiImageCircleAiLine } from '@remixicon/react' import { RiImageCircleAiLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config' import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
import Divider from '../divider' import Divider from '../divider'
import EmojiPickerInner from '../emoji-picker/Inner' import EmojiPickerInner from '../emoji-picker/Inner'
import { useLocalFileUploader } from '../image-uploader/hooks' import { useLocalFileUploader } from '../image-uploader/hooks'
import Modal from '../modal'
import ImageInput from './ImageInput' import ImageInput from './ImageInput'
import s from './style.module.css' import s from './style.module.css'
import getCroppedImg from './utils' import getCroppedImg from './utils'
@ -45,7 +44,6 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
onSelect, onSelect,
onClose, onClose,
initialEmoji, initialEmoji,
className,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -113,57 +111,54 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
} }
return ( return (
<Modal <Dialog open>
onClose={noop} <DialogContent className={cn('max-h-none w-full overflow-hidden! border-none text-left align-middle', s.container, 'h-[462px]! w-[362px]! p-0!')}>
isShow
closable={false} {!DISABLE_UPLOAD_IMAGE_AS_ICON && (
wrapperClassName={className} <div className="w-full p-2 pb-0">
className={cn(s.container, 'h-[462px]! w-[362px]! p-0!')} <div className="flex items-center justify-center gap-2 rounded-xl bg-background-body p-1 text-text-primary">
> {tabs.map(tab => (
{!DISABLE_UPLOAD_IMAGE_AS_ICON && ( <button
<div className="w-full p-2 pb-0"> type="button"
<div className="flex items-center justify-center gap-2 rounded-xl bg-background-body p-1 text-text-primary"> key={tab.key}
{tabs.map(tab => ( className={cn(
<button 'flex h-8 flex-1 shrink-0 items-center justify-center rounded-lg p-2 system-sm-medium text-text-tertiary',
type="button" activeTab === tab.key && 'bg-components-main-nav-nav-button-bg-active text-text-accent shadow-md',
key={tab.key} )}
className={cn( onClick={() => setActiveTab(tab.key as AppIconType)}
'flex h-8 flex-1 shrink-0 items-center justify-center rounded-lg p-2 system-sm-medium text-text-tertiary', >
activeTab === tab.key && 'bg-components-main-nav-nav-button-bg-active text-text-accent shadow-md', {tab.icon}
)} {' '}
onClick={() => setActiveTab(tab.key as AppIconType)}
>
{tab.icon}
{' '}
&nbsp; &nbsp;
{tab.label} {tab.label}
</button> </button>
))} ))}
</div>
</div> </div>
)}
{activeTab === 'emoji' && (
<EmojiPickerInner
className={cn('flex-1 overflow-hidden pt-2')}
emoji={initialEmoji?.icon}
background={initialEmoji?.background ?? undefined}
onSelect={handleSelectEmoji}
/>
)}
{activeTab === 'image' && <ImageInput className={cn('flex-1 overflow-hidden')} onImageInput={handleImageInput} />}
<Divider className="m-0" />
<div className="flex w-full items-center justify-center gap-2 p-3">
<Button className="w-full" onClick={() => onClose?.()}>
{t('iconPicker.cancel', { ns: 'app' })}
</Button>
<Button variant="primary" className="w-full" disabled={uploading} loading={uploading} onClick={handleSelect}>
{t('iconPicker.ok', { ns: 'app' })}
</Button>
</div> </div>
)} </DialogContent>
</Dialog>
{activeTab === 'emoji' && (
<EmojiPickerInner
className={cn('flex-1 overflow-hidden pt-2')}
emoji={initialEmoji?.icon}
background={initialEmoji?.background ?? undefined}
onSelect={handleSelectEmoji}
/>
)}
{activeTab === 'image' && <ImageInput className={cn('flex-1 overflow-hidden')} onImageInput={handleImageInput} />}
<Divider className="m-0" />
<div className="flex w-full items-center justify-center gap-2 p-3">
<Button className="w-full" onClick={() => onClose?.()}>
{t('iconPicker.cancel', { ns: 'app' })}
</Button>
<Button variant="primary" className="w-full" disabled={uploading} loading={uploading} onClick={handleSelect}>
{t('iconPicker.ok', { ns: 'app' })}
</Button>
</div>
</Modal>
) )
} }

View File

@ -62,18 +62,15 @@ vi.mock('@/next/navigation', () => ({
usePathname: () => '/test', usePathname: () => '/test',
})) }))
// Mock Modal to avoid Headless UI issues in tests vi.mock('@langgenius/dify-ui/dialog', () => ({
vi.mock('@/app/components/base/modal', () => ({ Dialog: ({ children, open }: { children: React.ReactNode, open?: boolean }) =>
default: ({ children, isShow, title }: { children: React.ReactNode, isShow: boolean, title: React.ReactNode }) => { open === false ? null : <>{children}</>,
if (!isShow) DialogContent: ({ children }: { children: React.ReactNode }) => (
return null <div data-testid="modal">{children}</div>
return ( ),
<div data-testid="modal"> DialogTitle: ({ children }: { children: React.ReactNode }) => (
{!!title && <div data-testid="modal-title">{title}</div>} <div data-testid="modal-title">{children}</div>
{children} ),
</div>
)
},
})) }))
describe('Sidebar Index', () => { describe('Sidebar Index', () => {

View File

@ -4,25 +4,13 @@ import userEvent from '@testing-library/user-event'
import * as ReactI18next from 'react-i18next' import * as ReactI18next from 'react-i18next'
import RenameModal from '../rename-modal' import RenameModal from '../rename-modal'
vi.mock('@/app/components/base/modal', () => ({ vi.mock('@langgenius/dify-ui/dialog', () => ({
default: ({ Dialog: ({ children, open }: { children: ReactNode, open?: boolean }) =>
title, open === false ? null : <>{children}</>,
isShow, DialogContent: ({ children }: { children: ReactNode }) => (
children, <div role="dialog">{children}</div>
}: { ),
title: ReactNode DialogTitle: ({ children }: { children: ReactNode }) => <h2>{children}</h2>,
isShow: boolean
children: ReactNode
}) => {
if (!isShow)
return null
return (
<div role="dialog">
<h2>{title}</h2>
{children}
</div>
)
},
})) }))
describe('RenameModal', () => { describe('RenameModal', () => {

View File

@ -1,138 +0,0 @@
import { act, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import CustomDialog from '../index'
describe('CustomDialog Component', () => {
const setup = () => userEvent.setup()
it('should render children and title when show is true', async () => {
render(
<CustomDialog show={true} title="Modal Title">
<div data-testid="dialog-content">Main Content</div>
</CustomDialog>,
)
const title = await screen.findByText('Modal Title')
const content = screen.getByTestId('dialog-content')
expect(title).toBeInTheDocument()
expect(content).toBeInTheDocument()
expect(screen.getByRole('dialog')).toBeInTheDocument()
})
it('should not render anything when show is false', async () => {
render(
<CustomDialog show={false} title="Hidden Title">
<div>Content</div>
</CustomDialog>,
)
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
expect(screen.queryByText('Hidden Title')).not.toBeInTheDocument()
})
it('should apply the correct semantic tag to title using titleAs', async () => {
render(
<CustomDialog show={true} title="Semantic Title" titleAs="h1">
Content
</CustomDialog>,
)
const title = await screen.findByRole('heading', { level: 1 })
expect(title).toHaveTextContent('Semantic Title')
})
it('should render the footer only when the prop is provided', async () => {
const { rerender } = render(
<CustomDialog show={true}>Content</CustomDialog>,
)
await screen.findByRole('dialog')
expect(screen.queryByText('Footer Content')).not.toBeInTheDocument()
rerender(
<CustomDialog show={true} footer={<div data-testid="footer-node">Footer Content</div>}>
Content
</CustomDialog>,
)
expect(await screen.findByTestId('footer-node')).toBeInTheDocument()
})
it('should call onClose when Escape key is pressed', async () => {
const user = setup()
const onCloseMock = vi.fn()
render(
<CustomDialog show={true} onClose={onCloseMock}>
Content
</CustomDialog>,
)
await screen.findByRole('dialog')
await act(async () => {
await user.keyboard('{Escape}')
})
expect(onCloseMock).toHaveBeenCalledTimes(1)
})
it('should call onClose when the backdrop is clicked', async () => {
const user = setup()
const onCloseMock = vi.fn()
render(
<CustomDialog show={true} onClose={onCloseMock}>
Content
</CustomDialog>,
)
await screen.findByRole('dialog')
const backdrop = document.querySelector('.bg-background-overlay-backdrop')
expect(backdrop).toBeInTheDocument()
await act(async () => {
await user.click(backdrop!)
})
expect(onCloseMock).toHaveBeenCalledTimes(1)
})
it('should apply custom class names to internal elements', async () => {
render(
<CustomDialog
show={true}
title="Title"
className="custom-panel-container"
titleClassName="custom-title-style"
bodyClassName="custom-body-style"
footer="Footer"
footerClassName="custom-footer-style"
>
<div data-testid="content">Content</div>
</CustomDialog>,
)
await screen.findByRole('dialog')
expect(document.querySelector('.custom-panel-container')).toBeInTheDocument()
expect(document.querySelector('.custom-title-style')).toBeInTheDocument()
expect(document.querySelector('.custom-body-style')).toBeInTheDocument()
expect(document.querySelector('.custom-footer-style')).toBeInTheDocument()
})
it('should maintain accessibility attributes (aria-modal)', async () => {
render(
<CustomDialog show={true} title="Accessibility Test">
<button>Focusable Item</button>
</CustomDialog>,
)
const dialog = await screen.findByRole('dialog')
// Headless UI should automatically set aria-modal="true"
expect(dialog).toHaveAttribute('aria-modal', 'true')
})
})

View File

@ -1,152 +0,0 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { useEffect, useState } from 'react'
import Dialog from '.'
const meta = {
title: 'Base/Feedback/Dialog',
component: Dialog,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Modal dialog built on Headless UI. Provides animated overlay, title slot, and optional footer region.',
},
},
},
tags: ['autodocs'],
argTypes: {
className: {
control: 'text',
description: 'Additional classes applied to the panel.',
},
titleClassName: {
control: 'text',
description: 'Extra classes for the title element.',
},
bodyClassName: {
control: 'text',
description: 'Extra classes for the content area.',
},
footerClassName: {
control: 'text',
description: 'Extra classes for the footer container.',
},
title: {
control: 'text',
description: 'Dialog title.',
},
show: {
control: 'boolean',
description: 'Controls visibility of the dialog.',
},
onClose: {
control: false,
description: 'Called when the dialog backdrop or close handler fires.',
},
},
args: {
title: 'Manage API Keys',
show: false,
children: null,
},
} satisfies Meta<typeof Dialog>
export default meta
type Story = StoryObj<typeof meta>
const DialogDemo = (props: React.ComponentProps<typeof Dialog>) => {
const [open, setOpen] = useState(props.show)
useEffect(() => {
setOpen(props.show)
}, [props.show])
return (
<div className="relative flex h-[480px] items-center justify-center bg-gray-100">
<button
className="rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-primary-700"
onClick={() => setOpen(true)}
>
Show dialog
</button>
<Dialog
{...props}
show={open}
onClose={() => {
props.onClose?.()
setOpen(false)
}}
>
<div className="space-y-4 text-sm text-gray-600">
<p>
Centralize API key management for collaborators. You can revoke, rotate, or generate new keys directly from this dialog.
</p>
<div className="rounded-lg border border-dashed border-gray-200 bg-gray-50 p-4 text-xs text-gray-500">
This placeholder area represents a form or table that would live inside the dialog body.
</div>
</div>
</Dialog>
</div>
)
}
export const Default: Story = {
render: args => <DialogDemo {...args} />,
args: {
footer: (
<>
<button className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-50">
Cancel
</button>
<button className="rounded-md bg-primary-600 px-3 py-1.5 text-sm text-white hover:bg-primary-700">
Save changes
</button>
</>
),
},
}
export const WithoutFooter: Story = {
render: args => <DialogDemo {...args} />,
args: {
footer: undefined,
title: 'Read-only summary',
},
parameters: {
docs: {
description: {
story: 'Demonstrates the dialog when no footer actions are provided.',
},
},
},
}
export const CustomStyling: Story = {
render: args => <DialogDemo {...args} />,
args: {
className: 'max-w-[560px] bg-white/95 backdrop-blur-sm',
bodyClassName: 'bg-gray-50 rounded-xl p-5',
footerClassName: 'justify-between px-4 pb-4 pt-4',
titleClassName: 'text-lg text-primary-600',
footer: (
<>
<span className="text-xs text-gray-400">Last synced 2 minutes ago</span>
<div className="flex gap-2">
<button className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-50">
Close
</button>
<button className="rounded-md bg-primary-600 px-3 py-1.5 text-sm text-white hover:bg-primary-700">
Refresh data
</button>
</div>
</>
),
},
parameters: {
docs: {
description: {
story: 'Applies custom classes to the panel, body, title, and footer to match different surfaces.',
},
},
},
}

View File

@ -1,70 +0,0 @@
import type { ElementType, ReactNode } from 'react'
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react'
import { cn } from '@langgenius/dify-ui/cn'
import { Fragment, useCallback } from 'react'
// https://headlessui.com/react/dialog
type DialogProps = {
className?: string
titleClassName?: string
bodyClassName?: string
footerClassName?: string
titleAs?: ElementType
title?: ReactNode
children: ReactNode
footer?: ReactNode
show: boolean
onClose?: () => void
}
const CustomDialog = ({
className,
titleClassName,
bodyClassName,
footerClassName,
titleAs,
title,
children,
footer,
show,
onClose,
}: DialogProps) => {
const close = useCallback(() => onClose?.(), [onClose])
return (
<Transition appear show={show} as={Fragment}>
<Dialog as="div" className="relative z-40" onClose={close}>
<TransitionChild>
<div className={cn('fixed inset-0 bg-background-overlay-backdrop backdrop-blur-[6px]', 'duration-300 ease-in data-closed:opacity-0', 'data-enter:opacity-100', 'data-leave:opacity-0')} />
</TransitionChild>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center">
<TransitionChild>
<DialogPanel className={cn('w-full max-w-[800px] overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-6 shadow-xl transition-all', 'duration-100 ease-in data-closed:scale-95 data-closed:opacity-0', 'data-enter:scale-100 data-enter:opacity-100', 'data-enter:scale-95 data-leave:opacity-0', className)}>
{Boolean(title) && (
<DialogTitle
as={titleAs || 'h3'}
className={cn('pr-8 pb-3 title-2xl-semi-bold text-text-primary', titleClassName)}
>
{title}
</DialogTitle>
)}
<div className={cn(bodyClassName)}>
{children}
</div>
{Boolean(footer) && (
<div className={cn('flex items-center justify-end gap-2 px-6 pt-3 pb-6', footerClassName)}>
{footer}
</div>
)}
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</Transition>
)
}
export default CustomDialog

View File

@ -2,12 +2,11 @@
import type { FC } from 'react' import type { FC } from 'react'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { noop } from 'es-toolkit/function' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import * as React from 'react' import * as React from 'react'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider' import Divider from '@/app/components/base/divider'
import Modal from '@/app/components/base/modal'
import EmojiPickerInner from './Inner' import EmojiPickerInner from './Inner'
type IEmojiPickerProps = { type IEmojiPickerProps = {
@ -21,7 +20,6 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
isModal = true, isModal = true,
onSelect, onSelect,
onClose, onClose,
className,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [selectedEmoji, setSelectedEmoji] = useState('') const [selectedEmoji, setSelectedEmoji] = useState('')
@ -34,39 +32,36 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
return isModal return isModal
? ( ? (
<Modal <Dialog open>
onClose={noop} <DialogContent className={cn('max-h-none w-full overflow-hidden! text-left align-middle', 'flex max-h-[552px] flex-col rounded-xl border-[0.5px] border-divider-subtle p-0 shadow-xl')}>
isShow
closable={false} <EmojiPickerInner
wrapperClassName={className} className="pt-3"
className={cn('flex max-h-[552px] flex-col rounded-xl border-[0.5px] border-divider-subtle p-0 shadow-xl')} onSelect={handleSelectEmoji}
> />
<EmojiPickerInner <Divider className="mt-3 mb-0" />
className="pt-3" <div className="flex w-full items-center justify-center gap-2 p-3">
onSelect={handleSelectEmoji} <Button
/> className="w-full"
<Divider className="mt-3 mb-0" /> onClick={() => {
<div className="flex w-full items-center justify-center gap-2 p-3"> onClose?.()
<Button }}
className="w-full" >
onClick={() => { {t('iconPicker.cancel', { ns: 'app' })}
onClose?.() </Button>
}} <Button
> disabled={selectedEmoji === '' || !selectedBackground}
{t('iconPicker.cancel', { ns: 'app' })} variant="primary"
</Button> className="w-full"
<Button onClick={() => {
disabled={selectedEmoji === '' || !selectedBackground} onSelect?.(selectedEmoji, selectedBackground!)
variant="primary" }}
className="w-full" >
onClick={() => { {t('iconPicker.ok', { ns: 'app' })}
onSelect?.(selectedEmoji, selectedBackground!) </Button>
}} </div>
> </DialogContent>
{t('iconPicker.ok', { ns: 'app' })} </Dialog>
</Button>
</div>
</Modal>
) )
: <></> : <></>
} }

View File

@ -2,11 +2,11 @@
import type { FC } from 'react' import type { FC } from 'react'
import type { AnnotationReplyConfig } from '@/models/debug' import type { AnnotationReplyConfig } from '@/models/debug'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react' import * as React from 'react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector' import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
@ -58,52 +58,61 @@ const ConfigParamModal: FC<Props> = ({ isShow, onHide: doHide, onSave, isInit, a
setLoading(false) setLoading(false)
} }
return ( return (
<Modal isShow={isShow} onClose={onHide} className="!mt-14 !w-[640px] !max-w-none !p-6"> <Dialog
<div className="mb-2 title-2xl-semi-bold text-text-primary"> open={isShow}
{t(`initSetup.${isInit ? 'title' : 'configTitle'}`, { ns: 'appAnnotation' })} onOpenChange={(open) => {
</div> if (!open)
onHide()
}}
>
<DialogContent className="!mt-14 !w-[640px] !max-w-none overflow-hidden! border-none !p-6 text-left align-middle">
<div className="mt-6 space-y-3"> <div className="mb-2 title-2xl-semi-bold text-text-primary">
<Item title={t('feature.annotation.scoreThreshold.title', { ns: 'appDebug' })} tooltip={t('feature.annotation.scoreThreshold.description', { ns: 'appDebug' })}> {t(`initSetup.${isInit ? 'title' : 'configTitle'}`, { ns: 'appAnnotation' })}
<ScoreSlider </div>
className="mt-1"
value={(annotationConfig.score_threshold || ANNOTATION_DEFAULT.score_threshold) * 100}
onChange={(val) => {
setAnnotationConfig({
...annotationConfig,
score_threshold: val / 100,
})
}}
/>
</Item>
<Item title={t('modelProvider.embeddingModel.key', { ns: 'common' })} tooltip={t('embeddingModelSwitchTip', { ns: 'appAnnotation' })}> <div className="mt-6 space-y-3">
<div className="pt-1"> <Item title={t('feature.annotation.scoreThreshold.title', { ns: 'appDebug' })} tooltip={t('feature.annotation.scoreThreshold.description', { ns: 'appDebug' })}>
<ModelSelector <ScoreSlider
defaultModel={embeddingModel && { className="mt-1"
provider: embeddingModel.providerName, value={(annotationConfig.score_threshold || ANNOTATION_DEFAULT.score_threshold) * 100}
model: embeddingModel.modelName, onChange={(val) => {
}} setAnnotationConfig({
modelList={embeddingsModelList} ...annotationConfig,
onSelect={(val) => { score_threshold: val / 100,
setEmbeddingModel({
providerName: val.provider,
modelName: val.model,
}) })
}} }}
/> />
</div> </Item>
</Item>
</div>
<div className="mt-6 flex justify-end gap-2"> <Item title={t('modelProvider.embeddingModel.key', { ns: 'common' })} tooltip={t('embeddingModelSwitchTip', { ns: 'appAnnotation' })}>
<Button onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button> <div className="pt-1">
<Button variant="primary" onClick={handleSave} loading={isLoading}> <ModelSelector
<div></div> defaultModel={embeddingModel && {
<div>{t(`initSetup.${isInit ? 'confirmBtn' : 'configConfirmBtn'}`, { ns: 'appAnnotation' })}</div> provider: embeddingModel.providerName,
</Button> model: embeddingModel.modelName,
</div> }}
</Modal> modelList={embeddingsModelList}
onSelect={(val) => {
setEmbeddingModel({
providerName: val.provider,
modelName: val.model,
})
}}
/>
</div>
</Item>
</div>
<div className="mt-6 flex justify-end gap-2">
<Button onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button variant="primary" onClick={handleSave} loading={isLoading}>
<div></div>
<div>{t(`initSetup.${isInit ? 'confirmBtn' : 'configConfirmBtn'}`, { ns: 'appAnnotation' })}</div>
</Button>
</div>
</DialogContent>
</Dialog>
) )
} }
export default React.memo(ConfigParamModal) export default React.memo(ConfigParamModal)

View File

@ -3,12 +3,11 @@ import type { CodeBasedExtensionItem } from '@/models/common'
import type { ModerationConfig, ModerationContentConfig } from '@/models/debug' import type { ModerationConfig, ModerationContentConfig } from '@/models/debug'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { noop } from 'es-toolkit/function'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider' import Divider from '@/app/components/base/divider'
import Modal from '@/app/components/base/modal'
import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector' import ApiBasedExtensionSelector from '@/app/components/header/account-setting/api-based-extension-page/selector'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { CustomConfigurationStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { CustomConfigurationStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
@ -226,165 +225,164 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
} }
return ( return (
<Modal <Dialog open>
isShow <DialogContent className="mt-14! w-[600px]! max-w-none! overflow-hidden! border-none p-6! text-left align-middle">
onClose={noop}
className="mt-14! w-[600px]! max-w-none! p-6!" <div className="flex items-center justify-between">
> <div className="title-2xl-semi-bold text-text-primary">{t('feature.moderation.modal.title', { ns: 'appDebug' })}</div>
<div className="flex items-center justify-between"> <div
<div className="title-2xl-semi-bold text-text-primary">{t('feature.moderation.modal.title', { ns: 'appDebug' })}</div> role="button"
<div tabIndex={0}
role="button" className="cursor-pointer p-1"
tabIndex={0} onClick={onCancel}
className="cursor-pointer p-1" onKeyDown={(e) => {
onClick={onCancel} if (e.key === 'Enter' || e.key === ' ') {
onKeyDown={(e) => { e.preventDefault()
if (e.key === 'Enter' || e.key === ' ') { onCancel()
e.preventDefault() }
onCancel() }}
} >
}} <span className="i-ri-close-line h-4 w-4 text-text-tertiary" />
> </div>
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" />
</div> </div>
</div> <div className="py-2">
<div className="py-2"> <div className="text-sm leading-9 font-medium text-text-primary">
<div className="text-sm leading-9 font-medium text-text-primary"> {t('feature.moderation.modal.provider.title', { ns: 'appDebug' })}
{t('feature.moderation.modal.provider.title', { ns: 'appDebug' })} </div>
</div> <div className="grid grid-cols-3 gap-2.5">
<div className="grid grid-cols-3 gap-2.5"> {
{ providers.map(provider => (
providers.map(provider => ( <div
<div key={provider.key}
key={provider.key} className={cn(
className={cn( 'flex h-8 cursor-default items-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 system-sm-regular text-text-secondary',
'flex h-8 cursor-default items-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 system-sm-regular text-text-secondary', localeData.type !== provider.key && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
localeData.type !== provider.key && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs', localeData.type === provider.key && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg system-sm-medium shadow-xs',
localeData.type === provider.key && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg system-sm-medium shadow-xs', localeData.type === 'openai_moderation' && provider.key === 'openai_moderation' && !isOpenAIProviderConfigured && 'text-text-disabled',
localeData.type === 'openai_moderation' && provider.key === 'openai_moderation' && !isOpenAIProviderConfigured && 'text-text-disabled', )}
)} onClick={() => handleDataTypeChange(provider.key)}
onClick={() => handleDataTypeChange(provider.key)}
>
<div className={cn(
'mr-2 h-4 w-4 rounded-full border border-components-radio-border bg-components-radio-bg shadow-xs',
localeData.type === provider.key && 'border-[5px] border-components-radio-border-checked',
)}
> >
<div className={cn(
'mr-2 h-4 w-4 rounded-full border border-components-radio-border bg-components-radio-bg shadow-xs',
localeData.type === provider.key && 'border-[5px] border-components-radio-border-checked',
)}
>
</div>
{provider.name}
</div>
))
}
</div>
{
!isLoading && !isOpenAIProviderConfigured && localeData.type === 'openai_moderation' && (
<div className="mt-2 flex items-center rounded-lg border border-[#FEF0C7] bg-[#FFFAEB] px-3 py-2">
<span className="mr-1 i-custom-vender-line-general-info-circle h-4 w-4 text-[#F79009]" />
<div className="flex items-center text-xs font-medium text-gray-700">
{t('feature.moderation.modal.openaiNotConfig.before', { ns: 'appDebug' })}
<span
className="cursor-pointer text-primary-600"
onClick={handleOpenSettingsModal}
>
&nbsp;
{t('settings.provider', { ns: 'common' })}
&nbsp;
</span>
{t('feature.moderation.modal.openaiNotConfig.after', { ns: 'appDebug' })}
</div> </div>
{provider.name}
</div> </div>
)) )
} }
</div> </div>
{ {
!isLoading && !isOpenAIProviderConfigured && localeData.type === 'openai_moderation' && ( localeData.type === 'keywords' && (
<div className="mt-2 flex items-center rounded-lg border border-[#FEF0C7] bg-[#FFFAEB] px-3 py-2"> <div className="py-2">
<span className="mr-1 i-custom-vender-line-general-info-circle h-4 w-4 text-[#F79009]" /> <div className="mb-1 text-sm font-medium text-text-primary">{t('feature.moderation.modal.provider.keywords', { ns: 'appDebug' })}</div>
<div className="flex items-center text-xs font-medium text-gray-700"> <div className="mb-2 text-xs text-text-tertiary">{t('feature.moderation.modal.keywords.tip', { ns: 'appDebug' })}</div>
{t('feature.moderation.modal.openaiNotConfig.before', { ns: 'appDebug' })} <div className="relative h-[88px] rounded-lg bg-components-input-bg-normal px-3 py-2">
<span <textarea
className="cursor-pointer text-primary-600" value={localeData.config?.keywords || ''}
onClick={handleOpenSettingsModal} onChange={handleDataKeywordsChange}
> className="block h-full w-full resize-none appearance-none bg-transparent text-sm text-text-secondary outline-hidden"
&nbsp; placeholder={t('feature.moderation.modal.keywords.placeholder', { ns: 'appDebug' }) || ''}
{t('settings.provider', { ns: 'common' })} />
&nbsp; <div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 text-xs font-medium text-text-quaternary">
</span> <span>{(localeData.config?.keywords || '').split('\n').filter(Boolean).length}</span>
{t('feature.moderation.modal.openaiNotConfig.after', { ns: 'appDebug' })} /
<span className="text-text-tertiary">
100
{t('feature.moderation.modal.keywords.line', { ns: 'appDebug' })}
</span>
</div>
</div> </div>
</div> </div>
) )
} }
</div> {
{ localeData.type === 'api' && (
localeData.type === 'keywords' && ( <div className="py-2">
<div className="py-2"> <div className="flex h-9 items-center justify-between">
<div className="mb-1 text-sm font-medium text-text-primary">{t('feature.moderation.modal.provider.keywords', { ns: 'appDebug' })}</div> <div className="text-sm font-medium text-text-primary">{t('apiBasedExtension.selector.title', { ns: 'common' })}</div>
<div className="mb-2 text-xs text-text-tertiary">{t('feature.moderation.modal.keywords.tip', { ns: 'appDebug' })}</div> <a
<div className="relative h-[88px] rounded-lg bg-components-input-bg-normal px-3 py-2"> href={docLink('/use-dify/workspace/api-extension/api-extension')}
<textarea target="_blank"
value={localeData.config?.keywords || ''} rel="noopener noreferrer"
onChange={handleDataKeywordsChange} className="group flex items-center text-xs text-text-tertiary hover:text-primary-600"
className="block h-full w-full resize-none appearance-none bg-transparent text-sm text-text-secondary outline-hidden" >
placeholder={t('feature.moderation.modal.keywords.placeholder', { ns: 'appDebug' }) || ''} <span className="mr-1 i-custom-vender-line-education-book-open-01 h-3 w-3 text-text-tertiary group-hover:text-primary-600" />
/> {t('apiBasedExtension.link', { ns: 'common' })}
<div className="absolute right-2 bottom-2 flex h-5 items-center rounded-md bg-background-section px-1 text-xs font-medium text-text-quaternary"> </a>
<span>{(localeData.config?.keywords || '').split('\n').filter(Boolean).length}</span>
/
<span className="text-text-tertiary">
100
{t('feature.moderation.modal.keywords.line', { ns: 'appDebug' })}
</span>
</div> </div>
<ApiBasedExtensionSelector
value={localeData.config?.api_based_extension_id || ''}
onChange={handleDataApiBasedChange}
/>
</div> </div>
</div> )
) }
} {
{ systemTypes.findIndex(t => t === localeData.type) < 0
localeData.type === 'api' && ( && currentProvider?.form_schema
<div className="py-2"> && (
<div className="flex h-9 items-center justify-between"> <FormGeneration
<div className="text-sm font-medium text-text-primary">{t('apiBasedExtension.selector.title', { ns: 'common' })}</div> forms={currentProvider?.form_schema}
<a value={localeData.config}
href={docLink('/use-dify/workspace/api-extension/api-extension')} onChange={handleDataExtraChange}
target="_blank"
rel="noopener noreferrer"
className="group flex items-center text-xs text-text-tertiary hover:text-primary-600"
>
<span className="mr-1 i-custom-vender-line-education-book-open-01 h-3 w-3 text-text-tertiary group-hover:text-primary-600" />
{t('apiBasedExtension.link', { ns: 'common' })}
</a>
</div>
<ApiBasedExtensionSelector
value={localeData.config?.api_based_extension_id || ''}
onChange={handleDataApiBasedChange}
/> />
</div> )
) }
} <Divider bgStyle="gradient" className="my-3 h-px" />
{ <ModerationContent
systemTypes.findIndex(t => t === localeData.type) < 0 title={t('feature.moderation.modal.content.input', { ns: 'appDebug' }) || ''}
&& currentProvider?.form_schema config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }}
&& ( onConfigChange={config => handleDataContentChange('inputs_config', config)}
<FormGeneration info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
forms={currentProvider?.form_schema} showPreset={localeData.type !== 'api'}
value={localeData.config} />
onChange={handleDataExtraChange} <ModerationContent
/> title={t('feature.moderation.modal.content.output', { ns: 'appDebug' }) || ''}
) config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }}
} onConfigChange={config => handleDataContentChange('outputs_config', config)}
<Divider bgStyle="gradient" className="my-3 h-px" /> info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
<ModerationContent showPreset={localeData.type !== 'api'}
title={t('feature.moderation.modal.content.input', { ns: 'appDebug' }) || ''} />
config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }} <div className="mt-1 mb-8 text-xs font-medium text-text-tertiary">{t('feature.moderation.modal.content.condition', { ns: 'appDebug' })}</div>
onConfigChange={config => handleDataContentChange('inputs_config', config)} <div className="flex items-center justify-end">
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''} <Button
showPreset={localeData.type !== 'api'} onClick={onCancel}
/> className="mr-2"
<ModerationContent >
title={t('feature.moderation.modal.content.output', { ns: 'appDebug' }) || ''} {t('operation.cancel', { ns: 'common' })}
config={localeData.config?.outputs_config || { enabled: false, preset_response: '' }} </Button>
onConfigChange={config => handleDataContentChange('outputs_config', config)} <Button
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''} variant="primary"
showPreset={localeData.type !== 'api'} onClick={handleSave}
/> disabled={localeData.type === 'openai_moderation' && !isOpenAIProviderConfigured}
<div className="mt-1 mb-8 text-xs font-medium text-text-tertiary">{t('feature.moderation.modal.content.condition', { ns: 'appDebug' })}</div> >
<div className="flex items-center justify-end"> {t('operation.save', { ns: 'common' })}
<Button </Button>
onClick={onCancel} </div>
className="mr-2" </DialogContent>
> </Dialog>
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button
variant="primary"
onClick={handleSave}
disabled={localeData.type === 'openai_moderation' && !isOpenAIProviderConfigured}
>
{t('operation.save', { ns: 'common' })}
</Button>
</div>
</Modal>
) )
} }

View File

@ -1,172 +0,0 @@
import { act, fireEvent, render, screen } from '@testing-library/react'
import Modal from '..'
describe('Modal', () => {
describe('Render', () => {
it('should not render content when isShow is false', () => {
render(
<Modal isShow={false} title="Test Modal">
<div>Modal Content</div>
</Modal>,
)
expect(screen.queryByText('Test Modal')).not.toBeInTheDocument()
expect(screen.queryByText('Modal Content')).not.toBeInTheDocument()
})
it('should render content when isShow is true', async () => {
await act(async () => {
render(
<Modal isShow={true} title="Test Modal">
<div>Modal Content</div>
</Modal>,
)
})
expect(screen.getByText('Test Modal')).toBeInTheDocument()
expect(screen.getByText('Modal Content')).toBeInTheDocument()
})
it('should render description when provided', async () => {
await act(async () => {
render(
<Modal isShow={true} title="Test Modal" description="Test Description">
<div>Content</div>
</Modal>,
)
})
expect(screen.getByText('Test Description')).toBeInTheDocument()
})
})
describe('Interaction', () => {
it('should call onClose when close button is clicked', async () => {
const handleClose = vi.fn()
await act(async () => {
render(
<Modal isShow={true} title="Test Modal" closable={true} onClose={handleClose}>
<div>Content</div>
</Modal>,
)
})
const closeButton = screen.getByTestId('modal-close-button')
expect(closeButton).toBeInTheDocument()
await act(async () => {
fireEvent.click(closeButton!)
})
expect(handleClose).toHaveBeenCalledTimes(1)
})
it('should prevent propagation when clicking the scrollable container', async () => {
await act(async () => {
render(
<Modal isShow={true} title="Test Modal">
<div>Content</div>
</Modal>,
)
})
const wrapper = document.querySelector('.overflow-y-auto')
expect(wrapper).toBeInTheDocument()
const event = new MouseEvent('click', { bubbles: true, cancelable: true })
const stopPropagationSpy = vi.spyOn(event, 'stopPropagation')
const preventDefaultSpy = vi.spyOn(event, 'preventDefault')
await act(async () => {
wrapper!.dispatchEvent(event)
})
expect(stopPropagationSpy).toHaveBeenCalled()
expect(preventDefaultSpy).toHaveBeenCalled()
})
it('should handle clickOutsideNotClose prop', async () => {
const handleClose = vi.fn()
await act(async () => {
render(
<Modal isShow={true} title="Test Modal" clickOutsideNotClose={true} onClose={handleClose}>
<div>Content</div>
</Modal>,
)
})
await act(async () => {
fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape', code: 'Escape' })
})
expect(handleClose).not.toHaveBeenCalled()
})
})
describe('Props', () => {
it('should apply custom className to the panel', async () => {
await act(async () => {
render(
<Modal isShow={true} title="Test Modal" className="custom-panel-class">
<div>Content</div>
</Modal>,
)
})
const panel = screen.getByText('Test Modal').parentElement
expect(panel).toHaveClass('custom-panel-class')
})
it('should apply wrapperClassName and containerClassName', async () => {
await act(async () => {
render(
<Modal
isShow={true}
title="Test Modal"
wrapperClassName="custom-wrapper"
containerClassName="custom-container"
>
<div>Content</div>
</Modal>,
)
})
const dialog = document.querySelector('.custom-wrapper')
expect(dialog).toBeInTheDocument()
const container = document.querySelector('.custom-container')
expect(container).toBeInTheDocument()
})
it('should apply overlayOpacity background when overlayOpacity is true', async () => {
await act(async () => {
render(
<Modal isShow={true} title="Test Modal" overlayOpacity={true}>
<div>Content</div>
</Modal>,
)
})
const overlay = document.querySelector('.bg-workflow-canvas-canvas-overlay')
expect(overlay).toBeInTheDocument()
})
it('should toggle overflow-visible class based on overflowVisible prop', async () => {
const { rerender } = render(
<Modal isShow={true} title="Test Modal" overflowVisible={true}>
<div>Content</div>
</Modal>,
)
let panel = screen.getByText('Test Modal').parentElement
expect(panel).toHaveClass('overflow-visible')
await act(async () => {
rerender(
<Modal isShow={true} title="Test Modal" overflowVisible={false}>
<div>Content</div>
</Modal>,
)
})
panel = screen.getByText('Test Modal').parentElement
expect(panel).toHaveClass('overflow-hidden')
})
})
})

View File

@ -1,128 +0,0 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { useEffect, useState } from 'react'
import Modal from '.'
const meta = {
title: 'Base/Feedback/Modal',
component: Modal,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Lightweight modal wrapper with optional header/description and close icon.',
},
},
},
tags: ['autodocs'],
argTypes: {
className: {
control: 'text',
description: 'Extra classes applied to the modal panel.',
},
wrapperClassName: {
control: 'text',
description: 'Additional wrapper classes for the dialog.',
},
isShow: {
control: 'boolean',
description: 'Controls whether the modal is visible.',
},
title: {
control: 'text',
description: 'Heading displayed at the top of the modal.',
},
description: {
control: 'text',
description: 'Secondary text beneath the title.',
},
closable: {
control: 'boolean',
description: 'Whether the close icon should be shown.',
},
overflowVisible: {
control: 'boolean',
description: 'Allows content to overflow the modal panel.',
},
onClose: {
control: false,
description: 'Callback invoked when the modal requests to close.',
},
},
args: {
isShow: false,
title: 'Create new API key',
description: 'Generate a scoped key for this workspace. You can revoke it at any time.',
closable: true,
},
} satisfies Meta<typeof Modal>
export default meta
type Story = StoryObj<typeof meta>
const ModalDemo = (props: React.ComponentProps<typeof Modal>) => {
const [open, setOpen] = useState(props.isShow)
useEffect(() => {
setOpen(props.isShow)
}, [props.isShow])
return (
<div className="relative flex h-[480px] items-center justify-center bg-gray-100">
<button
className="rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-primary-700"
onClick={() => setOpen(true)}
>
Show modal
</button>
<Modal
{...props}
isShow={open}
onClose={() => {
props.onClose?.()
setOpen(false)
}}
>
<div className="mt-6 space-y-4 text-sm text-gray-600">
<p>
Provide a descriptive name for this key so collaborators know its purpose. Restrict usage with scopes to limit access.
</p>
<div className="rounded-lg border border-dashed border-gray-200 bg-gray-50 p-4 text-xs text-gray-500">
Form fields and validation messaging would appear here. This placeholder keeps the story lightweight.
</div>
</div>
<div className="mt-8 flex justify-end gap-3">
<button
className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-50"
onClick={() => setOpen(false)}
>
Cancel
</button>
<button className="rounded-md bg-primary-600 px-3 py-1.5 text-sm text-white hover:bg-primary-700">
Create key
</button>
</div>
</Modal>
</div>
)
}
export const Default: Story = {
render: args => <ModalDemo {...args} />,
}
export const OverflowVisible: Story = {
render: args => <ModalDemo {...args} />,
args: {
overflowVisible: true,
description: 'Demonstrates the modal configured to let the body content overflow.',
className: 'max-w-[540px]',
},
parameters: {
docs: {
description: {
story: 'Shows the modal with `overflowVisible` enabled for content that needs to escape the panel bounds.',
},
},
},
}

View File

@ -1,93 +0,0 @@
/**
* @deprecated Use `@langgenius/dify-ui/dialog` instead.
* This component will be removed after migration is complete.
* See: https://github.com/langgenius/dify/issues/32767
*/
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react'
import { cn } from '@langgenius/dify-ui/cn'
import { noop } from 'es-toolkit/function'
import { Fragment } from 'react'
// https://headlessui.com/react/dialog
type IModal = {
className?: string
wrapperClassName?: string
containerClassName?: string
isShow: boolean
onClose?: () => void
title?: React.ReactNode
description?: React.ReactNode
children?: React.ReactNode
closable?: boolean
overflowVisible?: boolean
overlayOpacity?: boolean // For semi-transparent overlay instead of default
clickOutsideNotClose?: boolean // Prevent closing when clicking outside modal
}
export default function Modal({
className,
wrapperClassName,
containerClassName,
isShow,
onClose = noop,
title,
description,
children,
closable = false,
overflowVisible = false,
overlayOpacity = false,
clickOutsideNotClose = false,
}: IModal) {
return (
<Transition appear show={isShow} as={Fragment}>
<Dialog as="div" className={cn('relative z-60', wrapperClassName)} onClose={clickOutsideNotClose ? noop : onClose}>
<TransitionChild>
<div className={cn('fixed inset-0', overlayOpacity ? 'bg-workflow-canvas-canvas-overlay' : 'bg-background-overlay', 'duration-300 ease-in data-closed:opacity-0', 'data-enter:opacity-100', 'data-leave:opacity-0')} />
</TransitionChild>
<div
className="fixed inset-0 overflow-y-auto"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
}}
>
<div className={cn('flex min-h-full items-center justify-center p-4 text-center', containerClassName)}>
<TransitionChild>
<DialogPanel className={cn('relative w-full max-w-[480px] rounded-2xl bg-components-panel-bg p-6 text-left align-middle shadow-xl transition-all', overflowVisible ? 'overflow-visible' : 'overflow-hidden', 'duration-100 ease-in data-closed:scale-95 data-closed:opacity-0', 'data-enter:scale-100 data-enter:opacity-100', 'data-enter:scale-95 data-leave:opacity-0', className)}>
{!!title && (
<DialogTitle
as="h3"
className="title-2xl-semi-bold text-text-primary"
>
{title}
</DialogTitle>
)}
{!!description && (
<div className="mt-2 body-md-regular text-text-secondary">
{description}
</div>
)}
{closable
&& (
<div className="absolute top-6 right-6 z-10 flex h-5 w-5 items-center justify-center rounded-2xl hover:cursor-pointer hover:bg-state-base-hover">
<span
className="i-ri-close-line h-4 w-4 text-text-tertiary"
onClick={
(e) => {
e.stopPropagation()
onClose()
}
}
data-testid="modal-close-button"
/>
</div>
)}
{children}
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</Transition>
)
}

View File

@ -4,6 +4,7 @@ import type { WorkflowNodesMap } from '../workflow-variable-block/node'
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types' import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
import type { Type } from '@/app/components/workflow/nodes/llm/types' import type { Type } from '@/app/components/workflow/nodes/llm/types'
import type { ValueSelector, Var } from '@/app/components/workflow/types' import type { ValueSelector, Var } from '@/app/components/workflow/types'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import * as React from 'react' import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react' import { useCallback, useEffect, useMemo, useRef } from 'react'
@ -11,7 +12,6 @@ import { useTranslation } from 'react-i18next'
import { InputVarType } from '@/app/components/workflow/types' import { InputVarType } from '@/app/components/workflow/types'
import ActionButton from '../../../action-button' import ActionButton from '../../../action-button'
import { VariableX } from '../../../icons/src/vender/workflow' import { VariableX } from '../../../icons/src/vender/workflow'
import Modal from '../../../modal'
import InputField from './input-field' import InputField from './input-field'
import VariableBlock from './variable-block' import VariableBlock from './variable-block'
@ -157,20 +157,24 @@ const HITLInputComponentUI: FC<HITLInputComponentUIProps> = ({
</div> </div>
{isShowEditModal && ( {isShowEditModal && (
<Modal <Dialog
isShow open
onClose={hideEditModal} onOpenChange={(open) => {
wrapperClassName="z-999" if (!open)
className="max-w-[372px] p-0!" hideEditModal()
}}
> >
<InputField <DialogContent className="w-full max-w-[372px] overflow-hidden! border-none p-0! text-left align-middle">
nodeId={nodeId}
isEdit <InputField
payload={formInput} nodeId={nodeId}
onChange={handleChange} isEdit
onCancel={hideEditModal} payload={formInput}
/> onChange={handleChange}
</Modal> onCancel={hideEditModal}
/>
</DialogContent>
</Dialog>
)} )}
</div> </div>
) )

View File

@ -1,4 +1,4 @@
import type { LexicalCommand } from 'lexical' import type { ShortcutPopupInsertHandler } from '../index'
import { LexicalComposer } from '@lexical/react/LexicalComposer' import { LexicalComposer } from '@lexical/react/LexicalComposer'
import { ContentEditable } from '@lexical/react/LexicalContentEditable' import { ContentEditable } from '@lexical/react/LexicalContentEditable'
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary' import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'
@ -50,7 +50,7 @@ const CONTENT_EDITABLE_ID = 'ce'
type MinimalEditorProps = { type MinimalEditorProps = {
withContainer?: boolean withContainer?: boolean
hotkey?: string | string[] | string[][] | ((e: KeyboardEvent) => boolean) hotkey?: string | string[] | string[][] | ((e: KeyboardEvent) => boolean)
children?: React.ReactNode | ((close: () => void, onInsert: (command: LexicalCommand<unknown>, params: unknown[]) => void) => React.ReactNode) children?: React.ReactNode | ((close: () => void, onInsert: ShortcutPopupInsertHandler) => React.ReactNode)
className?: string className?: string
onOpen?: () => void onOpen?: () => void
onClose?: () => void onClose?: () => void
@ -316,7 +316,7 @@ describe('ShortcutsPopupPlugin', () => {
it('renders children as render function and provides close/onInsert', async () => { it('renders children as render function and provides close/onInsert', async () => {
const TEST_COMMAND = createCommand<unknown>('TEST_COMMAND') const TEST_COMMAND = createCommand<unknown>('TEST_COMMAND')
const childrenFn = vi.fn((close: () => void, onInsert: (cmd: LexicalCommand<unknown>, params: unknown[]) => void) => ( const childrenFn = vi.fn((close: () => void, onInsert: ShortcutPopupInsertHandler) => (
<div> <div>
<button type="button" data-testid="close-btn" onClick={close}>Close</button> <button type="button" data-testid="close-btn" onClick={close}>Close</button>
<button type="button" data-testid="insert-btn" onClick={() => onInsert(TEST_COMMAND, ['param1'])}>Insert</button> <button type="button" data-testid="insert-btn" onClick={() => onInsert(TEST_COMMAND, ['param1'])}>Insert</button>
@ -346,7 +346,7 @@ describe('ShortcutsPopupPlugin', () => {
const TEST_COMMAND = createCommand<unknown>('TEST_INSERT_COMMAND') const TEST_COMMAND = createCommand<unknown>('TEST_INSERT_COMMAND')
render( render(
<MinimalEditor> <MinimalEditor>
{(close: () => void, onInsert: (cmd: LexicalCommand<unknown>, params: unknown[]) => void) => ( {(close: () => void, onInsert: ShortcutPopupInsertHandler) => (
<div> <div>
<button type="button" data-testid="insert-btn" onClick={() => onInsert(TEST_COMMAND, ['value'])}>Insert</button> <button type="button" data-testid="insert-btn" onClick={() => onInsert(TEST_COMMAND, ['value'])}>Insert</button>
</div> </div>

View File

@ -23,6 +23,7 @@ import {
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
export const SHORTCUTS_EMPTY_CONTENT = 'shortcuts_empty_content' export const SHORTCUTS_EMPTY_CONTENT = 'shortcuts_empty_content'
export type ShortcutPopupInsertHandler = <Payload>(command: LexicalCommand<Payload>, params: Payload) => void
// Hotkey can be: // Hotkey can be:
// - string: 'mod+/' // - string: 'mod+/'
@ -33,7 +34,7 @@ export type Hotkey = string | string[] | string[][] | ((e: KeyboardEvent) => boo
type ShortcutPopupPluginProps = { type ShortcutPopupPluginProps = {
hotkey?: Hotkey hotkey?: Hotkey
children?: React.ReactNode | ((close: () => void, onInsert: (command: LexicalCommand<unknown>, params: any[]) => void) => React.ReactNode) children?: React.ReactNode | ((close: () => void, onInsert: ShortcutPopupInsertHandler) => React.ReactNode)
className?: string className?: string
container?: Element | null container?: Element | null
onOpen?: () => void onOpen?: () => void
@ -158,8 +159,9 @@ export default function ShortcutsPopupPlugin({
apply({ availableWidth, availableHeight, elements }) { apply({ availableWidth, availableHeight, elements }) {
Object.assign(elements.floating.style, { Object.assign(elements.floating.style, {
maxWidth: `${Math.min(400, availableWidth)}px`, maxWidth: `${Math.min(400, availableWidth)}px`,
maxHeight: `${Math.min(300, availableHeight)}px`, maxHeight: `${Math.max(0, availableHeight)}px`,
overflow: 'auto', overflowX: 'hidden',
overflowY: 'auto',
}) })
}, },
padding: 8, padding: 8,
@ -236,7 +238,7 @@ export default function ShortcutsPopupPlugin({
setOpen(true) setOpen(true)
onOpen?.() onOpen?.()
}, [onOpen]) }, [editor, onOpen, refs])
const closePortal = useCallback(() => { const closePortal = useCallback(() => {
setOpen(false) setOpen(false)
@ -280,7 +282,7 @@ export default function ShortcutsPopupPlugin({
return () => document.removeEventListener('mousedown', onMouseDown, false) return () => document.removeEventListener('mousedown', onMouseDown, false)
}, [open, closePortal]) }, [open, closePortal])
const handleInsert = useCallback((command: LexicalCommand<unknown>, params: any) => { const handleInsert = useCallback(<Payload,>(command: LexicalCommand<Payload>, params: Payload) => {
editor.dispatchCommand(command, params) editor.dispatchCommand(command, params)
closePortal() closePortal()
}, [editor, closePortal]) }, [editor, closePortal])

View File

@ -29,26 +29,26 @@ type ModalSnapshot = {
className?: string className?: string
} }
let mockModalProps: ModalSnapshot | null = null let mockModalProps: ModalSnapshot | null = null
vi.mock('../../../base/modal', () => ({ let mockOnOpenChange: ((open: boolean) => void) | undefined
default: ({ isShow, children, onClose, closable, className }: { isShow: boolean, children: React.ReactNode, onClose: () => void, closable?: boolean, className?: string }) => { vi.mock('@langgenius/dify-ui/dialog', () => ({
Dialog: ({ open, onOpenChange, children }: { open?: boolean, onOpenChange?: (open: boolean) => void, children: React.ReactNode }) => {
mockOnOpenChange = onOpenChange
mockModalProps = { isShow: open !== false }
return open === false ? null : <>{children}</>
},
DialogContent: ({ children, className }: { children: React.ReactNode, className?: string }) => {
mockModalProps = { mockModalProps = {
isShow, isShow: true,
closable, closable: true,
className, className,
} }
if (!isShow)
return null
return ( return (
<div data-testid="annotation-full-modal" data-classname={className ?? ''}> <div data-testid="annotation-full-modal" data-classname={className ?? ''}>
{closable && (
<button type="button" data-testid="mock-modal-close" onClick={onClose}>
close
</button>
)}
{children} {children}
</div> </div>
) )
}, },
DialogCloseButton: () => <button type="button" data-testid="mock-modal-close" onClick={() => mockOnOpenChange?.(false)}>close</button>,
})) }))
describe('AnnotationFullModal', () => { describe('AnnotationFullModal', () => {
@ -56,6 +56,7 @@ describe('AnnotationFullModal', () => {
vi.clearAllMocks() vi.clearAllMocks()
mockUpgradeBtnProps = null mockUpgradeBtnProps = null
mockModalProps = null mockModalProps = null
mockOnOpenChange = undefined
}) })
// Rendering marketing copy inside modal // Rendering marketing copy inside modal
@ -71,8 +72,9 @@ describe('AnnotationFullModal', () => {
expect(mockModalProps).toEqual(expect.objectContaining({ expect(mockModalProps).toEqual(expect.objectContaining({
isShow: true, isShow: true,
closable: true, closable: true,
className: 'p-0!', className: expect.stringContaining('p-0!'),
})) }))
expect(mockModalProps?.className).toContain('w-full')
}) })
}) })

View File

@ -1,10 +1,10 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import * as React from 'react' import * as React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import GridMask from '@/app/components/base/grid-mask' import GridMask from '@/app/components/base/grid-mask'
import Modal from '../../base/modal'
import UpgradeBtn from '../upgrade-btn' import UpgradeBtn from '../upgrade-btn'
import s from './style.module.css' import s from './style.module.css'
import Usage from './usage' import Usage from './usage'
@ -20,28 +20,33 @@ const AnnotationFullModal: FC<Props> = ({
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Modal <Dialog
isShow={show} open={show}
onClose={onHide} onOpenChange={(open) => {
closable if (!open)
className="p-0!" onHide()
}}
> >
<GridMask wrapperClassName="rounded-lg" canvasClassName="rounded-lg" gradientClassName="rounded-lg"> <DialogContent className="w-full overflow-hidden! border-none p-0! text-left align-middle">
<div className="mt-6 flex cursor-pointer flex-col rounded-lg border-2 border-solid border-transparent px-7 py-6 shadow-md transition-all duration-200 ease-in-out"> <DialogCloseButton data-testid="modal-close-button" />
<div className="flex items-center justify-between">
<div className={cn(s.textGradient, 'text-[18px] leading-[27px] font-semibold')}>
<div>{t('annotatedResponse.fullTipLine1', { ns: 'billing' })}</div>
<div>{t('annotatedResponse.fullTipLine2', { ns: 'billing' })}</div>
</div>
<GridMask wrapperClassName="rounded-lg" canvasClassName="rounded-lg" gradientClassName="rounded-lg">
<div className="mt-6 flex cursor-pointer flex-col rounded-lg border-2 border-solid border-transparent px-7 py-6 shadow-md transition-all duration-200 ease-in-out">
<div className="flex items-center justify-between">
<div className={cn(s.textGradient, 'text-[18px] leading-[27px] font-semibold')}>
<div>{t('annotatedResponse.fullTipLine1', { ns: 'billing' })}</div>
<div>{t('annotatedResponse.fullTipLine2', { ns: 'billing' })}</div>
</div>
</div>
<Usage className="mt-4" />
<div className="mt-7 flex justify-end">
<UpgradeBtn loc="annotation-create" />
</div>
</div> </div>
<Usage className="mt-4" /> </GridMask>
<div className="mt-7 flex justify-end"> </DialogContent>
<UpgradeBtn loc="annotation-create" /> </Dialog>
</div>
</div>
</GridMask>
</Modal>
) )
} }
export default React.memo(AnnotationFullModal) export default React.memo(AnnotationFullModal)

View File

@ -1,6 +1,13 @@
import { Button } from '@langgenius/dify-ui/button' import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
type DSLConfirmModalProps = { type DSLConfirmModalProps = {
versions?: { versions?: {
@ -20,32 +27,36 @@ const DSLConfirmModal = ({
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Modal <AlertDialog
isShow open
onClose={() => onCancel()} onOpenChange={(open) => {
className="w-[480px]" if (!open)
onCancel()
}}
> >
<div className="flex flex-col items-start gap-2 self-stretch pb-4"> <AlertDialogContent className="w-[480px] overflow-hidden! border-none text-left align-middle shadow-xl">
<div className="title-2xl-semi-bold text-text-primary">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</div> <div className="flex flex-col items-start gap-2 self-stretch p-6 pb-4">
<div className="flex grow flex-col system-md-regular text-text-secondary"> <AlertDialogTitle className="title-2xl-semi-bold text-text-primary">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</AlertDialogTitle>
<div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div> <AlertDialogDescription render={<div />} className="flex grow flex-col system-md-regular text-text-secondary">
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div> <div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
<br /> <div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
<div> <br />
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })} <div>
<span className="system-md-medium">{versions.importedVersion}</span> {t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
</div> <span className="system-md-medium">{versions.importedVersion}</span>
<div> </div>
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })} <div>
<span className="system-md-medium">{versions.systemVersion}</span> {t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
</div> <span className="system-md-medium">{versions.systemVersion}</span>
</div>
</AlertDialogDescription>
</div> </div>
</div> <AlertDialogActions>
<div className="flex items-start justify-end gap-2 self-stretch pt-6"> <AlertDialogCancelButton variant="secondary">{t('newApp.Cancel', { ns: 'app' })}</AlertDialogCancelButton>
<Button variant="secondary" onClick={() => onCancel()}>{t('newApp.Cancel', { ns: 'app' })}</Button> <AlertDialogConfirmButton onClick={onConfirm} disabled={confirmDisabled}>{t('newApp.Confirm', { ns: 'app' })}</AlertDialogConfirmButton>
<Button variant="primary" tone="destructive" onClick={onConfirm} disabled={confirmDisabled}>{t('newApp.Confirm', { ns: 'app' })}</Button> </AlertDialogActions>
</div> </AlertDialogContent>
</Modal> </AlertDialog>
) )
} }

View File

@ -1,10 +1,9 @@
'use client' 'use client'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { useKeyPress } from 'ahooks' import { useKeyPress } from 'ahooks'
import { noop } from 'es-toolkit/function'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import DSLConfirmModal from './dsl-confirm-modal' import DSLConfirmModal from './dsl-confirm-modal'
import Header from './header' import Header from './header'
import { CreateFromDSLModalTab, useDSLImport } from './hooks/use-dsl-import' import { CreateFromDSLModalTab, useDSLImport } from './hooks/use-dsl-import'
@ -58,51 +57,50 @@ const CreateFromDSLModal = ({
return ( return (
<> <>
<Modal <Dialog open={show}>
className="w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0 shadow-xl" <DialogContent className="w-full max-w-[480px]! overflow-hidden! rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0! text-left align-middle shadow-xl">
isShow={show}
onClose={noop} <Header onClose={onClose} />
> <Tab
<Header onClose={onClose} /> currentTab={currentTab}
<Tab setCurrentTab={setCurrentTab}
currentTab={currentTab} />
setCurrentTab={setCurrentTab} <div className="px-6 py-4">
/> {currentTab === CreateFromDSLModalTab.FROM_FILE && (
<div className="px-6 py-4"> <Uploader
{currentTab === CreateFromDSLModalTab.FROM_FILE && ( className="mt-0"
<Uploader file={currentFile}
className="mt-0" updateFile={handleFile}
file={currentFile}
updateFile={handleFile}
/>
)}
{currentTab === CreateFromDSLModalTab.FROM_URL && (
<div>
<div className="leading6 mb-1 system-md-semibold text-text-secondary">
DSL URL
</div>
<Input
placeholder={t('importFromDSLUrlPlaceholder', { ns: 'app' }) || ''}
value={dslUrlValue}
onChange={e => setDslUrlValue(e.target.value)}
/> />
</div> )}
)} {currentTab === CreateFromDSLModalTab.FROM_URL && (
</div> <div>
<div className="flex justify-end gap-x-2 p-6 pt-5"> <div className="leading6 mb-1 system-md-semibold text-text-secondary">
<Button onClick={onClose}> DSL URL
{t('newApp.Cancel', { ns: 'app' })} </div>
</Button> <Input
<Button placeholder={t('importFromDSLUrlPlaceholder', { ns: 'app' }) || ''}
disabled={buttonDisabled} value={dslUrlValue}
variant="primary" onChange={e => setDslUrlValue(e.target.value)}
onClick={handleCreateApp} />
className="gap-1" </div>
> )}
<span>{t('newApp.import', { ns: 'app' })}</span> </div>
</Button> <div className="flex justify-end gap-x-2 p-6 pt-5">
</div> <Button onClick={onClose}>
</Modal> {t('newApp.Cancel', { ns: 'app' })}
</Button>
<Button
disabled={buttonDisabled}
variant="primary"
onClick={handleCreateApp}
className="gap-1"
>
<span>{t('newApp.import', { ns: 'app' })}</span>
</Button>
</div>
</DialogContent>
</Dialog>
{showConfirmModal && ( {showConfirmModal && (
<DSLConfirmModal <DSLConfirmModal
versions={versions} versions={versions}

View File

@ -8,12 +8,12 @@ import {
AlertDialogDescription, AlertDialogDescription,
AlertDialogTitle, AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog' } from '@langgenius/dify-ui/alert-dialog'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react' import * as React from 'react'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { trackEvent } from '@/app/components/base/amplitude' import { trackEvent } from '@/app/components/base/amplitude'
import Modal from '@/app/components/base/modal'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { useRouter } from '@/next/navigation' import { useRouter } from '@/next/navigation'
import { useCreatePipelineDatasetFromCustomized } from '@/service/knowledge/use-create-dataset' import { useCreatePipelineDatasetFromCustomized } from '@/service/knowledge/use-create-dataset'
@ -153,16 +153,21 @@ const TemplateCard = ({
handleDelete={handleDelete} handleDelete={handleDelete}
/> />
{showEditModal && ( {showEditModal && (
<Modal <Dialog
isShow={showEditModal} open={showEditModal}
onClose={closeEditModal} onOpenChange={(open) => {
className="max-w-[520px] p-0" if (!open)
closeEditModal()
}}
> >
<EditPipelineInfo <DialogContent className="w-full max-w-[520px] overflow-hidden! border-none p-0 text-left align-middle">
pipeline={pipeline}
onClose={closeEditModal} <EditPipelineInfo
/> pipeline={pipeline}
</Modal> onClose={closeEditModal}
/>
</DialogContent>
</Dialog>
)} )}
<AlertDialog open={showDeleteConfirm} onOpenChange={open => !open && onCancelDelete()}> <AlertDialog open={showDeleteConfirm} onOpenChange={open => !open && onCancelDelete()}>
<AlertDialogContent> <AlertDialogContent>
@ -183,18 +188,23 @@ const TemplateCard = ({
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
{showDetailModal && ( {showDetailModal && (
<Modal <Dialog
isShow={showDetailModal} open={showDetailModal}
onClose={closeDetailsModal} onOpenChange={(open) => {
className="h-[calc(100vh-64px)] max-w-[1680px] rounded-3xl p-0" if (!open)
closeDetailsModal()
}}
> >
<Details <DialogContent className="h-[calc(100vh-64px)] max-h-none w-full max-w-[1680px] overflow-hidden! rounded-3xl border-none p-0 text-left align-middle">
id={pipeline.id}
type={type} <Details
onClose={closeDetailsModal} id={pipeline.id}
onApplyTemplate={handleUseTemplate} type={type}
/> onClose={closeDetailsModal}
</Modal> onApplyTemplate={handleUseTemplate}
/>
</DialogContent>
</Dialog>
)} )}
</div> </div>
) )

View File

@ -1,13 +1,13 @@
'use client' 'use client'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react' import * as React from 'react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { trackEvent } from '@/app/components/base/amplitude' import { trackEvent } from '@/app/components/base/amplitude'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import { useRouter } from '@/next/navigation' import { useRouter } from '@/next/navigation'
import { createEmptyDataset } from '@/service/datasets' import { createEmptyDataset } from '@/service/datasets'
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset' import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
@ -46,21 +46,30 @@ const EmptyDatasetCreationModal = ({ show = false, onHide }: IProps) => {
} }
} }
return ( return (
<Modal isShow={show} onClose={onHide} className={cn(s.modal, '!max-w-[520px]', 'px-8')}> <Dialog
<div className={s.modalHeader}> open={show}
<div className={s.title}>{t('stepOne.modal.title', { ns: 'datasetCreation' })}</div> onOpenChange={(open) => {
<span className={s.close} onClick={onHide} /> if (!open)
</div> onHide()
<div className={s.tip}>{t('stepOne.modal.tip', { ns: 'datasetCreation' })}</div> }}
<div className={s.form}> >
<div className={s.label}>{t('stepOne.modal.input', { ns: 'datasetCreation' })}</div> <DialogContent className={cn('w-full overflow-hidden! border-none text-left align-middle', cn(s.modal, '!max-w-[520px]', 'px-8'))}>
<Input value={inputValue} placeholder={t('stepOne.modal.placeholder', { ns: 'datasetCreation' }) || ''} onChange={e => setInputValue(e.target.value)} />
</div> <div className={s.modalHeader}>
<div className="flex flex-row-reverse"> <div className={s.title}>{t('stepOne.modal.title', { ns: 'datasetCreation' })}</div>
<Button className="ml-2 w-24" variant="primary" onClick={submit}>{t('stepOne.modal.confirmButton', { ns: 'datasetCreation' })}</Button> <span className={s.close} onClick={onHide} />
<Button className="w-24" onClick={onHide}>{t('stepOne.modal.cancelButton', { ns: 'datasetCreation' })}</Button> </div>
</div> <div className={s.tip}>{t('stepOne.modal.tip', { ns: 'datasetCreation' })}</div>
</Modal> <div className={s.form}>
<div className={s.label}>{t('stepOne.modal.input', { ns: 'datasetCreation' })}</div>
<Input value={inputValue} placeholder={t('stepOne.modal.placeholder', { ns: 'datasetCreation' }) || ''} onChange={e => setInputValue(e.target.value)} />
</div>
<div className="flex flex-row-reverse">
<Button className="ml-2 w-24" variant="primary" onClick={submit}>{t('stepOne.modal.confirmButton', { ns: 'datasetCreation' })}</Button>
<Button className="w-24" onClick={onHide}>{t('stepOne.modal.cancelButton', { ns: 'datasetCreation' })}</Button>
</div>
</DialogContent>
</Dialog>
) )
} }
export default EmptyDatasetCreationModal export default EmptyDatasetCreationModal

View File

@ -1,9 +1,16 @@
'use client' 'use client'
import { Button } from '@langgenius/dify-ui/button' import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import * as React from 'react' import * as React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import s from './index.module.css' import s from './index.module.css'
type IProps = { type IProps = {
@ -25,20 +32,24 @@ const StopEmbeddingModal = ({
} }
return ( return (
<Modal <AlertDialog
isShow={show} open={show}
onClose={onHide} onOpenChange={(open) => {
className={cn(s.modal, 'max-w-[480px]!', 'px-8')} if (!open)
onHide()
}}
> >
<div className={s.icon} /> <AlertDialogContent className={cn(s.modal, 'max-w-[480px]! overflow-hidden! border-none px-8 py-6 text-left align-middle shadow-xl')}>
<span className={s.close} onClick={onHide} /> <div className={s.icon} />
<div className={s.title}>{t('stepThree.modelTitle', { ns: 'datasetCreation' })}</div> <span className={s.close} onClick={onHide} />
<div className={s.content}>{t('stepThree.modelContent', { ns: 'datasetCreation' })}</div> <AlertDialogTitle className={s.title}>{t('stepThree.modelTitle', { ns: 'datasetCreation' })}</AlertDialogTitle>
<div className="flex flex-row-reverse"> <AlertDialogDescription className={s.content}>{t('stepThree.modelContent', { ns: 'datasetCreation' })}</AlertDialogDescription>
<Button className="ml-2 w-24" variant="primary" onClick={submit}>{t('stepThree.modelButtonConfirm', { ns: 'datasetCreation' })}</Button> <AlertDialogActions className="flex-row-reverse gap-0 p-0">
<Button className="w-24" onClick={onHide}>{t('stepThree.modelButtonCancel', { ns: 'datasetCreation' })}</Button> <AlertDialogConfirmButton className="ml-2 w-24" tone="default" onClick={submit}>{t('stepThree.modelButtonConfirm', { ns: 'datasetCreation' })}</AlertDialogConfirmButton>
</div> <AlertDialogCancelButton className="w-24" variant="secondary">{t('stepThree.modelButtonCancel', { ns: 'datasetCreation' })}</AlertDialogCancelButton>
</Modal> </AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
) )
} }

View File

@ -1,13 +1,13 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import * as React from 'react' import * as React from 'react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import { renameDocumentName } from '@/service/datasets' import { renameDocumentName } from '@/service/datasets'
type Props = { type Props = {
@ -55,23 +55,31 @@ const RenameModal: FC<Props> = ({
} }
return ( return (
<Modal <Dialog
title={t('list.table.rename', { ns: 'datasetDocuments' })} open
isShow onOpenChange={(open) => {
onClose={onClose} if (!open)
onClose()
}}
> >
<div className="mt-6 text-sm leading-[21px] font-medium text-text-primary">{t('list.table.name', { ns: 'datasetDocuments' })}</div> <DialogContent className="overflow-hidden! border-none text-left align-middle">
<Input <DialogTitle className="title-2xl-semi-bold text-text-primary">
className="mt-2 h-10" {t('list.table.rename', { ns: 'datasetDocuments' })}
value={newName} </DialogTitle>
onChange={e => setNewName(e.target.value)}
/>
<div className="mt-10 flex justify-end"> <div className="mt-6 text-sm leading-[21px] font-medium text-text-primary">{t('list.table.name', { ns: 'datasetDocuments' })}</div>
<Button className="mr-2 shrink-0" onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button> <Input
<Button variant="primary" className="shrink-0" onClick={handleSave} loading={saveLoading}>{t('operation.save', { ns: 'common' })}</Button> className="mt-2 h-10"
</div> value={newName}
</Modal> onChange={e => setNewName(e.target.value)}
/>
<div className="mt-10 flex justify-end">
<Button className="mr-2 shrink-0" onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button variant="primary" className="shrink-0" onClick={handleSave} loading={saveLoading}>{t('operation.save', { ns: 'common' })}</Button>
</div>
</DialogContent>
</Dialog>
) )
} }
export default React.memo(RenameModal) export default React.memo(RenameModal)

View File

@ -2,12 +2,11 @@
import type { FC } from 'react' import type { FC } from 'react'
import type { ChunkingMode, FileItem } from '@/models/datasets' import type { ChunkingMode, FileItem } from '@/models/datasets'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import * as React from 'react' import * as React from 'react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import CSVDownloader from './csv-downloader' import CSVDownloader from './csv-downloader'
import CSVUploader from './csv-uploader' import CSVUploader from './csv-uploader'
@ -41,27 +40,30 @@ const BatchModal: FC<IBatchModalProps> = ({
}, [isShow]) }, [isShow])
return ( return (
<Modal isShow={isShow} onClose={noop} className="max-w-[520px]! rounded-xl! px-8 py-6"> <Dialog open={isShow}>
<div className="relative pb-1 text-xl leading-[30px] font-medium text-text-primary">{t('list.batchModal.title', { ns: 'datasetDocuments' })}</div> <DialogContent className="w-full max-w-[520px]! overflow-hidden! rounded-xl! border-none px-8 py-6 text-left align-middle">
<div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onCancel}>
<RiCloseLine className="h-4 w-4 text-text-secondary" /> <div className="relative pb-1 text-xl leading-[30px] font-medium text-text-primary">{t('list.batchModal.title', { ns: 'datasetDocuments' })}</div>
</div> <div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onCancel}>
<CSVUploader <RiCloseLine className="h-4 w-4 text-text-secondary" />
file={currentCSV} </div>
updateFile={handleFile} <CSVUploader
/> file={currentCSV}
<CSVDownloader updateFile={handleFile}
docForm={docForm} />
/> <CSVDownloader
<div className="mt-[28px] flex justify-end pt-6"> docForm={docForm}
<Button className="mr-2" onClick={onCancel}> />
{t('list.batchModal.cancel', { ns: 'datasetDocuments' })} <div className="mt-[28px] flex justify-end pt-6">
</Button> <Button className="mr-2" onClick={onCancel}>
<Button variant="primary" onClick={handleSend} disabled={!currentCSV || !currentCSV.file || !currentCSV.file.id}> {t('list.batchModal.cancel', { ns: 'datasetDocuments' })}
{t('list.batchModal.run', { ns: 'datasetDocuments' })} </Button>
</Button> <Button variant="primary" onClick={handleSend} disabled={!currentCSV || !currentCSV.file || !currentCSV.file.id}>
</div> {t('list.batchModal.run', { ns: 'datasetDocuments' })}
</Modal> </Button>
</div>
</DialogContent>
</Dialog>
) )
} }
export default React.memo(BatchModal) export default React.memo(BatchModal)

View File

@ -1,12 +1,19 @@
import type { FC } from 'react' import type { FC } from 'react'
import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { RiLoader2Line } from '@remixicon/react' import { RiLoader2Line } from '@remixicon/react'
import { useCountDown } from 'ahooks' import { useCountDown } from 'ahooks'
import { noop } from 'es-toolkit/function'
import * as React from 'react' import * as React from 'react'
import { useRef, useState } from 'react' import { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
type IDefaultContentProps = { type IDefaultContentProps = {
@ -22,18 +29,18 @@ const DefaultContent: FC<IDefaultContentProps> = React.memo(({
return ( return (
<> <>
<div className="pb-4"> <div className="p-6 pb-4">
<span className="title-2xl-semi-bold text-text-primary">{t('segment.regenerationConfirmTitle', { ns: 'datasetDocuments' })}</span> <AlertDialogTitle className="title-2xl-semi-bold text-text-primary">{t('segment.regenerationConfirmTitle', { ns: 'datasetDocuments' })}</AlertDialogTitle>
<p className="system-md-regular text-text-secondary">{t('segment.regenerationConfirmMessage', { ns: 'datasetDocuments' })}</p> <AlertDialogDescription className="system-md-regular text-text-secondary">{t('segment.regenerationConfirmMessage', { ns: 'datasetDocuments' })}</AlertDialogDescription>
</div> </div>
<div className="flex justify-end gap-x-2 pt-6"> <AlertDialogActions>
<Button onClick={onCancel}> <AlertDialogCancelButton variant="secondary" onClick={onCancel}>
{t('operation.cancel', { ns: 'common' })} {t('operation.cancel', { ns: 'common' })}
</Button> </AlertDialogCancelButton>
<Button variant="primary" tone="destructive" onClick={onConfirm}> <AlertDialogConfirmButton onClick={onConfirm}>
{t('operation.regenerate', { ns: 'common' })} {t('operation.regenerate', { ns: 'common' })}
</Button> </AlertDialogConfirmButton>
</div> </AlertDialogActions>
</> </>
) )
}) })
@ -45,11 +52,11 @@ const RegeneratingContent: FC = React.memo(() => {
return ( return (
<> <>
<div className="pb-4"> <div className="p-6 pb-4">
<span className="title-2xl-semi-bold text-text-primary">{t('segment.regeneratingTitle', { ns: 'datasetDocuments' })}</span> <span className="title-2xl-semi-bold text-text-primary">{t('segment.regeneratingTitle', { ns: 'datasetDocuments' })}</span>
<p className="system-md-regular text-text-secondary">{t('segment.regeneratingMessage', { ns: 'datasetDocuments' })}</p> <p className="system-md-regular text-text-secondary">{t('segment.regeneratingMessage', { ns: 'datasetDocuments' })}</p>
</div> </div>
<div className="flex justify-end pt-6"> <div className="flex justify-end p-6">
<Button variant="primary" tone="destructive" disabled className="inline-flex items-center gap-x-0.5"> <Button variant="primary" tone="destructive" disabled className="inline-flex items-center gap-x-0.5">
<RiLoader2Line className="h-4 w-4 animate-spin text-components-button-destructive-primary-text-disabled" /> <RiLoader2Line className="h-4 w-4 animate-spin text-components-button-destructive-primary-text-disabled" />
<span>{t('operation.regenerate', { ns: 'common' })}</span> <span>{t('operation.regenerate', { ns: 'common' })}</span>
@ -79,11 +86,11 @@ const RegenerationCompletedContent: FC<IRegenerationCompletedContentProps> = Rea
return ( return (
<> <>
<div className="pb-4"> <div className="p-6 pb-4">
<span className="title-2xl-semi-bold text-text-primary">{t('segment.regenerationSuccessTitle', { ns: 'datasetDocuments' })}</span> <span className="title-2xl-semi-bold text-text-primary">{t('segment.regenerationSuccessTitle', { ns: 'datasetDocuments' })}</span>
<p className="system-md-regular text-text-secondary">{t('segment.regenerationSuccessMessage', { ns: 'datasetDocuments' })}</p> <p className="system-md-regular text-text-secondary">{t('segment.regenerationSuccessMessage', { ns: 'datasetDocuments' })}</p>
</div> </div>
<div className="flex justify-end pt-6"> <div className="flex justify-end p-6">
<Button variant="primary" onClick={onClose}> <Button variant="primary" onClick={onClose}>
{`${t('operation.close', { ns: 'common' })}${countdown === 0 ? '' : `(${Math.round(countdown / 1000)})`}`} {`${t('operation.close', { ns: 'common' })}${countdown === 0 ? '' : `(${Math.round(countdown / 1000)})`}`}
</Button> </Button>
@ -123,11 +130,13 @@ const RegenerationModal: FC<IRegenerationModalProps> = ({
}) })
return ( return (
<Modal isShow={isShow} onClose={noop} className="max-w-[480px]! rounded-2xl!" wrapperClassName="z-10000!"> <AlertDialog open={isShow}>
{!loading && !updateSucceeded && <DefaultContent onCancel={onCancel} onConfirm={onConfirm} />} <AlertDialogContent className="max-w-[480px]! overflow-hidden! rounded-2xl border-none text-left align-middle shadow-xl">
{loading && !updateSucceeded && <RegeneratingContent />} {!loading && !updateSucceeded && <DefaultContent onCancel={onCancel} onConfirm={onConfirm} />}
{!loading && updateSucceeded && <RegenerationCompletedContent onClose={onClose} />} {loading && !updateSucceeded && <RegeneratingContent />}
</Modal> {!loading && updateSucceeded && <RegenerationCompletedContent onClose={onClose} />}
</AlertDialogContent>
</AlertDialog>
) )
} }

View File

@ -11,14 +11,16 @@ vi.mock('@/app/components/base/markdown', () => ({
Markdown: ({ content }: { content: string }) => <div data-testid="markdown">{content}</div>, Markdown: ({ content }: { content: string }) => <div data-testid="markdown">{content}</div>,
})) }))
vi.mock('@/app/components/base/modal', () => ({ vi.mock('@langgenius/dify-ui/dialog', () => ({
default: ({ children, title, onClose }: { children: React.ReactNode, title: string, onClose: () => void }) => ( Dialog: ({ children, open }: { children: React.ReactNode, open?: boolean }) =>
open === false ? null : <>{children}</>,
DialogContent: ({ children }: { children: React.ReactNode }) => (
<div data-testid="modal"> <div data-testid="modal">
<div data-testid="modal-title">{title}</div>
<button data-testid="modal-close" onClick={onClose}>close</button>
{children} {children}
</div> </div>
), ),
DialogTitle: ({ children }: { children: React.ReactNode }) => <div data-testid="modal-title">{children}</div>,
DialogCloseButton: () => <button data-testid="modal-close">close</button>,
})) }))
vi.mock('../../../common/image-list', () => ({ vi.mock('../../../common/image-list', () => ({

View File

@ -2,12 +2,12 @@
import type { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types' import type { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types'
import type { HitTesting } from '@/models/datasets' import type { HitTesting } from '@/models/datasets'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import * as React from 'react' import * as React from 'react'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import FileIcon from '@/app/components/base/file-uploader/file-type-icon' import FileIcon from '@/app/components/base/file-uploader/file-type-icon'
import { Markdown } from '@/app/components/base/markdown' import { Markdown } from '@/app/components/base/markdown'
import Modal from '@/app/components/base/modal'
import Tag from '@/app/components/datasets/documents/detail/completed/common/tag' import Tag from '@/app/components/datasets/documents/detail/completed/common/tag'
import ImageList from '../../common/image-list' import ImageList from '../../common/image-list'
import Dot from '../../documents/detail/completed/common/dot' import Dot from '../../documents/detail/completed/common/dot'
@ -52,93 +52,100 @@ const ChunkDetailModal = ({
const showKeywords = !isParentChildRetrieval && keywords && keywords.length > 0 const showKeywords = !isParentChildRetrieval && keywords && keywords.length > 0
return ( return (
<Modal <Dialog
title={t(`${i18nPrefix}chunkDetail`, { ns: 'datasetHitTesting' })} open
isShow onOpenChange={(open) => {
closable if (!open)
onClose={onHide} onHide()
className={cn(isParentChildRetrieval ? 'min-w-[1200px]!' : 'min-w-[800px]!')} }}
> >
<div className="mt-4 flex"> <DialogContent className={cn('w-full overflow-hidden! border-none text-left align-middle', cn(isParentChildRetrieval ? 'min-w-[1200px]!' : 'min-w-[800px]!'))}>
<div className={cn('flex-1', isParentChildRetrieval && 'pr-6')}> <DialogCloseButton data-testid="modal-close-button" />
{/* Meta info */} <DialogTitle className="title-2xl-semi-bold text-text-primary">
<div className="flex items-center justify-between"> {t(`${i18nPrefix}chunkDetail`, { ns: 'datasetHitTesting' })}
<div className="flex grow items-center space-x-2"> </DialogTitle>
<SegmentIndexTag
labelPrefix={labelPrefix} <div className="mt-4 flex">
positionId={position} <div className={cn('flex-1', isParentChildRetrieval && 'pr-6')}>
className={cn('w-fit group-hover:opacity-100')} {/* Meta info */}
/> <div className="flex items-center justify-between">
<Dot /> <div className="flex grow items-center space-x-2">
<div className="flex grow items-center space-x-1"> <SegmentIndexTag
<FileIcon type={extension} size="sm" /> labelPrefix={labelPrefix}
<span className="w-0 grow truncate text-[13px] font-normal text-text-secondary">{document.name}</span> positionId={position}
className={cn('w-fit group-hover:opacity-100')}
/>
<Dot />
<div className="flex grow items-center space-x-1">
<FileIcon type={extension} size="sm" />
<span className="w-0 grow truncate text-[13px] font-normal text-text-secondary">{document.name}</span>
</div>
</div> </div>
<Score value={score} />
</div> </div>
<Score value={score} /> {/* Content */}
</div> <div className="relative">
{/* Content */} {!answer && (
<div className="relative"> <Markdown
{!answer && ( className={cn('mt-2! text-text-secondary!', heighClassName)}
<Markdown content={sign_content || content}
className={cn('mt-2! text-text-secondary!', heighClassName)} customDisallowedElements={['input']}
content={sign_content || content} />
customDisallowedElements={['input']} )}
/> {answer && (
)} <div className="break-all">
{answer && ( <div className="flex gap-x-1">
<div className="break-all"> <div className="w-4 shrink-0 text-[13px] leading-[20px] font-medium text-text-tertiary">Q</div>
<div className="flex gap-x-1"> <div className={cn('line-clamp-20 body-md-regular text-text-secondary')}>
<div className="w-4 shrink-0 text-[13px] leading-[20px] font-medium text-text-tertiary">Q</div> {content}
<div className={cn('line-clamp-20 body-md-regular text-text-secondary')}> </div>
{content} </div>
<div className="flex gap-x-1">
<div className="w-4 shrink-0 text-[13px] leading-[20px] font-medium text-text-tertiary">A</div>
<div className={cn('line-clamp-20 body-md-regular text-text-secondary')}>
{answer}
</div>
</div> </div>
</div> </div>
<div className="flex gap-x-1"> )}
<div className="w-4 shrink-0 text-[13px] leading-[20px] font-medium text-text-tertiary">A</div> {/* Mask */}
<div className={cn('line-clamp-20 body-md-regular text-text-secondary')}> <Mask className="absolute inset-x-0 bottom-0" />
{answer} </div>
{(showImages || showKeywords || !!summary) && (
<div className="flex flex-col gap-y-3 pt-3">
{showImages && (
<ImageList images={images} size="md" className="py-1" />
)}
{!!summary && (
<SummaryText value={summary} disabled />
)}
{showKeywords && (
<div className="flex flex-col gap-y-1">
<div className="text-xs font-medium text-text-tertiary uppercase">{t(`${i18nPrefix}keyword`, { ns: 'datasetHitTesting' })}</div>
<div className="flex flex-wrap gap-x-2">
{keywords.map(keyword => (
<Tag key={keyword} text={keyword} />
))}
</div>
</div> </div>
</div> )}
</div> </div>
)} )}
{/* Mask */}
<Mask className="absolute inset-x-0 bottom-0" />
</div> </div>
{(showImages || showKeywords || !!summary) && (
<div className="flex flex-col gap-y-3 pt-3"> {isParentChildRetrieval && (
{showImages && ( <div className="flex-1 pb-6 pl-6">
<ImageList images={images} size="md" className="py-1" /> <div className="system-xs-semibold-uppercase text-text-secondary">{t(`${i18nPrefix}hitChunks`, { ns: 'datasetHitTesting', num: child_chunks.length })}</div>
)} <div className={cn('mt-1 space-y-2', heighClassName)}>
{!!summary && ( {child_chunks.map(item => (
<SummaryText value={summary} disabled /> <ChildChunksItem key={item.id} payload={item} isShowAll />
)} ))}
{showKeywords && ( </div>
<div className="flex flex-col gap-y-1">
<div className="text-xs font-medium text-text-tertiary uppercase">{t(`${i18nPrefix}keyword`, { ns: 'datasetHitTesting' })}</div>
<div className="flex flex-wrap gap-x-2">
{keywords.map(keyword => (
<Tag key={keyword} text={keyword} />
))}
</div>
</div>
)}
</div> </div>
)} )}
</div> </div>
</DialogContent>
{isParentChildRetrieval && ( </Dialog>
<div className="flex-1 pb-6 pl-6">
<div className="system-xs-semibold-uppercase text-text-secondary">{t(`${i18nPrefix}hitChunks`, { ns: 'datasetHitTesting', num: child_chunks.length })}</div>
<div className={cn('mt-1 space-y-2', heighClassName)}>
{child_chunks.map(item => (
<ChildChunksItem key={item.id} payload={item} isShowAll />
))}
</div>
</div>
)}
</div>
</Modal>
) )
} }

View File

@ -2,11 +2,11 @@
import type { FC } from 'react' import type { FC } from 'react'
import type { ExternalKnowledgeBaseHitTesting } from '@/models/datasets' import type { ExternalKnowledgeBaseHitTesting } from '@/models/datasets'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import * as React from 'react' import * as React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types' import { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types'
import Modal from '@/app/components/base/modal'
import ResultItemFooter from './result-item-footer' import ResultItemFooter from './result-item-footer'
import ResultItemMeta from './result-item-meta' import ResultItemMeta from './result-item-meta'
@ -38,20 +38,27 @@ const ResultItemExternal: FC<Props> = ({ payload, positionId }) => {
<ResultItemFooter docType={FileAppearanceTypeEnum.custom} docTitle={title} showDetailModal={showDetailModal} /> <ResultItemFooter docType={FileAppearanceTypeEnum.custom} docTitle={title} showDetailModal={showDetailModal} />
{isShowDetailModal && ( {isShowDetailModal && (
<Modal <Dialog
title={t(`${i18nPrefix}chunkDetail`, { ns: 'datasetHitTesting' })} open={isShowDetailModal}
className="min-w-[800px]!" onOpenChange={(open) => {
closable if (!open)
onClose={hideDetailModal} hideDetailModal()
isShow={isShowDetailModal} }}
> >
<div className="mt-4 flex-1"> <DialogContent className="w-full min-w-[800px]! overflow-hidden! border-none text-left align-middle">
<ResultItemMeta labelPrefix="Chunk" positionId={positionId} wordCount={content.length} score={score} /> <DialogCloseButton data-testid="modal-close-button" />
<div className={cn('mt-2 body-md-regular break-all text-text-secondary', 'h-[min(539px,80vh)] overflow-y-auto')}> <DialogTitle className="title-2xl-semi-bold text-text-primary">
{content} {t(`${i18nPrefix}chunkDetail`, { ns: 'datasetHitTesting' })}
</DialogTitle>
<div className="mt-4 flex-1">
<ResultItemMeta labelPrefix="Chunk" positionId={positionId} wordCount={content.length} score={score} />
<div className={cn('mt-2 body-md-regular break-all text-text-secondary', 'h-[min(539px,80vh)] overflow-y-auto')}>
{content}
</div>
</div> </div>
</div> </DialogContent>
</Modal> </Dialog>
)} )}
</div> </div>
) )

View File

@ -2,6 +2,7 @@
import type { FC } from 'react' import type { FC } from 'react'
import type { BuiltInMetadataItem, MetadataItemInBatchEdit, MetadataItemWithEdit } from '../types' import type { BuiltInMetadataItem, MetadataItemInBatchEdit, MetadataItemWithEdit } from '../types'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { RiQuestionLine } from '@remixicon/react' import { RiQuestionLine } from '@remixicon/react'
import { produce } from 'immer' import { produce } from 'immer'
@ -11,7 +12,6 @@ import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider' import Divider from '@/app/components/base/divider'
import { useCreateMetaData } from '@/service/knowledge/use-metadata' import { useCreateMetaData } from '@/service/knowledge/use-metadata'
import Checkbox from '../../../base/checkbox' import Checkbox from '../../../base/checkbox'
import Modal from '../../../base/modal'
import Tooltip from '../../../base/tooltip' import Tooltip from '../../../base/tooltip'
import AddMetadataButton from '../add-metadata-button' import AddMetadataButton from '../add-metadata-button'
import useCheckMetadataName from '../hooks/use-check-metadata-name' import useCheckMetadataName from '../hooks/use-check-metadata-name'
@ -91,46 +91,59 @@ const EditMetadataBatchModal: FC<Props> = ({ datasetId, documentNum, list, onSav
onSave(templeList.filter(item => item.updateType !== UpdateType.delete), addedList, isApplyToAllSelectDocument) onSave(templeList.filter(item => item.updateType !== UpdateType.delete), addedList, isApplyToAllSelectDocument)
}, [templeList, addedList, isApplyToAllSelectDocument, onSave]) }, [templeList, addedList, isApplyToAllSelectDocument, onSave])
return ( return (
<Modal title={t(`${i18nPrefix}.editMetadata`, { ns: 'dataset' })} isShow closable onClose={onHide} className="!max-w-[640px]"> <Dialog
<div className="mt-1 system-xs-medium text-text-accent">{t(`${i18nPrefix}.editDocumentsNum`, { ns: 'dataset', num: documentNum })}</div> open
<div className="max-h-[305px] overflow-x-hidden overflow-y-auto"> onOpenChange={(open) => {
<div className="mt-4 space-y-2"> if (!open)
{templeList.map(item => (<EditMetadataBatchItem key={item.id} payload={item} onChange={handleTemplesChange} onRemove={handleTempleItemRemove} onReset={handleItemReset} />))} onHide()
</div> }}
<div className="mt-4 pl-[18px]"> >
<div className="flex items-center"> <DialogContent className="w-full !max-w-[640px] overflow-hidden! border-none text-left align-middle">
<div className="mr-2 shrink-0 system-xs-medium-uppercase text-text-tertiary">{t('metadata.createMetadata.title', { ns: 'dataset' })}</div> <DialogCloseButton data-testid="modal-close-button" />
<Divider bgStyle="gradient" /> <DialogTitle className="title-2xl-semi-bold text-text-primary">
</div> {t(`${i18nPrefix}.editMetadata`, { ns: 'dataset' })}
<div className="mt-2 space-y-2"> </DialogTitle>
{addedList.map((item, i) => (<AddedMetadataItem key={i} payload={item} onChange={handleAddedListChange} onRemove={handleAddedItemRemove(i)} />))}
</div>
<div className="mt-3">
<SelectMetadataModal datasetId={datasetId} popupPlacement="top-start" popupOffset={{ mainAxis: 4, crossAxis: 0 }} trigger={<AddMetadataButton />} onSave={handleAddMetaData} onSelect={data => setAddedList([...addedList, data as MetadataItemWithEdit])} onManage={onShowManage} />
</div>
</div>
</div>
<div className="mt-4 flex items-center justify-between"> <div className="mt-1 system-xs-medium text-text-accent">{t(`${i18nPrefix}.editDocumentsNum`, { ns: 'dataset', num: documentNum })}</div>
<div className="flex items-center select-none"> <div className="max-h-[305px] overflow-x-hidden overflow-y-auto">
<Checkbox checked={isApplyToAllSelectDocument} onCheck={() => setIsApplyToAllSelectDocument(!isApplyToAllSelectDocument)} id="apply-to-all" /> <div className="mt-4 space-y-2">
<div className="mr-1 ml-2 system-xs-medium text-text-secondary">{t(`${i18nPrefix}.applyToAllSelectDocument`, { ns: 'dataset' })}</div> {templeList.map(item => (<EditMetadataBatchItem key={item.id} payload={item} onChange={handleTemplesChange} onRemove={handleTempleItemRemove} onReset={handleItemReset} />))}
<Tooltip popupContent={<div className="max-w-[240px]">{t(`${i18nPrefix}.applyToAllSelectDocumentTip`, { ns: 'dataset' })}</div>}> </div>
<div className="cursor-pointer p-px"> <div className="mt-4 pl-[18px]">
<RiQuestionLine className="size-3.5 text-text-tertiary" /> <div className="flex items-center">
<div className="mr-2 shrink-0 system-xs-medium-uppercase text-text-tertiary">{t('metadata.createMetadata.title', { ns: 'dataset' })}</div>
<Divider bgStyle="gradient" />
</div> </div>
</Tooltip> <div className="mt-2 space-y-2">
{addedList.map((item, i) => (<AddedMetadataItem key={i} payload={item} onChange={handleAddedListChange} onRemove={handleAddedItemRemove(i)} />))}
</div>
<div className="mt-3">
<SelectMetadataModal datasetId={datasetId} popupPlacement="top-start" popupOffset={{ mainAxis: 4, crossAxis: 0 }} trigger={<AddMetadataButton />} onSave={handleAddMetaData} onSelect={data => setAddedList([...addedList, data as MetadataItemWithEdit])} onManage={onShowManage} />
</div>
</div>
</div> </div>
<div className="flex items-center space-x-2">
<Button onClick={onHide}> <div className="mt-4 flex items-center justify-between">
{t('operation.cancel', { ns: 'common' })} <div className="flex items-center select-none">
</Button> <Checkbox checked={isApplyToAllSelectDocument} onCheck={() => setIsApplyToAllSelectDocument(!isApplyToAllSelectDocument)} id="apply-to-all" />
<Button onClick={handleSave} variant="primary"> <div className="mr-1 ml-2 system-xs-medium text-text-secondary">{t(`${i18nPrefix}.applyToAllSelectDocument`, { ns: 'dataset' })}</div>
{t('operation.save', { ns: 'common' })} <Tooltip popupContent={<div className="max-w-[240px]">{t(`${i18nPrefix}.applyToAllSelectDocumentTip`, { ns: 'dataset' })}</div>}>
</Button> <div className="cursor-pointer p-px">
<RiQuestionLine className="size-3.5 text-text-tertiary" />
</div>
</Tooltip>
</div>
<div className="flex items-center space-x-2">
<Button onClick={onHide}>
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button onClick={handleSave} variant="primary">
{t('operation.save', { ns: 'common' })}
</Button>
</div>
</div> </div>
</div> </DialogContent>
</Modal> </Dialog>
) )
} }
export default React.memo(EditMetadataBatchModal) export default React.memo(EditMetadataBatchModal)

View File

@ -12,6 +12,7 @@ import {
} from '@langgenius/dify-ui/alert-dialog' } from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { Switch } from '@langgenius/dify-ui/switch' import { Switch } from '@langgenius/dify-ui/switch'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { RiAddLine, RiDeleteBinLine, RiEditLine } from '@remixicon/react' import { RiAddLine, RiDeleteBinLine, RiEditLine } from '@remixicon/react'
@ -21,7 +22,6 @@ import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Drawer from '@/app/components/base/drawer' import Drawer from '@/app/components/base/drawer'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import CreateModal from '@/app/components/datasets/metadata/metadata-dataset/create-metadata-modal' import CreateModal from '@/app/components/datasets/metadata/metadata-dataset/create-metadata-modal'
import { getIcon } from '../utils/get-icon' import { getIcon } from '../utils/get-icon'
@ -230,33 +230,45 @@ const DatasetMetadataDrawer: FC<Props> = ({
</div> </div>
{isShowRenameModal && ( {isShowRenameModal && (
<Modal isShow title={t(`${i18nPrefix}.rename`, { ns: 'dataset' })} onClose={() => setIsShowRenameModal(false)}> <Dialog
<Field label={t(`${i18nPrefix}.name`, { ns: 'dataset' })} className="mt-4"> open
<Input onOpenChange={(open) => {
value={templeName} if (!open)
onChange={e => setTempleName(e.target.value)} setIsShowRenameModal(false)
placeholder={t(`${i18nPrefix}.namePlaceholder`, { ns: 'dataset' })} }}
/> >
</Field> <DialogContent className="overflow-hidden! border-none text-left align-middle">
<div className="mt-4 flex justify-end"> <DialogTitle className="title-2xl-semi-bold text-text-primary">
<Button {t(`${i18nPrefix}.rename`, { ns: 'dataset' })}
className="mr-2" </DialogTitle>
onClick={() => {
setIsShowRenameModal(false) <Field label={t(`${i18nPrefix}.name`, { ns: 'dataset' })} className="mt-4">
setTempleName(currPayload!.name) <Input
}} value={templeName}
> onChange={e => setTempleName(e.target.value)}
{t('operation.cancel', { ns: 'common' })} placeholder={t(`${i18nPrefix}.namePlaceholder`, { ns: 'dataset' })}
</Button> />
<Button </Field>
onClick={handleRenamed} <div className="mt-4 flex justify-end">
variant="primary" <Button
disabled={!templeName} className="mr-2"
> onClick={() => {
{t('operation.save', { ns: 'common' })} setIsShowRenameModal(false)
</Button> setTempleName(currPayload!.name)
</div> }}
</Modal> >
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button
onClick={handleRenamed}
variant="primary"
disabled={!templeName}
>
{t('operation.save', { ns: 'common' })}
</Button>
</div>
</DialogContent>
</Dialog>
)} )}
</div> </div>
</Drawer> </Drawer>

View File

@ -4,13 +4,12 @@ import type { AppIconSelection } from '../../base/app-icon-picker'
import type { DataSet } from '@/models/datasets' import type { DataSet } from '@/models/datasets'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import { useCallback, useRef, useState } from 'react' import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import Textarea from '@/app/components/base/textarea' import Textarea from '@/app/components/base/textarea'
import { updateDatasetSetting } from '@/service/datasets' import { updateDatasetSetting } from '@/service/datasets'
import AppIcon from '../../base/app-icon' import AppIcon from '../../base/app-icon'
@ -89,38 +88,41 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
} }
}, [appIcon, description, dataset.id, externalKnowledgeApiId, externalKnowledgeId, name, onClose, onSuccess, t]) }, [appIcon, description, dataset.id, externalKnowledgeApiId, externalKnowledgeId, name, onClose, onSuccess, t])
return ( return (
<Modal className="w-[520px] max-w-[520px] rounded-xl px-8 py-6" isShow={show} onClose={noop}> <Dialog open={show}>
<div className="flex items-center justify-between pb-2"> <DialogContent className="w-full max-w-[520px] overflow-hidden! rounded-xl border-none px-8 py-6 text-left align-middle">
<div className="text-xl leading-[30px] font-medium text-text-primary">{t('title', { ns: 'datasetSettings' })}</div>
<div className="cursor-pointer p-2" onClick={onClose}> <div className="flex items-center justify-between pb-2">
<RiCloseLine className="h-4 w-4 text-text-tertiary" /> <div className="text-xl leading-[30px] font-medium text-text-primary">{t('title', { ns: 'datasetSettings' })}</div>
</div> <div className="cursor-pointer p-2" onClick={onClose}>
</div> <RiCloseLine className="h-4 w-4 text-text-tertiary" />
<div>
<div className={cn('flex flex-col py-4')}>
<div className="shrink-0 py-2 text-sm leading-[20px] font-medium text-text-primary">
{t('form.name', { ns: 'datasetSettings' })}
</div>
<div className="flex items-center gap-x-2">
<AppIcon size="medium" onClick={handleOpenAppIconPicker} className="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} showEditIcon />
<Input value={name} onChange={e => setName(e.target.value)} className="h-9 grow" placeholder={t('form.namePlaceholder', { ns: 'datasetSettings' }) || ''} />
</div> </div>
</div> </div>
<div className={cn('flex flex-col py-4')}> <div>
<div className="shrink-0 py-2 text-sm leading-[20px] font-medium text-text-primary"> <div className={cn('flex flex-col py-4')}>
{t('form.desc', { ns: 'datasetSettings' })} <div className="shrink-0 py-2 text-sm leading-[20px] font-medium text-text-primary">
{t('form.name', { ns: 'datasetSettings' })}
</div>
<div className="flex items-center gap-x-2">
<AppIcon size="medium" onClick={handleOpenAppIconPicker} className="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} showEditIcon />
<Input value={name} onChange={e => setName(e.target.value)} className="h-9 grow" placeholder={t('form.namePlaceholder', { ns: 'datasetSettings' }) || ''} />
</div>
</div> </div>
<div className="w-full"> <div className={cn('flex flex-col py-4')}>
<Textarea value={description} onChange={e => setDescription(e.target.value)} className="resize-none" placeholder={t('form.descPlaceholder', { ns: 'datasetSettings' }) || ''} /> <div className="shrink-0 py-2 text-sm leading-[20px] font-medium text-text-primary">
{t('form.desc', { ns: 'datasetSettings' })}
</div>
<div className="w-full">
<Textarea value={description} onChange={e => setDescription(e.target.value)} className="resize-none" placeholder={t('form.descPlaceholder', { ns: 'datasetSettings' }) || ''} />
</div>
</div> </div>
</div> </div>
</div> <div className="flex justify-end pt-6">
<div className="flex justify-end pt-6"> <Button className="mr-2" onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button className="mr-2" onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button> <Button disabled={loading} variant="primary" onClick={onConfirm}>{t('operation.save', { ns: 'common' })}</Button>
<Button disabled={loading} variant="primary" onClick={onConfirm}>{t('operation.save', { ns: 'common' })}</Button> </div>
</div> {showAppIconPicker && (<AppIconPicker onSelect={handleSelectAppIcon} onClose={handleCloseAppIconPicker} />)}
{showAppIconPicker && (<AppIconPicker onSelect={handleSelectAppIcon} onClose={handleCloseAppIconPicker} />)} </DialogContent>
</Modal> </Dialog>
) )
} }
export default RenameDatasetModal export default RenameDatasetModal

View File

@ -2,8 +2,9 @@
import type { CreateApiKeyResponse } from '@/models/app' import type { CreateApiKeyResponse } from '@/models/app'
import { XMarkIcon } from '@heroicons/react/20/solid' import { XMarkIcon } from '@heroicons/react/20/solid'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import InputCopy from './input-copy' import InputCopy from './input-copy'
import s from './style.module.css' import s from './style.module.css'
@ -22,21 +23,33 @@ const SecretKeyGenerateModal = ({
}: ISecretKeyGenerateModalProps) => { }: ISecretKeyGenerateModalProps) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Modal isShow={isShow} onClose={onClose} title={`${t('apiKeyModal.apiSecretKey', { ns: 'appApi' })}`} className={`px-8 ${className}`}> <Dialog
<div className="-mt-6 -mr-2 mb-4 flex justify-end"> open={isShow}
<XMarkIcon className="h-6 w-6 cursor-pointer text-text-tertiary" onClick={onClose} /> onOpenChange={(open) => {
</div> if (!open)
<p className="mt-1 text-[13px] leading-5 font-normal text-text-tertiary">{t('apiKeyModal.generateTips', { ns: 'appApi' })}</p> onClose()
<div className="my-4"> }}
<InputCopy className="w-full" value={newKey?.token} /> >
</div> <DialogContent className={cn('w-full max-w-[480px] overflow-hidden! border-none px-8 text-left align-middle', className)}>
<div className="my-4 flex justify-end"> <DialogTitle className="title-2xl-semi-bold text-text-primary">
<Button className={`shrink-0 ${s.w64}`} onClick={onClose}> {`${t('apiKeyModal.apiSecretKey', { ns: 'appApi' })}`}
<span className="text-xs font-medium text-text-secondary">{t('actionMsg.ok', { ns: 'appApi' })}</span> </DialogTitle>
</Button>
</div>
</Modal> <div className="-mt-6 -mr-2 mb-4 flex justify-end">
<XMarkIcon className="h-6 w-6 cursor-pointer text-text-tertiary" onClick={onClose} />
</div>
<p className="mt-1 text-[13px] leading-5 font-normal text-text-tertiary">{t('apiKeyModal.generateTips', { ns: 'appApi' })}</p>
<div className="my-4">
<InputCopy className="w-full" value={newKey?.token} />
</div>
<div className="my-4 flex justify-end">
<Button className={`shrink-0 ${s.w64}`} onClick={onClose}>
<span className="text-xs font-medium text-text-secondary">{t('actionMsg.ok', { ns: 'appApi' })}</span>
</Button>
</div>
</DialogContent>
</Dialog>
) )
} }

View File

@ -11,6 +11,8 @@ import {
AlertDialogTitle, AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog' } from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { RiDeleteBinLine } from '@remixicon/react' import { RiDeleteBinLine } from '@remixicon/react'
import { import {
useState, useState,
@ -19,7 +21,6 @@ import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button' import ActionButton from '@/app/components/base/action-button'
import CopyFeedback from '@/app/components/base/copy-feedback' import CopyFeedback from '@/app/components/base/copy-feedback'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import useTimestamp from '@/hooks/use-timestamp' import useTimestamp from '@/hooks/use-timestamp'
import { import {
@ -104,77 +105,89 @@ const SecretKeyModal = ({
} }
return ( return (
<Modal isShow={isShow} onClose={onClose} title={`${t('apiKeyModal.apiSecretKey', { ns: 'appApi' })}`} className={`${s.customModal} flex flex-col px-8`}> <Dialog
<div className="-mt-6 -mr-2 mb-4 flex justify-end"> open={isShow}
<XMarkIcon className="h-6 w-6 cursor-pointer text-text-tertiary" onClick={onClose} /> onOpenChange={(open) => {
</div> if (!open)
<p className="mt-1 shrink-0 text-[13px] leading-5 font-normal text-text-tertiary">{t('apiKeyModal.apiSecretKeyTips', { ns: 'appApi' })}</p> onClose()
{isApiKeysLoading && <div className="mt-4"><Loading /></div>} }}
{ >
!!apiKeysList?.data?.length && ( <DialogContent className={cn('max-h-[calc(100vh-80px)]! w-full max-w-[800px]! overflow-hidden! border-none text-left align-middle', `${s.customModal} flex flex-col px-8`)}>
<div className="mt-4 flex grow flex-col overflow-hidden"> <DialogTitle className="title-2xl-semi-bold text-text-primary">
<div className="flex h-9 shrink-0 items-center border-b border-divider-regular text-xs font-semibold text-text-tertiary"> {`${t('apiKeyModal.apiSecretKey', { ns: 'appApi' })}`}
<div className="w-64 shrink-0 px-3">{t('apiKeyModal.secretKey', { ns: 'appApi' })}</div> </DialogTitle>
<div className="w-[200px] shrink-0 px-3">{t('apiKeyModal.created', { ns: 'appApi' })}</div>
<div className="w-[200px] shrink-0 px-3">{t('apiKeyModal.lastUsed', { ns: 'appApi' })}</div> <div className="-mt-6 -mr-2 mb-4 flex justify-end">
<div className="grow px-3"></div> <XMarkIcon className="h-6 w-6 cursor-pointer text-text-tertiary" onClick={onClose} />
</div> </div>
<div className="grow overflow-auto"> <p className="mt-1 shrink-0 text-[13px] leading-5 font-normal text-text-tertiary">{t('apiKeyModal.apiSecretKeyTips', { ns: 'appApi' })}</p>
{apiKeysList.data.map(api => ( {isApiKeysLoading && <div className="mt-4"><Loading /></div>}
<div className="flex h-9 items-center border-b border-divider-regular text-sm font-normal text-text-secondary" key={api.id}> {
<div className="w-64 shrink-0 truncate px-3 font-mono">{generateToken(api.token)}</div> !!apiKeysList?.data?.length && (
<div className="w-[200px] shrink-0 truncate px-3">{formatTime(Number(api.created_at), t('dateTimeFormat', { ns: 'appLog' }) as string)}</div> <div className="mt-4 flex grow flex-col overflow-hidden">
<div className="w-[200px] shrink-0 truncate px-3">{api.last_used_at ? formatTime(Number(api.last_used_at), t('dateTimeFormat', { ns: 'appLog' }) as string) : t('never', { ns: 'appApi' })}</div> <div className="flex h-9 shrink-0 items-center border-b border-divider-regular text-xs font-semibold text-text-tertiary">
<div className="flex grow space-x-2 px-3"> <div className="w-64 shrink-0 px-3">{t('apiKeyModal.secretKey', { ns: 'appApi' })}</div>
<CopyFeedback content={api.token} /> <div className="w-[200px] shrink-0 px-3">{t('apiKeyModal.created', { ns: 'appApi' })}</div>
{isCurrentWorkspaceManager && ( <div className="w-[200px] shrink-0 px-3">{t('apiKeyModal.lastUsed', { ns: 'appApi' })}</div>
<ActionButton <div className="grow px-3"></div>
onClick={() => { </div>
setDelKeyId(api.id) <div className="grow overflow-auto">
setShowConfirmDelete(true) {apiKeysList.data.map(api => (
}} <div className="flex h-9 items-center border-b border-divider-regular text-sm font-normal text-text-secondary" key={api.id}>
> <div className="w-64 shrink-0 truncate px-3 font-mono">{generateToken(api.token)}</div>
<RiDeleteBinLine className="h-4 w-4" /> <div className="w-[200px] shrink-0 truncate px-3">{formatTime(Number(api.created_at), t('dateTimeFormat', { ns: 'appLog' }) as string)}</div>
</ActionButton> <div className="w-[200px] shrink-0 truncate px-3">{api.last_used_at ? formatTime(Number(api.last_used_at), t('dateTimeFormat', { ns: 'appLog' }) as string) : t('never', { ns: 'appApi' })}</div>
)} <div className="flex grow space-x-2 px-3">
<CopyFeedback content={api.token} />
{isCurrentWorkspaceManager && (
<ActionButton
onClick={() => {
setDelKeyId(api.id)
setShowConfirmDelete(true)
}}
>
<RiDeleteBinLine className="h-4 w-4" />
</ActionButton>
)}
</div>
</div> </div>
</div> ))}
))} </div>
</div> </div>
</div> )
) }
} <div className="flex">
<div className="flex"> <Button className={`mt-4 flex shrink-0 ${s.autoWidth}`} onClick={onCreate} disabled={!currentWorkspace || !isCurrentWorkspaceEditor}>
<Button className={`mt-4 flex shrink-0 ${s.autoWidth}`} onClick={onCreate} disabled={!currentWorkspace || !isCurrentWorkspaceEditor}> <PlusIcon className="mr-1 flex h-4 w-4 shrink-0" />
<PlusIcon className="mr-1 flex h-4 w-4 shrink-0" /> <div className="text-xs font-medium text-text-secondary">{t('apiKeyModal.createNewSecretKey', { ns: 'appApi' })}</div>
<div className="text-xs font-medium text-text-secondary">{t('apiKeyModal.createNewSecretKey', { ns: 'appApi' })}</div> </Button>
</Button> </div>
</div> <SecretKeyGenerateModal className="shrink-0" isShow={isVisible} onClose={() => setVisible(false)} newKey={newKey} />
<SecretKeyGenerateModal className="shrink-0" isShow={isVisible} onClose={() => setVisible(false)} newKey={newKey} /> <AlertDialog
<AlertDialog open={showConfirmDelete}
open={showConfirmDelete} onOpenChange={handleDeleteConfirmOpenChange}
onOpenChange={handleDeleteConfirmOpenChange} >
> <AlertDialogContent>
<AlertDialogContent> <div className="flex flex-col gap-2 px-6 pt-6 pb-4">
<div className="flex flex-col gap-2 px-6 pt-6 pb-4"> <AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary"> {t('actionMsg.deleteConfirmTitle', { ns: 'appApi' })}
{t('actionMsg.deleteConfirmTitle', { ns: 'appApi' })} </AlertDialogTitle>
</AlertDialogTitle> <AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary"> {t('actionMsg.deleteConfirmTips', { ns: 'appApi' })}
{t('actionMsg.deleteConfirmTips', { ns: 'appApi' })} </AlertDialogDescription>
</AlertDialogDescription> </div>
</div> <AlertDialogActions>
<AlertDialogActions> <AlertDialogCancelButton>
<AlertDialogCancelButton> {t('operation.cancel', { ns: 'common' })}
{t('operation.cancel', { ns: 'common' })} </AlertDialogCancelButton>
</AlertDialogCancelButton> <AlertDialogConfirmButton onClick={onDel}>
<AlertDialogConfirmButton onClick={onDel}> {t('operation.confirm', { ns: 'common' })}
{t('operation.confirm', { ns: 'common' })} </AlertDialogConfirmButton>
</AlertDialogConfirmButton> </AlertDialogActions>
</AlertDialogActions> </AlertDialogContent>
</AlertDialogContent> </AlertDialog>
</AlertDialog> </DialogContent>
</Modal> </Dialog>
) )
} }

View File

@ -3,12 +3,12 @@
import type { FC } from 'react' import type { FC } from 'react'
import type { App as AppType } from '@/models/explore' import type { App as AppType } from '@/models/explore'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { useSuspenseQuery } from '@tanstack/react-query' import { useSuspenseQuery } from '@tanstack/react-query'
import * as React from 'react' import * as React from 'react'
import { useState } from 'react' import { useState } from 'react'
import AppUnavailable from '@/app/components/base/app-unavailable' import AppUnavailable from '@/app/components/base/app-unavailable'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal/index'
import { IS_CLOUD_EDITION } from '@/config' import { IS_CLOUD_EDITION } from '@/config'
import { systemFeaturesQueryOptions } from '@/service/system-features' import { systemFeaturesQueryOptions } from '@/service/system-features'
import { useGetTryAppInfo } from '@/service/use-try-app' import { useGetTryAppInfo } from '@/service/use-try-app'
@ -40,54 +40,59 @@ const TryApp: FC<Props> = ({
const { data: appDetail, isLoading, isError, error } = useGetTryAppInfo(appId) const { data: appDetail, isLoading, isError, error } = useGetTryAppInfo(appId)
return ( return (
<Modal <Dialog
isShow open
onClose={onClose} onOpenChange={(open) => {
className="h-[calc(100vh-32px)] max-w-[calc(100vw-32px)] min-w-[1280px] overflow-x-auto p-2" if (!open)
onClose()
}}
> >
{isLoading ? ( <DialogContent className="h-[calc(100vh-32px)] max-h-none w-full max-w-[calc(100vw-32px)] min-w-[1280px] overflow-hidden overflow-x-auto border-none p-2 text-left align-middle">
<div className="flex h-full items-center justify-center">
<Loading type="area" /> {isLoading ? (
</div> <div className="flex h-full items-center justify-center">
) : isError ? ( <Loading type="area" />
<div className="flex h-full items-center justify-center">
<AppUnavailable className="h-auto w-auto" isUnknownReason={!error} unknownReason={error instanceof Error ? error.message : undefined} />
</div>
) : !appDetail ? (
<div className="flex h-full items-center justify-center">
<AppUnavailable className="h-auto w-auto" isUnknownReason />
</div>
) : (
<div className="flex h-full flex-col">
<div className="flex shrink-0 justify-between pl-4">
<Tab
value={activeType}
onChange={setType}
disableTry={app ? !isTrialApp : false}
/>
<Button
size="large"
variant="tertiary"
className="flex size-7 items-center justify-center rounded-[10px] p-0 text-components-button-tertiary-text"
onClick={onClose}
>
<span className="i-ri-close-line size-5" />
</Button>
</div> </div>
{/* Main content */} ) : isError ? (
<div className="mt-2 flex h-0 grow justify-between space-x-2"> <div className="flex h-full items-center justify-center">
{activeType === TypeEnum.TRY ? <App appId={appId} appDetail={appDetail} /> : <Preview appId={appId} appDetail={appDetail} />} <AppUnavailable className="h-auto w-auto" isUnknownReason={!error} unknownReason={error instanceof Error ? error.message : undefined} />
<AppInfo
className="w-[360px] shrink-0"
appDetail={appDetail}
appId={appId}
category={category}
onCreate={onCreate}
/>
</div> </div>
</div> ) : !appDetail ? (
)} <div className="flex h-full items-center justify-center">
</Modal> <AppUnavailable className="h-auto w-auto" isUnknownReason />
</div>
) : (
<div className="flex h-full flex-col">
<div className="flex shrink-0 justify-between pl-4">
<Tab
value={activeType}
onChange={setType}
disableTry={app ? !isTrialApp : false}
/>
<Button
size="large"
variant="tertiary"
className="flex size-7 items-center justify-center rounded-[10px] p-0 text-components-button-tertiary-text"
onClick={onClose}
>
<span className="i-ri-close-line size-5" />
</Button>
</div>
{/* Main content */}
<div className="mt-2 flex h-0 grow justify-between space-x-2">
{activeType === TypeEnum.TRY ? <App appId={appId} appDetail={appDetail} /> : <Preview appId={appId} appDetail={appDetail} />}
<AppInfo
className="w-[360px] shrink-0"
appDetail={appDetail}
appId={appId}
category={category}
onCreate={onCreate}
/>
</div>
</div>
)}
</DialogContent>
</Dialog>
) )
} }
export default React.memo(TryApp) export default React.memo(TryApp)

View File

@ -1,15 +1,15 @@
'use client' 'use client'
import type { LangGeniusVersionResponse } from '@/models/common' import type { LangGeniusVersionResponse } from '@/models/common'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import { useSuspenseQuery } from '@tanstack/react-query' import { useSuspenseQuery } from '@tanstack/react-query'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import DifyLogo from '@/app/components/base/logo/dify-logo' import DifyLogo from '@/app/components/base/logo/dify-logo'
import Modal from '@/app/components/base/modal'
import { IS_CE_EDITION } from '@/config' import { IS_CE_EDITION } from '@/config'
import Link from '@/next/link'
import Link from '@/next/link'
import { systemFeaturesQueryOptions } from '@/service/system-features' import { systemFeaturesQueryOptions } from '@/service/system-features'
type IAccountSettingProps = { type IAccountSettingProps = {
@ -26,87 +26,92 @@ export default function AccountAbout({
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
return ( return (
<Modal <Dialog
isShow open
onClose={onCancel} onOpenChange={(open) => {
className="w-[480px]! max-w-[480px]! px-6! py-4!" if (!open)
onCancel()
}}
> >
<div className="relative"> <DialogContent className="w-[calc(100vw-2rem)]! max-w-[480px]! overflow-hidden! border-none px-6! py-4! text-left align-middle">
<div className="absolute top-0 right-0 flex h-8 w-8 cursor-pointer items-center justify-center" onClick={onCancel}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div>
<div className="flex flex-col items-center gap-4 py-8">
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? (
<img
src={systemFeatures.branding.workspace_logo}
className="block h-7 w-auto object-contain"
alt="logo"
/>
)
: <DifyLogo size="large" className="mx-auto" />}
<div className="text-center text-xs font-normal text-text-tertiary"> <div className="relative">
Version <div className="absolute top-0 right-0 flex h-8 w-8 cursor-pointer items-center justify-center" onClick={onCancel}>
{langGeniusVersionInfo?.current_version} <RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div> </div>
<div className="flex flex-col items-center gap-2 text-center text-xs font-normal text-text-secondary"> <div className="flex flex-col items-center gap-4 py-8">
<div> {systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
© ? (
{dayjs().year()} <img
{' '} src={systemFeatures.branding.workspace_logo}
LangGenius, Inc., Contributors. className="block h-7 w-auto object-contain"
alt="logo"
/>
)
: <DifyLogo size="large" className="mx-auto" />}
<div className="text-center text-xs font-normal text-text-tertiary">
Version
{langGeniusVersionInfo?.current_version}
</div> </div>
<div className="text-text-accent"> <div className="flex flex-col items-center gap-2 text-center text-xs font-normal text-text-secondary">
<div>
©
{dayjs().year()}
{' '}
LangGenius, Inc., Contributors.
</div>
<div className="text-text-accent">
{
IS_CE_EDITION
? <Link href="https://github.com/langgenius/dify/blob/main/LICENSE" target="_blank" rel="noopener noreferrer">Open Source License</Link>
: (
<>
<Link href="https://dify.ai/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</Link>
,&nbsp;
<Link href="https://dify.ai/terms" target="_blank" rel="noopener noreferrer">Terms of Service</Link>
</>
)
}
</div>
</div>
</div>
<div className="-mx-6 mb-4 h-[0.5px] bg-divider-regular" />
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 text-xs font-medium text-text-tertiary">
{ {
IS_CE_EDITION isLatest
? <Link href="https://github.com/langgenius/dify/blob/main/LICENSE" target="_blank" rel="noopener noreferrer">Open Source License</Link> ? t('about.latestAvailable', { ns: 'common', version: langGeniusVersionInfo.latest_version })
: ( : t('about.nowAvailable', { ns: 'common', version: langGeniusVersionInfo.latest_version })
<> }
<Link href="https://dify.ai/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</Link> </div>
,&nbsp; <div className="flex shrink-0 items-center">
<Link href="https://dify.ai/terms" target="_blank" rel="noopener noreferrer">Terms of Service</Link> <Button className="mr-2" size="small">
</> <Link
) href="https://github.com/langgenius/dify/releases"
target="_blank"
rel="noopener noreferrer"
>
{t('about.changeLog', { ns: 'common' })}
</Link>
</Button>
{
!isLatest && !IS_CE_EDITION && (
<Button variant="primary" size="small">
<Link
href={langGeniusVersionInfo.release_notes}
target="_blank"
rel="noopener noreferrer"
>
{t('about.updateNow', { ns: 'common' })}
</Link>
</Button>
)
} }
</div> </div>
</div> </div>
</div> </div>
<div className="-mx-8 mb-4 h-[0.5px] bg-divider-regular" /> </DialogContent>
<div className="flex items-center justify-between"> </Dialog>
<div className="text-xs font-medium text-text-tertiary">
{
isLatest
? t('about.latestAvailable', { ns: 'common', version: langGeniusVersionInfo.latest_version })
: t('about.nowAvailable', { ns: 'common', version: langGeniusVersionInfo.latest_version })
}
</div>
<div className="flex items-center">
<Button className="mr-2" size="small">
<Link
href="https://github.com/langgenius/dify/releases"
target="_blank"
rel="noopener noreferrer"
>
{t('about.changeLog', { ns: 'common' })}
</Link>
</Button>
{
!isLatest && !IS_CE_EDITION && (
<Button variant="primary" size="small">
<Link
href={langGeniusVersionInfo.release_notes}
target="_blank"
rel="noopener noreferrer"
>
{t('about.updateNow', { ns: 'common' })}
</Link>
</Button>
)
}
</div>
</div>
</div>
</Modal>
) )
} }

View File

@ -1,12 +1,11 @@
import type { FC } from 'react' import type { FC } from 'react'
import type { ApiBasedExtension } from '@/models/common' import type { ApiBasedExtension } from '@/models/common'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { noop } from 'es-toolkit/function'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education' import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
import Modal from '@/app/components/base/modal'
import { useDocLink } from '@/context/i18n' import { useDocLink } from '@/context/i18n'
import { addApiBasedExtension, updateApiBasedExtension } from '@/service/common' import { addApiBasedExtension, updateApiBasedExtension } from '@/service/common'
@ -61,45 +60,48 @@ const ApiBasedExtensionModal: FC<ApiBasedExtensionModalProps> = ({ data, onCance
} }
} }
return ( return (
<Modal isShow onClose={noop} wrapperClassName="z-1002" className="w-[640px]! max-w-none! p-8! pb-6!"> <Dialog open>
<div className="mb-2 text-xl font-semibold text-text-primary"> <DialogContent className="w-[640px]! max-w-none! overflow-hidden! border-none p-8! pb-6! text-left align-middle">
{data.name
? t('apiBasedExtension.modal.editTitle', { ns: 'common' }) <div className="mb-2 text-xl font-semibold text-text-primary">
: t('apiBasedExtension.modal.title', { ns: 'common' })} {data.name
</div> ? t('apiBasedExtension.modal.editTitle', { ns: 'common' })
<div className="py-2"> : t('apiBasedExtension.modal.title', { ns: 'common' })}
<div className="text-sm leading-9 font-medium text-text-primary">
{t('apiBasedExtension.modal.name.title', { ns: 'common' })}
</div> </div>
<input value={localeData.name || ''} onChange={e => handleDataChange('name', e.target.value)} className="block h-9 w-full appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-text-primary outline-hidden" placeholder={t('apiBasedExtension.modal.name.placeholder', { ns: 'common' }) || ''} /> <div className="py-2">
</div> <div className="text-sm leading-9 font-medium text-text-primary">
<div className="py-2"> {t('apiBasedExtension.modal.name.title', { ns: 'common' })}
<div className="flex h-9 items-center justify-between text-sm font-medium text-text-primary"> </div>
{t('apiBasedExtension.modal.apiEndpoint.title', { ns: 'common' })} <input value={localeData.name || ''} onChange={e => handleDataChange('name', e.target.value)} className="block h-9 w-full appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-text-primary outline-hidden" placeholder={t('apiBasedExtension.modal.name.placeholder', { ns: 'common' }) || ''} />
<a href={docLink('/use-dify/workspace/api-extension/api-extension')} target="_blank" rel="noopener noreferrer" className="group flex items-center text-xs font-normal text-text-accent">
<BookOpen01 className="mr-1 h-3 w-3" />
{t('apiBasedExtension.link', { ns: 'common' })}
</a>
</div> </div>
<input value={localeData.api_endpoint || ''} onChange={e => handleDataChange('api_endpoint', e.target.value)} className="block h-9 w-full appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-text-primary outline-hidden" placeholder={t('apiBasedExtension.modal.apiEndpoint.placeholder', { ns: 'common' }) || ''} /> <div className="py-2">
</div> <div className="flex h-9 items-center justify-between text-sm font-medium text-text-primary">
<div className="py-2"> {t('apiBasedExtension.modal.apiEndpoint.title', { ns: 'common' })}
<div className="text-sm leading-9 font-medium text-text-primary"> <a href={docLink('/use-dify/workspace/api-extension/api-extension')} target="_blank" rel="noopener noreferrer" className="group flex items-center text-xs font-normal text-text-accent">
{t('apiBasedExtension.modal.apiKey.title', { ns: 'common' })} <BookOpen01 className="mr-1 h-3 w-3" />
{t('apiBasedExtension.link', { ns: 'common' })}
</a>
</div>
<input value={localeData.api_endpoint || ''} onChange={e => handleDataChange('api_endpoint', e.target.value)} className="block h-9 w-full appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-text-primary outline-hidden" placeholder={t('apiBasedExtension.modal.apiEndpoint.placeholder', { ns: 'common' }) || ''} />
</div> </div>
<div className="flex items-center"> <div className="py-2">
<input value={localeData.api_key || ''} onChange={e => handleDataChange('api_key', e.target.value)} className="mr-2 block h-9 grow appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-text-primary outline-hidden" placeholder={t('apiBasedExtension.modal.apiKey.placeholder', { ns: 'common' }) || ''} /> <div className="text-sm leading-9 font-medium text-text-primary">
{t('apiBasedExtension.modal.apiKey.title', { ns: 'common' })}
</div>
<div className="flex items-center">
<input value={localeData.api_key || ''} onChange={e => handleDataChange('api_key', e.target.value)} className="mr-2 block h-9 grow appearance-none rounded-lg bg-components-input-bg-normal px-3 text-sm text-text-primary outline-hidden" placeholder={t('apiBasedExtension.modal.apiKey.placeholder', { ns: 'common' }) || ''} />
</div>
</div> </div>
</div> <div className="mt-6 flex items-center justify-end">
<div className="mt-6 flex items-center justify-end"> <Button onClick={onCancel} className="mr-2">
<Button onClick={onCancel} className="mr-2"> {t('operation.cancel', { ns: 'common' })}
{t('operation.cancel', { ns: 'common' })} </Button>
</Button> <Button variant="primary" disabled={!localeData.name || !localeData.api_endpoint || !localeData.api_key || loading} onClick={handleSave}>
<Button variant="primary" disabled={!localeData.name || !localeData.api_endpoint || !localeData.api_key || loading} onClick={handleSave}> {t('operation.save', { ns: 'common' })}
{t('operation.save', { ns: 'common' })} </Button>
</Button> </div>
</div> </DialogContent>
</Modal> </Dialog>
) )
} }
export default ApiBasedExtensionModal export default ApiBasedExtensionModal

View File

@ -9,11 +9,11 @@ import {
} from '@langgenius/dify-ui/alert-dialog' } from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal'
import { SwitchCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth' import { SwitchCredentialInLoadBalancing } from '@/app/components/header/account-setting/model-provider-page/model-auth'
import { useGetModelCredential, useUpdateModelLoadBalancingConfig } from '@/service/use-models' import { useGetModelCredential, useUpdateModelLoadBalancingConfig } from '@/service/use-models'
import { ConfigurationMethodEnum, FormTypeEnum } from '../declarations' import { ConfigurationMethodEnum, FormTypeEnum } from '../declarations'
@ -180,99 +180,103 @@ const ModelLoadBalancingModal = ({ provider, configurateMethod, currentCustomCon
}, [refetch, onClose]) }, [refetch, onClose])
return ( return (
<> <>
<Modal <Dialog
isShow={Boolean(model) && open} open={Boolean(model) && open}
onClose={onClose} onOpenChange={(open) => {
wrapperClassName="z-1002" if (!open)
className="w-[640px] max-w-none px-8 pt-8" onClose?.()
title={( }}
<div className="pb-3 font-semibold">
<div className="h-[30px]">
{draftConfig?.enabled
? t('modelProvider.auth.configLoadBalancing', { ns: 'common' })
: t('modelProvider.auth.configModel', { ns: 'common' })}
</div>
{Boolean(model) && (
<div className="flex h-5 items-center">
<ModelIcon className="mr-2 shrink-0" provider={provider} modelName={model!.model} />
<ModelName className="grow system-md-regular text-text-secondary" modelItem={model!} showModelType showMode showContextSize />
</div>
)}
</div>
)}
> >
{!draftConfig <DialogContent className="w-[640px] max-w-none overflow-hidden! border-none px-8 pt-8 text-left align-middle">
? <Loading type="area" /> <DialogTitle className="title-2xl-semi-bold text-text-primary">
: ( <div className="pb-3 font-semibold">
<> <div className="h-[30px]">
<div className="py-2"> {draftConfig?.enabled
<div className={cn('min-h-16 rounded-xl border bg-components-panel-bg transition-colors', draftConfig.enabled ? 'cursor-pointer border-components-panel-border' : 'cursor-default border-util-colors-blue-blue-600')} onClick={draftConfig.enabled ? () => toggleModalBalancing(false) : undefined}> ? t('modelProvider.auth.configLoadBalancing', { ns: 'common' })
<div className="flex items-center gap-2 px-[15px] py-3 select-none"> : t('modelProvider.auth.configModel', { ns: 'common' })}
<div className="flex h-8 w-8 shrink-0 grow-0 items-center justify-center rounded-lg border border-components-card-border bg-components-card-bg"> </div>
{Boolean(model) && (<ModelIcon className="shrink-0" provider={provider} modelName={model!.model} />)} {Boolean(model) && (
</div> <div className="flex h-5 items-center">
<div className="grow"> <ModelIcon className="mr-2 shrink-0" provider={provider} modelName={model!.model} />
<div className="text-sm text-text-secondary"> <ModelName className="grow system-md-regular text-text-secondary" modelItem={model!} showModelType showMode showContextSize />
{providerFormSchemaPredefined
? t('modelProvider.auth.providerManaged', { ns: 'common' })
: t('modelProvider.auth.specifyModelCredential', { ns: 'common' })}
</div>
<div className="text-xs text-text-tertiary">
{providerFormSchemaPredefined
? t('modelProvider.auth.providerManagedTip', { ns: 'common' })
: t('modelProvider.auth.specifyModelCredentialTip', { ns: 'common' })}
</div>
</div>
{!providerFormSchemaPredefined && (<SwitchCredentialInLoadBalancing provider={provider} customModelCredential={customModelCredential ?? initialCustomModelCredential} setCustomModelCredential={setCustomModelCredential} model={model} credentials={available_credentials} onUpdate={handleUpdateWhenSwitchCredential} onRemove={handleUpdateWhenSwitchCredential} />)}
</div>
</div>
{modelCredential && (
<ModelLoadBalancingConfigs {...{
draftConfig,
setDraftConfig,
provider,
currentCustomConfigurationModelFixedFields: {
__model_name: model.model,
__model_type: model.model_type,
},
configurationMethod: model.fetch_from,
className: 'mt-2',
modelCredential,
onUpdate: handleUpdate,
onRemove: handleUpdateWhenSwitchCredential,
model: {
model: model.model,
model_type: model.model_type,
},
}}
/>
)}
</div> </div>
)}
</div>
</DialogTitle>
<div className="mt-6 flex items-center justify-between gap-2"> {!draftConfig
<div> ? <Loading type="area" />
{!providerFormSchemaPredefined && ( : (
<Button onClick={() => openConfirmDelete(undefined, { model: model.model, model_type: model.model_type })} className="text-components-button-destructive-secondary-text"> <>
{t('modelProvider.auth.removeModel', { ns: 'common' })} <div className="py-2">
</Button> <div className={cn('min-h-16 rounded-xl border bg-components-panel-bg transition-colors', draftConfig.enabled ? 'cursor-pointer border-components-panel-border' : 'cursor-default border-util-colors-blue-blue-600')} onClick={draftConfig.enabled ? () => toggleModalBalancing(false) : undefined}>
<div className="flex items-center gap-2 px-[15px] py-3 select-none">
<div className="flex h-8 w-8 shrink-0 grow-0 items-center justify-center rounded-lg border border-components-card-border bg-components-card-bg">
{Boolean(model) && (<ModelIcon className="shrink-0" provider={provider} modelName={model!.model} />)}
</div>
<div className="grow">
<div className="text-sm text-text-secondary">
{providerFormSchemaPredefined
? t('modelProvider.auth.providerManaged', { ns: 'common' })
: t('modelProvider.auth.specifyModelCredential', { ns: 'common' })}
</div>
<div className="text-xs text-text-tertiary">
{providerFormSchemaPredefined
? t('modelProvider.auth.providerManagedTip', { ns: 'common' })
: t('modelProvider.auth.specifyModelCredentialTip', { ns: 'common' })}
</div>
</div>
{!providerFormSchemaPredefined && (<SwitchCredentialInLoadBalancing provider={provider} customModelCredential={customModelCredential ?? initialCustomModelCredential} setCustomModelCredential={setCustomModelCredential} model={model} credentials={available_credentials} onUpdate={handleUpdateWhenSwitchCredential} onRemove={handleUpdateWhenSwitchCredential} />)}
</div>
</div>
{modelCredential && (
<ModelLoadBalancingConfigs {...{
draftConfig,
setDraftConfig,
provider,
currentCustomConfigurationModelFixedFields: {
__model_name: model.model,
__model_type: model.model_type,
},
configurationMethod: model.fetch_from,
className: 'mt-2',
modelCredential,
onUpdate: handleUpdate,
onRemove: handleUpdateWhenSwitchCredential,
model: {
model: model.model,
model_type: model.model_type,
},
}}
/>
)} )}
</div> </div>
<div className="space-x-2">
<Button onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button> <div className="mt-6 flex items-center justify-between gap-2">
<Button <div>
variant="primary" {!providerFormSchemaPredefined && (
onClick={handleSave} <Button onClick={() => openConfirmDelete(undefined, { model: model.model, model_type: model.model_type })} className="text-components-button-destructive-secondary-text">
disabled={loading {t('modelProvider.auth.removeModel', { ns: 'common' })}
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2) </Button>
|| isLoading} )}
> </div>
{t('operation.save', { ns: 'common' })} <div className="space-x-2">
</Button> <Button onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button
variant="primary"
onClick={handleSave}
disabled={loading
|| (draftConfig?.enabled && (draftConfig?.configs.filter(config => config.enabled).length ?? 0) < 2)
|| isLoading}
>
{t('operation.save', { ns: 'common' })}
</Button>
</div>
</div> </div>
</div> </>
</> )}
)} </DialogContent>
</Modal> </Dialog>
<AlertDialog open={!!deleteModel} onOpenChange={open => !open && closeConfirmDelete()}> <AlertDialog open={!!deleteModel} onOpenChange={open => !open && closeConfirmDelete()}>
<AlertDialogContent> <AlertDialogContent>
<div className="flex flex-col gap-2 px-6 pt-6 pb-4"> <div className="flex flex-col gap-2 px-6 pt-6 pb-4">

View File

@ -2,10 +2,10 @@
import type { FC } from 'react' import type { FC } from 'react'
import type { Dependency } from '../../types' import type { Dependency } from '../../types'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import * as React from 'react' import * as React from 'react'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import { InstallStep } from '../../types' import { InstallStep } from '../../types'
import useHideLogic from '../hooks/use-hide-logic' import useHideLogic from '../hooks/use-hide-logic'
import ReadyToInstall from './ready-to-install' import ReadyToInstall from './ready-to-install'
@ -50,26 +50,31 @@ const InstallBundle: FC<Props> = ({
}, [step, t]) }, [step, t])
return ( return (
<Modal <Dialog
isShow={true} open
onClose={foldAnimInto} onOpenChange={(open) => {
className={cn(modalClassName, 'shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0')} if (!open)
closable foldAnimInto()
}}
> >
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6"> <DialogContent className={cn('relative w-full max-w-[480px] overflow-hidden! text-left align-middle', cn(modalClassName, 'shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0'))}>
<div className="self-stretch title-2xl-semi-bold text-text-primary"> <DialogCloseButton data-testid="modal-close-button" />
{getTitle()}
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<div className="self-stretch title-2xl-semi-bold text-text-primary">
{getTitle()}
</div>
</div> </div>
</div> <ReadyToInstall
<ReadyToInstall step={step}
step={step} onStepChange={setStep}
onStepChange={setStep} onStartToInstall={handleStartToInstall}
onStartToInstall={handleStartToInstall} setIsInstalling={setIsInstalling}
setIsInstalling={setIsInstalling} allPlugins={fromDSLPayload}
allPlugins={fromDSLPayload} onClose={onClose}
onClose={onClose} />
/> </DialogContent>
</Modal> </Dialog>
) )
} }

View File

@ -3,11 +3,11 @@
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../types' import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../types'
import type { InstallState } from '@/app/components/plugins/types' import type { InstallState } from '@/app/components/plugins/types'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react' import * as React from 'react'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon' import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { InstallStepFromGitHub } from '../../types' import { InstallStepFromGitHub } from '../../types'
import Installed from '../base/installed' import Installed from '../base/installed'
@ -160,74 +160,80 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
} }
return ( return (
<Modal <Dialog
isShow={true} open
onClose={foldAnimInto} onOpenChange={(open) => {
className={cn(modalClassName, `shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] if (!open)
border-components-panel-border bg-components-panel-bg p-0`)} foldAnimInto()
closable }}
> >
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6"> <DialogContent className={cn('w-[560px] max-w-none! overflow-hidden! text-left align-middle', cn(modalClassName, `shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px]
<div className="flex grow flex-col items-start gap-1"> border-components-panel-border bg-components-panel-bg p-0`))}
<div className="self-stretch title-2xl-semi-bold text-text-primary"> >
{getTitle()} <DialogCloseButton data-testid="modal-close-button" />
</div>
<div className="self-stretch system-xs-regular text-text-tertiary"> <div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
{!([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step)) && t('installFromGitHub.installNote', { ns: 'plugin' })} <div className="flex grow flex-col items-start gap-1">
<div className="self-stretch title-2xl-semi-bold text-text-primary">
{getTitle()}
</div>
<div className="self-stretch system-xs-regular text-text-tertiary">
{!([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step)) && t('installFromGitHub.installNote', { ns: 'plugin' })}
</div>
</div> </div>
</div> </div>
</div> {([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step))
{([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step)) ? (
? ( <Installed
<Installed payload={manifest}
payload={manifest} isFailed={[InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installFailed].includes(state.step)}
isFailed={[InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installFailed].includes(state.step)} errMsg={errorMsg}
errMsg={errorMsg} onCancel={onClose}
onCancel={onClose} />
/> )
) : (
: ( <div className={`flex flex-col items-start justify-center self-stretch px-6 py-3 ${state.step === InstallStepFromGitHub.installed ? 'gap-2' : 'gap-4'}`}>
<div className={`flex flex-col items-start justify-center self-stretch px-6 py-3 ${state.step === InstallStepFromGitHub.installed ? 'gap-2' : 'gap-4'}`}> {state.step === InstallStepFromGitHub.setUrl && (
{state.step === InstallStepFromGitHub.setUrl && ( <SetURL
<SetURL repoUrl={state.repoUrl}
repoUrl={state.repoUrl} onChange={value => setState(prevState => ({ ...prevState, repoUrl: value }))}
onChange={value => setState(prevState => ({ ...prevState, repoUrl: value }))} onNext={handleUrlSubmit}
onNext={handleUrlSubmit} onCancel={onClose}
onCancel={onClose} />
/> )}
)} {state.step === InstallStepFromGitHub.selectPackage && (
{state.step === InstallStepFromGitHub.selectPackage && ( <SelectPackage
<SelectPackage updatePayload={updatePayload!}
updatePayload={updatePayload!} repoUrl={state.repoUrl}
repoUrl={state.repoUrl} selectedVersion={state.selectedVersion}
selectedVersion={state.selectedVersion} versions={versions}
versions={versions} onSelectVersion={item => setState(prevState => ({ ...prevState, selectedVersion: String(item.value) }))}
onSelectVersion={item => setState(prevState => ({ ...prevState, selectedVersion: String(item.value) }))} selectedPackage={state.selectedPackage}
selectedPackage={state.selectedPackage} packages={packages}
packages={packages} onSelectPackage={item => setState(prevState => ({ ...prevState, selectedPackage: String(item.value) }))}
onSelectPackage={item => setState(prevState => ({ ...prevState, selectedPackage: String(item.value) }))} onUploaded={handleUploaded}
onUploaded={handleUploaded} onFailed={handleUploadFail}
onFailed={handleUploadFail} onBack={handleBack}
onBack={handleBack} />
/> )}
)} {state.step === InstallStepFromGitHub.readyToInstall && (
{state.step === InstallStepFromGitHub.readyToInstall && ( <Loaded
<Loaded updatePayload={updatePayload!}
updatePayload={updatePayload!} uniqueIdentifier={uniqueIdentifier!}
uniqueIdentifier={uniqueIdentifier!} payload={manifest as any}
payload={manifest as any} repoUrl={state.repoUrl}
repoUrl={state.repoUrl} selectedVersion={state.selectedVersion}
selectedVersion={state.selectedVersion} selectedPackage={state.selectedPackage}
selectedPackage={state.selectedPackage} onBack={handleBack}
onBack={handleBack} onStartToInstall={handleStartToInstall}
onStartToInstall={handleStartToInstall} onInstalled={handleInstalled}
onInstalled={handleInstalled} onFailed={handleFailed}
onFailed={handleFailed} />
/> )}
)} </div>
</div> )}
)} </DialogContent>
</Modal> </Dialog>
) )
} }

View File

@ -2,10 +2,10 @@
import type { Dependency, PluginDeclaration } from '../../types' import type { Dependency, PluginDeclaration } from '../../types'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import * as React from 'react' import * as React from 'react'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon' import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { InstallStep } from '../../types' import { InstallStep } from '../../types'
import useHideLogic from '../hooks/use-hide-logic' import useHideLogic from '../hooks/use-hide-logic'
@ -86,52 +86,57 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
}, []) }, [])
return ( return (
<Modal <Dialog
isShow={true} open
onClose={foldAnimInto} onOpenChange={(open) => {
className={cn(modalClassName, 'shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0')} if (!open)
closable foldAnimInto()
}}
> >
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6"> <DialogContent className={cn('w-[560px] max-w-none! overflow-hidden! text-left align-middle', cn(modalClassName, 'shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0'))}>
<div className="self-stretch title-2xl-semi-bold text-text-primary"> <DialogCloseButton data-testid="modal-close-button" />
{getTitle()}
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<div className="self-stretch title-2xl-semi-bold text-text-primary">
{getTitle()}
</div>
</div> </div>
</div> {step === InstallStep.uploading && (
{step === InstallStep.uploading && ( <Uploading
<Uploading isBundle={isBundle}
isBundle={isBundle} file={file}
file={file} onCancel={onClose}
onCancel={onClose} onPackageUploaded={handlePackageUploaded}
onPackageUploaded={handlePackageUploaded} onBundleUploaded={handleBundleUploaded}
onBundleUploaded={handleBundleUploaded} onFailed={handleUploadFail}
onFailed={handleUploadFail} />
/> )}
)} {isBundle
{isBundle ? (
? ( <ReadyToInstallBundle
<ReadyToInstallBundle step={step}
step={step} onStepChange={setStep}
onStepChange={setStep} onStartToInstall={handleStartToInstall}
onStartToInstall={handleStartToInstall} setIsInstalling={setIsInstalling}
setIsInstalling={setIsInstalling} onClose={onClose}
onClose={onClose} allPlugins={dependencies}
allPlugins={dependencies} />
/> )
) : (
: ( <ReadyToInstallPackage
<ReadyToInstallPackage step={step}
step={step} onStepChange={setStep}
onStepChange={setStep} onStartToInstall={handleStartToInstall}
onStartToInstall={handleStartToInstall} setIsInstalling={setIsInstalling}
setIsInstalling={setIsInstalling} onClose={onClose}
onClose={onClose} uniqueIdentifier={uniqueIdentifier}
uniqueIdentifier={uniqueIdentifier} manifest={manifest}
manifest={manifest} errorMsg={errorMsg}
errorMsg={errorMsg} onError={setErrorMsg}
onError={setErrorMsg} />
/> )}
)} </DialogContent>
</Modal> </Dialog>
) )
} }

View File

@ -2,10 +2,10 @@
import type { Dependency, Plugin, PluginManifestInMarket } from '../../types' import type { Dependency, Plugin, PluginManifestInMarket } from '../../types'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import * as React from 'react' import * as React from 'react'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import { InstallStep } from '../../types' import { InstallStep } from '../../types'
import Installed from '../base/installed' import Installed from '../base/installed'
import useHideLogic from '../hooks/use-hide-logic' import useHideLogic from '../hooks/use-hide-logic'
@ -70,60 +70,64 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
}, [setIsInstalling]) }, [setIsInstalling])
return ( return (
<Modal <Dialog
isShow={true} open
onClose={foldAnimInto} onOpenChange={(open) => {
wrapperClassName="z-9999" if (!open)
className={cn(modalClassName, 'shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0')} foldAnimInto()
closable }}
> >
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6"> <DialogContent className={cn('w-[560px] max-w-none! overflow-hidden! text-left align-middle', cn(modalClassName, 'shadows-shadow-xl flex min-w-[560px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0'))}>
<div className="self-stretch title-2xl-semi-bold text-text-primary"> <DialogCloseButton data-testid="modal-close-button" />
{getTitle()}
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<div className="self-stretch title-2xl-semi-bold text-text-primary">
{getTitle()}
</div>
</div> </div>
</div> {
{ isBundle
isBundle ? (
? ( <ReadyToInstallBundle
<ReadyToInstallBundle step={step}
step={step} onStepChange={setStep}
onStepChange={setStep} onStartToInstall={handleStartToInstall}
onStartToInstall={handleStartToInstall} setIsInstalling={setIsInstalling}
setIsInstalling={setIsInstalling} onClose={onClose}
onClose={onClose} allPlugins={dependencies!}
allPlugins={dependencies!} isFromMarketPlace
isFromMarketPlace />
/> )
) : (
: ( <>
<> {
{ step === InstallStep.readyToInstall && (
step === InstallStep.readyToInstall && ( <Install
<Install uniqueIdentifier={uniqueIdentifier}
uniqueIdentifier={uniqueIdentifier} payload={manifest!}
payload={manifest!} onCancel={onClose}
onCancel={onClose} onInstalled={handleInstalled}
onInstalled={handleInstalled} onFailed={handleFailed}
onFailed={handleFailed} onStartToInstall={handleStartToInstall}
onStartToInstall={handleStartToInstall} />
/> )
) }
} {
{ [InstallStep.installed, InstallStep.installFailed].includes(step) && (
[InstallStep.installed, InstallStep.installFailed].includes(step) && ( <Installed
<Installed payload={manifest!}
payload={manifest!} isMarketPayload
isMarketPayload isFailed={step === InstallStep.installFailed}
isFailed={step === InstallStep.installFailed} errMsg={errorMsg}
errMsg={errorMsg} onCancel={onSuccess}
onCancel={onSuccess} />
/> )
) }
} </>
</> )
) }
} </DialogContent>
</Modal> </Dialog>
) )
} }

View File

@ -2,14 +2,11 @@ import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import SchemaModal from '../schema-modal' import SchemaModal from '../schema-modal'
vi.mock('@/app/components/base/modal', () => ({ vi.mock('@langgenius/dify-ui/dialog', () => ({
default: ({ Dialog: ({ children, open }: { children: React.ReactNode, open?: boolean }) =>
children, open === false ? null : <>{children}</>,
isShow, DialogContent: ({ children }: { children: React.ReactNode }) => <div data-testid="modal">{children}</div>,
}: { DialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
children: React.ReactNode
isShow: boolean
}) => isShow ? <div data-testid="modal">{children}</div> : null,
})) }))
vi.mock('@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor', () => ({ vi.mock('@/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor', () => ({

View File

@ -2,9 +2,9 @@ import type { UseMutationResult } from '@tanstack/react-query'
import type { FC, ReactNode } from 'react' import type { FC, ReactNode } from 'react'
import type { Plugin } from '../types' import type { Plugin } from '../types'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import * as React from 'react' import * as React from 'react'
import { memo } from 'react' import { memo } from 'react'
import Modal from '@/app/components/base/modal'
import Card from '@/app/components/plugins/card' import Card from '@/app/components/plugins/card'
type Props = { type Props = {
@ -33,45 +33,52 @@ const PluginMutationModal: FC<Props> = ({
modalBottomLeft, modalBottomLeft,
}: Props) => { }: Props) => {
return ( return (
<Modal <Dialog
isShow={true} open
onClose={onCancel} onOpenChange={(open) => {
className="min-w-[560px]" if (!open)
closable onCancel()
title={modelTitle} }}
> >
<div className="mt-3 mb-2 system-md-regular text-text-secondary"> <DialogContent className="w-full min-w-[560px] overflow-hidden! border-none text-left align-middle">
{description} <DialogCloseButton data-testid="modal-close-button" />
</div> <DialogTitle className="title-2xl-semi-bold text-text-primary">
<div className="flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2"> {modelTitle}
<Card </DialogTitle>
installed={mutation.isSuccess}
payload={plugin} <div className="mt-3 mb-2 system-md-regular text-text-secondary">
className="w-full" {description}
titleLeft={cardTitleLeft}
/>
</div>
<div className="flex items-center gap-2 self-stretch pt-5">
<div>
{modalBottomLeft}
</div> </div>
<div className="ml-auto flex gap-2"> <div className="flex flex-wrap content-start items-start gap-1 self-stretch rounded-2xl bg-background-section-burn p-2">
{!mutation.isPending && ( <Card
<Button onClick={onCancel}> installed={mutation.isSuccess}
{cancelButtonText} payload={plugin}
className="w-full"
titleLeft={cardTitleLeft}
/>
</div>
<div className="flex items-center gap-2 self-stretch pt-5">
<div>
{modalBottomLeft}
</div>
<div className="ml-auto flex gap-2">
{!mutation.isPending && (
<Button onClick={onCancel}>
{cancelButtonText}
</Button>
)}
<Button
variant="primary"
loading={mutation.isPending}
onClick={mutate}
disabled={mutation.isPending}
>
{confirmButtonText}
</Button> </Button>
)} </div>
<Button
variant="primary"
loading={mutation.isPending}
onClick={mutate}
disabled={mutation.isPending}
>
{confirmButtonText}
</Button>
</div> </div>
</div> </DialogContent>
</Modal> </Dialog>
) )
} }

View File

@ -2,17 +2,19 @@ import { render, screen } from '@testing-library/react'
import * as React from 'react' import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../../base/modal', () => ({ vi.mock('@langgenius/dify-ui/dialog', () => ({
default: ({ children, title, isShow }: { children: React.ReactNode, title: string, isShow: boolean }) => ( Dialog: ({ children, open }: { children: React.ReactNode, open?: boolean }) => (
isShow open !== false
? ( ? (
<div data-testid="modal"> <div data-testid="modal">
<div data-testid="modal-title">{title}</div>
{children} {children}
</div> </div>
) )
: null : null
), ),
DialogContent: ({ children }: { children: React.ReactNode }) => <>{children}</>,
DialogTitle: ({ children }: { children: React.ReactNode }) => <div data-testid="modal-title">{children}</div>,
DialogCloseButton: () => <button type="button">Close</button>,
})) }))
vi.mock('../../base/key-value-item', () => ({ vi.mock('../../base/key-value-item', () => ({

View File

@ -1,8 +1,8 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import * as React from 'react' import * as React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '../../base/modal'
import KeyValueItem from '../base/key-value-item' import KeyValueItem from '../base/key-value-item'
import { convertRepoToUrl } from '../install-plugin/utils' import { convertRepoToUrl } from '../install-plugin/utils'
@ -23,19 +23,26 @@ const PlugInfo: FC<Props> = ({
const { t } = useTranslation() const { t } = useTranslation()
const labelWidthClassName = 'w-[96px]' const labelWidthClassName = 'w-[96px]'
return ( return (
<Modal <Dialog
title={t(`${i18nPrefix}.title`, { ns: 'plugin' })} open
className="w-[480px]" onOpenChange={(open) => {
isShow if (!open)
onClose={onHide} onHide()
closable }}
> >
<div className="mt-5 space-y-3"> <DialogContent className="w-full max-w-[480px]! overflow-hidden! border-none text-left align-middle">
{repository && <KeyValueItem label={t(`${i18nPrefix}.repository`, { ns: 'plugin' })} labelWidthClassName={labelWidthClassName} value={`${convertRepoToUrl(repository)}`} valueMaxWidthClassName="max-w-[190px]" />} <DialogCloseButton data-testid="modal-close-button" />
{release && <KeyValueItem label={t(`${i18nPrefix}.release`, { ns: 'plugin' })} labelWidthClassName={labelWidthClassName} value={release} />} <DialogTitle className="title-2xl-semi-bold text-text-primary">
{packageName && <KeyValueItem label={t(`${i18nPrefix}.packageName`, { ns: 'plugin' })} labelWidthClassName={labelWidthClassName} value={packageName} />} {t(`${i18nPrefix}.title`, { ns: 'plugin' })}
</div> </DialogTitle>
</Modal>
<div className="mt-5 space-y-3">
{repository && <KeyValueItem label={t(`${i18nPrefix}.repository`, { ns: 'plugin' })} labelWidthClassName={labelWidthClassName} value={`${convertRepoToUrl(repository)}`} valueMaxWidthClassName="max-w-[190px]" />}
{release && <KeyValueItem label={t(`${i18nPrefix}.release`, { ns: 'plugin' })} labelWidthClassName={labelWidthClassName} value={release} />}
{packageName && <KeyValueItem label={t(`${i18nPrefix}.packageName`, { ns: 'plugin' })} labelWidthClassName={labelWidthClassName} value={packageName} />}
</div>
</DialogContent>
</Dialog>
) )
} }
export default React.memo(PlugInfo) export default React.memo(PlugInfo)

View File

@ -14,28 +14,25 @@ const mockSystemFeatures = { enable_marketplace: true }
const render = (ui: ReactElement) => const render = (ui: ReactElement) =>
renderWithSystemFeatures(ui, { systemFeatures: mockSystemFeatures }) renderWithSystemFeatures(ui, { systemFeatures: mockSystemFeatures })
// Mock Modal component let mockDialogOnOpenChange: ((open: boolean) => void) | undefined
vi.mock('@/app/components/base/modal', () => ({
default: ({ children, isShow, onClose, closable, className }: { vi.mock('@langgenius/dify-ui/dialog', () => ({
Dialog: ({ children, open, onOpenChange }: {
children: React.ReactNode children: React.ReactNode
isShow: boolean open?: boolean
onClose: () => void onOpenChange?: (open: boolean) => void
closable?: boolean
className?: string
}) => { }) => {
if (!isShow) mockDialogOnOpenChange = onOpenChange
return null return open === false ? null : <>{children}</>
return (
<div data-testid="modal" className={className}>
{closable && (
<button data-testid="modal-close" onClick={onClose}>
Close
</button>
)}
{children}
</div>
)
}, },
DialogContent: ({ children, className }: { children: React.ReactNode, className?: string }) => (
<div data-testid="modal" className={className}>{children}</div>
),
DialogCloseButton: () => (
<button data-testid="modal-close" onClick={() => mockDialogOnOpenChange?.(false)}>
Close
</button>
),
})) }))
// Mock OptionCard component // Mock OptionCard component

View File

@ -3,11 +3,11 @@ import type { FC } from 'react'
import type { AutoUpdateConfig } from './auto-update-setting/types' import type { AutoUpdateConfig } from './auto-update-setting/types'
import type { Permissions, ReferenceSetting } from '@/app/components/plugins/types' import type { Permissions, ReferenceSetting } from '@/app/components/plugins/types'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import { useSuspenseQuery } from '@tanstack/react-query' import { useSuspenseQuery } from '@tanstack/react-query'
import * as React from 'react' import * as React from 'react'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import { PermissionType } from '@/app/components/plugins/types' import { PermissionType } from '@/app/components/plugins/types'
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card' import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import { systemFeaturesQueryOptions } from '@/service/system-features' import { systemFeaturesQueryOptions } from '@/service/system-features'
@ -53,59 +53,64 @@ const PluginSettingModal: FC<Props> = ({
}, [onHide, onSave, tempAutoUpdateConfig, tempPrivilege]) }, [onHide, onSave, tempAutoUpdateConfig, tempPrivilege])
return ( return (
<Modal <Dialog
isShow open
onClose={onHide} onOpenChange={(open) => {
closable if (!open)
className="w-[620px] max-w-[620px] p-0!" onHide()
}}
> >
<div className="shadows-shadow-xl flex w-full flex-col items-start rounded-2xl border border-components-panel-border bg-components-panel-bg"> <DialogContent className="w-[620px] max-w-[620px] overflow-hidden! border-none p-0! text-left align-middle">
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6"> <DialogCloseButton data-testid="modal-close-button" />
<span className="self-stretch title-2xl-semi-bold text-text-primary">{t(`${i18nPrefix}.title`, { ns: 'plugin' })}</span>
</div> <div className="shadows-shadow-xl flex w-full flex-col items-start rounded-2xl border border-components-panel-border bg-components-panel-bg">
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3"> <div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
{[ <span className="self-stretch title-2xl-semi-bold text-text-primary">{t(`${i18nPrefix}.title`, { ns: 'plugin' })}</span>
{ title: t(`${i18nPrefix}.whoCanInstall`, { ns: 'plugin' }), key: 'install_permission', value: tempPrivilege?.install_permission || PermissionType.noOne }, </div>
{ title: t(`${i18nPrefix}.whoCanDebug`, { ns: 'plugin' }), key: 'debug_permission', value: tempPrivilege?.debug_permission || PermissionType.noOne }, <div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
].map(({ title, key, value }) => ( {[
<div key={key} className="flex flex-col items-start gap-1 self-stretch"> { title: t(`${i18nPrefix}.whoCanInstall`, { ns: 'plugin' }), key: 'install_permission', value: tempPrivilege?.install_permission || PermissionType.noOne },
<Label label={title} /> { title: t(`${i18nPrefix}.whoCanDebug`, { ns: 'plugin' }), key: 'debug_permission', value: tempPrivilege?.debug_permission || PermissionType.noOne },
<div className="flex w-full items-start justify-between gap-2"> ].map(({ title, key, value }) => (
{[PermissionType.everyone, PermissionType.admin, PermissionType.noOne].map(option => ( <div key={key} className="flex flex-col items-start gap-1 self-stretch">
<OptionCard <Label label={title} />
key={option} <div className="flex w-full items-start justify-between gap-2">
title={t(`${i18nPrefix}.${option}`, { ns: 'plugin' })} {[PermissionType.everyone, PermissionType.admin, PermissionType.noOne].map(option => (
onSelect={() => handlePrivilegeChange(key)(option)} <OptionCard
selected={value === option} key={option}
className="flex-1" title={t(`${i18nPrefix}.${option}`, { ns: 'plugin' })}
/> onSelect={() => handlePrivilegeChange(key)(option)}
))} selected={value === option}
className="flex-1"
/>
))}
</div>
</div> </div>
</div> ))}
))} </div>
{
enable_marketplace && (
<AutoUpdateSetting payload={tempAutoUpdateConfig} onChange={setTempAutoUpdateConfig} />
)
}
<div className="flex h-[76px] items-center justify-end gap-2 self-stretch p-6 pt-5">
<Button
className="min-w-[72px]"
onClick={onHide}
>
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button
className="min-w-[72px]"
variant="primary"
onClick={handleSave}
>
{t('operation.save', { ns: 'common' })}
</Button>
</div>
</div> </div>
{ </DialogContent>
enable_marketplace && ( </Dialog>
<AutoUpdateSetting payload={tempAutoUpdateConfig} onChange={setTempAutoUpdateConfig} />
)
}
<div className="flex h-[76px] items-center justify-end gap-2 self-stretch p-6 pt-5">
<Button
className="min-w-[72px]"
onClick={onHide}
>
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button
className="min-w-[72px]"
variant="primary"
onClick={handleSave}
>
{t('operation.save', { ns: 'common' })}
</Button>
</div>
</div>
</Modal>
) )
} }

View File

@ -17,9 +17,12 @@ vi.mock('@/app/components/workflow/store', () => ({
}), }),
})) }))
vi.mock('@/app/components/base/modal', () => ({ vi.mock('@langgenius/dify-ui/dialog', () => ({
default: ({ children, isShow }: { children: React.ReactNode, isShow: boolean }) => Dialog: ({ children, open }: { children: React.ReactNode, open?: boolean }) =>
isShow ? <div data-testid="modal">{children}</div> : null, open === false ? null : <>{children}</>,
DialogContent: ({ children, className }: { children: React.ReactNode, className?: string }) => (
<div data-testid="modal" className={className}>{children}</div>
),
})) }))
vi.mock('@langgenius/dify-ui/button', () => ({ vi.mock('@langgenius/dify-ui/button', () => ({

View File

@ -121,18 +121,14 @@ vi.mock('@langgenius/dify-ui/button', () => ({
), ),
})) }))
vi.mock('@/app/components/base/modal', () => ({ vi.mock('@langgenius/dify-ui/dialog', () => ({
default: ({ children, isShow, _onClose, className }: PropsWithChildren<{ Dialog: ({ children, open }: PropsWithChildren<{ open?: boolean }>) =>
isShow: boolean open === false ? null : <>{children}</>,
_onClose: () => void DialogContent: ({ children, className }: PropsWithChildren<{ className?: string }>) => (
className?: string <div data-testid="modal" className={className}>
}>) => isShow {children}
? ( </div>
<div data-testid="modal" className={className}> ),
{children}
</div>
)
: null,
})) }))
vi.mock('@/app/components/workflow/constants', () => ({ vi.mock('@/app/components/workflow/constants', () => ({

View File

@ -31,13 +31,13 @@ describe('VersionMismatchModal', () => {
it('should render dialog when isShow is true', () => { it('should render dialog when isShow is true', () => {
render(<VersionMismatchModal {...defaultProps} />) render(<VersionMismatchModal {...defaultProps} />)
expect(screen.getByRole('dialog')).toBeInTheDocument() expect(screen.getByRole('alertdialog')).toBeInTheDocument()
}) })
it('should not render dialog when isShow is false', () => { it('should not render dialog when isShow is false', () => {
render(<VersionMismatchModal {...defaultProps} isShow={false} />) render(<VersionMismatchModal {...defaultProps} isShow={false} />)
expect(screen.queryByRole('dialog')).not.toBeInTheDocument() expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
}) })
it('should render error title', () => { it('should render error title', () => {

View File

@ -2,14 +2,13 @@
import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import type { IconInfo } from '@/models/datasets' import type { IconInfo } from '@/models/datasets'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import AppIconPicker from '@/app/components/base/app-icon-picker' import AppIconPicker from '@/app/components/base/app-icon-picker'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import Textarea from '@/app/components/base/textarea' import Textarea from '@/app/components/base/textarea'
import { useWorkflowStore } from '@/app/components/workflow/store' import { useWorkflowStore } from '@/app/components/workflow/store'
@ -77,71 +76,70 @@ const PublishAsKnowledgePipelineModal = ({
return ( return (
<> <>
<Modal <Dialog open>
isShow <DialogContent className="relative w-full max-w-[480px]! overflow-hidden! border-none p-0! text-left align-middle">
onClose={noop}
className="relative w-[520px]! p-0!" <div className="relative flex items-center p-6 pr-14 pb-3 title-2xl-semi-bold text-text-primary">
> {t('common.publishAs', { ns: 'pipeline' })}
<div className="relative flex items-center p-6 pr-14 pb-3 title-2xl-semi-bold text-text-primary"> <div
{t('common.publishAs', { ns: 'pipeline' })} data-testid="publish-modal-close-btn"
<div className="absolute top-5 right-5 flex h-8 w-8 cursor-pointer items-center justify-center"
data-testid="publish-modal-close-btn" onClick={onCancel}
className="absolute top-5 right-5 flex h-8 w-8 cursor-pointer items-center justify-center" >
onClick={onCancel} <RiCloseLine className="h-4 w-4 text-text-tertiary" />
> </div>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div> </div>
</div> <div className="px-6 py-3">
<div className="px-6 py-3"> <div className="mb-5 flex">
<div className="mb-5 flex"> <div className="mr-3 grow">
<div className="mr-3 grow"> <div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary"> {t('common.publishAsPipeline.name', { ns: 'pipeline' })}
{t('common.publishAsPipeline.name', { ns: 'pipeline' })} </div>
<Input
value={pipelineName}
onChange={e => setPipelineName(e.target.value)}
placeholder={t('common.publishAsPipeline.namePlaceholder', { ns: 'pipeline' }) || ''}
/>
</div> </div>
<Input <AppIcon
value={pipelineName} size="xxl"
onChange={e => setPipelineName(e.target.value)} onClick={() => { setShowAppIconPicker(true) }}
placeholder={t('common.publishAsPipeline.namePlaceholder', { ns: 'pipeline' }) || ''} className="mt-2 shrink-0 cursor-pointer"
iconType={pipelineIcon?.icon_type}
icon={pipelineIcon?.icon}
background={pipelineIcon?.icon_background}
imageUrl={pipelineIcon?.icon_url}
/> />
</div> </div>
<AppIcon <div>
size="xxl" <div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">
onClick={() => { setShowAppIconPicker(true) }} {t('common.publishAsPipeline.description', { ns: 'pipeline' })}
className="mt-2 shrink-0 cursor-pointer" </div>
iconType={pipelineIcon?.icon_type} <Textarea
icon={pipelineIcon?.icon} className="resize-none"
background={pipelineIcon?.icon_background} placeholder={t('common.publishAsPipeline.descriptionPlaceholder', { ns: 'pipeline' }) || ''}
imageUrl={pipelineIcon?.icon_url} value={description}
/> onChange={e => setDescription(e.target.value)}
</div> />
<div>
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">
{t('common.publishAsPipeline.description', { ns: 'pipeline' })}
</div> </div>
<Textarea
className="resize-none"
placeholder={t('common.publishAsPipeline.descriptionPlaceholder', { ns: 'pipeline' }) || ''}
value={description}
onChange={e => setDescription(e.target.value)}
/>
</div> </div>
</div> <div className="flex items-center justify-end px-6 py-5">
<div className="flex items-center justify-end px-6 py-5"> <Button
<Button className="mr-2"
className="mr-2" onClick={onCancel}
onClick={onCancel} >
> {t('operation.cancel', { ns: 'common' })}
{t('operation.cancel', { ns: 'common' })} </Button>
</Button> <Button
<Button disabled={!pipelineName?.trim() || confirmDisabled}
disabled={!pipelineName?.trim() || confirmDisabled} variant="primary"
variant="primary" onClick={() => handleConfirm()}
onClick={() => handleConfirm()} >
> {t('common.publish', { ns: 'workflow' })}
{t('common.publish', { ns: 'workflow' })} </Button>
</Button> </div>
</div> </DialogContent>
</Modal> </Dialog>
{showAppIconPicker && ( {showAppIconPicker && (
<AppIconPicker <AppIconPicker
onSelect={handleSelectIcon} onSelect={handleSelectIcon}

View File

@ -1,6 +1,7 @@
'use client' 'use client'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { import {
RiAlertFill, RiAlertFill,
RiCloseLine, RiCloseLine,
@ -9,7 +10,6 @@ import {
import { memo } from 'react' import { memo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Uploader from '@/app/components/app/create-from-dsl-modal/uploader' import Uploader from '@/app/components/app/create-from-dsl-modal/uploader'
import Modal from '@/app/components/base/modal'
import { useUpdateDSLModal } from '../hooks/use-update-dsl-modal' import { useUpdateDSLModal } from '../hooks/use-update-dsl-modal'
import VersionMismatchModal from './version-mismatch-modal' import VersionMismatchModal from './version-mismatch-modal'
@ -39,66 +39,71 @@ const UpdateDSLModal = ({
return ( return (
<> <>
<Modal <Dialog
className="w-[520px] rounded-2xl p-6" open={show}
isShow={show} onOpenChange={(open) => {
onClose={onCancel} if (!open)
onCancel()
}}
> >
<div className="mb-3 flex items-center justify-between"> <DialogContent className="w-full max-w-[480px]! overflow-hidden! rounded-2xl border-none p-6 text-left align-middle">
<div className="title-2xl-semi-bold text-text-primary">{t('common.importDSL', { ns: 'workflow' })}</div>
<div className="flex h-[22px] w-[22px] cursor-pointer items-center justify-center" onClick={onCancel}> <div className="mb-3 flex items-center justify-between">
<RiCloseLine className="h-[18px] w-[18px] text-text-tertiary" /> <div className="title-2xl-semi-bold text-text-primary">{t('common.importDSL', { ns: 'workflow' })}</div>
</div> <div className="flex h-[22px] w-[22px] cursor-pointer items-center justify-center" onClick={onCancel}>
</div> <RiCloseLine className="h-[18px] w-[18px] text-text-tertiary" />
<div className="relative mb-2 flex grow gap-0.5 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-xs">
<div className="absolute top-0 left-0 h-full w-full bg-toast-warning-bg opacity-40" />
<div className="flex items-start justify-center p-1">
<RiAlertFill className="h-4 w-4 shrink-0 text-text-warning-secondary" />
</div>
<div className="flex grow flex-col items-start gap-0.5 py-1">
<div className="system-xs-medium whitespace-pre-line text-text-primary">{t('common.importDSLTip', { ns: 'workflow' })}</div>
<div className="flex items-start gap-1 self-stretch pt-1 pb-0.5">
<Button
size="small"
variant="secondary"
className="z-1000"
onClick={onBackup}
>
<RiFileDownloadLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
<div className="flex items-center justify-center gap-1 px-[3px]">
{t('common.backupCurrentDraft', { ns: 'workflow' })}
</div>
</Button>
</div> </div>
</div> </div>
</div> <div className="relative mb-2 flex grow gap-0.5 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-xs">
<div> <div className="absolute top-0 left-0 h-full w-full bg-toast-warning-bg opacity-40" />
<div className="pt-2 system-md-semibold text-text-primary"> <div className="flex items-start justify-center p-1">
{t('common.chooseDSL', { ns: 'workflow' })} <RiAlertFill className="h-4 w-4 shrink-0 text-text-warning-secondary" />
</div>
<div className="flex grow flex-col items-start gap-0.5 py-1">
<div className="system-xs-medium whitespace-pre-line text-text-primary">{t('common.importDSLTip', { ns: 'workflow' })}</div>
<div className="flex items-start gap-1 self-stretch pt-1 pb-0.5">
<Button
size="small"
variant="secondary"
className="z-1000"
onClick={onBackup}
>
<RiFileDownloadLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
<div className="flex items-center justify-center gap-1 px-[3px]">
{t('common.backupCurrentDraft', { ns: 'workflow' })}
</div>
</Button>
</div>
</div>
</div> </div>
<div className="flex w-full flex-col items-start justify-center gap-4 self-stretch py-4"> <div>
<Uploader <div className="pt-2 system-md-semibold text-text-primary">
file={currentFile} {t('common.chooseDSL', { ns: 'workflow' })}
updateFile={handleFile} </div>
className="mt-0! w-full" <div className="flex w-full flex-col items-start justify-center gap-4 self-stretch py-4">
accept=".pipeline" <Uploader
displayName="PIPELINE" file={currentFile}
/> updateFile={handleFile}
className="mt-0! w-full"
accept=".pipeline"
displayName="PIPELINE"
/>
</div>
</div> </div>
</div> <div className="flex items-center justify-end gap-2 self-stretch pt-5">
<div className="flex items-center justify-end gap-2 self-stretch pt-5"> <Button onClick={onCancel}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button onClick={onCancel}>{t('newApp.Cancel', { ns: 'app' })}</Button> <Button
<Button disabled={!currentFile || loading}
disabled={!currentFile || loading} variant="primary"
variant="primary" tone="destructive"
tone="destructive" onClick={handleImport}
onClick={handleImport} loading={loading}
loading={loading} >
> {t('common.overwriteAndImport', { ns: 'workflow' })}
{t('common.overwriteAndImport', { ns: 'workflow' })} </Button>
</Button> </div>
</div> </DialogContent>
</Modal> </Dialog>
<VersionMismatchModal <VersionMismatchModal
isShow={showErrorModal} isShow={showErrorModal}
versions={versions} versions={versions}

View File

@ -1,7 +1,14 @@
import type { MouseEventHandler } from 'react' import type { MouseEventHandler } from 'react'
import { Button } from '@langgenius/dify-ui/button' import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
type VersionMismatchModalProps = { type VersionMismatchModalProps = {
isShow: boolean isShow: boolean
@ -22,32 +29,36 @@ const VersionMismatchModal = ({
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Modal <AlertDialog
isShow={isShow} open={isShow}
onClose={onClose} onOpenChange={(open) => {
className="w-[480px]" if (!open)
onClose()
}}
> >
<div className="flex flex-col items-start gap-2 self-stretch pb-4"> <AlertDialogContent className="w-[480px] overflow-hidden! border-none text-left align-middle shadow-xl">
<div className="title-2xl-semi-bold text-text-primary">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</div> <div className="flex flex-col items-start gap-2 self-stretch p-6 pb-4">
<div className="flex grow flex-col system-md-regular text-text-secondary"> <AlertDialogTitle className="title-2xl-semi-bold text-text-primary">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</AlertDialogTitle>
<div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div> <AlertDialogDescription render={<div />} className="flex grow flex-col system-md-regular text-text-secondary">
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div> <div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
<br /> <div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
<div> <br />
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })} <div>
<span className="system-md-medium">{versions?.importedVersion}</span> {t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
</div> <span className="system-md-medium">{versions?.importedVersion}</span>
<div> </div>
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })} <div>
<span className="system-md-medium">{versions?.systemVersion}</span> {t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
</div> <span className="system-md-medium">{versions?.systemVersion}</span>
</div>
</AlertDialogDescription>
</div> </div>
</div> <AlertDialogActions>
<div className="flex items-start justify-end gap-2 self-stretch pt-6"> <AlertDialogCancelButton variant="secondary">{t('newApp.Cancel', { ns: 'app' })}</AlertDialogCancelButton>
<Button variant="secondary" onClick={onClose}>{t('newApp.Cancel', { ns: 'app' })}</Button> <AlertDialogConfirmButton onClick={onConfirm}>{t('newApp.Confirm', { ns: 'app' })}</AlertDialogConfirmButton>
<Button variant="primary" tone="destructive" onClick={onConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button> </AlertDialogActions>
</div> </AlertDialogContent>
</Modal> </AlertDialog>
) )
} }

View File

@ -1,8 +1,8 @@
import type { SiteInfo } from '@/models/share' import type { SiteInfo } from '@/models/share'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import * as React from 'react' import * as React from 'react'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import Modal from '@/app/components/base/modal'
import { appDefaultIconBackground } from '@/config' import { appDefaultIconBackground } from '@/config'
type Props = { type Props = {
@ -17,37 +17,42 @@ const InfoModal = ({
data, data,
}: Props) => { }: Props) => {
return ( return (
<Modal <Dialog
isShow={isShow} open={isShow}
onClose={onClose} onOpenChange={(open) => {
className="max-w-[400px] min-w-[400px] p-0!" if (!open)
closable onClose()
}}
> >
<div className={cn('flex flex-col items-center gap-4 px-4 pt-10 pb-8')}> <DialogContent className="w-full max-w-[400px] min-w-[400px] overflow-hidden! border-none p-0! text-left align-middle">
<AppIcon <DialogCloseButton data-testid="modal-close-button" />
size="xxl"
iconType={data?.icon_type} <div className={cn('flex flex-col items-center gap-4 px-4 pt-10 pb-8')}>
icon={data?.icon} <AppIcon
background={data?.icon_background || appDefaultIconBackground} size="xxl"
imageUrl={data?.icon_url} iconType={data?.icon_type}
/> icon={data?.icon}
<div className="system-xl-semibold text-text-secondary">{data?.title}</div> background={data?.icon_background || appDefaultIconBackground}
<div className="system-xs-regular text-text-tertiary"> imageUrl={data?.icon_url}
{/* copyright */} />
{data?.copyright && ( <div className="system-xl-semibold text-text-secondary">{data?.title}</div>
<div> <div className="system-xs-regular text-text-tertiary">
© {/* copyright */}
{(new Date()).getFullYear()} {data?.copyright && (
{' '} <div>
{data?.copyright} ©
</div> {(new Date()).getFullYear()}
)} {' '}
{data?.custom_disclaimer && ( {data?.copyright}
<div className="mt-2">{data.custom_disclaimer}</div> </div>
)} )}
{data?.custom_disclaimer && (
<div className="mt-2">{data.custom_disclaimer}</div>
)}
</div>
</div> </div>
</div> </DialogContent>
</Modal> </Dialog>
) )
} }

View File

@ -3,12 +3,11 @@ import type {
MCPServerDetail, MCPServerDetail,
} from '@/app/components/tools/types' } from '@/app/components/tools/types'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import * as React from 'react' import * as React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider' import Divider from '@/app/components/base/divider'
import Modal from '@/app/components/base/modal'
import Textarea from '@/app/components/base/textarea' import Textarea from '@/app/components/base/textarea'
import MCPServerParamItem from '@/app/components/tools/mcp/mcp-server-param-item' import MCPServerParamItem from '@/app/components/tools/mcp/mcp-server-param-item'
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager' import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
@ -20,11 +19,19 @@ import {
type ModalProps = { type ModalProps = {
appID: string appID: string
latestParams?: any[] latestParams?: MCPServerParam[]
data?: MCPServerDetail data?: MCPServerDetail
show: boolean show: boolean
onHide: () => void onHide: () => void
appInfo?: any appInfo?: {
description?: string
}
}
type MCPServerParam = {
variable?: string
label?: string
type?: string
} }
const MCPServerModal = ({ const MCPServerModal = ({
@ -42,7 +49,7 @@ const MCPServerModal = ({
const defaultDescription = data?.description || appInfo?.description || '' const defaultDescription = data?.description || appInfo?.description || ''
const [description, setDescription] = React.useState(defaultDescription) const [description, setDescription] = React.useState(defaultDescription)
const [params, setParams] = React.useState(data?.parameters || {}) const [params, setParams] = React.useState<Record<string, string>>(data?.parameters || {})
const handleParamChange = (variable: string, value: string) => { const handleParamChange = (variable: string, value: string) => {
setParams(prev => ({ setParams(prev => ({
@ -52,10 +59,14 @@ const MCPServerModal = ({
} }
const getParamValue = () => { const getParamValue = () => {
const res = {} as any const res: Record<string, string> = {}
latestParams.map((param) => { latestParams.forEach((param) => {
res[param.variable] = params[param.variable] if (!param.variable)
return param return
const value = params[param.variable]
if (value !== undefined)
res[param.variable] = value
}) })
return res return res
} }
@ -78,7 +89,11 @@ const MCPServerModal = ({
const submit = async () => { const submit = async () => {
if (!data) { if (!data) {
const payload: any = { const payload: {
appID: string
description?: string
parameters: Record<string, string>
} = {
appID, appID,
parameters: getParamValue(), parameters: getParamValue(),
} }
@ -92,13 +107,18 @@ const MCPServerModal = ({
onHide() onHide()
} }
else { else {
const payload: any = { const payload: {
appID: string
id: string
description: string
parameters: Record<string, string>
} = {
appID, appID,
id: data.id, id: data.id,
parameters: getParamValue(), parameters: getParamValue(),
description,
} }
payload.description = description
await updateMCPServer(payload) await updateMCPServer(payload)
invalidateMCPServerDetail(appID) invalidateMCPServerDetail(appID)
emitMcpServerUpdate('updated') emitMcpServerUpdate('updated')
@ -107,56 +127,67 @@ const MCPServerModal = ({
} }
return ( return (
<Modal <Dialog
isShow={show} open={show}
onClose={onHide} onOpenChange={(open) => {
className={cn('relative max-w-[520px]! p-0!')} if (!open)
onHide()
}}
> >
<div className="absolute top-5 right-5 z-10 cursor-pointer p-1.5" onClick={onHide}> <DialogContent className="w-[calc(100vw-2rem)] max-w-[520px]! overflow-hidden! border-none p-0! text-left align-middle transition-all duration-100 ease-in">
<RiCloseLine className="h-5 w-5 text-text-tertiary" /> <div className="absolute top-5 right-5 z-10 cursor-pointer p-1.5" onClick={onHide}>
</div> <RiCloseLine className="h-5 w-5 text-text-tertiary" />
<div className="relative p-6 pb-3 title-2xl-semi-bold text-xl text-text-primary">
{!data ? t('mcp.server.modal.addTitle', { ns: 'tools' }) : t('mcp.server.modal.editTitle', { ns: 'tools' })}
</div>
<div className="space-y-5 px-6 py-3">
<div className="space-y-0.5">
<div className="flex h-6 items-center gap-1">
<div className="system-sm-medium text-text-secondary">{t('mcp.server.modal.description', { ns: 'tools' })}</div>
<div className="system-xs-regular text-text-destructive-secondary">*</div>
</div>
<Textarea
className="h-[96px] resize-none"
value={description}
placeholder={t('mcp.server.modal.descriptionPlaceholder', { ns: 'tools' })}
onChange={e => setDescription(e.target.value)}
>
</Textarea>
</div> </div>
{latestParams.length > 0 && ( <div className="relative p-6 pb-3 title-2xl-semi-bold text-xl text-text-primary">
<div> {!data ? t('mcp.server.modal.addTitle', { ns: 'tools' }) : t('mcp.server.modal.editTitle', { ns: 'tools' })}
<div className="mb-1 flex items-center gap-2"> </div>
<div className="shrink-0 system-xs-medium-uppercase text-text-primary">{t('mcp.server.modal.parameters', { ns: 'tools' })}</div> <div className="space-y-5 px-6 py-3">
<Divider type="horizontal" className="m-0! h-px! grow bg-divider-subtle" /> <div className="space-y-0.5">
</div> <div className="flex h-6 items-center gap-1">
<div className="mb-2 body-xs-regular text-text-tertiary">{t('mcp.server.modal.parametersTip', { ns: 'tools' })}</div> <div className="system-sm-medium text-text-secondary">{t('mcp.server.modal.description', { ns: 'tools' })}</div>
<div className="space-y-3"> <div className="system-xs-regular text-text-destructive-secondary">*</div>
{latestParams.map(paramItem => (
<MCPServerParamItem
key={paramItem.variable}
data={paramItem}
value={params[paramItem.variable] || ''}
onChange={value => handleParamChange(paramItem.variable, value)}
/>
))}
</div> </div>
<Textarea
className="h-[96px] resize-none"
value={description}
placeholder={t('mcp.server.modal.descriptionPlaceholder', { ns: 'tools' })}
onChange={e => setDescription(e.target.value)}
>
</Textarea>
</div> </div>
)} {latestParams.length > 0 && (
</div> <div>
<div className="flex flex-row-reverse p-6 pt-5"> <div className="mb-1 flex items-center gap-2">
<Button disabled={!description || creating || updating} className="ml-2" variant="primary" onClick={submit}>{data ? t('mcp.modal.save', { ns: 'tools' }) : t('mcp.server.modal.confirm', { ns: 'tools' })}</Button> <div className="shrink-0 system-xs-medium-uppercase text-text-primary">{t('mcp.server.modal.parameters', { ns: 'tools' })}</div>
<Button onClick={onHide}>{t('mcp.modal.cancel', { ns: 'tools' })}</Button> <Divider type="horizontal" className="m-0! h-px! grow bg-divider-subtle" />
</div> </div>
</Modal> <div className="mb-2 body-xs-regular text-text-tertiary">{t('mcp.server.modal.parametersTip', { ns: 'tools' })}</div>
<div className="space-y-3">
{latestParams.map((paramItem) => {
if (!paramItem.variable)
return null
const { variable } = paramItem
return (
<MCPServerParamItem
key={variable}
data={paramItem}
value={params[variable] || ''}
onChange={value => handleParamChange(variable, value)}
/>
)
})}
</div>
</div>
)}
</div>
<div className="flex flex-row-reverse p-6 pt-5">
<Button disabled={!description || creating || updating} className="ml-2" variant="primary" onClick={submit}>{data ? t('mcp.modal.save', { ns: 'tools' }) : t('mcp.server.modal.confirm', { ns: 'tools' })}</Button>
<Button onClick={onHide}>{t('mcp.modal.cancel', { ns: 'tools' })}</Button>
</div>
</DialogContent>
</Dialog>
) )
} }

View File

@ -4,17 +4,15 @@ import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import type { ToolWithProvider } from '@/app/components/workflow/types' import type { ToolWithProvider } from '@/app/components/workflow/types'
import type { AppIconType } from '@/types/app' import type { AppIconType } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn' import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine, RiEditLine } from '@remixicon/react' import { RiCloseLine, RiEditLine } from '@remixicon/react'
import { useHover } from 'ahooks' import { useHover } from 'ahooks'
import { noop } from 'es-toolkit/function'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import AppIconPicker from '@/app/components/base/app-icon-picker' import AppIconPicker from '@/app/components/base/app-icon-picker'
import { Mcp } from '@/app/components/base/icons/src/vender/other' import { Mcp } from '@/app/components/base/icons/src/vender/other'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import TabSlider from '@/app/components/base/tab-slider' import TabSlider from '@/app/components/base/tab-slider'
import { MCPAuthMethod } from '@/app/components/tools/types' import { MCPAuthMethod } from '@/app/components/tools/types'
import { shouldUseMcpIconForAppIcon } from '@/utils/mcp' import { shouldUseMcpIconForAppIcon } from '@/utils/mcp'
@ -281,18 +279,16 @@ const MCPModal: FC<DuplicateAppModalProps> = ({
const formKey = data?.id ?? 'create' const formKey = data?.id ?? 'create'
return ( return (
<Modal <Dialog open={show}>
isShow={show} <DialogContent className="w-full max-w-[520px]! overflow-hidden! border-none p-6 text-left align-middle">
onClose={noop} <MCPModalContent
className={cn('relative max-w-[520px]!', 'p-6')} key={formKey}
> data={data}
<MCPModalContent onConfirm={onConfirm}
key={formKey} onHide={onHide}
data={data} />
onConfirm={onConfirm} </DialogContent>
onHide={onHide} </Dialog>
/>
</Modal>
) )
} }

View File

@ -4,12 +4,12 @@ import type { Authorization as AuthorizationPayloadType } from '../../types'
import type { Var } from '@/app/components/workflow/types' import type { Var } from '@/app/components/workflow/types'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { produce } from 'immer' import { produce } from 'immer'
import * as React from 'react' import * as React from 'react'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import BaseInput from '@/app/components/base/input' import BaseInput from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var' import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import { VarType } from '@/app/components/workflow/types' import { VarType } from '@/app/components/workflow/types'
@ -115,70 +115,78 @@ const Authorization: FC<Props> = ({
onHide() onHide()
}, [tempPayload, onChange, onHide]) }, [tempPayload, onChange, onHide])
return ( return (
<Modal <Dialog
title={t(`${i18nPrefix}.authorization`, { ns: 'workflow' })} open={isShow}
isShow={isShow} onOpenChange={(open) => {
onClose={onHide} if (!open)
onHide()
}}
> >
<div> <DialogContent className="overflow-hidden! border-none text-left align-middle">
<div className="space-y-2"> <DialogTitle className="title-2xl-semi-bold text-text-primary">
<Field title={t(`${i18nPrefix}.authorizationType`, { ns: 'workflow' })}> {t(`${i18nPrefix}.authorization`, { ns: 'workflow' })}
<RadioGroup </DialogTitle>
options={[
{ value: AuthorizationType.none, label: t(`${i18nPrefix}.no-auth`, { ns: 'workflow' }) },
{ value: AuthorizationType.apiKey, label: t(`${i18nPrefix}.api-key`, { ns: 'workflow' }) },
]}
value={tempPayload.type}
onChange={handleAuthTypeChange}
/>
</Field>
{tempPayload.type === AuthorizationType.apiKey && ( <div>
<> <div className="space-y-2">
<Field title={t(`${i18nPrefix}.auth-type`, { ns: 'workflow' })}> <Field title={t(`${i18nPrefix}.authorizationType`, { ns: 'workflow' })}>
<RadioGroup <RadioGroup
options={[ options={[
{ value: APIType.basic, label: t(`${i18nPrefix}.basic`, { ns: 'workflow' }) }, { value: AuthorizationType.none, label: t(`${i18nPrefix}.no-auth`, { ns: 'workflow' }) },
{ value: APIType.bearer, label: t(`${i18nPrefix}.bearer`, { ns: 'workflow' }) }, { value: AuthorizationType.apiKey, label: t(`${i18nPrefix}.api-key`, { ns: 'workflow' }) },
{ value: APIType.custom, label: t(`${i18nPrefix}.custom`, { ns: 'workflow' }) }, ]}
]} value={tempPayload.type}
value={tempPayload.config?.type || APIType.basic} onChange={handleAuthTypeChange}
onChange={handleAuthAPITypeChange} />
/> </Field>
</Field>
{tempPayload.config?.type === APIType.custom && ( {tempPayload.type === AuthorizationType.apiKey && (
<Field title={t(`${i18nPrefix}.header`, { ns: 'workflow' })} isRequired> <>
<BaseInput <Field title={t(`${i18nPrefix}.auth-type`, { ns: 'workflow' })}>
value={tempPayload.config?.header || ''} <RadioGroup
onChange={handleAPIKeyOrHeaderChange('header')} options={[
{ value: APIType.basic, label: t(`${i18nPrefix}.basic`, { ns: 'workflow' }) },
{ value: APIType.bearer, label: t(`${i18nPrefix}.bearer`, { ns: 'workflow' }) },
{ value: APIType.custom, label: t(`${i18nPrefix}.custom`, { ns: 'workflow' }) },
]}
value={tempPayload.config?.type || APIType.basic}
onChange={handleAuthAPITypeChange}
/> />
</Field> </Field>
)} {tempPayload.config?.type === APIType.custom && (
<Field title={t(`${i18nPrefix}.header`, { ns: 'workflow' })} isRequired>
<BaseInput
value={tempPayload.config?.header || ''}
onChange={handleAPIKeyOrHeaderChange('header')}
/>
</Field>
)}
<Field title={t(`${i18nPrefix}.api-key-title`, { ns: 'workflow' })} isRequired> <Field title={t(`${i18nPrefix}.api-key-title`, { ns: 'workflow' })} isRequired>
<div className="flex"> <div className="flex">
<Input <Input
instanceId="http-api-key" instanceId="http-api-key"
className={cn(isFocus ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'w-0 grow rounded-lg border px-3 py-[6px]')} className={cn(isFocus ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'w-0 grow rounded-lg border px-3 py-[6px]')}
value={tempPayload.config?.api_key || ''} value={tempPayload.config?.api_key || ''}
onChange={handleAPIKeyChange} onChange={handleAPIKeyChange}
nodesOutputVars={availableVars} nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent} availableNodes={availableNodesWithParent}
onFocusChange={setIsFocus} onFocusChange={setIsFocus}
placeholder={' '} placeholder={' '}
placeholderClassName="leading-[21px]!" placeholderClassName="leading-[21px]!"
/> />
</div> </div>
</Field> </Field>
</> </>
)} )}
</div>
<div className="mt-6 flex justify-end space-x-2">
<Button onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button variant="primary" onClick={handleConfirm}>{t('operation.save', { ns: 'common' })}</Button>
</div>
</div> </div>
<div className="mt-6 flex justify-end space-x-2"> </DialogContent>
<Button onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button> </Dialog>
<Button variant="primary" onClick={handleConfirm}>{t('operation.save', { ns: 'common' })}</Button>
</div>
</div>
</Modal>
) )
} }
export default React.memo(Authorization) export default React.memo(Authorization)

View File

@ -2,11 +2,11 @@
import type { FC } from 'react' import type { FC } from 'react'
import type { HttpNodeType } from '../types' import type { HttpNodeType } from '../types'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react' import * as React from 'react'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import Textarea from '@/app/components/base/textarea' import Textarea from '@/app/components/base/textarea'
import { useNodesInteractions } from '@/app/components/workflow/hooks' import { useNodesInteractions } from '@/app/components/workflow/hooks'
import { parseCurl } from './curl-parser' import { parseCurl } from './curl-parser'
@ -42,28 +42,35 @@ const CurlPanel: FC<Props> = ({ nodeId, isShow, onHide, handleCurlImport }) => {
}, [onHide, nodeId, inputString, handleNodeSelect, handleCurlImport]) }, [onHide, nodeId, inputString, handleNodeSelect, handleCurlImport])
return ( return (
<Modal <Dialog
title={t('nodes.http.curl.title', { ns: 'workflow' })} open={isShow}
isShow={isShow} onOpenChange={(open) => {
onClose={onHide} if (!open)
className="w-[400px]! max-w-[400px]! p-4!" onHide()
}}
> >
<div> <DialogContent className="w-[400px]! max-w-[400px]! overflow-hidden! border-none p-4! text-left align-middle">
<Textarea <DialogTitle className="title-2xl-semi-bold text-text-primary">
value={inputString} {t('nodes.http.curl.title', { ns: 'workflow' })}
className="my-3 h-40 w-full grow" </DialogTitle>
onChange={e => setInputString(e.target.value)}
placeholder={t('nodes.http.curl.placeholder', { ns: 'workflow' })!} <div>
/> <Textarea
</div> value={inputString}
<div className="mt-4 flex justify-end space-x-2"> className="my-3 h-40 w-full grow"
<Button className="w-[95px]!" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button> onChange={e => setInputString(e.target.value)}
<Button className="w-[95px]!" variant="primary" onClick={handleSave}> placeholder={t('nodes.http.curl.placeholder', { ns: 'workflow' })!}
{' '} />
{t('operation.save', { ns: 'common' })} </div>
</Button> <div className="mt-4 flex justify-end space-x-2">
</div> <Button className="w-[95px]!" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
</Modal> <Button className="w-[95px]!" variant="primary" onClick={handleSave}>
{' '}
{t('operation.save', { ns: 'common' })}
</Button>
</div>
</DialogContent>
</Dialog>
) )
} }

View File

@ -1,7 +1,7 @@
import type { FC } from 'react' import type { FC } from 'react'
import type { SchemaRoot } from '../../types' import type { SchemaRoot } from '../../types'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import * as React from 'react' import * as React from 'react'
import Modal from '../../../../../base/modal'
import JsonSchemaConfig from './json-schema-config' import JsonSchemaConfig from './json-schema-config'
type JsonSchemaConfigModalProps = { type JsonSchemaConfigModalProps = {
@ -18,17 +18,22 @@ const JsonSchemaConfigModal: FC<JsonSchemaConfigModalProps> = ({
onClose, onClose,
}) => { }) => {
return ( return (
<Modal <Dialog
isShow={isShow} open={isShow}
onClose={onClose} onOpenChange={(open) => {
className="h-[800px] max-w-[960px] p-0" if (!open)
onClose()
}}
> >
<JsonSchemaConfig <DialogContent className="h-[800px] max-h-none w-full max-w-[960px] overflow-hidden! border-none p-0 text-left align-middle">
defaultSchema={defaultSchema}
onSave={onSave} <JsonSchemaConfig
onClose={onClose} defaultSchema={defaultSchema}
/> onSave={onSave}
</Modal> onClose={onClose}
/>
</DialogContent>
</Dialog>
) )
} }

View File

@ -110,24 +110,23 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-param
), ),
})) }))
vi.mock('@/app/components/base/modal', () => ({ vi.mock('@langgenius/dify-ui/dialog', () => ({
__esModule: true, __esModule: true,
default: ({ Dialog: ({
children, children,
isShow, open,
title,
}: { }: {
children: ReactNode children: ReactNode
isShow?: boolean open?: boolean
title?: ReactNode }) => open !== false
}) => isShow
? ( ? (
<div data-testid="base-modal"> <div data-testid="base-modal">
<div>{title}</div>
{children} {children}
</div> </div>
) )
: null, : null,
DialogContent: ({ children }: { children: ReactNode }) => <>{children}</>,
DialogTitle: ({ children }: { children: ReactNode }) => <div>{children}</div>,
})) }))
vi.mock('@/app/components/workflow/nodes/_base/components/collapse', () => ({ vi.mock('@/app/components/workflow/nodes/_base/components/collapse', () => ({

View File

@ -3,6 +3,7 @@ import type { FC } from 'react'
import type { Param } from '../../types' import type { Param } from '../../types'
import type { MoreInfo } from '@/app/components/workflow/types' import type { MoreInfo } from '@/app/components/workflow/types'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select' import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { Switch } from '@langgenius/dify-ui/switch' import { Switch } from '@langgenius/dify-ui/switch'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
@ -13,7 +14,6 @@ import { useTranslation } from 'react-i18next'
import Field from '@/app/components/app/configuration/config-var/config-modal/field' import Field from '@/app/components/app/configuration/config-var/config-modal/field'
import ConfigSelect from '@/app/components/app/configuration/config-var/config-select' import ConfigSelect from '@/app/components/app/configuration/config-var/config-select'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import Textarea from '@/app/components/base/textarea' import Textarea from '@/app/components/base/textarea'
import { ChangeType } from '@/app/components/workflow/types' import { ChangeType } from '@/app/components/workflow/types'
import { checkKeys } from '@/utils/var' import { checkKeys } from '@/utils/var'
@ -124,64 +124,71 @@ const AddExtractParameter: FC<Props> = ({
</div> </div>
)} )}
{isShowModal && ( {isShowModal && (
<Modal <Dialog
title={t(`${i18nPrefix}.addExtractParameter`, { ns: 'workflow' })} open
isShow onOpenChange={(open) => {
onClose={hideModal} if (!open)
className="w-[400px]! max-w-[400px]! p-4!" hideModal()
}}
> >
<div> <DialogContent className="w-[400px]! max-w-[400px]! overflow-hidden! border-none p-4! text-left align-middle">
<div className="space-y-2"> <DialogTitle className="title-2xl-semi-bold text-text-primary">
<Field title={t(`${i18nPrefix}.addExtractParameterContent.name`, { ns: 'workflow' })}> {t(`${i18nPrefix}.addExtractParameter`, { ns: 'workflow' })}
<Input </DialogTitle>
value={param.name}
onChange={e => handleParamChange('name')(e.target.value)} <div>
placeholder={t(`${i18nPrefix}.addExtractParameterContent.namePlaceholder`, { ns: 'workflow' })!} <div className="space-y-2">
/> <Field title={t(`${i18nPrefix}.addExtractParameterContent.name`, { ns: 'workflow' })}>
</Field> <Input
<Field title={t(`${i18nPrefix}.addExtractParameterContent.type`, { ns: 'workflow' })}> value={param.name}
<Select onChange={e => handleParamChange('name')(e.target.value)}
value={param.type} placeholder={t(`${i18nPrefix}.addExtractParameterContent.namePlaceholder`, { ns: 'workflow' })!}
onValueChange={value => value && handleParamChange('type')(value)} />
>
<SelectTrigger className="w-full capitalize">
{param.type}
</SelectTrigger>
<SelectContent>
{TYPES.map(type => (
<SelectItem key={type} value={type} className="capitalize">
<SelectItemText className="capitalize">{type}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
{param.type === ParamType.select && (
<Field title={t('variableConfig.options', { ns: 'appDebug' })}>
<ConfigSelect options={param.options || []} onChange={handleParamChange('options')} />
</Field> </Field>
)} <Field title={t(`${i18nPrefix}.addExtractParameterContent.type`, { ns: 'workflow' })}>
<Field title={t(`${i18nPrefix}.addExtractParameterContent.description`, { ns: 'workflow' })}> <Select
<Textarea value={param.type}
value={param.description} onValueChange={value => value && handleParamChange('type')(value)}
onChange={e => handleParamChange('description')(e.target.value)} >
placeholder={t(`${i18nPrefix}.addExtractParameterContent.descriptionPlaceholder`, { ns: 'workflow' })!} <SelectTrigger className="w-full capitalize">
/> {param.type}
</Field> </SelectTrigger>
<Field title={t(`${i18nPrefix}.addExtractParameterContent.required`, { ns: 'workflow' })}> <SelectContent>
<> {TYPES.map(type => (
<div className="mb-1.5 text-xs leading-[18px] font-normal text-text-tertiary">{t(`${i18nPrefix}.addExtractParameterContent.requiredContent`, { ns: 'workflow' })}</div> <SelectItem key={type} value={type} className="capitalize">
<Switch size="lg" checked={param.required ?? false} onCheckedChange={handleParamChange('required')} /> <SelectItemText className="capitalize">{type}</SelectItemText>
</> <SelectItemIndicator />
</Field> </SelectItem>
))}
</SelectContent>
</Select>
</Field>
{param.type === ParamType.select && (
<Field title={t('variableConfig.options', { ns: 'appDebug' })}>
<ConfigSelect options={param.options || []} onChange={handleParamChange('options')} />
</Field>
)}
<Field title={t(`${i18nPrefix}.addExtractParameterContent.description`, { ns: 'workflow' })}>
<Textarea
value={param.description}
onChange={e => handleParamChange('description')(e.target.value)}
placeholder={t(`${i18nPrefix}.addExtractParameterContent.descriptionPlaceholder`, { ns: 'workflow' })!}
/>
</Field>
<Field title={t(`${i18nPrefix}.addExtractParameterContent.required`, { ns: 'workflow' })}>
<>
<div className="mb-1.5 text-xs leading-[18px] font-normal text-text-tertiary">{t(`${i18nPrefix}.addExtractParameterContent.requiredContent`, { ns: 'workflow' })}</div>
<Switch size="lg" checked={param.required ?? false} onCheckedChange={handleParamChange('required')} />
</>
</Field>
</div>
<div className="mt-4 flex justify-end space-x-2">
<Button className="w-[95px]!" onClick={hideModal}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button className="w-[95px]!" variant="primary" onClick={handleSave}>{isAdd ? t('operation.add', { ns: 'common' }) : t('operation.save', { ns: 'common' })}</Button>
</div>
</div> </div>
<div className="mt-4 flex justify-end space-x-2"> </DialogContent>
<Button className="w-[95px]!" onClick={hideModal}>{t('operation.cancel', { ns: 'common' })}</Button> </Dialog>
<Button className="w-[95px]!" variant="primary" onClick={handleSave}>{isAdd ? t('operation.add', { ns: 'common' }) : t('operation.save', { ns: 'common' })}</Button>
</div>
</div>
</Modal>
)} )}
</div> </div>
) )

View File

@ -3,10 +3,10 @@ import type {
ConversationVariable, ConversationVariable,
} from '@/app/components/workflow/types' } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn' import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import { useMount } from 'ahooks' import { useMount } from 'ahooks'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import { noop } from 'es-toolkit/function'
import { capitalize } from 'es-toolkit/string' import { capitalize } from 'es-toolkit/string'
import * as React from 'react' import * as React from 'react'
import { useCallback } from 'react' import { useCallback } from 'react'
@ -16,7 +16,6 @@ import {
CopyCheck, CopyCheck,
} from '@/app/components/base/icons/src/vender/line/files' } from '@/app/components/base/icons/src/vender/line/files'
import { BubbleX } from '@/app/components/base/icons/src/vender/line/others' import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
import Modal from '@/app/components/base/modal'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type' import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
@ -76,87 +75,86 @@ const ConversationVariableModal = ({
}) })
return ( return (
<Modal <Dialog open>
isShow <DialogContent className={cn('max-h-none w-full overflow-hidden! border-none text-left align-middle', cn('h-[640px] w-[920px] max-w-[920px] p-0'))}>
onClose={noop}
className={cn('h-[640px] w-[920px] max-w-[920px] p-0')} <div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onHide}>
> <RiCloseLine className="h-4 w-4 text-text-tertiary" />
<div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onHide}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div>
<div className="flex h-full w-full">
{/* LEFT */}
<div className="flex h-full w-[224px] shrink-0 flex-col border-r border-divider-burn bg-background-sidenav-bg">
<div className="shrink-0 pt-5 pr-4 pb-3 pl-5 system-xl-semibold text-text-primary">{t('chatVariable.panelTitle', { ns: 'workflow' })}</div>
<div className="grow overflow-y-auto px-3 py-2">
{varList.map(chatVar => (
<div key={chatVar.id} className={cn('group mb-0.5 flex cursor-pointer items-center rounded-lg p-2 hover:bg-state-base-hover', currentVar.id === chatVar.id && 'bg-state-base-hover')} onClick={() => setCurrentVar(chatVar)}>
<BubbleX className={cn('mr-1 h-4 w-4 shrink-0 text-text-tertiary group-hover:text-util-colors-teal-teal-700', currentVar.id === chatVar.id && 'text-util-colors-teal-teal-700')} />
<div title={chatVar.name} className={cn('truncate system-sm-medium text-text-tertiary group-hover:text-util-colors-teal-teal-700', currentVar.id === chatVar.id && 'text-util-colors-teal-teal-700')}>{chatVar.name}</div>
</div>
))}
</div>
</div> </div>
{/* RIGHT */} <div className="flex h-full w-full">
<div className="flex h-full w-0 grow flex-col bg-components-panel-bg"> {/* LEFT */}
<div className="shrink-0 p-4 pb-2"> <div className="flex h-full w-[224px] shrink-0 flex-col border-r border-divider-burn bg-background-sidenav-bg">
<div className="flex items-center gap-1 py-1"> <div className="shrink-0 pt-5 pr-4 pb-3 pl-5 system-xl-semibold text-text-primary">{t('chatVariable.panelTitle', { ns: 'workflow' })}</div>
<div className="system-xl-semibold text-text-primary">{currentVar.name}</div> <div className="grow overflow-y-auto px-3 py-2">
<div className="system-xs-medium text-text-tertiary">{capitalize(currentVar.value_type)}</div> {varList.map(chatVar => (
<div key={chatVar.id} className={cn('group mb-0.5 flex cursor-pointer items-center rounded-lg p-2 hover:bg-state-base-hover', currentVar.id === chatVar.id && 'bg-state-base-hover')} onClick={() => setCurrentVar(chatVar)}>
<BubbleX className={cn('mr-1 h-4 w-4 shrink-0 text-text-tertiary group-hover:text-util-colors-teal-teal-700', currentVar.id === chatVar.id && 'text-util-colors-teal-teal-700')} />
<div title={chatVar.name} className={cn('truncate system-sm-medium text-text-tertiary group-hover:text-util-colors-teal-teal-700', currentVar.id === chatVar.id && 'text-util-colors-teal-teal-700')}>{chatVar.name}</div>
</div>
))}
</div> </div>
</div> </div>
<div className="flex h-0 grow flex-col p-4 pt-2"> {/* RIGHT */}
<div className="mb-2 flex shrink-0 items-center gap-2"> <div className="flex h-full w-0 grow flex-col bg-components-panel-bg">
<div className="shrink-0 system-xs-medium-uppercase text-text-tertiary">{t('chatVariable.storedContent', { ns: 'workflow' }).toLocaleUpperCase()}</div> <div className="shrink-0 p-4 pb-2">
<div <div className="flex items-center gap-1 py-1">
className="h-px grow" <div className="system-xl-semibold text-text-primary">{currentVar.name}</div>
style={{ <div className="system-xs-medium text-text-tertiary">{capitalize(currentVar.value_type)}</div>
background: 'linear-gradient(to right, rgba(16, 24, 40, 0.08) 0%, rgba(255, 255, 255) 100%)',
}}
>
</div> </div>
{!!latestValueTimestampMap[currentVar.id] && (
<div className="shrink-0 system-xs-regular text-text-tertiary">
{t('chatVariable.updatedAt', { ns: 'workflow' })}
{formatTime(latestValueTimestampMap[currentVar.id]!, t('dateTimeFormat', { ns: 'appLog' }) as string)}
</div>
)}
</div> </div>
<div className="grow overflow-y-auto"> <div className="flex h-0 grow flex-col p-4 pt-2">
{currentVar.value_type !== ChatVarType.Number && currentVar.value_type !== ChatVarType.String && ( <div className="mb-2 flex shrink-0 items-center gap-2">
<div className="flex h-full flex-col rounded-lg bg-components-input-bg-normal px-2 pb-2"> <div className="shrink-0 system-xs-medium-uppercase text-text-tertiary">{t('chatVariable.storedContent', { ns: 'workflow' }).toLocaleUpperCase()}</div>
<div className="flex h-7 shrink-0 items-center justify-between pt-1 pr-2 pl-3"> <div
<div className="system-xs-semibold text-text-secondary">JSON</div> className="h-px grow"
<div className="flex items-center p-1"> style={{
{!isCopied background: 'linear-gradient(to right, rgba(16, 24, 40, 0.08) 0%, rgba(255, 255, 255) 100%)',
? ( }}
<Copy className="h-4 w-4 cursor-pointer text-text-tertiary" onClick={handleCopy} /> >
) </div>
: ( {!!latestValueTimestampMap[currentVar.id] && (
<CopyCheck className="h-4 w-4 text-text-tertiary" /> <div className="shrink-0 system-xs-regular text-text-tertiary">
)} {t('chatVariable.updatedAt', { ns: 'workflow' })}
{formatTime(latestValueTimestampMap[currentVar.id]!, t('dateTimeFormat', { ns: 'appLog' }) as string)}
</div>
)}
</div>
<div className="grow overflow-y-auto">
{currentVar.value_type !== ChatVarType.Number && currentVar.value_type !== ChatVarType.String && (
<div className="flex h-full flex-col rounded-lg bg-components-input-bg-normal px-2 pb-2">
<div className="flex h-7 shrink-0 items-center justify-between pt-1 pr-2 pl-3">
<div className="system-xs-semibold text-text-secondary">JSON</div>
<div className="flex items-center p-1">
{!isCopied
? (
<Copy className="h-4 w-4 cursor-pointer text-text-tertiary" onClick={handleCopy} />
)
: (
<CopyCheck className="h-4 w-4 text-text-tertiary" />
)}
</div>
</div>
<div className="grow pl-4">
<CodeEditor
readOnly
noWrapper
isExpand
language={CodeLanguage.json}
value={latestValueMap[currentVar.id] || ''}
isJSONStringifyBeauty
/>
</div> </div>
</div> </div>
<div className="grow pl-4"> )}
<CodeEditor {(currentVar.value_type === ChatVarType.Number || currentVar.value_type === ChatVarType.String) && (
readOnly <div className="h-full overflow-x-hidden overflow-y-auto rounded-lg bg-components-input-bg-normal px-4 py-3 system-md-regular text-components-input-text-filled">{latestValueMap[currentVar.id] || ''}</div>
noWrapper )}
isExpand </div>
language={CodeLanguage.json}
value={latestValueMap[currentVar.id] || ''}
isJSONStringifyBeauty
/>
</div>
</div>
)}
{(currentVar.value_type === ChatVarType.Number || currentVar.value_type === ChatVarType.String) && (
<div className="h-full overflow-x-hidden overflow-y-auto rounded-lg bg-components-input-bg-normal px-4 py-3 system-md-regular text-components-input-text-filled">{latestValueMap[currentVar.id] || ''}</div>
)}
</div> </div>
</div> </div>
</div> </div>
</div> </DialogContent>
</Modal> </Dialog>
) )
} }

View File

@ -33,7 +33,8 @@ const ActionMenu: FC<ActionMenuProps> = (props: ActionMenuProps) => {
onOpenChange={setOpen} onOpenChange={setOpen}
> >
<DropdownMenuTrigger <DropdownMenuTrigger
render={<Button size="small" className="px-1" onClick={e => e.stopPropagation()} />} nativeButton={false}
render={<Button nativeButton={false} size="small" className="px-1" onClick={e => e.stopPropagation()} />}
> >
<RiMoreFill className="h-4 w-4" /> <RiMoreFill className="h-4 w-4" />
</DropdownMenuTrigger> </DropdownMenuTrigger>

View File

@ -1,9 +1,16 @@
import type { FC } from 'react' import type { FC } from 'react'
import type { VersionHistory } from '@/types/workflow' import type { VersionHistory } from '@/types/workflow'
import { Button } from '@langgenius/dify-ui/button' import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import * as React from 'react' import * as React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
type DeleteConfirmModalProps = { type DeleteConfirmModalProps = {
isOpen: boolean isOpen: boolean
@ -21,24 +28,36 @@ const DeleteConfirmModal: FC<DeleteConfirmModalProps> = ({
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Modal className="p-0" isShow={isOpen} onClose={onClose}> <AlertDialog
<div className="flex flex-col gap-y-2 p-6 pb-4"> open={isOpen}
<div className="title-2xl-semi-bold text-text-primary"> onOpenChange={(open) => {
{`${t('operation.delete', { ns: 'common' })} ${versionInfo.marked_name || t('versionHistory.defaultName', { ns: 'workflow' })}`} if (!open)
onClose()
}}
>
<AlertDialogContent className="overflow-hidden! border-none text-left align-middle shadow-xl">
<div className="flex flex-col gap-y-2 p-6 pb-4">
<AlertDialogTitle className="title-2xl-semi-bold text-text-primary">
{`${t('operation.delete', { ns: 'common' })} ${versionInfo.marked_name || t('versionHistory.defaultName', { ns: 'workflow' })}`}
</AlertDialogTitle>
<AlertDialogDescription className="system-md-regular text-text-secondary">
{t('versionHistory.deletionTip', { ns: 'workflow' })}
</AlertDialogDescription>
</div> </div>
<p className="system-md-regular text-text-secondary"> <AlertDialogActions>
{t('versionHistory.deletionTip', { ns: 'workflow' })} <AlertDialogCancelButton
</p> nativeButton={false}
</div> variant="secondary"
<div className="flex items-center justify-end gap-x-2 p-6"> closeProps={{ nativeButton: false }}
<Button onClick={onClose}> >
{t('operation.cancel', { ns: 'common' })} {t('operation.cancel', { ns: 'common' })}
</Button> </AlertDialogCancelButton>
<Button variant="primary" tone="destructive" onClick={onDelete.bind(null, versionInfo.id)}> <AlertDialogConfirmButton nativeButton={false} onClick={onDelete.bind(null, versionInfo.id)}>
{t('operation.delete', { ns: 'common' })} {t('operation.delete', { ns: 'common' })}
</Button> </AlertDialogConfirmButton>
</div> </AlertDialogActions>
</Modal> </AlertDialogContent>
</AlertDialog>
) )
} }

View File

@ -22,7 +22,7 @@ const Empty: FC<EmptyProps> = ({
{t('versionHistory.filter.empty', { ns: 'workflow' })} {t('versionHistory.filter.empty', { ns: 'workflow' })}
</div> </div>
<div className="flex justify-center"> <div className="flex justify-center">
<Button size="small" onClick={onResetFilter}> <Button nativeButton={false} size="small" onClick={onResetFilter}>
{t('versionHistory.filter.reset', { ns: 'workflow' })} {t('versionHistory.filter.reset', { ns: 'workflow' })}
</Button> </Button>
</div> </div>

View File

@ -42,6 +42,7 @@ const Filter: FC<FilterProps> = ({
onOpenChange={setOpen} onOpenChange={setOpen}
> >
<PopoverTrigger <PopoverTrigger
nativeButton={false}
render={( render={(
<div <div
className={cn( className={cn(

View File

@ -1,9 +1,16 @@
import type { FC } from 'react' import type { FC } from 'react'
import type { VersionHistory } from '@/types/workflow' import type { VersionHistory } from '@/types/workflow'
import { Button } from '@langgenius/dify-ui/button' import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import * as React from 'react' import * as React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
type RestoreConfirmModalProps = { type RestoreConfirmModalProps = {
isOpen: boolean isOpen: boolean
@ -21,24 +28,36 @@ const RestoreConfirmModal: FC<RestoreConfirmModalProps> = ({
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Modal className="p-0" isShow={isOpen} onClose={onClose}> <AlertDialog
<div className="flex flex-col gap-y-2 p-6 pb-4"> open={isOpen}
<div className="title-2xl-semi-bold text-text-primary"> onOpenChange={(open) => {
{`${t('common.restore', { ns: 'workflow' })} ${versionInfo.marked_name || t('versionHistory.defaultName', { ns: 'workflow' })}`} if (!open)
onClose()
}}
>
<AlertDialogContent className="overflow-hidden! border-none text-left align-middle shadow-xl">
<div className="flex flex-col gap-y-2 p-6 pb-4">
<AlertDialogTitle className="title-2xl-semi-bold text-text-primary">
{`${t('common.restore', { ns: 'workflow' })} ${versionInfo.marked_name || t('versionHistory.defaultName', { ns: 'workflow' })}`}
</AlertDialogTitle>
<AlertDialogDescription className="system-md-regular text-text-secondary">
{t('versionHistory.restorationTip', { ns: 'workflow' })}
</AlertDialogDescription>
</div> </div>
<p className="system-md-regular text-text-secondary"> <AlertDialogActions>
{t('versionHistory.restorationTip', { ns: 'workflow' })} <AlertDialogCancelButton
</p> nativeButton={false}
</div> variant="secondary"
<div className="flex items-center justify-end gap-x-2 p-6"> closeProps={{ nativeButton: false }}
<Button onClick={onClose}> >
{t('operation.cancel', { ns: 'common' })} {t('operation.cancel', { ns: 'common' })}
</Button> </AlertDialogCancelButton>
<Button variant="primary" onClick={onRestore.bind(null, versionInfo)}> <AlertDialogConfirmButton nativeButton={false} tone="default" onClick={onRestore.bind(null, versionInfo)}>
{t('common.restore', { ns: 'workflow' })} {t('common.restore', { ns: 'workflow' })}
</Button> </AlertDialogConfirmButton>
</div> </AlertDialogActions>
</Modal> </AlertDialogContent>
</AlertDialog>
) )
} }

View File

@ -2,6 +2,7 @@
import type { MouseEventHandler } from 'react' import type { MouseEventHandler } from 'react'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast' import { toast } from '@langgenius/dify-ui/toast'
import { import {
RiAlertFill, RiAlertFill,
@ -17,7 +18,6 @@ import {
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Uploader from '@/app/components/app/create-from-dsl-modal/uploader' import Uploader from '@/app/components/app/create-from-dsl-modal/uploader'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import Modal from '@/app/components/base/modal'
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { useEventEmitterContextContext } from '@/context/event-emitter' import { useEventEmitterContextContext } from '@/context/event-emitter'
import { import {
@ -199,90 +199,100 @@ const UpdateDSLModal = ({
return ( return (
<> <>
<Modal <Dialog
className="w-[520px] rounded-2xl p-6" open={show}
isShow={show} onOpenChange={(open) => {
onClose={onCancel} if (!open)
onCancel()
}}
> >
<div className="mb-3 flex items-center justify-between"> <DialogContent className="w-full max-w-[480px]! overflow-hidden! rounded-2xl border-none p-6 text-left align-middle">
<div className="title-2xl-semi-bold text-text-primary">{t('importApp', { ns: 'app' })}</div>
<div className="flex h-[22px] w-[22px] cursor-pointer items-center justify-center" onClick={onCancel}> <div className="mb-3 flex items-center justify-between">
<RiCloseLine className="h-[18px] w-[18px] text-text-tertiary" /> <div className="title-2xl-semi-bold text-text-primary">{t('importApp', { ns: 'app' })}</div>
</div> <div className="flex h-[22px] w-[22px] cursor-pointer items-center justify-center" onClick={onCancel}>
</div> <RiCloseLine className="h-[18px] w-[18px] text-text-tertiary" />
<div className="relative mb-2 flex grow gap-0.5 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-xs">
<div className="absolute top-0 left-0 h-full w-full bg-toast-warning-bg opacity-40" />
<div className="flex items-start justify-center p-1">
<RiAlertFill className="h-4 w-4 shrink-0 text-text-warning-secondary" />
</div>
<div className="flex grow flex-col items-start gap-0.5 py-1">
<div className="system-xs-medium whitespace-pre-line text-text-primary">{t('common.importDSLTip', { ns: 'workflow' })}</div>
<div className="flex items-start gap-1 self-stretch pt-1 pb-0.5">
<Button
size="small"
variant="secondary"
className="z-1000"
onClick={onBackup}
>
<RiFileDownloadLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
<div className="flex items-center justify-center gap-1 px-[3px]">
{t('common.backupCurrentDraft', { ns: 'workflow' })}
</div>
</Button>
</div> </div>
</div> </div>
</div> <div className="relative mb-2 flex grow gap-0.5 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-xs">
<div> <div className="absolute top-0 left-0 h-full w-full bg-toast-warning-bg opacity-40" />
<div className="pt-2 system-md-semibold text-text-primary"> <div className="flex items-start justify-center p-1">
{t('common.chooseDSL', { ns: 'workflow' })} <RiAlertFill className="h-4 w-4 shrink-0 text-text-warning-secondary" />
</div>
<div className="flex grow flex-col items-start gap-0.5 py-1">
<div className="system-xs-medium whitespace-pre-line text-text-primary">{t('common.importDSLTip', { ns: 'workflow' })}</div>
<div className="flex items-start gap-1 self-stretch pt-1 pb-0.5">
<Button
size="small"
variant="secondary"
className="z-1000"
onClick={onBackup}
>
<RiFileDownloadLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
<div className="flex items-center justify-center gap-1 px-[3px]">
{t('common.backupCurrentDraft', { ns: 'workflow' })}
</div>
</Button>
</div>
</div>
</div> </div>
<div className="flex w-full flex-col items-start justify-center gap-4 self-stretch py-4"> <div>
<Uploader <div className="pt-2 system-md-semibold text-text-primary">
file={currentFile} {t('common.chooseDSL', { ns: 'workflow' })}
updateFile={handleFile} </div>
className="mt-0! w-full" <div className="flex w-full flex-col items-start justify-center gap-4 self-stretch py-4">
/> <Uploader
file={currentFile}
updateFile={handleFile}
className="mt-0! w-full"
/>
</div>
</div> </div>
</div> <div className="flex items-center justify-end gap-2 self-stretch pt-5">
<div className="flex items-center justify-end gap-2 self-stretch pt-5"> <Button onClick={onCancel}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button onClick={onCancel}>{t('newApp.Cancel', { ns: 'app' })}</Button> <Button
<Button disabled={!currentFile || loading}
disabled={!currentFile || loading} variant="primary"
variant="primary" tone="destructive"
tone="destructive" onClick={handleImport}
onClick={handleImport} loading={loading}
loading={loading} >
> {t('common.overwriteAndImport', { ns: 'workflow' })}
{t('common.overwriteAndImport', { ns: 'workflow' })} </Button>
</Button> </div>
</div> </DialogContent>
</Modal> </Dialog>
<Modal <Dialog
isShow={showErrorModal} open={showErrorModal}
onClose={() => setShowErrorModal(false)} onOpenChange={(open) => {
className="w-[480px]" if (!open)
setShowErrorModal(false)
}}
> >
<div className="flex flex-col items-start gap-2 self-stretch pb-4"> <DialogContent className="w-full max-w-[480px]! overflow-hidden! border-none text-left align-middle">
<div className="title-2xl-semi-bold text-text-primary">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</div>
<div className="flex grow flex-col system-md-regular text-text-secondary"> <div className="flex flex-col items-start gap-2 self-stretch pb-4">
<div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div> <div className="title-2xl-semi-bold text-text-primary">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</div>
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div> <div className="flex grow flex-col system-md-regular text-text-secondary">
<br /> <div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
<div> <div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })} <br />
<span className="system-md-medium">{versions?.importedVersion}</span> <div>
</div> {t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
<div> <span className="system-md-medium">{versions?.importedVersion}</span>
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })} </div>
<span className="system-md-medium">{versions?.systemVersion}</span> <div>
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
<span className="system-md-medium">{versions?.systemVersion}</span>
</div>
</div> </div>
</div> </div>
</div> <div className="flex items-start justify-end gap-2 self-stretch pt-6">
<div className="flex items-start justify-end gap-2 self-stretch pt-6"> <Button variant="secondary" onClick={() => setShowErrorModal(false)}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button variant="secondary" onClick={() => setShowErrorModal(false)}>{t('newApp.Cancel', { ns: 'app' })}</Button> <Button variant="primary" tone="destructive" onClick={onUpdateDSLConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button>
<Button variant="primary" tone="destructive" onClick={onUpdateDSLConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button> </div>
</div> </DialogContent>
</Modal> </Dialog>
</> </>
) )
} }

View File

@ -1,9 +1,9 @@
'use client' 'use client'
import { Button } from '@langgenius/dify-ui/button' import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { RiExternalLinkLine } from '@remixicon/react' import { RiExternalLinkLine } from '@remixicon/react'
import * as React from 'react' import * as React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import { useDocLink } from '@/context/i18n' import { useDocLink } from '@/context/i18n'
import { useModalContextSelector } from '@/context/modal-context' import { useModalContextSelector } from '@/context/modal-context'
import useTimestamp from '@/hooks/use-timestamp' import useTimestamp from '@/hooks/use-timestamp'
@ -41,63 +41,70 @@ const ExpireNoticeModal: React.FC<Props> = ({ expireAt, expired, onClose }) => {
} }
return ( return (
<Modal <Dialog
isShow open
onClose={onClose} onOpenChange={(open) => {
title={expired ? t(`${i18nPrefix}.expired.title`, { ns: 'education' }) : t(`${i18nPrefix}.isAboutToExpire.title`, { ns: 'education', date: formatTime(expireAt, t(`${i18nPrefix}.dateFormat`, { ns: 'education' }) as string), interpolation: { escapeValue: false } })} if (!open)
closable onClose()
className="max-w-[600px]" }}
> >
<div className="mt-5 space-y-5 body-md-regular text-text-secondary"> <DialogContent className="w-full max-w-[600px] overflow-hidden! border-none text-left align-middle">
<div> <DialogCloseButton data-testid="modal-close-button" />
{expired <DialogTitle className="title-2xl-semi-bold text-text-primary">
? ( {expired ? t(`${i18nPrefix}.expired.title`, { ns: 'education' }) : t(`${i18nPrefix}.isAboutToExpire.title`, { ns: 'education', date: formatTime(expireAt, t(`${i18nPrefix}.dateFormat`, { ns: 'education' }) as string), interpolation: { escapeValue: false } })}
<> </DialogTitle>
<div>{t(`${i18nPrefix}.expired.summary.line1`, { ns: 'education' })}</div>
<div>{t(`${i18nPrefix}.expired.summary.line2`, { ns: 'education' })}</div> <div className="mt-5 space-y-5 body-md-regular text-text-secondary">
</> <div>
) {expired
: t(`${i18nPrefix}.isAboutToExpire.summary`, { ns: 'education' })} ? (
<>
<div>{t(`${i18nPrefix}.expired.summary.line1`, { ns: 'education' })}</div>
<div>{t(`${i18nPrefix}.expired.summary.line2`, { ns: 'education' })}</div>
</>
)
: t(`${i18nPrefix}.isAboutToExpire.summary`, { ns: 'education' })}
</div>
<div>
<strong className="block title-md-semi-bold">{t(`${i18nPrefix}.stillInEducation.title`, { ns: 'education' })}</strong>
{t(`${i18nPrefix}.stillInEducation.${expired ? 'expired' : 'isAboutToExpire'}`, { ns: 'education' })}
</div>
<div>
<strong className="block title-md-semi-bold">{t(`${i18nPrefix}.alreadyGraduated.title`, { ns: 'education' })}</strong>
{t(`${i18nPrefix}.alreadyGraduated.${expired ? 'expired' : 'isAboutToExpire'}`, { ns: 'education' })}
</div>
</div> </div>
<div> <div className="mt-7 flex items-center justify-between space-x-2">
<strong className="block title-md-semi-bold">{t(`${i18nPrefix}.stillInEducation.title`, { ns: 'education' })}</strong> <Link className="flex items-center space-x-1 system-xs-regular text-text-accent" href={eduDocLink} target="_blank" rel="noopener noreferrer">
{t(`${i18nPrefix}.stillInEducation.${expired ? 'expired' : 'isAboutToExpire'}`, { ns: 'education' })} <div>{t('learn', { ns: 'education' })}</div>
<RiExternalLinkLine className="size-3" />
</Link>
<div className="flex space-x-2">
{expired
? (
<Button
onClick={() => {
onClose()
setShowPricingModal()
}}
className="flex items-center space-x-1"
>
<SparklesSoftAccent className="size-4" />
<div className="text-components-button-secondary-accent-text">{t(`${i18nPrefix}.action.upgrade`, { ns: 'education' })}</div>
</Button>
)
: (
<Button onClick={onClose}>
{t(`${i18nPrefix}.action.dismiss`, { ns: 'education' })}
</Button>
)}
<Button variant="primary" onClick={handleConfirm}>
{t(`${i18nPrefix}.action.reVerify`, { ns: 'education' })}
</Button>
</div>
</div> </div>
<div> </DialogContent>
<strong className="block title-md-semi-bold">{t(`${i18nPrefix}.alreadyGraduated.title`, { ns: 'education' })}</strong> </Dialog>
{t(`${i18nPrefix}.alreadyGraduated.${expired ? 'expired' : 'isAboutToExpire'}`, { ns: 'education' })}
</div>
</div>
<div className="mt-7 flex items-center justify-between space-x-2">
<Link className="flex items-center space-x-1 system-xs-regular text-text-accent" href={eduDocLink} target="_blank" rel="noopener noreferrer">
<div>{t('learn', { ns: 'education' })}</div>
<RiExternalLinkLine className="size-3" />
</Link>
<div className="flex space-x-2">
{expired
? (
<Button
onClick={() => {
onClose()
setShowPricingModal()
}}
className="flex items-center space-x-1"
>
<SparklesSoftAccent className="size-4" />
<div className="text-components-button-secondary-accent-text">{t(`${i18nPrefix}.action.upgrade`, { ns: 'education' })}</div>
</Button>
)
: (
<Button onClick={onClose}>
{t(`${i18nPrefix}.action.dismiss`, { ns: 'education' })}
</Button>
)}
<Button variant="primary" onClick={handleConfirm}>
{t(`${i18nPrefix}.action.reVerify`, { ns: 'education' })}
</Button>
</div>
</div>
</Modal>
) )
} }