feat: introduce trigger functionality (#27644)
Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com> Co-authored-by: Stream <Stream_2@qq.com> Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com> Co-authored-by: zhsama <torvalds@linux.do> Co-authored-by: Harry <xh001x@hotmail.com> Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com> Co-authored-by: yessenia <yessenia.contact@gmail.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: WTW0313 <twwu@dify.ai> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -24,7 +24,7 @@ import type { AnnotationReplyConfig } from '@/models/debug'
|
||||
import { sleep } from '@/utils'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
|
||||
import type { App } from '@/types/app'
|
||||
import { type App, AppModeEnum } from '@/types/app'
|
||||
import cn from '@/utils/classnames'
|
||||
import { delAnnotations } from '@/service/annotation'
|
||||
|
||||
@@ -37,7 +37,7 @@ const Annotation: FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const [isShowEdit, setIsShowEdit] = useState(false)
|
||||
const [annotationConfig, setAnnotationConfig] = useState<AnnotationReplyConfig | null>(null)
|
||||
const [isChatApp] = useState(appDetail.mode !== 'completion')
|
||||
const [isChatApp] = useState(appDetail.mode !== AppModeEnum.COMPLETION)
|
||||
const [controlRefreshSwitch, setControlRefreshSwitch] = useState(() => Date.now())
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const isAnnotationFull = enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse
|
||||
|
||||
@@ -22,37 +22,39 @@ const FeaturesWrappedAppPublisher = (props: Props) => {
|
||||
const features = useFeatures(s => s.features)
|
||||
const featuresStore = useFeaturesStore()
|
||||
const [restoreConfirmOpen, setRestoreConfirmOpen] = useState(false)
|
||||
const { more_like_this, opening_statement, suggested_questions, sensitive_word_avoidance, speech_to_text, text_to_speech, suggested_questions_after_answer, retriever_resource, annotation_reply, file_upload, resetAppConfig } = props.publishedConfig.modelConfig
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
props.resetAppConfig?.()
|
||||
resetAppConfig?.()
|
||||
const {
|
||||
features,
|
||||
setFeatures,
|
||||
} = featuresStore!.getState()
|
||||
const newFeatures = produce(features, (draft) => {
|
||||
draft.moreLikeThis = props.publishedConfig.modelConfig.more_like_this || { enabled: false }
|
||||
draft.moreLikeThis = more_like_this || { enabled: false }
|
||||
draft.opening = {
|
||||
enabled: !!props.publishedConfig.modelConfig.opening_statement,
|
||||
opening_statement: props.publishedConfig.modelConfig.opening_statement || '',
|
||||
suggested_questions: props.publishedConfig.modelConfig.suggested_questions || [],
|
||||
enabled: !!opening_statement,
|
||||
opening_statement: opening_statement || '',
|
||||
suggested_questions: suggested_questions || [],
|
||||
}
|
||||
draft.moderation = props.publishedConfig.modelConfig.sensitive_word_avoidance || { enabled: false }
|
||||
draft.speech2text = props.publishedConfig.modelConfig.speech_to_text || { enabled: false }
|
||||
draft.text2speech = props.publishedConfig.modelConfig.text_to_speech || { enabled: false }
|
||||
draft.suggested = props.publishedConfig.modelConfig.suggested_questions_after_answer || { enabled: false }
|
||||
draft.citation = props.publishedConfig.modelConfig.retriever_resource || { enabled: false }
|
||||
draft.annotationReply = props.publishedConfig.modelConfig.annotation_reply || { enabled: false }
|
||||
draft.moderation = sensitive_word_avoidance || { enabled: false }
|
||||
draft.speech2text = speech_to_text || { enabled: false }
|
||||
draft.text2speech = text_to_speech || { enabled: false }
|
||||
draft.suggested = suggested_questions_after_answer || { enabled: false }
|
||||
draft.citation = retriever_resource || { enabled: false }
|
||||
draft.annotationReply = annotation_reply || { enabled: false }
|
||||
draft.file = {
|
||||
image: {
|
||||
detail: props.publishedConfig.modelConfig.file_upload?.image?.detail || Resolution.high,
|
||||
enabled: !!props.publishedConfig.modelConfig.file_upload?.image?.enabled,
|
||||
number_limits: props.publishedConfig.modelConfig.file_upload?.image?.number_limits || 3,
|
||||
transfer_methods: props.publishedConfig.modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
detail: file_upload?.image?.detail || Resolution.high,
|
||||
enabled: !!file_upload?.image?.enabled,
|
||||
number_limits: file_upload?.image?.number_limits || 3,
|
||||
transfer_methods: file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
},
|
||||
enabled: !!(props.publishedConfig.modelConfig.file_upload?.enabled || props.publishedConfig.modelConfig.file_upload?.image?.enabled),
|
||||
allowed_file_types: props.publishedConfig.modelConfig.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
|
||||
allowed_file_extensions: props.publishedConfig.modelConfig.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
|
||||
allowed_file_upload_methods: props.publishedConfig.modelConfig.file_upload?.allowed_file_upload_methods || props.publishedConfig.modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
number_limits: props.publishedConfig.modelConfig.file_upload?.number_limits || props.publishedConfig.modelConfig.file_upload?.image?.number_limits || 3,
|
||||
enabled: !!(file_upload?.enabled || file_upload?.image?.enabled),
|
||||
allowed_file_types: file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
|
||||
allowed_file_extensions: file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
|
||||
allowed_file_upload_methods: file_upload?.allowed_file_upload_methods || file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
|
||||
number_limits: file_upload?.number_limits || file_upload?.image?.number_limits || 3,
|
||||
} as FileUpload
|
||||
})
|
||||
setFeatures(newFeatures)
|
||||
@@ -69,7 +71,7 @@ const FeaturesWrappedAppPublisher = (props: Props) => {
|
||||
...props,
|
||||
onPublish: handlePublish,
|
||||
onRestore: () => setRestoreConfirmOpen(true),
|
||||
}}/>
|
||||
}} />
|
||||
{restoreConfirmOpen && (
|
||||
<Confirm
|
||||
title={t('appDebug.resetConfig.title')}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -18,35 +19,73 @@ import {
|
||||
RiVerifiedBadgeLine,
|
||||
} from '@remixicon/react'
|
||||
import { useKeyPress } from 'ahooks'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '../../workflow/utils'
|
||||
import Toast from '../../base/toast'
|
||||
import type { ModelAndParameter } from '../configuration/debug/types'
|
||||
import Divider from '../../base/divider'
|
||||
import AccessControl from '../app-access-control'
|
||||
import Loading from '../../base/loading'
|
||||
import Toast from '../../base/toast'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import SuggestedAction from './suggested-action'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '../../workflow/utils'
|
||||
import AccessControl from '../app-access-control'
|
||||
import type { ModelAndParameter } from '../configuration/debug/types'
|
||||
import PublishWithMultipleModel from './publish-with-multiple-model'
|
||||
import SuggestedAction from './suggested-action'
|
||||
import EmbeddedModal from '@/app/components/app/overview/embedded'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { CodeBrowser } from '@/app/components/base/icons/src/vender/line/development'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { fetchInstalledAppList } from '@/service/explore'
|
||||
import EmbeddedModal from '@/app/components/app/overview/embedded'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { CodeBrowser } from '@/app/components/base/icons/src/vender/line/development'
|
||||
import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import { appDefaultIconBackground } from '@/config'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/access-control'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { fetchAppDetailDirect } from '@/service/apps'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/access-control'
|
||||
import { fetchAppDetailDirect } from '@/service/apps'
|
||||
import { fetchInstalledAppList } from '@/service/explore'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { basePath } from '@/utils/var'
|
||||
|
||||
const ACCESS_MODE_MAP: Record<AccessMode, { label: string, icon: React.ElementType }> = {
|
||||
[AccessMode.ORGANIZATION]: {
|
||||
label: 'organization',
|
||||
icon: RiBuildingLine,
|
||||
},
|
||||
[AccessMode.SPECIFIC_GROUPS_MEMBERS]: {
|
||||
label: 'specific',
|
||||
icon: RiLockLine,
|
||||
},
|
||||
[AccessMode.PUBLIC]: {
|
||||
label: 'anyone',
|
||||
icon: RiGlobalLine,
|
||||
},
|
||||
[AccessMode.EXTERNAL_MEMBERS]: {
|
||||
label: 'external',
|
||||
icon: RiVerifiedBadgeLine,
|
||||
},
|
||||
}
|
||||
|
||||
const AccessModeDisplay: React.FC<{ mode?: AccessMode }> = ({ mode }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!mode || !ACCESS_MODE_MAP[mode])
|
||||
return null
|
||||
|
||||
const { icon: Icon, label } = ACCESS_MODE_MAP[mode]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Icon className='h-4 w-4 shrink-0 text-text-secondary' />
|
||||
<div className='grow truncate'>
|
||||
<span className='system-sm-medium text-text-secondary'>{t(`app.accessControlDialog.accessItems.${label}`)}</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export type AppPublisherProps = {
|
||||
disabled?: boolean
|
||||
@@ -64,6 +103,9 @@ export type AppPublisherProps = {
|
||||
toolPublished?: boolean
|
||||
inputs?: InputVar[]
|
||||
onRefreshData?: () => void
|
||||
workflowToolAvailable?: boolean
|
||||
missingStartNode?: boolean
|
||||
hasTriggerNode?: boolean // Whether workflow currently contains any trigger nodes (used to hide missing-start CTA when triggers exist).
|
||||
}
|
||||
|
||||
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
|
||||
@@ -82,28 +124,48 @@ const AppPublisher = ({
|
||||
toolPublished,
|
||||
inputs,
|
||||
onRefreshData,
|
||||
workflowToolAvailable = true,
|
||||
missingStartNode = false,
|
||||
hasTriggerNode = false,
|
||||
}: AppPublisherProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [published, setPublished] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [showAppAccessControl, setShowAppAccessControl] = useState(false)
|
||||
const [isAppAccessSet, setIsAppAccessSet] = useState(true)
|
||||
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
|
||||
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const setAppDetail = useAppStore(s => s.setAppDetail)
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
|
||||
const appMode = (appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow') ? 'chat' : appDetail.mode
|
||||
|
||||
const appMode = (appDetail?.mode !== AppModeEnum.COMPLETION && appDetail?.mode !== AppModeEnum.WORKFLOW) ? AppModeEnum.CHAT : appDetail.mode
|
||||
const appURL = `${appBaseURL}${basePath}/${appMode}/${accessToken}`
|
||||
const isChatApp = ['chat', 'agent-chat', 'completion'].includes(appDetail?.mode || '')
|
||||
const isChatApp = [AppModeEnum.CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.COMPLETION].includes(appDetail?.mode || AppModeEnum.CHAT)
|
||||
|
||||
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp, refetch } = useGetUserCanAccessApp({ appId: appDetail?.id, enabled: false })
|
||||
const { data: appAccessSubjects, isLoading: isGettingAppWhiteListSubjects } = useAppWhiteListSubjects(appDetail?.id, open && systemFeatures.webapp_auth.enabled && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS)
|
||||
|
||||
const noAccessPermission = useMemo(() => systemFeatures.webapp_auth.enabled && appDetail && appDetail.access_mode !== AccessMode.EXTERNAL_MEMBERS && !userCanAccessApp?.result, [systemFeatures, appDetail, userCanAccessApp])
|
||||
const disabledFunctionButton = useMemo(() => (!publishedAt || missingStartNode || noAccessPermission), [publishedAt, missingStartNode, noAccessPermission])
|
||||
|
||||
const disabledFunctionTooltip = useMemo(() => {
|
||||
if (!publishedAt)
|
||||
return t('app.notPublishedYet')
|
||||
if (missingStartNode)
|
||||
return t('app.noUserInputNode')
|
||||
if (noAccessPermission)
|
||||
return t('app.noAccessPermission')
|
||||
}, [missingStartNode, noAccessPermission, publishedAt])
|
||||
|
||||
useEffect(() => {
|
||||
if (systemFeatures.webapp_auth.enabled && open && appDetail)
|
||||
refetch()
|
||||
}, [open, appDetail, refetch, systemFeatures])
|
||||
|
||||
const [showAppAccessControl, setShowAppAccessControl] = useState(false)
|
||||
const [isAppAccessSet, setIsAppAccessSet] = useState(true)
|
||||
useEffect(() => {
|
||||
if (appDetail && appAccessSubjects) {
|
||||
if (appDetail.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && appAccessSubjects.groups?.length === 0 && appAccessSubjects.members?.length === 0)
|
||||
@@ -174,8 +236,6 @@ const AppPublisher = ({
|
||||
}
|
||||
}, [appDetail, setAppDetail])
|
||||
|
||||
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (e) => {
|
||||
e.preventDefault()
|
||||
if (publishDisabled || published)
|
||||
@@ -183,6 +243,10 @@ const AppPublisher = ({
|
||||
handlePublish()
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
const hasPublishedVersion = !!publishedAt
|
||||
const workflowToolDisabled = !hasPublishedVersion || !workflowToolAvailable
|
||||
const workflowToolMessage = workflowToolDisabled ? t('workflow.common.workflowAsToolDisabledHint') : undefined
|
||||
|
||||
return (
|
||||
<>
|
||||
<PortalToFollowElem
|
||||
@@ -197,7 +261,7 @@ const AppPublisher = ({
|
||||
<PortalToFollowElemTrigger onClick={handleTrigger}>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='p-2'
|
||||
className='py-2 pl-3 pr-2'
|
||||
disabled={disabled}
|
||||
>
|
||||
{t('workflow.common.publish')}
|
||||
@@ -279,32 +343,7 @@ const AppPublisher = ({
|
||||
setShowAppAccessControl(true)
|
||||
}}>
|
||||
<div className='flex grow items-center gap-x-1.5 overflow-hidden pr-1'>
|
||||
{appDetail?.access_mode === AccessMode.ORGANIZATION
|
||||
&& <>
|
||||
<RiBuildingLine className='h-4 w-4 shrink-0 text-text-secondary' />
|
||||
<p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.organization')}</p>
|
||||
</>
|
||||
}
|
||||
{appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS
|
||||
&& <>
|
||||
<RiLockLine className='h-4 w-4 shrink-0 text-text-secondary' />
|
||||
<div className='grow truncate'>
|
||||
<span className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</span>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{appDetail?.access_mode === AccessMode.PUBLIC
|
||||
&& <>
|
||||
<RiGlobalLine className='h-4 w-4 shrink-0 text-text-secondary' />
|
||||
<p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.anyone')}</p>
|
||||
</>
|
||||
}
|
||||
{appDetail?.access_mode === AccessMode.EXTERNAL_MEMBERS
|
||||
&& <>
|
||||
<RiVerifiedBadgeLine className='h-4 w-4 shrink-0 text-text-secondary' />
|
||||
<p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.external')}</p>
|
||||
</>
|
||||
}
|
||||
<AccessModeDisplay mode={appDetail?.access_mode} />
|
||||
</div>
|
||||
{!isAppAccessSet && <p className='system-xs-regular shrink-0 text-text-tertiary'>{t('app.publishApp.notSet')}</p>}
|
||||
<div className='flex h-4 w-4 shrink-0 items-center justify-center'>
|
||||
@@ -313,80 +352,88 @@ const AppPublisher = ({
|
||||
</div>
|
||||
{!isAppAccessSet && <p className='system-xs-regular mt-1 text-text-warning'>{t('app.publishApp.notSetDesc')}</p>}
|
||||
</div>}
|
||||
<div className='flex flex-col gap-y-1 border-t-[0.5px] border-t-divider-regular p-4 pt-3'>
|
||||
<Tooltip triggerClassName='flex' disabled={!systemFeatures.webapp_auth.enabled || appDetail?.access_mode === AccessMode.EXTERNAL_MEMBERS || userCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}>
|
||||
<SuggestedAction
|
||||
className='flex-1'
|
||||
disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && appDetail?.access_mode !== AccessMode.EXTERNAL_MEMBERS && !userCanAccessApp?.result)}
|
||||
link={appURL}
|
||||
icon={<RiPlayCircleLine className='h-4 w-4' />}
|
||||
>
|
||||
{t('workflow.common.runApp')}
|
||||
</SuggestedAction>
|
||||
</Tooltip>
|
||||
{appDetail?.mode === 'workflow' || appDetail?.mode === 'completion'
|
||||
? (
|
||||
<Tooltip triggerClassName='flex' disabled={!systemFeatures.webapp_auth.enabled || appDetail.access_mode === AccessMode.EXTERNAL_MEMBERS || userCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}>
|
||||
{
|
||||
// Hide run/batch run app buttons when there is a trigger node.
|
||||
!hasTriggerNode && (
|
||||
<div className='flex flex-col gap-y-1 border-t-[0.5px] border-t-divider-regular p-4 pt-3'>
|
||||
<Tooltip triggerClassName='flex' disabled={!disabledFunctionButton} popupContent={disabledFunctionTooltip} asChild={false}>
|
||||
<SuggestedAction
|
||||
className='flex-1'
|
||||
disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && appDetail.access_mode !== AccessMode.EXTERNAL_MEMBERS && !userCanAccessApp?.result)}
|
||||
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
|
||||
icon={<RiPlayList2Line className='h-4 w-4' />}
|
||||
disabled={disabledFunctionButton}
|
||||
link={appURL}
|
||||
icon={<RiPlayCircleLine className='h-4 w-4' />}
|
||||
>
|
||||
{t('workflow.common.batchRunApp')}
|
||||
{t('workflow.common.runApp')}
|
||||
</SuggestedAction>
|
||||
</Tooltip>
|
||||
)
|
||||
: (
|
||||
<SuggestedAction
|
||||
onClick={() => {
|
||||
setEmbeddingModalOpen(true)
|
||||
handleTrigger()
|
||||
}}
|
||||
disabled={!publishedAt}
|
||||
icon={<CodeBrowser className='h-4 w-4' />}
|
||||
>
|
||||
{t('workflow.common.embedIntoSite')}
|
||||
</SuggestedAction>
|
||||
)}
|
||||
<Tooltip triggerClassName='flex' disabled={!systemFeatures.webapp_auth.enabled || userCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}>
|
||||
<SuggestedAction
|
||||
className='flex-1'
|
||||
onClick={() => {
|
||||
if (publishedAt)
|
||||
handleOpenInExplore()
|
||||
}}
|
||||
disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && !userCanAccessApp?.result)}
|
||||
icon={<RiPlanetLine className='h-4 w-4' />}
|
||||
>
|
||||
{t('workflow.common.openInExplore')}
|
||||
</SuggestedAction>
|
||||
</Tooltip>
|
||||
<SuggestedAction
|
||||
disabled={!publishedAt}
|
||||
link='./develop'
|
||||
icon={<RiTerminalBoxLine className='h-4 w-4' />}
|
||||
>
|
||||
{t('workflow.common.accessAPIReference')}
|
||||
</SuggestedAction>
|
||||
{appDetail?.mode === 'workflow' && (
|
||||
<WorkflowToolConfigureButton
|
||||
disabled={!publishedAt}
|
||||
published={!!toolPublished}
|
||||
detailNeedUpdate={!!toolPublished && published}
|
||||
workflowAppId={appDetail?.id}
|
||||
icon={{
|
||||
content: (appDetail.icon_type === 'image' ? '🤖' : appDetail?.icon) || '🤖',
|
||||
background: (appDetail.icon_type === 'image' ? appDefaultIconBackground : appDetail?.icon_background) || appDefaultIconBackground,
|
||||
}}
|
||||
name={appDetail?.name}
|
||||
description={appDetail?.description}
|
||||
inputs={inputs}
|
||||
handlePublish={handlePublish}
|
||||
onRefreshData={onRefreshData}
|
||||
/>
|
||||
{appDetail?.mode === AppModeEnum.WORKFLOW || appDetail?.mode === AppModeEnum.COMPLETION
|
||||
? (
|
||||
<Tooltip triggerClassName='flex' disabled={!disabledFunctionButton} popupContent={disabledFunctionTooltip} asChild={false}>
|
||||
<SuggestedAction
|
||||
className='flex-1'
|
||||
disabled={disabledFunctionButton}
|
||||
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
|
||||
icon={<RiPlayList2Line className='h-4 w-4' />}
|
||||
>
|
||||
{t('workflow.common.batchRunApp')}
|
||||
</SuggestedAction>
|
||||
</Tooltip>
|
||||
)
|
||||
: (
|
||||
<SuggestedAction
|
||||
onClick={() => {
|
||||
setEmbeddingModalOpen(true)
|
||||
handleTrigger()
|
||||
}}
|
||||
disabled={!publishedAt}
|
||||
icon={<CodeBrowser className='h-4 w-4' />}
|
||||
>
|
||||
{t('workflow.common.embedIntoSite')}
|
||||
</SuggestedAction>
|
||||
)}
|
||||
<Tooltip triggerClassName='flex' disabled={!disabledFunctionButton} popupContent={disabledFunctionTooltip} asChild={false}>
|
||||
<SuggestedAction
|
||||
className='flex-1'
|
||||
onClick={() => {
|
||||
if (publishedAt)
|
||||
handleOpenInExplore()
|
||||
}}
|
||||
disabled={disabledFunctionButton}
|
||||
icon={<RiPlanetLine className='h-4 w-4' />}
|
||||
>
|
||||
{t('workflow.common.openInExplore')}
|
||||
</SuggestedAction>
|
||||
</Tooltip>
|
||||
<Tooltip triggerClassName='flex' disabled={!!publishedAt && !missingStartNode} popupContent={!publishedAt ? t('app.notPublishedYet') : t('app.noUserInputNode')} asChild={false}>
|
||||
<SuggestedAction
|
||||
className='flex-1'
|
||||
disabled={!publishedAt || missingStartNode}
|
||||
link='./develop'
|
||||
icon={<RiTerminalBoxLine className='h-4 w-4' />}
|
||||
>
|
||||
{t('workflow.common.accessAPIReference')}
|
||||
</SuggestedAction>
|
||||
</Tooltip>
|
||||
{appDetail?.mode === AppModeEnum.WORKFLOW && (
|
||||
<WorkflowToolConfigureButton
|
||||
disabled={workflowToolDisabled}
|
||||
published={!!toolPublished}
|
||||
detailNeedUpdate={!!toolPublished && published}
|
||||
workflowAppId={appDetail?.id}
|
||||
icon={{
|
||||
content: (appDetail.icon_type === 'image' ? '🤖' : appDetail?.icon) || '🤖',
|
||||
background: (appDetail.icon_type === 'image' ? appDefaultIconBackground : appDetail?.icon_background) || appDefaultIconBackground,
|
||||
}}
|
||||
name={appDetail?.name}
|
||||
description={appDetail?.description}
|
||||
inputs={inputs}
|
||||
handlePublish={handlePublish}
|
||||
onRefreshData={onRefreshData}
|
||||
disabledReason={workflowToolMessage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
|
||||
@@ -25,7 +25,7 @@ import Tooltip from '@/app/components/base/tooltip'
|
||||
import PromptEditor from '@/app/components/base/prompt-editor'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { getNewVar, getVars } from '@/utils/var'
|
||||
import { AppType } from '@/types/app'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import type { ExternalDataTool } from '@/models/common'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
@@ -102,7 +102,7 @@ const AdvancedPromptInput: FC<Props> = ({
|
||||
},
|
||||
})
|
||||
}
|
||||
const isChatApp = mode !== AppType.completion
|
||||
const isChatApp = mode !== AppModeEnum.COMPLETION
|
||||
const [isCopied, setIsCopied] = React.useState(false)
|
||||
|
||||
const promptVariablesObj = (() => {
|
||||
|
||||
@@ -12,11 +12,13 @@ import Button from '@/app/components/base/button'
|
||||
import AdvancedMessageInput from '@/app/components/app/configuration/config-prompt/advanced-prompt-input'
|
||||
import { PromptRole } from '@/models/debug'
|
||||
import type { PromptItem, PromptVariable } from '@/models/debug'
|
||||
import { type AppType, ModelModeType } from '@/types/app'
|
||||
import type { AppModeEnum } from '@/types/app'
|
||||
import { ModelModeType } from '@/types/app'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { MAX_PROMPT_MESSAGE_LENGTH } from '@/config'
|
||||
|
||||
export type IPromptProps = {
|
||||
mode: AppType
|
||||
mode: AppModeEnum
|
||||
promptTemplate: string
|
||||
promptVariables: PromptVariable[]
|
||||
readonly?: boolean
|
||||
|
||||
@@ -10,7 +10,7 @@ import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { AppType } from '@/types/app'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { getNewVar, getVars } from '@/utils/var'
|
||||
import AutomaticBtn from '@/app/components/app/configuration/config/automatic/automatic-btn'
|
||||
import type { GenRes } from '@/service/debug'
|
||||
@@ -29,7 +29,7 @@ import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
export type ISimplePromptInput = {
|
||||
mode: AppType
|
||||
mode: AppModeEnum
|
||||
promptTemplate: string
|
||||
promptVariables: PromptVariable[]
|
||||
readonly?: boolean
|
||||
@@ -155,7 +155,7 @@ const Prompt: FC<ISimplePromptInput> = ({
|
||||
setModelConfig(newModelConfig)
|
||||
setPrevPromptConfig(modelConfig.configs)
|
||||
|
||||
if (mode !== AppType.completion) {
|
||||
if (mode !== AppModeEnum.COMPLETION) {
|
||||
setIntroduction(res.opening_statement || '')
|
||||
const newFeatures = produce(features, (draft) => {
|
||||
draft.opening = {
|
||||
@@ -177,7 +177,7 @@ const Prompt: FC<ISimplePromptInput> = ({
|
||||
{!noTitle && (
|
||||
<div className="flex h-11 items-center justify-between pl-3 pr-2.5">
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className='h2 system-sm-semibold-uppercase text-text-secondary'>{mode !== AppType.completion ? t('appDebug.chatSubTitle') : t('appDebug.completionSubTitle')}</div>
|
||||
<div className='h2 system-sm-semibold-uppercase text-text-secondary'>{mode !== AppModeEnum.COMPLETION ? t('appDebug.chatSubTitle') : t('appDebug.completionSubTitle')}</div>
|
||||
{!readonly && (
|
||||
<Tooltip
|
||||
popupContent={
|
||||
@@ -276,7 +276,7 @@ const Prompt: FC<ISimplePromptInput> = ({
|
||||
{showAutomatic && (
|
||||
<GetAutomaticResModal
|
||||
flowId={appId}
|
||||
mode={mode as AppType}
|
||||
mode={mode as AppModeEnum}
|
||||
isShow={showAutomatic}
|
||||
onClose={showAutomaticFalse}
|
||||
onFinished={handleAutomaticRes}
|
||||
|
||||
@@ -28,7 +28,7 @@ import { jsonConfigPlaceHolder, jsonObjectWrap } from './config'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { AppModeEnum, TransferMethod } from '@/types/app'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
|
||||
const TEXT_MAX_LENGTH = 256
|
||||
@@ -70,7 +70,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
||||
const { type, label, variable, options, max_length } = tempPayload
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const isBasicApp = appDetail?.mode !== 'advanced-chat' && appDetail?.mode !== 'workflow'
|
||||
const isBasicApp = appDetail?.mode !== AppModeEnum.ADVANCED_CHAT && appDetail?.mode !== AppModeEnum.WORKFLOW
|
||||
const isSupportJSON = false
|
||||
const jsonSchemaStr = useMemo(() => {
|
||||
const isJsonObject = type === InputVarType.jsonObject
|
||||
|
||||
@@ -17,7 +17,7 @@ import { getNewVar, hasDuplicateStr } from '@/utils/var'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { AppType } from '@/types/app'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import type { ExternalDataTool } from '@/models/common'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
@@ -201,7 +201,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
|
||||
const handleRemoveVar = (index: number) => {
|
||||
const removeVar = promptVariables[index]
|
||||
|
||||
if (mode === AppType.completion && dataSets.length > 0 && removeVar.is_context_var) {
|
||||
if (mode === AppModeEnum.COMPLETION && dataSets.length > 0 && removeVar.is_context_var) {
|
||||
showDeleteContextVarModal()
|
||||
setRemoveIndex(index)
|
||||
return
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
AuthCategory,
|
||||
PluginAuthInAgent,
|
||||
} from '@/app/components/plugins/plugin-auth'
|
||||
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
|
||||
|
||||
type Props = {
|
||||
showBackButton?: boolean
|
||||
@@ -193,7 +194,7 @@ const SettingBuiltInTool: FC<Props> = ({
|
||||
onClick={onHide}
|
||||
>
|
||||
<RiArrowLeftLine className='h-4 w-4' />
|
||||
BACK
|
||||
{t('plugin.detailPanel.operation.back')}
|
||||
</div>
|
||||
)}
|
||||
<div className='flex items-center gap-1'>
|
||||
@@ -215,6 +216,7 @@ const SettingBuiltInTool: FC<Props> = ({
|
||||
provider: collection.name,
|
||||
category: AuthCategory.tool,
|
||||
providerType: collection.type,
|
||||
detail: collection as any,
|
||||
}}
|
||||
credentialId={credentialId}
|
||||
onAuthorizationItemClick={onAuthorizationItemClick}
|
||||
@@ -244,13 +246,14 @@ const SettingBuiltInTool: FC<Props> = ({
|
||||
)}
|
||||
<div className='h-0 grow overflow-y-auto px-4'>
|
||||
{isInfoActive ? infoUI : settingUI}
|
||||
{!readonly && !isInfoActive && (
|
||||
<div className='flex shrink-0 justify-end space-x-2 rounded-b-[10px] bg-components-panel-bg py-2'>
|
||||
<Button className='flex h-8 items-center !px-3 !text-[13px] font-medium ' onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
<Button className='flex h-8 items-center !px-3 !text-[13px] font-medium' variant='primary' disabled={!isValid} onClick={() => onSave?.(addDefaultValue(tempSetting, formSchemas))}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!readonly && !isInfoActive && (
|
||||
<div className='mt-2 flex shrink-0 justify-end space-x-2 rounded-b-[10px] border-t border-divider-regular bg-components-panel-bg px-6 py-4'>
|
||||
<Button className='flex h-8 items-center !px-3 !text-[13px] font-medium ' onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
<Button className='flex h-8 items-center !px-3 !text-[13px] font-medium' variant='primary' disabled={!isValid} onClick={() => onSave?.(addDefaultValue(tempSetting, formSchemas))}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
)}
|
||||
<ReadmeEntrance pluginDetail={collection as any} className='mt-auto' />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -19,8 +19,7 @@ import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { generateBasicAppFirstTimeRule, generateRule } from '@/service/debug'
|
||||
import type { CompletionParams, Model } from '@/types/app'
|
||||
import type { AppType } from '@/types/app'
|
||||
import type { AppModeEnum, CompletionParams, Model } from '@/types/app'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
|
||||
@@ -44,7 +43,7 @@ import { useGenerateRuleTemplate } from '@/service/use-apps'
|
||||
|
||||
const i18nPrefix = 'appDebug.generate'
|
||||
export type IGetAutomaticResProps = {
|
||||
mode: AppType
|
||||
mode: AppModeEnum
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
onFinished: (res: GenRes) => void
|
||||
@@ -299,7 +298,6 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
|
||||
portalToFollowElemContentClassName='z-[1000]'
|
||||
isAdvancedMode={true}
|
||||
provider={model.provider}
|
||||
mode={model.mode}
|
||||
completionParams={model.completion_params}
|
||||
modelId={model.name}
|
||||
setModel={handleModelChange}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import { languageMap } from '../../../../workflow/nodes/_base/components/editor/code-editor/index'
|
||||
import { generateRule } from '@/service/debug'
|
||||
import type { GenRes } from '@/service/debug'
|
||||
import type { ModelModeType } from '@/types/app'
|
||||
import type { AppType, CompletionParams, Model } from '@/types/app'
|
||||
import type { AppModeEnum, ModelModeType } from '@/types/app'
|
||||
import type { CompletionParams, Model } from '@/types/app'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Generator } from '@/app/components/base/icons/src/vender/other'
|
||||
@@ -33,7 +33,7 @@ export type IGetCodeGeneratorResProps = {
|
||||
flowId: string
|
||||
nodeId: string
|
||||
currentCode?: string
|
||||
mode: AppType
|
||||
mode: AppModeEnum
|
||||
isShow: boolean
|
||||
codeLanguages: CodeLanguage
|
||||
onClose: () => void
|
||||
@@ -142,7 +142,7 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
||||
ideal_output: ideaOutput,
|
||||
language: languageMap[codeLanguages] || 'javascript',
|
||||
})
|
||||
if((res as any).code) // not current or current is the same as the template would return a code field
|
||||
if ((res as any).code) // not current or current is the same as the template would return a code field
|
||||
res.modified = (res as any).code
|
||||
|
||||
if (error) {
|
||||
@@ -214,7 +214,6 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
|
||||
portalToFollowElemContentClassName='z-[1000]'
|
||||
isAdvancedMode={true}
|
||||
provider={model.provider}
|
||||
mode={model.mode}
|
||||
completionParams={model.completion_params}
|
||||
modelId={model.name}
|
||||
setModel={handleModelChange}
|
||||
|
||||
@@ -14,8 +14,7 @@ import ConfigContext from '@/context/debug-configuration'
|
||||
import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
|
||||
import ConfigVar from '@/app/components/app/configuration/config-var'
|
||||
import type { ModelConfig, PromptVariable } from '@/models/debug'
|
||||
import type { AppType } from '@/types/app'
|
||||
import { ModelModeType } from '@/types/app'
|
||||
import { AppModeEnum, ModelModeType } from '@/types/app'
|
||||
|
||||
const Config: FC = () => {
|
||||
const {
|
||||
@@ -29,7 +28,7 @@ const Config: FC = () => {
|
||||
setModelConfig,
|
||||
setPrevPromptConfig,
|
||||
} = useContext(ConfigContext)
|
||||
const isChatApp = ['advanced-chat', 'agent-chat', 'chat'].includes(mode)
|
||||
const isChatApp = [AppModeEnum.ADVANCED_CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.CHAT].includes(mode)
|
||||
const formattingChangedDispatcher = useFormattingChangedDispatcher()
|
||||
|
||||
const promptTemplate = modelConfig.configs.prompt_template
|
||||
@@ -62,7 +61,7 @@ const Config: FC = () => {
|
||||
>
|
||||
{/* Template */}
|
||||
<ConfigPrompt
|
||||
mode={mode as AppType}
|
||||
mode={mode}
|
||||
promptTemplate={promptTemplate}
|
||||
promptVariables={promptVariables}
|
||||
onChange={handlePromptChange}
|
||||
|
||||
@@ -13,7 +13,7 @@ import CardItem from './card-item/item'
|
||||
import ParamsConfig from './params-config'
|
||||
import ContextVar from './context-var'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { AppType } from '@/types/app'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import {
|
||||
getMultipleRetrievalConfig,
|
||||
@@ -232,7 +232,7 @@ const DatasetConfig: FC = () => {
|
||||
draft.metadata_model_config = {
|
||||
provider: model.provider,
|
||||
name: model.modelId,
|
||||
mode: model.mode || 'chat',
|
||||
mode: model.mode || AppModeEnum.CHAT,
|
||||
completion_params: draft.metadata_model_config?.completion_params || { temperature: 0.7 },
|
||||
}
|
||||
})
|
||||
@@ -302,7 +302,7 @@ const DatasetConfig: FC = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{mode === AppType.completion && dataSet.length > 0 && (
|
||||
{mode === AppModeEnum.COMPLETION && dataSet.length > 0 && (
|
||||
<ContextVar
|
||||
value={selectedContextVar?.key}
|
||||
options={promptVariablesToSelect}
|
||||
|
||||
@@ -368,7 +368,6 @@ const ConfigContent: FC<Props> = ({
|
||||
popupClassName='!w-[387px]'
|
||||
portalToFollowElemContentClassName='!z-[1002]'
|
||||
isAdvancedMode={true}
|
||||
mode={model?.mode}
|
||||
provider={model?.provider}
|
||||
completionParams={model?.completion_params}
|
||||
modelId={model?.name}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useToastContext } from '@/app/components/base/toast'
|
||||
import { updateDatasetSetting } from '@/service/datasets'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import RetrievalSettings from '@/app/components/datasets/external-knowledge-base/create/RetrievalSettings'
|
||||
import RetrievalMethodConfig from '@/app/components/datasets/common/retrieval-method-config'
|
||||
@@ -277,7 +278,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
</div>
|
||||
<div className='mt-2 w-full text-xs leading-6 text-text-tertiary'>
|
||||
{t('datasetSettings.form.embeddingModelTip')}
|
||||
<span className='cursor-pointer text-text-accent' onClick={() => setShowAccountSettingModal({ payload: 'provider' })}>{t('datasetSettings.form.embeddingModelTipLink')}</span>
|
||||
<span className='cursor-pointer text-text-accent' onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })}>{t('datasetSettings.form.embeddingModelTipLink')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import Dropdown from '@/app/components/base/dropdown'
|
||||
import type { Item } from '@/app/components/base/dropdown'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { ModelStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
type DebugItemProps = {
|
||||
modelAndParameter: ModelAndParameter
|
||||
@@ -112,13 +113,13 @@ const DebugItem: FC<DebugItemProps> = ({
|
||||
</div>
|
||||
<div style={{ height: 'calc(100% - 40px)' }}>
|
||||
{
|
||||
(mode === 'chat' || mode === 'agent-chat') && currentProvider && currentModel && currentModel.status === ModelStatusEnum.active && (
|
||||
(mode === AppModeEnum.CHAT || mode === AppModeEnum.AGENT_CHAT) && currentProvider && currentModel && currentModel.status === ModelStatusEnum.active && (
|
||||
<ChatItem modelAndParameter={modelAndParameter} />
|
||||
)
|
||||
}
|
||||
{
|
||||
mode === 'completion' && currentProvider && currentModel && currentModel.status === ModelStatusEnum.active && (
|
||||
<TextGenerationItem modelAndParameter={modelAndParameter}/>
|
||||
mode === AppModeEnum.COMPLETION && currentProvider && currentModel && currentModel.status === ModelStatusEnum.active && (
|
||||
<TextGenerationItem modelAndParameter={modelAndParameter} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useFeatures } from '@/app/components/base/features/hooks'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
const DebugWithMultipleModel = () => {
|
||||
const {
|
||||
@@ -33,7 +34,7 @@ const DebugWithMultipleModel = () => {
|
||||
} = useDebugWithMultipleModelContext()
|
||||
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const isChatMode = mode === 'chat' || mode === 'agent-chat'
|
||||
const isChatMode = mode === AppModeEnum.CHAT || mode === AppModeEnum.AGENT_CHAT
|
||||
|
||||
const handleSend = useCallback((message: string, files?: FileEntity[]) => {
|
||||
if (checkCanSend && !checkCanSend())
|
||||
|
||||
@@ -26,7 +26,6 @@ const ModelParameterTrigger: FC<ModelParameterTriggerProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
mode,
|
||||
isAdvancedMode,
|
||||
} = useDebugConfigurationContext()
|
||||
const {
|
||||
@@ -57,7 +56,6 @@ const ModelParameterTrigger: FC<ModelParameterTriggerProps> = ({
|
||||
|
||||
return (
|
||||
<ModelParameterModal
|
||||
mode={mode}
|
||||
isAdvancedMode={isAdvancedMode}
|
||||
provider={modelAndParameter.provider}
|
||||
modelId={modelAndParameter.model}
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
APP_CHAT_WITH_MULTIPLE_MODEL,
|
||||
APP_CHAT_WITH_MULTIPLE_MODEL_RESTART,
|
||||
} from './types'
|
||||
import { AppType, ModelModeType, TransferMethod } from '@/types/app'
|
||||
import { AppModeEnum, ModelModeType, TransferMethod } from '@/types/app'
|
||||
import ChatUserInput from '@/app/components/app/configuration/debug/chat-user-input'
|
||||
import PromptValuePanel from '@/app/components/app/configuration/prompt-value-panel'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
@@ -144,7 +144,7 @@ const Debug: FC<IDebug> = ({
|
||||
const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([])
|
||||
|
||||
const checkCanSend = useCallback(() => {
|
||||
if (isAdvancedMode && mode !== AppType.completion) {
|
||||
if (isAdvancedMode && mode !== AppModeEnum.COMPLETION) {
|
||||
if (modelModeType === ModelModeType.completion) {
|
||||
if (!hasSetBlockStatus.history) {
|
||||
notify({ type: 'error', message: t('appDebug.otherError.historyNoBeEmpty') })
|
||||
@@ -410,7 +410,7 @@ const Debug: FC<IDebug> = ({
|
||||
)
|
||||
: null
|
||||
}
|
||||
{mode !== AppType.completion && (
|
||||
{mode !== AppModeEnum.COMPLETION && (
|
||||
<>
|
||||
<TooltipPlus
|
||||
popupContent={t('common.operation.refresh')}
|
||||
@@ -435,14 +435,14 @@ const Debug: FC<IDebug> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{mode !== AppType.completion && expanded && (
|
||||
{mode !== AppModeEnum.COMPLETION && expanded && (
|
||||
<div className='mx-3'>
|
||||
<ChatUserInput inputs={inputs} />
|
||||
</div>
|
||||
)}
|
||||
{mode === AppType.completion && (
|
||||
{mode === AppModeEnum.COMPLETION && (
|
||||
<PromptValuePanel
|
||||
appType={mode as AppType}
|
||||
appType={mode as AppModeEnum}
|
||||
onSend={handleSendTextCompletion}
|
||||
inputs={inputs}
|
||||
visionConfig={{
|
||||
@@ -490,7 +490,7 @@ const Debug: FC<IDebug> = ({
|
||||
!debugWithMultipleModel && (
|
||||
<div className="flex grow flex-col" ref={ref}>
|
||||
{/* Chat */}
|
||||
{mode !== AppType.completion && (
|
||||
{mode !== AppModeEnum.COMPLETION && (
|
||||
<div className='h-0 grow overflow-hidden'>
|
||||
<DebugWithSingleModel
|
||||
ref={debugWithSingleModelRef}
|
||||
@@ -499,7 +499,7 @@ const Debug: FC<IDebug> = ({
|
||||
</div>
|
||||
)}
|
||||
{/* Text Generation */}
|
||||
{mode === AppType.completion && (
|
||||
{mode === AppModeEnum.COMPLETION && (
|
||||
<>
|
||||
{(completionRes || isResponding) && (
|
||||
<>
|
||||
@@ -528,7 +528,7 @@ const Debug: FC<IDebug> = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{mode === AppType.completion && showPromptLogModal && (
|
||||
{mode === AppModeEnum.COMPLETION && showPromptLogModal && (
|
||||
<PromptLogModal
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
|
||||
@@ -3,14 +3,14 @@ import { clone } from 'lodash-es'
|
||||
import { produce } from 'immer'
|
||||
import type { ChatPromptConfig, CompletionPromptConfig, ConversationHistoriesRole, PromptItem } from '@/models/debug'
|
||||
import { PromptMode } from '@/models/debug'
|
||||
import { ModelModeType } from '@/types/app'
|
||||
import { AppModeEnum, ModelModeType } from '@/types/app'
|
||||
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
|
||||
import { PRE_PROMPT_PLACEHOLDER_TEXT, checkHasContextBlock, checkHasHistoryBlock, checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants'
|
||||
import { fetchPromptTemplate } from '@/service/debug'
|
||||
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
type Param = {
|
||||
appMode: string
|
||||
appMode?: AppModeEnum
|
||||
modelModeType: ModelModeType
|
||||
modelName: string
|
||||
promptMode: PromptMode
|
||||
@@ -104,6 +104,9 @@ const useAdvancedPromptConfig = ({
|
||||
const migrateToDefaultPrompt = async (isMigrateToCompetition?: boolean, toModelModeType?: ModelModeType) => {
|
||||
const mode = modelModeType
|
||||
const toReplacePrePrompt = prePrompt || ''
|
||||
if (!appMode)
|
||||
return
|
||||
|
||||
if (!isAdvancedPrompt) {
|
||||
const { chat_prompt_config, completion_prompt_config, stop } = await fetchPromptTemplate({
|
||||
appMode,
|
||||
@@ -122,7 +125,6 @@ const useAdvancedPromptConfig = ({
|
||||
})
|
||||
setChatPromptConfig(newPromptConfig)
|
||||
}
|
||||
|
||||
else {
|
||||
const newPromptConfig = produce(completion_prompt_config, (draft) => {
|
||||
draft.prompt.text = draft.prompt.text.replace(PRE_PROMPT_PLACEHOLDER_TEXT, toReplacePrePrompt)
|
||||
@@ -152,7 +154,7 @@ const useAdvancedPromptConfig = ({
|
||||
else
|
||||
draft.prompt.text = completionPromptConfig.prompt?.text.replace(PRE_PROMPT_PLACEHOLDER_TEXT, toReplacePrePrompt)
|
||||
|
||||
if (['advanced-chat', 'agent-chat', 'chat'].includes(appMode) && completionPromptConfig.conversation_histories_role.assistant_prefix && completionPromptConfig.conversation_histories_role.user_prefix)
|
||||
if ([AppModeEnum.ADVANCED_CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.CHAT].includes(appMode) && completionPromptConfig.conversation_histories_role.assistant_prefix && completionPromptConfig.conversation_histories_role.user_prefix)
|
||||
draft.conversation_histories_role = completionPromptConfig.conversation_histories_role
|
||||
})
|
||||
setCompletionPromptConfig(newPromptConfig)
|
||||
|
||||
@@ -47,11 +47,12 @@ import { fetchAppDetailDirect, updateAppModelConfig } from '@/service/apps'
|
||||
import { promptVariablesToUserInputsForm, userInputsFormToPromptVariables } from '@/utils/model-config'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { AgentStrategy, AppType, ModelModeType, RETRIEVE_TYPE, Resolution, TransferMethod } from '@/types/app'
|
||||
import { AgentStrategy, AppModeEnum, ModelModeType, RETRIEVE_TYPE, Resolution, TransferMethod } from '@/types/app'
|
||||
import { PromptMode } from '@/models/debug'
|
||||
import { ANNOTATION_DEFAULT, DATASET_DEFAULT, DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
|
||||
import SelectDataSet from '@/app/components/app/configuration/dataset-config/select-dataset'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
@@ -110,7 +111,7 @@ const Configuration: FC = () => {
|
||||
const pathname = usePathname()
|
||||
const matched = pathname.match(/\/app\/([^/]+)/)
|
||||
const appId = (matched?.length && matched[1]) ? matched[1] : ''
|
||||
const [mode, setMode] = useState('')
|
||||
const [mode, setMode] = useState<AppModeEnum>(AppModeEnum.CHAT)
|
||||
const [publishedConfig, setPublishedConfig] = useState<PublishConfig | null>(null)
|
||||
|
||||
const [conversationId, setConversationId] = useState<string | null>('')
|
||||
@@ -209,7 +210,7 @@ const Configuration: FC = () => {
|
||||
dataSets: [],
|
||||
agentConfig: DEFAULT_AGENT_SETTING,
|
||||
})
|
||||
const isAgent = mode === 'agent-chat'
|
||||
const isAgent = mode === AppModeEnum.AGENT_CHAT
|
||||
|
||||
const isOpenAI = modelConfig.provider === 'langgenius/openai/openai'
|
||||
|
||||
@@ -451,7 +452,7 @@ const Configuration: FC = () => {
|
||||
const appMode = mode
|
||||
|
||||
if (modeMode === ModelModeType.completion) {
|
||||
if (appMode !== AppType.completion) {
|
||||
if (appMode !== AppModeEnum.COMPLETION) {
|
||||
if (!completionPromptConfig.prompt?.text || !completionPromptConfig.conversation_histories_role.assistant_prefix || !completionPromptConfig.conversation_histories_role.user_prefix)
|
||||
await migrateToDefaultPrompt(true, ModelModeType.completion)
|
||||
}
|
||||
@@ -554,7 +555,7 @@ const Configuration: FC = () => {
|
||||
}
|
||||
setCollectionList(collectionList)
|
||||
const res = await fetchAppDetailDirect({ url: '/apps', id: appId })
|
||||
setMode(res.mode)
|
||||
setMode(res.mode as AppModeEnum)
|
||||
const modelConfig = res.model_config as BackendModelConfig
|
||||
const promptMode = modelConfig.prompt_type === PromptMode.advanced ? PromptMode.advanced : PromptMode.simple
|
||||
doSetPromptMode(promptMode)
|
||||
@@ -665,10 +666,10 @@ const Configuration: FC = () => {
|
||||
external_data_tools: modelConfig.external_data_tools ?? [],
|
||||
system_parameters: modelConfig.system_parameters,
|
||||
dataSets: datasets || [],
|
||||
agentConfig: res.mode === 'agent-chat' ? {
|
||||
agentConfig: res.mode === AppModeEnum.AGENT_CHAT ? {
|
||||
max_iteration: DEFAULT_AGENT_SETTING.max_iteration,
|
||||
...modelConfig.agent_mode,
|
||||
// remove dataset
|
||||
// remove dataset
|
||||
enabled: true, // modelConfig.agent_mode?.enabled is not correct. old app: the value of app with dataset's is always true
|
||||
tools: (modelConfig.agent_mode?.tools ?? []).filter((tool: any) => {
|
||||
return !tool.dataset
|
||||
@@ -705,7 +706,7 @@ const Configuration: FC = () => {
|
||||
provider: currentRerankProvider?.provider,
|
||||
model: currentRerankModel?.model,
|
||||
})
|
||||
setDatasetConfigs({
|
||||
const datasetConfigsToSet = {
|
||||
...modelConfig.dataset_configs,
|
||||
...retrievalConfig,
|
||||
...(retrievalConfig.reranking_model ? {
|
||||
@@ -714,13 +715,15 @@ const Configuration: FC = () => {
|
||||
reranking_provider_name: correctModelProvider(retrievalConfig.reranking_model.provider),
|
||||
},
|
||||
} : {}),
|
||||
} as DatasetConfigs)
|
||||
} as DatasetConfigs
|
||||
datasetConfigsToSet.retrieval_model = datasetConfigsToSet.retrieval_model ?? RETRIEVE_TYPE.multiWay
|
||||
setDatasetConfigs(datasetConfigsToSet)
|
||||
setHasFetchedDetail(true)
|
||||
})()
|
||||
}, [appId])
|
||||
|
||||
const promptEmpty = (() => {
|
||||
if (mode !== AppType.completion)
|
||||
if (mode !== AppModeEnum.COMPLETION)
|
||||
return false
|
||||
|
||||
if (isAdvancedMode) {
|
||||
@@ -734,7 +737,7 @@ const Configuration: FC = () => {
|
||||
else { return !modelConfig.configs.prompt_template }
|
||||
})()
|
||||
const cannotPublish = (() => {
|
||||
if (mode !== AppType.completion) {
|
||||
if (mode !== AppModeEnum.COMPLETION) {
|
||||
if (!isAdvancedMode)
|
||||
return false
|
||||
|
||||
@@ -749,7 +752,7 @@ const Configuration: FC = () => {
|
||||
}
|
||||
else { return promptEmpty }
|
||||
})()
|
||||
const contextVarEmpty = mode === AppType.completion && dataSets.length > 0 && !hasSetContextVar
|
||||
const contextVarEmpty = mode === AppModeEnum.COMPLETION && dataSets.length > 0 && !hasSetContextVar
|
||||
const onPublish = async (modelAndParameter?: ModelAndParameter, features?: FeaturesData) => {
|
||||
const modelId = modelAndParameter?.model || modelConfig.model_id
|
||||
const promptTemplate = modelConfig.configs.prompt_template
|
||||
@@ -759,7 +762,7 @@ const Configuration: FC = () => {
|
||||
notify({ type: 'error', message: t('appDebug.otherError.promptNoBeEmpty') })
|
||||
return
|
||||
}
|
||||
if (isAdvancedMode && mode !== AppType.completion) {
|
||||
if (isAdvancedMode && mode !== AppModeEnum.COMPLETION) {
|
||||
if (modelModeType === ModelModeType.completion) {
|
||||
if (!hasSetBlockStatus.history) {
|
||||
notify({ type: 'error', message: t('appDebug.otherError.historyNoBeEmpty') })
|
||||
@@ -981,7 +984,6 @@ const Configuration: FC = () => {
|
||||
<>
|
||||
<ModelParameterModal
|
||||
isAdvancedMode={isAdvancedMode}
|
||||
mode={mode}
|
||||
provider={modelConfig.provider}
|
||||
completionParams={completionParams}
|
||||
modelId={modelConfig.model_id}
|
||||
@@ -1020,7 +1022,7 @@ const Configuration: FC = () => {
|
||||
<div className='flex grow flex-col rounded-tl-2xl border-l-[0.5px] border-t-[0.5px] border-components-panel-border bg-chatbot-bg '>
|
||||
<Debug
|
||||
isAPIKeySet={isAPIKeySet}
|
||||
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
|
||||
onSetting={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })}
|
||||
inputs={inputs}
|
||||
modelParameterParams={{
|
||||
setModel: setModel as any,
|
||||
@@ -1040,7 +1042,7 @@ const Configuration: FC = () => {
|
||||
content={t('appDebug.trailUseGPT4Info.description')}
|
||||
isShow={showUseGPT4Confirm}
|
||||
onConfirm={() => {
|
||||
setShowAccountSettingModal({ payload: 'provider' })
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
|
||||
setShowUseGPT4Confirm(false)
|
||||
}}
|
||||
onCancel={() => setShowUseGPT4Confirm(false)}
|
||||
@@ -1072,7 +1074,7 @@ const Configuration: FC = () => {
|
||||
<Drawer showClose isOpen={isShowDebugPanel} onClose={hideDebugPanel} mask footer={null}>
|
||||
<Debug
|
||||
isAPIKeySet={isAPIKeySet}
|
||||
onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
|
||||
onSetting={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })}
|
||||
inputs={inputs}
|
||||
modelParameterParams={{
|
||||
setModel: setModel as any,
|
||||
@@ -1089,7 +1091,7 @@ const Configuration: FC = () => {
|
||||
show
|
||||
inWorkflow={false}
|
||||
showFileUpload={false}
|
||||
isChatMode={mode !== 'completion'}
|
||||
isChatMode={mode !== AppModeEnum.COMPLETION}
|
||||
disabled={false}
|
||||
onChange={handleFeaturesChange}
|
||||
onClose={() => setShowAppConfigureFeaturesModal(false)}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '@remixicon/react'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import type { Inputs } from '@/models/debug'
|
||||
import { AppType, ModelModeType } from '@/types/app'
|
||||
import { AppModeEnum, ModelModeType } from '@/types/app'
|
||||
import Select from '@/app/components/base/select'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
@@ -25,7 +25,7 @@ import cn from '@/utils/classnames'
|
||||
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
|
||||
|
||||
export type IPromptValuePanelProps = {
|
||||
appType: AppType
|
||||
appType: AppModeEnum
|
||||
onSend?: () => void
|
||||
inputs: Inputs
|
||||
visionConfig: VisionSettings
|
||||
@@ -55,7 +55,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
}, [promptVariables])
|
||||
|
||||
const canNotRun = useMemo(() => {
|
||||
if (mode !== AppType.completion)
|
||||
if (mode !== AppModeEnum.COMPLETION)
|
||||
return true
|
||||
|
||||
if (isAdvancedMode) {
|
||||
@@ -215,7 +215,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
<div className='mx-3'>
|
||||
<FeatureBar
|
||||
showFileUpload={false}
|
||||
isChatMode={appType !== AppType.completion}
|
||||
isChatMode={appType !== AppModeEnum.COMPLETION}
|
||||
onFeatureBarClick={setShowAppConfigureFeaturesModal} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -25,7 +25,7 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import Input from '@/app/components/base/input'
|
||||
import type { AppMode } from '@/types/app'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { DSLImportMode } from '@/models/app'
|
||||
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||
|
||||
@@ -61,7 +61,7 @@ const Apps = ({
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const [currentType, setCurrentType] = useState<AppMode[]>([])
|
||||
const [currentType, setCurrentType] = useState<AppModeEnum[]>([])
|
||||
const [currCategory, setCurrCategory] = useTabSearchParams({
|
||||
defaultTab: allCategoriesEn,
|
||||
disableSearchParams: true,
|
||||
@@ -93,15 +93,15 @@ const Apps = ({
|
||||
if (currentType.length === 0)
|
||||
return filteredByCategory
|
||||
return filteredByCategory.filter((item) => {
|
||||
if (currentType.includes('chat') && item.app.mode === 'chat')
|
||||
if (currentType.includes(AppModeEnum.CHAT) && item.app.mode === AppModeEnum.CHAT)
|
||||
return true
|
||||
if (currentType.includes('advanced-chat') && item.app.mode === 'advanced-chat')
|
||||
if (currentType.includes(AppModeEnum.ADVANCED_CHAT) && item.app.mode === AppModeEnum.ADVANCED_CHAT)
|
||||
return true
|
||||
if (currentType.includes('agent-chat') && item.app.mode === 'agent-chat')
|
||||
if (currentType.includes(AppModeEnum.AGENT_CHAT) && item.app.mode === AppModeEnum.AGENT_CHAT)
|
||||
return true
|
||||
if (currentType.includes('completion') && item.app.mode === 'completion')
|
||||
if (currentType.includes(AppModeEnum.COMPLETION) && item.app.mode === AppModeEnum.COMPLETION)
|
||||
return true
|
||||
if (currentType.includes('workflow') && item.app.mode === 'workflow')
|
||||
if (currentType.includes(AppModeEnum.WORKFLOW) && item.app.mode === AppModeEnum.WORKFLOW)
|
||||
return true
|
||||
return false
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ import { basePath } from '@/utils/var'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import type { AppMode } from '@/types/app'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { createApp } from '@/service/apps'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
@@ -35,7 +35,7 @@ type CreateAppProps = {
|
||||
onSuccess: () => void
|
||||
onClose: () => void
|
||||
onCreateFromTemplate?: () => void
|
||||
defaultAppMode?: AppMode
|
||||
defaultAppMode?: AppModeEnum
|
||||
}
|
||||
|
||||
function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }: CreateAppProps) {
|
||||
@@ -43,7 +43,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
const { push } = useRouter()
|
||||
const { notify } = useContext(ToastContext)
|
||||
|
||||
const [appMode, setAppMode] = useState<AppMode>(defaultAppMode || 'advanced-chat')
|
||||
const [appMode, setAppMode] = useState<AppModeEnum>(defaultAppMode || AppModeEnum.ADVANCED_CHAT)
|
||||
const [appIcon, setAppIcon] = useState<AppIconSelection>({ type: 'emoji', icon: '🤖', background: '#FFEAD5' })
|
||||
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
||||
const [name, setName] = useState('')
|
||||
@@ -57,7 +57,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
const isCreatingRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (appMode === 'chat' || appMode === 'agent-chat' || appMode === 'completion')
|
||||
if (appMode === AppModeEnum.CHAT || appMode === AppModeEnum.AGENT_CHAT || appMode === AppModeEnum.COMPLETION)
|
||||
setIsAppTypeExpanded(true)
|
||||
}, [appMode])
|
||||
|
||||
@@ -118,24 +118,24 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
<div>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<AppTypeCard
|
||||
active={appMode === 'workflow'}
|
||||
active={appMode === AppModeEnum.WORKFLOW}
|
||||
title={t('app.types.workflow')}
|
||||
description={t('app.newApp.workflowShortDescription')}
|
||||
icon={<div className='flex h-6 w-6 items-center justify-center rounded-md bg-components-icon-bg-indigo-solid'>
|
||||
<RiExchange2Fill className='h-4 w-4 text-components-avatar-shape-fill-stop-100' />
|
||||
</div>}
|
||||
onClick={() => {
|
||||
setAppMode('workflow')
|
||||
setAppMode(AppModeEnum.WORKFLOW)
|
||||
}} />
|
||||
<AppTypeCard
|
||||
active={appMode === 'advanced-chat'}
|
||||
active={appMode === AppModeEnum.ADVANCED_CHAT}
|
||||
title={t('app.types.advanced')}
|
||||
description={t('app.newApp.advancedShortDescription')}
|
||||
icon={<div className='flex h-6 w-6 items-center justify-center rounded-md bg-components-icon-bg-blue-light-solid'>
|
||||
<BubbleTextMod className='h-4 w-4 text-components-avatar-shape-fill-stop-100' />
|
||||
</div>}
|
||||
onClick={() => {
|
||||
setAppMode('advanced-chat')
|
||||
setAppMode(AppModeEnum.ADVANCED_CHAT)
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,34 +152,34 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
{isAppTypeExpanded && (
|
||||
<div className='flex flex-row gap-2'>
|
||||
<AppTypeCard
|
||||
active={appMode === 'chat'}
|
||||
active={appMode === AppModeEnum.CHAT}
|
||||
title={t('app.types.chatbot')}
|
||||
description={t('app.newApp.chatbotShortDescription')}
|
||||
icon={<div className='flex h-6 w-6 items-center justify-center rounded-md bg-components-icon-bg-blue-solid'>
|
||||
<ChatBot className='h-4 w-4 text-components-avatar-shape-fill-stop-100' />
|
||||
</div>}
|
||||
onClick={() => {
|
||||
setAppMode('chat')
|
||||
setAppMode(AppModeEnum.CHAT)
|
||||
}} />
|
||||
<AppTypeCard
|
||||
active={appMode === 'agent-chat'}
|
||||
active={appMode === AppModeEnum.AGENT_CHAT}
|
||||
title={t('app.types.agent')}
|
||||
description={t('app.newApp.agentShortDescription')}
|
||||
icon={<div className='flex h-6 w-6 items-center justify-center rounded-md bg-components-icon-bg-violet-solid'>
|
||||
<Logic className='h-4 w-4 text-components-avatar-shape-fill-stop-100' />
|
||||
</div>}
|
||||
onClick={() => {
|
||||
setAppMode('agent-chat')
|
||||
setAppMode(AppModeEnum.AGENT_CHAT)
|
||||
}} />
|
||||
<AppTypeCard
|
||||
active={appMode === 'completion'}
|
||||
active={appMode === AppModeEnum.COMPLETION}
|
||||
title={t('app.newApp.completeApp')}
|
||||
description={t('app.newApp.completionShortDescription')}
|
||||
icon={<div className='flex h-6 w-6 items-center justify-center rounded-md bg-components-icon-bg-teal-solid'>
|
||||
<ListSparkle className='h-4 w-4 text-components-avatar-shape-fill-stop-100' />
|
||||
</div>}
|
||||
onClick={() => {
|
||||
setAppMode('completion')
|
||||
setAppMode(AppModeEnum.COMPLETION)
|
||||
}} />
|
||||
</div>
|
||||
)}
|
||||
@@ -255,11 +255,11 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
<AppPreview mode={appMode} />
|
||||
<div className='absolute left-0 right-0 border-b border-b-divider-subtle'></div>
|
||||
<div className='flex h-[448px] w-[664px] items-center justify-center' style={{ background: 'repeating-linear-gradient(135deg, transparent, transparent 2px, rgba(16,24,40,0.04) 4px,transparent 3px, transparent 6px)' }}>
|
||||
<AppScreenShot show={appMode === 'chat'} mode='chat' />
|
||||
<AppScreenShot show={appMode === 'advanced-chat'} mode='advanced-chat' />
|
||||
<AppScreenShot show={appMode === 'agent-chat'} mode='agent-chat' />
|
||||
<AppScreenShot show={appMode === 'completion'} mode='completion' />
|
||||
<AppScreenShot show={appMode === 'workflow'} mode='workflow' />
|
||||
<AppScreenShot show={appMode === AppModeEnum.CHAT} mode={AppModeEnum.CHAT} />
|
||||
<AppScreenShot show={appMode === AppModeEnum.ADVANCED_CHAT} mode={AppModeEnum.ADVANCED_CHAT} />
|
||||
<AppScreenShot show={appMode === AppModeEnum.AGENT_CHAT} mode={AppModeEnum.AGENT_CHAT} />
|
||||
<AppScreenShot show={appMode === AppModeEnum.COMPLETION} mode={AppModeEnum.COMPLETION} />
|
||||
<AppScreenShot show={appMode === AppModeEnum.WORKFLOW} mode={AppModeEnum.WORKFLOW} />
|
||||
</div>
|
||||
<div className='absolute left-0 right-0 border-b border-b-divider-subtle'></div>
|
||||
</div>
|
||||
@@ -309,16 +309,16 @@ function AppTypeCard({ icon, title, description, active, onClick }: AppTypeCardP
|
||||
</div>
|
||||
}
|
||||
|
||||
function AppPreview({ mode }: { mode: AppMode }) {
|
||||
function AppPreview({ mode }: { mode: AppModeEnum }) {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const modeToPreviewInfoMap = {
|
||||
'chat': {
|
||||
[AppModeEnum.CHAT]: {
|
||||
title: t('app.types.chatbot'),
|
||||
description: t('app.newApp.chatbotUserDescription'),
|
||||
link: docLink('/guides/application-orchestrate/chatbot-application'),
|
||||
},
|
||||
'advanced-chat': {
|
||||
[AppModeEnum.ADVANCED_CHAT]: {
|
||||
title: t('app.types.advanced'),
|
||||
description: t('app.newApp.advancedUserDescription'),
|
||||
link: docLink('/guides/workflow/README', {
|
||||
@@ -326,12 +326,12 @@ function AppPreview({ mode }: { mode: AppMode }) {
|
||||
'ja-JP': '/guides/workflow/concepts',
|
||||
}),
|
||||
},
|
||||
'agent-chat': {
|
||||
[AppModeEnum.AGENT_CHAT]: {
|
||||
title: t('app.types.agent'),
|
||||
description: t('app.newApp.agentUserDescription'),
|
||||
link: docLink('/guides/application-orchestrate/agent'),
|
||||
},
|
||||
'completion': {
|
||||
[AppModeEnum.COMPLETION]: {
|
||||
title: t('app.newApp.completeApp'),
|
||||
description: t('app.newApp.completionUserDescription'),
|
||||
link: docLink('/guides/application-orchestrate/text-generator', {
|
||||
@@ -339,7 +339,7 @@ function AppPreview({ mode }: { mode: AppMode }) {
|
||||
'ja-JP': '/guides/application-orchestrate/README',
|
||||
}),
|
||||
},
|
||||
'workflow': {
|
||||
[AppModeEnum.WORKFLOW]: {
|
||||
title: t('app.types.workflow'),
|
||||
description: t('app.newApp.workflowUserDescription'),
|
||||
link: docLink('/guides/workflow/README', {
|
||||
@@ -358,14 +358,14 @@ function AppPreview({ mode }: { mode: AppMode }) {
|
||||
</div>
|
||||
}
|
||||
|
||||
function AppScreenShot({ mode, show }: { mode: AppMode; show: boolean }) {
|
||||
function AppScreenShot({ mode, show }: { mode: AppModeEnum; show: boolean }) {
|
||||
const { theme } = useTheme()
|
||||
const modeToImageMap = {
|
||||
'chat': 'Chatbot',
|
||||
'advanced-chat': 'Chatflow',
|
||||
'agent-chat': 'Agent',
|
||||
'completion': 'TextGenerator',
|
||||
'workflow': 'Workflow',
|
||||
[AppModeEnum.CHAT]: 'Chatbot',
|
||||
[AppModeEnum.ADVANCED_CHAT]: 'Chatflow',
|
||||
[AppModeEnum.AGENT_CHAT]: 'Agent',
|
||||
[AppModeEnum.COMPLETION]: 'TextGenerator',
|
||||
[AppModeEnum.WORKFLOW]: 'Workflow',
|
||||
}
|
||||
return <picture>
|
||||
<source media="(resolution: 1x)" srcSet={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}.png`} />
|
||||
|
||||
@@ -11,6 +11,7 @@ import Loading from '@/app/components/base/loading'
|
||||
import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
|
||||
import TabSlider from '@/app/components/base/tab-slider-plain'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
type Props = {
|
||||
pageType: PageType
|
||||
@@ -24,7 +25,7 @@ const LogAnnotation: FC<Props> = ({
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
|
||||
const options = useMemo(() => {
|
||||
if (appDetail?.mode === 'completion')
|
||||
if (appDetail?.mode === AppModeEnum.COMPLETION)
|
||||
return [{ value: PageType.log, text: t('appLog.title') }]
|
||||
return [
|
||||
{ value: PageType.log, text: t('appLog.title') },
|
||||
@@ -42,7 +43,7 @@ const LogAnnotation: FC<Props> = ({
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col px-6 pt-3'>
|
||||
{appDetail.mode !== 'workflow' && (
|
||||
{appDetail.mode !== AppModeEnum.WORKFLOW && (
|
||||
<TabSlider
|
||||
className='shrink-0'
|
||||
value={pageType}
|
||||
@@ -52,10 +53,10 @@ const LogAnnotation: FC<Props> = ({
|
||||
options={options}
|
||||
/>
|
||||
)}
|
||||
<div className={cn('h-0 grow', appDetail.mode !== 'workflow' && 'mt-3')}>
|
||||
{pageType === PageType.log && appDetail.mode !== 'workflow' && (<Log appDetail={appDetail} />)}
|
||||
<div className={cn('h-0 grow', appDetail.mode !== AppModeEnum.WORKFLOW && 'mt-3')}>
|
||||
{pageType === PageType.log && appDetail.mode !== AppModeEnum.WORKFLOW && (<Log appDetail={appDetail} />)}
|
||||
{pageType === PageType.annotation && (<Annotation appDetail={appDetail} />)}
|
||||
{pageType === PageType.log && appDetail.mode === 'workflow' && (<WorkflowLog appDetail={appDetail} />)}
|
||||
{pageType === PageType.log && appDetail.mode === AppModeEnum.WORKFLOW && (<WorkflowLog appDetail={appDetail} />)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -5,7 +5,8 @@ import Link from 'next/link'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { getRedirectionPath } from '@/utils/app-redirection'
|
||||
import type { App, AppMode } from '@/types/app'
|
||||
import type { App } from '@/types/app'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
const ThreeDotsIcon = ({ className }: SVGProps<SVGElement>) => {
|
||||
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||
@@ -16,9 +17,9 @@ const ThreeDotsIcon = ({ className }: SVGProps<SVGElement>) => {
|
||||
const EmptyElement: FC<{ appDetail: App }> = ({ appDetail }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getWebAppType = (appType: AppMode) => {
|
||||
if (appType !== 'completion' && appType !== 'workflow')
|
||||
return 'chat'
|
||||
const getWebAppType = (appType: AppModeEnum) => {
|
||||
if (appType !== AppModeEnum.COMPLETION && appType !== AppModeEnum.WORKFLOW)
|
||||
return AppModeEnum.CHAT
|
||||
return appType
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import Loading from '@/app/components/base/loading'
|
||||
import { fetchChatConversations, fetchCompletionConversations } from '@/service/log'
|
||||
import { APP_PAGE_LIMIT } from '@/config'
|
||||
import type { App } from '@/types/app'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
export type ILogsProps = {
|
||||
appDetail: App
|
||||
}
|
||||
@@ -37,7 +38,7 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
|
||||
const debouncedQueryParams = useDebounce(queryParams, { wait: 500 })
|
||||
|
||||
// Get the app type first
|
||||
const isChatMode = appDetail.mode !== 'completion'
|
||||
const isChatMode = appDetail.mode !== AppModeEnum.COMPLETION
|
||||
|
||||
const query = {
|
||||
page: currPage + 1,
|
||||
|
||||
@@ -20,7 +20,7 @@ import Indicator from '../../header/indicator'
|
||||
import VarPanel from './var-panel'
|
||||
import type { FeedbackFunc, FeedbackType, IChatItem, SubmitAnnotationFunc } from '@/app/components/base/chat/chat/type'
|
||||
import type { Annotation, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log'
|
||||
import type { App } from '@/types/app'
|
||||
import { type App, AppModeEnum } from '@/types/app'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
@@ -374,7 +374,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
|
||||
// Only load initial messages, don't auto-load more
|
||||
useEffect(() => {
|
||||
if (appDetail?.id && detail.id && appDetail?.mode !== 'completion' && !fetchInitiated.current) {
|
||||
if (appDetail?.id && detail.id && appDetail?.mode !== AppModeEnum.COMPLETION && !fetchInitiated.current) {
|
||||
// Mark as initialized, but don't auto-load more messages
|
||||
fetchInitiated.current = true
|
||||
// Still call fetchData to get initial messages
|
||||
@@ -583,8 +583,8 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
|
||||
}
|
||||
}, [hasMore, isLoading, loadMoreMessages])
|
||||
|
||||
const isChatMode = appDetail?.mode !== 'completion'
|
||||
const isAdvanced = appDetail?.mode === 'advanced-chat'
|
||||
const isChatMode = appDetail?.mode !== AppModeEnum.COMPLETION
|
||||
const isAdvanced = appDetail?.mode === AppModeEnum.ADVANCED_CHAT
|
||||
|
||||
const varList = (detail.model_config as any).user_input_form?.map((item: any) => {
|
||||
const itemContent = item[Object.keys(item)[0]]
|
||||
@@ -911,8 +911,8 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
||||
const closingConversationIdRef = useRef<string | null>(null)
|
||||
const pendingConversationIdRef = useRef<string | null>(null)
|
||||
const pendingConversationCacheRef = useRef<ConversationSelection | undefined>(undefined)
|
||||
const isChatMode = appDetail.mode !== 'completion' // Whether the app is a chat app
|
||||
const isChatflow = appDetail.mode === 'advanced-chat' // Whether the app is a chatflow app
|
||||
const isChatMode = appDetail.mode !== AppModeEnum.COMPLETION // Whether the app is a chat app
|
||||
const isChatflow = appDetail.mode === AppModeEnum.ADVANCED_CHAT // Whether the app is a chatflow app
|
||||
const { setShowPromptLogModal, setShowAgentLogModal, setShowMessageLogModal } = useAppStore(useShallow((state: AppStoreState) => ({
|
||||
setShowPromptLogModal: state.setShowPromptLogModal,
|
||||
setShowAgentLogModal: state.setShowAgentLogModal,
|
||||
|
||||
228
web/app/components/app/overview/__tests__/toggle-logic.test.ts
Normal file
228
web/app/components/app/overview/__tests__/toggle-logic.test.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { getWorkflowEntryNode } from '@/app/components/workflow/utils/workflow-entry'
|
||||
|
||||
// Mock the getWorkflowEntryNode function
|
||||
jest.mock('@/app/components/workflow/utils/workflow-entry', () => ({
|
||||
getWorkflowEntryNode: jest.fn(),
|
||||
}))
|
||||
|
||||
const mockGetWorkflowEntryNode = getWorkflowEntryNode as jest.MockedFunction<typeof getWorkflowEntryNode>
|
||||
|
||||
describe('App Card Toggle Logic', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
// Helper function that mirrors the actual logic from app-card.tsx
|
||||
const calculateToggleState = (
|
||||
appMode: string,
|
||||
currentWorkflow: any,
|
||||
isCurrentWorkspaceEditor: boolean,
|
||||
isCurrentWorkspaceManager: boolean,
|
||||
cardType: 'webapp' | 'api',
|
||||
) => {
|
||||
const isWorkflowApp = appMode === 'workflow'
|
||||
const appUnpublished = isWorkflowApp && !currentWorkflow?.graph
|
||||
const hasEntryNode = mockGetWorkflowEntryNode(currentWorkflow?.graph?.nodes || [])
|
||||
const missingEntryNode = isWorkflowApp && !hasEntryNode
|
||||
const hasInsufficientPermissions = cardType === 'webapp' ? !isCurrentWorkspaceEditor : !isCurrentWorkspaceManager
|
||||
const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingEntryNode
|
||||
const isMinimalState = appUnpublished || missingEntryNode
|
||||
|
||||
return {
|
||||
toggleDisabled,
|
||||
isMinimalState,
|
||||
appUnpublished,
|
||||
missingEntryNode,
|
||||
hasInsufficientPermissions,
|
||||
}
|
||||
}
|
||||
|
||||
describe('Entry Node Detection Logic', () => {
|
||||
it('should disable toggle when workflow missing entry node', () => {
|
||||
mockGetWorkflowEntryNode.mockReturnValue(false)
|
||||
|
||||
const result = calculateToggleState(
|
||||
'workflow',
|
||||
{ graph: { nodes: [] } },
|
||||
true,
|
||||
true,
|
||||
'webapp',
|
||||
)
|
||||
|
||||
expect(result.toggleDisabled).toBe(true)
|
||||
expect(result.missingEntryNode).toBe(true)
|
||||
expect(result.isMinimalState).toBe(true)
|
||||
})
|
||||
|
||||
it('should enable toggle when workflow has entry node', () => {
|
||||
mockGetWorkflowEntryNode.mockReturnValue(true)
|
||||
|
||||
const result = calculateToggleState(
|
||||
'workflow',
|
||||
{ graph: { nodes: [{ data: { type: 'start' } }] } },
|
||||
true,
|
||||
true,
|
||||
'webapp',
|
||||
)
|
||||
|
||||
expect(result.toggleDisabled).toBe(false)
|
||||
expect(result.missingEntryNode).toBe(false)
|
||||
expect(result.isMinimalState).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Published State Logic', () => {
|
||||
it('should disable toggle when workflow unpublished (no graph)', () => {
|
||||
const result = calculateToggleState(
|
||||
'workflow',
|
||||
null, // No workflow data = unpublished
|
||||
true,
|
||||
true,
|
||||
'webapp',
|
||||
)
|
||||
|
||||
expect(result.toggleDisabled).toBe(true)
|
||||
expect(result.appUnpublished).toBe(true)
|
||||
expect(result.isMinimalState).toBe(true)
|
||||
})
|
||||
|
||||
it('should disable toggle when workflow unpublished (empty graph)', () => {
|
||||
const result = calculateToggleState(
|
||||
'workflow',
|
||||
{}, // No graph property = unpublished
|
||||
true,
|
||||
true,
|
||||
'webapp',
|
||||
)
|
||||
|
||||
expect(result.toggleDisabled).toBe(true)
|
||||
expect(result.appUnpublished).toBe(true)
|
||||
expect(result.isMinimalState).toBe(true)
|
||||
})
|
||||
|
||||
it('should consider published state when workflow has graph', () => {
|
||||
mockGetWorkflowEntryNode.mockReturnValue(true)
|
||||
|
||||
const result = calculateToggleState(
|
||||
'workflow',
|
||||
{ graph: { nodes: [] } },
|
||||
true,
|
||||
true,
|
||||
'webapp',
|
||||
)
|
||||
|
||||
expect(result.appUnpublished).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Permissions Logic', () => {
|
||||
it('should disable webapp toggle when user lacks editor permissions', () => {
|
||||
mockGetWorkflowEntryNode.mockReturnValue(true)
|
||||
|
||||
const result = calculateToggleState(
|
||||
'workflow',
|
||||
{ graph: { nodes: [] } },
|
||||
false, // No editor permission
|
||||
true,
|
||||
'webapp',
|
||||
)
|
||||
|
||||
expect(result.toggleDisabled).toBe(true)
|
||||
expect(result.hasInsufficientPermissions).toBe(true)
|
||||
})
|
||||
|
||||
it('should disable api toggle when user lacks manager permissions', () => {
|
||||
mockGetWorkflowEntryNode.mockReturnValue(true)
|
||||
|
||||
const result = calculateToggleState(
|
||||
'workflow',
|
||||
{ graph: { nodes: [] } },
|
||||
true,
|
||||
false, // No manager permission
|
||||
'api',
|
||||
)
|
||||
|
||||
expect(result.toggleDisabled).toBe(true)
|
||||
expect(result.hasInsufficientPermissions).toBe(true)
|
||||
})
|
||||
|
||||
it('should enable toggle when user has proper permissions', () => {
|
||||
mockGetWorkflowEntryNode.mockReturnValue(true)
|
||||
|
||||
const webappResult = calculateToggleState(
|
||||
'workflow',
|
||||
{ graph: { nodes: [] } },
|
||||
true, // Has editor permission
|
||||
false,
|
||||
'webapp',
|
||||
)
|
||||
|
||||
const apiResult = calculateToggleState(
|
||||
'workflow',
|
||||
{ graph: { nodes: [] } },
|
||||
false,
|
||||
true, // Has manager permission
|
||||
'api',
|
||||
)
|
||||
|
||||
expect(webappResult.toggleDisabled).toBe(false)
|
||||
expect(apiResult.toggleDisabled).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Combined Conditions Logic', () => {
|
||||
it('should handle multiple disable conditions correctly', () => {
|
||||
mockGetWorkflowEntryNode.mockReturnValue(false)
|
||||
|
||||
const result = calculateToggleState(
|
||||
'workflow',
|
||||
null, // Unpublished
|
||||
false, // No permissions
|
||||
false,
|
||||
'webapp',
|
||||
)
|
||||
|
||||
// All three conditions should be true
|
||||
expect(result.appUnpublished).toBe(true)
|
||||
expect(result.missingEntryNode).toBe(true)
|
||||
expect(result.hasInsufficientPermissions).toBe(true)
|
||||
expect(result.toggleDisabled).toBe(true)
|
||||
expect(result.isMinimalState).toBe(true)
|
||||
})
|
||||
|
||||
it('should enable when all conditions are satisfied', () => {
|
||||
mockGetWorkflowEntryNode.mockReturnValue(true)
|
||||
|
||||
const result = calculateToggleState(
|
||||
'workflow',
|
||||
{ graph: { nodes: [{ data: { type: 'start' } }] } }, // Published
|
||||
true, // Has permissions
|
||||
true,
|
||||
'webapp',
|
||||
)
|
||||
|
||||
expect(result.appUnpublished).toBe(false)
|
||||
expect(result.missingEntryNode).toBe(false)
|
||||
expect(result.hasInsufficientPermissions).toBe(false)
|
||||
expect(result.toggleDisabled).toBe(false)
|
||||
expect(result.isMinimalState).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Non-Workflow Apps', () => {
|
||||
it('should not check workflow-specific conditions for non-workflow apps', () => {
|
||||
const result = calculateToggleState(
|
||||
'chat', // Non-workflow mode
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
'webapp',
|
||||
)
|
||||
|
||||
expect(result.appUnpublished).toBe(false) // isWorkflowApp is false
|
||||
expect(result.missingEntryNode).toBe(false) // isWorkflowApp is false
|
||||
expect(result.toggleDisabled).toBe(false)
|
||||
expect(result.isMinimalState).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -9,6 +9,7 @@ import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/gene
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
|
||||
const APIKeyInfoPanel: FC = () => {
|
||||
const isCloud = !IS_CE_EDITION
|
||||
@@ -47,7 +48,7 @@ const APIKeyInfoPanel: FC = () => {
|
||||
<Button
|
||||
variant='primary'
|
||||
className='mt-2 space-x-2'
|
||||
onClick={() => setShowAccountSettingModal({ payload: 'provider' })}
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })}
|
||||
>
|
||||
<div className='text-sm font-medium'>{t('appOverview.apiKeyInfo.setAPIBtn')}</div>
|
||||
<LinkExternal02 className='h-4 w-4' />
|
||||
|
||||
@@ -39,7 +39,11 @@ import { fetchAppDetailDirect } from '@/service/apps'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import AccessControl from '../app-access-control'
|
||||
import { useAppWhiteListSubjects } from '@/service/access-control'
|
||||
import { useAppWorkflow } from '@/service/use-workflow'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
export type IAppCardProps = {
|
||||
className?: string
|
||||
@@ -65,6 +69,8 @@ function AppCard({
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const { isCurrentWorkspaceManager, isCurrentWorkspaceEditor } = useAppContext()
|
||||
const { data: currentWorkflow } = useAppWorkflow(appInfo.mode === AppModeEnum.WORKFLOW ? appInfo.id : '')
|
||||
const docLink = useDocLink()
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
const [showSettingsModal, setShowSettingsModal] = useState(false)
|
||||
@@ -85,7 +91,7 @@ function AppCard({
|
||||
api: [{ opName: t('appOverview.overview.apiInfo.doc'), opIcon: RiBookOpenLine }],
|
||||
app: [],
|
||||
}
|
||||
if (appInfo.mode !== 'completion' && appInfo.mode !== 'workflow')
|
||||
if (appInfo.mode !== AppModeEnum.COMPLETION && appInfo.mode !== AppModeEnum.WORKFLOW)
|
||||
operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.embedded.entry'), opIcon: RiWindowLine })
|
||||
|
||||
operationsMap.webapp.push({ opName: t('appOverview.overview.appInfo.customize.entry'), opIcon: RiPaintBrushLine })
|
||||
@@ -98,12 +104,18 @@ function AppCard({
|
||||
|
||||
const isApp = cardType === 'webapp'
|
||||
const basicName = isApp
|
||||
? appInfo?.site?.title
|
||||
? t('appOverview.overview.appInfo.title')
|
||||
: t('appOverview.overview.apiInfo.title')
|
||||
const toggleDisabled = isApp ? !isCurrentWorkspaceEditor : !isCurrentWorkspaceManager
|
||||
const runningStatus = isApp ? appInfo.enable_site : appInfo.enable_api
|
||||
const isWorkflowApp = appInfo.mode === AppModeEnum.WORKFLOW
|
||||
const appUnpublished = isWorkflowApp && !currentWorkflow?.graph
|
||||
const hasStartNode = currentWorkflow?.graph?.nodes?.some(node => node.data.type === BlockEnum.Start)
|
||||
const missingStartNode = isWorkflowApp && !hasStartNode
|
||||
const hasInsufficientPermissions = isApp ? !isCurrentWorkspaceEditor : !isCurrentWorkspaceManager
|
||||
const toggleDisabled = hasInsufficientPermissions || appUnpublished || missingStartNode
|
||||
const runningStatus = (appUnpublished || missingStartNode) ? false : (isApp ? appInfo.enable_site : appInfo.enable_api)
|
||||
const isMinimalState = appUnpublished || missingStartNode
|
||||
const { app_base_url, access_token } = appInfo.site ?? {}
|
||||
const appMode = (appInfo.mode !== 'completion' && appInfo.mode !== 'workflow') ? 'chat' : appInfo.mode
|
||||
const appMode = (appInfo.mode !== AppModeEnum.COMPLETION && appInfo.mode !== AppModeEnum.WORKFLOW) ? AppModeEnum.CHAT : appInfo.mode
|
||||
const appUrl = `${app_base_url}${basePath}/${appMode}/${access_token}`
|
||||
const apiUrl = appInfo?.api_base_url
|
||||
|
||||
@@ -175,10 +187,10 @@ function AppCard({
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
`${isInPanel ? 'border-l-[0.5px] border-t' : 'border-[0.5px] shadow-xs'} w-full max-w-full rounded-xl border-effects-highlight ${className ?? ''}`}
|
||||
`${isInPanel ? 'border-l-[0.5px] border-t' : 'border-[0.5px] shadow-xs'} w-full max-w-full rounded-xl border-effects-highlight ${className ?? ''} ${isMinimalState ? 'h-12' : ''}`}
|
||||
>
|
||||
<div className={`${customBgColor ?? 'bg-background-default'} rounded-xl`}>
|
||||
<div className='flex w-full flex-col items-start justify-center gap-3 self-stretch border-b-[0.5px] border-divider-subtle p-3'>
|
||||
<div className={`flex w-full flex-col items-start justify-center gap-3 self-stretch p-3 ${isMinimalState ? 'border-0' : 'border-b-[0.5px] border-divider-subtle'}`}>
|
||||
<div className='flex w-full items-center gap-3 self-stretch'>
|
||||
<AppBasic
|
||||
iconType={cardType}
|
||||
@@ -200,58 +212,83 @@ function AppCard({
|
||||
: t('appOverview.overview.status.disable')}
|
||||
</div>
|
||||
</div>
|
||||
<Switch defaultValue={runningStatus} onChange={onChangeStatus} disabled={toggleDisabled} />
|
||||
</div>
|
||||
<div className='flex flex-col items-start justify-center self-stretch'>
|
||||
<div className="system-xs-medium pb-1 text-text-tertiary">
|
||||
{isApp
|
||||
? t('appOverview.overview.appInfo.accessibleAddress')
|
||||
: t('appOverview.overview.apiInfo.accessibleAddress')}
|
||||
</div>
|
||||
<div className="inline-flex h-9 w-full items-center gap-0.5 rounded-lg bg-components-input-bg-normal p-1 pl-2">
|
||||
<div className="flex h-4 min-w-0 flex-1 items-start justify-start gap-2 px-1">
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-xs font-medium text-text-secondary">
|
||||
{isApp ? appUrl : apiUrl}
|
||||
</div>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
toggleDisabled && (appUnpublished || missingStartNode) ? (
|
||||
<>
|
||||
<div className="mb-1 text-xs font-normal text-text-secondary">
|
||||
{t('appOverview.overview.appInfo.enableTooltip.description')}
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer text-xs font-normal text-text-accent hover:underline"
|
||||
onClick={() => window.open(docLink('/guides/workflow/node/user-input'), '_blank')}
|
||||
>
|
||||
{t('appOverview.overview.appInfo.enableTooltip.learnMore')}
|
||||
</div>
|
||||
</>
|
||||
) : ''
|
||||
}
|
||||
position="right"
|
||||
popupClassName="w-58 max-w-60 rounded-xl bg-components-panel-bg px-3.5 py-3 shadow-lg"
|
||||
offset={24}
|
||||
>
|
||||
<div>
|
||||
<Switch defaultValue={runningStatus} onChange={onChangeStatus} disabled={toggleDisabled} />
|
||||
</div>
|
||||
<CopyFeedback
|
||||
content={isApp ? appUrl : apiUrl}
|
||||
className={'!size-6'}
|
||||
/>
|
||||
{isApp && <ShareQRCode content={isApp ? appUrl : apiUrl} />}
|
||||
{isApp && <Divider type="vertical" className="!mx-0.5 !h-3.5 shrink-0" />}
|
||||
{/* button copy link/ button regenerate */}
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
type='warning'
|
||||
title={t('appOverview.overview.appInfo.regenerate')}
|
||||
content={t('appOverview.overview.appInfo.regenerateNotice')}
|
||||
isShow={showConfirmDelete}
|
||||
onConfirm={() => {
|
||||
onGenCode()
|
||||
setShowConfirmDelete(false)
|
||||
}}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
{!isMinimalState && (
|
||||
<div className='flex flex-col items-start justify-center self-stretch'>
|
||||
<div className="system-xs-medium pb-1 text-text-tertiary">
|
||||
{isApp
|
||||
? t('appOverview.overview.appInfo.accessibleAddress')
|
||||
: t('appOverview.overview.apiInfo.accessibleAddress')}
|
||||
</div>
|
||||
<div className="inline-flex h-9 w-full items-center gap-0.5 rounded-lg bg-components-input-bg-normal p-1 pl-2">
|
||||
<div className="flex h-4 min-w-0 flex-1 items-start justify-start gap-2 px-1">
|
||||
<div className="overflow-hidden text-ellipsis whitespace-nowrap text-xs font-medium text-text-secondary">
|
||||
{isApp ? appUrl : apiUrl}
|
||||
</div>
|
||||
</div>
|
||||
<CopyFeedback
|
||||
content={isApp ? appUrl : apiUrl}
|
||||
className={'!size-6'}
|
||||
/>
|
||||
)}
|
||||
{isApp && isCurrentWorkspaceManager && (
|
||||
<Tooltip
|
||||
popupContent={t('appOverview.overview.appInfo.regenerate') || ''}
|
||||
>
|
||||
<div
|
||||
className="h-6 w-6 cursor-pointer rounded-md hover:bg-state-base-hover"
|
||||
onClick={() => setShowConfirmDelete(true)}
|
||||
{isApp && <ShareQRCode content={isApp ? appUrl : apiUrl} />}
|
||||
{isApp && <Divider type="vertical" className="!mx-0.5 !h-3.5 shrink-0" />}
|
||||
{/* button copy link/ button regenerate */}
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
type='warning'
|
||||
title={t('appOverview.overview.appInfo.regenerate')}
|
||||
content={t('appOverview.overview.appInfo.regenerateNotice')}
|
||||
isShow={showConfirmDelete}
|
||||
onConfirm={() => {
|
||||
onGenCode()
|
||||
setShowConfirmDelete(false)
|
||||
}}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
/>
|
||||
)}
|
||||
{isApp && isCurrentWorkspaceManager && (
|
||||
<Tooltip
|
||||
popupContent={t('appOverview.overview.appInfo.regenerate') || ''}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
`h-full w-full ${style.refreshIcon} ${genLoading ? style.generateLogo : ''}`}
|
||||
></div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
className="h-6 w-6 cursor-pointer rounded-md hover:bg-state-base-hover"
|
||||
onClick={() => setShowConfirmDelete(true)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
`h-full w-full ${style.refreshIcon} ${genLoading ? style.generateLogo : ''}`}
|
||||
></div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isApp && systemFeatures.webapp_auth.enabled && appDetail && <div className='flex flex-col items-start justify-center self-stretch'>
|
||||
)}
|
||||
{!isMinimalState && isApp && systemFeatures.webapp_auth.enabled && appDetail && <div className='flex flex-col items-start justify-center self-stretch'>
|
||||
<div className="system-xs-medium pb-1 text-text-tertiary">{t('app.publishApp.title')}</div>
|
||||
<div className='flex h-9 w-full cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal py-1 pl-2.5 pr-2'
|
||||
onClick={handleClickAccessControl}>
|
||||
@@ -287,43 +324,45 @@ function AppCard({
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
<div className={'flex items-center gap-1 self-stretch p-3'}>
|
||||
{!isApp && <SecretKeyButton appId={appInfo.id} />}
|
||||
{OPERATIONS_MAP[cardType].map((op) => {
|
||||
const disabled
|
||||
= op.opName === t('appOverview.overview.appInfo.settings.entry')
|
||||
? false
|
||||
: !runningStatus
|
||||
return (
|
||||
<Button
|
||||
className="mr-1 min-w-[88px]"
|
||||
size="small"
|
||||
variant={'ghost'}
|
||||
key={op.opName}
|
||||
onClick={genClickFuncByName(op.opName)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
t('appOverview.overview.appInfo.preUseReminder') ?? ''
|
||||
}
|
||||
popupClassName={disabled ? 'mt-[-8px]' : '!hidden'}
|
||||
{!isMinimalState && (
|
||||
<div className={'flex items-center gap-1 self-stretch p-3'}>
|
||||
{!isApp && <SecretKeyButton appId={appInfo.id} />}
|
||||
{OPERATIONS_MAP[cardType].map((op) => {
|
||||
const disabled
|
||||
= op.opName === t('appOverview.overview.appInfo.settings.entry')
|
||||
? false
|
||||
: !runningStatus
|
||||
return (
|
||||
<Button
|
||||
className="mr-1 min-w-[88px]"
|
||||
size="small"
|
||||
variant={'ghost'}
|
||||
key={op.opName}
|
||||
onClick={genClickFuncByName(op.opName)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-[1px]">
|
||||
<op.opIcon className="h-3.5 w-3.5" />
|
||||
<div className={`${(runningStatus || !disabled) ? 'text-text-tertiary' : 'text-components-button-ghost-text-disabled'} system-xs-medium px-[3px]`}>{op.opName}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
t('appOverview.overview.appInfo.preUseReminder') ?? ''
|
||||
}
|
||||
popupClassName={disabled ? 'mt-[-8px]' : '!hidden'}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-[1px]">
|
||||
<op.opIcon className="h-3.5 w-3.5" />
|
||||
<div className={`${(runningStatus || !disabled) ? 'text-text-tertiary' : 'text-components-button-ghost-text-disabled'} system-xs-medium px-[3px]`}>{op.opName}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isApp
|
||||
? (
|
||||
<>
|
||||
<SettingsModal
|
||||
isChat={appMode === 'chat'}
|
||||
isChat={appMode === AppModeEnum.CHAT}
|
||||
appInfo={appInfo}
|
||||
isShow={showSettingsModal}
|
||||
onClose={() => setShowSettingsModal(false)}
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from 'react'
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import type { AppMode } from '@/types/app'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Tag from '@/app/components/base/tag'
|
||||
@@ -15,7 +15,7 @@ type IShareLinkProps = {
|
||||
linkUrl: string
|
||||
api_base_url: string
|
||||
appId: string
|
||||
mode: AppMode
|
||||
mode: AppModeEnum
|
||||
}
|
||||
|
||||
const StepNum: FC<{ children: React.ReactNode }> = ({ children }) =>
|
||||
@@ -42,7 +42,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const isChatApp = mode === 'chat' || mode === 'advanced-chat'
|
||||
const isChatApp = mode === AppModeEnum.CHAT || mode === AppModeEnum.ADVANCED_CHAT
|
||||
|
||||
return <Modal
|
||||
title={t(`${prefixCustomize}.title`)}
|
||||
|
||||
@@ -16,12 +16,13 @@ import Switch from '@/app/components/base/switch'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import type { AppIconType, AppSSO, Language } from '@/types/app'
|
||||
import { type AppIconType, AppModeEnum, type AppSSO, type Language } from '@/types/app'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
|
||||
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||
import cn from '@/utils/classnames'
|
||||
@@ -113,7 +114,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
if (isFreePlan)
|
||||
setShowPricingModal()
|
||||
else
|
||||
setShowAccountSettingModal({ payload: 'billing' })
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
|
||||
}, [isFreePlan, setShowAccountSettingModal, setShowPricingModal])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -328,7 +329,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className={cn('system-sm-semibold py-1 text-text-secondary')}>{t(`${prefixSettings}.workflow.subTitle`)}</div>
|
||||
<Switch
|
||||
disabled={!(appInfo.mode === 'workflow' || appInfo.mode === 'advanced-chat')}
|
||||
disabled={!(appInfo.mode === AppModeEnum.WORKFLOW || appInfo.mode === AppModeEnum.ADVANCED_CHAT)}
|
||||
defaultValue={inputInfo.show_workflow_steps}
|
||||
onChange={v => setInputInfo({ ...inputInfo, show_workflow_steps: v })}
|
||||
/>
|
||||
|
||||
224
web/app/components/app/overview/trigger-card.tsx
Normal file
224
web/app/components/app/overview/trigger-card.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import { TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import type { AppSSO } from '@/types/app'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import {
|
||||
type AppTrigger,
|
||||
useAppTriggers,
|
||||
useInvalidateAppTriggers,
|
||||
useUpdateTriggerStatus,
|
||||
} from '@/service/use-tools'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { useTriggerStatusStore } from '@/app/components/workflow/store/trigger-status'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
|
||||
export type ITriggerCardProps = {
|
||||
appInfo: AppDetailResponse & Partial<AppSSO>
|
||||
onToggleResult?: (err: Error | null, message?: string) => void
|
||||
}
|
||||
|
||||
const getTriggerIcon = (trigger: AppTrigger, triggerPlugins: any[]) => {
|
||||
const { trigger_type, status, provider_name } = trigger
|
||||
|
||||
// Status dot styling based on trigger status
|
||||
const getStatusDot = () => {
|
||||
if (status === 'enabled') {
|
||||
return (
|
||||
<div className="absolute -left-0.5 -top-0.5 h-1.5 w-1.5 rounded-sm border border-black/15 bg-green-500" />
|
||||
)
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<div className="absolute -left-0.5 -top-0.5 h-1.5 w-1.5 rounded-sm border border-components-badge-status-light-disabled-border-inner bg-components-badge-status-light-disabled-bg shadow-status-indicator-gray-shadow" />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Get BlockEnum type from trigger_type
|
||||
let blockType: BlockEnum
|
||||
switch (trigger_type) {
|
||||
case 'trigger-webhook':
|
||||
blockType = BlockEnum.TriggerWebhook
|
||||
break
|
||||
case 'trigger-schedule':
|
||||
blockType = BlockEnum.TriggerSchedule
|
||||
break
|
||||
case 'trigger-plugin':
|
||||
blockType = BlockEnum.TriggerPlugin
|
||||
break
|
||||
default:
|
||||
blockType = BlockEnum.TriggerWebhook
|
||||
}
|
||||
|
||||
let triggerIcon: string | undefined
|
||||
if (trigger_type === 'trigger-plugin' && provider_name) {
|
||||
const targetTriggers = triggerPlugins || []
|
||||
const foundTrigger = targetTriggers.find(triggerWithProvider =>
|
||||
canFindTool(triggerWithProvider.id, provider_name)
|
||||
|| triggerWithProvider.id.includes(provider_name)
|
||||
|| triggerWithProvider.name === provider_name,
|
||||
)
|
||||
triggerIcon = foundTrigger?.icon
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<BlockIcon
|
||||
type={blockType}
|
||||
size="md"
|
||||
toolIcon={triggerIcon}
|
||||
/>
|
||||
{getStatusDot()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TriggerCard({ appInfo, onToggleResult }: ITriggerCardProps) {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const appId = appInfo.id
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const { data: triggersResponse, isLoading } = useAppTriggers(appId)
|
||||
const { mutateAsync: updateTriggerStatus } = useUpdateTriggerStatus()
|
||||
const invalidateAppTriggers = useInvalidateAppTriggers()
|
||||
const { data: triggerPlugins } = useAllTriggerPlugins()
|
||||
|
||||
// Zustand store for trigger status sync
|
||||
const { setTriggerStatus, setTriggerStatuses } = useTriggerStatusStore()
|
||||
|
||||
const triggers = triggersResponse?.data || []
|
||||
const triggerCount = triggers.length
|
||||
|
||||
// Sync trigger statuses to Zustand store when data loads initially or after API calls
|
||||
React.useEffect(() => {
|
||||
if (triggers.length > 0) {
|
||||
const statusMap = triggers.reduce((acc, trigger) => {
|
||||
// Map API status to EntryNodeStatus: only 'enabled' shows green, others show gray
|
||||
acc[trigger.node_id] = trigger.status === 'enabled' ? 'enabled' : 'disabled'
|
||||
return acc
|
||||
}, {} as Record<string, 'enabled' | 'disabled'>)
|
||||
|
||||
// Only update if there are actual changes to prevent overriding optimistic updates
|
||||
setTriggerStatuses(statusMap)
|
||||
}
|
||||
}, [triggers, setTriggerStatuses])
|
||||
|
||||
const onToggleTrigger = async (trigger: AppTrigger, enabled: boolean) => {
|
||||
try {
|
||||
// Immediately update Zustand store for real-time UI sync
|
||||
const newStatus = enabled ? 'enabled' : 'disabled'
|
||||
setTriggerStatus(trigger.node_id, newStatus)
|
||||
|
||||
await updateTriggerStatus({
|
||||
appId,
|
||||
triggerId: trigger.id,
|
||||
enableTrigger: enabled,
|
||||
})
|
||||
invalidateAppTriggers(appId)
|
||||
|
||||
// Success toast notification
|
||||
onToggleResult?.(null)
|
||||
}
|
||||
catch (error) {
|
||||
// Rollback Zustand store state on error
|
||||
const rollbackStatus = enabled ? 'disabled' : 'enabled'
|
||||
setTriggerStatus(trigger.node_id, rollbackStatus)
|
||||
|
||||
// Error toast notification
|
||||
onToggleResult?.(error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="w-full max-w-full rounded-xl border-l-[0.5px] border-t border-effects-highlight">
|
||||
<div className="rounded-xl bg-background-default">
|
||||
<div className="flex w-full flex-col items-start justify-center gap-3 self-stretch border-b-[0.5px] border-divider-subtle p-3">
|
||||
<div className="h-6 w-full animate-pulse rounded bg-components-input-bg-normal"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-full rounded-xl border-l-[0.5px] border-t border-effects-highlight">
|
||||
<div className="rounded-xl bg-background-default">
|
||||
<div className="flex w-full flex-col items-start justify-center gap-3 self-stretch border-b-[0.5px] border-divider-subtle p-3">
|
||||
<div className="flex w-full items-center gap-3 self-stretch">
|
||||
<div className="flex grow items-center">
|
||||
<div className="mr-2 shrink-0 rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-purple-purple-500 p-1 shadow-md">
|
||||
<TriggerAll className="h-4 w-4 text-text-primary-on-surface" />
|
||||
</div>
|
||||
<div className="group w-full">
|
||||
<div className="system-md-semibold min-w-0 overflow-hidden text-ellipsis break-normal text-text-secondary group-hover:text-text-primary">
|
||||
{triggerCount > 0
|
||||
? t('appOverview.overview.triggerInfo.triggersAdded', { count: triggerCount })
|
||||
: t('appOverview.overview.triggerInfo.noTriggerAdded')
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{triggerCount > 0 && (
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
{triggers.map(trigger => (
|
||||
<div key={trigger.id} className="flex w-full items-center gap-3">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<div className="shrink-0">
|
||||
{getTriggerIcon(trigger, triggerPlugins || [])}
|
||||
</div>
|
||||
<div className="system-sm-medium min-w-0 flex-1 truncate text-text-secondary">
|
||||
{trigger.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center">
|
||||
<div className={`${trigger.status === 'enabled' ? 'text-text-success' : 'text-text-warning'} system-xs-semibold-uppercase whitespace-nowrap`}>
|
||||
{trigger.status === 'enabled'
|
||||
? t('appOverview.overview.status.running')
|
||||
: t('appOverview.overview.status.disable')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<Switch
|
||||
defaultValue={trigger.status === 'enabled'}
|
||||
onChange={enabled => onToggleTrigger(trigger, enabled)}
|
||||
disabled={!isCurrentWorkspaceEditor}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{triggerCount === 0 && (
|
||||
<div className="p-3">
|
||||
<div className="system-xs-regular leading-4 text-text-tertiary">
|
||||
{t('appOverview.overview.triggerInfo.triggerStatusDescription')}{' '}
|
||||
<Link
|
||||
href={docLink('/guides/workflow/node/trigger')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-text-accent hover:underline"
|
||||
>
|
||||
{t('appOverview.overview.triggerInfo.learnAboutTriggers')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TriggerCard
|
||||
@@ -24,6 +24,7 @@ import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/aler
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { noop } from 'lodash-es'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
type SwitchAppModalProps = {
|
||||
show: boolean
|
||||
@@ -77,7 +78,7 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
|
||||
isCurrentWorkspaceEditor,
|
||||
{
|
||||
id: newAppID,
|
||||
mode: appDetail.mode === 'completion' ? 'workflow' : 'advanced-chat',
|
||||
mode: appDetail.mode === AppModeEnum.COMPLETION ? AppModeEnum.WORKFLOW : AppModeEnum.ADVANCED_CHAT,
|
||||
},
|
||||
removeOriginal ? replace : push,
|
||||
)
|
||||
|
||||
@@ -9,13 +9,14 @@ import {
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { BubbleTextMod, ChatBot, ListSparkle, Logic } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import type { AppMode } from '@/types/app'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
export type AppSelectorProps = {
|
||||
value: Array<AppMode>
|
||||
value: Array<AppModeEnum>
|
||||
onChange: (value: AppSelectorProps['value']) => void
|
||||
}
|
||||
|
||||
const allTypes: AppMode[] = ['workflow', 'advanced-chat', 'chat', 'agent-chat', 'completion']
|
||||
const allTypes: AppModeEnum[] = [AppModeEnum.WORKFLOW, AppModeEnum.ADVANCED_CHAT, AppModeEnum.CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.COMPLETION]
|
||||
|
||||
const AppTypeSelector = ({ value, onChange }: AppSelectorProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
@@ -66,7 +67,7 @@ const AppTypeSelector = ({ value, onChange }: AppSelectorProps) => {
|
||||
export default AppTypeSelector
|
||||
|
||||
type AppTypeIconProps = {
|
||||
type: AppMode
|
||||
type: AppModeEnum
|
||||
style?: React.CSSProperties
|
||||
className?: string
|
||||
wrapperClassName?: string
|
||||
@@ -75,27 +76,27 @@ type AppTypeIconProps = {
|
||||
export const AppTypeIcon = React.memo(({ type, className, wrapperClassName, style }: AppTypeIconProps) => {
|
||||
const wrapperClassNames = cn('inline-flex h-5 w-5 items-center justify-center rounded-md border border-divider-regular', wrapperClassName)
|
||||
const iconClassNames = cn('h-3.5 w-3.5 text-components-avatar-shape-fill-stop-100', className)
|
||||
if (type === 'chat') {
|
||||
if (type === AppModeEnum.CHAT) {
|
||||
return <div style={style} className={cn(wrapperClassNames, 'bg-components-icon-bg-blue-solid')}>
|
||||
<ChatBot className={iconClassNames} />
|
||||
</div>
|
||||
}
|
||||
if (type === 'agent-chat') {
|
||||
if (type === AppModeEnum.AGENT_CHAT) {
|
||||
return <div style={style} className={cn(wrapperClassNames, 'bg-components-icon-bg-violet-solid')}>
|
||||
<Logic className={iconClassNames} />
|
||||
</div>
|
||||
}
|
||||
if (type === 'advanced-chat') {
|
||||
if (type === AppModeEnum.ADVANCED_CHAT) {
|
||||
return <div style={style} className={cn(wrapperClassNames, 'bg-components-icon-bg-blue-light-solid')}>
|
||||
<BubbleTextMod className={iconClassNames} />
|
||||
</div>
|
||||
}
|
||||
if (type === 'workflow') {
|
||||
if (type === AppModeEnum.WORKFLOW) {
|
||||
return <div style={style} className={cn(wrapperClassNames, 'bg-components-icon-bg-indigo-solid')}>
|
||||
<RiExchange2Fill className={iconClassNames} />
|
||||
</div>
|
||||
}
|
||||
if (type === 'completion') {
|
||||
if (type === AppModeEnum.COMPLETION) {
|
||||
return <div style={style} className={cn(wrapperClassNames, 'bg-components-icon-bg-teal-solid')}>
|
||||
<ListSparkle className={iconClassNames} />
|
||||
</div>
|
||||
@@ -133,7 +134,7 @@ function AppTypeSelectTrigger({ values }: { readonly values: AppSelectorProps['v
|
||||
|
||||
type AppTypeSelectorItemProps = {
|
||||
checked: boolean
|
||||
type: AppMode
|
||||
type: AppModeEnum
|
||||
onClick: () => void
|
||||
}
|
||||
function AppTypeSelectorItem({ checked, type, onClick }: AppTypeSelectorItemProps) {
|
||||
@@ -147,21 +148,21 @@ function AppTypeSelectorItem({ checked, type, onClick }: AppTypeSelectorItemProp
|
||||
}
|
||||
|
||||
type AppTypeLabelProps = {
|
||||
type: AppMode
|
||||
type: AppModeEnum
|
||||
className?: string
|
||||
}
|
||||
export function AppTypeLabel({ type, className }: AppTypeLabelProps) {
|
||||
const { t } = useTranslation()
|
||||
let label = ''
|
||||
if (type === 'chat')
|
||||
if (type === AppModeEnum.CHAT)
|
||||
label = t('app.typeSelector.chatbot')
|
||||
if (type === 'agent-chat')
|
||||
if (type === AppModeEnum.AGENT_CHAT)
|
||||
label = t('app.typeSelector.agent')
|
||||
if (type === 'completion')
|
||||
if (type === AppModeEnum.COMPLETION)
|
||||
label = t('app.typeSelector.completion')
|
||||
if (type === 'advanced-chat')
|
||||
if (type === AppModeEnum.ADVANCED_CHAT)
|
||||
label = t('app.typeSelector.advanced')
|
||||
if (type === 'workflow')
|
||||
if (type === AppModeEnum.WORKFLOW)
|
||||
label = t('app.typeSelector.workflow')
|
||||
|
||||
return <span className={className}>{label}</span>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCloseLine, RiPlayLargeLine } from '@remixicon/react'
|
||||
import Run from '@/app/components/workflow/run'
|
||||
import { WorkflowContextProvider } from '@/app/components/workflow/context'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import TooltipPlus from '@/app/components/base/tooltip'
|
||||
import { useRouter } from 'next/navigation'
|
||||
@@ -10,9 +11,10 @@ import { useRouter } from 'next/navigation'
|
||||
type ILogDetail = {
|
||||
runID: string
|
||||
onClose: () => void
|
||||
canReplay?: boolean
|
||||
}
|
||||
|
||||
const DetailPanel: FC<ILogDetail> = ({ runID, onClose }) => {
|
||||
const DetailPanel: FC<ILogDetail> = ({ runID, onClose, canReplay = false }) => {
|
||||
const { t } = useTranslation()
|
||||
const appDetail = useStore(state => state.appDetail)
|
||||
const router = useRouter()
|
||||
@@ -29,24 +31,28 @@ const DetailPanel: FC<ILogDetail> = ({ runID, onClose }) => {
|
||||
</span>
|
||||
<div className='flex items-center bg-components-panel-bg'>
|
||||
<h1 className='system-xl-semibold shrink-0 px-4 py-1 text-text-primary'>{t('appLog.runDetail.workflowTitle')}</h1>
|
||||
<TooltipPlus
|
||||
popupContent={t('appLog.runDetail.testWithParams')}
|
||||
popupClassName='rounded-xl'
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='mr-1 flex h-6 w-6 items-center justify-center rounded-md hover:bg-state-base-hover'
|
||||
aria-label={t('appLog.runDetail.testWithParams')}
|
||||
onClick={handleReplay}
|
||||
{canReplay && (
|
||||
<TooltipPlus
|
||||
popupContent={t('appLog.runDetail.testWithParams')}
|
||||
popupClassName='rounded-xl'
|
||||
>
|
||||
<RiPlayLargeLine className='h-4 w-4 text-text-tertiary' />
|
||||
</button>
|
||||
</TooltipPlus>
|
||||
<button
|
||||
type='button'
|
||||
className='mr-1 flex h-6 w-6 items-center justify-center rounded-md hover:bg-state-base-hover'
|
||||
aria-label={t('appLog.runDetail.testWithParams')}
|
||||
onClick={handleReplay}
|
||||
>
|
||||
<RiPlayLargeLine className='h-4 w-4 text-text-tertiary' />
|
||||
</button>
|
||||
</TooltipPlus>
|
||||
)}
|
||||
</div>
|
||||
<Run
|
||||
runDetailUrl={runID ? `/apps/${appDetail?.id}/workflow-runs/${runID}` : ''}
|
||||
tracingListUrl={runID ? `/apps/${appDetail?.id}/workflow-runs/${runID}/node-executions` : ''}
|
||||
/>
|
||||
<WorkflowContextProvider>
|
||||
<Run
|
||||
runDetailUrl={runID ? `/apps/${appDetail?.id}/workflow-runs/${runID}` : ''}
|
||||
tracingListUrl={runID ? `/apps/${appDetail?.id}/workflow-runs/${runID}/node-executions` : ''}
|
||||
/>
|
||||
</WorkflowContextProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
|
||||
|
||||
const query = {
|
||||
page: currPage + 1,
|
||||
detail: true,
|
||||
limit,
|
||||
...(debouncedQueryParams.status !== 'all' ? { status: debouncedQueryParams.status } : {}),
|
||||
...(debouncedQueryParams.keyword ? { keyword: debouncedQueryParams.keyword } : {}),
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ArrowDownIcon } from '@heroicons/react/24/outline'
|
||||
import DetailPanel from './detail'
|
||||
import TriggerByDisplay from './trigger-by-display'
|
||||
import type { WorkflowAppLogDetail, WorkflowLogsResponse } from '@/models/log'
|
||||
import type { App } from '@/types/app'
|
||||
import { type App, AppModeEnum } from '@/types/app'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { WorkflowRunTriggeredFrom } from '@/models/log'
|
||||
|
||||
type ILogs = {
|
||||
logs?: WorkflowLogsResponse
|
||||
@@ -29,6 +32,28 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
|
||||
|
||||
const [showDrawer, setShowDrawer] = useState<boolean>(false)
|
||||
const [currentLog, setCurrentLog] = useState<WorkflowAppLogDetail | undefined>()
|
||||
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
|
||||
const [localLogs, setLocalLogs] = useState<WorkflowAppLogDetail[]>(logs?.data || [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!logs?.data) {
|
||||
setLocalLogs([])
|
||||
return
|
||||
}
|
||||
|
||||
const sortedLogs = [...logs.data].sort((a, b) => {
|
||||
const result = a.created_at - b.created_at
|
||||
return sortOrder === 'asc' ? result : -result
|
||||
})
|
||||
|
||||
setLocalLogs(sortedLogs)
|
||||
}, [logs?.data, sortOrder])
|
||||
|
||||
const handleSort = () => {
|
||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
||||
}
|
||||
|
||||
const isWorkflow = appDetail?.mode === AppModeEnum.WORKFLOW
|
||||
|
||||
const statusTdRender = (status: string) => {
|
||||
if (status === 'succeeded') {
|
||||
@@ -43,7 +68,7 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
|
||||
return (
|
||||
<div className='system-xs-semibold-uppercase inline-flex items-center gap-1'>
|
||||
<Indicator color={'red'} />
|
||||
<span className='text-util-colors-red-red-600'>Fail</span>
|
||||
<span className='text-util-colors-red-red-600'>Failure</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -88,15 +113,26 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
|
||||
<thead className='system-xs-medium-uppercase text-text-tertiary'>
|
||||
<tr>
|
||||
<td className='w-5 whitespace-nowrap rounded-l-lg bg-background-section-burn pl-2 pr-1'></td>
|
||||
<td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{t('appLog.table.header.startTime')}</td>
|
||||
<td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>
|
||||
<div className='flex cursor-pointer items-center hover:text-text-secondary' onClick={handleSort}>
|
||||
{t('appLog.table.header.startTime')}
|
||||
<ArrowDownIcon
|
||||
className={cn('ml-0.5 h-3 w-3 stroke-current stroke-2 transition-all',
|
||||
'text-text-tertiary',
|
||||
sortOrder === 'asc' ? 'rotate-180' : '',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{t('appLog.table.header.status')}</td>
|
||||
<td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{t('appLog.table.header.runtime')}</td>
|
||||
<td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{t('appLog.table.header.tokens')}</td>
|
||||
<td className='whitespace-nowrap rounded-r-lg bg-background-section-burn py-1.5 pl-3'>{t('appLog.table.header.user')}</td>
|
||||
<td className={cn('whitespace-nowrap bg-background-section-burn py-1.5 pl-3', !isWorkflow ? 'rounded-r-lg' : '')}>{t('appLog.table.header.user')}</td>
|
||||
{isWorkflow && <td className='whitespace-nowrap rounded-r-lg bg-background-section-burn py-1.5 pl-3'>{t('appLog.table.header.triggered_from')}</td>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="system-sm-regular text-text-secondary">
|
||||
{logs.data.map((log: WorkflowAppLogDetail) => {
|
||||
{localLogs.map((log: WorkflowAppLogDetail) => {
|
||||
const endUser = log.created_by_end_user ? log.created_by_end_user.session_id : log.created_by_account ? log.created_by_account.name : defaultValue
|
||||
return <tr
|
||||
key={log.id}
|
||||
@@ -125,6 +161,11 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
|
||||
{endUser}
|
||||
</div>
|
||||
</td>
|
||||
{isWorkflow && (
|
||||
<td className='p-3 pr-2'>
|
||||
<TriggerByDisplay triggeredFrom={log.workflow_run.triggered_from as WorkflowRunTriggeredFrom} triggerMetadata={log.details?.trigger_metadata} />
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
@@ -136,7 +177,11 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
|
||||
footer={null}
|
||||
panelClassName='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[600px] rounded-xl border border-components-panel-border'
|
||||
>
|
||||
<DetailPanel onClose={onCloseDrawer} runID={currentLog?.workflow_run.id || ''} />
|
||||
<DetailPanel
|
||||
onClose={onCloseDrawer}
|
||||
runID={currentLog?.workflow_run.id || ''}
|
||||
canReplay={currentLog?.workflow_run.triggered_from === 'app-run' || currentLog?.workflow_run.triggered_from === 'debugging'}
|
||||
/>
|
||||
</Drawer>
|
||||
</div>
|
||||
)
|
||||
|
||||
134
web/app/components/app/workflow-log/trigger-by-display.tsx
Normal file
134
web/app/components/app/workflow-log/trigger-by-display.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Code,
|
||||
KnowledgeRetrieval,
|
||||
Schedule,
|
||||
WebhookLine,
|
||||
WindowCursor,
|
||||
} from '@/app/components/base/icons/src/vender/workflow'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import type { TriggerMetadata } from '@/models/log'
|
||||
import { WorkflowRunTriggeredFrom } from '@/models/log'
|
||||
import { Theme } from '@/types/app'
|
||||
|
||||
type TriggerByDisplayProps = {
|
||||
triggeredFrom: WorkflowRunTriggeredFrom
|
||||
className?: string
|
||||
showText?: boolean
|
||||
triggerMetadata?: TriggerMetadata
|
||||
}
|
||||
|
||||
const getTriggerDisplayName = (triggeredFrom: WorkflowRunTriggeredFrom, t: any, metadata?: TriggerMetadata) => {
|
||||
if (triggeredFrom === WorkflowRunTriggeredFrom.PLUGIN && metadata?.event_name)
|
||||
return metadata.event_name
|
||||
|
||||
const nameMap: Record<WorkflowRunTriggeredFrom, string> = {
|
||||
'debugging': t('appLog.triggerBy.debugging'),
|
||||
'app-run': t('appLog.triggerBy.appRun'),
|
||||
'webhook': t('appLog.triggerBy.webhook'),
|
||||
'schedule': t('appLog.triggerBy.schedule'),
|
||||
'plugin': t('appLog.triggerBy.plugin'),
|
||||
'rag-pipeline-run': t('appLog.triggerBy.ragPipelineRun'),
|
||||
'rag-pipeline-debugging': t('appLog.triggerBy.ragPipelineDebugging'),
|
||||
}
|
||||
|
||||
return nameMap[triggeredFrom] || triggeredFrom
|
||||
}
|
||||
|
||||
const getPluginIcon = (metadata: TriggerMetadata | undefined, theme: Theme) => {
|
||||
if (!metadata)
|
||||
return null
|
||||
|
||||
const icon = theme === Theme.dark
|
||||
? metadata.icon_dark || metadata.icon
|
||||
: metadata.icon || metadata.icon_dark
|
||||
|
||||
if (!icon)
|
||||
return null
|
||||
|
||||
return (
|
||||
<BlockIcon
|
||||
type={BlockEnum.TriggerPlugin}
|
||||
size='md'
|
||||
toolIcon={icon}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const getTriggerIcon = (triggeredFrom: WorkflowRunTriggeredFrom, metadata: TriggerMetadata | undefined, theme: Theme) => {
|
||||
switch (triggeredFrom) {
|
||||
case 'webhook':
|
||||
return (
|
||||
<div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-blue-500 p-1 shadow-md'>
|
||||
<WebhookLine className='h-4 w-4 text-text-primary-on-surface' />
|
||||
</div>
|
||||
)
|
||||
case 'schedule':
|
||||
return (
|
||||
<div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-violet-violet-500 p-1 shadow-md'>
|
||||
<Schedule className='h-4 w-4 text-text-primary-on-surface' />
|
||||
</div>
|
||||
)
|
||||
case 'plugin':
|
||||
return getPluginIcon(metadata, theme) || (
|
||||
<BlockIcon
|
||||
type={BlockEnum.TriggerPlugin}
|
||||
size="md"
|
||||
/>
|
||||
)
|
||||
case 'debugging':
|
||||
return (
|
||||
<div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-blue-500 p-1 shadow-md'>
|
||||
<Code className='h-4 w-4 text-text-primary-on-surface' />
|
||||
</div>
|
||||
)
|
||||
case 'rag-pipeline-run':
|
||||
case 'rag-pipeline-debugging':
|
||||
return (
|
||||
<div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-green-green-500 p-1 shadow-md'>
|
||||
<KnowledgeRetrieval className='h-4 w-4 text-text-primary-on-surface' />
|
||||
</div>
|
||||
)
|
||||
case 'app-run':
|
||||
default:
|
||||
// For user input types (app-run, etc.), use webapp icon
|
||||
return (
|
||||
<div className='rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 p-1 shadow-md'>
|
||||
<WindowCursor className='h-4 w-4 text-text-primary-on-surface' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const TriggerByDisplay: FC<TriggerByDisplayProps> = ({
|
||||
triggeredFrom,
|
||||
className = '',
|
||||
showText = true,
|
||||
triggerMetadata,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
|
||||
const displayName = getTriggerDisplayName(triggeredFrom, t, triggerMetadata)
|
||||
const icon = getTriggerIcon(triggeredFrom, triggerMetadata, theme)
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-1.5 ${className}`}>
|
||||
<div className="flex items-center justify-center">
|
||||
{icon}
|
||||
</div>
|
||||
{showText && (
|
||||
<span className="system-sm-regular text-text-secondary">
|
||||
{displayName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TriggerByDisplay
|
||||
Reference in New Issue
Block a user