feat: knowledge pipeline (#25360)

Signed-off-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: twwu <twwu@dify.ai>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
Co-authored-by: jyong <718720800@qq.com>
Co-authored-by: Wu Tianwei <30284043+WTW0313@users.noreply.github.com>
Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: quicksand <quicksandzn@gmail.com>
Co-authored-by: Jyong <76649700+JohnJyong@users.noreply.github.com>
Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: Yongtao Huang <yongtaoh2022@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: nite-knite <nkCoding@gmail.com>
Co-authored-by: Hanqing Zhao <sherry9277@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Harry <xh001x@hotmail.com>
This commit is contained in:
-LAN-
2025-09-18 12:49:10 +08:00
committed by GitHub
parent 7dadb33003
commit 85cda47c70
1772 changed files with 102407 additions and 31710 deletions

View File

@@ -0,0 +1,47 @@
import {
GeneralChunk,
ParentChildChunk,
QuestionAndAnswer,
} from '@/app/components/base/icons/src/vender/knowledge'
import { EffectColor, type Option } from './types'
import { ChunkingMode } from '@/models/datasets'
import { useTranslation } from 'react-i18next'
export const useChunkStructure = () => {
const { t } = useTranslation()
const GeneralOption: Option = {
id: ChunkingMode.text,
icon: <GeneralChunk className='size-[18px]' />,
iconActiveColor: 'text-util-colors-indigo-indigo-600',
title: 'General',
description: t('datasetCreation.stepTwo.generalTip'),
effectColor: EffectColor.indigo,
showEffectColor: true,
}
const ParentChildOption: Option = {
id: ChunkingMode.parentChild,
icon: <ParentChildChunk className='size-[18px]' />,
iconActiveColor: 'text-util-colors-blue-light-blue-light-500',
title: 'Parent-Child',
description: t('datasetCreation.stepTwo.parentChildTip'),
effectColor: EffectColor.blueLight,
showEffectColor: true,
}
const QuestionAnswerOption: Option = {
id: ChunkingMode.qa,
icon: <QuestionAndAnswer className='size-[18px]' />,
title: 'Q&A',
description: t('datasetCreation.stepTwo.qaTip'),
}
const options = [
GeneralOption,
ParentChildOption,
QuestionAnswerOption,
]
return {
options,
}
}

View File

@@ -0,0 +1,40 @@
import type { ChunkingMode } from '@/models/datasets'
import React from 'react'
import { useChunkStructure } from './hooks'
import OptionCard from '../option-card'
type ChunkStructureProps = {
chunkStructure: ChunkingMode
}
const ChunkStructure = ({
chunkStructure,
}: ChunkStructureProps) => {
const {
options,
} = useChunkStructure()
return (
<div className='flex flex-col gap-y-1'>
{
options.map(option => (
<OptionCard
key={option.id}
id={option.id}
icon={option.icon}
iconActiveColor={option.iconActiveColor}
title={option.title}
description={option.description}
isActive={chunkStructure === option.id}
effectColor={option.effectColor}
showEffectColor
className='gap-x-1.5 p-3 pr-4'
disabled
/>
))
}
</div>
)
}
export default React.memo(ChunkStructure)

View File

@@ -0,0 +1,18 @@
import type { ChunkingMode } from '@/models/datasets'
export enum EffectColor {
indigo = 'indigo',
blueLight = 'blue-light',
orange = 'orange',
purple = 'purple',
}
export type Option = {
id: ChunkingMode
icon?: React.ReactNode
iconActiveColor?: string
title: string
description?: string
effectColor?: EffectColor
showEffectColor?: boolean
}

View File

@@ -1,26 +1,23 @@
'use client'
import { useState } from 'react'
import { useCallback, useRef, useState } from 'react'
import { useMount } from 'ahooks'
import { useContext } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import { useSWRConfig } from 'swr'
import { unstable_serialize } from 'swr/infinite'
import PermissionSelector from '../permission-selector'
import IndexMethodRadio from '../index-method-radio'
import IndexMethod from '../index-method'
import RetrievalSettings from '../../external-knowledge-base/create/RetrievalSettings'
import { IndexingType } from '../../create/step-two'
import RetrievalMethodConfig from '@/app/components/datasets/common/retrieval-method-config'
import EconomicalRetrievalMethodConfig from '@/app/components/datasets/common/economical-retrieval-method-config'
import { ToastContext } from '@/app/components/base/toast'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
import { updateDatasetSetting } from '@/service/datasets'
import { type DataSetListResponse, DatasetPermission } from '@/models/datasets'
import DatasetDetailContext from '@/context/dataset-detail'
import type { RetrievalConfig } from '@/types/app'
import { useAppContext } from '@/context/app-context'
import type { IconInfo } from '@/models/datasets'
import { ChunkingMode, DatasetPermission } from '@/models/datasets'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import type { AppIconType, RetrievalConfig } from '@/types/app'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
import {
@@ -31,29 +28,36 @@ import type { DefaultModel } from '@/app/components/header/account-setting/model
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { fetchMembers } from '@/service/common'
import type { Member } from '@/models/common'
import AlertTriangle from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback/AlertTriangle'
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 Divider from '@/app/components/base/divider'
import ChunkStructure from '../chunk-structure'
import Toast from '@/app/components/base/toast'
import { RiAlertFill } from '@remixicon/react'
import { useDocLink } from '@/context/i18n'
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
const rowClass = 'flex'
const labelClass = `
flex items-center shrink-0 w-[180px] h-9
`
const rowClass = 'flex gap-x-1'
const labelClass = 'flex items-center shrink-0 w-[180px] h-7 pt-1'
const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => {
if (!pageIndex || previousPageData.has_more)
return { url: 'datasets', params: { page: pageIndex + 1, limit: 30 } }
return null
const DEFAULT_APP_ICON: IconInfo = {
icon_type: 'emoji',
icon: '📙',
icon_background: '#FFF4ED',
icon_url: '',
}
const Form = () => {
const { t } = useTranslation()
const docLink = useDocLink()
const { notify } = useContext(ToastContext)
const { mutate } = useSWRConfig()
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
const { dataset: currentDataset, mutateDatasetRes: mutateDatasets } = useContext(DatasetDetailContext)
const isCurrentWorkspaceDatasetOperator = useAppContextWithSelector(state => state.isCurrentWorkspaceDatasetOperator)
const currentDataset = useDatasetDetailContextWithSelector(state => state.dataset)
const mutateDatasets = useDatasetDetailContextWithSelector(state => state.mutateDatasetRes)
const [loading, setLoading] = useState(false)
const [name, setName] = useState(currentDataset?.name ?? '')
const [iconInfo, setIconInfo] = useState(currentDataset?.icon_info || DEFAULT_APP_ICON)
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
const [description, setDescription] = useState(currentDataset?.description ?? '')
const [permission, setPermission] = useState(currentDataset?.permission)
const [topK, setTopK] = useState(currentDataset?.external_retrieval_model.top_k ?? 2)
@@ -62,6 +66,7 @@ const Form = () => {
const [selectedMemberIDs, setSelectedMemberIDs] = useState<string[]>(currentDataset?.partial_member_list || [])
const [memberList, setMemberList] = useState<Member[]>([])
const [indexMethod, setIndexMethod] = useState(currentDataset?.indexing_technique)
const [keywordNumber, setKeywordNumber] = useState(currentDataset?.keyword_number ?? 10)
const [retrievalConfig, setRetrievalConfig] = useState(currentDataset?.retrieval_model_dict as RetrievalConfig)
const [embeddingModel, setEmbeddingModel] = useState<DefaultModel>(
currentDataset?.embedding_model
@@ -78,6 +83,7 @@ const Form = () => {
modelList: rerankModelList,
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank)
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
const previousAppIcon = useRef(DEFAULT_APP_ICON)
const getMembers = async () => {
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {} })
@@ -87,24 +93,46 @@ const Form = () => {
setMemberList(accounts)
}
const handleSettingsChange = (data: { top_k?: number; score_threshold?: number; score_threshold_enabled?: boolean }) => {
const handleOpenAppIconPicker = useCallback(() => {
setShowAppIconPicker(true)
previousAppIcon.current = iconInfo
}, [iconInfo])
const handleSelectAppIcon = useCallback((icon: AppIconSelection) => {
const iconInfo: IconInfo = {
icon_type: icon.type,
icon: icon.type === 'emoji' ? icon.icon : icon.fileId,
icon_background: icon.type === 'emoji' ? icon.background : undefined,
icon_url: icon.type === 'emoji' ? undefined : icon.url,
}
setIconInfo(iconInfo)
setShowAppIconPicker(false)
}, [])
const handleCloseAppIconPicker = useCallback(() => {
setIconInfo(previousAppIcon.current)
setShowAppIconPicker(false)
}, [])
const handleSettingsChange = useCallback((data: { top_k?: number; score_threshold?: number; score_threshold_enabled?: boolean }) => {
if (data.top_k !== undefined)
setTopK(data.top_k)
if (data.score_threshold !== undefined)
setScoreThreshold(data.score_threshold)
if (data.score_threshold_enabled !== undefined)
setScoreThresholdEnabled(data.score_threshold_enabled)
}
}, [])
useMount(() => {
getMembers()
})
const invalidDatasetList = useInvalidDatasetList()
const handleSave = async () => {
if (loading)
return
if (!name?.trim()) {
notify({ type: 'error', message: t('datasetSettings.form.nameError') })
Toast.notify({ type: 'error', message: t('datasetSettings.form.nameError') })
return
}
if (
@@ -114,7 +142,7 @@ const Form = () => {
indexMethod,
})
) {
notify({ type: 'error', message: t('appDebug.datasetConfig.rerankModelRequired') })
Toast.notify({ type: 'error', message: t('appDebug.datasetConfig.rerankModelRequired') })
return
}
if (retrievalConfig.weights) {
@@ -127,6 +155,8 @@ const Form = () => {
datasetId: currentDataset!.id,
body: {
name,
icon_info: iconInfo,
doc_form: currentDataset?.doc_form,
description,
permission,
indexing_technique: indexMethod,
@@ -145,6 +175,7 @@ const Form = () => {
score_threshold_enabled: scoreThresholdEnabled,
},
}),
keyword_number: keywordNumber,
},
} as any
if (permission === DatasetPermission.partialMembers) {
@@ -156,35 +187,48 @@ const Form = () => {
})
}
await updateDatasetSetting(requestParams)
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
Toast.notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
if (mutateDatasets) {
await mutateDatasets()
mutate(unstable_serialize(getKey))
invalidDatasetList()
}
}
catch {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
}
finally {
setLoading(false)
}
}
const isShowIndexMethod = currentDataset && currentDataset.doc_form !== ChunkingMode.parentChild && currentDataset.indexing_technique && indexMethod
return (
<div className='flex w-full flex-col gap-y-4 px-14 py-8 sm:w-[880px]'>
<div className='flex w-full flex-col gap-y-4 px-20 py-8 sm:w-[960px]'>
{/* Dataset name and icon */}
<div className={rowClass}>
<div className={labelClass}>
<div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.name')}</div>
<div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.nameAndIcon')}</div>
</div>
<div className='grow'>
<div className='flex grow items-center gap-x-2'>
<AppIcon
size='small'
onClick={handleOpenAppIconPicker}
className='cursor-pointer'
iconType={iconInfo.icon_type as AppIconType}
icon={iconInfo.icon}
background={iconInfo.icon_background}
imageUrl={iconInfo.icon_url}
showEditIcon
/>
<Input
disabled={!currentDataset?.embedding_available}
className='h-9'
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
</div>
{/* Dataset description */}
<div className={rowClass}>
<div className={labelClass}>
<div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.desc')}</div>
@@ -192,13 +236,14 @@ const Form = () => {
<div className='grow'>
<Textarea
disabled={!currentDataset?.embedding_available}
className='h-[120px] resize-none'
className='resize-none'
placeholder={t('datasetSettings.form.descPlaceholder') || ''}
value={description}
onChange={e => setDescription(e.target.value)}
/>
</div>
</div>
{/* Permissions */}
<div className={rowClass}>
<div className={labelClass}>
<div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.permissions')}</div>
@@ -214,136 +259,199 @@ const Form = () => {
/>
</div>
</div>
{currentDataset && currentDataset.indexing_technique && (
<>
<div className='my-1 h-0 w-full border-b border-divider-subtle' />
<div className={rowClass}>
<div className={labelClass}>
<div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.indexMethod')}</div>
</div>
<div className='grow'>
<IndexMethodRadio
disable={!currentDataset?.embedding_available}
value={indexMethod}
onChange={v => setIndexMethod(v!)}
docForm={currentDataset.doc_form}
currentValue={currentDataset.indexing_technique}
/>
{currentDataset.indexing_technique === IndexingType.ECONOMICAL && indexMethod === IndexingType.QUALIFIED && <div className='mt-2 flex h-10 items-center gap-x-0.5 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-xs backdrop-blur-[5px]'>
<div className='absolute bottom-0 left-0 right-0 top-0 bg-[linear-gradient(92deg,rgba(247,144,9,0.25)_0%,rgba(255,255,255,0.00)_100%)] opacity-40'></div>
<div className='p-1'>
<AlertTriangle className='size-4 text-text-warning-secondary' />
</div>
<span className='system-xs-medium text-text-warning-secondary'>{t('datasetSettings.form.upgradeHighQualityTip')}</span>
</div>}
</div>
</div>
</>
)}
{indexMethod === 'high_quality' && (
<>
<div className={rowClass}>
<div className={labelClass}>
<div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.embeddingModel')}</div>
</div>
<div className='grow'>
<ModelSelector
triggerClassName=''
defaultModel={embeddingModel}
modelList={embeddingModelList}
onSelect={(model: DefaultModel) => {
setEmbeddingModel(model)
}}
/>
</div>
</div>
</>
)}
{/* Retrieval Method Config */}
{currentDataset?.provider === 'external'
? <>
<div className='my-1 h-0 w-full border-b border-divider-subtle' />
<div className={rowClass}>
<div className={labelClass}>
<div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.retrievalSetting.title')}</div>
</div>
<RetrievalSettings
topK={topK}
scoreThreshold={scoreThreshold}
scoreThresholdEnabled={scoreThresholdEnabled}
onChange={handleSettingsChange}
isInRetrievalSetting={true}
{
currentDataset?.doc_form && (
<>
<Divider
type='horizontal'
className='my-1 h-px bg-divider-subtle'
/>
</div>
<div className='my-1 h-0 w-full border-b border-divider-subtle' />
<div className={rowClass}>
<div className={labelClass}>
<div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.externalKnowledgeAPI')}</div>
</div>
<div className='w-full'>
<div className='flex h-full items-center gap-1 rounded-lg bg-components-input-bg-normal px-3 py-2'>
<ApiConnectionMod className='h-4 w-4 text-text-secondary' />
<div className='system-sm-medium overflow-hidden text-ellipsis text-text-secondary'>
{currentDataset?.external_knowledge_info.external_knowledge_api_name}
</div>
<div className='system-xs-regular text-text-tertiary'>·</div>
<div className='system-xs-regular text-text-tertiary'>{currentDataset?.external_knowledge_info.external_knowledge_api_endpoint}</div>
</div>
</div>
</div>
<div className={rowClass}>
<div className={labelClass}>
<div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.externalKnowledgeID')}</div>
</div>
<div className='w-full'>
<div className='flex h-full items-center gap-1 rounded-lg bg-components-input-bg-normal px-3 py-2'>
<div className='system-xs-regular text-text-tertiary'>{currentDataset?.external_knowledge_info.external_knowledge_id}</div>
</div>
</div>
</div>
</>
: indexMethod
? <>
<div className='my-1 h-0 w-full border-b border-divider-subtle' />
{/* Chunk Structure */}
<div className={rowClass}>
<div className={labelClass}>
<div>
<div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.retrievalSetting.title')}</div>
<div className='body-xs-regular text-text-tertiary'>
<a
target='_blank'
rel='noopener noreferrer'
href={docLink('/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#setting-the-retrieval-setting', {
'zh-Hans': '/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#指定检索方式',
'ja-JP': '/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#検索方法の指定',
})}
className='text-text-accent'>
{t('datasetSettings.form.retrievalSetting.learnMore')}
</a>
{t('datasetSettings.form.retrievalSetting.description')}
</div>
<div className='flex w-[180px] shrink-0 flex-col'>
<div className='system-sm-semibold flex h-8 items-center text-text-secondary'>
{t('datasetSettings.form.chunkStructure.title')}
</div>
<div className='body-xs-regular text-text-tertiary'>
<a
target='_blank'
rel='noopener noreferrer'
href={docLink('/guides/knowledge-base/create-knowledge-and-upload-documents/chunking-and-cleaning-text')}
className='text-text-accent'
>
{t('datasetSettings.form.chunkStructure.learnMore')}
</a>
{t('datasetSettings.form.chunkStructure.description')}
</div>
</div>
<div className='grow'>
{indexMethod === IndexingType.QUALIFIED
? (
<RetrievalMethodConfig
value={retrievalConfig}
onChange={setRetrievalConfig}
/>
)
: (
<EconomicalRetrievalMethodConfig
value={retrievalConfig}
onChange={setRetrievalConfig}
/>
)}
<ChunkStructure
chunkStructure={currentDataset?.doc_form}
/>
</div>
</div>
</>
)
}
{(isShowIndexMethod || indexMethod === 'high_quality') && (
<Divider
type='horizontal'
className='my-1 h-px bg-divider-subtle'
/>
)}
{isShowIndexMethod && (
<div className={rowClass}>
<div className={labelClass}>
<div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.indexMethod')}</div>
</div>
<div className='grow'>
<IndexMethod
value={indexMethod}
disabled={!currentDataset?.embedding_available}
onChange={v => setIndexMethod(v!)}
currentValue={currentDataset.indexing_technique}
keywordNumber={keywordNumber}
onKeywordNumberChange={setKeywordNumber}
/>
{currentDataset.indexing_technique === IndexingType.ECONOMICAL && indexMethod === IndexingType.QUALIFIED && (
<div className='relative mt-2 flex h-10 items-center gap-x-0.5 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur px-2 shadow-xs shadow-shadow-shadow-3'>
<div className='absolute left-0 top-0 flex h-full w-full items-center bg-toast-warning-bg opacity-40' />
<div className='p-1'>
<RiAlertFill className='size-4 text-text-warning-secondary' />
</div>
<span className='system-xs-medium text-text-primary'>
{t('datasetSettings.form.upgradeHighQualityTip')}
</span>
</div>
)}
</div>
</div>
)}
{indexMethod === IndexingType.QUALIFIED && (
<div className={rowClass}>
<div className={labelClass}>
<div className='system-sm-semibold text-text-secondary'>
{t('datasetSettings.form.embeddingModel')}
</div>
</div>
<div className='grow'>
<ModelSelector
defaultModel={embeddingModel}
modelList={embeddingModelList}
onSelect={setEmbeddingModel}
/>
</div>
</div>
)}
{/* Retrieval Method Config */}
{currentDataset?.provider === 'external'
? (
<>
<Divider
type='horizontal'
className='my-1 h-px bg-divider-subtle'
/>
<div className={rowClass}>
<div className={labelClass}>
<div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.retrievalSetting.title')}</div>
</div>
<RetrievalSettings
topK={topK}
scoreThreshold={scoreThreshold}
scoreThresholdEnabled={scoreThresholdEnabled}
onChange={handleSettingsChange}
isInRetrievalSetting={true}
/>
</div>
<Divider
type='horizontal'
className='my-1 h-px bg-divider-subtle'
/>
<div className={rowClass}>
<div className={labelClass}>
<div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.externalKnowledgeAPI')}</div>
</div>
<div className='w-full'>
<div className='flex h-full items-center gap-1 rounded-lg bg-components-input-bg-normal px-3 py-2'>
<ApiConnectionMod className='h-4 w-4 text-text-secondary' />
<div className='system-sm-medium overflow-hidden text-ellipsis text-text-secondary'>
{currentDataset?.external_knowledge_info.external_knowledge_api_name}
</div>
<div className='system-xs-regular text-text-tertiary'>·</div>
<div className='system-xs-regular text-text-tertiary'>
{currentDataset?.external_knowledge_info.external_knowledge_api_endpoint}
</div>
</div>
</div>
</div>
<div className={rowClass}>
<div className={labelClass}>
<div className='system-sm-semibold text-text-secondary'>{t('datasetSettings.form.externalKnowledgeID')}</div>
</div>
<div className='w-full'>
<div className='flex h-full items-center gap-1 rounded-lg bg-components-input-bg-normal px-3 py-2'>
<div className='system-xs-regular text-text-tertiary'>
{currentDataset?.external_knowledge_info.external_knowledge_id}
</div>
</div>
</div>
</div>
</>
)
// eslint-disable-next-line sonarjs/no-nested-conditional
: indexMethod
? (
<>
<Divider
type='horizontal'
className='my-1 h-px bg-divider-subtle'
/>
<div className={rowClass}>
<div className={labelClass}>
<div className='flex w-[180px] shrink-0 flex-col'>
<div className='system-sm-semibold flex h-7 items-center pt-1 text-text-secondary'>
{t('datasetSettings.form.retrievalSetting.title')}
</div>
<div className='body-xs-regular text-text-tertiary'>
<a
target='_blank'
rel='noopener noreferrer'
href={docLink('/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#setting-the-retrieval-setting', {
'zh-Hans': '/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#指定检索方式',
'ja-JP': '/guides/knowledge-base/create-knowledge-and-upload-documents/setting-indexing-methods#検索方法の指定',
})}
className='text-text-accent'
>
{t('datasetSettings.form.retrievalSetting.learnMore')}
</a>
{t('datasetSettings.form.retrievalSetting.description')}
</div>
</div>
</div>
<div className='grow'>
{indexMethod === IndexingType.QUALIFIED
? (
<RetrievalMethodConfig
value={retrievalConfig}
onChange={setRetrievalConfig}
/>
)
: (
<EconomicalRetrievalMethodConfig
value={retrievalConfig}
onChange={setRetrievalConfig}
/>
)}
</div>
</div>
</>
)
: null
}
<div className='my-1 h-0 w-full border-b border-divider-subtle' />
<Divider
type='horizontal'
className='my-1 h-px bg-divider-subtle'
/>
<div className={rowClass}>
<div className={labelClass} />
<div className='grow'>
@@ -358,6 +466,12 @@ const Form = () => {
</Button>
</div>
</div>
{showAppIconPicker && (
<AppIconPicker
onSelect={handleSelectAppIcon}
onClose={handleCloseAppIconPicker}
/>
)}
</div>
)
}

View File

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="24" rx="8" fill="#EEF4FF"/>
<path d="M11.6665 9.99998C12.9552 9.99998 13.9998 8.95531 13.9998 7.66665C13.9998 6.37798 12.9552 5.33331 11.6665 5.33331C10.3778 5.33331 9.33317 6.37798 9.33317 7.66665C9.33317 8.95531 10.3778 9.99998 11.6665 9.99998Z" fill="#444CE7"/>
<path d="M8.65017 9.75198C8.49106 9.52227 8.4115 9.40741 8.32581 9.36947C8.24888 9.33541 8.1679 9.33124 8.08788 9.35723C7.99875 9.38618 7.92865 9.46797 7.78845 9.63154C7.22585 10.2879 6.84213 11.1027 6.71374 12.0001H6.6665C6.29831 12.0001 5.99984 11.7016 5.99984 11.3334C5.99984 11.0875 6.13265 10.8718 6.33365 10.7555C6.65236 10.5712 6.76127 10.1634 6.57691 9.84466C6.39255 9.52595 5.98473 9.41704 5.66602 9.60141C5.06996 9.94621 4.6665 10.5923 4.6665 11.3334C4.6665 12.438 5.56193 13.3334 6.6665 13.3334H6.71377C6.85773 14.3389 7.32239 15.2415 7.9998 15.9328L7.99979 17.4822C7.99976 17.5616 7.99973 17.6565 8.00655 17.74C8.01446 17.8368 8.03476 17.9755 8.10879 18.1208C8.20466 18.309 8.35764 18.4619 8.54581 18.5578C8.6911 18.6318 8.82976 18.6521 8.92658 18.6601C9.0101 18.6669 9.10492 18.6668 9.18432 18.6668H10.4819C10.5613 18.6668 10.6562 18.6669 10.7397 18.6601C10.8365 18.6521 10.9752 18.6318 11.1205 18.5578C11.3086 18.4619 11.4616 18.309 11.5575 18.1208C11.6315 17.9755 11.6518 17.8368 11.6597 17.74C11.6665 17.6565 11.6665 17.5616 11.6665 17.4822L11.6665 17.3335H12.3331L12.3331 17.482C12.3331 17.5614 12.3331 17.6562 12.3399 17.7398C12.3478 17.8366 12.3681 17.9753 12.4421 18.1205C12.538 18.3087 12.691 18.4617 12.8791 18.5576C13.0244 18.6316 13.1631 18.6519 13.2599 18.6598C13.3434 18.6666 13.4382 18.6666 13.5176 18.6666H14.8153C14.8947 18.6666 14.9896 18.6666 15.0731 18.6598C15.1699 18.6519 15.3085 18.6316 15.4538 18.5576C15.642 18.4617 15.795 18.3087 15.8909 18.1205C15.9649 17.9753 15.9852 17.8366 15.9931 17.7398C15.9999 17.6562 15.9999 17.5614 15.9999 17.482L15.9999 16.884C16.7373 16.5337 17.3676 15.9963 17.83 15.3332L18.1486 15.3332C18.228 15.3333 18.3229 15.3333 18.4064 15.3265C18.5032 15.3186 18.6419 15.2983 18.7872 15.2242C18.9753 15.1284 19.1283 14.9754 19.2242 14.7872C19.2982 14.6419 19.3185 14.5033 19.3264 14.4064C19.3333 14.3229 19.3332 14.2281 19.3332 14.1487V11.8424C19.3332 11.7668 19.3332 11.6765 19.327 11.5968C19.3199 11.5047 19.3015 11.3725 19.2341 11.2326C19.1358 11.0286 18.9712 10.8639 18.7671 10.7656C18.6272 10.6982 18.4951 10.6799 18.403 10.6727C18.3435 10.6681 18.2781 10.6669 18.2173 10.6666C17.9935 10.1955 17.6934 9.76819 17.3332 9.40057L17.3332 8.68818C17.3332 8.58496 17.3333 8.46813 17.3243 8.36763C17.3144 8.25667 17.2886 8.08512 17.1832 7.91509C17.0519 7.70309 16.846 7.54783 16.6061 7.47976C16.4137 7.42517 16.2416 7.44747 16.1322 7.46844C16.0331 7.48743 15.9208 7.51956 15.8216 7.54795L15.7177 7.57763C15.581 7.61667 15.5127 7.6362 15.4646 7.67139C15.4182 7.70536 15.3899 7.73885 15.364 7.7902C15.3371 7.84338 15.3279 7.92449 15.3094 8.08672C15.101 9.91393 13.5495 11.3333 11.6665 11.3333C10.4162 11.3333 9.31201 10.7075 8.65017 9.75198Z" fill="#444CE7"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,12 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="24" rx="8" fill="#FFF6ED"/>
<path d="M11.9998 4.66669C12.368 4.66669 12.6665 4.96516 12.6665 5.33335V6.66669C12.6665 7.03488 12.368 7.33335 11.9998 7.33335C11.6316 7.33335 11.3332 7.03488 11.3332 6.66669V5.33335C11.3332 4.96516 11.6316 4.66669 11.9998 4.66669Z" fill="#FB6514"/>
<path d="M7.75705 6.81459C7.4967 6.55424 7.07459 6.55424 6.81424 6.81459C6.55389 7.07494 6.55389 7.49705 6.81424 7.75739L7.75705 8.7002C8.0174 8.96055 8.43951 8.96055 8.69986 8.7002C8.96021 8.43985 8.96021 8.01774 8.69986 7.75739L7.75705 6.81459Z" fill="#FB6514"/>
<path d="M4.6665 12C4.6665 11.6318 4.96498 11.3334 5.33317 11.3334H6.6665C7.03469 11.3334 7.33317 11.6318 7.33317 12C7.33317 12.3682 7.03469 12.6667 6.6665 12.6667H5.33317C4.96498 12.6667 4.6665 12.3682 4.6665 12Z" fill="#FB6514"/>
<path d="M17.3332 11.3334C16.965 11.3334 16.6665 11.6318 16.6665 12C16.6665 12.3682 16.965 12.6667 17.3332 12.6667H18.6665C19.0347 12.6667 19.3332 12.3682 19.3332 12C19.3332 11.6318 19.0347 11.3334 18.6665 11.3334H17.3332Z" fill="#FB6514"/>
<path d="M16.2424 15.2998C15.982 15.0394 15.5599 15.0394 15.2996 15.2998C15.0392 15.5601 15.0392 15.9822 15.2996 16.2426L16.2424 17.1854C16.5027 17.4457 16.9249 17.4457 17.1852 17.1854C17.4456 16.925 17.4456 16.5029 17.1852 16.2426L16.2424 15.2998Z" fill="#FB6514"/>
<path d="M17.1852 7.75739C17.4456 7.49705 17.4456 7.07494 17.1852 6.81459C16.9249 6.55424 16.5027 6.55424 16.2424 6.81459L15.2996 7.75739C15.0392 8.01774 15.0392 8.43985 15.2996 8.7002C15.5599 8.96055 15.982 8.96055 16.2424 8.7002L17.1852 7.75739Z" fill="#FB6514"/>
<path d="M11.9998 16.6667C12.368 16.6667 12.6665 16.9652 12.6665 17.3334V18.6667C12.6665 19.0349 12.368 19.3334 11.9998 19.3334C11.6316 19.3334 11.3332 19.0349 11.3332 18.6667V17.3334C11.3332 16.9652 11.6316 16.6667 11.9998 16.6667Z" fill="#FB6514"/>
<path d="M8.69986 16.2426C8.96021 15.9822 8.96021 15.5601 8.69986 15.2998C8.43951 15.0394 8.0174 15.0394 7.75705 15.2998L6.81424 16.2426C6.55389 16.5029 6.55389 16.925 6.81424 17.1854C7.07459 17.4457 7.4967 17.4457 7.75705 17.1854L8.69986 16.2426Z" fill="#FB6514"/>
<path d="M12.5977 8.3716C12.4853 8.14407 12.2536 8.00002 11.9999 8.00002C11.7461 8.00002 11.5144 8.14407 11.4021 8.3716L10.527 10.1443L8.5701 10.4304C8.31906 10.4671 8.11061 10.6431 8.03236 10.8844C7.95411 11.1257 8.01962 11.3906 8.20137 11.5676L9.61684 12.9463L9.28278 14.894C9.23988 15.1441 9.34271 15.3969 9.54803 15.5461C9.75335 15.6952 10.0255 15.7149 10.2502 15.5967L11.9999 14.6766L13.7496 15.5967C13.9742 15.7149 14.2464 15.6952 14.4517 15.5461C14.657 15.3969 14.7598 15.1441 14.7169 14.894L14.3829 12.9463L15.7983 11.5676C15.9801 11.3906 16.0456 11.1257 15.9674 10.8844C15.8891 10.6431 15.6806 10.4671 15.4296 10.4304L13.4727 10.1443L12.5977 8.3716Z" fill="#FB6514"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,106 +0,0 @@
'use client'
import { useTranslation } from 'react-i18next'
import Image from 'next/image'
import { useRef } from 'react'
import { useHover } from 'ahooks'
import { IndexingType } from '../../create/step-two'
import { OptionCard } from '../../create/step-two/option-card'
import { indexMethodIcon } from '../../create/icons'
import classNames from '@/utils/classnames'
import type { DataSet } from '@/models/datasets'
import { ChunkingMode } from '@/models/datasets'
import Badge from '@/app/components/base/badge'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
type IIndexMethodRadioProps = {
value?: DataSet['indexing_technique']
onChange: (v?: DataSet['indexing_technique']) => void
disable?: boolean
docForm?: ChunkingMode
currentValue?: DataSet['indexing_technique']
}
const IndexMethodRadio = ({
value,
onChange,
disable,
docForm,
currentValue,
}: IIndexMethodRadioProps) => {
const { t } = useTranslation()
const economyDomRef = useRef<HTMLDivElement>(null)
const isHoveringEconomy = useHover(economyDomRef)
const isEconomyDisabled = currentValue === IndexingType.QUALIFIED
const options = [
{
key: 'high_quality',
text: <div className='flex items-center'>
{t('datasetCreation.stepTwo.qualified')}
<Badge uppercase className='ml-auto border-text-accent-secondary text-text-accent-secondary'>
{t('datasetCreation.stepTwo.recommend')}
</Badge>
</div>,
desc: t('datasetSettings.form.indexMethodHighQualityTip'),
},
{
key: 'economy',
text: t('datasetSettings.form.indexMethodEconomy'),
desc: t('datasetSettings.form.indexMethodEconomyTip'),
},
]
return (
<div className={classNames('flex w-full justify-between gap-2')}>
{
options.map((option) => {
const isParentChild = docForm === ChunkingMode.parentChild
return (
<PortalToFollowElem
key={option.key}
open={
isHoveringEconomy && option.key === 'economy'
}
placement={'top'}
>
<PortalToFollowElemTrigger>
<OptionCard
disabled={
disable
|| (isEconomyDisabled && option.key === IndexingType.ECONOMICAL)
}
isActive={option.key === value}
onSwitched={() => {
if (isParentChild && option.key === IndexingType.ECONOMICAL)
return
if (isEconomyDisabled && option.key === IndexingType.ECONOMICAL)
return
if (!disable)
onChange(option.key as DataSet['indexing_technique'])
} }
icon={
<Image
src={option.key === 'high_quality' ? indexMethodIcon.high_quality : indexMethodIcon.economical}
alt={option.desc}
/>
}
title={option.text}
description={option.desc}
ref={option.key === 'economy' ? economyDomRef : undefined}
className={classNames((isEconomyDisabled && option.key === 'economy') && 'cursor-not-allowed')}
>
</OptionCard>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 60 }}>
<div className='rounded-lg border-components-panel-border bg-components-tooltip-bg p-3 text-xs font-medium text-text-secondary shadow-lg'>
{t('datasetSettings.form.indexMethodChangeToEconomyDisabledTip')}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
})
}
</div>
)
}
export default IndexMethodRadio

View File

@@ -0,0 +1,94 @@
'use client'
import { useTranslation } from 'react-i18next'
import { useRef } from 'react'
import { useHover } from 'ahooks'
import { IndexingType } from '../../create/step-two'
import classNames from '@/utils/classnames'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { Economic, HighQuality } from '@/app/components/base/icons/src/vender/knowledge'
import { EffectColor } from '../chunk-structure/types'
import OptionCard from '../option-card'
import KeywordNumber from './keyword-number'
type IndexMethodProps = {
value: IndexingType
onChange: (id: IndexingType) => void
disabled?: boolean
currentValue?: IndexingType
keywordNumber: number
onKeywordNumberChange: (value: number) => void
}
const IndexMethod = ({
value,
onChange,
disabled,
currentValue,
keywordNumber,
onKeywordNumberChange,
}: IndexMethodProps) => {
const { t } = useTranslation()
const economyDomRef = useRef<HTMLDivElement>(null)
const isHoveringEconomy = useHover(economyDomRef)
const isEconomyDisabled = currentValue === IndexingType.QUALIFIED
return (
<div className={classNames('flex flex-col gap-y-2')}>
{/* High Quality */}
<OptionCard
id={IndexingType.QUALIFIED}
isActive={value === IndexingType.QUALIFIED}
onClick={onChange}
icon={<HighQuality className='size-[18px]' />}
iconActiveColor='text-util-colors-orange-orange-500'
title={t('datasetCreation.stepTwo.qualified')}
description={t('datasetSettings.form.indexMethodHighQualityTip')}
disabled={disabled}
isRecommended
effectColor={EffectColor.orange}
showEffectColor
className='gap-x-2'
/>
{/* Economy */}
<PortalToFollowElem
open={isHoveringEconomy}
offset={4}
placement={'right'}
>
<PortalToFollowElemTrigger>
<OptionCard
ref={economyDomRef}
id={IndexingType.ECONOMICAL}
isActive={value === IndexingType.ECONOMICAL}
onClick={onChange}
icon={<Economic className='size-[18px]' />}
iconActiveColor='text-util-colors-indigo-indigo-600'
title={t('datasetSettings.form.indexMethodEconomy')}
description={t('datasetSettings.form.indexMethodEconomyTip', { count: keywordNumber })}
disabled={disabled || isEconomyDisabled}
effectColor={EffectColor.indigo}
showEffectColor
showChildren
className='gap-x-2'
>
<KeywordNumber
keywordNumber={keywordNumber}
onKeywordNumberChange={onKeywordNumberChange}
/>
</OptionCard>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent style={{ zIndex: 60 }}>
<div className='rounded-lg border-components-panel-border bg-components-tooltip-bg p-3 text-xs font-medium text-text-secondary shadow-lg'>
{t('datasetSettings.form.indexMethodChangeToEconomyDisabledTip')}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
)
}
export default IndexMethod

View File

@@ -0,0 +1,52 @@
import { InputNumber } from '@/app/components/base/input-number'
import Slider from '@/app/components/base/slider'
import Tooltip from '@/app/components/base/tooltip'
import { RiQuestionLine } from '@remixicon/react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
type KeyWordNumberProps = {
keywordNumber: number
onKeywordNumberChange: (value: number) => void
}
const KeyWordNumber = ({
keywordNumber,
onKeywordNumberChange,
}: KeyWordNumberProps) => {
const { t } = useTranslation()
const handleInputChange = useCallback((value: number | undefined) => {
if (value)
onKeywordNumberChange(value)
}, [onKeywordNumberChange])
return (
<div className='flex items-center gap-x-1'>
<div className='flex grow items-center gap-x-0.5'>
<div className='system-xs-medium truncate text-text-secondary'>
{t('datasetSettings.form.numberOfKeywords')}
</div>
<Tooltip
popupContent='number of keywords'
>
<RiQuestionLine className='h-3.5 w-3.5 text-text-quaternary' />
</Tooltip>
</div>
<Slider
className='mr-3 w-[206px] shrink-0'
value={keywordNumber}
max={50}
onChange={onKeywordNumberChange}
/>
<InputNumber
wrapperClassName='shrink-0 w-12'
type='number'
value={keywordNumber}
onChange={handleInputChange}
/>
</div>
)
}
export default React.memo(KeyWordNumber)

View File

@@ -0,0 +1,120 @@
import type { ReactNode } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
import Badge from '@/app/components/base/badge'
import { useTranslation } from 'react-i18next'
import { EffectColor } from './chunk-structure/types'
import { ArrowShape } from '../../base/icons/src/vender/knowledge'
const HEADER_EFFECT_MAP: Record<EffectColor, string> = {
[EffectColor.indigo]: 'bg-util-colors-indigo-indigo-600 opacity-50',
[EffectColor.blueLight]: 'bg-util-colors-blue-light-blue-light-600 opacity-80',
[EffectColor.orange]: 'bg-util-colors-orange-orange-500 opacity-50',
[EffectColor.purple]: 'bg-util-colors-purple-purple-600 opacity-80',
}
type OptionCardProps<T> = {
id: T
className?: string
isActive?: boolean
icon?: ReactNode
iconActiveColor?: string
title: string
description?: string
isRecommended?: boolean
effectColor?: EffectColor
showEffectColor?: boolean
disabled?: boolean
onClick?: (id: T) => void
children?: ReactNode
showChildren?: boolean
ref?: React.Ref<HTMLDivElement>
}
const OptionCard = <T,>({
id,
className,
isActive,
icon,
iconActiveColor,
title,
description,
isRecommended,
effectColor,
showEffectColor,
disabled,
onClick,
children,
showChildren,
ref,
}: OptionCardProps<T>) => {
const { t } = useTranslation()
return (
<div
ref={ref}
className={cn(
'cursor-pointer overflow-hidden rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg',
isActive && 'border border-components-option-card-option-selected-border ring-[1px] ring-components-option-card-option-selected-border',
disabled && 'cursor-not-allowed opacity-50',
)}
onClick={() => {
if (isActive || disabled) return
onClick?.(id)
}}
>
<div className={cn(
'relative flex rounded-t-xl p-2',
className,
)}>
{
effectColor && showEffectColor && (
<div className={cn(
'absolute left-[-2px] top-[-2px] h-14 w-14 rounded-full blur-[80px]',
`${HEADER_EFFECT_MAP[effectColor]}`,
)} />
)
}
{
icon && (
<div className={cn(
'flex size-6 shrink-0 items-center justify-center text-text-tertiary',
isActive && iconActiveColor,
)}>
{icon}
</div>
)
}
<div className='flex grow flex-col gap-y-0.5 py-px'>
<div className='flex items-center gap-x-1'>
<span className='system-sm-medium text-text-secondary'>
{title}
</span>
{
isRecommended && (
<Badge className='h-[18px] border-text-accent-secondary text-text-accent-secondary'>
{t('datasetCreation.stepTwo.recommend')}
</Badge>
)
}
</div>
{
description && (
<div className='system-xs-regular text-text-tertiary'>
{description}
</div>
)
}
</div>
</div>
{
children && showChildren && (
<div className='relative rounded-b-xl bg-components-panel-bg p-4'>
<ArrowShape className='absolute left-[14px] top-[-11px] size-4 text-components-panel-bg' />
{children}
</div>
)
}
</div>
)
}
export default React.memo(OptionCard) as typeof OptionCard

View File

@@ -1,8 +1,8 @@
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import React, { useMemo, useState } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import { useDebounceFn } from 'ahooks'
import { RiArrowDownSLine } from '@remixicon/react'
import { RiArrowDownSLine, RiGroup2Line, RiLock2Line } from '@remixicon/react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
@@ -10,11 +10,12 @@ import {
} from '@/app/components/base/portal-to-follow-elem'
import Avatar from '@/app/components/base/avatar'
import Input from '@/app/components/base/input'
import { Check } from '@/app/components/base/icons/src/vender/line/general'
import { Users01, UsersPlus } from '@/app/components/base/icons/src/vender/solid/users'
import { DatasetPermission } from '@/models/datasets'
import { useAppContext } from '@/context/app-context'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import type { Member } from '@/models/common'
import Item from './permission-item'
import MemberItem from './member-item'
export type RoleSelectorProps = {
disabled?: boolean
permission?: DatasetPermission
@@ -24,9 +25,16 @@ export type RoleSelectorProps = {
onMemberSelect: (v: string[]) => void
}
const PermissionSelector = ({ disabled, permission, value, memberList, onChange, onMemberSelect }: RoleSelectorProps) => {
const PermissionSelector = ({
disabled,
permission,
value,
memberList,
onChange,
onMemberSelect,
}: RoleSelectorProps) => {
const { t } = useTranslation()
const { userProfile } = useAppContext()
const userProfile = useAppContextWithSelector(state => state.userProfile)
const [open, setOpen] = useState(false)
const [keywords, setKeywords] = useState('')
@@ -38,18 +46,18 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
setKeywords(value)
handleSearch()
}
const selectMember = (member: Member) => {
const selectMember = useCallback((member: Member) => {
if (value.includes(member.id))
onMemberSelect(value.filter(v => v !== member.id))
else
onMemberSelect([...value, member.id])
}
}, [value, onMemberSelect])
const selectedMembers = useMemo(() => {
return [
userProfile,
...memberList.filter(member => member.id !== userProfile.id).filter(member => value.includes(member.id)),
].map(member => member.name).join(', ')
]
}, [userProfile, value, memberList])
const showMe = useMemo(() => {
@@ -60,9 +68,25 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
return memberList.filter(member => (member.name.includes(searchKeywords) || member.email.includes(searchKeywords)) && member.id !== userProfile.id && ['owner', 'admin', 'editor', 'dataset_operator'].includes(member.role))
}, [memberList, searchKeywords, userProfile])
const onSelectOnlyMe = useCallback(() => {
onChange(DatasetPermission.onlyMe)
setOpen(false)
}, [onChange])
const onSelectAllMembers = useCallback(() => {
onChange(DatasetPermission.allTeamMembers)
setOpen(false)
}, [onChange])
const onSelectPartialMembers = useCallback(() => {
onChange(DatasetPermission.partialMembers)
onMemberSelect([userProfile.id])
}, [onChange, onMemberSelect, userProfile])
const isOnlyMe = permission === DatasetPermission.onlyMe
const isAllTeamMembers = permission === DatasetPermission.allTeamMembers
const isPartialMembers = permission === DatasetPermission.partialMembers
const selectedMemberNames = selectedMembers.map(member => member.name).join(', ')
return (
<PortalToFollowElem
@@ -76,78 +100,118 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
onClick={() => !disabled && setOpen(v => !v)}
className='block'
>
<div className={cn('flex cursor-pointer items-center rounded-lg bg-components-input-bg-normal px-3 py-[6px] hover:bg-state-base-hover-alt',
<div className={cn('flex cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt',
open && 'bg-state-base-hover-alt',
disabled && '!cursor-not-allowed !bg-components-input-bg-disabled hover:!bg-components-input-bg-disabled',
)}>
{
isOnlyMe && (
<>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='mr-2 shrink-0' size={24} />
<div className='mr-2 grow text-sm leading-5 text-components-input-text-filled'>{t('datasetSettings.form.permissionsOnlyMe')}</div>
<div className='flex size-6 shrink-0 items-center justify-center'>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} size={20} />
</div>
<div className='system-sm-regular grow p-1 text-components-input-text-filled'>
{t('datasetSettings.form.permissionsOnlyMe')}
</div>
</>
)
}
{
isAllTeamMembers && (
<>
<div className='mr-2 flex h-6 w-6 items-center justify-center rounded-lg bg-[#EEF4FF]'>
<Users01 className='h-3.5 w-3.5 text-[#444CE7]' />
<div className='flex size-6 shrink-0 items-center justify-center'>
<RiGroup2Line className='size-4 text-text-secondary' />
</div>
<div className='system-sm-regular grow p-1 text-components-input-text-filled'>
{t('datasetSettings.form.permissionsAllMember')}
</div>
<div className='mr-2 grow text-sm leading-5 text-components-input-text-filled'>{t('datasetSettings.form.permissionsAllMember')}</div>
</>
)
}
{
isPartialMembers && (
<>
<div className='mr-2 flex h-6 w-6 items-center justify-center rounded-lg bg-[#EEF4FF]'>
<Users01 className='h-3.5 w-3.5 text-[#444CE7]' />
<div className='relative flex size-6 shrink-0 items-center justify-center'>
{
selectedMembers.length === 1 && (
<Avatar
avatar={selectedMembers[0].avatar_url}
name={selectedMembers[0].name}
size={20}
/>
)
}
{
selectedMembers.length >= 2 && (
<>
<Avatar
avatar={selectedMembers[0].avatar_url}
name={selectedMembers[0].name}
className='absolute left-0 top-0 z-0'
size={16}
/>
<Avatar
avatar={selectedMembers[1].avatar_url}
name={selectedMembers[1].name}
className='absolute bottom-0 right-0 z-10'
size={16}
/>
</>
)
}
</div>
<div
title={selectedMemberNames}
className='system-sm-regular grow truncate p-1 text-components-input-text-filled'
>
{selectedMemberNames}
</div>
<div title={selectedMembers} className='mr-2 grow truncate text-sm leading-5 text-components-input-text-filled'>{selectedMembers}</div>
</>
)
}
<RiArrowDownSLine className={cn('h-4 w-4 shrink-0 text-text-secondary', disabled && '!text-components-input-text-placeholder')} />
<RiArrowDownSLine
className={cn(
'h-4 w-4 shrink-0 text-text-quaternary group-hover:text-text-secondary',
open && 'text-text-secondary',
disabled && '!text-components-input-text-placeholder',
)}
/>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1002]'>
<div className='relative w-[480px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm'>
<div className='relative w-[480px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg shadow-shadow-shadow-5'>
<div className='p-1'>
<div className='cursor-pointer rounded-lg py-1 pl-3 pr-2 hover:bg-state-base-hover' onClick={() => {
onChange(DatasetPermission.onlyMe)
setOpen(false)
}}>
<div className='flex items-center gap-2'>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='mr-2 shrink-0' size={24} />
<div className='mr-2 grow text-sm leading-5 text-text-primary'>{t('datasetSettings.form.permissionsOnlyMe')}</div>
{isOnlyMe && <Check className='h-4 w-4 text-primary-600' />}
</div>
</div>
<div className='cursor-pointer rounded-lg py-1 pl-3 pr-2 hover:bg-state-base-hover' onClick={() => {
onChange(DatasetPermission.allTeamMembers)
setOpen(false)
}}>
<div className='flex items-center gap-2'>
<div className='mr-2 flex h-6 w-6 items-center justify-center rounded-lg bg-[#EEF4FF]'>
<Users01 className='h-3.5 w-3.5 text-[#444CE7]' />
{/* Only me */}
<Item
leftIcon={
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='shrink-0' size={24} />
}
text={t('datasetSettings.form.permissionsOnlyMe')}
onClick={onSelectOnlyMe}
isSelected={isOnlyMe}
/>
{/* All team members */}
<Item
leftIcon={
<div className='flex size-6 shrink-0 items-center justify-center'>
<RiGroup2Line className='size-4 text-text-secondary' />
</div>
<div className='mr-2 grow text-sm leading-5 text-text-primary'>{t('datasetSettings.form.permissionsAllMember')}</div>
{isAllTeamMembers && <Check className='h-4 w-4 text-primary-600' />}
</div>
</div>
<div className='cursor-pointer rounded-lg py-1 pl-3 pr-2 hover:bg-state-base-hover' onClick={() => {
onChange(DatasetPermission.partialMembers)
onMemberSelect([userProfile.id])
}}>
<div className='flex items-center gap-2'>
<div className={cn('mr-2 flex h-6 w-6 items-center justify-center rounded-lg bg-[#FFF6ED]', isPartialMembers && '!bg-[#EEF4FF]')}>
<UsersPlus className={cn('h-3.5 w-3.5 text-[#FB6514]', isPartialMembers && '!text-[#444CE7]')} />
}
text={t('datasetSettings.form.permissionsAllMember')}
onClick={onSelectAllMembers}
isSelected={isAllTeamMembers}
/>
{/* Partial members */}
<Item
leftIcon={
<div className='flex size-6 shrink-0 items-center justify-center'>
<RiLock2Line className='size-4 text-text-secondary' />
</div>
<div className='mr-2 grow text-sm leading-5 text-text-primary'>{t('datasetSettings.form.permissionsInvitedMembers')}</div>
{isPartialMembers && <Check className='h-4 w-4 text-primary-600' />}
</div>
</div>
}
text={t('datasetSettings.form.permissionsInvitedMembers')}
onClick={onSelectPartialMembers}
isSelected={isPartialMembers}
/>
</div>
{isPartialMembers && (
<div className='max-h-[360px] overflow-y-auto border-t-[1px] border-divider-regular pb-1 pl-1 pr-1'>
@@ -160,29 +224,37 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
onClear={() => handleKeywordsChange('')}
/>
</div>
{showMe && (
<div className='flex items-center gap-2 rounded-lg py-1 pl-3 pr-[10px]'>
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='shrink-0' size={24} />
<div className='grow'>
<div className='truncate text-[13px] font-medium leading-[18px] text-text-secondary'>
{userProfile.name}
<span className='text-xs font-normal text-text-tertiary'>{t('datasetSettings.form.me')}</span>
<div className='flex flex-col p-1'>
{showMe && (
<MemberItem
leftIcon={
<Avatar avatar={userProfile.avatar_url} name={userProfile.name} className='shrink-0' size={24} />
}
name={userProfile.name}
email={userProfile.email}
isSelected
isMe
/>
)}
{filteredMemberList.map(member => (
<MemberItem
leftIcon={
<Avatar avatar={member.avatar_url} name={member.name} className='shrink-0' size={24} />
}
name={member.name}
email={member.email}
isSelected={value.includes(member.id)}
onClick={selectMember.bind(null, member)}
/>
))}
{
!showMe && filteredMemberList.length === 0 && (
<div className='system-xs-regular flex items-center justify-center whitespace-pre-wrap px-1 py-6 text-center text-text-tertiary'>
{t('datasetSettings.form.onSearchResults')}
</div>
<div className='truncate text-xs leading-[18px] text-text-tertiary'>{userProfile.email}</div>
</div>
<Check className='h-4 w-4 shrink-0 text-text-accent opacity-30' />
</div>
)}
{filteredMemberList.map(member => (
<div key={member.id} className='flex cursor-pointer items-center gap-2 rounded-lg py-1 pl-3 pr-[10px] hover:bg-state-base-hover' onClick={() => selectMember(member)}>
<Avatar avatar={userProfile.avatar_url} name={member.name} className='shrink-0' size={24} />
<div className='grow'>
<div className='truncate text-[13px] font-medium leading-[18px] text-text-secondary'>{member.name}</div>
<div className='truncate text-xs leading-[18px] text-text-tertiary'>{member.email}</div>
</div>
{value.includes(member.id) && <Check className='h-4 w-4 shrink-0 text-text-accent' />}
</div>
))}
)
}
</div>
</div>
)}
</div>

View File

@@ -0,0 +1,45 @@
import cn from '@/utils/classnames'
import { RiCheckLine } from '@remixicon/react'
import React from 'react'
import { useTranslation } from 'react-i18next'
type MemberItemProps = {
leftIcon: React.ReactNode
name: string
email: string
isSelected: boolean
isMe?: boolean
onClick?: () => void
}
const MemberItem = ({
leftIcon,
name,
email,
isSelected,
isMe = false,
onClick,
}: MemberItemProps) => {
const { t } = useTranslation()
return (
<div
className='flex cursor-pointer items-center gap-2 rounded-lg py-1 pl-2 pr-[10px] hover:bg-state-base-hover'
onClick={onClick}
>
{leftIcon}
<div className='grow'>
<div className='system-sm-medium truncate text-text-secondary'>
{name}
{isMe && <span className='system-xs-regular text-text-tertiary'>
{t('datasetSettings.form.me')}
</span>}
</div>
<div className='system-xs-regular truncate text-text-tertiary'>{email}</div>
</div>
{isSelected && <RiCheckLine className={cn('size-4 shrink-0 text-text-accent', isMe && 'opacity-30')} />}
</div>
)
}
export default React.memo(MemberItem)

View File

@@ -0,0 +1,31 @@
import React from 'react'
import { RiCheckLine } from '@remixicon/react'
type PermissionItemProps = {
leftIcon: React.ReactNode
text: string
onClick: () => void
isSelected: boolean
}
const PermissionItem = ({
leftIcon,
text,
onClick,
isSelected,
}: PermissionItemProps) => {
return (
<div
className='flex cursor-pointer items-center gap-x-1 rounded-lg px-2 py-1 hover:bg-state-base-hover'
onClick={onClick}
>
{leftIcon}
<div className='system-md-regular grow px-1 text-text-secondary'>
{text}
</div>
{isSelected && <RiCheckLine className='size-4 text-text-accent' />}
</div>
)
}
export default React.memo(PermissionItem)