feat: new Dataset list

This commit is contained in:
twwu
2025-04-30 14:16:13 +08:00
parent 2613a380b6
commit a7f9259e27
37 changed files with 3076 additions and 237 deletions

View File

@ -0,0 +1,146 @@
'use client'
// Libraries
import { useEffect, useMemo, useRef, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { useBoolean, useDebounceFn } from 'ahooks'
import { useQuery } from '@tanstack/react-query'
// Components
import ExternalAPIPanel from '../external-api/external-api-panel'
import Datasets from './datasets'
import DatasetFooter from './dataset-footer'
import ApiServer from '../../develop/ApiServer'
import Doc from './doc'
import SegmentedControl from '@/app/components/base/segmented-control'
import { RiBook2Line, RiTerminalBoxLine } from '@remixicon/react'
import TagManagementModal from '@/app/components/base/tag-management'
import TagFilter from '@/app/components/base/tag-management/filter'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label'
// Services
import { fetchDatasetApiBaseUrl } from '@/service/datasets'
// Hooks
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import { useAppContext } from '@/context/app-context'
import { useExternalApiPanel } from '@/context/external-api-panel-context'
const Container = () => {
const { t } = useTranslation()
const router = useRouter()
const { currentWorkspace, isCurrentWorkspaceOwner } = useAppContext()
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
const { showExternalApiPanel, setShowExternalApiPanel } = useExternalApiPanel()
const [includeAll, { toggle: toggleIncludeAll }] = useBoolean(false)
document.title = `${t('dataset.knowledge')} - Dify`
const options = useMemo(() => {
return [
{ value: 'dataset', text: t('dataset.datasets'), Icon: RiBook2Line },
...(currentWorkspace.role === 'dataset_operator' ? [] : [{
value: 'api', text: t('dataset.datasetsApi'), Icon: RiTerminalBoxLine,
}]),
]
}, [currentWorkspace.role, t])
const [activeTab, setActiveTab] = useTabSearchParams({
defaultTab: 'dataset',
})
const containerRef = useRef<HTMLDivElement>(null)
const { data } = useQuery(
{
queryKey: ['datasetApiBaseInfo'],
queryFn: () => fetchDatasetApiBaseUrl('/datasets/api-base-info'),
enabled: activeTab !== 'dataset',
},
)
const [keywords, setKeywords] = useState('')
const [searchKeywords, setSearchKeywords] = useState('')
const { run: handleSearch } = useDebounceFn(() => {
setSearchKeywords(keywords)
}, { wait: 500 })
const handleKeywordsChange = (value: string) => {
setKeywords(value)
handleSearch()
}
const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
const [tagIDs, setTagIDs] = useState<string[]>([])
const { run: handleTagsUpdate } = useDebounceFn(() => {
setTagIDs(tagFilterValue)
}, { wait: 500 })
const handleTagsChange = (value: string[]) => {
setTagFilterValue(value)
handleTagsUpdate()
}
useEffect(() => {
if (currentWorkspace.role === 'normal')
return router.replace('/apps')
}, [currentWorkspace, router])
return (
<div ref={containerRef} className='scroll-container relative flex grow flex-col overflow-y-auto bg-background-body'>
<div className='sticky top-0 z-10 flex items-center justify-between gap-x-1 bg-background-body px-12 pb-2 pt-4'>
<SegmentedControl
value={activeTab}
onChange={newActiveTab => setActiveTab(newActiveTab as string)}
options={options}
size='large'
activeClassName='text-text-primary'
/>
{activeTab === 'dataset' && (
<div className='flex items-center justify-center gap-2'>
{isCurrentWorkspaceOwner && <CheckboxWithLabel
isChecked={includeAll}
onChange={toggleIncludeAll}
label={t('dataset.allKnowledge')}
labelClassName='system-md-regular text-text-secondary'
className='mr-2'
tooltip={t('dataset.allKnowledgeDescription') as string}
/>}
<TagFilter type='knowledge' value={tagFilterValue} onChange={handleTagsChange} />
<Input
showLeftIcon
showClearIcon
wrapperClassName='w-[200px]'
value={keywords}
onChange={e => handleKeywordsChange(e.target.value)}
onClear={() => handleKeywordsChange('')}
/>
<div className="h-4 w-[1px] bg-divider-regular" />
<Button
className='shadows-shadow-xs gap-0.5'
onClick={() => setShowExternalApiPanel(true)}
>
<ApiConnectionMod className='h-4 w-4 text-components-button-secondary-text' />
<div className='system-sm-medium flex items-center justify-center gap-1 px-0.5 text-components-button-secondary-text'>{t('dataset.externalAPIPanelTitle')}</div>
</Button>
</div>
)}
{activeTab === 'api' && data && <ApiServer apiBaseUrl={data.api_base_url || ''} />}
</div>
{activeTab === 'dataset' && (
<>
<Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} />
<DatasetFooter />
{showTagManagementModal && (
<TagManagementModal type='knowledge' show={showTagManagementModal} />
)}
</>
)}
{activeTab === 'api' && data && <Doc apiBaseUrl={data.api_base_url || ''} />}
{showExternalApiPanel && <ExternalAPIPanel onClose={() => setShowExternalApiPanel(false)} />}
</div>
)
}
export default Container

View File

@ -0,0 +1,240 @@
'use client'
import { useContext } from 'use-context-selector'
import { useRouter } from 'next/navigation'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiMoreFill } from '@remixicon/react'
import cn from '@/utils/classnames'
import Confirm from '@/app/components/base/confirm'
import { ToastContext } from '@/app/components/base/toast'
import { checkIsUsedInApp, deleteDataset } from '@/service/datasets'
import type { DataSet } from '@/models/datasets'
import Tooltip from '@/app/components/base/tooltip'
import { Folder } from '@/app/components/base/icons/src/vender/solid/files'
import type { HtmlContentProps } from '@/app/components/base/popover'
import CustomPopover from '@/app/components/base/popover'
import Divider from '@/app/components/base/divider'
import RenameDatasetModal from '@/app/components/datasets/rename-modal'
import type { Tag } from '@/app/components/base/tag-management/constant'
import TagSelector from '@/app/components/base/tag-management/selector'
import CornerLabel from '@/app/components/base/corner-label'
import { useAppContext } from '@/context/app-context'
export type DatasetCardProps = {
dataset: DataSet
onSuccess?: () => void
}
const DatasetCard = ({
dataset,
onSuccess,
}: DatasetCardProps) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const { push } = useRouter()
const EXTERNAL_PROVIDER = 'external' as const
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
const [tags, setTags] = useState<Tag[]>(dataset.tags)
const [showRenameModal, setShowRenameModal] = useState(false)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [confirmMessage, setConfirmMessage] = useState<string>('')
const isExternalProvider = (provider: string): boolean => provider === EXTERNAL_PROVIDER
const detectIsUsedByApp = useCallback(async () => {
try {
const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
setConfirmMessage(isUsedByApp ? t('dataset.datasetUsedByApp')! : t('dataset.deleteDatasetConfirmContent')!)
}
catch (e: any) {
const res = await e.json()
notify({ type: 'error', message: res?.message || 'Unknown error' })
}
setShowConfirmDelete(true)
}, [dataset.id, notify, t])
const onConfirmDelete = useCallback(async () => {
try {
await deleteDataset(dataset.id)
notify({ type: 'success', message: t('dataset.datasetDeleted') })
if (onSuccess)
onSuccess()
}
catch {
}
setShowConfirmDelete(false)
}, [dataset.id, notify, onSuccess, t])
const Operations = (props: HtmlContentProps & { showDelete: boolean }) => {
const onMouseLeave = async () => {
props.onClose?.()
}
const onClickRename = async (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
props.onClick?.()
e.preventDefault()
setShowRenameModal(true)
}
const onClickDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
props.onClick?.()
e.preventDefault()
detectIsUsedByApp()
}
return (
<div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
<div className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover' onClick={onClickRename}>
<span className='text-sm text-text-secondary'>{t('common.operation.settings')}</span>
</div>
{props.showDelete && (
<>
<Divider className="!my-1" />
<div
className='group mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-destructive-hover'
onClick={onClickDelete}
>
<span className={cn('text-sm text-text-secondary', 'group-hover:text-text-destructive')}>
{t('common.operation.delete')}
</span>
</div>
</>
)}
</div>
)
}
useEffect(() => {
setTags(dataset.tags)
}, [dataset])
return (
<>
<div
className='group relative col-span-1 flex min-h-[160px] cursor-pointer flex-col rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg'
data-disable-nprogress={true}
onClick={(e) => {
e.preventDefault()
isExternalProvider(dataset.provider)
? push(`/datasets/${dataset.id}/hitTesting`)
: push(`/datasets/${dataset.id}/documents`)
}}
>
{isExternalProvider(dataset.provider) && <CornerLabel label='External' className='absolute right-0' labelClassName='rounded-tr-xl' />}
<div className='flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pb-3 pt-[14px]'>
<div className={cn(
'flex shrink-0 items-center justify-center rounded-md border-[0.5px] border-[#E0EAFF] bg-[#F5F8FF] p-2.5',
!dataset.embedding_available && 'opacity-50 hover:opacity-100',
)}>
<Folder className='h-5 w-5 text-[#444CE7]' />
</div>
<div className='w-0 grow py-[1px]'>
<div className='flex items-center text-sm font-semibold leading-5 text-text-secondary'>
<div className={cn('truncate', !dataset.embedding_available && 'text-text-tertiary opacity-50 hover:opacity-100')} title={dataset.name}>{dataset.name}</div>
{!dataset.embedding_available && (
<Tooltip
popupContent={t('dataset.unavailableTip')}
>
<span className='ml-1 inline-flex w-max shrink-0 rounded-md border border-divider-regular px-1 text-xs font-normal leading-[18px] text-text-tertiary'>{t('dataset.unavailable')}</span>
</Tooltip>
)}
</div>
<div className='mt-[1px] flex items-center text-xs leading-[18px] text-text-tertiary'>
<div
className={cn('truncate', (!dataset.embedding_available || !dataset.document_count) && 'opacity-50')}
title={dataset.provider === 'external' ? `${dataset.app_count}${t('dataset.appCount')}` : `${dataset.document_count}${t('dataset.documentCount')} · ${Math.round(dataset.word_count / 1000)}${t('dataset.wordCount')} · ${dataset.app_count}${t('dataset.appCount')}`}
>
{dataset.provider === 'external'
? <>
<span>{dataset.app_count}{t('dataset.appCount')}</span>
</>
: <>
<span>{dataset.document_count}{t('dataset.documentCount')}</span>
<span className='mx-0.5 w-1 shrink-0 text-text-tertiary'>·</span>
<span>{Math.round(dataset.word_count / 1000)}{t('dataset.wordCount')}</span>
<span className='mx-0.5 w-1 shrink-0 text-text-tertiary'>·</span>
<span>{dataset.app_count}{t('dataset.appCount')}</span>
</>
}
</div>
</div>
</div>
</div>
<div
className={cn(
'mb-2 max-h-[72px] grow px-[14px] text-xs leading-normal text-text-tertiary group-hover:line-clamp-2 group-hover:max-h-[36px]',
tags.length ? 'line-clamp-2' : 'line-clamp-4',
!dataset.embedding_available && 'opacity-50 hover:opacity-100',
)}
title={dataset.description}>
{dataset.description}
</div>
<div className={cn(
'mt-4 h-[42px] shrink-0 items-center pb-[6px] pl-[14px] pr-[6px] pt-1',
tags.length ? 'flex' : '!hidden group-hover:!flex',
)}>
<div className={cn('flex w-0 grow items-center gap-1', !dataset.embedding_available && 'opacity-50 hover:opacity-100')} onClick={(e) => {
e.stopPropagation()
e.preventDefault()
}}>
<div className={cn(
'mr-[41px] w-full grow group-hover:!mr-0 group-hover:!block',
tags.length ? '!block' : '!hidden',
)}>
<TagSelector
position='bl'
type='knowledge'
targetID={dataset.id}
value={tags.map(tag => tag.id)}
selectedTags={tags}
onCacheUpdate={setTags}
onChange={onSuccess}
/>
</div>
</div>
<div className='mx-1 !hidden h-[14px] w-[1px] shrink-0 bg-divider-regular group-hover:!flex' />
<div className='!hidden shrink-0 group-hover:!flex'>
<CustomPopover
htmlContent={<Operations showDelete={!isCurrentWorkspaceDatasetOperator} />}
position="br"
trigger="click"
btnElement={
<div
className='flex h-8 w-8 cursor-pointer items-center justify-center rounded-md'
>
<RiMoreFill className='h-4 w-4 text-text-secondary' />
</div>
}
btnClassName={open =>
cn(
open ? '!bg-black/5 !shadow-none' : '!bg-transparent',
'h-8 w-8 rounded-md border-none !p-2 hover:!bg-black/5',
)
}
className={'!z-20 h-fit !w-[128px]'}
/>
</div>
</div>
</div>
{showRenameModal && (
<RenameDatasetModal
show={showRenameModal}
dataset={dataset}
onClose={() => setShowRenameModal(false)}
onSuccess={onSuccess}
/>
)}
{showConfirmDelete && (
<Confirm
title={t('dataset.deleteDatasetConfirmTitle')}
content={confirmMessage}
isShow={showConfirmDelete}
onConfirm={onConfirmDelete}
onCancel={() => setShowConfirmDelete(false)}
/>
)}
</>
)
}
export default DatasetCard

View File

@ -0,0 +1,244 @@
'use client'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import type { DataSet } from '@/models/datasets'
import { ChunkingMode } from '@/models/datasets'
import { useAppContext } from '@/context/app-context'
import { General, Graph, ParentChild, Qa } from '@/app/components/base/icons/src/public/knowledge'
import { useKnowledge } from '@/hooks/use-knowledge'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { Tag } from '@/app/components/base/tag-management/constant'
import TagSelector from '@/app/components/base/tag-management/selector'
import cn from '@/utils/classnames'
import { useHover } from 'ahooks'
import { RiFileTextFill, RiMoreFill, RiRobot2Fill } from '@remixicon/react'
import Tooltip from '@/app/components/base/tooltip'
import { useGetLanguage } from '@/context/i18n'
import dayjs from 'dayjs'
import { checkIsUsedInApp, deleteDataset } from '@/service/datasets'
import RenameDatasetModal from '../../rename-modal'
import Confirm from '@/app/components/base/confirm'
import Toast from '@/app/components/base/toast'
import CustomPopover from '@/app/components/base/popover'
import Operations from './operations'
const EXTERNAL_PROVIDER = 'external'
const DOC_FORM_ICON: Record<ChunkingMode, React.ComponentType<{ className: string }>> = {
[ChunkingMode.text]: General,
[ChunkingMode.qa]: Qa,
[ChunkingMode.parentChild]: ParentChild,
[ChunkingMode.graph]: Graph,
}
const DOC_FORM_TEXT: Record<ChunkingMode, string> = {
[ChunkingMode.text]: 'general',
[ChunkingMode.qa]: 'qa',
[ChunkingMode.parentChild]: 'parentChild',
[ChunkingMode.graph]: 'graph',
}
export type DatasetCardProps = {
dataset: DataSet
onSuccess?: () => void
}
const DatasetCard = ({
dataset,
onSuccess,
}: DatasetCardProps) => {
const { t } = useTranslation()
const { push } = useRouter()
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
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 isExternalProvider = useMemo(() => {
return dataset.provider === EXTERNAL_PROVIDER
}, [dataset.provider])
const Icon = DOC_FORM_ICON[dataset.doc_form] || General
const { formatIndexingTechniqueAndMethod } = useKnowledge()
const documentCount = useMemo(() => {
const availableDocCount = dataset.available_document_count || dataset.document_count
if (availableDocCount === dataset.document_count)
return `${dataset.document_count}`
if (availableDocCount < dataset.document_count)
return `${availableDocCount} / ${dataset.document_count}`
}, [dataset.document_count, dataset.available_document_count])
const documentCountTooltip = useMemo(() => {
const availableDocCount = dataset.available_document_count || dataset.document_count
if (availableDocCount === dataset.document_count)
return t('dataset.docAllEnabled', { count: availableDocCount })
if (availableDocCount < dataset.document_count)
return t('dataset.docAllEnabled', { count: dataset.document_count, num: availableDocCount })
}, [t, dataset.document_count, dataset.available_document_count])
const language = useGetLanguage()
const formatTimeFromNow = useCallback((time: number) => {
return dayjs(time * 1_000).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow()
}, [language])
const detectIsUsedByApp = useCallback(async () => {
try {
const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
setConfirmMessage(isUsedByApp ? t('dataset.datasetUsedByApp')! : t('dataset.deleteDatasetConfirmContent')!)
}
catch (e: any) {
const res = await e.json()
Toast.notify({ type: 'error', message: res?.message || 'Unknown error' })
}
setShowConfirmDelete(true)
}, [dataset.id, t])
const onConfirmDelete = useCallback(async () => {
try {
await deleteDataset(dataset.id)
Toast.notify({ type: 'success', message: t('dataset.datasetDeleted') })
if (onSuccess)
onSuccess()
}
catch {
}
setShowConfirmDelete(false)
}, [dataset.id, onSuccess, t])
useEffect(() => {
setTags(dataset.tags)
}, [dataset])
return (
<>
<div
className={cn(
'group relative col-span-1 flex h-[166px] 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',
!dataset.embedding_available && 'opacity-30',
)}
data-disable-nprogress={true}
onClick={(e) => {
e.preventDefault()
isExternalProvider
? push(`/datasets/${dataset.id}/hitTesting`)
: push(`/datasets/${dataset.id}/documents`)
}}
>
<div className='flex items-center gap-x-3 px-4 pb-2 pt-4'>
<div className='relative flex size-10 shrink-0 items-center justify-center rounded-[10px] border-[0.5px] border-divider-regular bg-components-icon-bg-violet-soft'>
<span className='title-4xl-semi-bold'>📙</span>
<div className='absolute -bottom-1 -right-1 z-10'>
<Icon className='size-4' />
</div>
</div>
<div className='flex grow flex-col gap-y-1 py-px'>
<div className='system-md-semibold truncate text-text-secondary' title={dataset.name}>{dataset.name}</div>
<div className='system-2xs-medium-uppercase flex items-center gap-x-3 text-text-tertiary'>
<span>{t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`)}</span>
<span>{formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}</span>
</div>
</div>
</div>
<div className='system-xs-regular line-clamp-2 h-10 px-4 py-1 text-text-tertiary' title={dataset.description}>
{dataset.description}
</div>
<div
className='relative w-full px-3'
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
}}
>
<div
ref={tagSelectorRef}
className={'invisible w-full group-hover: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-10 h-full w-20 bg-tag-selector-mask-bg group-hover:bg-tag-selector-mask-hover-bg',
isHoveringTagSelector && 'hidden',
)}
/>
</div>
<div className='flex items-center gap-x-3 px-4 pb-3 pt-2 text-text-tertiary'>
<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('dataset.appCount')}`}>
<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('dataset.updated')} ${formatTimeFromNow(dataset.updated_at)}`}</span>
</div>
<div className='absolute right-2 top-2 hidden group-hover:block'>
<CustomPopover
htmlContent={
<Operations
showDelete={!isCurrentWorkspaceDatasetOperator}
openRenameModal={() => {
setShowRenameModal(true)
}}
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={
<RiMoreFill className='h-5 w-5 text-text-tertiary' />
}
btnClassName={open =>
cn(
'size-9 cursor-pointer justify-center rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0 p-0.5 shadow-lg shadow-shadow-shadow-5 ring-[2px] ring-inset ring-components-actionbar-bg hover:border-components-actionbar-border hover:bg-state-base-hover',
open ? 'border-components-actionbar-border bg-state-base-hover' : '',
)
}
/>
</div>
</div>
{showRenameModal && (
<RenameDatasetModal
show={showRenameModal}
dataset={dataset}
onClose={() => setShowRenameModal(false)}
onSuccess={onSuccess}
/>
)}
{showConfirmDelete && (
<Confirm
title={t('dataset.deleteDatasetConfirmTitle')}
content={confirmMessage}
isShow={showConfirmDelete}
onConfirm={onConfirmDelete}
onCancel={() => setShowConfirmDelete(false)}
/>
)}
</>
)
}
export default DatasetCard

View File

@ -0,0 +1,94 @@
import Divider from '@/app/components/base/divider'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiDeleteBinLine, RiEditLine, RiFileCopyLine } from '@remixicon/react'
type OperationsProps = {
showDelete: boolean
openRenameModal: () => void
detectIsUsedByApp: () => void
}
const Operations = ({
showDelete,
openRenameModal,
detectIsUsedByApp,
}: OperationsProps) => {
const { t } = useTranslation()
const onClickRename = async (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.preventDefault()
openRenameModal()
}
const onClickDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
e.preventDefault()
detectIsUsedByApp()
}
return (
<div className='relative flex w-full flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg shadow-shadow-shadow-5'>
<div className='flex flex-col p-1'>
<div
className='flex cursor-pointer items-center gap-x-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
onClick={onClickRename}
>
<RiEditLine className='size-4 text-text-tertiary' />
<span className='system-md-regular px-1 text-text-secondary'>
{t('common.operation.edit')}
</span>
</div>
<div
className='flex cursor-pointer items-center gap-x-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
onClick={() => { console.log('duplicate') }}
>
<RiFileCopyLine className='size-4 text-text-tertiary' />
<span className='system-md-regular px-1 text-text-secondary'>
{t('common.operation.duplicate')}
</span>
</div>
</div>
<Divider type='horizontal' className='my-0 bg-divider-subtle' />
<div className='flex flex-col p-1'>
<div
className='flex cursor-pointer items-center gap-x-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
onClick={() => { console.log('Export') }}
>
<RiEditLine className='size-4 text-text-tertiary' />
<span className='system-md-regular px-1 text-text-secondary'>
Export Solution
</span>
</div>
<div
className='flex cursor-pointer items-center gap-x-1 rounded-lg px-2 py-1.5 hover:bg-state-base-hover'
onClick={() => { console.log('Import') }}
>
<RiFileCopyLine className='size-4 text-text-tertiary' />
<span className='system-md-regular px-1 text-text-secondary'>
Import Solution
</span>
</div>
</div>
{showDelete && (
<>
<Divider type='horizontal' className='my-0 bg-divider-subtle' />
<div className='flex flex-col p-1'>
<div
className='group flex cursor-pointer items-center gap-x-1 rounded-lg px-2 py-1.5 hover:bg-state-destructive-hover'
onClick={onClickDelete}
>
<RiDeleteBinLine className='size-4 text-text-tertiary group-hover:text-text-destructive' />
<span className='system-md-regular px-1 text-text-secondary group-hover:text-text-destructive'>
{t('common.operation.delete')}
</span>
</div>
</div>
</>
)}
</div>
)
}
export default React.memo(Operations)

View File

@ -0,0 +1,20 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
const DatasetFooter = () => {
const { t } = useTranslation()
return (
<footer className='shrink-0 grow-0 px-12 py-6'>
<h3 className='text-gradient text-xl font-semibold leading-tight'>{t('dataset.didYouKnow')}</h3>
<p className='mt-1 text-sm font-normal leading-tight text-text-secondary'>
{t('dataset.intro1')}<span className='inline-flex items-center gap-1 text-text-accent'>{t('dataset.intro2')}</span>{t('dataset.intro3')}<br />
{t('dataset.intro4')}<span className='inline-flex items-center gap-1 text-text-accent'>{t('dataset.intro5')}</span>{t('dataset.intro6')}
</p>
</footer>
)
}
export default React.memo(DatasetFooter)

View File

@ -0,0 +1,96 @@
'use client'
import { useEffect, useMemo, useRef } from 'react'
import useSWRInfinite from 'swr/infinite'
import { debounce } from 'lodash-es'
import { useTranslation } from 'react-i18next'
import NewDatasetCard from './new-dataset-card'
import DatasetCard from './dataset-card'
import type { DataSetListResponse, FetchDatasetsParams } from '@/models/datasets'
import { fetchDatasets } from '@/service/datasets'
import { useAppContext } from '@/context/app-context'
const getKey = (
pageIndex: number,
previousPageData: DataSetListResponse,
tags: string[],
keyword: string,
includeAll: boolean,
) => {
if (!pageIndex || previousPageData.has_more) {
const params: FetchDatasetsParams = {
url: 'datasets',
params: {
page: pageIndex + 1,
limit: 30,
include_all: includeAll,
},
}
if (tags.length)
params.params.tag_ids = tags
if (keyword)
params.params.keyword = keyword
return params
}
return null
}
type Props = {
containerRef: React.RefObject<HTMLDivElement>
tags: string[]
keywords: string
includeAll: boolean
}
const Datasets = ({
containerRef,
tags,
keywords,
includeAll,
}: Props) => {
const { t } = useTranslation()
const { isCurrentWorkspaceEditor } = useAppContext()
const { data, isLoading, setSize, mutate } = useSWRInfinite(
(pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords, includeAll),
fetchDatasets,
{ revalidateFirstPage: false, revalidateAll: true },
)
const loadingStateRef = useRef(false)
const anchorRef = useRef<HTMLAnchorElement>(null)
useEffect(() => {
loadingStateRef.current = isLoading
document.title = `${t('dataset.knowledge')} - Dify`
}, [isLoading, t])
const onScroll = useMemo(() => {
return debounce(() => {
if (!loadingStateRef.current && containerRef.current && anchorRef.current) {
const { scrollTop, clientHeight } = containerRef.current
const anchorOffset = anchorRef.current.offsetTop
if (anchorOffset - scrollTop - clientHeight < 100)
setSize(size => size + 1)
}
}, 50)
}, [containerRef, setSize])
useEffect(() => {
const currentContainer = containerRef.current
currentContainer?.addEventListener('scroll', onScroll)
return () => {
currentContainer?.removeEventListener('scroll', onScroll)
onScroll.cancel()
}
}, [onScroll, containerRef])
return (
<nav className='grid shrink-0 grow grid-cols-1 content-start gap-3 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
{ isCurrentWorkspaceEditor && <NewDatasetCard ref={anchorRef} /> }
{data?.map(({ data: datasets }) => datasets.map(dataset => (
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={mutate} />),
))}
</nav>
)
}
export default Datasets

View File

@ -0,0 +1,131 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import { RiListUnordered } from '@remixicon/react'
import TemplateEn from './template/template.en.mdx'
import TemplateZh from './template/template.zh.mdx'
import TemplateJa from './template/template.ja.mdx'
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n/language'
import useTheme from '@/hooks/use-theme'
import { Theme } from '@/types/app'
import cn from '@/utils/classnames'
type DocProps = {
apiBaseUrl: string
}
const Doc = ({ apiBaseUrl }: DocProps) => {
const { locale } = useContext(I18n)
const { t } = useTranslation()
const [toc, setToc] = useState<Array<{ href: string; text: string }>>([])
const [isTocExpanded, setIsTocExpanded] = useState(false)
const { theme } = useTheme()
// Set initial TOC expanded state based on screen width
useEffect(() => {
const mediaQuery = window.matchMedia('(min-width: 1280px)')
setIsTocExpanded(mediaQuery.matches)
}, [])
// Extract TOC from article content
useEffect(() => {
const extractTOC = () => {
const article = document.querySelector('article')
if (article) {
const headings = article.querySelectorAll('h2')
const tocItems = Array.from(headings).map((heading) => {
const anchor = heading.querySelector('a')
if (anchor) {
return {
href: anchor.getAttribute('href') || '',
text: anchor.textContent || '',
}
}
return null
}).filter((item): item is { href: string; text: string } => item !== null)
setToc(tocItems)
}
}
setTimeout(extractTOC, 0)
}, [locale])
// Handle TOC item click
const handleTocClick = (e: React.MouseEvent<HTMLAnchorElement>, item: { href: string; text: string }) => {
e.preventDefault()
const targetId = item.href.replace('#', '')
const element = document.getElementById(targetId)
if (element) {
const scrollContainer = document.querySelector('.scroll-container')
if (scrollContainer) {
const headerOffset = -40
const elementTop = element.offsetTop - headerOffset
scrollContainer.scrollTo({
top: elementTop,
behavior: 'smooth',
})
}
}
}
const Template = useMemo(() => {
switch (locale) {
case LanguagesSupported[1]:
return <TemplateZh apiBaseUrl={apiBaseUrl} />
case LanguagesSupported[7]:
return <TemplateJa apiBaseUrl={apiBaseUrl} />
default:
return <TemplateEn apiBaseUrl={apiBaseUrl} />
}
}, [apiBaseUrl, locale])
return (
<div className="flex">
<div className={`fixed right-20 top-32 z-10 transition-all ${isTocExpanded ? 'w-64' : 'w-10'}`}>
{isTocExpanded
? (
<nav className="toc max-h-[calc(100vh-150px)] w-full overflow-y-auto rounded-lg bg-components-panel-bg p-4 shadow-md">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold text-text-primary">{t('appApi.develop.toc')}</h3>
<button
onClick={() => setIsTocExpanded(false)}
className="text-text-tertiary hover:text-text-secondary"
>
</button>
</div>
<ul className="space-y-2">
{toc.map((item, index) => (
<li key={index}>
<a
href={item.href}
className="text-text-secondary transition-colors duration-200 hover:text-text-primary hover:underline"
onClick={e => handleTocClick(e, item)}
>
{item.text}
</a>
</li>
))}
</ul>
</nav>
)
: (
<button
onClick={() => setIsTocExpanded(true)}
className="flex h-10 w-10 items-center justify-center rounded-full bg-components-button-secondary-bg shadow-md transition-colors duration-200 hover:bg-components-button-secondary-bg-hover"
>
<RiListUnordered className="h-6 w-6 text-components-button-secondary-text" />
</button>
)}
</div>
<article className={cn('prose-xl prose mx-1 rounded-t-xl bg-background-default px-4 pt-16 sm:mx-12', theme === Theme.dark && 'dark:prose-invert')}>
{Template}
</article>
</div>
)
}
export default Doc

View File

@ -0,0 +1,37 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { basePath } from '@/utils/var'
import {
RiAddLine,
RiFunctionAddLine,
} from '@remixicon/react'
import Link from './link'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
type CreateAppCardProps = {
ref: React.RefObject<HTMLAnchorElement>
}
const CreateAppCard = ({
ref,
..._
}: CreateAppCardProps) => {
const { t } = useTranslation()
return (
<div className='flex h-[166px] flex-col gap-y-0.5 rounded-xl bg-background-default-dimmed'>
<div className='flex grow flex-col items-center justify-center p-2'>
<Link href={`${basePath}/datasets/create-from-pipeline`} Icon={RiFunctionAddLine} text={t('dataset.createFromPipeline')} />
<Link ref={ref} href={`${basePath}/datasets/create`} Icon={RiAddLine} text={t('dataset.createDataset')} />
</div>
<div className='border-t-[0.5px] border-divider-subtle p-2'>
<Link href={`${basePath}/datasets/connect`} Icon={ApiConnectionMod} text={t('dataset.connectDataset')} />
</div>
</div>
)
}
CreateAppCard.displayName = 'CreateAppCard'
export default CreateAppCard

View File

@ -0,0 +1,28 @@
import React from 'react'
type LinkProps = {
Icon: React.ComponentType<{ className?: string }>
text: string
href: string
ref?: React.RefObject<HTMLAnchorElement>
}
const Link = ({
Icon,
text,
href,
ref,
}: LinkProps) => {
return (
<a
ref={ref}
className='flex w-full items-center gap-x-2 rounded-lg bg-transparent px-4 py-2 text-text-tertiary shadow-shadow-shadow-3 hover:bg-background-default-dodge hover:text-text-secondary hover:shadow-xs'
href={href}
>
<Icon className='h-4 w-4 shrink-0' />
<span className='system-sm-medium grow'>{text}</span>
</a>
)
}
export default React.memo(Link)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff