FEAT: NEW WORKFLOW ENGINE (#3160)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Yeuoly <admin@srmxy.cn> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: StyleZhang <jasonapring2015@outlook.com> Co-authored-by: jyong <jyong@dify.ai> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: jyong <718720800@qq.com>
This commit is contained in:
@@ -5,22 +5,26 @@ import { useRouter } from 'next/navigation'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import style from '../list.module.css'
|
||||
import AppModeLabel from './AppModeLabel'
|
||||
import s from './style.module.css'
|
||||
import SettingsModal from '@/app/components/app/overview/settings'
|
||||
import type { ConfigParams } from '@/app/components/app/overview/settings'
|
||||
import type { App } from '@/types/app'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { deleteApp, fetchAppDetail, updateAppSiteConfig } from '@/service/apps'
|
||||
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
|
||||
import DuplicateAppModal from '@/app/components/app/duplicate-modal'
|
||||
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppsContext, { useAppContext } from '@/context/app-context'
|
||||
import type { HtmlContentProps } from '@/app/components/base/popover'
|
||||
import CustomPopover from '@/app/components/base/popover'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { AiText, ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
|
||||
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
||||
import EditAppModal from '@/app/components/explore/create-app-modal'
|
||||
import SwitchAppModal from '@/app/components/app/switch-app-modal'
|
||||
|
||||
export type AppCardProps = {
|
||||
app: App
|
||||
@@ -39,12 +43,10 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
state => state.mutateApps,
|
||||
)
|
||||
|
||||
const [showEditModal, setShowEditModal] = useState(false)
|
||||
const [showDuplicateModal, setShowDuplicateModal] = useState(false)
|
||||
const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const [showSettingsModal, setShowSettingsModal] = useState(false)
|
||||
const [detailState, setDetailState] = useState<{
|
||||
loading: boolean
|
||||
detail?: App
|
||||
}>({ loading: false })
|
||||
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
try {
|
||||
@@ -64,51 +66,105 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
setShowConfirmDelete(false)
|
||||
}, [app.id])
|
||||
|
||||
const getAppDetail = async () => {
|
||||
setDetailState({ loading: true })
|
||||
const [err, res] = await asyncRunSafe(
|
||||
fetchAppDetail({ url: '/apps', id: app.id }),
|
||||
)
|
||||
if (!err) {
|
||||
setDetailState({ loading: false, detail: res })
|
||||
setShowSettingsModal(true)
|
||||
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
|
||||
name,
|
||||
icon,
|
||||
icon_background,
|
||||
description,
|
||||
}) => {
|
||||
try {
|
||||
await updateAppInfo({
|
||||
appID: app.id,
|
||||
name,
|
||||
icon,
|
||||
icon_background,
|
||||
description,
|
||||
})
|
||||
setShowEditModal(false)
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('app.editDone'),
|
||||
})
|
||||
if (onRefresh)
|
||||
onRefresh()
|
||||
mutateApps()
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: t('app.editFailed') })
|
||||
}
|
||||
}, [app.id, mutateApps, notify, onRefresh, t])
|
||||
|
||||
const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon, icon_background }) => {
|
||||
try {
|
||||
const newApp = await copyApp({
|
||||
appID: app.id,
|
||||
name,
|
||||
icon,
|
||||
icon_background,
|
||||
mode: app.mode,
|
||||
})
|
||||
setShowDuplicateModal(false)
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('app.newApp.appCreated'),
|
||||
})
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
if (onRefresh)
|
||||
onRefresh()
|
||||
mutateApps()
|
||||
onPlanInfoChanged()
|
||||
getRedirection(isCurrentWorkspaceManager, newApp, push)
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
}
|
||||
else { setDetailState({ loading: false }) }
|
||||
}
|
||||
|
||||
const onSaveSiteConfig = useCallback(
|
||||
async (params: ConfigParams) => {
|
||||
const [err] = await asyncRunSafe(
|
||||
updateAppSiteConfig({
|
||||
url: `/apps/${app.id}/site`,
|
||||
body: params,
|
||||
}),
|
||||
)
|
||||
if (!err) {
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('common.actionMsg.modifiedSuccessfully'),
|
||||
})
|
||||
if (onRefresh)
|
||||
onRefresh()
|
||||
mutateApps()
|
||||
}
|
||||
else {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: t('common.actionMsg.modifiedUnsuccessfully'),
|
||||
})
|
||||
}
|
||||
},
|
||||
[app.id],
|
||||
)
|
||||
const onExport = async () => {
|
||||
try {
|
||||
const { data } = await exportAppConfig(app.id)
|
||||
const a = document.createElement('a')
|
||||
const file = new Blob([data], { type: 'application/yaml' })
|
||||
a.href = URL.createObjectURL(file)
|
||||
a.download = `${app.name}.yml`
|
||||
a.click()
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: t('app.exportFailed') })
|
||||
}
|
||||
}
|
||||
|
||||
const onSwitch = () => {
|
||||
if (onRefresh)
|
||||
onRefresh()
|
||||
mutateApps()
|
||||
setShowSwitchModal(false)
|
||||
}
|
||||
|
||||
const Operations = (props: HtmlContentProps) => {
|
||||
const onClickSettings = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
await getAppDetail()
|
||||
setShowEditModal(true)
|
||||
}
|
||||
const onClickDuplicate = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
setShowDuplicateModal(true)
|
||||
}
|
||||
const onClickExport = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
onExport()
|
||||
}
|
||||
const onClickSwitch = async (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
props.onClick?.()
|
||||
e.preventDefault()
|
||||
setShowSwitchModal(true)
|
||||
}
|
||||
const onClickDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
@@ -117,11 +173,28 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
setShowConfirmDelete(true)
|
||||
}
|
||||
return (
|
||||
<div className="w-full py-1">
|
||||
<button className={s.actionItem} onClick={onClickSettings} disabled={detailState.loading}>
|
||||
<span className={s.actionName}>{t('common.operation.settings')}</span>
|
||||
<div className="relative w-full py-1">
|
||||
<button className={s.actionItem} onClick={onClickSettings}>
|
||||
<span className={s.actionName}>{t('app.editApp')}</span>
|
||||
</button>
|
||||
|
||||
<Divider className="!my-1" />
|
||||
<button className={s.actionItem} onClick={onClickDuplicate}>
|
||||
<span className={s.actionName}>{t('app.duplicate')}</span>
|
||||
</button>
|
||||
<button className={s.actionItem} onClick={onClickExport}>
|
||||
<span className={s.actionName}>{t('app.export')}</span>
|
||||
</button>
|
||||
{(app.mode === 'completion' || app.mode === 'chat') && (
|
||||
<>
|
||||
<Divider className="!my-1" />
|
||||
<div
|
||||
className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer'
|
||||
onClick={onClickSwitch}
|
||||
>
|
||||
<span className='text-gray-700 text-sm leading-5'>{t('app.switch')}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Divider className="!my-1" />
|
||||
<div
|
||||
className={cn(s.actionItem, s.deleteActionItem, 'group')}
|
||||
@@ -139,22 +212,47 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
<>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
if (showSettingsModal)
|
||||
return
|
||||
e.preventDefault()
|
||||
|
||||
push(`/app/${app.id}/${isCurrentWorkspaceManager ? 'configuration' : 'overview'}`)
|
||||
getRedirection(isCurrentWorkspaceManager, app, push)
|
||||
}}
|
||||
className={style.listItem}
|
||||
className='group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
|
||||
>
|
||||
<div className={style.listItemTitle}>
|
||||
<AppIcon
|
||||
size="small"
|
||||
icon={app.icon}
|
||||
background={app.icon_background}
|
||||
/>
|
||||
<div className={style.listItemHeading}>
|
||||
<div className={style.listItemHeadingContent}>{app.name}</div>
|
||||
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
|
||||
<div className='relative shrink-0'>
|
||||
<AppIcon
|
||||
size="large"
|
||||
icon={app.icon}
|
||||
background={app.icon_background}
|
||||
/>
|
||||
<span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'>
|
||||
{app.mode === 'advanced-chat' && (
|
||||
<ChatBot className='w-3 h-3 text-[#1570EF]' />
|
||||
)}
|
||||
{app.mode === 'agent-chat' && (
|
||||
<CuteRobote className='w-3 h-3 text-indigo-600' />
|
||||
)}
|
||||
{app.mode === 'chat' && (
|
||||
<ChatBot className='w-3 h-3 text-[#1570EF]' />
|
||||
)}
|
||||
{app.mode === 'completion' && (
|
||||
<AiText className='w-3 h-3 text-[#0E9384]' />
|
||||
)}
|
||||
{app.mode === 'workflow' && (
|
||||
<Route className='w-3 h-3 text-[#f79009]' />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className='grow w-0 py-[1px]'>
|
||||
<div className='flex items-center text-sm leading-5 font-semibold text-gray-800'>
|
||||
<div className='truncate' title={app.name}>{app.name}</div>
|
||||
</div>
|
||||
<div className='flex items-center text-[10px] leading-[18px] text-gray-500 font-medium'>
|
||||
{app.mode === 'advanced-chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
|
||||
{app.mode === 'chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
|
||||
{app.mode === 'agent-chat' && <div className='truncate'>{t('app.types.agent').toUpperCase()}</div>}
|
||||
{app.mode === 'workflow' && <div className='truncate'>{t('app.types.workflow').toUpperCase()}</div>}
|
||||
{app.mode === 'completion' && <div className='truncate'>{t('app.types.completion').toUpperCase()}</div>}
|
||||
</div>
|
||||
</div>
|
||||
{isCurrentWorkspaceManager && <CustomPopover
|
||||
htmlContent={<Operations />}
|
||||
@@ -164,20 +262,49 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
btnClassName={open =>
|
||||
cn(
|
||||
open ? '!bg-gray-100 !shadow-none' : '!bg-transparent',
|
||||
style.actionIconWrapper,
|
||||
'!hidden h-8 w-8 !p-2 rounded-md border-none hover:!bg-gray-100 group-hover:!inline-flex',
|
||||
)
|
||||
}
|
||||
className={'!w-[128px] h-fit !z-0'}
|
||||
className={'!w-[128px] h-fit !z-20'}
|
||||
popupClassName={
|
||||
(app.mode === 'completion' || app.mode === 'chat')
|
||||
? '!w-[238px] translate-x-[-110px]'
|
||||
: ''
|
||||
}
|
||||
manualClose
|
||||
/>}
|
||||
</div>
|
||||
<div className={style.listItemDescription}>
|
||||
{app.model_config?.pre_prompt}
|
||||
</div>
|
||||
<div className={style.listItemFooter}>
|
||||
<AppModeLabel mode={app.mode} isAgent={app.is_agent} />
|
||||
</div>
|
||||
|
||||
<div className='mb-1 px-[14px] text-xs leading-normal text-gray-500 line-clamp-4'>{app.description}</div>
|
||||
{showEditModal && (
|
||||
<EditAppModal
|
||||
isEditModal
|
||||
appIcon={app.icon}
|
||||
appIconBackground={app.icon_background}
|
||||
appName={app.name}
|
||||
appDescription={app.description}
|
||||
show={showEditModal}
|
||||
onConfirm={onEdit}
|
||||
onHide={() => setShowEditModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showDuplicateModal && (
|
||||
<DuplicateAppModal
|
||||
appName={app.name}
|
||||
icon={app.icon}
|
||||
icon_background={app.icon_background}
|
||||
show={showDuplicateModal}
|
||||
onConfirm={onCopy}
|
||||
onHide={() => setShowDuplicateModal(false)}
|
||||
/>
|
||||
)}
|
||||
{showSwitchModal && (
|
||||
<SwitchAppModal
|
||||
show={showSwitchModal}
|
||||
appDetail={app}
|
||||
onClose={() => setShowSwitchModal(false)}
|
||||
onSuccess={onSwitch}
|
||||
/>
|
||||
)}
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('app.deleteAppConfirmTitle')}
|
||||
@@ -188,14 +315,6 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
/>
|
||||
)}
|
||||
{showSettingsModal && detailState.detail && (
|
||||
<SettingsModal
|
||||
appInfo={detailState.detail}
|
||||
isShow={showSettingsModal}
|
||||
onClose={() => setShowSettingsModal(false)}
|
||||
onSave={onSaveSiteConfig}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { type AppMode } from '@/types/app'
|
||||
import {
|
||||
AiText,
|
||||
CuteRobote,
|
||||
} from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import { BubbleText } from '@/app/components/base/icons/src/vender/solid/education'
|
||||
|
||||
export type AppModeLabelProps = {
|
||||
mode: AppMode
|
||||
isAgent?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AppModeLabel = ({
|
||||
mode,
|
||||
isAgent,
|
||||
className,
|
||||
}: AppModeLabelProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={`inline-flex items-center px-2 h-6 rounded-md border border-gray-100 text-xs text-gray-500 ${className}`}>
|
||||
{
|
||||
mode === 'completion' && (
|
||||
<>
|
||||
<AiText className='mr-1 w-3 h-3 text-gray-400' />
|
||||
{t('app.newApp.completeApp')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
mode === 'chat' && !isAgent && (
|
||||
<>
|
||||
<BubbleText className='mr-1 w-3 h-3 text-gray-400' />
|
||||
{t('appDebug.assistantType.chatAssistant.name')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
mode === 'chat' && isAgent && (
|
||||
<>
|
||||
<CuteRobote className='mr-1 w-3 h-3 text-gray-400' />
|
||||
{t('appDebug.assistantType.agentAssistant.name')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppModeLabel
|
||||
@@ -11,10 +11,16 @@ import { fetchAppList } from '@/service/apps'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { CheckModal } from '@/hooks/use-pay'
|
||||
import TabSliderNew from '@/app/components/base/tab-slider-new'
|
||||
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
|
||||
import TabSlider from '@/app/components/base/tab-slider'
|
||||
import { SearchLg } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { DotsGrid, SearchLg } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import {
|
||||
// AiText,
|
||||
ChatBot,
|
||||
CuteRobot,
|
||||
} from '@/app/components/base/icons/src/vender/line/communication'
|
||||
import { Route } from '@/app/components/base/icons/src/vender/line/mapsAndTravel'
|
||||
|
||||
const getKey = (
|
||||
pageIndex: number,
|
||||
@@ -27,6 +33,8 @@ const getKey = (
|
||||
|
||||
if (activeTab !== 'all')
|
||||
params.params.mode = activeTab
|
||||
else
|
||||
delete params.params.mode
|
||||
|
||||
return params
|
||||
}
|
||||
@@ -45,14 +53,16 @@ const Apps = () => {
|
||||
const { data, isLoading, setSize, mutate } = useSWRInfinite(
|
||||
(pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, searchKeywords),
|
||||
fetchAppList,
|
||||
{ revalidateFirstPage: false },
|
||||
{ revalidateFirstPage: true },
|
||||
)
|
||||
|
||||
const anchorRef = useRef<HTMLDivElement>(null)
|
||||
const options = [
|
||||
{ value: 'all', text: t('app.types.all') },
|
||||
{ value: 'chat', text: t('app.types.assistant') },
|
||||
{ value: 'completion', text: t('app.types.completion') },
|
||||
{ value: 'all', text: t('app.types.all'), icon: <DotsGrid className='w-[14px] h-[14px] mr-1'/> },
|
||||
{ value: 'chat', text: t('app.types.chatbot'), icon: <ChatBot className='w-[14px] h-[14px] mr-1'/> },
|
||||
{ value: 'agent-chat', text: t('app.types.agent'), icon: <CuteRobot className='w-[14px] h-[14px] mr-1'/> },
|
||||
// { value: 'completion', text: t('app.newApp.completeApp'), icon: <AiText className='w-[14px] h-[14px] mr-1'/> },
|
||||
{ value: 'workflow', text: t('app.types.workflow'), icon: <Route className='w-[14px] h-[14px] mr-1'/> },
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
@@ -61,7 +71,7 @@ const Apps = () => {
|
||||
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
|
||||
mutate()
|
||||
}
|
||||
}, [mutate, t])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let observer: IntersectionObserver | undefined
|
||||
@@ -91,6 +101,11 @@ const Apps = () => {
|
||||
return (
|
||||
<>
|
||||
<div className='sticky top-0 flex justify-between items-center pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-10 flex-wrap gap-y-2'>
|
||||
<TabSliderNew
|
||||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
options={options}
|
||||
/>
|
||||
<div className="flex items-center px-2 w-[200px] h-8 rounded-lg bg-gray-200">
|
||||
<div className="pointer-events-none shrink-0 flex items-center mr-1.5 justify-center w-4 h-4">
|
||||
<SearchLg className="h-3.5 w-3.5 text-gray-500" aria-hidden="true" />
|
||||
@@ -117,12 +132,6 @@ const Apps = () => {
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<TabSlider
|
||||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
options={options}
|
||||
/>
|
||||
|
||||
</div>
|
||||
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
|
||||
{isCurrentWorkspaceManager
|
||||
|
||||
@@ -1,38 +1,77 @@
|
||||
'use client'
|
||||
|
||||
import { forwardRef, useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import style from '../list.module.css'
|
||||
import NewAppDialog from './NewAppDialog'
|
||||
import CreateAppTemplateDialog from '@/app/components/app/create-app-dialog'
|
||||
import CreateAppModal from '@/app/components/app/create-app-modal'
|
||||
import CreateFromDSLModal from '@/app/components/app/create-from-dsl-modal'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
|
||||
export type CreateAppCardProps = {
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
const CreateAppCard = forwardRef<HTMLAnchorElement, CreateAppCardProps>(({ onSuccess }, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { onPlanInfoChanged } = useProviderContext()
|
||||
|
||||
const [showNewAppDialog, setShowNewAppDialog] = useState(false)
|
||||
const [showNewAppTemplateDialog, setShowNewAppTemplateDialog] = useState(false)
|
||||
const [showNewAppModal, setShowNewAppModal] = useState(false)
|
||||
const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false)
|
||||
return (
|
||||
<a ref={ref} className={classNames(style.listItem, style.newItemCard)} onClick={() => setShowNewAppDialog(true)}>
|
||||
<div className={style.listItemTitle}>
|
||||
<span className={style.newItemIcon}>
|
||||
<span className={classNames(style.newItemIconImage, style.newItemIconAdd)} />
|
||||
</span>
|
||||
<div className={classNames(style.listItemHeading, style.newItemCardHeading)}>
|
||||
{t('app.createApp')}
|
||||
<a
|
||||
ref={ref}
|
||||
className='relative col-span-1 flex flex-col justify-between min-h-[160px] bg-gray-200 rounded-xl border-[0.5px] border-black/5'
|
||||
>
|
||||
<div className='grow p-2 rounded-t-xl'>
|
||||
<div className='px-6 pt-2 pb-1 text-xs font-medium leading-[18px] text-gray-500'>{t('app.createApp')}</div>
|
||||
<div className='flex items-center mb-1 px-6 py-[7px] rounded-lg text-[13px] font-medium leading-[18px] text-gray-600 cursor-pointer hover:text-primary-600 hover:bg-white' onClick={() => setShowNewAppModal(true)}>
|
||||
<FilePlus01 className='shrink-0 mr-2 w-4 h-4' />
|
||||
{t('app.newApp.startFromBlank')}
|
||||
</div>
|
||||
<div className='flex items-center px-6 py-[7px] rounded-lg text-[13px] font-medium leading-[18px] text-gray-600 cursor-pointer hover:text-primary-600 hover:bg-white' onClick={() => setShowNewAppTemplateDialog(true)}>
|
||||
<FilePlus02 className='shrink-0 mr-2 w-4 h-4' />
|
||||
{t('app.newApp.startFromTemplate')}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className='text-xs text-gray-500'>{t('app.createFromConfigFile')}</div> */}
|
||||
<NewAppDialog show={showNewAppDialog} onSuccess={
|
||||
() => {
|
||||
<div
|
||||
className='p-2 border-t-[0.5px] border-black/5 rounded-b-xl'
|
||||
onClick={() => setShowCreateFromDSLModal(true)}
|
||||
>
|
||||
<div className='flex items-center px-6 py-[7px] rounded-lg text-[13px] font-medium leading-[18px] text-gray-600 cursor-pointer hover:text-primary-600 hover:bg-white'>
|
||||
<FileArrow01 className='shrink-0 mr-2 w-4 h-4' />
|
||||
{t('app.importDSL')}
|
||||
</div>
|
||||
</div>
|
||||
<CreateAppModal
|
||||
show={showNewAppModal}
|
||||
onClose={() => setShowNewAppModal(false)}
|
||||
onSuccess={() => {
|
||||
onPlanInfoChanged()
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
}} onClose={() => setShowNewAppDialog(false)} />
|
||||
}}
|
||||
/>
|
||||
<CreateAppTemplateDialog
|
||||
show={showNewAppTemplateDialog}
|
||||
onClose={() => setShowNewAppTemplateDialog(false)}
|
||||
onSuccess={() => {
|
||||
onPlanInfoChanged()
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
}}
|
||||
/>
|
||||
<CreateFromDSLModal
|
||||
show={showCreateFromDSLModal}
|
||||
onClose={() => setShowCreateFromDSLModal(false)}
|
||||
onSuccess={() => {
|
||||
onPlanInfoChanged()
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { MouseEventHandler } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import classNames from 'classnames'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import style from '../list.module.css'
|
||||
import AppModeLabel from './AppModeLabel'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Dialog from '@/app/components/base/dialog'
|
||||
import type { AppMode } from '@/types/app'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { createApp, fetchAppTemplates } from '@/service/apps'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppsContext, { useAppContext } from '@/context/app-context'
|
||||
import EmojiPicker from '@/app/components/base/emoji-picker'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
||||
import { AiText } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
|
||||
type NewAppDialogProps = {
|
||||
show: boolean
|
||||
onSuccess?: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
|
||||
const router = useRouter()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const nameInputRef = useRef<HTMLInputElement>(null)
|
||||
const [newAppMode, setNewAppMode] = useState<AppMode>()
|
||||
const [isWithTemplate, setIsWithTemplate] = useState(false)
|
||||
const [selectedTemplateIndex, setSelectedTemplateIndex] = useState<number>(-1)
|
||||
|
||||
// Emoji Picker
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
||||
const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })
|
||||
|
||||
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
|
||||
|
||||
const { data: templates, mutate } = useSWR({ url: '/app-templates' }, fetchAppTemplates)
|
||||
const mutateTemplates = useCallback(
|
||||
() => mutate(),
|
||||
[],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
mutateTemplates()
|
||||
setIsWithTemplate(false)
|
||||
}
|
||||
}, [mutateTemplates, show])
|
||||
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
|
||||
|
||||
const isCreatingRef = useRef(false)
|
||||
const onCreate: MouseEventHandler = useCallback(async () => {
|
||||
const name = nameInputRef.current?.value
|
||||
if (!name) {
|
||||
notify({ type: 'error', message: t('app.newApp.nameNotEmpty') })
|
||||
return
|
||||
}
|
||||
if (!templates || (isWithTemplate && !(selectedTemplateIndex > -1))) {
|
||||
notify({ type: 'error', message: t('app.newApp.appTemplateNotSelected') })
|
||||
return
|
||||
}
|
||||
if (!isWithTemplate && !newAppMode) {
|
||||
notify({ type: 'error', message: t('app.newApp.appTypeRequired') })
|
||||
return
|
||||
}
|
||||
if (isCreatingRef.current)
|
||||
return
|
||||
isCreatingRef.current = true
|
||||
try {
|
||||
const app = await createApp({
|
||||
name,
|
||||
icon: emoji.icon,
|
||||
icon_background: emoji.icon_background,
|
||||
mode: isWithTemplate ? templates.data[selectedTemplateIndex].mode : newAppMode!,
|
||||
config: isWithTemplate ? templates.data[selectedTemplateIndex].model_config : undefined,
|
||||
})
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
if (onClose)
|
||||
onClose()
|
||||
notify({ type: 'success', message: t('app.newApp.appCreated') })
|
||||
mutateApps()
|
||||
router.push(`/app/${app.id}/${isCurrentWorkspaceManager ? 'configuration' : 'overview'}`)
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
}
|
||||
isCreatingRef.current = false
|
||||
}, [isWithTemplate, newAppMode, notify, router, templates, selectedTemplateIndex, emoji])
|
||||
|
||||
return <>
|
||||
{showEmojiPicker && <EmojiPicker
|
||||
onSelect={(icon, icon_background) => {
|
||||
setEmoji({ icon, icon_background })
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setEmoji({ icon: '🤖', icon_background: '#FFEAD5' })
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
/>}
|
||||
<Dialog
|
||||
show={show}
|
||||
title={t('app.newApp.startToCreate')}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button disabled={isAppsFull} type="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className='overflow-y-auto'>
|
||||
<div className={style.newItemCaption}>
|
||||
<h3 className='inline'>{t('app.newApp.captionAppType')}</h3>
|
||||
{isWithTemplate && (
|
||||
<>
|
||||
<span className='block ml-[9px] mr-[9px] w-[1px] h-[13px] bg-gray-200' />
|
||||
<span
|
||||
className='inline-flex items-center gap-1 text-xs font-medium cursor-pointer text-primary-600'
|
||||
onClick={() => setIsWithTemplate(false)}
|
||||
>
|
||||
{t('app.newApp.hideTemplates')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isWithTemplate && (
|
||||
(
|
||||
<>
|
||||
<ul className='grid grid-cols-1 md:grid-cols-2 gap-4'>
|
||||
<li
|
||||
className={classNames(style.listItem, style.selectable, newAppMode === 'chat' && style.selected)}
|
||||
onClick={() => setNewAppMode('chat')}
|
||||
>
|
||||
<div className={style.listItemTitle}>
|
||||
<span className={style.newItemIcon}>
|
||||
<span className={classNames(style.newItemIconImage, style.newItemIconChat)} />
|
||||
</span>
|
||||
<div className={style.listItemHeading}>
|
||||
<div className={style.listItemHeadingContent}>{t('app.newApp.chatApp')}</div>
|
||||
</div>
|
||||
<div className='flex items-center h-[18px] border border-indigo-300 px-1 rounded-[5px] text-xs font-medium text-indigo-600 uppercase truncate'>{t('app.newApp.agentAssistant')}</div>
|
||||
</div>
|
||||
<div className={`${style.listItemDescription} ${style.noClip}`}>{t('app.newApp.chatAppIntro')}</div>
|
||||
{/* <div className={classNames(style.listItemFooter, 'justify-end')}>
|
||||
<a className={style.listItemLink} href='https://udify.app/chat/7CQBa5yyvYLSkZtx' target='_blank' rel='noopener noreferrer'>{t('app.newApp.previewDemo')}<span className={classNames(style.linkIcon, style.grayLinkIcon)} /></a>
|
||||
</div> */}
|
||||
</li>
|
||||
<li
|
||||
className={classNames(style.listItem, style.selectable, newAppMode === 'completion' && style.selected)}
|
||||
onClick={() => setNewAppMode('completion')}
|
||||
>
|
||||
<div className={style.listItemTitle}>
|
||||
<span className={style.newItemIcon}>
|
||||
{/* <span className={classNames(style.newItemIconImage, style.newItemIconComplete)} /> */}
|
||||
<AiText className={classNames('w-5 h-5', newAppMode === 'completion' ? 'text-[#155EEF]' : 'text-gray-700')} />
|
||||
</span>
|
||||
<div className={style.listItemHeading}>
|
||||
<div className={style.listItemHeadingContent}>{t('app.newApp.completeApp')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${style.listItemDescription} ${style.noClip}`}>{t('app.newApp.completeAppIntro')}</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</>
|
||||
)
|
||||
)}
|
||||
|
||||
{isWithTemplate && (
|
||||
<ul className='grid grid-cols-1 md:grid-cols-2 gap-4'>
|
||||
{templates?.data?.map((template, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={classNames(style.listItem, style.selectable, selectedTemplateIndex === index && style.selected)}
|
||||
onClick={() => setSelectedTemplateIndex(index)}
|
||||
>
|
||||
<div className={style.listItemTitle}>
|
||||
<AppIcon size='small' />
|
||||
<div className={style.listItemHeading}>
|
||||
<div className={style.listItemHeadingContent}>{template.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.listItemDescription}>{template.model_config?.pre_prompt}</div>
|
||||
<div className='inline-block pl-3.5'>
|
||||
<AppModeLabel mode={template.mode} isAgent={template.model_config.agent_mode.enabled} className='mt-2' />
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<div className='mt-8'>
|
||||
<h3 className={style.newItemCaption}>{t('app.newApp.captionName')}</h3>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} />
|
||||
<input ref={nameInputRef} className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' placeholder={t('app.appNamePlaceholder') || ''} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
!isWithTemplate && (
|
||||
<div className='flex items-center h-[34px] mt-2'>
|
||||
<span
|
||||
className='inline-flex items-center gap-1 text-xs font-medium cursor-pointer text-primary-600'
|
||||
onClick={() => setIsWithTemplate(true)}
|
||||
>
|
||||
{t('app.newApp.showTemplates')}<span className={style.rightIcon} />
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
{isAppsFull && <AppsFull loc='app-create' />}
|
||||
</Dialog>
|
||||
</>
|
||||
}
|
||||
|
||||
export default NewAppDialog
|
||||
@@ -10,7 +10,7 @@
|
||||
mask-image: url(~@/assets/action.svg);
|
||||
}
|
||||
.actionItem {
|
||||
@apply h-9 py-2 px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer;
|
||||
@apply h-8 py-[6px] px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer;
|
||||
width: calc(100% - 0.5rem);
|
||||
}
|
||||
.deleteActionItem {
|
||||
@@ -19,3 +19,11 @@
|
||||
.actionName {
|
||||
@apply text-gray-700 text-sm;
|
||||
}
|
||||
|
||||
/* .completionPic {
|
||||
background-image: url(~@/app/components/app-sidebar/completion.png)
|
||||
}
|
||||
|
||||
.expertPic {
|
||||
background-image: url(~@/app/components/app-sidebar/expert.png)
|
||||
} */
|
||||
|
||||
Reference in New Issue
Block a user