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:
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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
|
||||
94
web/app/components/datasets/settings/index-method/index.tsx
Normal file
94
web/app/components/datasets/settings/index-method/index.tsx
Normal 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
|
||||
@@ -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)
|
||||
120
web/app/components/datasets/settings/option-card.tsx
Normal file
120
web/app/components/datasets/settings/option-card.tsx
Normal 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
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user