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:
Yeuoly
2025-11-12 17:59:37 +08:00
committed by GitHub
parent ca7794305b
commit b76e17b25d
785 changed files with 41186 additions and 3725 deletions

View File

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

View File

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

View File

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

View File

@@ -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 = (() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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`} />

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

@@ -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 } : {}),

View File

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

View 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