Merge branch 'fix/chore-fix' into dev/plugin-deploy

This commit is contained in:
Yeuoly
2024-11-25 17:19:51 +08:00
250 changed files with 7636 additions and 1975 deletions

View File

@ -22,6 +22,7 @@ export const useWorkflowTemplate = () => {
...nodesInitialData.llm,
memory: {
window: { enabled: false, size: 10 },
query_prompt_template: '{{#sys.query#}}',
},
selected: true,
},

View File

@ -16,6 +16,7 @@ import { InputVarType, NodeRunningStatus } from '@/app/components/workflow/types
import ResultPanel from '@/app/components/workflow/run/result-panel'
import Toast from '@/app/components/base/toast'
import { TransferMethod } from '@/types/app'
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
const i18nPrefix = 'workflow.singleRun'
@ -39,6 +40,11 @@ function formatValue(value: string | any, type: InputVarType) {
return JSON.parse(item)
})
}
if (type === InputVarType.multiFiles)
return getProcessedFiles(value)
if (type === InputVarType.singleFile)
return getProcessedFiles([value])[0]
return value
}

View File

@ -160,6 +160,7 @@ const CodeEditor: FC<Props> = ({
hideSearch
vars={availableVars}
onChange={handleSelectVar}
isSupportFileVar={false}
/>
</div>
)}

View File

@ -128,7 +128,7 @@ const PanelOperatorPopup = ({
className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
onClick={() => {
onClosePopup()
handleNodesCopy()
handleNodesCopy(id)
}}
>
{t('workflow.common.copy')}

View File

@ -18,6 +18,7 @@ type Props = {
isSupportConstantValue?: boolean
onlyLeafNodeVar?: boolean
filterVar?: (payload: Var, valueSelector: ValueSelector) => boolean
isSupportFileVar?: boolean
}
const VarList: FC<Props> = ({
@ -29,6 +30,7 @@ const VarList: FC<Props> = ({
isSupportConstantValue,
onlyLeafNodeVar,
filterVar,
isSupportFileVar = true,
}) => {
const { t } = useTranslation()
@ -94,6 +96,7 @@ const VarList: FC<Props> = ({
defaultVarKindType={item.variable_type}
onlyLeafNodeVar={onlyLeafNodeVar}
filterVar={filterVar}
isSupportFileVar={isSupportFileVar}
/>
{!readonly && (
<RemoveButton

View File

@ -59,6 +59,7 @@ type Props = {
isInTable?: boolean
onRemove?: () => void
typePlaceHolder?: string
isSupportFileVar?: boolean
}
const VarReferencePicker: FC<Props> = ({
@ -81,6 +82,7 @@ const VarReferencePicker: FC<Props> = ({
isInTable,
onRemove,
typePlaceHolder,
isSupportFileVar = true,
}) => {
const { t } = useTranslation()
const store = useStoreApi()
@ -382,6 +384,7 @@ const VarReferencePicker: FC<Props> = ({
vars={outputVars}
onChange={handleVarReferenceChange}
itemWidth={isAddBtnTrigger ? 260 : triggerWidth}
isSupportFileVar={isSupportFileVar}
/>
)}
</PortalToFollowElemContent>

View File

@ -8,11 +8,13 @@ type Props = {
vars: NodeOutPutVar[]
onChange: (value: ValueSelector, varDetail: Var) => void
itemWidth?: number
isSupportFileVar?: boolean
}
const VarReferencePopup: FC<Props> = ({
vars,
onChange,
itemWidth,
isSupportFileVar = true,
}) => {
// max-h-[300px] overflow-y-auto todo: use portal to handle long list
return (
@ -24,7 +26,7 @@ const VarReferencePopup: FC<Props> = ({
vars={vars}
onChange={onChange}
itemWidth={itemWidth}
isSupportFileVar
isSupportFileVar={isSupportFileVar}
/>
</div >
)

View File

@ -89,6 +89,7 @@ const Panel: FC<NodePanelProps<CodeNodeType>> = ({
list={inputs.variables}
onChange={handleVarListChange}
filterVar={filterVar}
isSupportFileVar={false}
/>
</Field>
<Split />

View File

@ -0,0 +1,154 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { BodyType, type HttpNodeType, Method } from '../types'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast'
import { useNodesInteractions } from '@/app/components/workflow/hooks'
type Props = {
nodeId: string
isShow: boolean
onHide: () => void
handleCurlImport: (node: HttpNodeType) => void
}
const parseCurl = (curlCommand: string): { node: HttpNodeType | null; error: string | null } => {
if (!curlCommand.trim().toLowerCase().startsWith('curl'))
return { node: null, error: 'Invalid cURL command. Command must start with "curl".' }
const node: Partial<HttpNodeType> = {
title: 'HTTP Request',
desc: 'Imported from cURL',
method: Method.get,
url: '',
headers: '',
params: '',
body: { type: BodyType.none, data: '' },
}
const args = curlCommand.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []
for (let i = 1; i < args.length; i++) {
const arg = args[i].replace(/^['"]|['"]$/g, '')
switch (arg) {
case '-X':
case '--request':
if (i + 1 >= args.length)
return { node: null, error: 'Missing HTTP method after -X or --request.' }
node.method = (args[++i].replace(/^['"]|['"]$/g, '') as Method) || Method.get
break
case '-H':
case '--header':
if (i + 1 >= args.length)
return { node: null, error: 'Missing header value after -H or --header.' }
node.headers += (node.headers ? '\n' : '') + args[++i].replace(/^['"]|['"]$/g, '')
break
case '-d':
case '--data':
case '--data-raw':
case '--data-binary':
if (i + 1 >= args.length)
return { node: null, error: 'Missing data value after -d, --data, --data-raw, or --data-binary.' }
node.body = { type: BodyType.rawText, data: args[++i].replace(/^['"]|['"]$/g, '') }
break
case '-F':
case '--form': {
if (i + 1 >= args.length)
return { node: null, error: 'Missing form data after -F or --form.' }
if (node.body?.type !== BodyType.formData)
node.body = { type: BodyType.formData, data: '' }
const formData = args[++i].replace(/^['"]|['"]$/g, '')
const [key, ...valueParts] = formData.split('=')
if (!key)
return { node: null, error: 'Invalid form data format.' }
let value = valueParts.join('=')
// To support command like `curl -F "file=@/path/to/file;type=application/zip"`
// the `;type=application/zip` should translate to `Content-Type: application/zip`
const typeMatch = value.match(/^(.+?);type=(.+)$/)
if (typeMatch) {
const [, actualValue, mimeType] = typeMatch
value = actualValue
node.headers += `${node.headers ? '\n' : ''}Content-Type: ${mimeType}`
}
node.body.data += `${node.body.data ? '\n' : ''}${key}:${value}`
break
}
case '--json':
if (i + 1 >= args.length)
return { node: null, error: 'Missing JSON data after --json.' }
node.body = { type: BodyType.json, data: args[++i].replace(/^['"]|['"]$/g, '') }
break
default:
if (arg.startsWith('http') && !node.url)
node.url = arg
break
}
}
if (!node.url)
return { node: null, error: 'Missing URL or url not start with http.' }
// Extract query params from URL
const urlParts = node.url?.split('?') || []
if (urlParts.length > 1) {
node.url = urlParts[0]
node.params = urlParts[1].replace(/&/g, '\n').replace(/=/g, ': ')
}
return { node: node as HttpNodeType, error: null }
}
const CurlPanel: FC<Props> = ({ nodeId, isShow, onHide, handleCurlImport }) => {
const [inputString, setInputString] = useState('')
const { handleNodeSelect } = useNodesInteractions()
const { t } = useTranslation()
const handleSave = useCallback(() => {
const { node, error } = parseCurl(inputString)
if (error) {
Toast.notify({
type: 'error',
message: error,
})
return
}
if (!node)
return
onHide()
handleCurlImport(node)
// Close the panel then open it again to make the panel re-render
handleNodeSelect(nodeId, true)
setTimeout(() => {
handleNodeSelect(nodeId)
}, 0)
}, [onHide, nodeId, inputString, handleNodeSelect, handleCurlImport])
return (
<Modal
title={t('workflow.nodes.http.curl.title')}
isShow={isShow}
onClose={onHide}
className='!w-[400px] !max-w-[400px] !p-4'
>
<div>
<textarea
value={inputString}
className='w-full my-3 p-3 text-sm text-gray-900 border-0 rounded-lg grow bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200 h-40'
onChange={e => setInputString(e.target.value)}
placeholder={t('workflow.nodes.http.curl.placeholder')!}
/>
</div>
<div className='mt-4 flex justify-end space-x-2'>
<Button className='!w-[95px]' onClick={onHide} >{t('common.operation.cancel')}</Button>
<Button className='!w-[95px]' variant='primary' onClick={handleSave} > {t('common.operation.save')}</Button>
</div>
</Modal>
)
}
export default React.memo(CurlPanel)

View File

@ -8,11 +8,13 @@ import EditBody from './components/edit-body'
import AuthorizationModal from './components/authorization'
import type { HttpNodeType } from './types'
import Timeout from './components/timeout'
import CurlPanel from './components/curl-panel'
import cn from '@/utils/classnames'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
import { FileArrow01 } from '@/app/components/base/icons/src/vender/line/files'
import type { NodePanelProps } from '@/app/components/workflow/types'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import ResultPanel from '@/app/components/workflow/run/result-panel'
@ -53,6 +55,10 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
inputVarValues,
setInputVarValues,
runResult,
isShowCurlPanel,
showCurlPanel,
hideCurlPanel,
handleCurlImport,
} = useConfig(id, data)
// To prevent prompt editor in body not update data.
if (!isDataReady)
@ -64,14 +70,25 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
<Field
title={t(`${i18nPrefix}.api`)}
operations={
<div
onClick={showAuthorization}
className={cn(!readOnly && 'cursor-pointer hover:bg-gray-50', 'flex items-center h-6 space-x-1 px-2 rounded-md ')}
>
{!readOnly && <Settings01 className='w-3 h-3 text-gray-500' />}
<div className='text-xs font-medium text-gray-500'>
{t(`${i18nPrefix}.authorization.authorization`)}
<span className='ml-1 text-gray-700'>{t(`${i18nPrefix}.authorization.${inputs.authorization.type}`)}</span>
<div className='flex'>
<div
onClick={showAuthorization}
className={cn(!readOnly && 'cursor-pointer hover:bg-gray-50', 'flex items-center h-6 space-x-1 px-2 rounded-md ')}
>
{!readOnly && <Settings01 className='w-3 h-3 text-gray-500' />}
<div className='text-xs font-medium text-gray-500'>
{t(`${i18nPrefix}.authorization.authorization`)}
<span className='ml-1 text-gray-700'>{t(`${i18nPrefix}.authorization.${inputs.authorization.type}`)}</span>
</div>
</div>
<div
onClick={showCurlPanel}
className={cn(!readOnly && 'cursor-pointer hover:bg-gray-50', 'flex items-center h-6 space-x-1 px-2 rounded-md ')}
>
{!readOnly && <FileArrow01 className='w-3 h-3 text-gray-500' />}
<div className='text-xs font-medium text-gray-500'>
{t(`${i18nPrefix}.curl.title`)}
</div>
</div>
</div>
}
@ -180,7 +197,15 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
result={<ResultPanel {...runResult} showSteps={false} />}
/>
)}
</div >
{(isShowCurlPanel && !readOnly) && (
<CurlPanel
nodeId={id}
isShow
onHide={hideCurlPanel}
handleCurlImport={handleCurlImport}
/>
)}
</div>
)
}

View File

@ -164,6 +164,23 @@ const useConfig = (id: string, payload: HttpNodeType) => {
setRunInputData(newPayload)
}, [setRunInputData])
// curl import panel
const [isShowCurlPanel, {
setTrue: showCurlPanel,
setFalse: hideCurlPanel,
}] = useBoolean(false)
const handleCurlImport = useCallback((newNode: HttpNodeType) => {
const newInputs = produce(inputs, (draft: HttpNodeType) => {
draft.method = newNode.method
draft.url = newNode.url
draft.headers = newNode.headers
draft.params = newNode.params
draft.body = newNode.body
})
setInputs(newInputs)
}, [inputs, setInputs])
return {
readOnly,
isDataReady,
@ -203,6 +220,11 @@ const useConfig = (id: string, payload: HttpNodeType) => {
inputVarValues,
setInputVarValues,
runResult,
// curl import
isShowCurlPanel,
showCurlPanel,
hideCurlPanel,
handleCurlImport,
}
}

View File

@ -144,6 +144,7 @@ const ConfigPromptItem: FC<Props> = ({
onEditionTypeChange={onEditionTypeChange}
varList={varList}
handleAddVariable={handleAddVariable}
isSupportFileVar
/>
)
}

View File

@ -67,6 +67,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
handleStop,
varInputs,
runResult,
filterJinjia2InputVar,
} = useConfig(id, data)
const model = inputs.model
@ -194,7 +195,8 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
list={inputs.prompt_config?.jinja2_variables || []}
onChange={handleVarListChange}
onVarNameChange={handleVarNameChange}
filterVar={filterVar}
filterVar={filterJinjia2InputVar}
isSupportFileVar={false}
/>
</Field>
)}
@ -233,6 +235,7 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
hasSetBlockStatus={hasSetBlockStatus}
nodesOutputVars={availableVars}
availableNodes={availableNodesWithParent}
isSupportFileVar
/>
{inputs.memory.query_prompt_template && !inputs.memory.query_prompt_template.includes('{{#sys.query#}}') && (

View File

@ -278,11 +278,15 @@ const useConfig = (id: string, payload: LLMNodeType) => {
}, [inputs, setInputs])
const filterInputVar = useCallback((varPayload: Var) => {
return [VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.file, VarType.arrayFile].includes(varPayload.type)
}, [])
const filterJinjia2InputVar = useCallback((varPayload: Var) => {
return [VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber].includes(varPayload.type)
}, [])
const filterMemoryPromptVar = useCallback((varPayload: Var) => {
return [VarType.arrayObject, VarType.array, VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber].includes(varPayload.type)
return [VarType.arrayObject, VarType.array, VarType.number, VarType.string, VarType.secret, VarType.arrayString, VarType.arrayNumber, VarType.file, VarType.arrayFile].includes(varPayload.type)
}, [])
const {
@ -406,6 +410,7 @@ const useConfig = (id: string, payload: LLMNodeType) => {
handleRun,
handleStop,
runResult,
filterJinjia2InputVar,
}
}

View File

@ -64,6 +64,7 @@ const Panel: FC<NodePanelProps<TemplateTransformNodeType>> = ({
onChange={handleVarListChange}
onVarNameChange={handleVarNameChange}
filterVar={filterVar}
isSupportFileVar={false}
/>
</Field>
<Split />

View File

@ -10,8 +10,9 @@ import {
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import {
RiAlertLine,
RiAlertFill,
RiCloseLine,
RiFileDownloadLine,
} from '@remixicon/react'
import { WORKFLOW_DATA_UPDATE } from './constants'
import {
@ -21,11 +22,19 @@ import {
initialEdges,
initialNodes,
} from './utils'
import {
importDSL,
importDSLConfirm,
} from '@/service/apps'
import { fetchWorkflowDraft } from '@/service/workflow'
import {
DSLImportMode,
DSLImportStatus,
} from '@/models/app'
import Uploader from '@/app/components/app/create-from-dsl-modal/uploader'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import { ToastContext } from '@/app/components/base/toast'
import { updateWorkflowDraftFromDSL } from '@/service/workflow'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { useStore as useAppStore } from '@/app/components/app/store'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
@ -50,6 +59,10 @@ const UpdateDSLModal = ({
const [loading, setLoading] = useState(false)
const { eventEmitter } = useEventEmitterContextContext()
const { mutateAsync, mutate } = useMutationCheckDependenciesBeforeImportDSL()
const [show, setShow] = useState(true)
const [showErrorModal, setShowErrorModal] = useState(false)
const [versions, setVersions] = useState<{ importedVersion: string; systemVersion: string }>()
const [importId, setImportId] = useState<string>()
const readFile = (file: File) => {
const reader = new FileReader()
@ -68,6 +81,51 @@ const UpdateDSLModal = ({
setFileContent('')
}
const handleWorkflowUpdate = async (app_id: string) => {
const {
graph,
features,
hash,
} = await fetchWorkflowDraft(`/apps/${app_id}/workflows/draft`)
const { nodes, edges, viewport } = graph
const newFeatures = {
file: {
image: {
enabled: !!features.file_upload?.image?.enabled,
number_limits: features.file_upload?.image?.number_limits || 3,
transfer_methods: features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
},
enabled: !!(features.file_upload?.enabled || features.file_upload?.image?.enabled),
allowed_file_types: features.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
allowed_file_extensions: features.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
allowed_file_upload_methods: features.file_upload?.allowed_file_upload_methods || features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
number_limits: features.file_upload?.number_limits || features.file_upload?.image?.number_limits || 3,
},
opening: {
enabled: !!features.opening_statement,
opening_statement: features.opening_statement,
suggested_questions: features.suggested_questions,
},
suggested: features.suggested_questions_after_answer || { enabled: false },
speech2text: features.speech_to_text || { enabled: false },
text2speech: features.text_to_speech || { enabled: false },
citation: features.retriever_resource || { enabled: false },
moderation: features.sensitive_word_avoidance || { enabled: false },
}
eventEmitter?.emit({
type: WORKFLOW_DATA_UPDATE,
payload: {
nodes: initialNodes(nodes, edges),
edges: initialEdges(edges, nodes),
viewport,
features: newFeatures,
hash,
},
} as any)
}
const isCreatingRef = useRef(false)
const handleImport: MouseEventHandler = useCallback(async () => {
if (isCreatingRef.current)
@ -109,21 +167,6 @@ const UpdateDSLModal = ({
citation: features.retriever_resource || { enabled: false },
moderation: features.sensitive_word_avoidance || { enabled: false },
}
eventEmitter?.emit({
type: WORKFLOW_DATA_UPDATE,
payload: {
nodes: initialNodes(nodes, edges),
edges: initialEdges(edges, nodes),
viewport,
features: newFeatures,
hash,
},
} as any)
if (onImport)
onImport()
notify({ type: 'success', message: t('workflow.common.importSuccess') })
setLoading(false)
onCancel()
}
}
catch (e) {
@ -133,52 +176,119 @@ const UpdateDSLModal = ({
isCreatingRef.current = false
}, [currentFile, fileContent, onCancel, notify, t, eventEmitter, appDetail, onImport, mutateAsync])
const onUpdateDSLConfirm: MouseEventHandler = async () => {
try {
if (!importId)
return
const response = await importDSLConfirm({
import_id: importId,
})
const { status, app_id } = response
if (status === DSLImportStatus.COMPLETED) {
if (!app_id) {
notify({ type: 'error', message: t('workflow.common.importFailure') })
return
}
handleWorkflowUpdate(app_id)
if (onImport)
onImport()
notify({ type: 'success', message: t('workflow.common.importSuccess') })
setLoading(false)
onCancel()
}
else if (status === DSLImportStatus.FAILED) {
setLoading(false)
notify({ type: 'error', message: t('workflow.common.importFailure') })
}
}
catch (e) {
setLoading(false)
notify({ type: 'error', message: t('workflow.common.importFailure') })
}
}
return (
<Modal
className='p-6 w-[520px] rounded-2xl'
isShow={true}
onClose={() => {}}
>
<div className='flex items-center justify-between mb-6'>
<div className='text-2xl font-semibold text-[#101828]'>{t('workflow.common.importDSL')}</div>
<div className='flex items-center justify-center w-[22px] h-[22px] cursor-pointer' onClick={onCancel}>
<RiCloseLine className='w-5 h-5 text-gray-500' />
<>
<Modal
className='p-6 w-[520px] rounded-2xl'
isShow={show}
onClose={onCancel}
>
<div className='flex items-center justify-between mb-3'>
<div className='title-2xl-semi-bold text-text-primary'>{t('workflow.common.importDSL')}</div>
<div className='flex items-center justify-center w-[22px] h-[22px] cursor-pointer' onClick={onCancel}>
<RiCloseLine className='w-[18px] h-[18px] text-text-tertiary' />
</div>
</div>
<div className='flex relative p-2 mb-2 gap-0.5 flex-grow rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xs overflow-hidden'>
<div className='absolute top-0 left-0 w-full h-full opacity-40 bg-[linear-gradient(92deg,rgba(247,144,9,0.25)_0%,rgba(255,255,255,0.00)_100%)]' />
<div className='flex p-1 justify-center items-start'>
<RiAlertFill className='w-4 h-4 flex-shrink-0 text-text-warning-secondary' />
</div>
<div className='flex py-1 flex-col items-start gap-0.5 flex-grow'>
<div className='text-text-primary system-xs-medium whitespace-pre-line'>{t('workflow.common.importDSLTip')}</div>
<div className='flex pt-1 pb-0.5 items-start gap-1 self-stretch'>
<Button
size='small'
variant='secondary'
className='z-[1000]'
onClick={onBackup}
>
<RiFileDownloadLine className='w-3.5 h-3.5 text-components-button-secondary-text' />
<div className='flex px-[3px] justify-center items-center gap-1'>
{t('workflow.common.backupCurrentDraft')}
</div>
</Button>
</div>
</div>
</div>
</div>
<div className='flex mb-4 px-4 py-3 bg-[#FFFAEB] rounded-xl border border-[#FEDF89]'>
<RiAlertLine className='shrink-0 mt-0.5 mr-2 w-4 h-4 text-[#F79009]' />
<div>
<div className='mb-2 text-sm font-medium text-[#354052]'>{t('workflow.common.importDSLTip')}</div>
<div className='pt-2 text-text-primary system-md-semibold'>
{t('workflow.common.chooseDSL')}
</div>
<div className='flex w-full py-4 flex-col justify-center items-start gap-4 self-stretch'>
<Uploader
file={currentFile}
updateFile={handleFile}
className='!mt-0 w-full'
/>
</div>
</div>
<div className='flex pt-5 gap-2 items-center justify-end self-stretch'>
<Button onClick={onCancel}>{t('app.newApp.Cancel')}</Button>
<Button
variant='secondary-accent'
onClick={onBackup}
disabled={!currentFile || loading}
variant='warning'
onClick={handleImport}
loading={loading}
>
{t('workflow.common.backupCurrentDraft')}
{t('workflow.common.overwriteAndImport')}
</Button>
</div>
</div>
<div className='mb-8'>
<div className='mb-1 text-[13px] font-semibold text-[#354052]'>
{t('workflow.common.chooseDSL')}
</Modal>
<Modal
isShow={showErrorModal}
onClose={() => setShowErrorModal(false)}
className='w-[480px]'
>
<div className='flex pb-4 flex-col items-start gap-2 self-stretch'>
<div className='text-text-primary title-2xl-semi-bold'>{t('app.newApp.appCreateDSLErrorTitle')}</div>
<div className='flex flex-grow flex-col text-text-secondary system-md-regular'>
<div>{t('app.newApp.appCreateDSLErrorPart1')}</div>
<div>{t('app.newApp.appCreateDSLErrorPart2')}</div>
<br />
<div>{t('app.newApp.appCreateDSLErrorPart3')}<span className='system-md-medium'>{versions?.importedVersion}</span></div>
<div>{t('app.newApp.appCreateDSLErrorPart4')}<span className='system-md-medium'>{versions?.systemVersion}</span></div>
</div>
</div>
<Uploader
file={currentFile}
updateFile={handleFile}
className='!mt-0'
/>
</div>
<div className='flex justify-end'>
<Button className='mr-2' onClick={onCancel}>{t('app.newApp.Cancel')}</Button>
<Button
disabled={!currentFile || loading}
variant='warning'
onClick={handleImport}
loading={loading}
>
{t('workflow.common.overwriteAndImport')}
</Button>
</div>
</Modal>
<div className='flex pt-6 justify-end items-start gap-2 self-stretch'>
<Button variant='secondary' onClick={() => setShowErrorModal(false)}>{t('app.newApp.Cancel')}</Button>
<Button variant='primary' destructive onClick={onUpdateDSLConfirm}>{t('app.newApp.Confirm')}</Button>
</div>
</Modal>
</>
)
}