feat: Refactor dataset pipeline creation components and add internationalization support

pull/21398/head
twwu 1 year ago
parent d196872059
commit 4025cd0b46

@ -28,13 +28,12 @@ import DatasetDetailContext from '@/context/dataset-detail'
import { DataSourceType } from '@/models/datasets' import { DataSourceType } from '@/models/datasets'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useStore } from '@/app/components/app/store' import { useStore } from '@/app/components/app/store'
import { getLocaleOnClient } from '@/i18n'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import LinkedAppsPanel from '@/app/components/base/linked-apps-panel' import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
import { PipelineFill, PipelineLine } from '@/app/components/base/icons/src/public/pipeline' import { PipelineFill, PipelineLine } from '@/app/components/base/icons/src/public/pipeline'
import { Divider } from '@/app/components/base/icons/src/public/knowledge' import { Divider } from '@/app/components/base/icons/src/public/knowledge'
import { LanguagesSupported } from '@/i18n/language' import { useGetDocLanguage } from '@/context/i18n'
export type IAppDetailLayoutProps = { export type IAppDetailLayoutProps = {
children: React.ReactNode children: React.ReactNode
@ -54,8 +53,8 @@ const ExtraInfo = React.memo(({
documentCount, documentCount,
expand, expand,
}: IExtraInfoProps) => { }: IExtraInfoProps) => {
const locale = getLocaleOnClient()
const { t } = useTranslation() const { t } = useTranslation()
const docLanguage = useGetDocLanguage()
const hasRelatedApps = relatedApps?.data && relatedApps?.data?.length > 0 const hasRelatedApps = relatedApps?.data && relatedApps?.data?.length > 0
const relatedAppsTotal = relatedApps?.data?.length || 0 const relatedAppsTotal = relatedApps?.data?.length || 0
@ -122,11 +121,7 @@ const ExtraInfo = React.memo(({
<div className='my-2 text-xs text-text-tertiary'>{t('common.datasetMenus.emptyTip')}</div> <div className='my-2 text-xs text-text-tertiary'>{t('common.datasetMenus.emptyTip')}</div>
<a <a
className='mt-2 inline-flex cursor-pointer items-center text-xs text-text-accent' className='mt-2 inline-flex cursor-pointer items-center text-xs text-text-accent'
href={ href={`https://docs.dify.ai/${docLanguage}/guides/knowledge-base/integrate-knowledge-within-application`}
locale === LanguagesSupported[1]
? 'https://docs.dify.ai/v/zh-hans/guides/knowledge-base/integrate-knowledge-within-application'
: 'https://docs.dify.ai/guides/knowledge-base/integrate-knowledge-within-application'
}
target='_blank' rel='noopener noreferrer' target='_blank' rel='noopener noreferrer'
> >
<RiBookOpenLine className='mr-1 text-text-accent' /> <RiBookOpenLine className='mr-1 text-text-accent' />

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

@ -1,3 +1,4 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import AppIconPicker 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 Textarea from '@/app/components/base/textarea'
import type { AppIconType } from '@/types/app' import type { AppIconType } from '@/types/app'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import React, { useCallback, useRef, useState } from 'react'
import PermissionSelector from '../../settings/permission-selector' import PermissionSelector from '../../settings/permission-selector'
import type { CreateDatasetReq } from '@/models/datasets'
import { DatasetPermission } from '@/models/datasets' import { DatasetPermission } from '@/models/datasets'
import { useMembers } from '@/service/use-common' import { useMembers } from '@/service/use-common'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { useCreateDataset } from '@/service/knowledge/use-create-dataset'
import type { Member } from '@/models/common'
type CreateFromScratchProps = { type CreateFromScratchProps = {
onClose: () => void onClose?: () => void
onCreate: () => void onCreate?: () => void
} }
const DEFAULT_APP_ICON: AppIconSelection = { const DEFAULT_APP_ICON: AppIconSelection = {
@ -36,8 +39,14 @@ const CreateFromScratch = ({
const [showAppIconPicker, setShowAppIconPicker] = useState(false) const [showAppIconPicker, setShowAppIconPicker] = useState(false)
const [selectedMemberIDs, setSelectedMemberIDs] = useState<string[]>([]) const [selectedMemberIDs, setSelectedMemberIDs] = useState<string[]>([])
const previousAppIcon = useRef<AppIconSelection>(DEFAULT_APP_ICON) 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 handleAppNameChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value const value = event.target.value
@ -68,6 +77,8 @@ const CreateFromScratch = ({
setPermission(value!) setPermission(value!)
}, []) }, [])
const { mutateAsync: createEmptyDataset } = useCreateDataset()
const handleCreate = useCallback(() => { const handleCreate = useCallback(() => {
if (!name) { if (!name) {
Toast.notify({ Toast.notify({
@ -76,16 +87,38 @@ const CreateFromScratch = ({
}) })
return return
} }
onCreate() const request: CreateDatasetReq = {
onClose() name,
}, [name, onCreate, onClose]) 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 ( return (
<div className='relative flex flex-col'> <div className='relative flex flex-col'>
{/* Header */} {/* Header */}
<div className='pb-3 pl-6 pr-14 pt-6'> <div className='pb-3 pl-6 pr-14 pt-6'>
<span className='title-2xl-semi-bold text-text-primary'> <span className='title-2xl-semi-bold text-text-primary'>
Create Knowledge {t('datasetPipeline.creation.createKnowledge')}
</span> </span>
</div> </div>
<button <button
@ -98,11 +131,13 @@ const CreateFromScratch = ({
<div className='flex flex-col gap-y-5 px-6 py-3'> <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 items-end gap-x-3 self-stretch'>
<div className='flex grow flex-col gap-y-1 pb-1'> <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 <Input
onChange={handleAppNameChange} onChange={handleAppNameChange}
value={name} value={name}
placeholder='Please enter the name of the Knowledge Base' placeholder={t('datasetPipeline.creation.knowledgeNameAndIconPlaceholder')}
/> />
</div> </div>
<AppIcon <AppIcon
@ -117,21 +152,25 @@ const CreateFromScratch = ({
/> />
</div> </div>
<div className='flex flex-col gap-y-1'> <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 <Textarea
onChange={handleDescriptionChange} onChange={handleDescriptionChange}
value={description} 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>
<div className='flex flex-col gap-y-1'> <div className='flex flex-col gap-y-1'>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>Permissions</label> <label className='system-sm-medium flex h-6 items-center text-text-secondary'>
{t('datasetPipeline.creation.knowledgePermissions')}
</label>
<PermissionSelector <PermissionSelector
permission={permission} permission={permission}
value={selectedMemberIDs} value={selectedMemberIDs}
onChange={handlePermissionChange} onChange={handlePermissionChange}
onMemberSelect={setSelectedMemberIDs} onMemberSelect={setSelectedMemberIDs}
memberList={memberList?.accounts || []} memberList={memberList}
/> />
</div> </div>
</div> </div>

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

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

@ -1,56 +1,64 @@
import { ChunkingMode } from '@/models/datasets' import { usePipelineTemplateList } from '@/service/use-pipeline'
import TemplateCard from './template-card' import TemplateCard from './template-card'
import { ChunkingMode } from '@/models/datasets'
export type Pipeline = { import type { PipelineTemple } from '@/models/pipeline'
id: string
name: string
icon_type: 'emoji' | 'image'
icon?: string
icon_background?: string
file_id?: string
url?: string
description: string
doc_form: ChunkingMode
}
const BuiltInPipelineList = () => { const BuiltInPipelineList = () => {
const mockData: Pipeline[] = [{ // TODO: remove mock data
const mockData: PipelineTemple[] = [{
id: '1', id: '1',
name: 'Pipeline 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.', 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_info: {
icon: '🤖', icon: '🤖',
icon_background: '#F0FDF9', icon_background: '#F0FDF9',
icon_type: 'emoji',
},
doc_form: ChunkingMode.text, doc_form: ChunkingMode.text,
position: 0,
}, { }, {
id: '2', id: '2',
name: 'Pipeline 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.', 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_info: {
icon: '🏖️', icon: '🏖️',
icon_background: '#FFF4ED', icon_background: '#FFF4ED',
icon_type: 'emoji',
},
doc_form: ChunkingMode.parentChild, doc_form: ChunkingMode.parentChild,
position: 1,
}, { }, {
id: '3', id: '3',
name: 'Pipeline 3', name: 'Pipeline 3',
description: 'This is a description of Pipeline 3', description: 'This is a description of Pipeline 3',
icon_type: 'emoji', icon_info: {
icon: '🚀', icon: '🚀',
icon_background: '#FEFBE8', icon_background: '#FEFBE8',
icon_type: 'emoji',
},
doc_form: ChunkingMode.qa, doc_form: ChunkingMode.qa,
position: 2,
}, { }, {
id: '4', id: '4',
name: 'Pipeline 4', name: 'Pipeline 4',
description: 'This is a description of Pipeline 4', description: 'This is a description of Pipeline 4',
icon_type: 'emoji', icon_info: {
icon: '🍯', icon: '🍯',
icon_background: '#F5F3FF', icon_background: '#F5F3FF',
icon_type: 'emoji',
},
doc_form: ChunkingMode.graph, 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 ( 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'> <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 <TemplateCard
key={index} key={index}
pipeline={pipeline} pipeline={pipeline}

@ -1,45 +1,64 @@
import { ChunkingMode } from '@/models/datasets' import { ChunkingMode } from '@/models/datasets'
import type { Pipeline } from './built-in-pipeline-list'
import TemplateCard from './template-card' import TemplateCard from './template-card'
import { usePipelineTemplateList } from '@/service/use-pipeline'
import type { PipelineTemple } from '@/models/pipeline'
const CustomizedList = () => { const CustomizedList = () => {
const mockData: Pipeline[] = [{ const mockData: PipelineTemple[] = [{
id: '1', id: '1',
name: 'Pipeline 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.', 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_info: {
icon: '🤖', icon: '🤖',
icon_background: '#F0FDF9', icon_background: '#F0FDF9',
icon_type: 'emoji',
},
doc_form: ChunkingMode.text, doc_form: ChunkingMode.text,
position: 0,
}, { }, {
id: '2', id: '2',
name: 'Pipeline 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.', 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_info: {
icon: '🏖️', icon: '🏖️',
icon_background: '#FFF4ED', icon_background: '#FFF4ED',
icon_type: 'emoji',
},
doc_form: ChunkingMode.parentChild, doc_form: ChunkingMode.parentChild,
position: 1,
}, { }, {
id: '3', id: '3',
name: 'Pipeline 3', name: 'Pipeline 3',
description: 'This is a description of Pipeline 3', description: 'This is a description of Pipeline 3',
icon_type: 'emoji', icon_info: {
icon: '🚀', icon: '🚀',
icon_background: '#FEFBE8', icon_background: '#FEFBE8',
icon_type: 'emoji',
},
doc_form: ChunkingMode.qa, doc_form: ChunkingMode.qa,
position: 2,
}, { }, {
id: '4', id: '4',
name: 'Pipeline 4', name: 'Pipeline 4',
description: 'This is a description of Pipeline 4', description: 'This is a description of Pipeline 4',
icon_type: 'emoji', icon_info: {
icon: '🍯', icon: '🍯',
icon_background: '#F5F3FF', icon_background: '#F5F3FF',
icon_type: 'emoji',
},
doc_form: ChunkingMode.graph, doc_form: ChunkingMode.graph,
position: 3,
}] }]
const { data: pipelineList, isLoading } = usePipelineTemplateList({ type: 'customized' })
const list = pipelineList?.pipelines || mockData
if (isLoading || !list)
return null
return ( 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'> <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 <TemplateCard
key={index} key={index}
pipeline={pipeline} pipeline={pipeline}

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

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

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

@ -2,9 +2,8 @@
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { DataSet } from '@/models/datasets' import type { DataSet } from '@/models/datasets'
import { ChunkingMode } from '@/models/datasets' import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { useAppContext } from '@/context/app-context' import { General } from '@/app/components/base/icons/src/public/knowledge'
import { ExternalKnowledgeBase, General, Graph, ParentChild, Qa } from '@/app/components/base/icons/src/public/knowledge'
import { useKnowledge } from '@/hooks/use-knowledge' import { useKnowledge } from '@/hooks/use-knowledge'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { Tag } from '@/app/components/base/tag-management/constant' 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 Operations from './operations'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import CornerLabel from '@/app/components/base/corner-label' import CornerLabel from '@/app/components/base/corner-label'
import { DOC_FORM_ICON, DOC_FORM_TEXT } from '@/models/datasets'
const EXTERNAL_PROVIDER = 'external' const EXTERNAL_PROVIDER = 'external'
export const DOC_FORM_ICON: Record<ChunkingMode | 'external', React.ComponentType<{ className: string }>> = { type DatasetCardProps = {
[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 = {
dataset: DataSet dataset: DataSet
onSuccess?: () => void onSuccess?: () => void
} }
@ -53,7 +38,7 @@ const DatasetCard = ({
const { t } = useTranslation() const { t } = useTranslation()
const { push } = useRouter() const { push } = useRouter()
const { isCurrentWorkspaceDatasetOperator } = useAppContext() const isCurrentWorkspaceDatasetOperator = useAppContextWithSelector(state => state.isCurrentWorkspaceDatasetOperator)
const [tags, setTags] = useState<Tag[]>(dataset.tags) const [tags, setTags] = useState<Tag[]>(dataset.tags)
const tagSelectorRef = useRef<HTMLDivElement>(null) const tagSelectorRef = useRef<HTMLDivElement>(null)
const isHoveringTagSelector = useHover(tagSelectorRef) const isHoveringTagSelector = useHover(tagSelectorRef)

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -0,0 +1,25 @@
const translation = {
creation: {
title: 'Create knowledge pipeline',
createFromScratch: {
title: 'Create from scratch',
description: 'Blank knowledge pipeline',
},
ImportDSL: {
title: 'Import',
description: 'Import from a DSL file',
},
createKnowledge: 'Create Knowledge',
knowledgeNameAndIcon: 'Knowledge name & icon',
knowledgeNameAndIconPlaceholder: 'Please enter the name of the Knowledge Base',
knowledgeDescription: 'Knowledge description',
knowledgeDescriptionPlaceholder: '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)',
knowledgePermissions: 'Permissions',
},
tabs: {
builtInPipeline: 'Built-in pipeline',
customized: 'Customized',
},
}
export default translation

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -34,6 +34,7 @@ const loadLangResources = (lang: string) => ({
datasetHitTesting: require(`./${lang}/dataset-hit-testing`).default, datasetHitTesting: require(`./${lang}/dataset-hit-testing`).default,
datasetSettings: require(`./${lang}/dataset-settings`).default, datasetSettings: require(`./${lang}/dataset-settings`).default,
datasetCreation: require(`./${lang}/dataset-creation`).default, datasetCreation: require(`./${lang}/dataset-creation`).default,
datasetPipeline: require(`./${lang}/dataset-pipeline`).default,
explore: require(`./${lang}/explore`).default, explore: require(`./${lang}/explore`).default,
billing: require(`./${lang}/billing`).default, billing: require(`./${lang}/billing`).default,
custom: require(`./${lang}/custom`).default, custom: require(`./${lang}/custom`).default,

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -0,0 +1,25 @@
const translation = {
creation: {
title: '创建知识库流水线',
createFromScratch: {
title: '从零开始创建',
description: '空白知识库流水线',
},
ImportDSL: {
title: '导入',
description: '从 DSL 文件导入',
},
createKnowledge: '创建知识库',
knowledgeNameAndIcon: '知识库名称和图标',
knowledgeNameAndIconPlaceholder: '请输入知识库名称',
knowledgeDescription: '知识库描述',
knowledgeDescriptionPlaceholder: '描述知识库中的内容。详细的描述可以让 AI 更准确地访问数据集的内容。如果为空Dify 将使用默认的命中策略。(可选)',
knowledgePermissions: '权限',
},
tabs: {
builtInPipeline: '内置流水线',
customized: '自定义',
},
}
export default translation

@ -0,0 +1,3 @@
const translation = {}
export default translation

@ -4,6 +4,7 @@ import type { Tag } from '@/app/components/base/tag-management/constant'
import type { IndexingType } from '@/app/components/datasets/create/step-two' import type { IndexingType } from '@/app/components/datasets/create/step-two'
import type { MetadataFilteringVariableType } from '@/app/components/workflow/nodes/knowledge-retrieval/types' import type { MetadataFilteringVariableType } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
import type { MetadataItemWithValue } from '@/app/components/datasets/metadata/types' import type { MetadataItemWithValue } from '@/app/components/datasets/metadata/types'
import { ExternalKnowledgeBase, General, Graph, ParentChild, Qa } from '@/app/components/base/icons/src/public/knowledge'
export enum DataSourceType { export enum DataSourceType {
FILE = 'upload_file', FILE = 'upload_file',
@ -31,16 +32,18 @@ export type MetadataInDoc = {
name: string name: string
} }
export type DataSet = { export type IconInfo = {
id: string
name: string
indexing_status: DocumentIndexingStatus
icon_info: {
icon: string icon: string
icon_background?: string icon_background?: string
icon_type: AppIconType icon_type: AppIconType
icon_url?: string icon_url?: string
} }
export type DataSet = {
id: string
name: string
indexing_status: DocumentIndexingStatus
icon_info: IconInfo
description: string description: string
permission: DatasetPermission permission: DatasetPermission
data_source_type: DataSourceType data_source_type: DataSourceType
@ -346,7 +349,7 @@ export type DocumentListResponse = {
export type DocumentReq = { export type DocumentReq = {
original_document_id?: string original_document_id?: string
indexing_technique?: string indexing_technique?: IndexingType
doc_form: ChunkingMode doc_form: ChunkingMode
doc_language: string doc_language: string
process_rule: ProcessRule process_rule: ProcessRule
@ -700,3 +703,47 @@ export type BatchImportResponse = {
job_id: string job_id: string
job_status: string job_status: string
} }
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 CreateDatasetReq = {
name: string
description: string
icon_info: IconInfo
doc_form?: ChunkingMode
permission: DatasetPermission
partial_member_list?: {
user_id: string
role?: 'owner' | 'admin' | 'editor' | 'normal' | 'dataset_operator'
}[]
indexing_technique?: IndexingType
retrieval_mode?: RetrievalConfig
embedding_model?: string
embedding_model_provider?: string
}
export type CreateDatasetResponse = {
id: string
name: string
description: string
permission: DatasetPermission
data_source_type: DataSourceType
indexing_technique: IndexingType
created_by: string
created_at: number
updated_by: string
updated_at: number
}

@ -0,0 +1,36 @@
import type { ChunkingMode, IconInfo } from './datasets'
export type PipelineTemplateListParams = {
type: 'built-in' | 'customized'
}
export type PipelineTemple = {
id: string
name: string
icon_info: IconInfo
description: string
position: number
doc_form: ChunkingMode
}
export type PipelineTemplateListResponse = {
pipelines: PipelineTemple[]
}
export type PipelineTemplateByIdResponse = {
name: string
icon_info: IconInfo
description: string
export_data: string
}
export type UpdatePipelineInfoPayload = {
pipelineId: string
name: string
icon_info: IconInfo
description: string
}
export type ExportPipelineDSLResponse = {
data: string
}

@ -3,8 +3,24 @@ import type { MutationOptions } from '@tanstack/react-query'
import { useMutation } from '@tanstack/react-query' import { useMutation } from '@tanstack/react-query'
import { createDocument, createFirstDocument, fetchDefaultProcessRule, fetchFileIndexingEstimate } from '../datasets' import { createDocument, createFirstDocument, fetchDefaultProcessRule, fetchFileIndexingEstimate } from '../datasets'
import type { IndexingType } from '@/app/components/datasets/create/step-two' import type { IndexingType } from '@/app/components/datasets/create/step-two'
import type { ChunkingMode, CrawlOptions, CrawlResultItem, CreateDocumentReq, CustomFile, DataSourceType, FileIndexingEstimateResponse, IndexingEstimateParams, NotionInfo, ProcessRule, ProcessRuleResponse, createDocumentResponse } from '@/models/datasets' import type {
ChunkingMode,
CrawlOptions,
CrawlResultItem,
CreateDatasetReq,
CreateDatasetResponse,
CreateDocumentReq,
CustomFile,
DataSourceType,
FileIndexingEstimateResponse,
IndexingEstimateParams,
NotionInfo,
ProcessRule,
ProcessRuleResponse,
createDocumentResponse,
} from '@/models/datasets'
import type { DataSourceProvider, NotionPage } from '@/models/common' import type { DataSourceProvider, NotionPage } from '@/models/common'
import { post } from '../base'
export const getNotionInfo = ( export const getNotionInfo = (
notionPages: NotionPage[], notionPages: NotionPage[],
@ -221,3 +237,14 @@ export const useFetchDefaultProcessRule = (
...mutationOptions, ...mutationOptions,
}) })
} }
export const useCreateDataset = (
mutationOptions: MutationOptions<CreateDatasetResponse, Error, CreateDatasetReq> = {},
) => {
return useMutation({
mutationFn: (req: CreateDatasetReq) => {
return post<CreateDatasetResponse>('/datasets', { body: req })
},
...mutationOptions,
})
}

@ -0,0 +1,75 @@
import { useMutation, useQuery } from '@tanstack/react-query'
import { del, get, patch } from './base'
import type {
ExportPipelineDSLResponse,
PipelineTemplateByIdResponse,
PipelineTemplateListParams,
PipelineTemplateListResponse,
UpdatePipelineInfoPayload,
} from '@/models/pipeline'
const NAME_SPACE = 'pipeline'
export const usePipelineTemplateList = (params: PipelineTemplateListParams) => {
return useQuery<PipelineTemplateListResponse>({
queryKey: [NAME_SPACE, 'template', 'list'],
queryFn: () => {
return get<PipelineTemplateListResponse>('/rag/pipeline/template', { params })
},
})
}
export const usePipelineTemplateById = (templateId: string) => {
return useQuery<PipelineTemplateByIdResponse>({
queryKey: [NAME_SPACE, 'template', templateId],
queryFn: () => {
return get<PipelineTemplateByIdResponse>(`/rag/pipeline/template/${templateId}`)
},
})
}
export const useUpdatePipelineInfo = ({
onSuccess,
onError,
}: {
onSuccess?: () => void
onError?: (error: any) => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'template', 'update'],
mutationFn: (payload: UpdatePipelineInfoPayload) => {
const { pipelineId, ...rest } = payload
return patch(`/rag/pipeline/${pipelineId}`, {
body: rest,
})
},
onSuccess,
onError,
})
}
export const useDeletePipeline = ({
onSuccess,
onError,
}: {
onSuccess?: () => void
onError?: (error: any) => void
}) => {
return useMutation({
mutationKey: [NAME_SPACE, 'template', 'delete'],
mutationFn: (pipelineId: string) => {
return del(`/rag/pipeline/${pipelineId}`)
},
onSuccess,
onError,
})
}
export const useExportPipelineDSL = (pipelineId: string) => {
return useQuery<ExportPipelineDSLResponse>({
queryKey: [NAME_SPACE, 'template', 'export', pipelineId],
queryFn: () => {
return get<ExportPipelineDSLResponse>(`/rag/pipeline/${pipelineId}`)
},
})
}
Loading…
Cancel
Save