diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx index d9a196d854..164c2dc7ba 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx @@ -8,8 +8,8 @@ const Settings = async () => { return (
-
-
{t('title')}
+
+
{t('title')}
{t('desc')}
diff --git a/web/app/components/app-sidebar/dataset-info.tsx b/web/app/components/app-sidebar/dataset-info.tsx index e6bb15152f..ee58e11064 100644 --- a/web/app/components/app-sidebar/dataset-info.tsx +++ b/web/app/components/app-sidebar/dataset-info.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next' import AppIcon from '../base/app-icon' import Effect from '../base/effect' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' +import type { DataSet } from '@/models/datasets' import { DOC_FORM_ICON_WITH_BG, DOC_FORM_TEXT } from '@/models/datasets' import { useKnowledge } from '@/hooks/use-knowledge' import Badge from '../base/badge' @@ -20,16 +21,16 @@ const DatasetInfo: FC = ({ extraInfo, }) => { const { t } = useTranslation() - const dataset = useDatasetDetailContextWithSelector(state => state.dataset) - const iconInfo = dataset!.icon_info || { + const dataset = useDatasetDetailContextWithSelector(state => state.dataset) as DataSet + const iconInfo = dataset.icon_info || { icon: '📙', icon_type: 'emoji', icon_background: '#FFF4ED', icon_url: '', } - const isExternal = dataset!.provider === 'external' + const isExternal = dataset.provider === 'external' const { formatIndexingTechniqueAndMethod } = useKnowledge() - const Icon = isExternal ? DOC_FORM_ICON_WITH_BG.external : DOC_FORM_ICON_WITH_BG[dataset!.doc_form] + const Icon = isExternal ? DOC_FORM_ICON_WITH_BG.external : DOC_FORM_ICON_WITH_BG[dataset.doc_form] return (
@@ -53,22 +54,22 @@ const DatasetInfo: FC = ({
- {dataset!.name} + {dataset.name}
{isExternal && t('dataset.externalTag')} {!isExternal && (
- {t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset!.doc_form]}`)} - {formatIndexingTechniqueAndMethod(dataset!.indexing_technique, dataset!.retrieval_model_dict?.search_method)} + {t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`)} + {formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}
)}

- {dataset!.description} + {dataset.description}

diff --git a/web/app/components/base/effect/index.tsx b/web/app/components/base/effect/index.tsx index 666674dd08..95afb1ba5f 100644 --- a/web/app/components/base/effect/index.tsx +++ b/web/app/components/base/effect/index.tsx @@ -10,7 +10,7 @@ const Effect = ({ }: EffectProps) => { return (
) } diff --git a/web/app/components/datasets/settings/chunk-structure/hooks.tsx b/web/app/components/datasets/settings/chunk-structure/hooks.tsx new file mode 100644 index 0000000000..84bae7b58d --- /dev/null +++ b/web/app/components/datasets/settings/chunk-structure/hooks.tsx @@ -0,0 +1,42 @@ +import { + GeneralChunk, + ParentChildChunk, + QuestionAndAnswer, +} from '@/app/components/base/icons/src/vender/knowledge' +import { EffectColor, type Option } from './types' +import { ChunkingMode } from '@/models/datasets' + +export const useChunkStructure = () => { + const GeneralOption: Option = { + id: ChunkingMode.text, + icon: , + title: 'General', + description: 'General text chunking mode, the chunks retrieved and recalled are the same.', + effectColor: EffectColor.indigo, + showEffectColor: true, + } + const ParentChildOption: Option = { + id: ChunkingMode.parentChild, + icon: , + title: 'Parent-Child', + description: 'When using the parent-child mode, the child-chunk is used for retrieval and the parent-chunk is used for recall as context.', + effectColor: EffectColor.blueLight, + showEffectColor: true, + } + const QuestionAnswerOption: Option = { + id: ChunkingMode.qa, + icon: , + title: 'Q&A', + description: 'When using structured Q&A data, you can create documents that pair questions with answers. These documents are indexed based on the question portion, allowing the system to retrieve relevant answers based on query similarity', + } + + const options = [ + GeneralOption, + ParentChildOption, + QuestionAnswerOption, + ] + + return { + options, + } +} diff --git a/web/app/components/datasets/settings/chunk-structure/index.tsx b/web/app/components/datasets/settings/chunk-structure/index.tsx new file mode 100644 index 0000000000..3a65212dbc --- /dev/null +++ b/web/app/components/datasets/settings/chunk-structure/index.tsx @@ -0,0 +1,43 @@ +import type { ChunkingMode } from '@/models/datasets' +import React from 'react' +import { useChunkStructure } from './hooks' +import OptionCard from '../option-card' + +type ChunkStructureProps = { + chunkStructure: ChunkingMode + onChunkStructureChange: (value: ChunkingMode) => void +} + +const ChunkStructure = ({ + chunkStructure, + onChunkStructureChange, +}: ChunkStructureProps) => { + const { + options, + } = useChunkStructure() + + return ( +
+ { + options.map(option => ( + { + onChunkStructureChange(option.id) + }} + showHighlightBorder={chunkStructure === option.id} + effectColor={option.effectColor} + showEffectColor + className='gap-x-1.5 p-3 pr-4' + /> + )) + } +
+ ) +} + +export default React.memo(ChunkStructure) diff --git a/web/app/components/datasets/settings/chunk-structure/types.ts b/web/app/components/datasets/settings/chunk-structure/types.ts new file mode 100644 index 0000000000..64c21d65f2 --- /dev/null +++ b/web/app/components/datasets/settings/chunk-structure/types.ts @@ -0,0 +1,17 @@ +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 + title: string + description?: string + effectColor?: EffectColor + showEffectColor?: boolean +} diff --git a/web/app/components/datasets/settings/form/index.tsx b/web/app/components/datasets/settings/form/index.tsx index c36c16c815..a072c265b1 100644 --- a/web/app/components/datasets/settings/form/index.tsx +++ b/web/app/components/datasets/settings/form/index.tsx @@ -1,5 +1,5 @@ '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' @@ -11,16 +11,16 @@ import RetrievalSettings from '../../external-knowledge-base/create/RetrievalSet 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 { IconInfo } from '@/models/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 { 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,12 +31,16 @@ 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' const rowClass = 'flex' -const labelClass = ` - flex items-center shrink-0 w-[180px] h-9 -` +const labelClass = 'flex items-center shrink-0 w-[180px] h-7 pt-1' const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => { if (!pageIndex || previousPageData.has_more) @@ -44,16 +48,25 @@ const getKey = (pageIndex: number, previousPageData: DataSetListResponse) => { return null } +const DEFAULT_APP_ICON: IconInfo = { + icon_type: 'emoji', + icon: '📙', + icon_background: '#FFF4ED', + icon_url: '', +} + const Form = () => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const { mutate } = useSWRConfig() - const { isCurrentWorkspaceDatasetOperator } = useAppContext() + const isCurrentWorkspaceDatasetOperator = useAppContextWithSelector(state => state.isCurrentWorkspaceDatasetOperator) const { dataset: currentDataset, mutateDatasetRes: mutateDatasets } = useContext(DatasetDetailContext) 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 [chunkStructure, setChunkStructure] = useState(currentDataset?.doc_form) const [topK, setTopK] = useState(currentDataset?.external_retrieval_model.top_k ?? 2) const [scoreThreshold, setScoreThreshold] = useState(currentDataset?.external_retrieval_model.score_threshold ?? 0.5) const [scoreThresholdEnabled, setScoreThresholdEnabled] = useState(currentDataset?.external_retrieval_model.score_threshold_enabled ?? false) @@ -76,6 +89,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: {} }) @@ -85,14 +99,35 @@ 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() @@ -102,7 +137,7 @@ const Form = () => { 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 ( @@ -112,7 +147,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) { @@ -125,6 +160,8 @@ const Form = () => { datasetId: currentDataset!.id, body: { name, + icon_info: iconInfo, + doc_form: chunkStructure, description, permission, indexing_technique: indexMethod, @@ -154,14 +191,14 @@ 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)) } } catch { - notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) + Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) } finally { setLoading(false) @@ -169,20 +206,31 @@ const Form = () => { } return ( -
+
+ {/* Dataset name and icon */}
-
{t('datasetSettings.form.name')}
+
{t('datasetSettings.form.nameAndIcon')}
-
+
+ setName(e.target.value)} />
+ {/* Dataset description */}
{t('datasetSettings.form.desc')}
@@ -190,13 +238,14 @@ const Form = () => {