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:
takatost
2024-04-08 18:51:46 +08:00
committed by GitHub
parent 2fb9850af5
commit 7753ba2d37
1161 changed files with 103836 additions and 10327 deletions

View File

@@ -6,11 +6,9 @@ export type IProps = {
params: { appId: string }
}
const Logs = async ({
params: { appId },
}: IProps) => {
const Logs = async () => {
return (
<Main pageType={PageType.annotation} appId={appId} />
<Main pageType={PageType.annotation} />
)
}

View File

@@ -1,25 +1,20 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useMemo } from 'react'
import { useUnmount } from 'ahooks'
import React, { useCallback, useEffect, useState } from 'react'
import { usePathname, useRouter } from 'next/navigation'
import cn from 'classnames'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import {
ChartBarSquareIcon,
Cog8ToothIcon,
CommandLineIcon,
DocumentTextIcon,
} from '@heroicons/react/24/outline'
import {
ChartBarSquareIcon as ChartBarSquareSolidIcon,
Cog8ToothIcon as Cog8ToothSolidIcon,
CommandLineIcon as CommandLineSolidIcon,
DocumentTextIcon as DocumentTextSolidIcon,
} from '@heroicons/react/24/solid'
import s from './style.module.css'
import { useStore } from '@/app/components/app/store'
import AppSideBar from '@/app/components/app-sidebar'
import type { NavIcon } from '@/app/components/app-sidebar/navLink'
import { fetchAppDetail } from '@/service/apps'
import { useAppContext } from '@/context/app-context'
import Loading from '@/app/components/base/loading'
import { BarChartSquare02, FileHeart02, PromptEngineering, TerminalSquare } from '@/app/components/base/icons/src/vender/line/development'
import { BarChartSquare02 as BarChartSquare02Solid, FileHeart02 as FileHeart02Solid, PromptEngineering as PromptEngineeringSolid, TerminalSquare as TerminalSquareSolid } from '@/app/components/base/icons/src/vender/solid/development'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
export type IAppDetailLayoutProps = {
children: React.ReactNode
@@ -32,40 +27,103 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
params: { appId }, // get appId in path
} = props
const { t } = useTranslation()
const router = useRouter()
const pathname = usePathname()
const media = useBreakpoints()
const isMobile = media === MediaType.mobile
const { isCurrentWorkspaceManager } = useAppContext()
const detailParams = { url: '/apps', id: appId }
const { data: response } = useSWR(detailParams, fetchAppDetail)
const { appDetail, setAppDetail, setAppSiderbarExpand } = useStore()
const [navigation, setNavigation] = useState<Array<{
name: string
href: string
icon: NavIcon
selectedIcon: NavIcon
}>>([])
const navigation = useMemo(() => {
const getNavigations = useCallback((appId: string, isCurrentWorkspaceManager: boolean, mode: string) => {
const navs = [
...(isCurrentWorkspaceManager ? [{ name: t('common.appMenus.promptEng'), href: `/app/${appId}/configuration`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon }] : []),
{ name: t('common.appMenus.overview'), href: `/app/${appId}/overview`, icon: ChartBarSquareIcon, selectedIcon: ChartBarSquareSolidIcon },
{ name: t('common.appMenus.apiAccess'), href: `/app/${appId}/develop`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
{ name: t('common.appMenus.logAndAnn'), href: `/app/${appId}/logs`, icon: DocumentTextIcon, selectedIcon: DocumentTextSolidIcon },
...(isCurrentWorkspaceManager
? [{
name: t('common.appMenus.promptEng'),
href: `/app/${appId}/${(mode === 'workflow' || mode === 'advanced-chat') ? 'workflow' : 'configuration'}`,
icon: PromptEngineering,
selectedIcon: PromptEngineeringSolid,
}]
: []
),
{
name: t('common.appMenus.apiAccess'),
href: `/app/${appId}/develop`,
icon: TerminalSquare,
selectedIcon: TerminalSquareSolid,
},
{
name: mode !== 'workflow'
? t('common.appMenus.logAndAnn')
: t('common.appMenus.logs'),
href: `/app/${appId}/logs`,
icon: FileHeart02,
selectedIcon: FileHeart02Solid,
},
{
name: t('common.appMenus.overview'),
href: `/app/${appId}/overview`,
icon: BarChartSquare02,
selectedIcon: BarChartSquare02Solid,
},
]
return navs
}, [appId, isCurrentWorkspaceManager, t])
}, [t])
const appModeName = (() => {
if (response?.mode?.toUpperCase() === 'COMPLETION')
return t('app.newApp.completeApp')
const isAgent = !!response?.is_agent
if (isAgent)
return t('appDebug.assistantType.agentAssistant.name')
return t('appDebug.assistantType.chatAssistant.name')
})()
useEffect(() => {
if (response?.name)
document.title = `${(response.name || 'App')} - Dify`
}, [response])
if (!response)
return null
if (appDetail) {
document.title = `${(appDetail.name || 'App')} - Dify`
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
const mode = isMobile ? 'collapse' : 'expand'
setAppSiderbarExpand(isMobile ? mode : localeMode)
// TODO: consider screen size and mode
// if ((appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (pathname).endsWith('workflow'))
// setAppSiderbarExpand('collapse')
}
}, [appDetail, isMobile])
useEffect(() => {
setAppDetail()
fetchAppDetail({ url: '/apps', id: appId }).then((res) => {
// redirections
if ((res.mode === 'workflow' || res.mode === 'advanced-chat') && (pathname).endsWith('configuration')) {
router.replace(`/app/${appId}/workflow`)
}
else if ((res.mode !== 'workflow' && res.mode !== 'advanced-chat') && (pathname).endsWith('workflow')) {
router.replace(`/app/${appId}/configuration`)
}
else {
setAppDetail(res)
setNavigation(getNavigations(appId, isCurrentWorkspaceManager, res.mode))
}
})
}, [appId, isCurrentWorkspaceManager])
useUnmount(() => {
setAppDetail()
})
if (!appDetail) {
return (
<div className='flex h-full items-center justify-center bg-white'>
<Loading />
</div>
)
}
return (
<div className={cn(s.app, 'flex', 'overflow-hidden')}>
<AppSideBar title={response.name} icon={response.icon} icon_background={response.icon_background} desc={appModeName} navigation={navigation} />
<div className="bg-white grow overflow-hidden">{children}</div>
{appDetail && (
<AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background} desc={appDetail.mode} navigation={navigation} />
)}
<div className="bg-white grow overflow-hidden">
{children}
</div>
</div>
)
}

View File

@@ -2,15 +2,9 @@ import React from 'react'
import Main from '@/app/components/app/log-annotation'
import { PageType } from '@/app/components/app/configuration/toolbox/annotation/type'
export type IProps = {
params: { appId: string }
}
const Logs = async ({
params: { appId },
}: IProps) => {
const Logs = async () => {
return (
<Main pageType={PageType.log} appId={appId} />
<Main pageType={PageType.log} />
)
}

View File

@@ -3,7 +3,6 @@ import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import useSWR, { useSWRConfig } from 'swr'
import AppCard from '@/app/components/app/overview/appCard'
import Loading from '@/app/components/base/loading'
import { ToastContext } from '@/app/components/base/toast'
@@ -18,20 +17,22 @@ import type { UpdateAppSiteCodeResponse } from '@/models/app'
import { asyncRunSafe } from '@/utils'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import type { IAppCardProps } from '@/app/components/app/overview/appCard'
import { useStore as useAppStore } from '@/app/components/app/store'
export type ICardViewProps = {
appId: string
}
const CardView: FC<ICardViewProps> = ({ appId }) => {
const detailParams = { url: '/apps', id: appId }
const { data: response } = useSWR(detailParams, fetchAppDetail)
const { mutate } = useSWRConfig()
const { notify } = useContext(ToastContext)
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const { appDetail, setAppDetail } = useAppStore()
if (!response)
return <Loading />
const updateAppDetail = async () => {
fetchAppDetail({ url: '/apps', id: appId }).then((res) => {
setAppDetail(res)
})
}
const handleCallbackResult = (err: Error | null, message?: string) => {
const type = err ? 'error' : 'success'
@@ -39,7 +40,7 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
message ||= (type === 'success' ? 'modifiedSuccessfully' : 'modifiedUnsuccessfully')
if (type === 'success')
mutate(detailParams)
updateAppDetail()
notify({
type,
@@ -92,10 +93,13 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
handleCallbackResult(err, err ? 'generatedUnsuccessfully' : 'generatedSuccessfully')
}
if (!appDetail)
return <Loading />
return (
<div className="grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6">
<AppCard
appInfo={response}
appInfo={appDetail}
cardType="webapp"
onChangeStatus={onChangeSiteStatus}
onGenerateCode={onGenerateCode}
@@ -103,7 +107,7 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
/>
<AppCard
cardType="api"
appInfo={response}
appInfo={appDetail}
onChangeStatus={onChangeApiStatus}
/>
</div>

View File

@@ -3,13 +3,12 @@ import React, { useState } from 'react'
import dayjs from 'dayjs'
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { fetchAppDetail } from '@/service/apps'
import type { PeriodParams } from '@/app/components/app/overview/appChart'
import { AvgResponseTime, AvgSessionInteractions, ConversationsChart, CostChart, EndUsersChart, TokenPerSecond, UserSatisfactionRate } from '@/app/components/app/overview/appChart'
import { AvgResponseTime, AvgSessionInteractions, AvgUserInteractions, ConversationsChart, CostChart, EndUsersChart, TokenPerSecond, UserSatisfactionRate, WorkflowCostChart, WorkflowDailyTerminalsChart, WorkflowMessagesChart } from '@/app/components/app/overview/appChart'
import type { Item } from '@/app/components/base/select'
import { SimpleSelect } from '@/app/components/base/select'
import { TIME_PERIOD_LIST } from '@/app/components/app/log/filter'
import { useStore as useAppStore } from '@/app/components/app/store'
dayjs.extend(quarterOfYear)
@@ -22,10 +21,10 @@ export type IChartViewProps = {
}
export default function ChartView({ appId }: IChartViewProps) {
const detailParams = { url: '/apps', id: appId }
const { data: response } = useSWR(detailParams, fetchAppDetail)
const isChatApp = response?.mode === 'chat'
const { t } = useTranslation()
const { appDetail } = useAppStore()
const isChatApp = appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow'
const isWorkflow = appDetail?.mode === 'workflow'
const [period, setPeriod] = useState<PeriodParams>({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } })
const onSelect = (item: Item) => {
@@ -42,7 +41,7 @@ export default function ChartView({ appId }: IChartViewProps) {
}
}
if (!response)
if (!appDetail)
return null
return (
@@ -56,24 +55,42 @@ export default function ChartView({ appId }: IChartViewProps) {
defaultValue={7}
/>
</div>
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<ConversationsChart period={period} id={appId} />
<EndUsersChart period={period} id={appId} />
</div>
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
{isChatApp
? (
<AvgSessionInteractions period={period} id={appId} />
)
: (
<AvgResponseTime period={period} id={appId} />
)}
<TokenPerSecond period={period} id={appId} />
</div>
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<UserSatisfactionRate period={period} id={appId} />
<CostChart period={period} id={appId} />
</div>
{!isWorkflow && (
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<ConversationsChart period={period} id={appId} />
<EndUsersChart period={period} id={appId} />
</div>
)}
{!isWorkflow && (
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
{isChatApp
? (
<AvgSessionInteractions period={period} id={appId} />
)
: (
<AvgResponseTime period={period} id={appId} />
)}
<TokenPerSecond period={period} id={appId} />
</div>
)}
{!isWorkflow && (
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<UserSatisfactionRate period={period} id={appId} />
<CostChart period={period} id={appId} />
</div>
)}
{isWorkflow && (
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<WorkflowMessagesChart period={period} id={appId} />
<WorkflowDailyTerminalsChart period={period} id={appId} />
</div>
)}
{isWorkflow && (
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<WorkflowCostChart period={period} id={appId} />
<AvgUserInteractions period={period} id={appId} />
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,12 @@
'use client'
import Workflow from '@/app/components/workflow'
const Page = () => {
return (
<div className='w-full h-full overflow-x-auto'>
<Workflow />
</div>
)
}
export default Page

View File

@@ -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>
</>
)

View File

@@ -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

View File

@@ -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

View File

@@ -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>
)
})

View File

@@ -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

View File

@@ -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)
} */

View File

@@ -28,7 +28,6 @@ import type { RelatedApp, RelatedAppResponse } from '@/models/datasets'
import { getLocaleOnClient } from '@/i18n'
import AppSideBar from '@/app/components/app-sidebar'
import Divider from '@/app/components/base/divider'
import Indicator from '@/app/components/header/indicator'
import AppIcon from '@/app/components/base/app-icon'
import Loading from '@/app/components/base/loading'
import FloatPopoverContainer from '@/app/components/base/float-popover-container'
@@ -36,6 +35,9 @@ import DatasetDetailContext from '@/context/dataset-detail'
import { DataSourceType } from '@/models/datasets'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { LanguagesSupported } from '@/i18n/language'
import { useStore } from '@/app/components/app/store'
import { AiText, ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication'
import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
export type IAppDetailLayoutProps = {
children: React.ReactNode
@@ -51,18 +53,31 @@ type ILikedItemProps = {
const LikedItem = ({
type = 'app',
appStatus = true,
detail,
isMobile,
}: ILikedItemProps) => {
return (
<Link className={classNames(s.itemWrapper, 'px-0', isMobile && 'justify-center')} href={`/app/${detail?.id}/overview`}>
<Link className={classNames(s.itemWrapper, 'px-2', isMobile && 'justify-center')} href={`/app/${detail?.id}/overview`}>
<div className={classNames(s.iconWrapper, 'mr-0')}>
<AppIcon size='tiny' icon={detail?.icon} background={detail?.icon_background} />
{type === 'app' && (
<div className={s.statusPoint}>
<Indicator color={appStatus ? 'green' : 'gray'} />
</div>
<span className='absolute bottom-[-2px] right-[-2px] w-3.5 h-3.5 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'>
{detail.mode === 'advanced-chat' && (
<ChatBot className='w-2.5 h-2.5 text-[#1570EF]' />
)}
{detail.mode === 'agent-chat' && (
<CuteRobote className='w-2.5 h-2.5 text-indigo-600' />
)}
{detail.mode === 'chat' && (
<ChatBot className='w-2.5 h-2.5 text-[#1570EF]' />
)}
{detail.mode === 'completion' && (
<AiText className='w-2.5 h-2.5 text-[#0E9384]' />
)}
{detail.mode === 'workflow' && (
<Route className='w-2.5 h-2.5 text-[#f79009]' />
)}
</span>
)}
</div>
{!isMobile && <div className={classNames(s.appInfo, 'ml-2')}>{detail?.name || '--'}</div>}
@@ -116,7 +131,7 @@ const ExtraInfo = ({ isMobile, relatedApps }: IExtraInfoProps) => {
<Divider className='mt-5' />
{(relatedApps?.data && relatedApps?.data?.length > 0) && (
<>
{!isMobile && <div className={s.subTitle}>{relatedApps?.total || '--'} {t('common.datasetMenus.relatedApp')}</div>}
{!isMobile && <div className='w-full px-2 pb-1 pt-4 uppercase text-xs text-gray-500 font-medium'>{relatedApps?.total || '--'} {t('common.datasetMenus.relatedApp')}</div>}
{isMobile && <div className={classNames(s.subTitle, 'flex items-center justify-center !px-0 gap-1')}>
{relatedApps?.total || '--'}
<PaperClipIcon className='h-4 w-4 text-gray-700' />
@@ -136,7 +151,7 @@ const ExtraInfo = ({ isMobile, relatedApps }: IExtraInfoProps) => {
</div>
}
>
<div className={classNames('mt-5 p-3', isMobile && 'border-[0.5px] border-gray-200 shadow-lg rounded-lg bg-white w-[150px]')}>
<div className={classNames('mt-5 p-3', isMobile && 'border-[0.5px] border-gray-200 shadow-lg rounded-lg bg-white w-[160px]')}>
<div className='flex items-center justify-start gap-2'>
<div className={s.emptyIconDiv}>
<Squares2X2Icon className='w-3 h-3 text-gray-500' />
@@ -198,6 +213,14 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
document.title = `${datasetRes.name || 'Dataset'} - Dify`
}, [datasetRes])
const { setAppSiderbarExpand } = useStore()
useEffect(() => {
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
const mode = isMobile ? 'collapse' : 'expand'
setAppSiderbarExpand(isMobile ? mode : localeMode)
}, [isMobile, setAppSiderbarExpand])
if (!datasetRes && !error)
return <Loading />

View File

@@ -5,7 +5,7 @@
@apply truncate text-gray-700 text-sm font-normal;
}
.iconWrapper {
@apply relative w-6 h-6 bg-[#D5F5F6] rounded-md;
@apply relative w-6 h-6 rounded-lg;
}
.statusPoint {
@apply flex justify-center items-center absolute -right-0.5 -bottom-0.5 w-2.5 h-2.5 bg-white rounded;

View File

@@ -10,7 +10,7 @@ import Datasets from './Datasets'
import DatasetFooter from './DatasetFooter'
import ApiServer from './ApiServer'
import Doc from './Doc'
import TabSlider from '@/app/components/base/tab-slider'
import TabSliderNew from '@/app/components/base/tab-slider-new'
// Services
import { fetchDatasetApiBaseUrl } from '@/service/datasets'
@@ -35,7 +35,7 @@ const Container = () => {
return (
<div ref={containerRef} className='grow relative flex flex-col bg-gray-100 overflow-y-auto'>
<div className='sticky top-0 flex justify-between pt-4 px-12 pb-2 leading-[56px] bg-gray-100 z-10 flex-wrap gap-y-2'>
<TabSlider
<TabSliderNew
value={activeTab}
onChange={newActiveTab => setActiveTab(newActiveTab)}
options={options}

View File

@@ -1,5 +1,5 @@
.listItem {
@apply col-span-1 bg-white border-2 border-solid border-transparent rounded-lg shadow-xs min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg;
@apply col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-xs min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg;
}
.listItem.newItemCard {