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'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import CustomDialog from '@/app/components/base/dialog'
import Textarea from '@/app/components/base/textarea'
import { useAppContext } from '@/context/app-context'
import { useRouter } from '@/next/navigation'
@ -47,26 +47,34 @@ export default function FeedBack(props: DeleteAccountProps) {
handleSuccess()
}, [handleSuccess, props])
return (
<CustomDialog
show={true}
onClose={props.onCancel}
title={t('account.feedbackTitle', { ns: 'common' })}
className="max-w-[480px]"
footer={false}
<Dialog
open
onOpenChange={(open) => {
if (!open)
props.onCancel()
}}
>
<label className="mt-3 mb-1 flex items-center system-sm-semibold text-text-secondary">{t('account.feedbackLabel', { ns: 'common' })}</label>
<Textarea
rows={6}
value={userFeedback}
placeholder={t('account.feedbackPlaceholder', { ns: 'common' }) as string}
onChange={(e) => {
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>
</CustomDialog>
<DialogContent
className="max-w-[480px] overflow-hidden!"
backdropClassName="bg-background-overlay-backdrop backdrop-blur-[6px]"
>
<DialogTitle className="pr-8 pb-3 title-2xl-semi-bold text-text-primary">
{t('account.feedbackTitle', { ns: 'common' })}
</DialogTitle>
<label className="mt-3 mb-1 flex items-center system-sm-semibold text-text-secondary">{t('account.feedbackLabel', { ns: 'common' })}</label>
<Textarea
rows={6}
value={userFeedback}
placeholder={t('account.feedbackPlaceholder', { ns: 'common' }) as string}
onChange={(e) => {
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'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { useCallback, useState } from 'react'
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 CheckEmail from './components/check-email'
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 (
<CustomDialog
show={true}
onClose={props.onCancel}
title={t('account.delete', { ns: 'common' })}
className="max-w-[480px]"
footer={false}
<Dialog
open
onOpenChange={(open) => {
if (!open)
props.onCancel()
}}
>
{!showVerifyEmail && <CheckEmail onCancel={props.onCancel} onConfirm={handleEmailCheckSuccess} />}
{showVerifyEmail && (
<VerifyEmail
onCancel={props.onCancel}
onConfirm={() => {
setShowFeedbackDialog(true)
}}
/>
)}
</CustomDialog>
<DialogContent
className="max-w-[480px] overflow-hidden!"
backdropClassName="bg-background-overlay-backdrop backdrop-blur-[6px]"
>
<DialogTitle className="pr-8 pb-3 title-2xl-semi-bold text-text-primary">
{t('account.delete', { ns: 'common' })}
</DialogTitle>
{!showVerifyEmail && <CheckEmail onCancel={props.onCancel} onConfirm={handleEmailCheckSuccess} />}
{showVerifyEmail && (
<VerifyEmail
onCancel={props.onCancel}
onConfirm={() => {
setShowFeedbackDialog(true)
}}
/>
)}
</DialogContent>
</Dialog>
)
}

View File

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

View File

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

View File

@ -2,10 +2,10 @@
import type { FC } from 'react'
import type { ConversationHistoriesRole } from '@/models/debug'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
type Props = {
isShow: boolean
@ -25,37 +25,45 @@ const EditModal: FC<Props> = ({
const { t } = useTranslation()
const [tempData, setTempData] = useState(data)
return (
<Modal
title={t('feature.conversationHistory.editModal.title', { ns: 'appDebug' })}
isShow={isShow}
onClose={onClose}
<Dialog
open={isShow}
onOpenChange={(open) => {
if (!open)
onClose()
}}
>
<div className="mt-6 text-sm leading-[21px] font-medium text-text-primary">{t('feature.conversationHistory.editModal.userPrefix', { ns: 'appDebug' })}</div>
<input
className="mt-2 box-border h-10 w-full rounded-lg bg-components-input-bg-normal px-3 text-sm leading-10"
value={tempData.user_prefix}
onChange={e => setTempData({
...tempData,
user_prefix: e.target.value,
})}
/>
<DialogContent className="w-full max-w-[480px] overflow-hidden! border-none p-6 text-left align-middle">
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{t('feature.conversationHistory.editModal.title', { ns: 'appDebug' })}
</DialogTitle>
<div className="mt-6 text-sm leading-[21px] font-medium text-text-primary">{t('feature.conversationHistory.editModal.assistantPrefix', { ns: 'appDebug' })}</div>
<input
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}
onChange={e => setTempData({
...tempData,
assistant_prefix: e.target.value,
})}
placeholder={t('chat.conversationNamePlaceholder', { ns: 'common' }) || ''}
/>
<div className="mt-6 text-sm leading-[21px] font-medium text-text-primary">{t('feature.conversationHistory.editModal.userPrefix', { ns: 'appDebug' })}</div>
<input
className="mt-2 box-border h-10 w-full rounded-lg bg-components-input-bg-normal px-3 text-sm leading-10"
value={tempData.user_prefix}
onChange={e => setTempData({
...tempData,
user_prefix: 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={() => onSave(tempData)} loading={saveLoading}>{t('operation.save', { ns: 'common' })}</Button>
</div>
</Modal>
<div className="mt-6 text-sm leading-[21px] font-medium text-text-primary">{t('feature.conversationHistory.editModal.assistantPrefix', { ns: 'appDebug' })}</div>
<input
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}
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 { Item as SelectItem } from './type-select'
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 * as React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { useStore as useAppStore } from '@/app/components/app/store'
import Modal from '@/app/components/base/modal'
import ConfigContext from '@/context/debug-configuration'
import { AppModeEnum } from '@/types/app'
import { checkKeys, getNewVarInWorkflow, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var'
@ -141,35 +141,43 @@ const ConfigModal: FC<IConfigModalProps> = ({
}
return (
<Modal
title={t(`variableConfig.${isCreate ? 'addModalTitle' : 'editModalTitle'}`, { ns: 'appDebug' })}
isShow={isShow}
onClose={onClose}
<Dialog
open={isShow}
onOpenChange={(open) => {
if (!open)
onClose()
}}
>
<div className="mb-8" ref={modalRef} tabIndex={-1}>
<ConfigModalFormFields
checkboxDefaultSelectValue={checkboxDefaultSelectValue}
isStringInput={isStringInput}
jsonSchemaStr={jsonSchemaStr}
maxLength={max_length}
modelId={modelConfig.model_id}
onFilePayloadChange={payload => setTempPayload(payload as InputVar)}
onJSONSchemaChange={handleJSONSchemaChange}
onPayloadChange={handlePayloadChange}
onTypeChange={handleTypeChange}
onVarKeyBlur={handleVarKeyBlur}
onVarNameChange={handleVarNameChange}
options={options}
selectOptions={selectOptions}
tempPayload={tempPayload}
t={t}
<DialogContent className="overflow-hidden! border-none text-left align-middle">
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{t(`variableConfig.${isCreate ? 'addModalTitle' : 'editModalTitle'}`, { ns: 'appDebug' })}
</DialogTitle>
<div className="mb-8" ref={modalRef} tabIndex={-1}>
<ConfigModalFormFields
checkboxDefaultSelectValue={checkboxDefaultSelectValue}
isStringInput={isStringInput}
jsonSchemaStr={jsonSchemaStr}
maxLength={max_length}
modelId={modelConfig.model_id}
onFilePayloadChange={payload => setTempPayload(payload as InputVar)}
onJSONSchemaChange={handleJSONSchemaChange}
onPayloadChange={handlePayloadChange}
onTypeChange={handleTypeChange}
onVarKeyBlur={handleVarKeyBlur}
onVarNameChange={handleVarNameChange}
options={options}
selectOptions={selectOptions}
tempPayload={tempPayload}
t={t}
/>
</div>
<ModalFoot
onConfirm={handleConfirm}
onCancel={onClose}
/>
</div>
<ModalFoot
onConfirm={handleConfirm}
onCancel={onClose}
/>
</Modal>
</DialogContent>
</Dialog>
)
}
export default React.memo(ConfigModal)

View File

@ -14,6 +14,7 @@ import {
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import {
RiDatabase2Line,
@ -32,9 +33,8 @@ import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Generator } from '@/app/components/base/icons/src/vender/other'
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 ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import { generateBasicAppFirstTimeRule, generateRule } from '@/service/debug'
@ -282,143 +282,148 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
}
return (
<Modal
isShow={isShow}
onClose={onClose}
className="min-w-[1140px] p-0!"
<Dialog
open={isShow}
onOpenChange={(open) => {
if (!open)
onClose()
}}
>
<div className="flex h-[680px] flex-wrap">
<div className="h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6">
<div className="mb-5">
<div className={`text-lg leading-[28px] font-bold ${s.textGradient}`}>{t('generate.title', { ns: 'appDebug' })}</div>
<div className="mt-1 text-[13px] font-normal text-text-tertiary">{t('generate.description', { ns: 'appDebug' })}</div>
</div>
<div>
<ModelParameterModal
popupClassName="w-[520px]!"
isAdvancedMode={true}
provider={model.provider}
completionParams={model.completion_params}
modelId={model.name}
setModel={handleModelChange}
onCompletionParamsChange={handleCompletionParamsChange}
hideDebugWithMultipleModel
/>
</div>
{isBasicMode && (
<div className="mt-4">
<div className="flex items-center">
<div className="mr-3 shrink-0 text-xs leading-[18px] font-semibold text-text-tertiary uppercase">{t('generate.tryIt', { ns: 'appDebug' })}</div>
<div
className="h-px grow"
style={{
background: 'linear-gradient(to right, rgba(243, 244, 246, 1), rgba(243, 244, 246, 0))',
}}
>
<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="flex h-[680px] flex-wrap">
<div className="h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6">
<div className="mb-5">
<div className={`text-lg leading-[28px] font-bold ${s.textGradient}`}>{t('generate.title', { ns: 'appDebug' })}</div>
<div className="mt-1 text-[13px] font-normal text-text-tertiary">{t('generate.description', { ns: 'appDebug' })}</div>
</div>
<div>
<ModelParameterModal
popupClassName="w-[520px]!"
isAdvancedMode={true}
provider={model.provider}
completionParams={model.completion_params}
modelId={model.name}
setModel={handleModelChange}
onCompletionParamsChange={handleCompletionParamsChange}
hideDebugWithMultipleModel
/>
</div>
{isBasicMode && (
<div className="mt-4">
<div className="flex items-center">
<div className="mr-3 shrink-0 text-xs leading-[18px] font-semibold text-text-tertiary uppercase">{t('generate.tryIt', { ns: 'appDebug' })}</div>
<div
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 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)}
/>
))}
)}
{/* inputs */}
<div className="mt-4">
<div>
<div className="mb-1.5 system-sm-semibold-uppercase text-text-secondary">{t('generate.instruction', { ns: 'appDebug' })}</div>
{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>
{(!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>
)}
{/* inputs */}
<div className="mt-4">
<div>
<div className="mb-1.5 system-sm-semibold-uppercase text-text-secondary">{t('generate.instruction', { ns: 'appDebug' })}</div>
{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>
{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>
{(!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}
{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>
</DialogContent>
</Dialog>
)
}
export default React.memo(GetAutomaticRes)

View File

@ -13,6 +13,7 @@ import {
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import {
useBoolean,
@ -23,7 +24,6 @@ import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Generator } from '@/app/components/base/icons/src/vender/other'
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 { 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'
@ -202,99 +202,104 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
)
return (
<Modal
isShow={isShow}
onClose={onClose}
className="min-w-[1140px] p-0!"
<Dialog
open={isShow}
onOpenChange={(open) => {
if (!open)
onClose()
}}
>
<div className="relative flex h-[680px] flex-wrap">
<div className="h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6">
<div className="mb-5">
<div className={`text-lg leading-[28px] font-bold ${s.textGradient}`}>{t('codegen.title', { ns: 'appDebug' })}</div>
<div className="mt-1 text-[13px] font-normal text-text-tertiary">{t('codegen.description', { ns: 'appDebug' })}</div>
</div>
<div className="mb-4">
<ModelParameterModal
popupClassName="w-[520px]!"
isAdvancedMode={true}
provider={model.provider}
completionParams={model.completion_params}
modelId={model.name}
setModel={handleModelChange}
onCompletionParamsChange={handleCompletionParamsChange}
hideDebugWithMultipleModel
/>
</div>
<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}
<DialogContent className="max-h-none w-full min-w-[1140px] overflow-hidden! border-none p-0! text-left align-middle">
<div className="relative flex h-[680px] flex-wrap">
<div className="h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6">
<div className="mb-5">
<div className={`text-lg leading-[28px] font-bold ${s.textGradient}`}>{t('codegen.title', { ns: 'appDebug' })}</div>
<div className="mt-1 text-[13px] font-normal text-text-tertiary">{t('codegen.description', { ns: 'appDebug' })}</div>
</div>
<div className="mb-4">
<ModelParameterModal
popupClassName="w-[520px]!"
isAdvancedMode={true}
provider={model.provider}
completionParams={model.completion_params}
modelId={model.name}
setModel={handleModelChange}
onCompletionParamsChange={handleCompletionParamsChange}
hideDebugWithMultipleModel
/>
</div>
<IdeaOutput
value={ideaOutput}
onChange={setIdeaOutput}
/>
<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>
<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('codegen.generate', { ns: 'appDebug' })}</span>
</Button>
<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('codegen.generate', { ns: 'appDebug' })}</span>
</Button>
</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>
{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>
<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('codegen.overwriteConfirmTitle', { ns: 'appDebug' })}
</AlertDialogTitle>
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
{t('codegen.overwriteConfirmMessage', { 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>
</Modal>
<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('codegen.overwriteConfirmTitle', { ns: 'appDebug' })}
</AlertDialogTitle>
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
{t('codegen.overwriteConfirmMessage', { 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>
</DialogContent>
</Dialog>
)
}

View File

@ -3,12 +3,12 @@ import type { DataSet } from '@/models/datasets'
import type { DatasetConfigs } from '@/models/debug'
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 { RiEqualizer2Line } from '@remixicon/react'
import { memo, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
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 { useCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import {
@ -123,32 +123,36 @@ const ParamsConfig = ({
</Button>
{
rerankSettingModalOpen && (
<Modal
isShow={rerankSettingModalOpen}
onClose={() => {
setRerankSettingModalOpen(false)
<Dialog
open={rerankSettingModalOpen}
onOpenChange={(open) => {
if (!open) {
setRerankSettingModalOpen(false)
}
}}
className="sm:min-w-[528px]"
>
<ConfigContent
datasetConfigs={tempDataSetConfigs}
onChange={handleSetTempDataSetConfigs}
selectedDatasets={selectedDatasets}
/>
<DialogContent className="w-full max-w-[480px] overflow-hidden! border-none text-left align-middle sm:min-w-[528px]">
<div className="mt-6 flex justify-end">
<Button
className="mr-2 shrink-0"
onClick={() => {
setTempDataSetConfigs(datasetConfigs)
setRerankSettingModalOpen(false)
}}
>
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button variant="primary" className="shrink-0" onClick={handleSave}>{t('operation.save', { ns: 'common' })}</Button>
</div>
</Modal>
<ConfigContent
datasetConfigs={tempDataSetConfigs}
onChange={handleSetTempDataSetConfigs}
selectedDatasets={selectedDatasets}
/>
<div className="mt-6 flex justify-end">
<Button
className="mr-2 shrink-0"
onClick={() => {
setTempDataSetConfigs(datasetConfigs)
setRerankSettingModalOpen(false)
}}
>
{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 Modal from '@/app/components/base/modal'
type DSLConfirmModalProps = {
versions?: {
@ -20,32 +27,36 @@ const DSLConfirmModal = ({
const { t } = useTranslation()
return (
<Modal
isShow
onClose={() => onCancel()}
className="w-[480px]"
<AlertDialog
open
onOpenChange={(open) => {
if (!open)
onCancel()
}}
>
<div className="flex flex-col items-start gap-2 self-stretch pb-4">
<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>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
<br />
<div>
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
<span className="system-md-medium">{versions.importedVersion}</span>
</div>
<div>
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
<span className="system-md-medium">{versions.systemVersion}</span>
</div>
<AlertDialogContent className="w-[480px] overflow-hidden! border-none text-left align-middle shadow-xl">
<div className="flex flex-col items-start gap-2 self-stretch p-6 pb-4">
<AlertDialogTitle className="title-2xl-semi-bold text-text-primary">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</AlertDialogTitle>
<AlertDialogDescription render={<div />} className="flex grow flex-col system-md-regular text-text-secondary">
<div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
<br />
<div>
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
<span className="system-md-medium">{versions.importedVersion}</span>
</div>
<div>
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
<span className="system-md-medium">{versions.systemVersion}</span>
</div>
</AlertDialogDescription>
</div>
</div>
<div className="flex items-start justify-end gap-2 self-stretch pt-6">
<Button variant="secondary" onClick={() => onCancel()}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button variant="primary" tone="destructive" onClick={onConfirm} disabled={confirmDisabled}>{t('newApp.Confirm', { ns: 'app' })}</Button>
</div>
</Modal>
<AlertDialogActions>
<AlertDialogCancelButton variant="secondary">{t('newApp.Cancel', { ns: 'app' })}</AlertDialogCancelButton>
<AlertDialogConfirmButton onClick={onConfirm} disabled={confirmDisabled}>{t('newApp.Confirm', { ns: 'app' })}</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
)
}

View File

@ -3,14 +3,13 @@
import type { MouseEventHandler } from 'react'
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 { RiCloseLine } from '@remixicon/react'
import { useDebounceFn, useKeyPress } from 'ahooks'
import { noop } from 'es-toolkit/function'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
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 { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
@ -219,108 +218,112 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
return (
<>
<Modal
className="w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0 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 h-8 w-8 cursor-pointer items-center"
onClick={() => onClose()}
>
<RiCloseLine className="h-5 w-5 text-text-tertiary" />
<Dialog open={show}>
<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">
<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 h-8 w-8 cursor-pointer items-center"
onClick={() => onClose()}
>
<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">
{
tabs.map(tab => (
<div
key={tab.key}
className={cn(
'relative flex h-full cursor-pointer items-center',
currentTab === tab.key && 'text-text-primary',
)}
onClick={() => setCurrentTab(tab.key)}
>
{tab.label}
{
currentTab === tab.key && (
<div className="absolute bottom-0 h-[2px] w-full bg-util-colors-blue-brand-blue-brand-600"></div>
)
}
</div>
))
}
</div>
<div className="px-6 py-4">
{
currentTab === CreateFromDSLModalTab.FROM_FILE && (
<Uploader
className="mt-0"
file={currentFile}
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 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 => (
<div
key={tab.key}
className={cn(
'relative flex h-full cursor-pointer items-center',
currentTab === tab.key && 'text-text-primary',
)}
onClick={() => setCurrentTab(tab.key)}
>
{tab.label}
{
currentTab === tab.key && (
<div className="absolute bottom-0 h-[2px] w-full bg-util-colors-blue-brand-blue-brand-600"></div>
)
}
</div>
))
}
</div>
<div className="px-6 py-4">
{
currentTab === CreateFromDSLModalTab.FROM_FILE && (
<Uploader
className="mt-0"
file={currentFile}
updateFile={handleFile}
/>
</div>
)
}
</div>
{isAppsFull && (
<div className="px-6">
<AppsFull className="mt-0" loc="app-create-dsl" />
)
}
{
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>
)}
<div className="flex justify-end px-6 py-5">
<Button className="mr-2" onClick={onClose}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button
disabled={buttonDisabled}
variant="primary"
onClick={handleCreateApp}
className="gap-1"
>
<span>{t('newApp.Create', { ns: 'app' })}</span>
<ShortcutsName keys={['ctrl', '↵']} bgColor="white" />
</Button>
</div>
</Modal>
<Modal
isShow={showErrorModal}
onClose={() => setShowErrorModal(false)}
className="w-[480px]"
{isAppsFull && (
<div className="px-6">
<AppsFull className="mt-0" loc="app-create-dsl" />
</div>
)}
<div className="flex justify-end px-6 py-5">
<Button className="mr-2" onClick={onClose}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button
disabled={buttonDisabled}
variant="primary"
onClick={handleCreateApp}
className="gap-1"
>
<span>{t('newApp.Create', { ns: 'app' })}</span>
<ShortcutsName keys={['ctrl', '↵']} bgColor="white" />
</Button>
</div>
</DialogContent>
</Dialog>
<Dialog
open={showErrorModal}
onOpenChange={(open) => {
if (!open)
setShowErrorModal(false)
}}
>
<div className="flex flex-col items-start gap-2 self-stretch pb-4">
<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>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
<br />
<div>
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
<span className="system-md-medium">{versions?.importedVersion}</span>
</div>
<div>
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
<span className="system-md-medium">{versions?.systemVersion}</span>
<DialogContent className="w-full max-w-[480px]! overflow-hidden! border-none text-left align-middle">
<div className="flex flex-col items-start gap-2 self-stretch pb-4">
<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>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
<br />
<div>
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
<span className="system-md-medium">{versions?.importedVersion}</span>
</div>
<div>
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
<span className="system-md-medium">{versions?.systemVersion}</span>
</div>
</div>
</div>
</div>
<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="primary" tone="destructive" onClick={onDSLConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button>
</div>
</Modal>
<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="primary" tone="destructive" onClick={onDSLConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button>
</div>
</DialogContent>
</Dialog>
</>
)
}

View File

@ -1,16 +1,14 @@
'use client'
import type { AppIconType } from '@/types/app'
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 { RiCloseLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
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 { useProviderContext } from '@/context/provider-context'
import AppIconPicker from '../../base/app-icon-picker'
@ -71,40 +69,39 @@ const DuplicateAppModal = ({
return (
<>
<Modal
isShow={show}
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>
<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"
/>
<Dialog open={show}>
<DialogContent className="w-full max-w-[480px]! overflow-hidden! border-none px-8 text-left align-middle">
<div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onHide}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</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>
</Modal>
<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>
{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 && (
<AppIconPicker
onSelect={(payload) => {

View File

@ -12,9 +12,9 @@ import {
} from '@langgenius/dify-ui/alert-dialog'
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 { RiCloseLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
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 { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
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 { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useAppContext } from '@/context/app-context'
@ -109,69 +108,68 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
return (
<>
<Modal
className={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>
<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"
/>
<Dialog open={show}>
<DialogContent className={cn('w-full overflow-hidden! border-none text-left align-middle', cn('w-[600px] max-w-[600px] p-8'))}>
<div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onClose}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</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 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="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 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>
</Modal>
<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>
{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
open={showConfirmDelete}
onOpenChange={handleConfirmDeleteOpenChange}

View File

@ -4,15 +4,14 @@ import type { OnImageInput } from './ImageInput'
import type { AppIconType, ImageFile } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { RiImageCircleAiLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
import Divider from '../divider'
import EmojiPickerInner from '../emoji-picker/Inner'
import { useLocalFileUploader } from '../image-uploader/hooks'
import Modal from '../modal'
import ImageInput from './ImageInput'
import s from './style.module.css'
import getCroppedImg from './utils'
@ -45,7 +44,6 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
onSelect,
onClose,
initialEmoji,
className,
}) => {
const { t } = useTranslation()
@ -113,57 +111,54 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
}
return (
<Modal
onClose={noop}
isShow
closable={false}
wrapperClassName={className}
className={cn(s.container, 'h-[462px]! w-[362px]! p-0!')}
>
{!DISABLE_UPLOAD_IMAGE_AS_ICON && (
<div className="w-full p-2 pb-0">
<div className="flex items-center justify-center gap-2 rounded-xl bg-background-body p-1 text-text-primary">
{tabs.map(tab => (
<button
type="button"
key={tab.key}
className={cn(
'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',
)}
onClick={() => setActiveTab(tab.key as AppIconType)}
>
{tab.icon}
{' '}
<Dialog open>
<DialogContent className={cn('max-h-none w-full overflow-hidden! border-none text-left align-middle', s.container, 'h-[462px]! w-[362px]! p-0!')}>
{!DISABLE_UPLOAD_IMAGE_AS_ICON && (
<div className="w-full p-2 pb-0">
<div className="flex items-center justify-center gap-2 rounded-xl bg-background-body p-1 text-text-primary">
{tabs.map(tab => (
<button
type="button"
key={tab.key}
className={cn(
'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',
)}
onClick={() => setActiveTab(tab.key as AppIconType)}
>
{tab.icon}
{' '}
&nbsp;
{tab.label}
</button>
))}
{tab.label}
</button>
))}
</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>
)}
{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>
</DialogContent>
</Dialog>
)
}

View File

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

View File

@ -4,25 +4,13 @@ import userEvent from '@testing-library/user-event'
import * as ReactI18next from 'react-i18next'
import RenameModal from '../rename-modal'
vi.mock('@/app/components/base/modal', () => ({
default: ({
title,
isShow,
children,
}: {
title: ReactNode
isShow: boolean
children: ReactNode
}) => {
if (!isShow)
return null
return (
<div role="dialog">
<h2>{title}</h2>
{children}
</div>
)
},
vi.mock('@langgenius/dify-ui/dialog', () => ({
Dialog: ({ children, open }: { children: ReactNode, open?: boolean }) =>
open === false ? null : <>{children}</>,
DialogContent: ({ children }: { children: ReactNode }) => (
<div role="dialog">{children}</div>
),
DialogTitle: ({ children }: { children: ReactNode }) => <h2>{children}</h2>,
}))
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 { Button } from '@langgenius/dify-ui/button'
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 { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import Modal from '@/app/components/base/modal'
import EmojiPickerInner from './Inner'
type IEmojiPickerProps = {
@ -21,7 +20,6 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
isModal = true,
onSelect,
onClose,
className,
}) => {
const { t } = useTranslation()
const [selectedEmoji, setSelectedEmoji] = useState('')
@ -34,39 +32,36 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
return isModal
? (
<Modal
onClose={noop}
isShow
closable={false}
wrapperClassName={className}
className={cn('flex max-h-[552px] flex-col rounded-xl border-[0.5px] border-divider-subtle p-0 shadow-xl')}
>
<EmojiPickerInner
className="pt-3"
onSelect={handleSelectEmoji}
/>
<Divider className="mt-3 mb-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
disabled={selectedEmoji === '' || !selectedBackground}
variant="primary"
className="w-full"
onClick={() => {
onSelect?.(selectedEmoji, selectedBackground!)
}}
>
{t('iconPicker.ok', { ns: 'app' })}
</Button>
</div>
</Modal>
<Dialog open>
<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')}>
<EmojiPickerInner
className="pt-3"
onSelect={handleSelectEmoji}
/>
<Divider className="mt-3 mb-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
disabled={selectedEmoji === '' || !selectedBackground}
variant="primary"
className="w-full"
onClick={() => {
onSelect?.(selectedEmoji, selectedBackground!)
}}
>
{t('iconPicker.ok', { ns: 'app' })}
</Button>
</div>
</DialogContent>
</Dialog>
)
: <></>
}

View File

@ -2,11 +2,11 @@
import type { FC } from 'react'
import type { AnnotationReplyConfig } from '@/models/debug'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react'
import { useState } from 'react'
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 { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
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)
}
return (
<Modal isShow={isShow} onClose={onHide} className="!mt-14 !w-[640px] !max-w-none !p-6">
<div className="mb-2 title-2xl-semi-bold text-text-primary">
{t(`initSetup.${isInit ? 'title' : 'configTitle'}`, { ns: 'appAnnotation' })}
</div>
<Dialog
open={isShow}
onOpenChange={(open) => {
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">
<Item title={t('feature.annotation.scoreThreshold.title', { ns: 'appDebug' })} tooltip={t('feature.annotation.scoreThreshold.description', { ns: 'appDebug' })}>
<ScoreSlider
className="mt-1"
value={(annotationConfig.score_threshold || ANNOTATION_DEFAULT.score_threshold) * 100}
onChange={(val) => {
setAnnotationConfig({
...annotationConfig,
score_threshold: val / 100,
})
}}
/>
</Item>
<div className="mb-2 title-2xl-semi-bold text-text-primary">
{t(`initSetup.${isInit ? 'title' : 'configTitle'}`, { ns: 'appAnnotation' })}
</div>
<Item title={t('modelProvider.embeddingModel.key', { ns: 'common' })} tooltip={t('embeddingModelSwitchTip', { ns: 'appAnnotation' })}>
<div className="pt-1">
<ModelSelector
defaultModel={embeddingModel && {
provider: embeddingModel.providerName,
model: embeddingModel.modelName,
}}
modelList={embeddingsModelList}
onSelect={(val) => {
setEmbeddingModel({
providerName: val.provider,
modelName: val.model,
<div className="mt-6 space-y-3">
<Item title={t('feature.annotation.scoreThreshold.title', { ns: 'appDebug' })} tooltip={t('feature.annotation.scoreThreshold.description', { ns: 'appDebug' })}>
<ScoreSlider
className="mt-1"
value={(annotationConfig.score_threshold || ANNOTATION_DEFAULT.score_threshold) * 100}
onChange={(val) => {
setAnnotationConfig({
...annotationConfig,
score_threshold: val / 100,
})
}}
/>
</div>
</Item>
</div>
</Item>
<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>
</Modal>
<Item title={t('modelProvider.embeddingModel.key', { ns: 'common' })} tooltip={t('embeddingModelSwitchTip', { ns: 'appAnnotation' })}>
<div className="pt-1">
<ModelSelector
defaultModel={embeddingModel && {
provider: embeddingModel.providerName,
model: embeddingModel.modelName,
}}
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)

View File

@ -3,12 +3,11 @@ import type { CodeBasedExtensionItem } from '@/models/common'
import type { ModerationConfig, ModerationContentConfig } from '@/models/debug'
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 { noop } from 'es-toolkit/function'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
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 { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { CustomConfigurationStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
@ -226,165 +225,164 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
}
return (
<Modal
isShow
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
role="button"
tabIndex={0}
className="cursor-pointer p-1"
onClick={onCancel}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onCancel()
}
}}
>
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" />
<Dialog open>
<DialogContent className="mt-14! w-[600px]! max-w-none! overflow-hidden! border-none p-6! text-left align-middle">
<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
role="button"
tabIndex={0}
className="cursor-pointer p-1"
onClick={onCancel}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
onCancel()
}
}}
>
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" />
</div>
</div>
</div>
<div className="py-2">
<div className="text-sm leading-9 font-medium text-text-primary">
{t('feature.moderation.modal.provider.title', { ns: 'appDebug' })}
</div>
<div className="grid grid-cols-3 gap-2.5">
{
providers.map(provider => (
<div
key={provider.key}
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',
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 === 'openai_moderation' && provider.key === 'openai_moderation' && !isOpenAIProviderConfigured && 'text-text-disabled',
)}
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="py-2">
<div className="text-sm leading-9 font-medium text-text-primary">
{t('feature.moderation.modal.provider.title', { ns: 'appDebug' })}
</div>
<div className="grid grid-cols-3 gap-2.5">
{
providers.map(provider => (
<div
key={provider.key}
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',
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 === 'openai_moderation' && provider.key === 'openai_moderation' && !isOpenAIProviderConfigured && 'text-text-disabled',
)}
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>
{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>
{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' })}
localeData.type === 'keywords' && (
<div className="py-2">
<div className="mb-1 text-sm font-medium text-text-primary">{t('feature.moderation.modal.provider.keywords', { ns: 'appDebug' })}</div>
<div className="mb-2 text-xs text-text-tertiary">{t('feature.moderation.modal.keywords.tip', { ns: 'appDebug' })}</div>
<div className="relative h-[88px] rounded-lg bg-components-input-bg-normal px-3 py-2">
<textarea
value={localeData.config?.keywords || ''}
onChange={handleDataKeywordsChange}
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' }) || ''}
/>
<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>{(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>
</div>
)
}
</div>
{
localeData.type === 'keywords' && (
<div className="py-2">
<div className="mb-1 text-sm font-medium text-text-primary">{t('feature.moderation.modal.provider.keywords', { ns: 'appDebug' })}</div>
<div className="mb-2 text-xs text-text-tertiary">{t('feature.moderation.modal.keywords.tip', { ns: 'appDebug' })}</div>
<div className="relative h-[88px] rounded-lg bg-components-input-bg-normal px-3 py-2">
<textarea
value={localeData.config?.keywords || ''}
onChange={handleDataKeywordsChange}
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' }) || ''}
/>
<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>{(localeData.config?.keywords || '').split('\n').filter(Boolean).length}</span>
/
<span className="text-text-tertiary">
100
{t('feature.moderation.modal.keywords.line', { ns: 'appDebug' })}
</span>
{
localeData.type === 'api' && (
<div className="py-2">
<div className="flex h-9 items-center justify-between">
<div className="text-sm font-medium text-text-primary">{t('apiBasedExtension.selector.title', { ns: 'common' })}</div>
<a
href={docLink('/use-dify/workspace/api-extension/api-extension')}
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>
</div>
)
}
{
localeData.type === 'api' && (
<div className="py-2">
<div className="flex h-9 items-center justify-between">
<div className="text-sm font-medium text-text-primary">{t('apiBasedExtension.selector.title', { ns: 'common' })}</div>
<a
href={docLink('/use-dify/workspace/api-extension/api-extension')}
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}
)
}
{
systemTypes.findIndex(t => t === localeData.type) < 0
&& currentProvider?.form_schema
&& (
<FormGeneration
forms={currentProvider?.form_schema}
value={localeData.config}
onChange={handleDataExtraChange}
/>
</div>
)
}
{
systemTypes.findIndex(t => t === localeData.type) < 0
&& currentProvider?.form_schema
&& (
<FormGeneration
forms={currentProvider?.form_schema}
value={localeData.config}
onChange={handleDataExtraChange}
/>
)
}
<Divider bgStyle="gradient" className="my-3 h-px" />
<ModerationContent
title={t('feature.moderation.modal.content.input', { ns: 'appDebug' }) || ''}
config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }}
onConfigChange={config => handleDataContentChange('inputs_config', config)}
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
showPreset={localeData.type !== 'api'}
/>
<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)}
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
showPreset={localeData.type !== 'api'}
/>
<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">
<Button
onClick={onCancel}
className="mr-2"
>
{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>
)
}
<Divider bgStyle="gradient" className="my-3 h-px" />
<ModerationContent
title={t('feature.moderation.modal.content.input', { ns: 'appDebug' }) || ''}
config={localeData.config?.inputs_config || { enabled: false, preset_response: '' }}
onConfigChange={config => handleDataContentChange('inputs_config', config)}
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
showPreset={localeData.type !== 'api'}
/>
<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)}
info={(localeData.type === 'api' && t('feature.moderation.modal.content.fromApi', { ns: 'appDebug' })) || ''}
showPreset={localeData.type !== 'api'}
/>
<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">
<Button
onClick={onCancel}
className="mr-2"
>
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button
variant="primary"
onClick={handleSave}
disabled={localeData.type === 'openai_moderation' && !isOpenAIProviderConfigured}
>
{t('operation.save', { ns: 'common' })}
</Button>
</div>
</DialogContent>
</Dialog>
)
}

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

View File

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

View File

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

View File

@ -29,26 +29,26 @@ type ModalSnapshot = {
className?: string
}
let mockModalProps: ModalSnapshot | null = null
vi.mock('../../../base/modal', () => ({
default: ({ isShow, children, onClose, closable, className }: { isShow: boolean, children: React.ReactNode, onClose: () => void, closable?: boolean, className?: string }) => {
let mockOnOpenChange: ((open: boolean) => void) | undefined
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 = {
isShow,
closable,
isShow: true,
closable: true,
className,
}
if (!isShow)
return null
return (
<div data-testid="annotation-full-modal" data-classname={className ?? ''}>
{closable && (
<button type="button" data-testid="mock-modal-close" onClick={onClose}>
close
</button>
)}
{children}
</div>
)
},
DialogCloseButton: () => <button type="button" data-testid="mock-modal-close" onClick={() => mockOnOpenChange?.(false)}>close</button>,
}))
describe('AnnotationFullModal', () => {
@ -56,6 +56,7 @@ describe('AnnotationFullModal', () => {
vi.clearAllMocks()
mockUpgradeBtnProps = null
mockModalProps = null
mockOnOpenChange = undefined
})
// Rendering marketing copy inside modal
@ -71,8 +72,9 @@ describe('AnnotationFullModal', () => {
expect(mockModalProps).toEqual(expect.objectContaining({
isShow: 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'
import type { FC } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import GridMask from '@/app/components/base/grid-mask'
import Modal from '../../base/modal'
import UpgradeBtn from '../upgrade-btn'
import s from './style.module.css'
import Usage from './usage'
@ -20,28 +20,33 @@ const AnnotationFullModal: FC<Props> = ({
const { t } = useTranslation()
return (
<Modal
isShow={show}
onClose={onHide}
closable
className="p-0!"
<Dialog
open={show}
onOpenChange={(open) => {
if (!open)
onHide()
}}
>
<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>
<DialogContent className="w-full overflow-hidden! border-none p-0! text-left align-middle">
<DialogCloseButton data-testid="modal-close-button" />
<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>
<Usage className="mt-4" />
<div className="mt-7 flex justify-end">
<UpgradeBtn loc="annotation-create" />
</div>
</div>
</GridMask>
</Modal>
</GridMask>
</DialogContent>
</Dialog>
)
}
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 Modal from '@/app/components/base/modal'
type DSLConfirmModalProps = {
versions?: {
@ -20,32 +27,36 @@ const DSLConfirmModal = ({
const { t } = useTranslation()
return (
<Modal
isShow
onClose={() => onCancel()}
className="w-[480px]"
<AlertDialog
open
onOpenChange={(open) => {
if (!open)
onCancel()
}}
>
<div className="flex flex-col items-start gap-2 self-stretch pb-4">
<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>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
<br />
<div>
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
<span className="system-md-medium">{versions.importedVersion}</span>
</div>
<div>
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
<span className="system-md-medium">{versions.systemVersion}</span>
</div>
<AlertDialogContent className="w-[480px] overflow-hidden! border-none text-left align-middle shadow-xl">
<div className="flex flex-col items-start gap-2 self-stretch p-6 pb-4">
<AlertDialogTitle className="title-2xl-semi-bold text-text-primary">{t('newApp.appCreateDSLErrorTitle', { ns: 'app' })}</AlertDialogTitle>
<AlertDialogDescription render={<div />} className="flex grow flex-col system-md-regular text-text-secondary">
<div>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
<br />
<div>
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
<span className="system-md-medium">{versions.importedVersion}</span>
</div>
<div>
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
<span className="system-md-medium">{versions.systemVersion}</span>
</div>
</AlertDialogDescription>
</div>
</div>
<div className="flex items-start justify-end gap-2 self-stretch pt-6">
<Button variant="secondary" onClick={() => onCancel()}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button variant="primary" tone="destructive" onClick={onConfirm} disabled={confirmDisabled}>{t('newApp.Confirm', { ns: 'app' })}</Button>
</div>
</Modal>
<AlertDialogActions>
<AlertDialogCancelButton variant="secondary">{t('newApp.Cancel', { ns: 'app' })}</AlertDialogCancelButton>
<AlertDialogConfirmButton onClick={onConfirm} disabled={confirmDisabled}>{t('newApp.Confirm', { ns: 'app' })}</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
)
}

View File

@ -1,10 +1,9 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { useKeyPress } from 'ahooks'
import { noop } from 'es-toolkit/function'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import DSLConfirmModal from './dsl-confirm-modal'
import Header from './header'
import { CreateFromDSLModalTab, useDSLImport } from './hooks/use-dsl-import'
@ -58,51 +57,50 @@ const CreateFromDSLModal = ({
return (
<>
<Modal
className="w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0 shadow-xl"
isShow={show}
onClose={noop}
>
<Header onClose={onClose} />
<Tab
currentTab={currentTab}
setCurrentTab={setCurrentTab}
/>
<div className="px-6 py-4">
{currentTab === CreateFromDSLModalTab.FROM_FILE && (
<Uploader
className="mt-0"
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)}
<Dialog open={show}>
<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">
<Header onClose={onClose} />
<Tab
currentTab={currentTab}
setCurrentTab={setCurrentTab}
/>
<div className="px-6 py-4">
{currentTab === CreateFromDSLModalTab.FROM_FILE && (
<Uploader
className="mt-0"
file={currentFile}
updateFile={handleFile}
/>
</div>
)}
</div>
<div className="flex justify-end gap-x-2 p-6 pt-5">
<Button onClick={onClose}>
{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>
</Modal>
)}
{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>
)}
</div>
<div className="flex justify-end gap-x-2 p-6 pt-5">
<Button onClick={onClose}>
{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 && (
<DSLConfirmModal
versions={versions}

View File

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

View File

@ -1,13 +1,13 @@
'use client'
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 * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { trackEvent } from '@/app/components/base/amplitude'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import { useRouter } from '@/next/navigation'
import { createEmptyDataset } from '@/service/datasets'
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
@ -46,21 +46,30 @@ const EmptyDatasetCreationModal = ({ show = false, onHide }: IProps) => {
}
}
return (
<Modal isShow={show} onClose={onHide} className={cn(s.modal, '!max-w-[520px]', 'px-8')}>
<div className={s.modalHeader}>
<div className={s.title}>{t('stepOne.modal.title', { ns: 'datasetCreation' })}</div>
<span className={s.close} onClick={onHide} />
</div>
<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>
<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>
</Modal>
<Dialog
open={show}
onOpenChange={(open) => {
if (!open)
onHide()
}}
>
<DialogContent className={cn('w-full overflow-hidden! border-none text-left align-middle', cn(s.modal, '!max-w-[520px]', 'px-8'))}>
<div className={s.modalHeader}>
<div className={s.title}>{t('stepOne.modal.title', { ns: 'datasetCreation' })}</div>
<span className={s.close} onClick={onHide} />
</div>
<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>
<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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,12 +2,12 @@
import type { FileAppearanceTypeEnum } from '@/app/components/base/file-uploader/types'
import type { HitTesting } from '@/models/datasets'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import * as React from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import FileIcon from '@/app/components/base/file-uploader/file-type-icon'
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 ImageList from '../../common/image-list'
import Dot from '../../documents/detail/completed/common/dot'
@ -52,93 +52,100 @@ const ChunkDetailModal = ({
const showKeywords = !isParentChildRetrieval && keywords && keywords.length > 0
return (
<Modal
title={t(`${i18nPrefix}chunkDetail`, { ns: 'datasetHitTesting' })}
isShow
closable
onClose={onHide}
className={cn(isParentChildRetrieval ? 'min-w-[1200px]!' : 'min-w-[800px]!')}
<Dialog
open
onOpenChange={(open) => {
if (!open)
onHide()
}}
>
<div className="mt-4 flex">
<div className={cn('flex-1', isParentChildRetrieval && 'pr-6')}>
{/* Meta info */}
<div className="flex items-center justify-between">
<div className="flex grow items-center space-x-2">
<SegmentIndexTag
labelPrefix={labelPrefix}
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>
<DialogContent className={cn('w-full overflow-hidden! border-none text-left align-middle', cn(isParentChildRetrieval ? 'min-w-[1200px]!' : 'min-w-[800px]!'))}>
<DialogCloseButton data-testid="modal-close-button" />
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{t(`${i18nPrefix}chunkDetail`, { ns: 'datasetHitTesting' })}
</DialogTitle>
<div className="mt-4 flex">
<div className={cn('flex-1', isParentChildRetrieval && 'pr-6')}>
{/* Meta info */}
<div className="flex items-center justify-between">
<div className="flex grow items-center space-x-2">
<SegmentIndexTag
labelPrefix={labelPrefix}
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>
<Score value={score} />
</div>
<Score value={score} />
</div>
{/* Content */}
<div className="relative">
{!answer && (
<Markdown
className={cn('mt-2! text-text-secondary!', heighClassName)}
content={sign_content || content}
customDisallowedElements={['input']}
/>
)}
{answer && (
<div className="break-all">
<div className="flex gap-x-1">
<div className="w-4 shrink-0 text-[13px] leading-[20px] font-medium text-text-tertiary">Q</div>
<div className={cn('line-clamp-20 body-md-regular text-text-secondary')}>
{content}
{/* Content */}
<div className="relative">
{!answer && (
<Markdown
className={cn('mt-2! text-text-secondary!', heighClassName)}
content={sign_content || content}
customDisallowedElements={['input']}
/>
)}
{answer && (
<div className="break-all">
<div className="flex gap-x-1">
<div className="w-4 shrink-0 text-[13px] leading-[20px] font-medium text-text-tertiary">Q</div>
<div className={cn('line-clamp-20 body-md-regular text-text-secondary')}>
{content}
</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>
<div className={cn('line-clamp-20 body-md-regular text-text-secondary')}>
{answer}
</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>
<div className={cn('line-clamp-20 body-md-regular text-text-secondary')}>
{answer}
)}
{/* Mask */}
<Mask className="absolute inset-x-0 bottom-0" />
</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>
)}
{/* Mask */}
<Mask className="absolute inset-x-0 bottom-0" />
</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>
)}
{isParentChildRetrieval && (
<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>
{isParentChildRetrieval && (
<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>
</DialogContent>
</Dialog>
)
}

View File

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

View File

@ -2,6 +2,7 @@
import type { FC } from 'react'
import type { BuiltInMetadataItem, MetadataItemInBatchEdit, MetadataItemWithEdit } from '../types'
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 { RiQuestionLine } from '@remixicon/react'
import { produce } from 'immer'
@ -11,7 +12,6 @@ import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import { useCreateMetaData } from '@/service/knowledge/use-metadata'
import Checkbox from '../../../base/checkbox'
import Modal from '../../../base/modal'
import Tooltip from '../../../base/tooltip'
import AddMetadataButton from '../add-metadata-button'
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)
}, [templeList, addedList, isApplyToAllSelectDocument, onSave])
return (
<Modal title={t(`${i18nPrefix}.editMetadata`, { ns: 'dataset' })} isShow closable onClose={onHide} className="!max-w-[640px]">
<div className="mt-1 system-xs-medium text-text-accent">{t(`${i18nPrefix}.editDocumentsNum`, { ns: 'dataset', num: documentNum })}</div>
<div className="max-h-[305px] overflow-x-hidden overflow-y-auto">
<div className="mt-4 space-y-2">
{templeList.map(item => (<EditMetadataBatchItem key={item.id} payload={item} onChange={handleTemplesChange} onRemove={handleTempleItemRemove} onReset={handleItemReset} />))}
</div>
<div className="mt-4 pl-[18px]">
<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 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>
<Dialog
open
onOpenChange={(open) => {
if (!open)
onHide()
}}
>
<DialogContent className="w-full !max-w-[640px] overflow-hidden! border-none text-left align-middle">
<DialogCloseButton data-testid="modal-close-button" />
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{t(`${i18nPrefix}.editMetadata`, { ns: 'dataset' })}
</DialogTitle>
<div className="mt-4 flex items-center justify-between">
<div className="flex items-center select-none">
<Checkbox checked={isApplyToAllSelectDocument} onCheck={() => setIsApplyToAllSelectDocument(!isApplyToAllSelectDocument)} id="apply-to-all" />
<div className="mr-1 ml-2 system-xs-medium text-text-secondary">{t(`${i18nPrefix}.applyToAllSelectDocument`, { ns: 'dataset' })}</div>
<Tooltip popupContent={<div className="max-w-[240px]">{t(`${i18nPrefix}.applyToAllSelectDocumentTip`, { ns: 'dataset' })}</div>}>
<div className="cursor-pointer p-px">
<RiQuestionLine className="size-3.5 text-text-tertiary" />
<div className="mt-1 system-xs-medium text-text-accent">{t(`${i18nPrefix}.editDocumentsNum`, { ns: 'dataset', num: documentNum })}</div>
<div className="max-h-[305px] overflow-x-hidden overflow-y-auto">
<div className="mt-4 space-y-2">
{templeList.map(item => (<EditMetadataBatchItem key={item.id} payload={item} onChange={handleTemplesChange} onRemove={handleTempleItemRemove} onReset={handleItemReset} />))}
</div>
<div className="mt-4 pl-[18px]">
<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>
</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 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 className="mt-4 flex items-center justify-between">
<div className="flex items-center select-none">
<Checkbox checked={isApplyToAllSelectDocument} onCheck={() => setIsApplyToAllSelectDocument(!isApplyToAllSelectDocument)} id="apply-to-all" />
<div className="mr-1 ml-2 system-xs-medium text-text-secondary">{t(`${i18nPrefix}.applyToAllSelectDocument`, { ns: 'dataset' })}</div>
<Tooltip popupContent={<div className="max-w-[240px]">{t(`${i18nPrefix}.applyToAllSelectDocumentTip`, { ns: 'dataset' })}</div>}>
<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>
</Modal>
</DialogContent>
</Dialog>
)
}
export default React.memo(EditMetadataBatchModal)

View File

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

View File

@ -4,13 +4,12 @@ import type { AppIconSelection } from '../../base/app-icon-picker'
import type { DataSet } from '@/models/datasets'
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 { RiCloseLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import Textarea from '@/app/components/base/textarea'
import { updateDatasetSetting } from '@/service/datasets'
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])
return (
<Modal className="w-[520px] max-w-[520px] rounded-xl px-8 py-6" isShow={show} onClose={noop}>
<div className="flex items-center justify-between pb-2">
<div className="text-xl leading-[30px] font-medium text-text-primary">{t('title', { ns: 'datasetSettings' })}</div>
<div className="cursor-pointer p-2" onClick={onClose}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div>
</div>
<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' }) || ''} />
<Dialog open={show}>
<DialogContent className="w-full max-w-[520px] overflow-hidden! rounded-xl border-none px-8 py-6 text-left align-middle">
<div className="flex items-center justify-between pb-2">
<div className="text-xl leading-[30px] font-medium text-text-primary">{t('title', { ns: 'datasetSettings' })}</div>
<div className="cursor-pointer p-2" onClick={onClose}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div>
</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.desc', { ns: 'datasetSettings' })}
<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 className="w-full">
<Textarea value={description} onChange={e => setDescription(e.target.value)} className="resize-none" placeholder={t('form.descPlaceholder', { ns: 'datasetSettings' }) || ''} />
<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.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 className="flex justify-end pt-6">
<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>
</div>
{showAppIconPicker && (<AppIconPicker onSelect={handleSelectAppIcon} onClose={handleCloseAppIconPicker} />)}
</Modal>
<div className="flex justify-end pt-6">
<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>
</div>
{showAppIconPicker && (<AppIconPicker onSelect={handleSelectAppIcon} onClose={handleCloseAppIconPicker} />)}
</DialogContent>
</Dialog>
)
}
export default RenameDatasetModal

View File

@ -2,8 +2,9 @@
import type { CreateApiKeyResponse } from '@/models/app'
import { XMarkIcon } from '@heroicons/react/20/solid'
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 Modal from '@/app/components/base/modal'
import InputCopy from './input-copy'
import s from './style.module.css'
@ -22,21 +23,33 @@ const SecretKeyGenerateModal = ({
}: ISecretKeyGenerateModalProps) => {
const { t } = useTranslation()
return (
<Modal isShow={isShow} onClose={onClose} title={`${t('apiKeyModal.apiSecretKey', { ns: 'appApi' })}`} className={`px-8 ${className}`}>
<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>
<Dialog
open={isShow}
onOpenChange={(open) => {
if (!open)
onClose()
}}
>
<DialogContent className={cn('w-full max-w-[480px] overflow-hidden! border-none px-8 text-left align-middle', className)}>
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{`${t('apiKeyModal.apiSecretKey', { ns: 'appApi' })}`}
</DialogTitle>
</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,
} from '@langgenius/dify-ui/alert-dialog'
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 {
useState,
@ -19,7 +21,6 @@ import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import CopyFeedback from '@/app/components/base/copy-feedback'
import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal'
import { useAppContext } from '@/context/app-context'
import useTimestamp from '@/hooks/use-timestamp'
import {
@ -104,77 +105,89 @@ const SecretKeyModal = ({
}
return (
<Modal isShow={isShow} onClose={onClose} title={`${t('apiKeyModal.apiSecretKey', { ns: 'appApi' })}`} className={`${s.customModal} flex flex-col px-8`}>
<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 shrink-0 text-[13px] leading-5 font-normal text-text-tertiary">{t('apiKeyModal.apiSecretKeyTips', { ns: 'appApi' })}</p>
{isApiKeysLoading && <div className="mt-4"><Loading /></div>}
{
!!apiKeysList?.data?.length && (
<div className="mt-4 flex grow flex-col overflow-hidden">
<div className="flex h-9 shrink-0 items-center border-b border-divider-regular text-xs font-semibold text-text-tertiary">
<div className="w-64 shrink-0 px-3">{t('apiKeyModal.secretKey', { ns: 'appApi' })}</div>
<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="grow px-3"></div>
</div>
<div className="grow overflow-auto">
{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>
<div className="w-[200px] shrink-0 truncate px-3">{formatTime(Number(api.created_at), t('dateTimeFormat', { ns: 'appLog' }) as string)}</div>
<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>
)}
<Dialog
open={isShow}
onOpenChange={(open) => {
if (!open)
onClose()
}}
>
<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`)}>
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{`${t('apiKeyModal.apiSecretKey', { ns: 'appApi' })}`}
</DialogTitle>
<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 shrink-0 text-[13px] leading-5 font-normal text-text-tertiary">{t('apiKeyModal.apiSecretKeyTips', { ns: 'appApi' })}</p>
{isApiKeysLoading && <div className="mt-4"><Loading /></div>}
{
!!apiKeysList?.data?.length && (
<div className="mt-4 flex grow flex-col overflow-hidden">
<div className="flex h-9 shrink-0 items-center border-b border-divider-regular text-xs font-semibold text-text-tertiary">
<div className="w-64 shrink-0 px-3">{t('apiKeyModal.secretKey', { ns: 'appApi' })}</div>
<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="grow px-3"></div>
</div>
<div className="grow overflow-auto">
{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>
<div className="w-[200px] shrink-0 truncate px-3">{formatTime(Number(api.created_at), t('dateTimeFormat', { ns: 'appLog' }) as string)}</div>
<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 className="flex">
<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" />
<div className="text-xs font-medium text-text-secondary">{t('apiKeyModal.createNewSecretKey', { ns: 'appApi' })}</div>
</Button>
</div>
<SecretKeyGenerateModal className="shrink-0" isShow={isVisible} onClose={() => setVisible(false)} newKey={newKey} />
<AlertDialog
open={showConfirmDelete}
onOpenChange={handleDeleteConfirmOpenChange}
>
<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('actionMsg.deleteConfirmTitle', { ns: 'appApi' })}
</AlertDialogTitle>
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
{t('actionMsg.deleteConfirmTips', { ns: 'appApi' })}
</AlertDialogDescription>
</div>
<AlertDialogActions>
<AlertDialogCancelButton>
{t('operation.cancel', { ns: 'common' })}
</AlertDialogCancelButton>
<AlertDialogConfirmButton onClick={onDel}>
{t('operation.confirm', { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
</Modal>
)
}
<div className="flex">
<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" />
<div className="text-xs font-medium text-text-secondary">{t('apiKeyModal.createNewSecretKey', { ns: 'appApi' })}</div>
</Button>
</div>
<SecretKeyGenerateModal className="shrink-0" isShow={isVisible} onClose={() => setVisible(false)} newKey={newKey} />
<AlertDialog
open={showConfirmDelete}
onOpenChange={handleDeleteConfirmOpenChange}
>
<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('actionMsg.deleteConfirmTitle', { ns: 'appApi' })}
</AlertDialogTitle>
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
{t('actionMsg.deleteConfirmTips', { ns: 'appApi' })}
</AlertDialogDescription>
</div>
<AlertDialogActions>
<AlertDialogCancelButton>
{t('operation.cancel', { ns: 'common' })}
</AlertDialogCancelButton>
<AlertDialogConfirmButton onClick={onDel}>
{t('operation.confirm', { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
</DialogContent>
</Dialog>
)
}

View File

@ -3,12 +3,12 @@
import type { FC } from 'react'
import type { App as AppType } from '@/models/explore'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { useSuspenseQuery } from '@tanstack/react-query'
import * as React from 'react'
import { useState } from 'react'
import AppUnavailable from '@/app/components/base/app-unavailable'
import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal/index'
import { IS_CLOUD_EDITION } from '@/config'
import { systemFeaturesQueryOptions } from '@/service/system-features'
import { useGetTryAppInfo } from '@/service/use-try-app'
@ -40,54 +40,59 @@ const TryApp: FC<Props> = ({
const { data: appDetail, isLoading, isError, error } = useGetTryAppInfo(appId)
return (
<Modal
isShow
onClose={onClose}
className="h-[calc(100vh-32px)] max-w-[calc(100vw-32px)] min-w-[1280px] overflow-x-auto p-2"
<Dialog
open
onOpenChange={(open) => {
if (!open)
onClose()
}}
>
{isLoading ? (
<div className="flex h-full items-center justify-center">
<Loading type="area" />
</div>
) : isError ? (
<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>
<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">
{isLoading ? (
<div className="flex h-full items-center justify-center">
<Loading type="area" />
</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}
/>
) : isError ? (
<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>
</div>
)}
</Modal>
) : !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>
{/* 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)

View File

@ -1,15 +1,15 @@
'use client'
import type { LangGeniusVersionResponse } from '@/models/common'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { RiCloseLine } from '@remixicon/react'
import { useSuspenseQuery } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { useTranslation } from 'react-i18next'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import Modal from '@/app/components/base/modal'
import { IS_CE_EDITION } from '@/config'
import Link from '@/next/link'
import Link from '@/next/link'
import { systemFeaturesQueryOptions } from '@/service/system-features'
type IAccountSettingProps = {
@ -26,87 +26,92 @@ export default function AccountAbout({
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
return (
<Modal
isShow
onClose={onCancel}
className="w-[480px]! max-w-[480px]! px-6! py-4!"
<Dialog
open
onOpenChange={(open) => {
if (!open)
onCancel()
}}
>
<div className="relative">
<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" />}
<DialogContent className="w-[calc(100vw-2rem)]! max-w-[480px]! overflow-hidden! border-none px-6! py-4! text-left align-middle">
<div className="text-center text-xs font-normal text-text-tertiary">
Version
{langGeniusVersionInfo?.current_version}
<div className="relative">
<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-2 text-center text-xs font-normal text-text-secondary">
<div>
©
{dayjs().year()}
{' '}
LangGenius, Inc., Contributors.
<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">
Version
{langGeniusVersionInfo?.current_version}
</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
? <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>
</>
)
isLatest
? t('about.latestAvailable', { ns: 'common', version: langGeniusVersionInfo.latest_version })
: t('about.nowAvailable', { ns: 'common', version: langGeniusVersionInfo.latest_version })
}
</div>
<div className="flex shrink-0 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>
<div className="-mx-8 mb-4 h-[0.5px] bg-divider-regular" />
<div className="flex items-center justify-between">
<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>
</DialogContent>
</Dialog>
)
}

View File

@ -1,12 +1,11 @@
import type { FC } from 'react'
import type { ApiBasedExtension } from '@/models/common'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { noop } from 'es-toolkit/function'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BookOpen01 } from '@/app/components/base/icons/src/vender/line/education'
import Modal from '@/app/components/base/modal'
import { useDocLink } from '@/context/i18n'
import { addApiBasedExtension, updateApiBasedExtension } from '@/service/common'
@ -61,45 +60,48 @@ const ApiBasedExtensionModal: FC<ApiBasedExtensionModalProps> = ({ data, onCance
}
}
return (
<Modal isShow onClose={noop} wrapperClassName="z-1002" className="w-[640px]! max-w-none! p-8! pb-6!">
<div className="mb-2 text-xl font-semibold text-text-primary">
{data.name
? t('apiBasedExtension.modal.editTitle', { ns: 'common' })
: t('apiBasedExtension.modal.title', { ns: 'common' })}
</div>
<div className="py-2">
<div className="text-sm leading-9 font-medium text-text-primary">
{t('apiBasedExtension.modal.name.title', { ns: 'common' })}
<Dialog open>
<DialogContent className="w-[640px]! max-w-none! overflow-hidden! border-none p-8! pb-6! text-left align-middle">
<div className="mb-2 text-xl font-semibold text-text-primary">
{data.name
? t('apiBasedExtension.modal.editTitle', { ns: 'common' })
: t('apiBasedExtension.modal.title', { ns: 'common' })}
</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>
<div className="py-2">
<div className="flex h-9 items-center justify-between text-sm font-medium text-text-primary">
{t('apiBasedExtension.modal.apiEndpoint.title', { 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 className="py-2">
<div className="text-sm leading-9 font-medium text-text-primary">
{t('apiBasedExtension.modal.name.title', { ns: 'common' })}
</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>
<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 className="py-2">
<div className="text-sm leading-9 font-medium text-text-primary">
{t('apiBasedExtension.modal.apiKey.title', { ns: 'common' })}
<div className="py-2">
<div className="flex h-9 items-center justify-between text-sm font-medium text-text-primary">
{t('apiBasedExtension.modal.apiEndpoint.title', { 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>
<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 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 className="py-2">
<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 className="mt-6 flex items-center justify-end">
<Button onClick={onCancel} className="mr-2">
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button variant="primary" disabled={!localeData.name || !localeData.api_endpoint || !localeData.api_key || loading} onClick={handleSave}>
{t('operation.save', { ns: 'common' })}
</Button>
</div>
</Modal>
<div className="mt-6 flex items-center justify-end">
<Button onClick={onCancel} className="mr-2">
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button variant="primary" disabled={!localeData.name || !localeData.api_endpoint || !localeData.api_key || loading} onClick={handleSave}>
{t('operation.save', { ns: 'common' })}
</Button>
</div>
</DialogContent>
</Dialog>
)
}
export default ApiBasedExtensionModal

View File

@ -9,11 +9,11 @@ import {
} from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
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 { useGetModelCredential, useUpdateModelLoadBalancingConfig } from '@/service/use-models'
import { ConfigurationMethodEnum, FormTypeEnum } from '../declarations'
@ -180,99 +180,103 @@ const ModelLoadBalancingModal = ({ provider, configurateMethod, currentCustomCon
}, [refetch, onClose])
return (
<>
<Modal
isShow={Boolean(model) && open}
onClose={onClose}
wrapperClassName="z-1002"
className="w-[640px] max-w-none px-8 pt-8"
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>
)}
<Dialog
open={Boolean(model) && open}
onOpenChange={(open) => {
if (!open)
onClose?.()
}}
>
{!draftConfig
? <Loading type="area" />
: (
<>
<div className="py-2">
<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,
},
}}
/>
)}
<DialogContent className="w-[640px] max-w-none overflow-hidden! border-none px-8 pt-8 text-left align-middle">
<DialogTitle className="title-2xl-semi-bold text-text-primary">
<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>
</DialogTitle>
<div className="mt-6 flex items-center justify-between gap-2">
<div>
{!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' })}
</Button>
{!draftConfig
? <Loading type="area" />
: (
<>
<div className="py-2">
<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 className="space-x-2">
<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 className="mt-6 flex items-center justify-between gap-2">
<div>
{!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' })}
</Button>
)}
</div>
<div className="space-x-2">
<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>
</>
)}
</Modal>
</>
)}
</DialogContent>
</Dialog>
<AlertDialog open={!!deleteModel} onOpenChange={open => !open && closeConfirmDelete()}>
<AlertDialogContent>
<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 { Dependency } from '../../types'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import { InstallStep } from '../../types'
import useHideLogic from '../hooks/use-hide-logic'
import ReadyToInstall from './ready-to-install'
@ -50,26 +50,31 @@ const InstallBundle: FC<Props> = ({
}, [step, t])
return (
<Modal
isShow={true}
onClose={foldAnimInto}
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')}
closable
<Dialog
open
onOpenChange={(open) => {
if (!open)
foldAnimInto()
}}
>
<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()}
<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'))}>
<DialogCloseButton data-testid="modal-close-button" />
<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>
<ReadyToInstall
step={step}
onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
allPlugins={fromDSLPayload}
onClose={onClose}
/>
</Modal>
<ReadyToInstall
step={step}
onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
allPlugins={fromDSLPayload}
onClose={onClose}
/>
</DialogContent>
</Dialog>
)
}

View File

@ -3,11 +3,11 @@
import type { PluginDeclaration, UpdateFromGitHubPayload } from '../../types'
import type { InstallState } from '@/app/components/plugins/types'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react'
import { useCallback, useState } from 'react'
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 { InstallStepFromGitHub } from '../../types'
import Installed from '../base/installed'
@ -160,74 +160,80 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
}
return (
<Modal
isShow={true}
onClose={foldAnimInto}
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`)}
closable
<Dialog
open
onOpenChange={(open) => {
if (!open)
foldAnimInto()
}}
>
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<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' })}
<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`))}
>
<DialogCloseButton data-testid="modal-close-button" />
<div className="flex items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<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>
{([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step))
? (
<Installed
payload={manifest}
isFailed={[InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installFailed].includes(state.step)}
errMsg={errorMsg}
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'}`}>
{state.step === InstallStepFromGitHub.setUrl && (
<SetURL
repoUrl={state.repoUrl}
onChange={value => setState(prevState => ({ ...prevState, repoUrl: value }))}
onNext={handleUrlSubmit}
onCancel={onClose}
/>
)}
{state.step === InstallStepFromGitHub.selectPackage && (
<SelectPackage
updatePayload={updatePayload!}
repoUrl={state.repoUrl}
selectedVersion={state.selectedVersion}
versions={versions}
onSelectVersion={item => setState(prevState => ({ ...prevState, selectedVersion: String(item.value) }))}
selectedPackage={state.selectedPackage}
packages={packages}
onSelectPackage={item => setState(prevState => ({ ...prevState, selectedPackage: String(item.value) }))}
onUploaded={handleUploaded}
onFailed={handleUploadFail}
onBack={handleBack}
/>
)}
{state.step === InstallStepFromGitHub.readyToInstall && (
<Loaded
updatePayload={updatePayload!}
uniqueIdentifier={uniqueIdentifier!}
payload={manifest as any}
repoUrl={state.repoUrl}
selectedVersion={state.selectedVersion}
selectedPackage={state.selectedPackage}
onBack={handleBack}
onStartToInstall={handleStartToInstall}
onInstalled={handleInstalled}
onFailed={handleFailed}
/>
)}
</div>
)}
</Modal>
{([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step))
? (
<Installed
payload={manifest}
isFailed={[InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installFailed].includes(state.step)}
errMsg={errorMsg}
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'}`}>
{state.step === InstallStepFromGitHub.setUrl && (
<SetURL
repoUrl={state.repoUrl}
onChange={value => setState(prevState => ({ ...prevState, repoUrl: value }))}
onNext={handleUrlSubmit}
onCancel={onClose}
/>
)}
{state.step === InstallStepFromGitHub.selectPackage && (
<SelectPackage
updatePayload={updatePayload!}
repoUrl={state.repoUrl}
selectedVersion={state.selectedVersion}
versions={versions}
onSelectVersion={item => setState(prevState => ({ ...prevState, selectedVersion: String(item.value) }))}
selectedPackage={state.selectedPackage}
packages={packages}
onSelectPackage={item => setState(prevState => ({ ...prevState, selectedPackage: String(item.value) }))}
onUploaded={handleUploaded}
onFailed={handleUploadFail}
onBack={handleBack}
/>
)}
{state.step === InstallStepFromGitHub.readyToInstall && (
<Loaded
updatePayload={updatePayload!}
uniqueIdentifier={uniqueIdentifier!}
payload={manifest as any}
repoUrl={state.repoUrl}
selectedVersion={state.selectedVersion}
selectedPackage={state.selectedPackage}
onBack={handleBack}
onStartToInstall={handleStartToInstall}
onInstalled={handleInstalled}
onFailed={handleFailed}
/>
)}
</div>
)}
</DialogContent>
</Dialog>
)
}

View File

@ -2,10 +2,10 @@
import type { Dependency, PluginDeclaration } from '../../types'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import * as React from 'react'
import { useCallback, useState } from 'react'
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 { InstallStep } from '../../types'
import useHideLogic from '../hooks/use-hide-logic'
@ -86,52 +86,57 @@ const InstallFromLocalPackage: React.FC<InstallFromLocalPackageProps> = ({
}, [])
return (
<Modal
isShow={true}
onClose={foldAnimInto}
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')}
closable
<Dialog
open
onOpenChange={(open) => {
if (!open)
foldAnimInto()
}}
>
<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()}
<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'))}>
<DialogCloseButton data-testid="modal-close-button" />
<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>
{step === InstallStep.uploading && (
<Uploading
isBundle={isBundle}
file={file}
onCancel={onClose}
onPackageUploaded={handlePackageUploaded}
onBundleUploaded={handleBundleUploaded}
onFailed={handleUploadFail}
/>
)}
{isBundle
? (
<ReadyToInstallBundle
step={step}
onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
onClose={onClose}
allPlugins={dependencies}
/>
)
: (
<ReadyToInstallPackage
step={step}
onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
onClose={onClose}
uniqueIdentifier={uniqueIdentifier}
manifest={manifest}
errorMsg={errorMsg}
onError={setErrorMsg}
/>
)}
</Modal>
{step === InstallStep.uploading && (
<Uploading
isBundle={isBundle}
file={file}
onCancel={onClose}
onPackageUploaded={handlePackageUploaded}
onBundleUploaded={handleBundleUploaded}
onFailed={handleUploadFail}
/>
)}
{isBundle
? (
<ReadyToInstallBundle
step={step}
onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
onClose={onClose}
allPlugins={dependencies}
/>
)
: (
<ReadyToInstallPackage
step={step}
onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
onClose={onClose}
uniqueIdentifier={uniqueIdentifier}
manifest={manifest}
errorMsg={errorMsg}
onError={setErrorMsg}
/>
)}
</DialogContent>
</Dialog>
)
}

View File

@ -2,10 +2,10 @@
import type { Dependency, Plugin, PluginManifestInMarket } from '../../types'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import { InstallStep } from '../../types'
import Installed from '../base/installed'
import useHideLogic from '../hooks/use-hide-logic'
@ -70,60 +70,64 @@ const InstallFromMarketplace: React.FC<InstallFromMarketplaceProps> = ({
}, [setIsInstalling])
return (
<Modal
isShow={true}
onClose={foldAnimInto}
wrapperClassName="z-9999"
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')}
closable
<Dialog
open
onOpenChange={(open) => {
if (!open)
foldAnimInto()
}}
>
<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()}
<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'))}>
<DialogCloseButton data-testid="modal-close-button" />
<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>
{
isBundle
? (
<ReadyToInstallBundle
step={step}
onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
onClose={onClose}
allPlugins={dependencies!}
isFromMarketPlace
/>
)
: (
<>
{
step === InstallStep.readyToInstall && (
<Install
uniqueIdentifier={uniqueIdentifier}
payload={manifest!}
onCancel={onClose}
onInstalled={handleInstalled}
onFailed={handleFailed}
onStartToInstall={handleStartToInstall}
/>
)
}
{
[InstallStep.installed, InstallStep.installFailed].includes(step) && (
<Installed
payload={manifest!}
isMarketPayload
isFailed={step === InstallStep.installFailed}
errMsg={errorMsg}
onCancel={onSuccess}
/>
)
}
</>
)
}
</Modal>
{
isBundle
? (
<ReadyToInstallBundle
step={step}
onStepChange={setStep}
onStartToInstall={handleStartToInstall}
setIsInstalling={setIsInstalling}
onClose={onClose}
allPlugins={dependencies!}
isFromMarketPlace
/>
)
: (
<>
{
step === InstallStep.readyToInstall && (
<Install
uniqueIdentifier={uniqueIdentifier}
payload={manifest!}
onCancel={onClose}
onInstalled={handleInstalled}
onFailed={handleFailed}
onStartToInstall={handleStartToInstall}
/>
)
}
{
[InstallStep.installed, InstallStep.installFailed].includes(step) && (
<Installed
payload={manifest!}
isMarketPayload
isFailed={step === InstallStep.installFailed}
errMsg={errorMsg}
onCancel={onSuccess}
/>
)
}
</>
)
}
</DialogContent>
</Dialog>
)
}

View File

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

View File

@ -2,17 +2,19 @@ import { render, screen } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('../../../base/modal', () => ({
default: ({ children, title, isShow }: { children: React.ReactNode, title: string, isShow: boolean }) => (
isShow
vi.mock('@langgenius/dify-ui/dialog', () => ({
Dialog: ({ children, open }: { children: React.ReactNode, open?: boolean }) => (
open !== false
? (
<div data-testid="modal">
<div data-testid="modal-title">{title}</div>
{children}
</div>
)
: 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', () => ({

View File

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

View File

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

View File

@ -3,11 +3,11 @@ import type { FC } from 'react'
import type { AutoUpdateConfig } from './auto-update-setting/types'
import type { Permissions, ReferenceSetting } from '@/app/components/plugins/types'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogCloseButton, DialogContent } from '@langgenius/dify-ui/dialog'
import { useSuspenseQuery } from '@tanstack/react-query'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import { PermissionType } from '@/app/components/plugins/types'
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import { systemFeaturesQueryOptions } from '@/service/system-features'
@ -53,59 +53,64 @@ const PluginSettingModal: FC<Props> = ({
}, [onHide, onSave, tempAutoUpdateConfig, tempPrivilege])
return (
<Modal
isShow
onClose={onHide}
closable
className="w-[620px] max-w-[620px] p-0!"
<Dialog
open
onOpenChange={(open) => {
if (!open)
onHide()
}}
>
<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 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>
</div>
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
{[
{ title: t(`${i18nPrefix}.whoCanInstall`, { ns: 'plugin' }), key: 'install_permission', value: tempPrivilege?.install_permission || PermissionType.noOne },
{ title: t(`${i18nPrefix}.whoCanDebug`, { ns: 'plugin' }), key: 'debug_permission', value: tempPrivilege?.debug_permission || PermissionType.noOne },
].map(({ title, key, value }) => (
<div key={key} className="flex flex-col items-start gap-1 self-stretch">
<Label label={title} />
<div className="flex w-full items-start justify-between gap-2">
{[PermissionType.everyone, PermissionType.admin, PermissionType.noOne].map(option => (
<OptionCard
key={option}
title={t(`${i18nPrefix}.${option}`, { ns: 'plugin' })}
onSelect={() => handlePrivilegeChange(key)(option)}
selected={value === option}
className="flex-1"
/>
))}
<DialogContent className="w-[620px] max-w-[620px] overflow-hidden! border-none p-0! text-left align-middle">
<DialogCloseButton data-testid="modal-close-button" />
<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 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>
</div>
<div className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3">
{[
{ title: t(`${i18nPrefix}.whoCanInstall`, { ns: 'plugin' }), key: 'install_permission', value: tempPrivilege?.install_permission || PermissionType.noOne },
{ title: t(`${i18nPrefix}.whoCanDebug`, { ns: 'plugin' }), key: 'debug_permission', value: tempPrivilege?.debug_permission || PermissionType.noOne },
].map(({ title, key, value }) => (
<div key={key} className="flex flex-col items-start gap-1 self-stretch">
<Label label={title} />
<div className="flex w-full items-start justify-between gap-2">
{[PermissionType.everyone, PermissionType.admin, PermissionType.noOne].map(option => (
<OptionCard
key={option}
title={t(`${i18nPrefix}.${option}`, { ns: 'plugin' })}
onSelect={() => handlePrivilegeChange(key)(option)}
selected={value === option}
className="flex-1"
/>
))}
</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>
{
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>
</Modal>
</DialogContent>
</Dialog>
)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import {
RiAlertFill,
RiCloseLine,
@ -9,7 +10,6 @@ import {
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
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 VersionMismatchModal from './version-mismatch-modal'
@ -39,66 +39,71 @@ const UpdateDSLModal = ({
return (
<>
<Modal
className="w-[520px] rounded-2xl p-6"
isShow={show}
onClose={onCancel}
<Dialog
open={show}
onOpenChange={(open) => {
if (!open)
onCancel()
}}
>
<div className="mb-3 flex items-center justify-between">
<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}>
<RiCloseLine className="h-[18px] w-[18px] text-text-tertiary" />
</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 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>
<DialogContent className="w-full max-w-[480px]! overflow-hidden! rounded-2xl border-none p-6 text-left align-middle">
<div className="mb-3 flex items-center justify-between">
<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}>
<RiCloseLine className="h-[18px] w-[18px] text-text-tertiary" />
</div>
</div>
</div>
<div>
<div className="pt-2 system-md-semibold text-text-primary">
{t('common.chooseDSL', { ns: 'workflow' })}
<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 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"
accept=".pipeline"
displayName="PIPELINE"
/>
<div>
<div className="pt-2 system-md-semibold text-text-primary">
{t('common.chooseDSL', { ns: 'workflow' })}
</div>
<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"
accept=".pipeline"
displayName="PIPELINE"
/>
</div>
</div>
</div>
<div className="flex items-center justify-end gap-2 self-stretch pt-5">
<Button onClick={onCancel}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button
disabled={!currentFile || loading}
variant="primary"
tone="destructive"
onClick={handleImport}
loading={loading}
>
{t('common.overwriteAndImport', { ns: 'workflow' })}
</Button>
</div>
</Modal>
<div className="flex items-center justify-end gap-2 self-stretch pt-5">
<Button onClick={onCancel}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button
disabled={!currentFile || loading}
variant="primary"
tone="destructive"
onClick={handleImport}
loading={loading}
>
{t('common.overwriteAndImport', { ns: 'workflow' })}
</Button>
</div>
</DialogContent>
</Dialog>
<VersionMismatchModal
isShow={showErrorModal}
versions={versions}

View File

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

View File

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

View File

@ -3,12 +3,11 @@ import type {
MCPServerDetail,
} from '@/app/components/tools/types'
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 * as React from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import Modal from '@/app/components/base/modal'
import Textarea from '@/app/components/base/textarea'
import MCPServerParamItem from '@/app/components/tools/mcp/mcp-server-param-item'
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
@ -20,11 +19,19 @@ import {
type ModalProps = {
appID: string
latestParams?: any[]
latestParams?: MCPServerParam[]
data?: MCPServerDetail
show: boolean
onHide: () => void
appInfo?: any
appInfo?: {
description?: string
}
}
type MCPServerParam = {
variable?: string
label?: string
type?: string
}
const MCPServerModal = ({
@ -42,7 +49,7 @@ const MCPServerModal = ({
const defaultDescription = data?.description || appInfo?.description || ''
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) => {
setParams(prev => ({
@ -52,10 +59,14 @@ const MCPServerModal = ({
}
const getParamValue = () => {
const res = {} as any
latestParams.map((param) => {
res[param.variable] = params[param.variable]
return param
const res: Record<string, string> = {}
latestParams.forEach((param) => {
if (!param.variable)
return
const value = params[param.variable]
if (value !== undefined)
res[param.variable] = value
})
return res
}
@ -78,7 +89,11 @@ const MCPServerModal = ({
const submit = async () => {
if (!data) {
const payload: any = {
const payload: {
appID: string
description?: string
parameters: Record<string, string>
} = {
appID,
parameters: getParamValue(),
}
@ -92,13 +107,18 @@ const MCPServerModal = ({
onHide()
}
else {
const payload: any = {
const payload: {
appID: string
id: string
description: string
parameters: Record<string, string>
} = {
appID,
id: data.id,
parameters: getParamValue(),
description,
}
payload.description = description
await updateMCPServer(payload)
invalidateMCPServerDetail(appID)
emitMcpServerUpdate('updated')
@ -107,56 +127,67 @@ const MCPServerModal = ({
}
return (
<Modal
isShow={show}
onClose={onHide}
className={cn('relative max-w-[520px]! p-0!')}
<Dialog
open={show}
onOpenChange={(open) => {
if (!open)
onHide()
}}
>
<div className="absolute top-5 right-5 z-10 cursor-pointer p-1.5" onClick={onHide}>
<RiCloseLine className="h-5 w-5 text-text-tertiary" />
</div>
<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>
<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">
<div className="absolute top-5 right-5 z-10 cursor-pointer p-1.5" onClick={onHide}>
<RiCloseLine className="h-5 w-5 text-text-tertiary" />
</div>
{latestParams.length > 0 && (
<div>
<div className="mb-1 flex items-center gap-2">
<div className="shrink-0 system-xs-medium-uppercase text-text-primary">{t('mcp.server.modal.parameters', { ns: 'tools' })}</div>
<Divider type="horizontal" className="m-0! h-px! grow bg-divider-subtle" />
</div>
<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 => (
<MCPServerParamItem
key={paramItem.variable}
data={paramItem}
value={params[paramItem.variable] || ''}
onChange={value => handleParamChange(paramItem.variable, value)}
/>
))}
<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>
<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>
</Modal>
{latestParams.length > 0 && (
<div>
<div className="mb-1 flex items-center gap-2">
<div className="shrink-0 system-xs-medium-uppercase text-text-primary">{t('mcp.server.modal.parameters', { ns: 'tools' })}</div>
<Divider type="horizontal" className="m-0! h-px! grow bg-divider-subtle" />
</div>
<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 { AppIconType } from '@/types/app'
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 { RiCloseLine, RiEditLine } from '@remixicon/react'
import { useHover } from 'ahooks'
import { noop } from 'es-toolkit/function'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import AppIconPicker from '@/app/components/base/app-icon-picker'
import { Mcp } from '@/app/components/base/icons/src/vender/other'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import TabSlider from '@/app/components/base/tab-slider'
import { MCPAuthMethod } from '@/app/components/tools/types'
import { shouldUseMcpIconForAppIcon } from '@/utils/mcp'
@ -281,18 +279,16 @@ const MCPModal: FC<DuplicateAppModalProps> = ({
const formKey = data?.id ?? 'create'
return (
<Modal
isShow={show}
onClose={noop}
className={cn('relative max-w-[520px]!', 'p-6')}
>
<MCPModalContent
key={formKey}
data={data}
onConfirm={onConfirm}
onHide={onHide}
/>
</Modal>
<Dialog open={show}>
<DialogContent className="w-full max-w-[520px]! overflow-hidden! border-none p-6 text-left align-middle">
<MCPModalContent
key={formKey}
data={data}
onConfirm={onConfirm}
onHide={onHide}
/>
</DialogContent>
</Dialog>
)
}

View File

@ -4,12 +4,12 @@ import type { Authorization as AuthorizationPayloadType } from '../../types'
import type { Var } from '@/app/components/workflow/types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { produce } from 'immer'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
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 useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import { VarType } from '@/app/components/workflow/types'
@ -115,70 +115,78 @@ const Authorization: FC<Props> = ({
onHide()
}, [tempPayload, onChange, onHide])
return (
<Modal
title={t(`${i18nPrefix}.authorization`, { ns: 'workflow' })}
isShow={isShow}
onClose={onHide}
<Dialog
open={isShow}
onOpenChange={(open) => {
if (!open)
onHide()
}}
>
<div>
<div className="space-y-2">
<Field title={t(`${i18nPrefix}.authorizationType`, { ns: 'workflow' })}>
<RadioGroup
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>
<DialogContent className="overflow-hidden! border-none text-left align-middle">
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{t(`${i18nPrefix}.authorization`, { ns: 'workflow' })}
</DialogTitle>
{tempPayload.type === AuthorizationType.apiKey && (
<>
<Field title={t(`${i18nPrefix}.auth-type`, { ns: 'workflow' })}>
<RadioGroup
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>
{tempPayload.config?.type === APIType.custom && (
<Field title={t(`${i18nPrefix}.header`, { ns: 'workflow' })} isRequired>
<BaseInput
value={tempPayload.config?.header || ''}
onChange={handleAPIKeyOrHeaderChange('header')}
<div>
<div className="space-y-2">
<Field title={t(`${i18nPrefix}.authorizationType`, { ns: 'workflow' })}>
<RadioGroup
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 && (
<>
<Field title={t(`${i18nPrefix}.auth-type`, { ns: 'workflow' })}>
<RadioGroup
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>
)}
{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>
<div className="flex">
<Input
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]')}
value={tempPayload.config?.api_key || ''}
onChange={handleAPIKeyChange}
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
onFocusChange={setIsFocus}
placeholder={' '}
placeholderClassName="leading-[21px]!"
/>
</div>
</Field>
</>
)}
<Field title={t(`${i18nPrefix}.api-key-title`, { ns: 'workflow' })} isRequired>
<div className="flex">
<Input
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]')}
value={tempPayload.config?.api_key || ''}
onChange={handleAPIKeyChange}
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
onFocusChange={setIsFocus}
placeholder={' '}
placeholderClassName="leading-[21px]!"
/>
</div>
</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 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>
</Modal>
</DialogContent>
</Dialog>
)
}
export default React.memo(Authorization)

View File

@ -2,11 +2,11 @@
import type { FC } from 'react'
import type { HttpNodeType } from '../types'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import Textarea from '@/app/components/base/textarea'
import { useNodesInteractions } from '@/app/components/workflow/hooks'
import { parseCurl } from './curl-parser'
@ -42,28 +42,35 @@ const CurlPanel: FC<Props> = ({ nodeId, isShow, onHide, handleCurlImport }) => {
}, [onHide, nodeId, inputString, handleNodeSelect, handleCurlImport])
return (
<Modal
title={t('nodes.http.curl.title', { ns: 'workflow' })}
isShow={isShow}
onClose={onHide}
className="w-[400px]! max-w-[400px]! p-4!"
<Dialog
open={isShow}
onOpenChange={(open) => {
if (!open)
onHide()
}}
>
<div>
<Textarea
value={inputString}
className="my-3 h-40 w-full grow"
onChange={e => setInputString(e.target.value)}
placeholder={t('nodes.http.curl.placeholder', { ns: 'workflow' })!}
/>
</div>
<div className="mt-4 flex justify-end space-x-2">
<Button className="w-[95px]!" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button className="w-[95px]!" variant="primary" onClick={handleSave}>
{' '}
{t('operation.save', { ns: 'common' })}
</Button>
</div>
</Modal>
<DialogContent className="w-[400px]! max-w-[400px]! overflow-hidden! border-none p-4! text-left align-middle">
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{t('nodes.http.curl.title', { ns: 'workflow' })}
</DialogTitle>
<div>
<Textarea
value={inputString}
className="my-3 h-40 w-full grow"
onChange={e => setInputString(e.target.value)}
placeholder={t('nodes.http.curl.placeholder', { ns: 'workflow' })!}
/>
</div>
<div className="mt-4 flex justify-end space-x-2">
<Button className="w-[95px]!" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
<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 { SchemaRoot } from '../../types'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import * as React from 'react'
import Modal from '../../../../../base/modal'
import JsonSchemaConfig from './json-schema-config'
type JsonSchemaConfigModalProps = {
@ -18,17 +18,22 @@ const JsonSchemaConfigModal: FC<JsonSchemaConfigModalProps> = ({
onClose,
}) => {
return (
<Modal
isShow={isShow}
onClose={onClose}
className="h-[800px] max-w-[960px] p-0"
<Dialog
open={isShow}
onOpenChange={(open) => {
if (!open)
onClose()
}}
>
<JsonSchemaConfig
defaultSchema={defaultSchema}
onSave={onSave}
onClose={onClose}
/>
</Modal>
<DialogContent className="h-[800px] max-h-none w-full max-w-[960px] overflow-hidden! border-none p-0 text-left align-middle">
<JsonSchemaConfig
defaultSchema={defaultSchema}
onSave={onSave}
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,
default: ({
Dialog: ({
children,
isShow,
title,
open,
}: {
children: ReactNode
isShow?: boolean
title?: ReactNode
}) => isShow
open?: boolean
}) => open !== false
? (
<div data-testid="base-modal">
<div>{title}</div>
{children}
</div>
)
: null,
DialogContent: ({ children }: { children: ReactNode }) => <>{children}</>,
DialogTitle: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}))
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 { MoreInfo } from '@/app/components/workflow/types'
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 { Switch } from '@langgenius/dify-ui/switch'
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 ConfigSelect from '@/app/components/app/configuration/config-var/config-select'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import Textarea from '@/app/components/base/textarea'
import { ChangeType } from '@/app/components/workflow/types'
import { checkKeys } from '@/utils/var'
@ -124,64 +124,71 @@ const AddExtractParameter: FC<Props> = ({
</div>
)}
{isShowModal && (
<Modal
title={t(`${i18nPrefix}.addExtractParameter`, { ns: 'workflow' })}
isShow
onClose={hideModal}
className="w-[400px]! max-w-[400px]! p-4!"
<Dialog
open
onOpenChange={(open) => {
if (!open)
hideModal()
}}
>
<div>
<div className="space-y-2">
<Field title={t(`${i18nPrefix}.addExtractParameterContent.name`, { ns: 'workflow' })}>
<Input
value={param.name}
onChange={e => handleParamChange('name')(e.target.value)}
placeholder={t(`${i18nPrefix}.addExtractParameterContent.namePlaceholder`, { ns: 'workflow' })!}
/>
</Field>
<Field title={t(`${i18nPrefix}.addExtractParameterContent.type`, { ns: 'workflow' })}>
<Select
value={param.type}
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')} />
<DialogContent className="w-[400px]! max-w-[400px]! overflow-hidden! border-none p-4! text-left align-middle">
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{t(`${i18nPrefix}.addExtractParameter`, { ns: 'workflow' })}
</DialogTitle>
<div>
<div className="space-y-2">
<Field title={t(`${i18nPrefix}.addExtractParameterContent.name`, { ns: 'workflow' })}>
<Input
value={param.name}
onChange={e => handleParamChange('name')(e.target.value)}
placeholder={t(`${i18nPrefix}.addExtractParameterContent.namePlaceholder`, { ns: 'workflow' })!}
/>
</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>
<Field title={t(`${i18nPrefix}.addExtractParameterContent.type`, { ns: 'workflow' })}>
<Select
value={param.type}
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 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 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>
</Modal>
</DialogContent>
</Dialog>
)}
</div>
)

View File

@ -3,10 +3,10 @@ import type {
ConversationVariable,
} from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { RiCloseLine } from '@remixicon/react'
import { useMount } from 'ahooks'
import copy from 'copy-to-clipboard'
import { noop } from 'es-toolkit/function'
import { capitalize } from 'es-toolkit/string'
import * as React from 'react'
import { useCallback } from 'react'
@ -16,7 +16,6 @@ import {
CopyCheck,
} from '@/app/components/base/icons/src/vender/line/files'
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 { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type'
@ -76,87 +75,86 @@ const ConversationVariableModal = ({
})
return (
<Modal
isShow
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>
<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>
<Dialog open>
<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'))}>
<div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onHide}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div>
{/* RIGHT */}
<div className="flex h-full w-0 grow flex-col bg-components-panel-bg">
<div className="shrink-0 p-4 pb-2">
<div className="flex items-center gap-1 py-1">
<div className="system-xl-semibold text-text-primary">{currentVar.name}</div>
<div className="system-xs-medium text-text-tertiary">{capitalize(currentVar.value_type)}</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 className="flex h-0 grow flex-col p-4 pt-2">
<div className="mb-2 flex shrink-0 items-center gap-2">
<div className="shrink-0 system-xs-medium-uppercase text-text-tertiary">{t('chatVariable.storedContent', { ns: 'workflow' }).toLocaleUpperCase()}</div>
<div
className="h-px grow"
style={{
background: 'linear-gradient(to right, rgba(16, 24, 40, 0.08) 0%, rgba(255, 255, 255) 100%)',
}}
>
{/* RIGHT */}
<div className="flex h-full w-0 grow flex-col bg-components-panel-bg">
<div className="shrink-0 p-4 pb-2">
<div className="flex items-center gap-1 py-1">
<div className="system-xl-semibold text-text-primary">{currentVar.name}</div>
<div className="system-xs-medium text-text-tertiary">{capitalize(currentVar.value_type)}</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 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 className="flex h-0 grow flex-col p-4 pt-2">
<div className="mb-2 flex shrink-0 items-center gap-2">
<div className="shrink-0 system-xs-medium-uppercase text-text-tertiary">{t('chatVariable.storedContent', { ns: 'workflow' }).toLocaleUpperCase()}</div>
<div
className="h-px grow"
style={{
background: 'linear-gradient(to right, rgba(16, 24, 40, 0.08) 0%, rgba(255, 255, 255) 100%)',
}}
>
</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 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 className="grow pl-4">
<CodeEditor
readOnly
noWrapper
isExpand
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>
)}
)}
{(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>
</Modal>
</DialogContent>
</Dialog>
)
}

View File

@ -33,7 +33,8 @@ const ActionMenu: FC<ActionMenuProps> = (props: ActionMenuProps) => {
onOpenChange={setOpen}
>
<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" />
</DropdownMenuTrigger>

View File

@ -1,9 +1,16 @@
import type { FC } from 'react'
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 { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
type DeleteConfirmModalProps = {
isOpen: boolean
@ -21,24 +28,36 @@ const DeleteConfirmModal: FC<DeleteConfirmModalProps> = ({
const { t } = useTranslation()
return (
<Modal className="p-0" isShow={isOpen} onClose={onClose}>
<div className="flex flex-col gap-y-2 p-6 pb-4">
<div className="title-2xl-semi-bold text-text-primary">
{`${t('operation.delete', { ns: 'common' })} ${versionInfo.marked_name || t('versionHistory.defaultName', { ns: 'workflow' })}`}
<AlertDialog
open={isOpen}
onOpenChange={(open) => {
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>
<p className="system-md-regular text-text-secondary">
{t('versionHistory.deletionTip', { ns: 'workflow' })}
</p>
</div>
<div className="flex items-center justify-end gap-x-2 p-6">
<Button onClick={onClose}>
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button variant="primary" tone="destructive" onClick={onDelete.bind(null, versionInfo.id)}>
{t('operation.delete', { ns: 'common' })}
</Button>
</div>
</Modal>
<AlertDialogActions>
<AlertDialogCancelButton
nativeButton={false}
variant="secondary"
closeProps={{ nativeButton: false }}
>
{t('operation.cancel', { ns: 'common' })}
</AlertDialogCancelButton>
<AlertDialogConfirmButton nativeButton={false} onClick={onDelete.bind(null, versionInfo.id)}>
{t('operation.delete', { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
)
}

View File

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

View File

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

View File

@ -1,9 +1,16 @@
import type { FC } from 'react'
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 { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
type RestoreConfirmModalProps = {
isOpen: boolean
@ -21,24 +28,36 @@ const RestoreConfirmModal: FC<RestoreConfirmModalProps> = ({
const { t } = useTranslation()
return (
<Modal className="p-0" isShow={isOpen} onClose={onClose}>
<div className="flex flex-col gap-y-2 p-6 pb-4">
<div className="title-2xl-semi-bold text-text-primary">
{`${t('common.restore', { ns: 'workflow' })} ${versionInfo.marked_name || t('versionHistory.defaultName', { ns: 'workflow' })}`}
<AlertDialog
open={isOpen}
onOpenChange={(open) => {
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>
<p className="system-md-regular text-text-secondary">
{t('versionHistory.restorationTip', { ns: 'workflow' })}
</p>
</div>
<div className="flex items-center justify-end gap-x-2 p-6">
<Button onClick={onClose}>
{t('operation.cancel', { ns: 'common' })}
</Button>
<Button variant="primary" onClick={onRestore.bind(null, versionInfo)}>
{t('common.restore', { ns: 'workflow' })}
</Button>
</div>
</Modal>
<AlertDialogActions>
<AlertDialogCancelButton
nativeButton={false}
variant="secondary"
closeProps={{ nativeButton: false }}
>
{t('operation.cancel', { ns: 'common' })}
</AlertDialogCancelButton>
<AlertDialogConfirmButton nativeButton={false} tone="default" onClick={onRestore.bind(null, versionInfo)}>
{t('common.restore', { ns: 'workflow' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
)
}

View File

@ -2,6 +2,7 @@
import type { MouseEventHandler } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import {
RiAlertFill,
@ -17,7 +18,6 @@ import {
import { useTranslation } from 'react-i18next'
import Uploader from '@/app/components/app/create-from-dsl-modal/uploader'
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 { useEventEmitterContextContext } from '@/context/event-emitter'
import {
@ -199,90 +199,100 @@ const UpdateDSLModal = ({
return (
<>
<Modal
className="w-[520px] rounded-2xl p-6"
isShow={show}
onClose={onCancel}
<Dialog
open={show}
onOpenChange={(open) => {
if (!open)
onCancel()
}}
>
<div className="mb-3 flex items-center justify-between">
<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}>
<RiCloseLine className="h-[18px] w-[18px] text-text-tertiary" />
</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 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>
<DialogContent className="w-full max-w-[480px]! overflow-hidden! rounded-2xl border-none p-6 text-left align-middle">
<div className="mb-3 flex items-center justify-between">
<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}>
<RiCloseLine className="h-[18px] w-[18px] text-text-tertiary" />
</div>
</div>
</div>
<div>
<div className="pt-2 system-md-semibold text-text-primary">
{t('common.chooseDSL', { ns: 'workflow' })}
<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 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 className="pt-2 system-md-semibold text-text-primary">
{t('common.chooseDSL', { ns: 'workflow' })}
</div>
<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 className="flex items-center justify-end gap-2 self-stretch pt-5">
<Button onClick={onCancel}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button
disabled={!currentFile || loading}
variant="primary"
tone="destructive"
onClick={handleImport}
loading={loading}
>
{t('common.overwriteAndImport', { ns: 'workflow' })}
</Button>
</div>
</Modal>
<Modal
isShow={showErrorModal}
onClose={() => setShowErrorModal(false)}
className="w-[480px]"
<div className="flex items-center justify-end gap-2 self-stretch pt-5">
<Button onClick={onCancel}>{t('newApp.Cancel', { ns: 'app' })}</Button>
<Button
disabled={!currentFile || loading}
variant="primary"
tone="destructive"
onClick={handleImport}
loading={loading}
>
{t('common.overwriteAndImport', { ns: 'workflow' })}
</Button>
</div>
</DialogContent>
</Dialog>
<Dialog
open={showErrorModal}
onOpenChange={(open) => {
if (!open)
setShowErrorModal(false)
}}
>
<div className="flex flex-col items-start gap-2 self-stretch pb-4">
<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>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
<br />
<div>
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
<span className="system-md-medium">{versions?.importedVersion}</span>
</div>
<div>
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
<span className="system-md-medium">{versions?.systemVersion}</span>
<DialogContent className="w-full max-w-[480px]! overflow-hidden! border-none text-left align-middle">
<div className="flex flex-col items-start gap-2 self-stretch pb-4">
<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>{t('newApp.appCreateDSLErrorPart1', { ns: 'app' })}</div>
<div>{t('newApp.appCreateDSLErrorPart2', { ns: 'app' })}</div>
<br />
<div>
{t('newApp.appCreateDSLErrorPart3', { ns: 'app' })}
<span className="system-md-medium">{versions?.importedVersion}</span>
</div>
<div>
{t('newApp.appCreateDSLErrorPart4', { ns: 'app' })}
<span className="system-md-medium">{versions?.systemVersion}</span>
</div>
</div>
</div>
</div>
<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="primary" tone="destructive" onClick={onUpdateDSLConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button>
</div>
</Modal>
<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="primary" tone="destructive" onClick={onUpdateDSLConfirm}>{t('newApp.Confirm', { ns: 'app' })}</Button>
</div>
</DialogContent>
</Dialog>
</>
)
}

View File

@ -1,9 +1,9 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { RiExternalLinkLine } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import { useDocLink } from '@/context/i18n'
import { useModalContextSelector } from '@/context/modal-context'
import useTimestamp from '@/hooks/use-timestamp'
@ -41,63 +41,70 @@ const ExpireNoticeModal: React.FC<Props> = ({ expireAt, expired, onClose }) => {
}
return (
<Modal
isShow
onClose={onClose}
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 } })}
closable
className="max-w-[600px]"
<Dialog
open
onOpenChange={(open) => {
if (!open)
onClose()
}}
>
<div className="mt-5 space-y-5 body-md-regular text-text-secondary">
<div>
{expired
? (
<>
<div>{t(`${i18nPrefix}.expired.summary.line1`, { ns: 'education' })}</div>
<div>{t(`${i18nPrefix}.expired.summary.line2`, { ns: 'education' })}</div>
</>
)
: t(`${i18nPrefix}.isAboutToExpire.summary`, { ns: 'education' })}
<DialogContent className="w-full max-w-[600px] overflow-hidden! border-none text-left align-middle">
<DialogCloseButton data-testid="modal-close-button" />
<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 className="mt-5 space-y-5 body-md-regular text-text-secondary">
<div>
{expired
? (
<>
<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>
<strong className="block title-md-semi-bold">{t(`${i18nPrefix}.stillInEducation.title`, { ns: 'education' })}</strong>
{t(`${i18nPrefix}.stillInEducation.${expired ? 'expired' : 'isAboutToExpire'}`, { ns: 'education' })}
<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>
<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 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>
</DialogContent>
</Dialog>
)
}