Merge remote-tracking branch 'origin/feat/rag-2' into feat/rag-2

This commit is contained in:
jyong 2025-08-22 11:34:13 +08:00
commit 2f6c51927e
8 changed files with 37 additions and 430 deletions

View File

@ -1,182 +0,0 @@
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'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import type { Member } from '@/models/common'
import { DatasetPermission } from '@/models/datasets'
import { useMembers } from '@/service/use-common'
import type { AppIconType } from '@/types/app'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import PermissionSelector from '@/app/components/datasets/settings/permission-selector'
import Button from '@/app/components/base/button'
import { RiCloseLine } from '@remixicon/react'
import Toast from '@/app/components/base/toast'
import type { CreateFormData } from '@/models/pipeline'
const DEFAULT_APP_ICON: AppIconSelection = {
type: 'emoji',
icon: '📙',
background: '#FFF4ED',
}
type CreateFormProps = {
onCreate: (payload: CreateFormData) => void
onClose: () => void
}
const CreateForm = ({
onCreate,
onClose,
}: CreateFormProps) => {
const { t } = useTranslation()
const [name, setName] = useState('')
const [appIcon, setAppIcon] = useState<AppIconSelection>(DEFAULT_APP_ICON)
const [description, setDescription] = useState('')
const [permission, setPermission] = useState(DatasetPermission.onlyMe)
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
const [selectedMemberIDs, setSelectedMemberIDs] = useState<string[]>([])
const previousAppIcon = useRef<AppIconSelection>(DEFAULT_APP_ICON)
const [memberList, setMemberList] = useState<Member[]>([])
const { data: members } = useMembers()
useEffect(() => {
if (members?.accounts)
setMemberList(members.accounts)
}, [members])
const handleAppNameChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value
setName(value)
}, [])
const handleOpenAppIconPicker = useCallback(() => {
setShowAppIconPicker(true)
previousAppIcon.current = appIcon
}, [appIcon])
const handleSelectAppIcon = useCallback((icon: AppIconSelection) => {
setAppIcon(icon)
setShowAppIconPicker(false)
}, [])
const handleCloseAppIconPicker = useCallback(() => {
setAppIcon(previousAppIcon.current)
setShowAppIconPicker(false)
}, [])
const handleDescriptionChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = event.target.value
setDescription(value)
}, [])
const handlePermissionChange = useCallback((value?: DatasetPermission) => {
setPermission(value!)
}, [])
const handleCreate = useCallback(() => {
if (!name) {
Toast.notify({
type: 'error',
message: 'Please enter a name for the Knowledge Base.',
})
return
}
onCreate({
name,
appIcon,
description,
permission,
selectedMemberIDs,
})
}, [name, appIcon, description, permission, selectedMemberIDs, onCreate])
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'>
{t('datasetPipeline.creation.createKnowledge')}
</span>
</div>
<button
className='absolute right-5 top-5 flex size-8 items-center justify-center'
onClick={onClose}
>
<RiCloseLine className='size-5 text-text-tertiary' />
</button>
{/* Form */}
<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'>
{t('datasetPipeline.knowledgeNameAndIcon')}
</label>
<Input
onChange={handleAppNameChange}
value={name}
placeholder={t('datasetPipeline.knowledgeNameAndIconPlaceholder')}
/>
</div>
<AppIcon
size='xxl'
onClick={handleOpenAppIconPicker}
className='cursor-pointer'
iconType={appIcon.type as AppIconType}
icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
background={appIcon.type === 'image' ? undefined : appIcon.background}
imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
showEditIcon
/>
</div>
<div className='flex flex-col gap-y-1'>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>
{t('datasetPipeline.knowledgeDescription')}
</label>
<Textarea
onChange={handleDescriptionChange}
value={description}
placeholder={t('datasetPipeline.knowledgeDescriptionPlaceholder')}
/>
</div>
<div className='flex flex-col gap-y-1'>
<label className='system-sm-medium flex h-6 items-center text-text-secondary'>
{t('datasetPipeline.knowledgePermissions')}
</label>
<PermissionSelector
permission={permission}
value={selectedMemberIDs}
onChange={handlePermissionChange}
onMemberSelect={setSelectedMemberIDs}
memberList={memberList}
/>
</div>
</div>
{/* Actions */}
<div className='flex items-center justify-end gap-x-2 p-6 pt-5'>
<Button
variant='secondary'
onClick={onClose}
>
{t('common.operation.cancel')}
</Button>
<Button
variant='primary'
onClick={handleCreate}
>
{t('common.operation.create')}
</Button>
</div>
{showAppIconPicker && (
<AppIconPicker
onSelect={handleSelectAppIcon}
onClose={handleCloseAppIconPicker}
/>
)}
</div>
)
}
export default React.memo(CreateForm)

View File

@ -1,99 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react'
import type { CreateDatasetReq } from '@/models/datasets'
import { ChunkingMode, DatasetPermission } from '@/models/datasets'
import { useMembers } from '@/service/use-common'
import { useCreatePipelineDataset } from '@/service/knowledge/use-create-dataset'
import type { Member } from '@/models/common'
import CreateForm from '../create-form'
import type { CreateFormData } from '@/models/pipeline'
import Modal from '@/app/components/base/modal'
import { useRouter } from 'next/navigation'
import Toast from '@/app/components/base/toast'
import { useTranslation } from 'react-i18next'
import { useResetDatasetList } from '@/service/knowledge/use-dataset'
type CreateFromScratchModalProps = {
show: boolean
onClose: () => void
}
const CreateFromScratchModal = ({
show,
onClose,
}: CreateFromScratchModalProps) => {
const { t } = useTranslation()
const { push } = useRouter()
const [memberList, setMemberList] = useState<Member[]>([])
const { data: members } = useMembers()
useEffect(() => {
if (members?.accounts)
setMemberList(members.accounts)
}, [members])
const { mutateAsync: createEmptyDataset } = useCreatePipelineDataset()
const resetDatasetList = useResetDatasetList()
const handleCreate = useCallback(async (payload: CreateFormData) => {
const { name, appIcon, description, permission, selectedMemberIDs } = payload
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,
},
doc_form: ChunkingMode.text,
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
}
await createEmptyDataset(request, {
onSuccess: (data) => {
if (data) {
const { id } = data
Toast.notify({
type: 'success',
message: t('datasetPipeline.creation.successTip'),
})
resetDatasetList()
push(`/datasets/${id}/pipeline`)
}
},
onError: () => {
Toast.notify({
type: 'error',
message: t('datasetPipeline.creation.errorTip'),
})
},
onSettled: () => {
onClose?.()
},
})
}, [createEmptyDataset, memberList, onClose, push, resetDatasetList, t])
return (
<Modal
isShow={show}
onClose={onClose}
className='max-w-[520px] p-0'
>
<CreateForm
onCreate={handleCreate}
onClose={onClose}
/>
</Modal>
)
}
export default CreateFromScratchModal

View File

@ -1,56 +1,44 @@
import React, { useCallback, useState } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { RiAddCircleLine } from '@remixicon/react'
import CreateFromScratchModal from '../create-options/create-from-scratch-modal'
// import { useCreatePipelineDataset } from '@/service/knowledge/use-create-dataset'
// import { useResetDatasetList } from '@/service/knowledge/use-dataset'
// import Toast from '@/app/components/base/toast'
// import { useRouter } from 'next/navigation'
import { useCreatePipelineDataset } from '@/service/knowledge/use-create-dataset'
import { useResetDatasetList } from '@/service/knowledge/use-dataset'
import Toast from '@/app/components/base/toast'
import { useRouter } from 'next/navigation'
const CreateCard = () => {
const { t } = useTranslation()
// const { push } = useRouter()
const { push } = useRouter()
const [showCreateModal, setShowCreateModal] = useState(false)
const { mutateAsync: createEmptyDataset } = useCreatePipelineDataset()
const resetDatasetList = useResetDatasetList()
// const { mutateAsync: createEmptyDataset } = useCreatePipelineDataset()
// const resetDatasetList = useResetDatasetList()
// todo: Directly create a pipeline dataset, no need to fill in the form
// const handleCreate = useCallback(async () => {
// await createEmptyDataset({}, {
// onSuccess: (data) => {
// if (data) {
// const { id } = data
// Toast.notify({
// type: 'success',
// message: t('datasetPipeline.creation.successTip'),
// })
// resetDatasetList()
// push(`/datasets/${id}/pipeline`)
// }
// },
// onError: () => {
// Toast.notify({
// type: 'error',
// message: t('datasetPipeline.creation.errorTip'),
// })
// },
// })
// }, [createEmptyDataset, push, resetDatasetList, t])
const openCreateFromScratch = useCallback(() => {
setShowCreateModal(true)
}, [])
const closeCreateFromScratch = useCallback(() => {
setShowCreateModal(false)
}, [])
const handleCreate = useCallback(async () => {
await createEmptyDataset(undefined, {
onSuccess: (data) => {
if (data) {
const { id } = data
Toast.notify({
type: 'success',
message: t('datasetPipeline.creation.successTip'),
})
resetDatasetList()
push(`/datasets/${id}/pipeline`)
}
},
onError: () => {
Toast.notify({
type: 'error',
message: t('datasetPipeline.creation.errorTip'),
})
},
})
}, [createEmptyDataset, push, resetDatasetList, t])
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'
onClick={openCreateFromScratch}
onClick={handleCreate}
>
<div className='flex items-center gap-x-3 p-4 pb-2'>
<div className='flex size-10 shrink-0 items-center justify-center rounded-[10px] border border-dashed border-divider-regular bg-background-section group-hover:border-state-accent-hover-alt group-hover:bg-state-accent-hover'>
@ -63,10 +51,6 @@ const CreateCard = () => {
<p className='system-xs-regular line-clamp-3 px-4 py-1 text-text-tertiary'>
{t('datasetPipeline.creation.createFromScratch.description')}
</p>
<CreateFromScratchModal
show={showCreateModal}
onClose={closeCreateFromScratch}
/>
</div>
)
}

View File

@ -36,7 +36,7 @@ const Content = ({
<Icon className='size-4' />
</div>
</div>
<div className='flex grow flex-col gap-y-1 py-px'>
<div className='flex grow flex-col gap-y-1 overflow-hidden py-px'>
<div
className='system-md-semibold truncate text-text-secondary'
title={name}

View File

@ -1,69 +0,0 @@
import Modal from '@/app/components/base/modal'
import CreateForm from '../../create-form'
import { useCallback, useEffect, useState } from 'react'
import type { CreateFormData } from '@/models/pipeline'
import { ChunkingMode, type CreateDatasetReq, DatasetPermission } from '@/models/datasets'
import type { Member } from '@/models/common'
import { useMembers } from '@/service/use-common'
type CreateModalProps = {
show: boolean
onClose: () => void
onCreate: (payload: Omit<CreateDatasetReq, 'yaml_content'>) => Promise<void>
}
const CreateModal = ({
show,
onClose,
onCreate,
}: CreateModalProps) => {
const [memberList, setMemberList] = useState<Member[]>([])
const { data: members } = useMembers()
useEffect(() => {
if (members?.accounts)
setMemberList(members.accounts)
}, [members])
const handleCreate = useCallback(async (payload: CreateFormData) => {
const { name, appIcon, description, permission, selectedMemberIDs } = payload
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,
},
doc_form: ChunkingMode.text,
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
}
onCreate(request)
}, [memberList, onCreate])
return (
<Modal
isShow={show}
onClose={onClose}
className='max-w-[520px] p-0'
>
<CreateForm
onCreate={handleCreate}
onClose={onClose}
/>
</Modal>
)
}
export default CreateModal

View File

@ -17,9 +17,7 @@ import { useRouter } from 'next/navigation'
import Details from './details'
import Content from './content'
import Actions from './actions'
import type { CreateDatasetReq } from '@/models/datasets'
import { useCreatePipelineDatasetFromCustomized } from '@/service/knowledge/use-create-dataset'
import CreateModal from './create-modal'
import { useInvalid } from '@/service/use-base'
import { useResetDatasetList } from '@/service/knowledge/use-dataset'
@ -39,7 +37,6 @@ const TemplateCard = ({
const [showEditModal, setShowEditModal] = useState(false)
const [showDeleteConfirm, setShowConfirmDelete] = useState(false)
const [showDetailModal, setShowDetailModal] = useState(false)
const [showCreateModal, setShowCreateModal] = useState(false)
const { refetch: getPipelineTemplateInfo } = usePipelineTemplateById({
template_id: pipeline.id,
@ -49,12 +46,7 @@ const TemplateCard = ({
const { handleCheckPluginDependencies } = usePluginDependencies()
const resetDatasetList = useResetDatasetList()
const openCreateModal = useCallback(() => {
setShowCreateModal(true)
}, [])
// todo: Directly create a pipeline dataset, no need to fill in the form
const handleUseTemplate = useCallback(async (payload: Omit<CreateDatasetReq, 'yaml_content'>) => {
const handleUseTemplate = useCallback(async () => {
const { data: pipelineTemplateInfo } = await getPipelineTemplateInfo()
if (!pipelineTemplateInfo) {
Toast.notify({
@ -64,7 +56,6 @@ const TemplateCard = ({
return
}
const request = {
...payload,
yaml_content: pipelineTemplateInfo.export_data,
}
await createDataset(request, {
@ -76,7 +67,6 @@ const TemplateCard = ({
resetDatasetList()
if (newDataset.pipeline_id)
await handleCheckPluginDependencies(newDataset.pipeline_id, true)
setShowCreateModal(false)
push(`/datasets/${newDataset.dataset_id}/pipeline`)
},
onError: () => {
@ -158,7 +148,7 @@ const TemplateCard = ({
chunkStructure={pipeline.chunk_structure}
/>
<Actions
onApplyTemplate={openCreateModal}
onApplyTemplate={handleUseTemplate}
handleShowTemplateDetails={handleShowTemplateDetails}
showMoreOperations={showMoreOperations}
openEditModal={openEditModal}
@ -196,18 +186,10 @@ const TemplateCard = ({
id={pipeline.id}
type={type}
onClose={closeDetailsModal}
onApplyTemplate={openCreateModal}
onApplyTemplate={handleUseTemplate}
/>
</Modal>
)}
{showCreateModal && (
<CreateModal
show={showCreateModal}
onClose={() => setShowCreateModal(false)}
onCreate={handleUseTemplate}
/>
)
}
</div>
)
}

View File

@ -752,15 +752,6 @@ export const DOC_FORM_TEXT: Record<ChunkingMode, string> = {
}
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'
}[]
yaml_content?: string
}

View File

@ -245,12 +245,12 @@ export const useFetchDefaultProcessRule = (
}
export const useCreatePipelineDataset = (
mutationOptions: MutationOptions<CreateDatasetResponse, Error, CreateDatasetReq> = {},
mutationOptions: MutationOptions<CreateDatasetResponse, Error> = {},
) => {
return useMutation({
mutationKey: [NAME_SPACE, 'create-pipeline-empty-dataset'],
mutationFn: (req: CreateDatasetReq) => {
return post<CreateDatasetResponse>('/rag/pipeline/empty-dataset', { body: req })
mutationFn: () => {
return post<CreateDatasetResponse>('/rag/pipeline/empty-dataset')
},
...mutationOptions,
})