fix(trigger): disable some options when no start node

This commit is contained in:
yessenia 2025-10-20 19:38:59 +08:00
parent 4102f0bc9d
commit d5e2649608
5 changed files with 154 additions and 105 deletions

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

@ -1,10 +1,25 @@
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 {
memo,
useCallback,
useEffect,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button'
import type { CommonNodeType } from '@/app/components/workflow/types'
import { BlockEnum, type InputVar } from '@/app/components/workflow/types'
import { appDefaultIconBackground } from '@/config'
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 { fetchAppDetail } from '@/service/apps'
import { fetchInstalledAppList } from '@/service/explore'
import { AppModeEnum } from '@/types/app'
import type { PublishWorkflowParams } from '@/types/workflow'
import { basePath } from '@/utils/var'
import {
RiArrowDownSLine,
RiArrowRightSLine,
@ -18,35 +33,61 @@ 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 Tooltip from '../../base/tooltip'
import SuggestedAction from './suggested-action'
import PublishWithMultipleModel from './publish-with-multiple-model'
import Button from '@/app/components/base/button'
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 { fetchAppDetail } from '@/service/apps'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
memo,
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useNodes } from 'reactflow'
import Divider from '../../base/divider'
import Loading from '../../base/loading'
import Toast from '../../base/toast'
import Tooltip from '../../base/tooltip'
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'
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
@ -86,26 +127,46 @@ const AppPublisher = ({
workflowToolAvailable = true,
}: 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 nodes = useNodes<CommonNodeType>()
const missingStartNode = !nodes.some(node => node.data.type === BlockEnum.Start)
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)
@ -171,8 +232,6 @@ const AppPublisher = ({
})
}, [appDetail, setAppDetail])
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (e) => {
e.preventDefault()
if (publishDisabled || published)
@ -280,32 +339,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'>
@ -315,22 +349,22 @@ const AppPublisher = ({
{!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}>
<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)}
disabled={disabledFunctionButton}
link={appURL}
icon={<RiPlayCircleLine className='h-4 w-4' />}
>
{t('workflow.common.runApp')}
</SuggestedAction>
</Tooltip>
{appDetail?.mode === 'workflow' || appDetail?.mode === 'completion'
{appDetail?.mode === AppModeEnum.WORKFLOW || appDetail?.mode === AppModeEnum.COMPLETION
? (
<Tooltip triggerClassName='flex' disabled={!systemFeatures.webapp_auth.enabled || appDetail.access_mode === AccessMode.EXTERNAL_MEMBERS || userCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}>
<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)}
disabled={disabledFunctionButton}
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
icon={<RiPlayList2Line className='h-4 w-4' />}
>
@ -350,26 +384,29 @@ const AppPublisher = ({
{t('workflow.common.embedIntoSite')}
</SuggestedAction>
)}
<Tooltip triggerClassName='flex' disabled={!systemFeatures.webapp_auth.enabled || userCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}>
<Tooltip triggerClassName='flex' disabled={!disabledFunctionButton} popupContent={disabledFunctionTooltip} asChild={false}>
<SuggestedAction
className='flex-1'
onClick={() => {
if (publishedAt)
handleOpenInExplore()
}}
disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && !userCanAccessApp?.result)}
disabled={disabledFunctionButton}
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>
<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 === 'workflow' && (
<WorkflowToolConfigureButton
disabled={workflowToolDisabled}

View File

@ -254,6 +254,8 @@ const translation = {
notSetDesc: 'Currently nobody can access the web app. Please set permissions.',
},
noAccessPermission: 'No permission to access web app',
noUserInputNode: 'Missing user input node',
notPublishedYet: 'App is not published yet',
maxActiveRequests: 'Max concurrent requests',
maxActiveRequestsPlaceholder: 'Enter 0 for unlimited',
maxActiveRequestsTip: 'Maximum number of concurrent active requests per app (0 for unlimited)',

View File

@ -253,6 +253,8 @@ const translation = {
notSetDesc: '当前任何人都无法访问 Web 应用。请设置访问权限。',
},
noAccessPermission: '没有权限访问 web 应用',
noUserInputNode: '缺少用户输入节点',
notPublishedYet: '应用暂未发布',
maxActiveRequests: '最大活跃请求数',
maxActiveRequestsPlaceholder: '0 表示不限制',
maxActiveRequestsTip: '当前应用的最大活跃请求数0 表示不限制)',

View File

@ -59,8 +59,14 @@ export type VariableInput = {
/**
* App modes
*/
export const AppModes = ['advanced-chat', 'agent-chat', 'chat', 'completion', 'workflow'] as const
export type AppMode = typeof AppModes[number]
export enum AppModeEnum {
COMPLETION = 'completion',
WORKFLOW = 'workflow',
CHAT = 'chat',
ADVANCED_CHAT = 'advanced-chat',
AGENT_CHAT = 'agent-chat',
}
export const AppModes = [AppModeEnum.COMPLETION, AppModeEnum.WORKFLOW, AppModeEnum.CHAT, AppModeEnum.ADVANCED_CHAT, AppModeEnum.AGENT_CHAT] as const
/**
* Variable type
@ -330,7 +336,7 @@ export type App = {
use_icon_as_answer_icon: boolean
/** Mode */
mode: AppMode
mode: AppModeEnum
/** Enable web app */
enable_site: boolean
/** Enable web API */
@ -378,7 +384,7 @@ export type AppTemplate = {
/** Description */
description: string
/** Mode */
mode: AppMode
mode: AppModeEnum
/** Model */
model_config: ModelConfig
}