Files
dify/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.tsx
2025-12-30 10:46:52 +08:00

353 lines
13 KiB
TypeScript

import type { IconInfo } from '@/models/datasets'
import type { PublishWorkflowParams } from '@/types/workflow'
import {
RiArrowRightUpLine,
RiHammerLine,
RiPlayCircleLine,
RiTerminalBoxLine,
} from '@remixicon/react'
import {
useBoolean,
useKeyPress,
} from 'ahooks'
import Link from 'next/link'
import { useParams, useRouter } from 'next/navigation'
import {
memo,
useCallback,
useState,
} from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { trackEvent } from '@/app/components/base/amplitude'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import Divider from '@/app/components/base/divider'
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
import PremiumBadge from '@/app/components/base/premium-badge'
import { useToastContext } from '@/app/components/base/toast'
import {
useChecklistBeforePublish,
} from '@/app/components/workflow/hooks'
import {
useStore,
useWorkflowStore,
} from '@/app/components/workflow/store'
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useModalContextSelector } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
import { useInvalid } from '@/service/use-base'
import {
publishedPipelineInfoQueryKeyPrefix,
useInvalidCustomizedTemplateList,
usePublishAsCustomizedPipeline,
} from '@/service/use-pipeline'
import { usePublishWorkflow } from '@/service/use-workflow'
import { cn } from '@/utils/classnames'
import PublishAsKnowledgePipelineModal from '../../publish-as-knowledge-pipeline-modal'
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
const Popup = () => {
const { t } = useTranslation()
const { datasetId } = useParams()
const { push } = useRouter()
const publishedAt = useStore(s => s.publishedAt)
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
const pipelineId = useStore(s => s.pipelineId)
const mutateDatasetRes = useDatasetDetailContextWithSelector(s => s.mutateDatasetRes)
const [published, setPublished] = useState(false)
const { formatTimeFromNow } = useFormatTimeFromNow()
const { handleCheckBeforePublish } = useChecklistBeforePublish()
const { mutateAsync: publishWorkflow } = usePublishWorkflow()
const { notify } = useToastContext()
const workflowStore = useWorkflowStore()
const { isAllowPublishAsCustomKnowledgePipelineTemplate } = useProviderContext()
const setShowPricingModal = useModalContextSelector(s => s.setShowPricingModal)
const apiReferenceUrl = useDatasetApiAccessUrl()
const [confirmVisible, {
setFalse: hideConfirm,
setTrue: showConfirm,
}] = useBoolean(false)
const [publishing, {
setFalse: hidePublishing,
setTrue: showPublishing,
}] = useBoolean(false)
const {
mutateAsync: publishAsCustomizedPipeline,
} = usePublishAsCustomizedPipeline()
const [showPublishAsKnowledgePipelineModal, {
setFalse: hidePublishAsKnowledgePipelineModal,
setTrue: setShowPublishAsKnowledgePipelineModal,
}] = useBoolean(false)
const [isPublishingAsCustomizedPipeline, {
setFalse: hidePublishingAsCustomizedPipeline,
setTrue: showPublishingAsCustomizedPipeline,
}] = useBoolean(false)
const invalidPublishedPipelineInfo = useInvalid([...publishedPipelineInfoQueryKeyPrefix, pipelineId])
const invalidDatasetList = useInvalidDatasetList()
const handlePublish = useCallback(async (params?: PublishWorkflowParams) => {
if (publishing)
return
try {
const checked = await handleCheckBeforePublish()
if (checked) {
if (!publishedAt && !confirmVisible) {
showConfirm()
return
}
showPublishing()
const res = await publishWorkflow({
url: `/rag/pipelines/${pipelineId}/workflows/publish`,
title: params?.title || '',
releaseNotes: params?.releaseNotes || '',
})
setPublished(true)
trackEvent('app_published_time', { action_mode: 'pipeline', app_id: datasetId, app_name: params?.title || '' })
if (res) {
notify({
type: 'success',
message: t('publishPipeline.success.message', { ns: 'datasetPipeline' }),
children: (
<div className="system-xs-regular text-text-secondary">
<Trans
i18nKey="publishPipeline.success.tip"
ns="datasetPipeline"
components={{
CustomLink: (
<Link
className="system-xs-medium text-text-accent"
href={`/datasets/${datasetId}/documents`}
>
</Link>
),
}}
/>
</div>
),
})
workflowStore.getState().setPublishedAt(res.created_at)
mutateDatasetRes?.()
invalidPublishedPipelineInfo()
invalidDatasetList()
}
}
}
catch {
notify({ type: 'error', message: t('publishPipeline.error.message', { ns: 'datasetPipeline' }) })
}
finally {
if (publishing)
hidePublishing()
if (confirmVisible)
hideConfirm()
}
}, [handleCheckBeforePublish, publishWorkflow, pipelineId, notify, t, workflowStore, mutateDatasetRes, invalidPublishedPipelineInfo, showConfirm, publishedAt, confirmVisible, hidePublishing, showPublishing, hideConfirm, publishing])
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (e) => {
e.preventDefault()
if (published)
return
handlePublish()
}, { exactMatch: true, useCapture: true })
const goToAddDocuments = useCallback(() => {
push(`/datasets/${datasetId}/documents/create-from-pipeline`)
}, [datasetId, push])
const invalidCustomizedTemplateList = useInvalidCustomizedTemplateList()
const handlePublishAsKnowledgePipeline = useCallback(async (
name: string,
icon: IconInfo,
description?: string,
) => {
try {
showPublishingAsCustomizedPipeline()
await publishAsCustomizedPipeline({
pipelineId: pipelineId || '',
name,
icon_info: icon,
description,
})
notify({
type: 'success',
message: t('publishTemplate.success.message', { ns: 'datasetPipeline' }),
children: (
<div className="flex flex-col gap-y-1">
<span className="system-xs-regular text-text-secondary">
{t('publishTemplate.success.tip', { ns: 'datasetPipeline' })}
</span>
<Link
href="https://docs.dify.ai"
target="_blank"
className="system-xs-medium-uppercase inline-block text-text-accent"
>
{t('publishTemplate.success.learnMore', { ns: 'datasetPipeline' })}
</Link>
</div>
),
})
invalidCustomizedTemplateList()
}
catch {
notify({ type: 'error', message: t('publishTemplate.error.message', { ns: 'datasetPipeline' }) })
}
finally {
hidePublishingAsCustomizedPipeline()
hidePublishAsKnowledgePipelineModal()
}
}, [
pipelineId,
publishAsCustomizedPipeline,
showPublishingAsCustomizedPipeline,
hidePublishingAsCustomizedPipeline,
hidePublishAsKnowledgePipelineModal,
notify,
t,
])
const handleClickPublishAsKnowledgePipeline = useCallback(() => {
if (!isAllowPublishAsCustomKnowledgePipelineTemplate)
setShowPricingModal()
else
setShowPublishAsKnowledgePipelineModal()
}, [isAllowPublishAsCustomKnowledgePipelineTemplate, setShowPublishAsKnowledgePipelineModal, setShowPricingModal])
return (
<div className={cn('rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl shadow-shadow-shadow-5', isAllowPublishAsCustomKnowledgePipelineTemplate ? 'w-[360px]' : 'w-[400px]')}>
<div className="p-4 pt-3">
<div className="system-xs-medium-uppercase flex h-6 items-center text-text-tertiary">
{publishedAt ? t('common.latestPublished', { ns: 'workflow' }) : t('common.currentDraftUnpublished', { ns: 'workflow' })}
</div>
{
publishedAt
? (
<div className="flex items-center justify-between">
<div className="system-sm-medium flex items-center text-text-secondary">
{t('common.publishedAt', { ns: 'workflow' })}
{' '}
{formatTimeFromNow(publishedAt)}
</div>
</div>
)
: (
<div className="system-sm-medium flex items-center text-text-secondary">
{t('common.autoSaved', { ns: 'workflow' })}
{' '}
·
{Boolean(draftUpdatedAt) && formatTimeFromNow(draftUpdatedAt!)}
</div>
)
}
<Button
variant="primary"
className="mt-3 w-full"
onClick={() => handlePublish()}
disabled={published || publishing}
>
{
published
? t('common.published', { ns: 'workflow' })
: (
<div className="flex gap-1">
<span>{t('common.publishUpdate', { ns: 'workflow' })}</span>
<div className="flex gap-0.5">
{PUBLISH_SHORTCUT.map(key => (
<span key={key} className="system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface">
{getKeyboardKeyNameBySystem(key)}
</span>
))}
</div>
</div>
)
}
</Button>
</div>
<div className="border-t-[0.5px] border-t-divider-regular p-4 pt-3">
<Button
className="mb-1 w-full hover:bg-state-accent-hover hover:text-text-accent"
variant="tertiary"
onClick={goToAddDocuments}
disabled={!publishedAt}
>
<div className="flex grow items-center">
<RiPlayCircleLine className="mr-2 h-4 w-4" />
{t('common.goToAddDocuments', { ns: 'pipeline' })}
</div>
<RiArrowRightUpLine className="ml-2 h-4 w-4 shrink-0" />
</Button>
<Link
href={apiReferenceUrl}
target="_blank"
rel="noopener noreferrer"
>
<Button
className="w-full hover:bg-state-accent-hover hover:text-text-accent"
variant="tertiary"
disabled={!publishedAt}
>
<div className="flex grow items-center">
<RiTerminalBoxLine className="mr-2 h-4 w-4" />
{t('common.accessAPIReference', { ns: 'workflow' })}
</div>
<RiArrowRightUpLine className="ml-2 h-4 w-4 shrink-0" />
</Button>
</Link>
<Divider className="my-2" />
<Button
className="w-full hover:bg-state-accent-hover hover:text-text-accent"
variant="tertiary"
onClick={handleClickPublishAsKnowledgePipeline}
disabled={!publishedAt || isPublishingAsCustomizedPipeline}
>
<div className="flex grow items-center gap-x-2 overflow-hidden">
<RiHammerLine className="h-4 w-4 shrink-0" />
<span className="grow truncate text-left" title={t('common.publishAs', { ns: 'pipeline' })}>
{t('common.publishAs', { ns: 'pipeline' })}
</span>
{!isAllowPublishAsCustomKnowledgePipelineTemplate && (
<PremiumBadge className="shrink-0 cursor-pointer select-none" size="s" color="indigo">
<SparklesSoft className="flex size-3 items-center text-components-premium-badge-indigo-text-stop-0" />
<span className="system-2xs-medium p-0.5">
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
</span>
</PremiumBadge>
)}
</div>
</Button>
</div>
{
confirmVisible && (
<Confirm
isShow={confirmVisible}
title={t('common.confirmPublish', { ns: 'pipeline' })}
content={t('common.confirmPublishContent', { ns: 'pipeline' })}
onCancel={hideConfirm}
onConfirm={handlePublish}
isDisabled={publishing}
/>
)
}
{
showPublishAsKnowledgePipelineModal && (
<PublishAsKnowledgePipelineModal
confirmDisabled={isPublishingAsCustomizedPipeline}
onConfirm={handlePublishAsKnowledgePipeline}
onCancel={hidePublishAsKnowledgePipelineModal}
/>
)
}
</div>
)
}
export default memo(Popup)