mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 18:27:19 +08:00
Merge branch 'jzh' into deploy/dev
This commit is contained in:
commit
f62cad931b
@ -1,7 +1,6 @@
|
||||
import type { AppPublisherProps } from '@/app/components/app/app-publisher'
|
||||
import type { ModelAndParameter } from '@/app/components/app/configuration/debug/types'
|
||||
import type { FileUpload } from '@/app/components/base/features/types'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import type { AppPublisherProps, AppPublisherPublishParams } from '@/app/components/app/app-publisher'
|
||||
import type { Features, FileUpload } from '@/app/components/base/features/types'
|
||||
import type { ModelConfig } from '@/models/debug'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogActions,
|
||||
@ -21,9 +20,15 @@ import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { Resolution } from '@/types/app'
|
||||
|
||||
type PublishedModelConfig = ModelConfig & {
|
||||
resetAppConfig?: () => void
|
||||
}
|
||||
|
||||
type Props = Omit<AppPublisherProps, 'onPublish'> & {
|
||||
onPublish?: (params?: ModelAndParameter | PublishWorkflowParams, features?: any) => Promise<any> | any
|
||||
publishedConfig?: any
|
||||
onPublish?: (params?: AppPublisherPublishParams, features?: Features) => Promise<unknown> | unknown
|
||||
publishedConfig: {
|
||||
modelConfig: PublishedModelConfig
|
||||
}
|
||||
resetAppConfig?: () => void
|
||||
}
|
||||
|
||||
@ -71,7 +76,7 @@ const FeaturesWrappedAppPublisher = (props: Props) => {
|
||||
setRestoreConfirmOpen(false)
|
||||
}, [featuresStore, props])
|
||||
|
||||
const handlePublish = useCallback((params?: ModelAndParameter | PublishWorkflowParams) => {
|
||||
const handlePublish = useCallback((params?: AppPublisherPublishParams) => {
|
||||
return props.onPublish?.(params, features)
|
||||
}, [features, props])
|
||||
|
||||
|
||||
@ -3,9 +3,7 @@ import type { ModelAndParameter } from '../configuration/debug/types'
|
||||
import type { WorkflowHiddenStartVariable, WorkflowLaunchInputValue } from '@/app/components/app/overview/app-card-utils'
|
||||
import type { CollaborationUpdate } from '@/app/components/workflow/collaboration/types/collaboration'
|
||||
import type { InputVar, Variable } from '@/app/components/workflow/types'
|
||||
import type { EvaluationWorkflowAssociatedTarget } from '@/types/evaluation'
|
||||
import type { I18nKeysWithPrefix } from '@/types/i18n'
|
||||
import type { PublishWorkflowParams, WorkflowTypeConversionTarget } from '@/types/workflow'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
@ -42,11 +40,9 @@ import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/acces
|
||||
import { fetchAppDetailDirect, publishToCreatorsPlatform } from '@/service/apps'
|
||||
import { fetchInstalledAppList } from '@/service/explore'
|
||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
import { useConvertWorkflowTypeMutation } from '@/service/use-apps'
|
||||
import { useEvaluationWorkflowAssociatedTargets } from '@/service/use-evaluation'
|
||||
import { useInvalidateAppWorkflow } from '@/service/use-workflow'
|
||||
import { fetchPublishedWorkflow } from '@/service/workflow'
|
||||
import { AppModeEnum, AppTypeEnum } from '@/types/app'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { getKeyboardKeyCodeBySystem } from '../../workflow/utils'
|
||||
import AccessControl from '../app-access-control'
|
||||
@ -57,12 +53,20 @@ import {
|
||||
PublisherSummarySection,
|
||||
} from './sections'
|
||||
import SuggestedAction from './suggested-action'
|
||||
import { useWorkflowTypeSwitch } from './use-workflow-type-switch'
|
||||
import {
|
||||
getDisabledFunctionTooltip,
|
||||
getPublisherAppUrl,
|
||||
isPublisherAccessConfigured,
|
||||
} from './utils'
|
||||
|
||||
export type AppPublisherPublishParams
|
||||
= | ModelAndParameter
|
||||
| (Pick<PublishWorkflowParams, 'title' | 'releaseNotes'> & {
|
||||
url?: string
|
||||
id?: string
|
||||
})
|
||||
|
||||
export type AppPublisherProps = {
|
||||
disabled?: boolean
|
||||
publishDisabled?: boolean
|
||||
@ -72,8 +76,8 @@ export type AppPublisherProps = {
|
||||
debugWithMultipleModel?: boolean
|
||||
multipleModelConfigs?: ModelAndParameter[]
|
||||
/** modelAndParameter is passed when debugWithMultipleModel is true */
|
||||
onPublish?: (params?: any) => Promise<any> | any
|
||||
onRestore?: () => Promise<any> | any
|
||||
onPublish?: (params?: AppPublisherPublishParams) => Promise<unknown> | unknown
|
||||
onRestore?: () => Promise<unknown> | unknown
|
||||
onToggle?: (state: boolean) => void
|
||||
crossAxisOffset?: number
|
||||
toolPublished?: boolean
|
||||
@ -89,32 +93,6 @@ export type AppPublisherProps = {
|
||||
|
||||
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
|
||||
|
||||
type WorkflowTypeSwitchLabelKey = I18nKeysWithPrefix<'workflow', 'common.'>
|
||||
|
||||
const WORKFLOW_TYPE_SWITCH_CONFIG: Record<WorkflowTypeConversionTarget, {
|
||||
targetType: WorkflowTypeConversionTarget
|
||||
publishLabelKey: WorkflowTypeSwitchLabelKey
|
||||
switchLabelKey: WorkflowTypeSwitchLabelKey
|
||||
tipKey: WorkflowTypeSwitchLabelKey
|
||||
}> = {
|
||||
workflow: {
|
||||
targetType: 'evaluation',
|
||||
publishLabelKey: 'common.publishAsEvaluationWorkflow',
|
||||
switchLabelKey: 'common.switchToEvaluationWorkflow',
|
||||
tipKey: 'common.switchToEvaluationWorkflowTip',
|
||||
},
|
||||
evaluation: {
|
||||
targetType: 'workflow',
|
||||
publishLabelKey: 'common.publishAsStandardWorkflow',
|
||||
switchLabelKey: 'common.switchToStandardWorkflow',
|
||||
tipKey: 'common.switchToStandardWorkflowTip',
|
||||
},
|
||||
} as const
|
||||
|
||||
const isWorkflowTypeConversionTarget = (type?: AppTypeEnum): type is WorkflowTypeConversionTarget => {
|
||||
return type === 'workflow' || type === 'evaluation'
|
||||
}
|
||||
|
||||
const AppPublisher = ({
|
||||
disabled = false,
|
||||
publishDisabled = false,
|
||||
@ -141,8 +119,6 @@ const AppPublisher = ({
|
||||
const [published, setPublished] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [showAppAccessControl, setShowAppAccessControl] = useState(false)
|
||||
const [showEvaluationWorkflowSwitchConfirm, setShowEvaluationWorkflowSwitchConfirm] = useState(false)
|
||||
const [evaluationWorkflowSwitchTargets, setEvaluationWorkflowSwitchTargets] = useState<EvaluationWorkflowAssociatedTarget[]>([])
|
||||
|
||||
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
|
||||
const [workflowLaunchDialogOpen, setWorkflowLaunchDialogOpen] = useState(false)
|
||||
@ -157,36 +133,10 @@ const AppPublisher = ({
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
|
||||
const { mutateAsync: convertWorkflowType, isPending: isConvertingWorkflowType } = useConvertWorkflowTypeMutation()
|
||||
|
||||
const appURL = getPublisherAppUrl({ appBaseUrl: appBaseURL, accessToken, mode: appDetail?.mode })
|
||||
const isChatApp = [AppModeEnum.CHAT, AppModeEnum.AGENT_CHAT, AppModeEnum.COMPLETION].includes(appDetail?.mode || AppModeEnum.CHAT)
|
||||
const workflowTypeSwitchConfig = useMemo(() => {
|
||||
if (!appDetail?.workflow_kind)
|
||||
return WORKFLOW_TYPE_SWITCH_CONFIG.workflow
|
||||
|
||||
if (!isWorkflowTypeConversionTarget(appDetail?.workflow_kind))
|
||||
return undefined
|
||||
|
||||
return WORKFLOW_TYPE_SWITCH_CONFIG[appDetail.workflow_kind]
|
||||
}, [appDetail?.workflow_kind])
|
||||
const isEvaluationWorkflowType = appDetail?.workflow_kind === AppTypeEnum.EVALUATION
|
||||
const {
|
||||
refetch: refetchEvaluationWorkflowAssociatedTargets,
|
||||
isFetching: isFetchingEvaluationWorkflowAssociatedTargets,
|
||||
} = useEvaluationWorkflowAssociatedTargets(appDetail?.id, { enabled: false })
|
||||
const workflowTypeSwitchDisabledReason = useMemo(() => {
|
||||
if (workflowTypeSwitchConfig?.targetType !== AppTypeEnum.EVALUATION)
|
||||
return undefined
|
||||
|
||||
if (!canAccessSnippetsAndEvaluation)
|
||||
return t('compliance.sandboxUpgradeTooltip', { ns: 'common' })
|
||||
|
||||
if (!hasHumanInputNode && !hasTriggerNode)
|
||||
return undefined
|
||||
|
||||
return t('common.switchToEvaluationWorkflowDisabledTip', { ns: 'workflow' })
|
||||
}, [canAccessSnippetsAndEvaluation, hasHumanInputNode, hasTriggerNode, t, workflowTypeSwitchConfig?.targetType])
|
||||
const hiddenLaunchVariables = useMemo<WorkflowHiddenStartVariable[]>(
|
||||
() => (inputs ?? []).filter(input => input.hide === true),
|
||||
[inputs],
|
||||
@ -230,7 +180,7 @@ const AppPublisher = ({
|
||||
refetch()
|
||||
}, [open, appDetail, refetch, systemFeatures])
|
||||
|
||||
const handlePublish = useCallback(async (params?: ModelAndParameter | PublishWorkflowParams) => {
|
||||
const handlePublish = useCallback(async (params?: AppPublisherPublishParams) => {
|
||||
try {
|
||||
await onPublish?.(params)
|
||||
setPublished(true)
|
||||
@ -312,109 +262,8 @@ const AppPublisher = ({
|
||||
}
|
||||
}, [appDetail, setAppDetail])
|
||||
|
||||
const getWorkflowTypeSwitchPublishUrl = useCallback(() => {
|
||||
if (!appDetail?.id || !workflowTypeSwitchConfig)
|
||||
return undefined
|
||||
|
||||
if (workflowTypeSwitchConfig.targetType === AppTypeEnum.EVALUATION)
|
||||
return `/apps/${appDetail.id}/workflows/publish/evaluation`
|
||||
|
||||
return `/apps/${appDetail.id}/workflows/publish`
|
||||
}, [appDetail?.id, workflowTypeSwitchConfig])
|
||||
|
||||
const performWorkflowTypeSwitch = useCallback(async () => {
|
||||
if (!appDetail?.id || !workflowTypeSwitchConfig)
|
||||
return false
|
||||
|
||||
try {
|
||||
if (!publishedAt) {
|
||||
const publishUrl = getWorkflowTypeSwitchPublishUrl()
|
||||
if (!publishUrl)
|
||||
return false
|
||||
|
||||
await handlePublish({
|
||||
url: publishUrl,
|
||||
title: '',
|
||||
releaseNotes: '',
|
||||
})
|
||||
|
||||
const latestAppDetail = await fetchAppDetailDirect({
|
||||
url: '/apps',
|
||||
id: appDetail.id,
|
||||
})
|
||||
setAppDetail(latestAppDetail)
|
||||
setShowEvaluationWorkflowSwitchConfirm(false)
|
||||
setEvaluationWorkflowSwitchTargets([])
|
||||
return true
|
||||
}
|
||||
|
||||
await convertWorkflowType({
|
||||
params: {
|
||||
appId: appDetail.id,
|
||||
},
|
||||
query: {
|
||||
target_type: workflowTypeSwitchConfig.targetType,
|
||||
},
|
||||
})
|
||||
|
||||
const latestAppDetail = await fetchAppDetailDirect({
|
||||
url: '/apps',
|
||||
id: appDetail.id,
|
||||
})
|
||||
setAppDetail(latestAppDetail)
|
||||
|
||||
if (publishedAt)
|
||||
setOpen(false)
|
||||
|
||||
setShowEvaluationWorkflowSwitchConfirm(false)
|
||||
setEvaluationWorkflowSwitchTargets([])
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
}, [appDetail?.id, convertWorkflowType, getWorkflowTypeSwitchPublishUrl, handlePublish, publishedAt, setAppDetail, workflowTypeSwitchConfig])
|
||||
|
||||
const handleWorkflowTypeSwitch = useCallback(async () => {
|
||||
if (!appDetail?.id || !workflowTypeSwitchConfig)
|
||||
return
|
||||
if (workflowTypeSwitchDisabledReason) {
|
||||
toast.error(workflowTypeSwitchDisabledReason)
|
||||
return
|
||||
}
|
||||
|
||||
if (appDetail.workflow_kind === AppTypeEnum.EVALUATION && workflowTypeSwitchConfig.targetType === AppTypeEnum.WORKFLOW) {
|
||||
const associatedTargetsResult = await refetchEvaluationWorkflowAssociatedTargets()
|
||||
|
||||
if (associatedTargetsResult.isError) {
|
||||
toast.error(t('common.switchToStandardWorkflowConfirm.loadFailed', { ns: 'workflow' }))
|
||||
return
|
||||
}
|
||||
|
||||
const associatedTargets = associatedTargetsResult.data?.items ?? []
|
||||
if (associatedTargets.length > 0) {
|
||||
setEvaluationWorkflowSwitchTargets(associatedTargets)
|
||||
setShowEvaluationWorkflowSwitchConfirm(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await performWorkflowTypeSwitch()
|
||||
}, [
|
||||
appDetail?.id,
|
||||
appDetail?.workflow_kind,
|
||||
performWorkflowTypeSwitch,
|
||||
refetchEvaluationWorkflowAssociatedTargets,
|
||||
t,
|
||||
workflowTypeSwitchConfig,
|
||||
workflowTypeSwitchDisabledReason,
|
||||
])
|
||||
|
||||
const handleEvaluationWorkflowSwitchConfirmOpenChange = useCallback((nextOpen: boolean) => {
|
||||
setShowEvaluationWorkflowSwitchConfirm(nextOpen)
|
||||
|
||||
if (!nextOpen)
|
||||
setEvaluationWorkflowSwitchTargets([])
|
||||
const handlePublishedWorkflowTypeSwitch = useCallback(() => {
|
||||
setOpen(false)
|
||||
}, [])
|
||||
|
||||
const handleOpenWorkflowLaunchDialog = useCallback((targetUrl: string) => {
|
||||
@ -442,6 +291,29 @@ const AppPublisher = ({
|
||||
window.open(targetUrl, '_blank')
|
||||
setWorkflowLaunchDialogOpen(false)
|
||||
}, [supportedWorkflowLaunchVariables, workflowLaunchTargetUrl, workflowLaunchValues])
|
||||
const {
|
||||
evaluationWorkflowSwitchTargets,
|
||||
handleEvaluationWorkflowSwitchConfirmOpenChange,
|
||||
handleWorkflowTypeSwitch,
|
||||
isConvertingWorkflowType,
|
||||
isEvaluationWorkflowType,
|
||||
performWorkflowTypeSwitch,
|
||||
showEvaluationWorkflowSwitchConfirm,
|
||||
workflowTypeSwitchConfig,
|
||||
workflowTypeSwitchDisabled,
|
||||
workflowTypeSwitchDisabledReason,
|
||||
} = useWorkflowTypeSwitch({
|
||||
appDetail,
|
||||
canAccessSnippetsAndEvaluation,
|
||||
hasHumanInputNode,
|
||||
hasTriggerNode,
|
||||
onPublish: handlePublish,
|
||||
onPublishedSwitch: handlePublishedWorkflowTypeSwitch,
|
||||
published,
|
||||
publishedAt,
|
||||
publishDisabled,
|
||||
setAppDetail,
|
||||
})
|
||||
|
||||
const handlePublishToMarketplace = useCallback(async () => {
|
||||
if (!appDetail?.id || publishingToMarketplace)
|
||||
@ -541,7 +413,7 @@ const AppPublisher = ({
|
||||
startNodeLimitExceeded={startNodeLimitExceeded}
|
||||
upgradeHighlightStyle={upgradeHighlightStyle}
|
||||
workflowTypeSwitchConfig={workflowTypeSwitchConfig}
|
||||
workflowTypeSwitchDisabled={publishDisabled || published || isConvertingWorkflowType || isFetchingEvaluationWorkflowAssociatedTargets || Boolean(workflowTypeSwitchDisabledReason)}
|
||||
workflowTypeSwitchDisabled={workflowTypeSwitchDisabled}
|
||||
workflowTypeSwitchDisabledReason={workflowTypeSwitchDisabledReason}
|
||||
onWorkflowTypeSwitch={handleWorkflowTypeSwitch}
|
||||
/>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { CSSProperties, ReactNode } from 'react'
|
||||
import type { ModelAndParameter } from '../configuration/debug/types'
|
||||
import type { AppPublisherProps } from './index'
|
||||
import type { I18nKeysWithPrefix } from '@/types/i18n'
|
||||
import type { PublishWorkflowParams, WorkflowTypeConversionTarget } from '@/types/workflow'
|
||||
import type { WorkflowTypeSwitchConfig } from './use-workflow-type-switch'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import {
|
||||
Tooltip,
|
||||
@ -23,8 +23,6 @@ import PublishWithMultipleModel from './publish-with-multiple-model'
|
||||
import SuggestedAction from './suggested-action'
|
||||
import { ACCESS_MODE_MAP } from './utils'
|
||||
|
||||
type WorkflowTypeSwitchLabelKey = I18nKeysWithPrefix<'workflow', 'common.'>
|
||||
|
||||
type SummarySectionProps = Pick<AppPublisherProps, | 'debugWithMultipleModel'
|
||||
| 'draftUpdatedAt'
|
||||
| 'multipleModelConfigs'
|
||||
@ -39,12 +37,7 @@ type SummarySectionProps = Pick<AppPublisherProps, | 'debugWithMultipleModel'
|
||||
published: boolean
|
||||
publishShortcut: string[]
|
||||
upgradeHighlightStyle: CSSProperties
|
||||
workflowTypeSwitchConfig?: {
|
||||
targetType: WorkflowTypeConversionTarget
|
||||
publishLabelKey: WorkflowTypeSwitchLabelKey
|
||||
switchLabelKey: WorkflowTypeSwitchLabelKey
|
||||
tipKey: WorkflowTypeSwitchLabelKey
|
||||
}
|
||||
workflowTypeSwitchConfig?: WorkflowTypeSwitchConfig
|
||||
workflowTypeSwitchDisabled: boolean
|
||||
workflowTypeSwitchDisabledReason?: string
|
||||
}
|
||||
|
||||
229
web/app/components/app/app-publisher/use-workflow-type-switch.ts
Normal file
229
web/app/components/app/app-publisher/use-workflow-type-switch.ts
Normal file
@ -0,0 +1,229 @@
|
||||
import type { ModelAndParameter } from '../configuration/debug/types'
|
||||
import type { App, AppSSO } from '@/types/app'
|
||||
import type { EvaluationWorkflowAssociatedTarget } from '@/types/evaluation'
|
||||
import type { I18nKeysWithPrefix } from '@/types/i18n'
|
||||
import type { PublishWorkflowParams, WorkflowKind, WorkflowTypeConversionTarget } from '@/types/workflow'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { fetchAppDetailDirect } from '@/service/apps'
|
||||
import { useConvertWorkflowTypeMutation } from '@/service/use-apps'
|
||||
import { useEvaluationWorkflowAssociatedTargets } from '@/service/use-evaluation'
|
||||
import { AppTypeEnum } from '@/types/app'
|
||||
|
||||
type WorkflowTypeSwitchLabelKey = I18nKeysWithPrefix<'workflow', 'common.'>
|
||||
|
||||
export type WorkflowTypeSwitchConfig = {
|
||||
targetType: WorkflowTypeConversionTarget
|
||||
publishLabelKey: WorkflowTypeSwitchLabelKey
|
||||
switchLabelKey: WorkflowTypeSwitchLabelKey
|
||||
tipKey: WorkflowTypeSwitchLabelKey
|
||||
}
|
||||
|
||||
const WORKFLOW_TYPE_SWITCH_CONFIG: Record<WorkflowTypeConversionTarget, WorkflowTypeSwitchConfig> = {
|
||||
workflow: {
|
||||
targetType: 'evaluation',
|
||||
publishLabelKey: 'common.publishAsEvaluationWorkflow',
|
||||
switchLabelKey: 'common.switchToEvaluationWorkflow',
|
||||
tipKey: 'common.switchToEvaluationWorkflowTip',
|
||||
},
|
||||
evaluation: {
|
||||
targetType: 'workflow',
|
||||
publishLabelKey: 'common.publishAsStandardWorkflow',
|
||||
switchLabelKey: 'common.switchToStandardWorkflow',
|
||||
tipKey: 'common.switchToStandardWorkflowTip',
|
||||
},
|
||||
} as const
|
||||
|
||||
const getWorkflowTypeSwitchConfig = (workflowKind?: WorkflowKind | null) => {
|
||||
if (!workflowKind || workflowKind === 'standard')
|
||||
return WORKFLOW_TYPE_SWITCH_CONFIG.workflow
|
||||
|
||||
if (workflowKind === 'evaluation')
|
||||
return WORKFLOW_TYPE_SWITCH_CONFIG.evaluation
|
||||
}
|
||||
|
||||
type UseWorkflowTypeSwitchParams = {
|
||||
appDetail?: App & Partial<AppSSO>
|
||||
canAccessSnippetsAndEvaluation: boolean
|
||||
hasHumanInputNode: boolean
|
||||
hasTriggerNode: boolean
|
||||
onPublish: (params?: ModelAndParameter | PublishWorkflowParams) => Promise<void>
|
||||
onPublishedSwitch: () => void
|
||||
published: boolean
|
||||
publishedAt?: number
|
||||
publishDisabled: boolean
|
||||
setAppDetail: (appDetail?: App & Partial<AppSSO>) => void
|
||||
}
|
||||
|
||||
export const useWorkflowTypeSwitch = ({
|
||||
appDetail,
|
||||
canAccessSnippetsAndEvaluation,
|
||||
hasHumanInputNode,
|
||||
hasTriggerNode,
|
||||
onPublish,
|
||||
onPublishedSwitch,
|
||||
published,
|
||||
publishedAt,
|
||||
publishDisabled,
|
||||
setAppDetail,
|
||||
}: UseWorkflowTypeSwitchParams) => {
|
||||
const { t } = useTranslation()
|
||||
const [showEvaluationWorkflowSwitchConfirm, setShowEvaluationWorkflowSwitchConfirm] = useState(false)
|
||||
const [evaluationWorkflowSwitchTargets, setEvaluationWorkflowSwitchTargets] = useState<EvaluationWorkflowAssociatedTarget[]>([])
|
||||
const { mutateAsync: convertWorkflowType, isPending: isConvertingWorkflowType } = useConvertWorkflowTypeMutation()
|
||||
const {
|
||||
refetch: refetchEvaluationWorkflowAssociatedTargets,
|
||||
isFetching: isFetchingEvaluationWorkflowAssociatedTargets,
|
||||
} = useEvaluationWorkflowAssociatedTargets(appDetail?.id, { enabled: false })
|
||||
|
||||
const workflowTypeSwitchConfig = useMemo(() => {
|
||||
return getWorkflowTypeSwitchConfig(appDetail?.workflow_kind)
|
||||
}, [appDetail?.workflow_kind])
|
||||
|
||||
const workflowTypeSwitchDisabledReason = useMemo(() => {
|
||||
if (workflowTypeSwitchConfig?.targetType !== AppTypeEnum.EVALUATION)
|
||||
return undefined
|
||||
|
||||
if (!canAccessSnippetsAndEvaluation)
|
||||
return t('compliance.sandboxUpgradeTooltip', { ns: 'common' })
|
||||
|
||||
if (!hasHumanInputNode && !hasTriggerNode)
|
||||
return undefined
|
||||
|
||||
return t('common.switchToEvaluationWorkflowDisabledTip', { ns: 'workflow' })
|
||||
}, [canAccessSnippetsAndEvaluation, hasHumanInputNode, hasTriggerNode, t, workflowTypeSwitchConfig?.targetType])
|
||||
|
||||
const getWorkflowTypeSwitchPublishUrl = useCallback(() => {
|
||||
if (!appDetail?.id || !workflowTypeSwitchConfig)
|
||||
return undefined
|
||||
|
||||
if (workflowTypeSwitchConfig.targetType === AppTypeEnum.EVALUATION)
|
||||
return `/apps/${appDetail.id}/workflows/publish/evaluation`
|
||||
|
||||
return `/apps/${appDetail.id}/workflows/publish`
|
||||
}, [appDetail?.id, workflowTypeSwitchConfig])
|
||||
|
||||
const resetEvaluationWorkflowSwitchConfirm = useCallback(() => {
|
||||
setShowEvaluationWorkflowSwitchConfirm(false)
|
||||
setEvaluationWorkflowSwitchTargets([])
|
||||
}, [])
|
||||
|
||||
const performWorkflowTypeSwitch = useCallback(async () => {
|
||||
if (!appDetail?.id || !workflowTypeSwitchConfig)
|
||||
return false
|
||||
|
||||
try {
|
||||
if (!publishedAt) {
|
||||
const publishUrl = getWorkflowTypeSwitchPublishUrl()
|
||||
if (!publishUrl)
|
||||
return false
|
||||
|
||||
await onPublish({
|
||||
url: publishUrl,
|
||||
title: '',
|
||||
releaseNotes: '',
|
||||
})
|
||||
|
||||
const latestAppDetail = await fetchAppDetailDirect({
|
||||
url: '/apps',
|
||||
id: appDetail.id,
|
||||
})
|
||||
setAppDetail(latestAppDetail)
|
||||
resetEvaluationWorkflowSwitchConfirm()
|
||||
return true
|
||||
}
|
||||
|
||||
await convertWorkflowType({
|
||||
params: {
|
||||
appId: appDetail.id,
|
||||
},
|
||||
query: {
|
||||
target_type: workflowTypeSwitchConfig.targetType,
|
||||
},
|
||||
})
|
||||
|
||||
const latestAppDetail = await fetchAppDetailDirect({
|
||||
url: '/apps',
|
||||
id: appDetail.id,
|
||||
})
|
||||
setAppDetail(latestAppDetail)
|
||||
onPublishedSwitch()
|
||||
resetEvaluationWorkflowSwitchConfirm()
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
}, [
|
||||
appDetail?.id,
|
||||
convertWorkflowType,
|
||||
getWorkflowTypeSwitchPublishUrl,
|
||||
onPublish,
|
||||
onPublishedSwitch,
|
||||
publishedAt,
|
||||
resetEvaluationWorkflowSwitchConfirm,
|
||||
setAppDetail,
|
||||
workflowTypeSwitchConfig,
|
||||
])
|
||||
|
||||
const handleWorkflowTypeSwitch = useCallback(async () => {
|
||||
if (!appDetail?.id || !workflowTypeSwitchConfig)
|
||||
return
|
||||
|
||||
if (workflowTypeSwitchDisabledReason) {
|
||||
toast.error(workflowTypeSwitchDisabledReason)
|
||||
return
|
||||
}
|
||||
|
||||
if (appDetail.workflow_kind === AppTypeEnum.EVALUATION && workflowTypeSwitchConfig.targetType === AppTypeEnum.WORKFLOW) {
|
||||
const associatedTargetsResult = await refetchEvaluationWorkflowAssociatedTargets()
|
||||
|
||||
if (associatedTargetsResult.isError) {
|
||||
toast.error(t('common.switchToStandardWorkflowConfirm.loadFailed', { ns: 'workflow' }))
|
||||
return
|
||||
}
|
||||
|
||||
const associatedTargets = associatedTargetsResult.data?.items ?? []
|
||||
if (associatedTargets.length > 0) {
|
||||
setEvaluationWorkflowSwitchTargets(associatedTargets)
|
||||
setShowEvaluationWorkflowSwitchConfirm(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await performWorkflowTypeSwitch()
|
||||
}, [
|
||||
appDetail?.id,
|
||||
appDetail?.workflow_kind,
|
||||
performWorkflowTypeSwitch,
|
||||
refetchEvaluationWorkflowAssociatedTargets,
|
||||
t,
|
||||
workflowTypeSwitchConfig,
|
||||
workflowTypeSwitchDisabledReason,
|
||||
])
|
||||
|
||||
const handleEvaluationWorkflowSwitchConfirmOpenChange = useCallback((nextOpen: boolean) => {
|
||||
setShowEvaluationWorkflowSwitchConfirm(nextOpen)
|
||||
|
||||
if (!nextOpen)
|
||||
setEvaluationWorkflowSwitchTargets([])
|
||||
}, [])
|
||||
|
||||
return {
|
||||
evaluationWorkflowSwitchTargets,
|
||||
handleEvaluationWorkflowSwitchConfirmOpenChange,
|
||||
handleWorkflowTypeSwitch,
|
||||
isConvertingWorkflowType,
|
||||
isEvaluationWorkflowType: appDetail?.workflow_kind === AppTypeEnum.EVALUATION,
|
||||
performWorkflowTypeSwitch,
|
||||
showEvaluationWorkflowSwitchConfirm,
|
||||
workflowTypeSwitchConfig,
|
||||
workflowTypeSwitchDisabled: publishDisabled
|
||||
|| published
|
||||
|| isConvertingWorkflowType
|
||||
|| isFetchingEvaluationWorkflowAssociatedTargets
|
||||
|| Boolean(workflowTypeSwitchDisabledReason),
|
||||
workflowTypeSwitchDisabledReason,
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { AppPublisherPublishParams } from '@/app/components/app/app-publisher'
|
||||
import type AppPublisher from '@/app/components/app/app-publisher/features-wrapper'
|
||||
import type { ModelAndParameter } from '@/app/components/app/configuration/debug/types'
|
||||
import type { Features as FeaturesData, OnFeaturesChange } from '@/app/components/base/features/types'
|
||||
@ -21,7 +22,6 @@ import type {
|
||||
TextToSpeechConfig,
|
||||
} from '@/models/debug'
|
||||
import type { VisionSettings } from '@/types/app'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { useBoolean, useGetState } from 'ahooks'
|
||||
import { clone } from 'es-toolkit/object'
|
||||
import { produce } from 'immer'
|
||||
@ -481,7 +481,7 @@ export const useConfiguration = (): ConfigurationViewModel => {
|
||||
resolvedModelModeType,
|
||||
])
|
||||
|
||||
const onPublish = useCallback(async (params?: ModelAndParameter | PublishWorkflowParams, features?: FeaturesData) => {
|
||||
const onPublish = useCallback(async (params?: AppPublisherPublishParams, features?: FeaturesData) => {
|
||||
const modelAndParameter = params && 'model' in params && 'provider' in params && 'parameters' in params
|
||||
? params
|
||||
: undefined
|
||||
|
||||
@ -6,7 +6,7 @@ import ConditionsSection from '../components/conditions-section'
|
||||
import { useEvaluationStore } from '../store'
|
||||
|
||||
const mockUpload = vi.hoisted(() => vi.fn())
|
||||
const mockUseAvailableEvaluationMetrics = vi.hoisted(() => vi.fn())
|
||||
const mockUseDatasetEvaluationMetrics = vi.hoisted(() => vi.fn())
|
||||
const mockUseDefaultEvaluationMetrics = vi.hoisted(() => vi.fn())
|
||||
const mockUseEvaluationConfig = vi.hoisted(() => vi.fn())
|
||||
const mockUseSaveEvaluationConfigMutation = vi.hoisted(() => vi.fn())
|
||||
@ -51,7 +51,7 @@ vi.mock('@/service/base', () => ({
|
||||
|
||||
vi.mock('@/service/use-evaluation', () => ({
|
||||
useEvaluationConfig: (...args: unknown[]) => mockUseEvaluationConfig(...args),
|
||||
useAvailableEvaluationMetrics: (...args: unknown[]) => mockUseAvailableEvaluationMetrics(...args),
|
||||
useDatasetEvaluationMetrics: (...args: unknown[]) => mockUseDatasetEvaluationMetrics(...args),
|
||||
useDefaultEvaluationMetrics: (...args: unknown[]) => mockUseDefaultEvaluationMetrics(...args),
|
||||
useSaveEvaluationConfigMutation: (...args: unknown[]) => mockUseSaveEvaluationConfigMutation(...args),
|
||||
useStartEvaluationRunMutation: (...args: unknown[]) => mockUseStartEvaluationRunMutation(...args),
|
||||
@ -119,7 +119,7 @@ describe('Evaluation', () => {
|
||||
data: null,
|
||||
})
|
||||
|
||||
mockUseAvailableEvaluationMetrics.mockReturnValue({
|
||||
mockUseDatasetEvaluationMetrics.mockReturnValue({
|
||||
data: {
|
||||
metrics: ['answer-correctness', 'faithfulness', 'context-precision', 'context-recall', 'context-relevance'],
|
||||
},
|
||||
@ -582,6 +582,7 @@ describe('Evaluation', () => {
|
||||
it('should render the pipeline-specific layout without auto-selecting a judge model', () => {
|
||||
renderWithQueryClient(<Evaluation resourceType="datasets" resourceId="dataset-1" />)
|
||||
|
||||
expect(mockUseDatasetEvaluationMetrics).toHaveBeenCalledWith('dataset-1')
|
||||
expect(screen.getByTestId('evaluation-model-selector')).toHaveTextContent('empty')
|
||||
expect(screen.getByText('evaluation.history.columns.time')).toBeInTheDocument()
|
||||
expect(screen.getByText('Context Precision')).toBeInTheDocument()
|
||||
@ -621,6 +622,33 @@ describe('Evaluation', () => {
|
||||
expect(screen.getByRole('button', { name: 'evaluation.pipeline.uploadAndRun' })).toBeEnabled()
|
||||
})
|
||||
|
||||
it('should download the fixed pipeline template columns', () => {
|
||||
const createElement = document.createElement.bind(document)
|
||||
let downloadLink: HTMLAnchorElement | undefined
|
||||
const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tagName, options) => {
|
||||
const element = createElement(tagName, options)
|
||||
|
||||
if (tagName === 'a') {
|
||||
downloadLink = element as HTMLAnchorElement
|
||||
vi.spyOn(downloadLink, 'click').mockImplementation(() => {})
|
||||
}
|
||||
|
||||
return element
|
||||
})
|
||||
|
||||
renderWithQueryClient(<Evaluation resourceType="datasets" resourceId="dataset-template" />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'select-model' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /Context Precision/i }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'evaluation.batch.downloadTemplate' }))
|
||||
|
||||
expect(downloadLink?.download).toBe('pipeline-evaluation-template.csv')
|
||||
expect(decodeURIComponent(downloadLink?.href ?? '')).toContain('query,expect_results\n')
|
||||
expect(decodeURIComponent(downloadLink?.href ?? '')).not.toContain('expected_output')
|
||||
|
||||
createElementSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('should upload and start a pipeline evaluation run', async () => {
|
||||
const startRun = vi.fn()
|
||||
mockUseStartEvaluationRunMutation.mockReturnValue({
|
||||
@ -639,14 +667,14 @@ describe('Evaluation', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'evaluation.pipeline.uploadAndRun' }))
|
||||
|
||||
expect(screen.getAllByText('query').length).toBeGreaterThan(0)
|
||||
expect(screen.getAllByText('Expect Results').length).toBeGreaterThan(0)
|
||||
expect(screen.getAllByText('expect_results').length).toBeGreaterThan(0)
|
||||
|
||||
const fileInput = document.querySelector<HTMLInputElement>('input[type="file"][accept=".csv"]')
|
||||
expect(fileInput).toBeInTheDocument()
|
||||
|
||||
fireEvent.change(fileInput!, {
|
||||
target: {
|
||||
files: [new File(['query,Expect Results'], 'pipeline-evaluation.csv', { type: 'text/csv' })],
|
||||
files: [new File(['query,expect_results'], 'pipeline-evaluation.csv', { type: 'text/csv' })],
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ type UseInputFieldsActionsParams = EvaluationResourceProps & {
|
||||
isInputFieldsLoading: boolean
|
||||
isPanelReady: boolean
|
||||
isRunnable: boolean
|
||||
templateContent?: string
|
||||
templateFileName: string
|
||||
}
|
||||
|
||||
@ -31,6 +32,7 @@ export const useInputFieldsActions = ({
|
||||
isInputFieldsLoading,
|
||||
isPanelReady,
|
||||
isRunnable,
|
||||
templateContent,
|
||||
templateFileName,
|
||||
}: UseInputFieldsActionsParams) => {
|
||||
const { t } = useTranslation('evaluation')
|
||||
@ -79,7 +81,7 @@ export const useInputFieldsActions = ({
|
||||
return
|
||||
}
|
||||
|
||||
const content = buildTemplateCsvContent(inputFields)
|
||||
const content = templateContent ?? buildTemplateCsvContent(inputFields)
|
||||
const link = document.createElement('a')
|
||||
link.href = `data:text/csv;charset=utf-8,${encodeURIComponent(content)}`
|
||||
link.download = templateFileName
|
||||
|
||||
@ -1,39 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import type { EvaluationResourceProps } from '../types'
|
||||
import { useEffect } from 'react'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
|
||||
import { useEvaluationResource, useEvaluationStore } from '../store'
|
||||
import { decodeModelSelection, encodeModelSelection } from '../utils'
|
||||
|
||||
type JudgeModelSelectorProps = EvaluationResourceProps & {
|
||||
autoSelectFirst?: boolean
|
||||
}
|
||||
|
||||
const JudgeModelSelector = ({
|
||||
resourceType,
|
||||
resourceId,
|
||||
autoSelectFirst = true,
|
||||
}: JudgeModelSelectorProps) => {
|
||||
}: EvaluationResourceProps) => {
|
||||
const { data: modelList } = useModelList(ModelTypeEnum.textGeneration)
|
||||
const resource = useEvaluationResource(resourceType, resourceId)
|
||||
const setJudgeModel = useEvaluationStore(state => state.setJudgeModel)
|
||||
const selectedModel = decodeModelSelection(resource.judgeModelId)
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoSelectFirst || resource.judgeModelId || !modelList.length)
|
||||
return
|
||||
|
||||
const firstProvider = modelList[0]
|
||||
const firstModel = firstProvider.models[0]
|
||||
if (!firstProvider || !firstModel)
|
||||
return
|
||||
|
||||
setJudgeModel(resourceType, resourceId, encodeModelSelection(firstProvider.provider, firstModel.model))
|
||||
}, [autoSelectFirst, modelList, resource.judgeModelId, resourceId, resourceType, setJudgeModel])
|
||||
|
||||
return (
|
||||
<ModelSelector
|
||||
defaultModel={selectedModel}
|
||||
|
||||
@ -27,8 +27,8 @@ const PipelineEvaluation = ({
|
||||
}, [ensureResource, resourceId, resourceType])
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col bg-background-default xl:flex-row">
|
||||
<div className="flex min-h-0 flex-col border-b border-divider-subtle bg-background-default xl:w-[450px] xl:shrink-0 xl:border-r xl:border-b-0">
|
||||
<div className="flex h-full min-h-0 flex-col overflow-y-auto bg-background-default xl:flex-row xl:overflow-hidden">
|
||||
<div className="flex shrink-0 flex-col border-b border-divider-subtle bg-background-default xl:min-h-0 xl:w-[450px] xl:border-r xl:border-b-0">
|
||||
<div className="px-6 pt-4 pb-2">
|
||||
<SectionHeader
|
||||
title={t('title')}
|
||||
@ -58,7 +58,6 @@ const PipelineEvaluation = ({
|
||||
<JudgeModelSelector
|
||||
resourceType={resourceType}
|
||||
resourceId={resourceId}
|
||||
autoSelectFirst={false}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@ -77,7 +76,7 @@ const PipelineEvaluation = ({
|
||||
|
||||
<div className="border-t border-divider-subtle" />
|
||||
|
||||
<div className="min-h-0 flex-1 px-6 py-4">
|
||||
<div className="px-6 py-4 xl:min-h-0 xl:flex-1">
|
||||
<HistoryTab
|
||||
resourceType={resourceType}
|
||||
resourceId={resourceId}
|
||||
@ -85,7 +84,7 @@ const PipelineEvaluation = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 bg-background-default">
|
||||
<div className="shrink-0 bg-background-default xl:min-h-0 xl:flex-1">
|
||||
<PipelineResultsPanel
|
||||
resourceType={resourceType}
|
||||
resourceId={resourceId}
|
||||
|
||||
@ -11,8 +11,9 @@ import { useInputFieldsActions } from '../batch-test-panel/input-fields/use-inpu
|
||||
|
||||
const PIPELINE_INPUT_FIELDS: InputField[] = [
|
||||
{ name: 'query', type: 'string' },
|
||||
{ name: 'Expect Results', type: 'string' },
|
||||
{ name: 'expect_results', type: 'string' },
|
||||
]
|
||||
const PIPELINE_TEMPLATE_CONTENT = 'query,expect_results\n'
|
||||
|
||||
const PipelineBatchActions = ({
|
||||
resourceType,
|
||||
@ -29,6 +30,7 @@ const PipelineBatchActions = ({
|
||||
isInputFieldsLoading: false,
|
||||
isPanelReady: isConfigReady,
|
||||
isRunnable,
|
||||
templateContent: PIPELINE_TEMPLATE_CONTENT,
|
||||
templateFileName: EVALUATION_TEMPLATE_FILE_NAMES[resourceType],
|
||||
})
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ const PipelineMetricItem = ({
|
||||
const metricDescription = getTranslatedMetricDescription(t, metric.id, metric.description)
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-3 px-1 py-1">
|
||||
<div className="flex h-8 items-center justify-between gap-3 px-1 py-1">
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-w-0 items-center gap-2 text-left"
|
||||
@ -56,7 +56,7 @@ const PipelineMetricItem = ({
|
||||
? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="system-xs-medium text-text-accent">{t('pipeline.passIf')}</span>
|
||||
<div className="w-[52px]">
|
||||
<div className="w-[64px]">
|
||||
<Input
|
||||
value={String(threshold)}
|
||||
type="number"
|
||||
@ -77,11 +77,12 @@ const PipelineMetricItem = ({
|
||||
type="button"
|
||||
disabled={disabledCondition}
|
||||
className={cn(
|
||||
'system-xs-medium text-text-tertiary',
|
||||
'flex items-center gap-0.5 system-xs-medium text-text-tertiary',
|
||||
disabledCondition && 'cursor-not-allowed text-components-button-secondary-accent-text-disabled',
|
||||
)}
|
||||
>
|
||||
+ Condition
|
||||
<span aria-hidden="true" className="i-ri-add-line h-3.5 w-3.5" />
|
||||
{t('conditions.addCondition')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -7,7 +7,7 @@ import { useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { useAvailableEvaluationMetrics } from '@/service/use-evaluation'
|
||||
import { useDatasetEvaluationMetrics } from '@/service/use-evaluation'
|
||||
import { usePublishedPipelineInfo } from '@/service/use-pipeline'
|
||||
import { useEvaluationResource, useEvaluationStore } from '../../store'
|
||||
import { buildMetricOption } from '../metric-selector/utils'
|
||||
@ -49,7 +49,7 @@ const PipelineMetricsSection = ({
|
||||
const addBuiltinMetric = useEvaluationStore(state => state.addBuiltinMetric)
|
||||
const removeMetric = useEvaluationStore(state => state.removeMetric)
|
||||
const updateMetricThreshold = useEvaluationStore(state => state.updateMetricThreshold)
|
||||
const { data: availableMetricsData } = useAvailableEvaluationMetrics()
|
||||
const { data: datasetMetricsData } = useDatasetEvaluationMetrics(resourceId)
|
||||
const { data: publishedPipeline } = usePublishedPipelineInfo(pipelineId || '')
|
||||
const resource = useEvaluationResource(resourceType, resourceId)
|
||||
const knowledgeIndexNodeInfoList = useMemo(
|
||||
@ -63,12 +63,12 @@ const PipelineMetricsSection = ({
|
||||
), [resource.metrics])
|
||||
const availableBuiltinMetrics = useMemo(() => {
|
||||
const metricIds = new Set([
|
||||
...(availableMetricsData?.metrics ?? []),
|
||||
...(datasetMetricsData?.metrics ?? []),
|
||||
...builtinMetricMap.keys(),
|
||||
])
|
||||
|
||||
return Array.from(metricIds).map(metricId => buildMetricOption(metricId))
|
||||
}, [availableMetricsData?.metrics, builtinMetricMap])
|
||||
}, [datasetMetricsData?.metrics, builtinMetricMap])
|
||||
|
||||
useEffect(() => {
|
||||
if (!knowledgeIndexNodeInfoList.length)
|
||||
|
||||
@ -57,7 +57,7 @@ const PipelineResultsPanel = ({
|
||||
|
||||
if (isEmpty) {
|
||||
return (
|
||||
<div className="flex min-h-[360px] flex-1 items-center justify-center xl:min-h-0">
|
||||
<div className="flex min-h-[360px] w-full items-center justify-center xl:h-full xl:min-h-0">
|
||||
<div className="flex flex-col items-center gap-4 px-4 text-center">
|
||||
<span aria-hidden="true" className="i-ri-file-list-3-line h-12 w-12 text-text-quaternary" />
|
||||
<div className="system-md-medium text-text-quaternary">{t('results.empty')}</div>
|
||||
@ -67,7 +67,7 @@ const PipelineResultsPanel = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col border-l border-divider-subtle bg-background-default">
|
||||
<div className="flex min-h-[360px] flex-col border-l border-divider-subtle bg-background-default xl:h-full xl:min-h-0">
|
||||
<div className="shrink-0 px-6 pt-4 pb-2">
|
||||
<h2 className="system-xl-semibold text-text-primary">{t('results.title')}</h2>
|
||||
</div>
|
||||
@ -75,7 +75,7 @@ const PipelineResultsPanel = ({
|
||||
<div className="px-6 py-4 system-sm-regular text-text-destructive">{t('results.loadFailed')}</div>
|
||||
)}
|
||||
{!runDetailQuery.isError && (
|
||||
<div className="flex min-h-0 flex-1 flex-col px-6 py-1">
|
||||
<div className="flex flex-col px-6 py-1 xl:min-h-0 xl:flex-1">
|
||||
<div className="flex shrink-0 flex-wrap items-center justify-between gap-3 py-1">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2 system-xs-regular text-text-secondary">
|
||||
<span>{getRunDate(runDetail?.run.started_at ?? runDetail?.run.created_at ?? null)}</span>
|
||||
|
||||
@ -26,7 +26,7 @@ const PipelineResultsTable = ({
|
||||
const { t } = useTranslation('evaluation')
|
||||
|
||||
return (
|
||||
<div className="min-h-0 flex-1 overflow-auto py-2">
|
||||
<div className="overflow-x-auto py-2 xl:min-h-0 xl:flex-1 xl:overflow-auto">
|
||||
<table className="min-w-full table-fixed border-collapse overflow-hidden rounded-lg">
|
||||
<colgroup>
|
||||
<col className="w-10" />
|
||||
@ -38,14 +38,14 @@ const PipelineResultsTable = ({
|
||||
<thead>
|
||||
<tr className="bg-background-section">
|
||||
<th className="h-7 rounded-l-lg" />
|
||||
<th className="system-xs-medium-uppercase h-7 px-3 text-left text-text-tertiary">{t('results.columns.query')}</th>
|
||||
<th className="system-xs-medium-uppercase h-7 px-3 text-left text-text-tertiary">{t('results.columns.expected')}</th>
|
||||
<th className="system-xs-medium-uppercase h-7 px-3 text-left text-text-tertiary">{t('results.columns.actual')}</th>
|
||||
<th className="h-7 px-3 text-left system-xs-medium-uppercase text-text-tertiary">{t('results.columns.query')}</th>
|
||||
<th className="h-7 px-3 text-left system-xs-medium-uppercase text-text-tertiary">{t('results.columns.expected')}</th>
|
||||
<th className="h-7 px-3 text-left system-xs-medium-uppercase text-text-tertiary">{t('results.columns.actual')}</th>
|
||||
{metricColumns.map((column, index) => (
|
||||
<th
|
||||
key={column.id}
|
||||
className={cn(
|
||||
'system-xs-medium-uppercase h-7 px-3 text-left text-text-tertiary',
|
||||
'h-7 px-3 text-left system-xs-medium-uppercase text-text-tertiary',
|
||||
index === metricColumns.length - 1 && 'rounded-r-lg',
|
||||
)}
|
||||
>
|
||||
@ -79,14 +79,14 @@ const PipelineResultsTable = ({
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
<td className="system-sm-regular h-10 px-3 py-3 align-top text-text-secondary">
|
||||
<td className="h-10 px-3 py-3 align-top system-sm-regular text-text-secondary">
|
||||
<div className="line-clamp-2 break-words">{getQueryContent(item)}</div>
|
||||
</td>
|
||||
<td className="system-sm-regular h-10 px-3 py-3 align-top text-text-secondary">
|
||||
<td className="h-10 px-3 py-3 align-top system-sm-regular text-text-secondary">
|
||||
<div className="line-clamp-2 break-words">{formatValue(item.expected_output)}</div>
|
||||
</td>
|
||||
<td className={cn(
|
||||
'system-sm-regular h-10 px-3 py-3 align-top',
|
||||
'h-10 px-3 py-3 align-top system-sm-regular',
|
||||
actualOutput ? 'text-text-secondary' : 'text-text-destructive',
|
||||
)}
|
||||
>
|
||||
@ -100,7 +100,7 @@ const PipelineResultsTable = ({
|
||||
return (
|
||||
<td
|
||||
key={column.id}
|
||||
className={cn('system-sm-regular h-10 px-3 py-3 align-top', getMetricTextClassName(metricValue, column))}
|
||||
className={cn('h-10 px-3 py-3 align-top system-sm-regular', getMetricTextClassName(metricValue, column))}
|
||||
>
|
||||
{formatValue(metricValue)}
|
||||
</td>
|
||||
|
||||
@ -316,5 +316,16 @@ describe('SnippetMain', () => {
|
||||
expect(capturedHooksStore?.handleStartWorkflowRun).toBe(mockHandleStartWorkflowRun)
|
||||
expect(capturedHooksStore?.handleWorkflowStartRunInWorkflow).toBe(mockHandleWorkflowStartRunInWorkflow)
|
||||
})
|
||||
|
||||
it('should pass snippet workflow run detail urls to WorkflowWithInnerContext', () => {
|
||||
renderSnippetMain()
|
||||
|
||||
const getWorkflowRunAndTraceUrl = capturedHooksStore?.getWorkflowRunAndTraceUrl as ((runId?: string) => { runUrl: string, traceUrl: string }) | undefined
|
||||
|
||||
expect(getWorkflowRunAndTraceUrl?.('run-1')).toEqual({
|
||||
runUrl: '/snippets/snippet-1/workflow-runs/run-1',
|
||||
traceUrl: '/snippets/snippet-1/workflow-runs/run-1/node-executions',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -13,6 +13,7 @@ import { useAvailableNodesMetaData } from '@/app/components/workflow-app/hooks'
|
||||
import { useSetWorkflowVarsWithValue } from '@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useConfigsMap } from '../hooks/use-configs-map'
|
||||
import { useGetRunAndTraceUrl } from '../hooks/use-get-run-and-trace-url'
|
||||
import { useInspectVarsCrud } from '../hooks/use-inspect-vars-crud'
|
||||
import { useNodesSyncDraft } from '../hooks/use-nodes-sync-draft'
|
||||
import { useSnippetRefreshDraft } from '../hooks/use-snippet-refresh-draft'
|
||||
@ -179,6 +180,7 @@ const SnippetMain = ({
|
||||
handleRun,
|
||||
inputFields: fields,
|
||||
})
|
||||
const { getWorkflowRunAndTraceUrl } = useGetRunAndTraceUrl(snippetId)
|
||||
|
||||
useEffect(() => {
|
||||
reset()
|
||||
@ -200,6 +202,7 @@ const SnippetMain = ({
|
||||
handleStopRun,
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
getWorkflowRunAndTraceUrl,
|
||||
availableNodesMetaData,
|
||||
fetchInspectVars,
|
||||
hasNodeInspectVars,
|
||||
@ -237,6 +240,7 @@ const SnippetMain = ({
|
||||
handleStartWorkflowRun,
|
||||
handleStopRun,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
getWorkflowRunAndTraceUrl,
|
||||
hasNodeInspectVars,
|
||||
hasSetInspectVar,
|
||||
invalidateConversationVarValues,
|
||||
|
||||
@ -263,7 +263,7 @@ const SnippetRunPanel = ({
|
||||
elapsed_time={workflowRunningData.result?.elapsed_time}
|
||||
total_tokens={workflowRunningData.result?.total_tokens}
|
||||
created_at={workflowRunningData.result?.created_at}
|
||||
created_by={workflowRunningData.result?.created_by}
|
||||
created_by={(workflowRunningData.result?.created_by as unknown as { name: string })?.name}
|
||||
steps={workflowRunningData.result?.total_steps}
|
||||
exceptionCounts={workflowRunningData.result?.exceptions_count}
|
||||
/>
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useGetRunAndTraceUrl } from '../use-get-run-and-trace-url'
|
||||
|
||||
describe('useGetRunAndTraceUrl', () => {
|
||||
it('should build snippet workflow run and trace urls from the snippet id', () => {
|
||||
const { result } = renderHook(() => useGetRunAndTraceUrl('snippet-1'))
|
||||
|
||||
expect(result.current.getWorkflowRunAndTraceUrl('run-1')).toEqual({
|
||||
runUrl: '/snippets/snippet-1/workflow-runs/run-1',
|
||||
traceUrl: '/snippets/snippet-1/workflow-runs/run-1/node-executions',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return empty urls when no run id is provided', () => {
|
||||
const { result } = renderHook(() => useGetRunAndTraceUrl('snippet-1'))
|
||||
|
||||
expect(result.current.getWorkflowRunAndTraceUrl()).toEqual({
|
||||
runUrl: '',
|
||||
traceUrl: '',
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,21 @@
|
||||
import { useCallback } from 'react'
|
||||
|
||||
export const useGetRunAndTraceUrl = (snippetId: string) => {
|
||||
const getWorkflowRunAndTraceUrl = useCallback((runId?: string) => {
|
||||
if (!runId) {
|
||||
return {
|
||||
runUrl: '',
|
||||
traceUrl: '',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
runUrl: `/snippets/${snippetId}/workflow-runs/${runId}`,
|
||||
traceUrl: `/snippets/${snippetId}/workflow-runs/${runId}/node-executions`,
|
||||
}
|
||||
}, [snippetId])
|
||||
|
||||
return {
|
||||
getWorkflowRunAndTraceUrl,
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,10 @@
|
||||
import type { ModelAndParameter } from '@/app/components/app/configuration/debug/types'
|
||||
import type { AppPublisherPublishParams } from '@/app/components/app/app-publisher'
|
||||
import type { EndNodeType } from '@/app/components/workflow/nodes/end/types'
|
||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||
import type {
|
||||
CommonEdgeType,
|
||||
Node,
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
@ -144,7 +143,7 @@ const FeaturesTrigger = () => {
|
||||
const needWarningNodes = useChecklist(nodes, edges)
|
||||
|
||||
const updatePublishedWorkflow = useInvalidateAppWorkflow()
|
||||
const onPublish = useCallback(async (params?: ModelAndParameter | PublishWorkflowParams) => {
|
||||
const onPublish = useCallback(async (params?: AppPublisherPublishParams) => {
|
||||
const publishParams = params && 'title' in params ? params : undefined
|
||||
// First check if there are any items in the checklist
|
||||
// if (!validateBeforeRun())
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { WorkflowKind } from '@/types/workflow'
|
||||
import { AppTypeEnum } from '@/types/app'
|
||||
import { BlockEnum, TRIGGER_NODE_TYPES } from '../types'
|
||||
|
||||
@ -6,7 +7,7 @@ const EVALUATION_WORKFLOW_RESTRICTED_NODE_TYPES = new Set<string>([
|
||||
...TRIGGER_NODE_TYPES,
|
||||
])
|
||||
|
||||
export const isEvaluationWorkflow = (appType?: string) => appType === AppTypeEnum.EVALUATION
|
||||
export const isEvaluationWorkflow = (appType?: WorkflowKind | null) => appType === AppTypeEnum.EVALUATION
|
||||
|
||||
export const isEvaluationWorkflowRestrictedNodeType = (nodeType?: string) => {
|
||||
if (!nodeType)
|
||||
|
||||
@ -284,13 +284,6 @@ export const evaluationNodeInfoContract = base
|
||||
}>())
|
||||
.output(type<EvaluationNodeInfoResponse>())
|
||||
|
||||
export const availableEvaluationMetricsContract = base
|
||||
.route({
|
||||
path: '/evaluation/available-metrics',
|
||||
method: 'GET',
|
||||
})
|
||||
.output(type<EvaluationMetricsListResponse>())
|
||||
|
||||
export const availableEvaluationWorkflowsContract = base
|
||||
.route({
|
||||
path: '/workspaces/current/available-evaluation-workflows',
|
||||
|
||||
@ -3,7 +3,6 @@ import { accountAvatarContract } from './console/account'
|
||||
import { appDeleteContract, appWorkflowTypeConvertContract, workflowOnlineUsersContract } from './console/apps'
|
||||
import { bindPartnerStackContract, invoicesContract } from './console/billing'
|
||||
import {
|
||||
availableEvaluationMetricsContract,
|
||||
availableEvaluationWorkflowsContract,
|
||||
cancelDatasetEvaluationRunContract,
|
||||
cancelEvaluationRunContract,
|
||||
@ -148,7 +147,6 @@ export const consoleRouterContract = {
|
||||
metrics: evaluationMetricsContract,
|
||||
defaultMetrics: evaluationDefaultMetricsContract,
|
||||
nodeInfo: evaluationNodeInfoContract,
|
||||
availableMetrics: availableEvaluationMetricsContract,
|
||||
availableWorkflows: availableEvaluationWorkflowsContract,
|
||||
associatedTargets: evaluationWorkflowAssociatedTargetsContract,
|
||||
file: evaluationFileContract,
|
||||
|
||||
@ -54,9 +54,17 @@ export const useEvaluationConfig = (
|
||||
return useQuery<EvaluationConfig>(getEvaluationConfigQueryOptions(resourceType, resourceId))
|
||||
}
|
||||
|
||||
export const useAvailableEvaluationMetrics = (enabled = true) => {
|
||||
return useQuery(consoleQuery.evaluation.availableMetrics.queryOptions({
|
||||
enabled,
|
||||
export const useDatasetEvaluationMetrics = (datasetId: string, enabled = true) => {
|
||||
return useQuery(consoleQuery.datasetEvaluation.metrics.queryOptions({
|
||||
input: datasetId
|
||||
? {
|
||||
params: {
|
||||
datasetId,
|
||||
},
|
||||
}
|
||||
: skipToken,
|
||||
enabled: !!datasetId && enabled,
|
||||
refetchOnWindowFocus: false,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import type {
|
||||
WeightedScoreEnum,
|
||||
} from '@/models/datasets'
|
||||
import type { AnnotationReplyConfig, ChatPromptConfig, CompletionPromptConfig, DatasetConfigs, PromptMode } from '@/models/debug'
|
||||
import type { WorkflowKind } from '@/types/workflow'
|
||||
|
||||
export type Theme = 'light' | 'dark' | 'system'
|
||||
export const Theme = {
|
||||
@ -392,7 +393,7 @@ export type App = {
|
||||
/** whether workflow trigger has un-published draft */
|
||||
has_draft_trigger?: boolean
|
||||
/** Type */
|
||||
workflow_kind?: AppTypeEnum
|
||||
workflow_kind?: WorkflowKind | null
|
||||
}
|
||||
|
||||
export type AppSSO = {
|
||||
|
||||
@ -429,6 +429,8 @@ export type PublishWorkflowParams = {
|
||||
|
||||
export type WorkflowTypeConversionTarget = 'workflow' | 'evaluation'
|
||||
|
||||
export type WorkflowKind = 'standard' | 'evaluation'
|
||||
|
||||
export type UpdateWorkflowParams = {
|
||||
url: string
|
||||
title: string
|
||||
|
||||
Loading…
Reference in New Issue
Block a user