feat: Check and compare the DSL version before import an app (#10969)

Co-authored-by: Yi <yxiaoisme@gmail.com>
This commit is contained in:
-LAN-
2024-11-22 15:05:04 +08:00
committed by GitHub
parent d9579f418d
commit 5172f0bf39
24 changed files with 1101 additions and 874 deletions

View File

@@ -12,9 +12,13 @@ import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import { ToastContext } from '@/app/components/base/toast'
import {
importApp,
importAppFromUrl,
importDSL,
importDSLConfirm,
} from '@/service/apps'
import {
DSLImportMode,
DSLImportStatus,
} from '@/models/app'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
@@ -43,6 +47,9 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
const [fileContent, setFileContent] = useState<string>()
const [currentTab, setCurrentTab] = useState(activeTab)
const [dslUrlValue, setDslUrlValue] = useState(dslUrl)
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()
@@ -66,6 +73,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
const isCreatingRef = useRef(false)
const onCreate: MouseEventHandler = async () => {
if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile)
return
@@ -75,25 +83,54 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
return
isCreatingRef.current = true
try {
let app
let response
if (currentTab === CreateFromDSLModalTab.FROM_FILE) {
app = await importApp({
data: fileContent || '',
response = await importDSL({
mode: DSLImportMode.YAML_CONTENT,
yaml_content: fileContent || '',
})
}
if (currentTab === CreateFromDSLModalTab.FROM_URL) {
app = await importAppFromUrl({
url: dslUrlValue || '',
response = await importDSL({
mode: DSLImportMode.YAML_URL,
yaml_url: dslUrlValue || '',
})
}
if (onSuccess)
onSuccess()
if (onClose)
onClose()
notify({ type: 'success', message: t('app.newApp.appCreated') })
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection(isCurrentWorkspaceEditor, app, push)
if (!response)
return
const { id, status, app_id, imported_dsl_version, current_dsl_version } = response
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
if (onSuccess)
onSuccess()
if (onClose)
onClose()
notify({
type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning',
message: t(status === DSLImportStatus.COMPLETED ? 'app.newApp.appCreated' : 'app.newApp.caution'),
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('app.newApp.appCreateDSLWarning'),
})
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection(isCurrentWorkspaceEditor, { id: app_id }, push)
}
else if (status === DSLImportStatus.PENDING) {
setVersions({
importedVersion: imported_dsl_version ?? '',
systemVersion: current_dsl_version ?? '',
})
if (onClose)
onClose()
setTimeout(() => {
setShowErrorModal(true)
}, 300)
setImportId(id)
}
else {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
}
catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
@@ -101,6 +138,38 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
isCreatingRef.current = false
}
const onDSLConfirm: MouseEventHandler = async () => {
try {
if (!importId)
return
const response = await importDSLConfirm({
import_id: importId,
})
const { status, app_id } = response
if (status === DSLImportStatus.COMPLETED) {
if (onSuccess)
onSuccess()
if (onClose)
onClose()
notify({
type: 'success',
message: t('app.newApp.appCreated'),
})
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
getRedirection(isCurrentWorkspaceEditor, { id: app_id }, push)
}
else if (status === DSLImportStatus.FAILED) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
}
catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
}
const tabs = [
{
key: CreateFromDSLModalTab.FROM_FILE,
@@ -123,74 +192,96 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
}, [isAppsFull, currentTab, currentFile, dslUrlValue])
return (
<Modal
className='p-0 w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'
isShow={show}
onClose={() => { }}
>
<div className='flex items-center justify-between pt-6 pl-6 pr-5 pb-3 text-text-primary title-2xl-semi-bold'>
{t('app.importFromDSL')}
<div
className='flex items-center w-8 h-8 cursor-pointer'
onClick={() => onClose()}
>
<RiCloseLine className='w-5 h-5 text-text-tertiary' />
<>
<Modal
className='p-0 w-[520px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'
isShow={show}
onClose={() => { }}
>
<div className='flex items-center justify-between pt-6 pl-6 pr-5 pb-3 text-text-primary title-2xl-semi-bold'>
{t('app.importFromDSL')}
<div
className='flex items-center w-8 h-8 cursor-pointer'
onClick={() => onClose()}
>
<RiCloseLine className='w-5 h-5 text-text-tertiary' />
</div>
</div>
</div>
<div className='flex items-center px-6 h-9 space-x-6 system-md-semibold text-text-tertiary border-b border-divider-subtle'>
{
tabs.map(tab => (
<div
key={tab.key}
className={cn(
'relative flex items-center h-full cursor-pointer',
currentTab === tab.key && 'text-text-primary',
)}
onClick={() => setCurrentTab(tab.key)}
>
{tab.label}
{
currentTab === tab.key && (
<div className='absolute bottom-0 w-full h-[2px] 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 leading6'>DSL URL</div>
<Input
placeholder={t('app.importFromDSLUrlPlaceholder') || ''}
value={dslUrlValue}
onChange={e => setDslUrlValue(e.target.value)}
<div className='flex items-center px-6 h-9 space-x-6 system-md-semibold text-text-tertiary border-b border-divider-subtle'>
{
tabs.map(tab => (
<div
key={tab.key}
className={cn(
'relative flex items-center h-full cursor-pointer',
currentTab === tab.key && 'text-text-primary',
)}
onClick={() => setCurrentTab(tab.key)}
>
{tab.label}
{
currentTab === tab.key && (
<div className='absolute bottom-0 w-full h-[2px] 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 leading6'>DSL URL</div>
<Input
placeholder={t('app.importFromDSLUrlPlaceholder') || ''}
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('app.newApp.Cancel')}</Button>
<Button disabled={buttonDisabled} variant="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
</div>
</Modal>
{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('app.newApp.Cancel')}</Button>
<Button disabled={buttonDisabled} variant="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
</div>
</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>
<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={onDSLConfirm}>{t('app.newApp.Confirm')}</Button>
</div>
</Modal>
</>
)
}

View File

@@ -6,6 +6,7 @@ import {
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { formatFileSize } from '@/utils/format'
import cn from '@/utils/classnames'
import { Yaml as YamlIcon } from '@/app/components/base/icons/src/public/files'
import { ToastContext } from '@/app/components/base/toast'
@@ -58,8 +59,13 @@ const Uploader: FC<Props> = ({
updateFile(files[0])
}
const selectHandle = () => {
if (fileUploader.current)
const originalFile = file
if (fileUploader.current) {
fileUploader.current.value = ''
fileUploader.current.click()
// If no file is selected, restore the original file
fileUploader.current.oncancel = () => updateFile(originalFile)
}
}
const removeFile = () => {
if (fileUploader.current)
@@ -96,7 +102,7 @@ const Uploader: FC<Props> = ({
/>
<div ref={dropRef}>
{!file && (
<div className={cn('flex items-center h-20 rounded-xl bg-gray-50 border border-dashed border-gray-200 text-sm font-normal', dragging && 'bg-[#F5F8FF] border border-[#B2CCFF]')}>
<div className={cn('flex items-center h-12 rounded-xl bg-gray-50 border border-dashed border-gray-200 text-sm font-normal', dragging && 'bg-[#F5F8FF] border border-[#B2CCFF]')}>
<div className='w-full flex items-center justify-center space-x-2'>
<UploadCloud01 className='w-6 h-6 mr-2' />
<div className='text-gray-500'>
@@ -108,17 +114,23 @@ const Uploader: FC<Props> = ({
</div>
)}
{file && (
<div className={cn('flex items-center h-20 px-6 rounded-xl bg-gray-50 border border-gray-200 text-sm font-normal group', 'hover:bg-[#F5F8FF] hover:border-[#B2CCFF]')}>
<YamlIcon className="shrink-0" />
<div className='flex ml-2 w-0 grow'>
<span className='max-w-[calc(100%_-_30px)] text-ellipsis whitespace-nowrap overflow-hidden text-gray-800'>{file.name.replace(/(.yaml|.yml)$/, '')}</span>
<span className='shrink-0 text-gray-500'>.yml</span>
<div className={cn('flex items-center rounded-lg bg-components-panel-on-panel-item-bg border-[0.5px] border-components-panel-border shadow-xs group', 'hover:bg-[#F5F8FF] hover:border-[#B2CCFF]')}>
<div className='flex p-3 justify-center items-center'>
<YamlIcon className="w-6 h-6 shrink-0" />
</div>
<div className='flex py-1 pr-2 grow flex-col items-start gap-0.5'>
<span className='max-w-[calc(100%_-_30px)] text-ellipsis whitespace-nowrap overflow-hidden text-text-secondary font-inter text-[12px] font-medium leading-4'>{file.name}</span>
<div className='flex h-3 items-center gap-1 self-stretch text-text-tertiary font-inter text-[10px] font-medium leading-3 uppercase'>
<span>YAML</span>
<span className='text-text-quaternary'>·</span>
<span>{formatFileSize(file.size)}</span>
</div>
</div>
<div className='hidden group-hover:flex items-center'>
<Button onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
<div className='mx-2 w-px h-4 bg-gray-200' />
<div className='p-2 cursor-pointer' onClick={removeFile}>
<RiDeleteBinLine className='w-4 h-4 text-gray-500' />
<RiDeleteBinLine className='w-4 h-4 text-text-tertiary' />
</div>
</div>
</div>

View File

@@ -3,16 +3,19 @@ import type { ReactNode } from 'react'
import React, { useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'
import {
CheckCircleIcon,
ExclamationTriangleIcon,
InformationCircleIcon,
XCircleIcon,
} from '@heroicons/react/20/solid'
RiAlertFill,
RiCheckboxCircleFill,
RiCloseLine,
RiErrorWarningFill,
RiInformation2Fill,
} from '@remixicon/react'
import { createContext, useContext } from 'use-context-selector'
import ActionButton from '@/app/components/base/action-button'
import classNames from '@/utils/classnames'
export type IToastProps = {
type?: 'success' | 'error' | 'warning' | 'info'
size?: 'md' | 'sm'
duration?: number
message: string
children?: ReactNode
@@ -21,60 +24,55 @@ export type IToastProps = {
}
type IToastContext = {
notify: (props: IToastProps) => void
close: () => void
}
export const ToastContext = createContext<IToastContext>({} as IToastContext)
export const useToastContext = () => useContext(ToastContext)
const Toast = ({
type = 'info',
size = 'md',
message,
children,
className,
}: IToastProps) => {
const { close } = useToastContext()
// sometimes message is react node array. Not handle it.
if (typeof message !== 'string')
return null
return <div className={classNames(
className,
'fixed rounded-md p-4 my-4 mx-8 z-[9999]',
'fixed w-[360px] rounded-xl my-4 mx-8 flex-grow z-[9999] overflow-hidden',
size === 'md' ? 'p-3' : 'p-2',
'border border-components-panel-border-subtle bg-components-panel-bg-blur shadow-sm',
'top-0',
'right-0',
type === 'success' ? 'bg-green-50' : '',
type === 'error' ? 'bg-red-50' : '',
type === 'warning' ? 'bg-yellow-50' : '',
type === 'info' ? 'bg-blue-50' : '',
)}>
<div className="flex">
<div className="flex-shrink-0">
{type === 'success' && <CheckCircleIcon className="w-5 h-5 text-green-400" aria-hidden="true" />}
{type === 'error' && <XCircleIcon className="w-5 h-5 text-red-400" aria-hidden="true" />}
{type === 'warning' && <ExclamationTriangleIcon className="w-5 h-5 text-yellow-400" aria-hidden="true" />}
{type === 'info' && <InformationCircleIcon className="w-5 h-5 text-blue-400" aria-hidden="true" />}
<div className={`absolute inset-0 opacity-40 ${
(type === 'success' && 'bg-[linear-gradient(92deg,rgba(23,178,106,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|| (type === 'warning' && 'bg-[linear-gradient(92deg,rgba(247,144,9,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|| (type === 'error' && 'bg-[linear-gradient(92deg,rgba(240,68,56,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
|| (type === 'info' && 'bg-[linear-gradient(92deg,rgba(11,165,236,0.25)_0%,rgba(255,255,255,0.00)_100%)]')
}`}
/>
<div className={`flex ${size === 'md' ? 'gap-1' : 'gap-0.5'}`}>
<div className={`flex justify-center items-center ${size === 'md' ? 'p-0.5' : 'p-1'}`}>
{type === 'success' && <RiCheckboxCircleFill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-success`} aria-hidden="true" />}
{type === 'error' && <RiErrorWarningFill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-destructive`} aria-hidden="true" />}
{type === 'warning' && <RiAlertFill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-warning-secondary`} aria-hidden="true" />}
{type === 'info' && <RiInformation2Fill className={`${size === 'md' ? 'w-5 h-5' : 'w-4 h-4'} text-text-accent`} aria-hidden="true" />}
</div>
<div className="ml-3">
<h3 className={
classNames(
'text-sm font-medium',
type === 'success' ? 'text-green-800' : '',
type === 'error' ? 'text-red-800' : '',
type === 'warning' ? 'text-yellow-800' : '',
type === 'info' ? 'text-blue-800' : '',
)
}>{message}</h3>
{children && <div className={
classNames(
'mt-2 text-sm',
type === 'success' ? 'text-green-700' : '',
type === 'error' ? 'text-red-700' : '',
type === 'warning' ? 'text-yellow-700' : '',
type === 'info' ? 'text-blue-700' : '',
)
}>
<div className={`flex py-1 ${size === 'md' ? 'px-1' : 'px-0.5'} flex-col items-start gap-1 flex-grow`}>
<div className='text-text-primary system-sm-semibold'>{message}</div>
{children && <div className='text-text-secondary system-xs-regular'>
{children}
</div>
}
</div>
<ActionButton className='z-[1000]' onClick={close}>
<RiCloseLine className='w-4 h-4 flex-shrink-0 text-text-tertiary' />
</ActionButton>
</div>
</div>
}
@@ -106,6 +104,7 @@ export const ToastProvider = ({
setMounted(true)
setParams(props)
},
close: () => setMounted(false),
}}>
{mounted && <Toast {...params} />}
{children}
@@ -114,16 +113,17 @@ export const ToastProvider = ({
Toast.notify = ({
type,
size = 'md',
message,
duration,
className,
}: Pick<IToastProps, 'type' | 'message' | 'duration' | 'className'>) => {
}: Pick<IToastProps, 'type' | 'size' | 'message' | 'duration' | 'className'>) => {
const defaultDuring = (type === 'success' || type === 'info') ? 3000 : 6000
if (typeof window === 'object') {
const holder = document.createElement('div')
const root = createRoot(holder)
root.render(<Toast type={type} message={message} duration={duration} className={className} />)
root.render(<Toast type={type} size={size} message={message} duration={duration} className={className} />)
document.body.appendChild(holder)
setTimeout(() => {
if (holder)

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'
@@ -48,6 +57,10 @@ const UpdateDSLModal = ({
const [fileContent, setFileContent] = useState<string>()
const [loading, setLoading] = useState(false)
const { eventEmitter } = useEventEmitterContextContext()
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()
@@ -66,6 +79,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)
@@ -76,51 +134,39 @@ const UpdateDSLModal = ({
try {
if (appDetail && fileContent) {
setLoading(true)
const {
graph,
features,
hash,
} = await updateWorkflowDraftFromDSL(appDetail.id, fileContent)
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 },
const response = await importDSL({ mode: DSLImportMode.YAML_CONTENT, yaml_content: fileContent, app_id: appDetail.id })
const { id, status, app_id, imported_dsl_version, current_dsl_version } = response
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
if (!app_id) {
notify({ type: 'error', message: t('workflow.common.importFailure') })
return
}
handleWorkflowUpdate(app_id)
if (onImport)
onImport()
notify({
type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning',
message: t(status === DSLImportStatus.COMPLETED ? 'workflow.common.importSuccess' : 'workflow.common.importWarning'),
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('workflow.common.importWarningDetails'),
})
setLoading(false)
onCancel()
}
else if (status === DSLImportStatus.PENDING) {
setShow(false)
setTimeout(() => {
setShowErrorModal(true)
}, 300)
setVersions({
importedVersion: imported_dsl_version ?? '',
systemVersion: current_dsl_version ?? '',
})
setImportId(id)
}
else {
setLoading(false)
notify({ type: 'error', message: t('workflow.common.importFailure') })
}
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) {
@@ -130,52 +176,119 @@ const UpdateDSLModal = ({
isCreatingRef.current = false
}, [currentFile, fileContent, onCancel, notify, t, eventEmitter, appDetail, onImport])
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>
</>
)
}