import type { ModelAndParameter } from '../configuration/debug/types' import type { InputVar, Variable } from '@/app/components/workflow/types' import type { PublishWorkflowParams } from '@/types/workflow' import { RiArrowDownSLine, RiArrowRightSLine, RiBuildingLine, RiGlobalLine, RiLockLine, RiPlanetLine, RiPlayCircleLine, RiPlayList2Line, RiTerminalBoxLine, RiVerifiedBadgeLine, } from '@remixicon/react' import { useKeyPress } from 'ahooks' import { memo, useCallback, useEffect, useMemo, useState, } from 'react' import { useTranslation } from 'react-i18next' import EmbeddedModal from '@/app/components/app/overview/embedded' import { useStore as useAppStore } from '@/app/components/app/store' import { trackEvent } from '@/app/components/base/amplitude' 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 UpgradeBtn from '@/app/components/billing/upgrade-btn' import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button' import { appDefaultIconBackground } from '@/config' import { useGlobalPublicStore } from '@/context/global-public-context' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' 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 { basePath } from '@/utils/var' 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 PublishWithMultipleModel from './publish-with-multiple-model' import SuggestedAction from './suggested-action' const ACCESS_MODE_MAP: Record = { [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 ( <>
{t(`app.accessControlDialog.accessItems.${label}` as any) as string}
) } export type AppPublisherProps = { disabled?: boolean publishDisabled?: boolean publishedAt?: number /** only needed in workflow / chatflow mode */ draftUpdatedAt?: number debugWithMultipleModel?: boolean multipleModelConfigs?: ModelAndParameter[] /** modelAndParameter is passed when debugWithMultipleModel is true */ onPublish?: (params?: any) => Promise | any onRestore?: () => Promise | any onToggle?: (state: boolean) => void crossAxisOffset?: number toolPublished?: boolean inputs?: InputVar[] outputs?: Variable[] onRefreshData?: () => void workflowToolAvailable?: boolean missingStartNode?: boolean hasTriggerNode?: boolean // Whether workflow currently contains any trigger nodes (used to hide missing-start CTA when triggers exist). startNodeLimitExceeded?: boolean } const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P'] const AppPublisher = ({ disabled = false, publishDisabled = false, publishedAt, draftUpdatedAt, debugWithMultipleModel = false, multipleModelConfigs = [], onPublish, onRestore, onToggle, crossAxisOffset = 0, toolPublished, inputs, outputs, onRefreshData, workflowToolAvailable = true, missingStartNode = false, hasTriggerNode = false, startNodeLimitExceeded = 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 !== AppModeEnum.COMPLETION && appDetail?.mode !== AppModeEnum.WORKFLOW) ? AppModeEnum.CHAT : appDetail.mode const appURL = `${appBaseURL}${basePath}/${appMode}/${accessToken}` 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 openAsyncWindow = useAsyncWindowOpen() 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]) useEffect(() => { if (appDetail && appAccessSubjects) { if (appDetail.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && appAccessSubjects.groups?.length === 0 && appAccessSubjects.members?.length === 0) setIsAppAccessSet(false) else setIsAppAccessSet(true) } else { setIsAppAccessSet(true) } }, [appAccessSubjects, appDetail]) const handlePublish = useCallback(async (params?: ModelAndParameter | PublishWorkflowParams) => { try { await onPublish?.(params) setPublished(true) trackEvent('app_published_time', { action_mode: 'app', app_id: appDetail?.id, app_name: appDetail?.name }) } catch { setPublished(false) } }, [appDetail, onPublish]) const handleRestore = useCallback(async () => { try { await onRestore?.() setOpen(false) } catch { } }, [onRestore]) const handleTrigger = useCallback(() => { const state = !open if (disabled) { setOpen(false) return } onToggle?.(state) setOpen(state) if (state) setPublished(false) }, [disabled, onToggle, open]) const handleOpenInExplore = useCallback(async () => { await openAsyncWindow(async () => { if (!appDetail?.id) throw new Error('App not found') const { installed_apps }: any = await fetchInstalledAppList(appDetail?.id) || {} if (installed_apps?.length > 0) return `${basePath}/explore/installed/${installed_apps[0].id}` throw new Error('No app found in Explore') }, { onError: (err) => { Toast.notify({ type: 'error', message: `${err.message || err}` }) }, }) }, [appDetail?.id, openAsyncWindow]) const handleAccessControlUpdate = useCallback(async () => { if (!appDetail) return try { const res = await fetchAppDetailDirect({ url: '/apps', id: appDetail.id }) setAppDetail(res) } finally { setShowAppAccessControl(false) } }, [appDetail, setAppDetail]) useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (e) => { e.preventDefault() if (publishDisabled || published) return handlePublish() }, { exactMatch: true, useCapture: true }) const hasPublishedVersion = !!publishedAt const workflowToolDisabled = !hasPublishedVersion || !workflowToolAvailable const workflowToolMessage = workflowToolDisabled ? t('workflow.common.workflowAsToolDisabledHint') : undefined const showStartNodeLimitHint = Boolean(startNodeLimitExceeded) const upgradeHighlightStyle = useMemo(() => ({ background: 'linear-gradient(97deg, var(--components-input-border-active-prompt-1, rgba(11, 165, 236, 0.95)) -3.64%, var(--components-input-border-active-prompt-2, rgba(21, 90, 239, 0.95)) 45.14%)', WebkitBackgroundClip: 'text', backgroundClip: 'text', WebkitTextFillColor: 'transparent', }), []) return ( <>
{publishedAt ? t('workflow.common.latestPublished') : t('workflow.common.currentDraftUnpublished')}
{publishedAt ? (
{t('workflow.common.publishedAt')} {' '} {formatTimeFromNow(publishedAt)}
{isChatApp && ( )}
) : (
{t('workflow.common.autoSaved')} {' '} · {Boolean(draftUpdatedAt) && formatTimeFromNow(draftUpdatedAt!)}
)} {debugWithMultipleModel ? ( handlePublish(item)} // textGenerationModelList={textGenerationModelList} /> ) : ( <> {showStartNodeLimitHint && (

{t('workflow.publishLimit.startNodeTitlePrefix')} {t('workflow.publishLimit.startNodeTitleSuffix')}

{t('workflow.publishLimit.startNodeDesc')}

)} )}
{(systemFeatures.webapp_auth.enabled && (isGettingUserCanAccessApp || isGettingAppWhiteListSubjects)) ?
: ( <> {systemFeatures.webapp_auth.enabled && (

{t('app.publishApp.title')}

{ setShowAppAccessControl(true) }} >
{!isAppAccessSet &&

{t('app.publishApp.notSet')}

}
{!isAppAccessSet &&

{t('app.publishApp.notSetDesc')}

}
)} { // Hide run/batch run app buttons when there is a trigger node. !hasTriggerNode && (
} > {t('workflow.common.runApp')} {appDetail?.mode === AppModeEnum.WORKFLOW || appDetail?.mode === AppModeEnum.COMPLETION ? ( } > {t('workflow.common.batchRunApp')} ) : ( { setEmbeddingModalOpen(true) handleTrigger() }} disabled={!publishedAt} icon={} > {t('workflow.common.embedIntoSite')} )} { if (publishedAt) handleOpenInExplore() }} disabled={disabledFunctionButton} icon={} > {t('workflow.common.openInExplore')} } > {t('workflow.common.accessAPIReference')} {appDetail?.mode === AppModeEnum.WORKFLOW && ( )}
) } )}
setEmbeddingModalOpen(false)} appBaseUrl={appBaseURL} accessToken={accessToken} /> {showAppAccessControl && { setShowAppAccessControl(false) }} />}
) } export default memo(AppPublisher)