feat: Refactor dataset pipeline creation components and add internationalization support

This commit is contained in:
twwu
2025-05-07 11:30:13 +08:00
parent d196872059
commit 4025cd0b46
36 changed files with 473 additions and 126 deletions

View File

@ -121,7 +121,6 @@ const CreateFromDSLModal = ({
message: t(status === DSLImportStatus.COMPLETED ? 'app.newApp.appCreated' : 'app.newApp.caution'),
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('app.newApp.appCreateDSLWarning'),
})
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
if (app_id)
await handleCheckPluginDependencies(app_id)
getRedirection(isCurrentWorkspaceEditor, { id: app_id!, mode: app_mode }, push)

View File

@ -1,3 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import AppIcon from '@/app/components/base/app-icon'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import AppIconPicker from '@/app/components/base/app-icon-picker'
@ -5,17 +6,19 @@ import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import type { AppIconType } from '@/types/app'
import { RiCloseLine } from '@remixicon/react'
import React, { useCallback, useRef, useState } from 'react'
import PermissionSelector from '../../settings/permission-selector'
import type { CreateDatasetReq } from '@/models/datasets'
import { DatasetPermission } from '@/models/datasets'
import { useMembers } from '@/service/use-common'
import Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast'
import { useCreateDataset } from '@/service/knowledge/use-create-dataset'
import type { Member } from '@/models/common'
type CreateFromScratchProps = {
onClose: () => void
onCreate: () => void
onClose?: () => void
onCreate?: () => void
}
const DEFAULT_APP_ICON: AppIconSelection = {
@ -36,8 +39,14 @@ const CreateFromScratch = ({
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
const [selectedMemberIDs, setSelectedMemberIDs] = useState<string[]>([])
const previousAppIcon = useRef<AppIconSelection>(DEFAULT_APP_ICON)
const [memberList, setMemberList] = useState<Member[]>([])
const { data: memberList } = useMembers()
const { data: members } = useMembers()
useEffect(() => {
if (members?.accounts)
setMemberList(members.accounts)
}, [members])
const handleAppNameChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value
@ -68,6 +77,8 @@ const CreateFromScratch = ({
setPermission(value!)
}, [])
const { mutateAsync: createEmptyDataset } = useCreateDataset()
const handleCreate = useCallback(() => {
if (!name) {
Toast.notify({
@ -76,16 +87,38 @@ const CreateFromScratch = ({
})
return
}
onCreate()
onClose()
}, [name, onCreate, onClose])
const request: CreateDatasetReq = {
name,
description,
icon_info: {
icon_type: appIcon.type,
icon: appIcon.type === 'image' ? appIcon.fileId : appIcon.icon,
icon_background: appIcon.type === 'image' ? undefined : appIcon.background,
icon_url: appIcon.type === 'image' ? appIcon.url : undefined,
},
permission,
}
// Handle permission
if (request.permission === DatasetPermission.partialMembers) {
const selectedMemberList = selectedMemberIDs.map((id) => {
return {
user_id: id,
role: memberList.find(member => member.id === id)?.role,
}
})
request.partial_member_list = selectedMemberList
}
createEmptyDataset(request)
onCreate?.()
onClose?.()
}, [name, permission, appIcon, description, createEmptyDataset, memberList, selectedMemberIDs, onCreate, onClose])
return (
<div className='relative flex flex-col'>
{/* Header */}
<div className='pb-3 pl-6 pr-14 pt-6'>
<span className='title-2xl-semi-bold text-text-primary'>
Create Knowledge
{t('datasetPipeline.creation.createKnowledge')}
</span>
</div>
<button
@ -98,11 +131,13 @@ const CreateFromScratch = ({
<div className='flex flex-col gap-y-5 px-6 py-3'>
<div className='flex items-end gap-x-3 self-stretch'>
<div className='flex grow flex-col gap-y-1 pb-1'>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>Knowledge name & icon</label>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>
{t('datasetPipeline.creation.knowledgeNameAndIcon')}
</label>
<Input
onChange={handleAppNameChange}
value={name}
placeholder='Please enter the name of the Knowledge Base'
placeholder={t('datasetPipeline.creation.knowledgeNameAndIconPlaceholder')}
/>
</div>
<AppIcon
@ -117,22 +152,26 @@ const CreateFromScratch = ({
/>
</div>
<div className='flex flex-col gap-y-1'>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>Knowledge description</label>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>
{t('datasetPipeline.creation.knowledgeDescription')}
</label>
<Textarea
onChange={handleDescriptionChange}
value={description}
placeholder='Describe what is in this Knowledge Base. A detailed description allows AI to access the content of the dataset more accurately. If empty, Dify will use the default hit strategy. (Optional)'
placeholder={t('datasetPipeline.creation.knowledgeDescriptionPlaceholder')}
/>
</div>
<div className='flex flex-col gap-y-1'>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>Permissions</label>
<PermissionSelector
permission={permission}
value={selectedMemberIDs}
onChange={handlePermissionChange}
onMemberSelect={setSelectedMemberIDs}
memberList={memberList?.accounts || []}
/>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>
{t('datasetPipeline.creation.knowledgePermissions')}
</label>
<PermissionSelector
permission={permission}
value={selectedMemberIDs}
onChange={handlePermissionChange}
onMemberSelect={setSelectedMemberIDs}
memberList={memberList}
/>
</div>
</div>
{/* Actions */}

View File

@ -6,8 +6,11 @@ import CreateFromScratch from './create-from-scratch'
import { useRouter, useSearchParams } from 'next/navigation'
import CreateFromDSLModal, { CreateFromDSLModalTab } from './create-from-dsl-modal'
import { useProviderContextSelector } from '@/context/provider-context'
import { useTranslation } from 'react-i18next'
const CreateOptions = () => {
const { t } = useTranslation()
const [showCreateModal, setShowCreateModal] = useState(false)
const [showImportModal, setShowImportModal] = useState(false)
@ -53,14 +56,14 @@ const CreateOptions = () => {
<div className='flex items-center gap-x-3 px-16 py-2'>
<Item
Icon={RiAddCircleFill}
title='Create from scratch'
description='Blank knowledge pipeline'
title={t('datasetPipeline.creation.createFromScratch.title')}
description={t('datasetPipeline.creation.createFromScratch.description')}
onClick={openCreateFromScratch}
/>
<Item
Icon={RiFileUploadLine}
title='Import'
description='Import from a DSL file'
title={t('datasetPipeline.creation.ImportDSL.title')}
description={t('datasetPipeline.creation.ImportDSL.description')}
onClick={openImportFromDSL}
/>
<Modal

View File

@ -1,11 +1,14 @@
import React from 'react'
import { RiArrowLeftLine } from '@remixicon/react'
import Button from '../../base/button'
import { useTranslation } from 'react-i18next'
const Header = () => {
const { t } = useTranslation()
return (
<div className='system-md-semibold relative flex px-16 pb-2 pt-5 text-text-primary'>
<span>Create knowledge pipeline</span>
<span>{t('datasetPipeline.creation.title')}</span>
<a
className='absolute bottom-0 left-5'
href='/datasets'

View File

@ -1,56 +1,64 @@
import { ChunkingMode } from '@/models/datasets'
import { usePipelineTemplateList } from '@/service/use-pipeline'
import TemplateCard from './template-card'
export type Pipeline = {
id: string
name: string
icon_type: 'emoji' | 'image'
icon?: string
icon_background?: string
file_id?: string
url?: string
description: string
doc_form: ChunkingMode
}
import { ChunkingMode } from '@/models/datasets'
import type { PipelineTemple } from '@/models/pipeline'
const BuiltInPipelineList = () => {
const mockData: Pipeline[] = [{
// TODO: remove mock data
const mockData: PipelineTemple[] = [{
id: '1',
name: 'Pipeline 1',
description: 'This is a description of Pipeline 1. When use the general chunking mode, the chunks retrieved and recalled are the same. When use the general chunking mode, the chunks retrieved and recalled are the same.',
icon_type: 'emoji',
icon: '🤖',
icon_background: '#F0FDF9',
icon_info: {
icon: '🤖',
icon_background: '#F0FDF9',
icon_type: 'emoji',
},
doc_form: ChunkingMode.text,
position: 0,
}, {
id: '2',
name: 'Pipeline 2',
description: 'This is a description of Pipeline 2. When use the general chunking mode, the chunks retrieved and recalled are the same.',
icon_type: 'emoji',
icon: '🏖️',
icon_background: '#FFF4ED',
icon_info: {
icon: '🏖️',
icon_background: '#FFF4ED',
icon_type: 'emoji',
},
doc_form: ChunkingMode.parentChild,
position: 1,
}, {
id: '3',
name: 'Pipeline 3',
description: 'This is a description of Pipeline 3',
icon_type: 'emoji',
icon: '🚀',
icon_background: '#FEFBE8',
icon_info: {
icon: '🚀',
icon_background: '#FEFBE8',
icon_type: 'emoji',
},
doc_form: ChunkingMode.qa,
position: 2,
}, {
id: '4',
name: 'Pipeline 4',
description: 'This is a description of Pipeline 4',
icon_type: 'emoji',
icon: '🍯',
icon_background: '#F5F3FF',
icon_info: {
icon: '🍯',
icon_background: '#F5F3FF',
icon_type: 'emoji',
},
doc_form: ChunkingMode.graph,
position: 3,
}]
const { data: pipelineList, isLoading } = usePipelineTemplateList({ type: 'built-in' })
const list = pipelineList?.pipelines || mockData
if (isLoading || !list)
return null
return (
<div className='grid grow grid-cols-1 gap-3 overflow-y-auto px-16 pt-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
{mockData.map((pipeline, index) => (
{list.map((pipeline, index) => (
<TemplateCard
key={index}
pipeline={pipeline}

View File

@ -1,45 +1,64 @@
import { ChunkingMode } from '@/models/datasets'
import type { Pipeline } from './built-in-pipeline-list'
import TemplateCard from './template-card'
import { usePipelineTemplateList } from '@/service/use-pipeline'
import type { PipelineTemple } from '@/models/pipeline'
const CustomizedList = () => {
const mockData: Pipeline[] = [{
const mockData: PipelineTemple[] = [{
id: '1',
name: 'Pipeline 1',
description: 'This is a description of Pipeline 1. When use the general chunking mode, the chunks retrieved and recalled are the same. When use the general chunking mode, the chunks retrieved and recalled are the same.',
icon_type: 'emoji',
icon: '🤖',
icon_background: '#F0FDF9',
icon_info: {
icon: '🤖',
icon_background: '#F0FDF9',
icon_type: 'emoji',
},
doc_form: ChunkingMode.text,
position: 0,
}, {
id: '2',
name: 'Pipeline 2',
description: 'This is a description of Pipeline 2. When use the general chunking mode, the chunks retrieved and recalled are the same.',
icon_type: 'emoji',
icon: '🏖️',
icon_background: '#FFF4ED',
icon_info: {
icon: '🏖️',
icon_background: '#FFF4ED',
icon_type: 'emoji',
},
doc_form: ChunkingMode.parentChild,
position: 1,
}, {
id: '3',
name: 'Pipeline 3',
description: 'This is a description of Pipeline 3',
icon_type: 'emoji',
icon: '🚀',
icon_background: '#FEFBE8',
icon_info: {
icon: '🚀',
icon_background: '#FEFBE8',
icon_type: 'emoji',
},
doc_form: ChunkingMode.qa,
position: 2,
}, {
id: '4',
name: 'Pipeline 4',
description: 'This is a description of Pipeline 4',
icon_type: 'emoji',
icon: '🍯',
icon_background: '#F5F3FF',
icon_info: {
icon: '🍯',
icon_background: '#F5F3FF',
icon_type: 'emoji',
},
doc_form: ChunkingMode.graph,
position: 3,
}]
const { data: pipelineList, isLoading } = usePipelineTemplateList({ type: 'customized' })
const list = pipelineList?.pipelines || mockData
if (isLoading || !list)
return null
return (
<div className='grid grow grid-cols-1 gap-3 overflow-y-auto px-16 pt-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
{mockData.map((pipeline, index) => (
{list.map((pipeline, index) => (
<TemplateCard
key={index}
pipeline={pipeline}

View File

@ -1,16 +1,20 @@
import { useCallback, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import Tab from './tab'
import BuiltInPipelineList from './built-in-pipeline-list'
import CustomizedList from './customized-list'
const OPTIONS = [
{ value: 'built-in', label: 'Built-in Pipeline' },
{ value: 'customized', label: 'Customized' },
]
import { useTranslation } from 'react-i18next'
const List = () => {
const { t } = useTranslation()
const [activeTab, setActiveTab] = useState('built-in')
const options = useMemo(() => {
return [
{ value: 'built-in', label: t('datasetPipeline.tabs.builtInPipeline') },
{ value: 'customized', label: t('datasetPipeline.tabs.customized') },
]
}, [t])
const handleTabChange = useCallback((tab: string) => {
setActiveTab(tab)
}, [])
@ -20,7 +24,7 @@ const List = () => {
<Tab
activeTab={activeTab}
handleTabChange={handleTabChange}
options={OPTIONS}
options={options}
/>
{
activeTab === 'built-in' && <BuiltInPipelineList />

View File

@ -8,12 +8,12 @@ import React, { useCallback, useRef, useState } from 'react'
import Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast'
import type { Pipeline } from '../built-in-pipeline-list'
import type { PipelineTemple } from '@/models/pipeline'
type EditPipelineInfoProps = {
onClose: () => void
onSave: () => void
pipeline: Pipeline
pipeline: PipelineTemple
}
const EditPipelineInfo = ({
@ -23,17 +23,18 @@ const EditPipelineInfo = ({
}: EditPipelineInfoProps) => {
const { t } = useTranslation()
const [name, setName] = useState(pipeline.name)
const iconInfo = pipeline.icon_info
const [appIcon, setAppIcon] = useState<AppIconSelection>(
pipeline.icon_type === 'image'
? { type: 'image' as const, url: pipeline.url || '', fileId: pipeline.file_id || '' }
: { type: 'emoji' as const, icon: pipeline.icon || '', background: pipeline.icon_background || '' },
iconInfo.icon_type === 'image'
? { type: 'image' as const, url: iconInfo.icon_url || '', fileId: iconInfo.icon || '' }
: { type: 'emoji' as const, icon: iconInfo.icon || '', background: iconInfo.icon_background || '' },
)
const [description, setDescription] = useState(pipeline.description)
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
const previousAppIcon = useRef<AppIconSelection>(
pipeline.icon_type === 'image'
? { type: 'image' as const, url: pipeline.url || '', fileId: pipeline.file_id || '' }
: { type: 'emoji' as const, icon: pipeline.icon || '', background: pipeline.icon_background || '' },
iconInfo.icon_type === 'image'
? { type: 'image' as const, url: iconInfo.icon_url || '', fileId: iconInfo.icon || '' }
: { type: 'emoji' as const, icon: iconInfo.icon || '', background: iconInfo.icon_background || '' },
)
const handleAppNameChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {

View File

@ -1,7 +1,5 @@
import React, { useCallback, useState } from 'react'
import type { Pipeline } from '../built-in-pipeline-list'
import AppIcon from '@/app/components/base/app-icon'
import { DOC_FORM_ICON, DOC_FORM_TEXT } from '../../../list/dataset-card'
import { General } from '@/app/components/base/icons/src/public/knowledge'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
@ -10,9 +8,11 @@ import CustomPopover from '@/app/components/base/popover'
import Operations from './operations'
import Modal from '@/app/components/base/modal'
import EditPipelineInfo from './edit-pipeline-info'
import type { PipelineTemple } from '@/models/pipeline'
import { DOC_FORM_ICON, DOC_FORM_TEXT } from '@/models/datasets'
type TemplateCardProps = {
pipeline: Pipeline
pipeline: PipelineTemple
showMoreOperations?: boolean
}
@ -32,6 +32,7 @@ const TemplateCard = ({
}, [])
const Icon = DOC_FORM_ICON[pipeline.doc_form] || General
const iconInfo = pipeline.icon_info
return (
<div className='group relative flex h-[132px] cursor-pointer flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pb-3 shadow-xs shadow-shadow-shadow-3'>
@ -39,10 +40,10 @@ const TemplateCard = ({
<div className='relative shrink-0'>
<AppIcon
size='large'
iconType={pipeline.icon_type}
icon={pipeline.icon_type === 'image' ? pipeline.file_id : pipeline.icon}
background={pipeline.icon_type === 'image' ? undefined : pipeline.icon_background}
imageUrl={pipeline.icon_type === 'image' ? pipeline.url : undefined}
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}
/>
<div className='absolute -bottom-1 -right-1 z-10'>
<Icon className='size-4' />

View File

@ -2,9 +2,8 @@
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 { ExternalKnowledgeBase, General, Graph, ParentChild, Qa } from '@/app/components/base/icons/src/public/knowledge'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { General } 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'
@ -23,25 +22,11 @@ import CustomPopover from '@/app/components/base/popover'
import Operations from './operations'
import AppIcon from '@/app/components/base/app-icon'
import CornerLabel from '@/app/components/base/corner-label'
import { DOC_FORM_ICON, DOC_FORM_TEXT } from '@/models/datasets'
const EXTERNAL_PROVIDER = 'external'
export const DOC_FORM_ICON: Record<ChunkingMode | 'external', React.ComponentType<{ className: string }>> = {
[ChunkingMode.text]: General,
[ChunkingMode.qa]: Qa,
[ChunkingMode.parentChild]: ParentChild,
[ChunkingMode.graph]: Graph,
external: ExternalKnowledgeBase,
}
export const DOC_FORM_TEXT: Record<ChunkingMode, string> = {
[ChunkingMode.text]: 'general',
[ChunkingMode.qa]: 'qa',
[ChunkingMode.parentChild]: 'parentChild',
[ChunkingMode.graph]: 'graph',
}
export type DatasetCardProps = {
type DatasetCardProps = {
dataset: DataSet
onSuccess?: () => void
}
@ -53,7 +38,7 @@ const DatasetCard = ({
const { t } = useTranslation()
const { push } = useRouter()
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
const isCurrentWorkspaceDatasetOperator = useAppContextWithSelector(state => state.isCurrentWorkspaceDatasetOperator)
const [tags, setTags] = useState<Tag[]>(dataset.tags)
const tagSelectorRef = useRef<HTMLDivElement>(null)
const isHoveringTagSelector = useHover(tagSelectorRef)