mirror of
https://github.com/langgenius/dify.git
synced 2026-01-19 11:45:05 +08:00
refactor: restructure DatasetCard component for improved readability and maintainability (#30617)
Co-authored-by: CodingOnStar <hanxujiang@dify.ai>
This commit is contained in:
@ -0,0 +1,36 @@
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import CornerLabel from '@/app/components/base/corner-label'
|
||||
|
||||
type CornerLabelsProps = {
|
||||
dataset: DataSet
|
||||
}
|
||||
|
||||
const CornerLabels = ({ dataset }: CornerLabelsProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!dataset.embedding_available) {
|
||||
return (
|
||||
<CornerLabel
|
||||
label={t('cornerLabel.unavailable', { ns: 'dataset' })}
|
||||
className="absolute right-0 top-0 z-10"
|
||||
labelClassName="rounded-tr-xl"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (dataset.runtime_mode === 'rag_pipeline') {
|
||||
return (
|
||||
<CornerLabel
|
||||
label={t('cornerLabel.pipeline', { ns: 'dataset' })}
|
||||
className="absolute right-0 top-0 z-10"
|
||||
labelClassName="rounded-tr-xl"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default React.memo(CornerLabels)
|
||||
@ -0,0 +1,62 @@
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { RiFileTextFill, RiRobot2Fill } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
const EXTERNAL_PROVIDER = 'external'
|
||||
|
||||
type DatasetCardFooterProps = {
|
||||
dataset: DataSet
|
||||
}
|
||||
|
||||
const DatasetCardFooter = ({ dataset }: DatasetCardFooterProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
const isExternalProvider = dataset.provider === EXTERNAL_PROVIDER
|
||||
|
||||
const documentCount = useMemo(() => {
|
||||
const availableDocCount = dataset.total_available_documents ?? 0
|
||||
if (availableDocCount < dataset.document_count)
|
||||
return `${availableDocCount} / ${dataset.document_count}`
|
||||
return `${dataset.document_count}`
|
||||
}, [dataset.document_count, dataset.total_available_documents])
|
||||
|
||||
const documentCountTooltip = useMemo(() => {
|
||||
const availableDocCount = dataset.total_available_documents ?? 0
|
||||
if (availableDocCount < dataset.document_count)
|
||||
return t('partialEnabled', { ns: 'dataset', count: dataset.document_count, num: availableDocCount })
|
||||
return t('docAllEnabled', { ns: 'dataset', count: availableDocCount })
|
||||
}, [t, dataset.document_count, dataset.total_available_documents])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-x-3 px-4 pb-3 pt-2 text-text-tertiary',
|
||||
!dataset.embedding_available && 'opacity-30',
|
||||
)}
|
||||
>
|
||||
<Tooltip popupContent={documentCountTooltip}>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<RiFileTextFill className="size-3 text-text-quaternary" />
|
||||
<span className="system-xs-medium">{documentCount}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{!isExternalProvider && (
|
||||
<Tooltip popupContent={`${dataset.app_count} ${t('appCount', { ns: 'dataset' })}`}>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<RiRobot2Fill className="size-3 text-text-quaternary" />
|
||||
<span className="system-xs-medium">{dataset.app_count}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
<span className="system-xs-regular text-divider-deep">/</span>
|
||||
<span className="system-xs-regular">{`${t('updated', { ns: 'dataset' })} ${formatTimeFromNow(dataset.updated_at * 1000)}`}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(DatasetCardFooter)
|
||||
@ -0,0 +1,148 @@
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { useKnowledge } from '@/hooks/use-knowledge'
|
||||
import { DOC_FORM_ICON_WITH_BG, DOC_FORM_TEXT } from '@/models/datasets'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
const EXTERNAL_PROVIDER = 'external'
|
||||
|
||||
type DatasetCardHeaderProps = {
|
||||
dataset: DataSet
|
||||
}
|
||||
|
||||
// DocModeInfo component - placed before usage
|
||||
type DocModeInfoProps = {
|
||||
dataset: DataSet
|
||||
isExternalProvider: boolean
|
||||
isShowDocModeInfo: boolean
|
||||
}
|
||||
|
||||
const DocModeInfo = ({
|
||||
dataset,
|
||||
isExternalProvider,
|
||||
isShowDocModeInfo,
|
||||
}: DocModeInfoProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { formatIndexingTechniqueAndMethod } = useKnowledge()
|
||||
|
||||
if (isExternalProvider) {
|
||||
return (
|
||||
<div className="system-2xs-medium-uppercase flex items-center gap-x-3 text-text-tertiary">
|
||||
<span>{t('externalKnowledgeBase', { ns: 'dataset' })}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isShowDocModeInfo)
|
||||
return null
|
||||
|
||||
const indexingText = dataset.indexing_technique
|
||||
? formatIndexingTechniqueAndMethod(
|
||||
dataset.indexing_technique as 'economy' | 'high_quality',
|
||||
dataset.retrieval_model_dict?.search_method as Parameters<typeof formatIndexingTechniqueAndMethod>[1],
|
||||
)
|
||||
: ''
|
||||
|
||||
return (
|
||||
<div className="system-2xs-medium-uppercase flex items-center gap-x-3 text-text-tertiary">
|
||||
{dataset.doc_form && (
|
||||
<span
|
||||
className="min-w-0 max-w-full truncate"
|
||||
title={t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })}
|
||||
>
|
||||
{t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })}
|
||||
</span>
|
||||
)}
|
||||
{dataset.indexing_technique && indexingText && (
|
||||
<span
|
||||
className="min-w-0 max-w-full truncate"
|
||||
title={indexingText}
|
||||
>
|
||||
{indexingText}
|
||||
</span>
|
||||
)}
|
||||
{dataset.is_multimodal && (
|
||||
<span
|
||||
className="min-w-0 max-w-full truncate"
|
||||
title={t('multimodal', { ns: 'dataset' })}
|
||||
>
|
||||
{t('multimodal', { ns: 'dataset' })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Main DatasetCardHeader component
|
||||
const DatasetCardHeader = ({ dataset }: DatasetCardHeaderProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
|
||||
const isExternalProvider = dataset.provider === EXTERNAL_PROVIDER
|
||||
|
||||
const isShowChunkingModeIcon = dataset.doc_form && (dataset.runtime_mode !== 'rag_pipeline' || dataset.is_published)
|
||||
const isShowDocModeInfo = Boolean(
|
||||
dataset.doc_form
|
||||
&& dataset.indexing_technique
|
||||
&& dataset.retrieval_model_dict?.search_method
|
||||
&& (dataset.runtime_mode !== 'rag_pipeline' || dataset.is_published),
|
||||
)
|
||||
|
||||
const chunkingModeIcon = dataset.doc_form ? DOC_FORM_ICON_WITH_BG[dataset.doc_form] : React.Fragment
|
||||
const Icon = isExternalProvider ? DOC_FORM_ICON_WITH_BG.external : chunkingModeIcon
|
||||
|
||||
const iconInfo = useMemo(() => dataset.icon_info || {
|
||||
icon: '📙',
|
||||
icon_type: 'emoji' as const,
|
||||
icon_background: '#FFF4ED',
|
||||
icon_url: '',
|
||||
}, [dataset.icon_info])
|
||||
|
||||
const editTimeText = useMemo(
|
||||
() => `${t('segment.editedAt', { ns: 'datasetDocuments' })} ${formatTimeFromNow(dataset.updated_at * 1000)}`,
|
||||
[t, dataset.updated_at, formatTimeFromNow],
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center gap-x-3 px-4 pb-2 pt-4', !dataset.embedding_available && 'opacity-30')}>
|
||||
<div className="relative shrink-0">
|
||||
<AppIcon
|
||||
size="large"
|
||||
iconType={iconInfo.icon_type}
|
||||
icon={iconInfo.icon}
|
||||
background={iconInfo.icon_type === 'image' ? undefined : iconInfo.icon_background}
|
||||
imageUrl={iconInfo.icon_type === 'image' ? iconInfo.icon_url : undefined}
|
||||
/>
|
||||
{(isShowChunkingModeIcon || isExternalProvider) && (
|
||||
<div className="absolute -bottom-1 -right-1 z-[5]">
|
||||
<Icon className="size-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex grow flex-col gap-y-1 overflow-hidden py-px">
|
||||
<div
|
||||
className="system-md-semibold truncate text-text-secondary"
|
||||
title={dataset.name}
|
||||
>
|
||||
{dataset.name}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[10px] font-medium leading-[18px] text-text-tertiary">
|
||||
<div className="truncate" title={dataset.author_name}>{dataset.author_name}</div>
|
||||
<div>·</div>
|
||||
<div className="truncate" title={editTimeText}>{editTimeText}</div>
|
||||
</div>
|
||||
<DocModeInfo
|
||||
dataset={dataset}
|
||||
isExternalProvider={isExternalProvider}
|
||||
isShowDocModeInfo={isShowDocModeInfo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(DatasetCardHeader)
|
||||
@ -0,0 +1,55 @@
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import RenameDatasetModal from '../../../rename-modal'
|
||||
|
||||
type ModalState = {
|
||||
showRenameModal: boolean
|
||||
showConfirmDelete: boolean
|
||||
confirmMessage: string
|
||||
}
|
||||
|
||||
type DatasetCardModalsProps = {
|
||||
dataset: DataSet
|
||||
modalState: ModalState
|
||||
onCloseRename: () => void
|
||||
onCloseConfirm: () => void
|
||||
onConfirmDelete: () => void
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
const DatasetCardModals = ({
|
||||
dataset,
|
||||
modalState,
|
||||
onCloseRename,
|
||||
onCloseConfirm,
|
||||
onConfirmDelete,
|
||||
onSuccess,
|
||||
}: DatasetCardModalsProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
{modalState.showRenameModal && (
|
||||
<RenameDatasetModal
|
||||
show={modalState.showRenameModal}
|
||||
dataset={dataset}
|
||||
onClose={onCloseRename}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
)}
|
||||
{modalState.showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('deleteDatasetConfirmTitle', { ns: 'dataset' })}
|
||||
content={modalState.confirmMessage}
|
||||
isShow={modalState.showConfirmDelete}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={onCloseConfirm}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(DatasetCardModals)
|
||||
@ -0,0 +1,18 @@
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type DescriptionProps = {
|
||||
dataset: DataSet
|
||||
}
|
||||
|
||||
const Description = ({ dataset }: DescriptionProps) => (
|
||||
<div
|
||||
className={cn('system-xs-regular line-clamp-2 h-10 px-4 py-1 text-text-tertiary', !dataset.embedding_available && 'opacity-30')}
|
||||
title={dataset.description}
|
||||
>
|
||||
{dataset.description}
|
||||
</div>
|
||||
)
|
||||
|
||||
export default React.memo(Description)
|
||||
@ -0,0 +1,52 @@
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { RiMoreFill } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import CustomPopover from '@/app/components/base/popover'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Operations from '../operations'
|
||||
|
||||
type OperationsPopoverProps = {
|
||||
dataset: DataSet
|
||||
isCurrentWorkspaceDatasetOperator: boolean
|
||||
openRenameModal: () => void
|
||||
handleExportPipeline: (include?: boolean) => void
|
||||
detectIsUsedByApp: () => void
|
||||
}
|
||||
|
||||
const OperationsPopover = ({
|
||||
dataset,
|
||||
isCurrentWorkspaceDatasetOperator,
|
||||
openRenameModal,
|
||||
handleExportPipeline,
|
||||
detectIsUsedByApp,
|
||||
}: OperationsPopoverProps) => (
|
||||
<div className="absolute right-2 top-2 z-[15] hidden group-hover:block">
|
||||
<CustomPopover
|
||||
htmlContent={(
|
||||
<Operations
|
||||
showDelete={!isCurrentWorkspaceDatasetOperator}
|
||||
showExportPipeline={dataset.runtime_mode === 'rag_pipeline'}
|
||||
openRenameModal={openRenameModal}
|
||||
handleExportPipeline={handleExportPipeline}
|
||||
detectIsUsedByApp={detectIsUsedByApp}
|
||||
/>
|
||||
)}
|
||||
className="z-20 min-w-[186px]"
|
||||
popupClassName="rounded-xl bg-none shadow-none ring-0 min-w-[186px]"
|
||||
position="br"
|
||||
trigger="click"
|
||||
btnElement={(
|
||||
<div className="flex size-8 items-center justify-center rounded-[10px] hover:bg-state-base-hover">
|
||||
<RiMoreFill className="h-5 w-5 text-text-tertiary" />
|
||||
</div>
|
||||
)}
|
||||
btnClassName={open =>
|
||||
cn(
|
||||
'size-9 cursor-pointer justify-center rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0 shadow-lg shadow-shadow-shadow-5 ring-[2px] ring-inset ring-components-actionbar-bg hover:border-components-actionbar-border',
|
||||
open ? 'border-components-actionbar-border bg-state-base-hover' : '',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default React.memo(OperationsPopover)
|
||||
@ -0,0 +1,55 @@
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import * as React from 'react'
|
||||
import TagSelector from '@/app/components/base/tag-management/selector'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type TagAreaProps = {
|
||||
dataset: DataSet
|
||||
tags: Tag[]
|
||||
setTags: (tags: Tag[]) => void
|
||||
onSuccess?: () => void
|
||||
isHoveringTagSelector: boolean
|
||||
onClick: (e: React.MouseEvent) => void
|
||||
}
|
||||
|
||||
const TagArea = React.forwardRef<HTMLDivElement, TagAreaProps>(({
|
||||
dataset,
|
||||
tags,
|
||||
setTags,
|
||||
onSuccess,
|
||||
isHoveringTagSelector,
|
||||
onClick,
|
||||
}, ref) => (
|
||||
<div
|
||||
className={cn('relative w-full px-3', !dataset.embedding_available && 'opacity-30')}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'invisible w-full group-hover:visible',
|
||||
tags.length > 0 && 'visible',
|
||||
)}
|
||||
>
|
||||
<TagSelector
|
||||
position="bl"
|
||||
type="knowledge"
|
||||
targetID={dataset.id}
|
||||
value={tags.map(tag => tag.id)}
|
||||
selectedTags={tags}
|
||||
onCacheUpdate={setTags}
|
||||
onChange={onSuccess}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-0 top-0 z-[5] h-full w-20 bg-tag-selector-mask-bg group-hover:bg-tag-selector-mask-hover-bg',
|
||||
isHoveringTagSelector && 'hidden',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
TagArea.displayName = 'TagArea'
|
||||
|
||||
export default TagArea
|
||||
@ -0,0 +1,138 @@
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useCheckDatasetUsage, useDeleteDataset } from '@/service/use-dataset-card'
|
||||
import { useExportPipelineDSL } from '@/service/use-pipeline'
|
||||
|
||||
type ModalState = {
|
||||
showRenameModal: boolean
|
||||
showConfirmDelete: boolean
|
||||
confirmMessage: string
|
||||
}
|
||||
|
||||
type UseDatasetCardStateOptions = {
|
||||
dataset: DataSet
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateOptions) => {
|
||||
const { t } = useTranslation()
|
||||
const [tags, setTags] = useState<Tag[]>(dataset.tags)
|
||||
|
||||
useEffect(() => {
|
||||
setTags(dataset.tags)
|
||||
}, [dataset.tags])
|
||||
|
||||
// Modal state
|
||||
const [modalState, setModalState] = useState<ModalState>({
|
||||
showRenameModal: false,
|
||||
showConfirmDelete: false,
|
||||
confirmMessage: '',
|
||||
})
|
||||
|
||||
// Export state
|
||||
const [exporting, setExporting] = useState(false)
|
||||
|
||||
// Modal handlers
|
||||
const openRenameModal = useCallback(() => {
|
||||
setModalState(prev => ({ ...prev, showRenameModal: true }))
|
||||
}, [])
|
||||
|
||||
const closeRenameModal = useCallback(() => {
|
||||
setModalState(prev => ({ ...prev, showRenameModal: false }))
|
||||
}, [])
|
||||
|
||||
const closeConfirmDelete = useCallback(() => {
|
||||
setModalState(prev => ({ ...prev, showConfirmDelete: false }))
|
||||
}, [])
|
||||
|
||||
// API mutations
|
||||
const { mutateAsync: checkUsage } = useCheckDatasetUsage()
|
||||
const { mutateAsync: deleteDatasetMutation } = useDeleteDataset()
|
||||
const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL()
|
||||
|
||||
// Export pipeline handler
|
||||
const handleExportPipeline = useCallback(async (include: boolean = false) => {
|
||||
const { pipeline_id, name } = dataset
|
||||
if (!pipeline_id || exporting)
|
||||
return
|
||||
|
||||
try {
|
||||
setExporting(true)
|
||||
const { data } = await exportPipelineConfig({
|
||||
pipelineId: pipeline_id,
|
||||
include,
|
||||
})
|
||||
const a = document.createElement('a')
|
||||
const file = new Blob([data], { type: 'application/yaml' })
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${name}.pipeline`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||
}
|
||||
finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}, [dataset, exportPipelineConfig, exporting, t])
|
||||
|
||||
// Delete flow handlers
|
||||
const detectIsUsedByApp = useCallback(async () => {
|
||||
try {
|
||||
const { is_using: isUsedByApp } = await checkUsage(dataset.id)
|
||||
const message = isUsedByApp
|
||||
? t('datasetUsedByApp', { ns: 'dataset' })!
|
||||
: t('deleteDatasetConfirmContent', { ns: 'dataset' })!
|
||||
setModalState(prev => ({
|
||||
...prev,
|
||||
confirmMessage: message,
|
||||
showConfirmDelete: true,
|
||||
}))
|
||||
}
|
||||
catch (e: unknown) {
|
||||
if (e instanceof Response) {
|
||||
const res = await e.json()
|
||||
Toast.notify({ type: 'error', message: res?.message || 'Unknown error' })
|
||||
}
|
||||
else {
|
||||
Toast.notify({ type: 'error', message: (e as Error)?.message || 'Unknown error' })
|
||||
}
|
||||
}
|
||||
}, [dataset.id, checkUsage, t])
|
||||
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
try {
|
||||
await deleteDatasetMutation(dataset.id)
|
||||
Toast.notify({ type: 'success', message: t('datasetDeleted', { ns: 'dataset' }) })
|
||||
onSuccess?.()
|
||||
}
|
||||
finally {
|
||||
closeConfirmDelete()
|
||||
}
|
||||
}, [dataset.id, deleteDatasetMutation, onSuccess, t, closeConfirmDelete])
|
||||
|
||||
return {
|
||||
// Tag state
|
||||
tags,
|
||||
setTags,
|
||||
|
||||
// Modal state
|
||||
modalState,
|
||||
openRenameModal,
|
||||
closeRenameModal,
|
||||
closeConfirmDelete,
|
||||
|
||||
// Export state
|
||||
exporting,
|
||||
|
||||
// Handlers
|
||||
handleExportPipeline,
|
||||
detectIsUsedByApp,
|
||||
onConfirmDelete,
|
||||
}
|
||||
}
|
||||
@ -1,28 +1,17 @@
|
||||
'use client'
|
||||
import type { Tag } from '@/app/components/base/tag-management/constant'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { RiFileTextFill, RiMoreFill, RiRobot2Fill } from '@remixicon/react'
|
||||
import { useHover } from 'ahooks'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import CornerLabel from '@/app/components/base/corner-label'
|
||||
import CustomPopover from '@/app/components/base/popover'
|
||||
import TagSelector from '@/app/components/base/tag-management/selector'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { useKnowledge } from '@/hooks/use-knowledge'
|
||||
import { DOC_FORM_ICON_WITH_BG, DOC_FORM_TEXT } from '@/models/datasets'
|
||||
import { checkIsUsedInApp, deleteDataset } from '@/service/datasets'
|
||||
import { useExportPipelineDSL } from '@/service/use-pipeline'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import RenameDatasetModal from '../../rename-modal'
|
||||
import Operations from './operations'
|
||||
import CornerLabels from './components/corner-labels'
|
||||
import DatasetCardFooter from './components/dataset-card-footer'
|
||||
import DatasetCardHeader from './components/dataset-card-header'
|
||||
import DatasetCardModals from './components/dataset-card-modals'
|
||||
import Description from './components/description'
|
||||
import OperationsPopover from './components/operations-popover'
|
||||
import TagArea from './components/tag-area'
|
||||
import { useDatasetCardState } from './hooks/use-dataset-card-state'
|
||||
|
||||
const EXTERNAL_PROVIDER = 'external'
|
||||
|
||||
@ -35,320 +24,80 @@ const DatasetCard = ({
|
||||
dataset,
|
||||
onSuccess,
|
||||
}: DatasetCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { push } = useRouter()
|
||||
|
||||
const isCurrentWorkspaceDatasetOperator = useAppContextWithSelector(state => state.isCurrentWorkspaceDatasetOperator)
|
||||
const [tags, setTags] = useState<Tag[]>(dataset.tags)
|
||||
const tagSelectorRef = useRef<HTMLDivElement>(null)
|
||||
const isHoveringTagSelector = useHover(tagSelectorRef)
|
||||
|
||||
const [showRenameModal, setShowRenameModal] = useState(false)
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const [confirmMessage, setConfirmMessage] = useState<string>('')
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const {
|
||||
tags,
|
||||
setTags,
|
||||
modalState,
|
||||
openRenameModal,
|
||||
closeRenameModal,
|
||||
closeConfirmDelete,
|
||||
handleExportPipeline,
|
||||
detectIsUsedByApp,
|
||||
onConfirmDelete,
|
||||
} = useDatasetCardState({ dataset, onSuccess })
|
||||
|
||||
const isExternalProvider = useMemo(() => {
|
||||
return dataset.provider === EXTERNAL_PROVIDER
|
||||
}, [dataset.provider])
|
||||
const isExternalProvider = dataset.provider === EXTERNAL_PROVIDER
|
||||
const isPipelineUnpublished = useMemo(() => {
|
||||
return dataset.runtime_mode === 'rag_pipeline' && !dataset.is_published
|
||||
}, [dataset.runtime_mode, dataset.is_published])
|
||||
const isShowChunkingModeIcon = useMemo(() => {
|
||||
return dataset.doc_form && (dataset.runtime_mode !== 'rag_pipeline' || dataset.is_published)
|
||||
}, [dataset.doc_form, dataset.runtime_mode, dataset.is_published])
|
||||
const isShowDocModeInfo = useMemo(() => {
|
||||
return dataset.doc_form && dataset.indexing_technique && dataset.retrieval_model_dict?.search_method && (dataset.runtime_mode !== 'rag_pipeline' || dataset.is_published)
|
||||
}, [dataset.doc_form, dataset.indexing_technique, dataset.retrieval_model_dict?.search_method, dataset.runtime_mode, dataset.is_published])
|
||||
|
||||
const chunkingModeIcon = dataset.doc_form ? DOC_FORM_ICON_WITH_BG[dataset.doc_form] : React.Fragment
|
||||
const Icon = isExternalProvider ? DOC_FORM_ICON_WITH_BG.external : chunkingModeIcon
|
||||
const iconInfo = dataset.icon_info || {
|
||||
icon: '📙',
|
||||
icon_type: 'emoji',
|
||||
icon_background: '#FFF4ED',
|
||||
icon_url: '',
|
||||
const handleCardClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
if (isExternalProvider)
|
||||
push(`/datasets/${dataset.id}/hitTesting`)
|
||||
else if (isPipelineUnpublished)
|
||||
push(`/datasets/${dataset.id}/pipeline`)
|
||||
else
|
||||
push(`/datasets/${dataset.id}/documents`)
|
||||
}
|
||||
const { formatIndexingTechniqueAndMethod } = useKnowledge()
|
||||
const documentCount = useMemo(() => {
|
||||
const availableDocCount = dataset.total_available_documents ?? 0
|
||||
if (availableDocCount === dataset.document_count)
|
||||
return `${dataset.document_count}`
|
||||
if (availableDocCount < dataset.document_count)
|
||||
return `${availableDocCount} / ${dataset.document_count}`
|
||||
}, [dataset.document_count, dataset.total_available_documents])
|
||||
const documentCountTooltip = useMemo(() => {
|
||||
const availableDocCount = dataset.total_available_documents ?? 0
|
||||
if (availableDocCount === dataset.document_count)
|
||||
return t('docAllEnabled', { ns: 'dataset', count: availableDocCount })
|
||||
if (availableDocCount < dataset.document_count)
|
||||
return t('partialEnabled', { ns: 'dataset', count: dataset.document_count, num: availableDocCount })
|
||||
}, [t, dataset.document_count, dataset.total_available_documents])
|
||||
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
const editTimeText = useMemo(() => {
|
||||
return `${t('segment.editedAt', { ns: 'datasetDocuments' })} ${formatTimeFromNow(dataset.updated_at * 1000)}`
|
||||
}, [t, dataset.updated_at, formatTimeFromNow])
|
||||
|
||||
const openRenameModal = useCallback(() => {
|
||||
setShowRenameModal(true)
|
||||
}, [])
|
||||
|
||||
const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL()
|
||||
|
||||
const handleExportPipeline = useCallback(async (include = false) => {
|
||||
const { pipeline_id, name } = dataset
|
||||
if (!pipeline_id)
|
||||
return
|
||||
|
||||
if (exporting)
|
||||
return
|
||||
|
||||
try {
|
||||
setExporting(true)
|
||||
const { data } = await exportPipelineConfig({
|
||||
pipelineId: pipeline_id,
|
||||
include,
|
||||
})
|
||||
const a = document.createElement('a')
|
||||
const file = new Blob([data], { type: 'application/yaml' })
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${name}.pipeline`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
catch {
|
||||
Toast.notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
|
||||
}
|
||||
finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}, [dataset, exportPipelineConfig, exporting, t])
|
||||
|
||||
const detectIsUsedByApp = useCallback(async () => {
|
||||
try {
|
||||
const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
|
||||
setConfirmMessage(isUsedByApp ? t('datasetUsedByApp', { ns: 'dataset' })! : t('deleteDatasetConfirmContent', { ns: 'dataset' })!)
|
||||
setShowConfirmDelete(true)
|
||||
}
|
||||
catch (e: any) {
|
||||
const res = await e.json()
|
||||
Toast.notify({ type: 'error', message: res?.message || 'Unknown error' })
|
||||
}
|
||||
}, [dataset.id, t])
|
||||
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
try {
|
||||
await deleteDataset(dataset.id)
|
||||
Toast.notify({ type: 'success', message: t('datasetDeleted', { ns: 'dataset' }) })
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
}
|
||||
finally {
|
||||
setShowConfirmDelete(false)
|
||||
}
|
||||
}, [dataset.id, onSuccess, t])
|
||||
|
||||
useEffect(() => {
|
||||
setTags(dataset.tags)
|
||||
}, [dataset])
|
||||
const handleTagAreaClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="group relative col-span-1 flex h-[190px] cursor-pointer flex-col rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-xs shadow-shadow-shadow-3 transition-all duration-200 ease-in-out hover:bg-components-card-bg-alt hover:shadow-md hover:shadow-shadow-shadow-5"
|
||||
data-disable-nprogress={true}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
if (isExternalProvider)
|
||||
push(`/datasets/${dataset.id}/hitTesting`)
|
||||
else if (isPipelineUnpublished)
|
||||
push(`/datasets/${dataset.id}/pipeline`)
|
||||
else
|
||||
push(`/datasets/${dataset.id}/documents`)
|
||||
}}
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
{!dataset.embedding_available && (
|
||||
<CornerLabel
|
||||
label={t('cornerLabel.unavailable', { ns: 'dataset' })}
|
||||
className="absolute right-0 top-0 z-10"
|
||||
labelClassName="rounded-tr-xl"
|
||||
/>
|
||||
)}
|
||||
{dataset.embedding_available && dataset.runtime_mode === 'rag_pipeline' && (
|
||||
<CornerLabel
|
||||
label={t('cornerLabel.pipeline', { ns: 'dataset' })}
|
||||
className="absolute right-0 top-0 z-10"
|
||||
labelClassName="rounded-tr-xl"
|
||||
/>
|
||||
)}
|
||||
<div className={cn('flex items-center gap-x-3 px-4 pb-2 pt-4', !dataset.embedding_available && 'opacity-30')}>
|
||||
<div className="relative shrink-0">
|
||||
<AppIcon
|
||||
size="large"
|
||||
iconType={iconInfo.icon_type}
|
||||
icon={iconInfo.icon}
|
||||
background={iconInfo.icon_type === 'image' ? undefined : iconInfo.icon_background}
|
||||
imageUrl={iconInfo.icon_type === 'image' ? iconInfo.icon_url : undefined}
|
||||
/>
|
||||
{(isShowChunkingModeIcon || isExternalProvider) && (
|
||||
<div className="absolute -bottom-1 -right-1 z-[5]">
|
||||
<Icon className="size-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex grow flex-col gap-y-1 overflow-hidden py-px">
|
||||
<div
|
||||
className="system-md-semibold truncate text-text-secondary"
|
||||
title={dataset.name}
|
||||
>
|
||||
{dataset.name}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[10px] font-medium leading-[18px] text-text-tertiary">
|
||||
<div className="truncate" title={dataset.author_name}>{dataset.author_name}</div>
|
||||
<div>·</div>
|
||||
<div className="truncate" title={editTimeText}>{editTimeText}</div>
|
||||
</div>
|
||||
<div className="system-2xs-medium-uppercase flex items-center gap-x-3 text-text-tertiary">
|
||||
{isExternalProvider && <span>{t('externalKnowledgeBase', { ns: 'dataset' })}</span>}
|
||||
{!isExternalProvider && isShowDocModeInfo && (
|
||||
<>
|
||||
{dataset.doc_form && (
|
||||
<span
|
||||
className="min-w-0 max-w-full truncate"
|
||||
title={t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })}
|
||||
>
|
||||
{t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })}
|
||||
</span>
|
||||
)}
|
||||
{dataset.indexing_technique && (
|
||||
<span
|
||||
className="min-w-0 max-w-full truncate"
|
||||
title={formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method) as any}
|
||||
>
|
||||
{formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method) as any}
|
||||
</span>
|
||||
)}
|
||||
{dataset.is_multimodal && (
|
||||
<span
|
||||
className="min-w-0 max-w-full truncate"
|
||||
title={t('multimodal', { ns: 'dataset' })}
|
||||
>
|
||||
{t('multimodal', { ns: 'dataset' })}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn('system-xs-regular line-clamp-2 h-10 px-4 py-1 text-text-tertiary', !dataset.embedding_available && 'opacity-30')}
|
||||
title={dataset.description}
|
||||
>
|
||||
{dataset.description}
|
||||
</div>
|
||||
<div
|
||||
className={cn('relative w-full px-3', !dataset.embedding_available && 'opacity-30')}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={tagSelectorRef}
|
||||
className={cn(
|
||||
'invisible w-full group-hover:visible',
|
||||
tags.length > 0 && 'visible',
|
||||
)}
|
||||
>
|
||||
<TagSelector
|
||||
position="bl"
|
||||
type="knowledge"
|
||||
targetID={dataset.id}
|
||||
value={tags.map(tag => tag.id)}
|
||||
selectedTags={tags}
|
||||
onCacheUpdate={setTags}
|
||||
onChange={onSuccess}
|
||||
/>
|
||||
</div>
|
||||
{/* Tag Mask */}
|
||||
<div
|
||||
className={cn(
|
||||
'absolute right-0 top-0 z-[5] h-full w-20 bg-tag-selector-mask-bg group-hover:bg-tag-selector-mask-hover-bg',
|
||||
isHoveringTagSelector && 'hidden',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-x-3 px-4 pb-3 pt-2 text-text-tertiary',
|
||||
!dataset.embedding_available && 'opacity-30',
|
||||
)}
|
||||
>
|
||||
<Tooltip popupContent={documentCountTooltip}>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<RiFileTextFill className="size-3 text-text-quaternary" />
|
||||
<span className="system-xs-medium">{documentCount}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{!isExternalProvider && (
|
||||
<Tooltip popupContent={`${dataset.app_count} ${t('appCount', { ns: 'dataset' })}`}>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<RiRobot2Fill className="size-3 text-text-quaternary" />
|
||||
<span className="system-xs-medium">{dataset.app_count}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
<span className="system-xs-regular text-divider-deep">/</span>
|
||||
<span className="system-xs-regular">{`${t('updated', { ns: 'dataset' })} ${formatTimeFromNow(dataset.updated_at * 1000)}`}</span>
|
||||
</div>
|
||||
<div className="absolute right-2 top-2 z-[15] hidden group-hover:block">
|
||||
<CustomPopover
|
||||
htmlContent={(
|
||||
<Operations
|
||||
showDelete={!isCurrentWorkspaceDatasetOperator}
|
||||
showExportPipeline={dataset.runtime_mode === 'rag_pipeline'}
|
||||
openRenameModal={openRenameModal}
|
||||
handleExportPipeline={handleExportPipeline}
|
||||
detectIsUsedByApp={detectIsUsedByApp}
|
||||
/>
|
||||
)}
|
||||
className="z-20 min-w-[186px]"
|
||||
popupClassName="rounded-xl bg-none shadow-none ring-0 min-w-[186px]"
|
||||
position="br"
|
||||
trigger="click"
|
||||
btnElement={(
|
||||
<div className="flex size-8 items-center justify-center rounded-[10px] hover:bg-state-base-hover">
|
||||
<RiMoreFill className="h-5 w-5 text-text-tertiary" />
|
||||
</div>
|
||||
)}
|
||||
btnClassName={open =>
|
||||
cn(
|
||||
'size-9 cursor-pointer justify-center rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0 shadow-lg shadow-shadow-shadow-5 ring-[2px] ring-inset ring-components-actionbar-bg hover:border-components-actionbar-border',
|
||||
open ? 'border-components-actionbar-border bg-state-base-hover' : '',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{showRenameModal && (
|
||||
<RenameDatasetModal
|
||||
show={showRenameModal}
|
||||
<CornerLabels dataset={dataset} />
|
||||
<DatasetCardHeader dataset={dataset} />
|
||||
<Description dataset={dataset} />
|
||||
<TagArea
|
||||
ref={tagSelectorRef}
|
||||
dataset={dataset}
|
||||
onClose={() => setShowRenameModal(false)}
|
||||
tags={tags}
|
||||
setTags={setTags}
|
||||
onSuccess={onSuccess}
|
||||
isHoveringTagSelector={isHoveringTagSelector}
|
||||
onClick={handleTagAreaClick}
|
||||
/>
|
||||
)}
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('deleteDatasetConfirmTitle', { ns: 'dataset' })}
|
||||
content={confirmMessage}
|
||||
isShow={showConfirmDelete}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
<DatasetCardFooter dataset={dataset} />
|
||||
<OperationsPopover
|
||||
dataset={dataset}
|
||||
isCurrentWorkspaceDatasetOperator={isCurrentWorkspaceDatasetOperator}
|
||||
openRenameModal={openRenameModal}
|
||||
handleExportPipeline={handleExportPipeline}
|
||||
detectIsUsedByApp={detectIsUsedByApp}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<DatasetCardModals
|
||||
dataset={dataset}
|
||||
modalState={modalState}
|
||||
onCloseRename={closeRenameModal}
|
||||
onCloseConfirm={closeConfirmDelete}
|
||||
onConfirmDelete={onConfirmDelete}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
18
web/service/use-dataset-card.ts
Normal file
18
web/service/use-dataset-card.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { checkIsUsedInApp, deleteDataset } from './datasets'
|
||||
|
||||
const NAME_SPACE = 'dataset-card'
|
||||
|
||||
export const useCheckDatasetUsage = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'check-usage'],
|
||||
mutationFn: (datasetId: string) => checkIsUsedInApp(datasetId),
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeleteDataset = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'delete'],
|
||||
mutationFn: (datasetId: string) => deleteDataset(datasetId),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user