Update doc (#5934)
Some checks are pending
Document deploy / sync-images (push) Waiting to run
Document deploy / generate-timestamp (push) Blocked by required conditions
Document deploy / build-images (map[domain:https://fastgpt.cn suffix:cn]) (push) Blocked by required conditions
Document deploy / build-images (map[domain:https://fastgpt.io suffix:io]) (push) Blocked by required conditions
Document deploy / update-images (map[deployment:fastgpt-docs domain:https://fastgpt.cn kube_config:KUBE_CONFIG_CN suffix:cn]) (push) Blocked by required conditions
Document deploy / update-images (map[deployment:fastgpt-docs domain:https://fastgpt.io kube_config:KUBE_CONFIG_IO suffix:io]) (push) Blocked by required conditions
Build FastGPT images in Personal warehouse / get-vars (push) Waiting to run
Build FastGPT images in Personal warehouse / build-fastgpt-images (map[arch:amd64 runs-on:ubuntu-24.04]) (push) Blocked by required conditions
Build FastGPT images in Personal warehouse / build-fastgpt-images (map[arch:arm64 runs-on:ubuntu-24.04-arm]) (push) Blocked by required conditions
Build FastGPT images in Personal warehouse / release-fastgpt-images (push) Blocked by required conditions

* fix: text split

* remove test

* doc

* doc

* feat: support quick create dataset in app (#5940)

* feat: support quick create dataset in app

* doc

* perf: create dataset modal

* remove log

---------

Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer 2025-11-17 21:02:39 +08:00 committed by GitHub
parent 2c681bcdd1
commit 7b82e1dcf8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 735 additions and 265 deletions

View File

@ -57,7 +57,7 @@ https://github.com/labring/FastGPT/assets/15308462/7d3a38df-eb0e-4388-9250-2409b
- [x] 知识库单点搜索测试 - [x] 知识库单点搜索测试
- [x] 对话时反馈引用并可修改与删除 - [x] 对话时反馈引用并可修改与删除
- [x] 完整调用链路日志 - [x] 完整调用链路日志
- [ ] 应用评测 - [x] 应用评测
- [ ] 高级编排 DeBug 调试模式 - [ ] 高级编排 DeBug 调试模式
- [ ] 应用节点日志 - [ ] 应用节点日志

View File

@ -9,6 +9,7 @@ description: 'FastGPT V4.14.2 更新说明'
1. 封装底层 Agent Call 方式,支持工具连续调用时上下文的压缩,以及单个工具长响应的压缩。 1. 封装底层 Agent Call 方式,支持工具连续调用时上下文的压缩,以及单个工具长响应的压缩。
2. 模板市场新 UI。 2. 模板市场新 UI。
3. 支持 Agent 编辑页快速创建知识库。
## ⚙️ 优化 ## ⚙️ 优化
@ -19,3 +20,16 @@ description: 'FastGPT V4.14.2 更新说明'
1. 简易应用模板未正常转化。 1. 简易应用模板未正常转化。
2. 工具调用中,包含两个以上连续用户选择时候,第二个用户选择异常。 2. 工具调用中,包含两个以上连续用户选择时候,第二个用户选择异常。
3. 门户中,团队应用类型错误。
## 插件
1. 修复:子工具头像丢失。
2. 修复:模型头像丢失。
3. 修复Worker 中错误引用 mongoose 依赖,导致超过 10s 的工具运行报错。
4. 优化:开发环境热更新时,不重复上传静态文件。
5. 新增5118 SEO 关键词挖掘工具。
6. 新增Tavity 内容提取高级配置。网页站点地图工具。
7. 新增:微信公众号工具集。
8. 新增:文档对比工具。
9. 新增kimiV2 和 GPT5.1 模型预设。

View File

@ -116,7 +116,7 @@
"document/content/docs/upgrading/4-13/4132.mdx": "2025-10-21T11:46:53+08:00", "document/content/docs/upgrading/4-13/4132.mdx": "2025-10-21T11:46:53+08:00",
"document/content/docs/upgrading/4-14/4140.mdx": "2025-11-06T15:43:00+08:00", "document/content/docs/upgrading/4-14/4140.mdx": "2025-11-06T15:43:00+08:00",
"document/content/docs/upgrading/4-14/4141.mdx": "2025-11-12T12:19:02+08:00", "document/content/docs/upgrading/4-14/4141.mdx": "2025-11-12T12:19:02+08:00",
"document/content/docs/upgrading/4-14/4142.mdx": "2025-11-14T13:21:17+08:00", "document/content/docs/upgrading/4-14/4142.mdx": "2025-11-17T19:34:52+08:00",
"document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/40.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/41.mdx": "2025-08-02T19:38:37+08:00",
"document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/upgrading/4-8/42.mdx": "2025-08-02T19:38:37+08:00",

View File

@ -112,7 +112,7 @@ export type AppSimpleEditFormType = {
[NodeInputKeyEnum.aiChatJsonSchema]?: string; [NodeInputKeyEnum.aiChatJsonSchema]?: string;
}; };
dataset: { dataset: {
datasets: SelectedDatasetType; datasets: SelectedDatasetType[];
} & AppDatasetSearchParamsType; } & AppDatasetSearchParamsType;
selectedTools: FlowNodeTemplateType[]; selectedTools: FlowNodeTemplateType[];
chatConfig: AppChatConfigType; chatConfig: AppChatConfigType;

View File

@ -10,7 +10,8 @@ import type {
SearchScoreTypeEnum, SearchScoreTypeEnum,
TrainingModeEnum, TrainingModeEnum,
ChunkSettingModeEnum, ChunkSettingModeEnum,
ChunkTriggerConfigTypeEnum ChunkTriggerConfigTypeEnum,
ParagraphChunkAIModeEnum
} from './constants'; } from './constants';
import type { DatasetPermission } from '../../support/permission/dataset/controller'; import type { DatasetPermission } from '../../support/permission/dataset/controller';
import type { import type {

View File

@ -128,7 +128,7 @@ export type SelectedDatasetType = {
avatar: string; avatar: string;
name: string; name: string;
vectorModel: EmbeddingModelItemType; vectorModel: EmbeddingModelItemType;
}[]; };
/* http node */ /* http node */
export type HttpParamAndHeaderItemType = { export type HttpParamAndHeaderItemType = {

View File

@ -19,7 +19,7 @@ import { getDatasetSearchToolResponsePrompt } from '../../../../../global/core/a
import { getNodeErrResponse } from '../utils'; import { getNodeErrResponse } from '../utils';
type DatasetSearchProps = ModuleDispatchProps<{ type DatasetSearchProps = ModuleDispatchProps<{
[NodeInputKeyEnum.datasetSelectList]: SelectedDatasetType; [NodeInputKeyEnum.datasetSelectList]: SelectedDatasetType[];
[NodeInputKeyEnum.datasetSimilarity]: number; [NodeInputKeyEnum.datasetSimilarity]: number;
[NodeInputKeyEnum.datasetMaxTokens]: number; [NodeInputKeyEnum.datasetMaxTokens]: number;
[NodeInputKeyEnum.userChatInput]?: string; [NodeInputKeyEnum.userChatInput]?: string;

View File

@ -95,7 +95,7 @@ const MyModal = ({
/> />
</> </>
)} )}
<Box ml={3} color={'myGray.900'} fontWeight={'500'}> <Box ml={iconSrc ? 3 : 0} color={'myGray.900'} fontWeight={'500'}>
{title} {title}
</Box> </Box>
<Box flex={1} /> <Box flex={1} />

View File

@ -2,6 +2,7 @@
"Add_tool": "Add tool", "Add_tool": "Add tool",
"AutoOptimize": "Automatic optimization", "AutoOptimize": "Automatic optimization",
"Click_to_delete_this_field": "Click to delete this field", "Click_to_delete_this_field": "Click to delete this field",
"Create_dataset": "Create a knowledge base",
"Custom_params": "input parameters", "Custom_params": "input parameters",
"Edit_tool": "Edit tool", "Edit_tool": "Edit tool",
"Filed_is_deprecated": "This field is deprecated", "Filed_is_deprecated": "This field is deprecated",
@ -126,6 +127,10 @@
"custom_plugin_user_guide_label": "User Guide", "custom_plugin_user_guide_label": "User Guide",
"custom_plugin_user_guide_placeholder": "Use markdown syntax", "custom_plugin_user_guide_placeholder": "Use markdown syntax",
"dataset": "dataset", "dataset": "dataset",
"dataset.Select_dataset_model_tip": "Only knowledge bases with the same index model can be selected",
"dataset.create_dataset_tips": "For more advanced operations, please go to",
"dataset_create_success": "The knowledge base was created successfully and files are being indexed in the background.",
"dataset_empty_tips": "You dont have a knowledge base yet, create one first.",
"dataset_search_tool_description": "Call the \"Semantic Search\" and \"Full-text Search\" capabilities to find reference content that may be related to the problem from the \"Knowledge Base\". \nPrioritize calling this tool to assist in answering user questions.", "dataset_search_tool_description": "Call the \"Semantic Search\" and \"Full-text Search\" capabilities to find reference content that may be related to the problem from the \"Knowledge Base\". \nPrioritize calling this tool to assist in answering user questions.",
"dataset_select": "Optional knowledge base", "dataset_select": "Optional knowledge base",
"day": "Day", "day": "Day",

View File

@ -783,7 +783,6 @@
"dataset.Manual collection Tip": "Manual datasets allow you to create an empty container to hold data", "dataset.Manual collection Tip": "Manual datasets allow you to create an empty container to hold data",
"dataset.Move Failed": "Move Error", "dataset.Move Failed": "Move Error",
"dataset.Select Dataset": "Select This Dataset", "dataset.Select Dataset": "Select This Dataset",
"dataset.Select Dataset Tips": "Only Datasets with the same index model can be selected",
"dataset.Select Folder": "Enter Folder", "dataset.Select Folder": "Enter Folder",
"dataset.Training Name": "Data Training", "dataset.Training Name": "Data Training",
"dataset.collections.Collection Embedding": "{{total}} Indexes", "dataset.collections.Collection Embedding": "{{total}} Indexes",

View File

@ -2,6 +2,7 @@
"Add_tool": "添加工具", "Add_tool": "添加工具",
"AutoOptimize": "自动优化", "AutoOptimize": "自动优化",
"Click_to_delete_this_field": "点击删除该字段", "Click_to_delete_this_field": "点击删除该字段",
"Create_dataset": "创建知识库",
"Custom_params": "输入参数", "Custom_params": "输入参数",
"Edit_tool": "编辑工具", "Edit_tool": "编辑工具",
"Filed_is_deprecated": "该字段已弃用", "Filed_is_deprecated": "该字段已弃用",
@ -129,6 +130,10 @@
"custom_plugin_user_guide_label": "使用说明", "custom_plugin_user_guide_label": "使用说明",
"custom_plugin_user_guide_placeholder": "使用 markdown 语法", "custom_plugin_user_guide_placeholder": "使用 markdown 语法",
"dataset": "知识库", "dataset": "知识库",
"dataset.Select_dataset_model_tip": "仅能选择同一个索引模型的知识库",
"dataset.create_dataset_tips": "更多高级操作请前往",
"dataset_create_success": "知识库创建成功,正在后台索引文件",
"dataset_empty_tips": "你还没有知识库,先创建一个吧",
"dataset_search_tool_description": "调用“语义检索”和“全文检索”能力,从“知识库”中查找可能与问题相关的参考内容。优先调用该工具来辅助回答用户的问题。", "dataset_search_tool_description": "调用“语义检索”和“全文检索”能力,从“知识库”中查找可能与问题相关的参考内容。优先调用该工具来辅助回答用户的问题。",
"dataset_select": "可选知识库", "dataset_select": "可选知识库",
"day": "日", "day": "日",

View File

@ -786,7 +786,6 @@
"dataset.Manual collection Tip": "手动数据集允许创建一个空的容器装入数据", "dataset.Manual collection Tip": "手动数据集允许创建一个空的容器装入数据",
"dataset.Move Failed": "移动出现错误~", "dataset.Move Failed": "移动出现错误~",
"dataset.Select Dataset": "选择该知识库", "dataset.Select Dataset": "选择该知识库",
"dataset.Select Dataset Tips": "仅能选择同一个索引模型的知识库",
"dataset.Select Folder": "进入文件夹", "dataset.Select Folder": "进入文件夹",
"dataset.Training Name": "数据训练", "dataset.Training Name": "数据训练",
"dataset.collections.Collection Embedding": "{{total}} 组索引中", "dataset.collections.Collection Embedding": "{{total}} 组索引中",

View File

@ -2,6 +2,7 @@
"Add_tool": "添加工具", "Add_tool": "添加工具",
"AutoOptimize": "自動優化", "AutoOptimize": "自動優化",
"Click_to_delete_this_field": "點擊刪除該字段", "Click_to_delete_this_field": "點擊刪除該字段",
"Create_dataset": "創建知識庫",
"Custom_params": "輸入參數", "Custom_params": "輸入參數",
"Filed_is_deprecated": "該字段已棄用", "Filed_is_deprecated": "該字段已棄用",
"HTTPTools_Create_Type": "創建方式", "HTTPTools_Create_Type": "創建方式",
@ -125,6 +126,10 @@
"custom_plugin_user_guide_label": "使用說明", "custom_plugin_user_guide_label": "使用說明",
"custom_plugin_user_guide_placeholder": "使用 markdown 語法", "custom_plugin_user_guide_placeholder": "使用 markdown 語法",
"dataset": "知識庫", "dataset": "知識庫",
"dataset.Select_dataset_model_tip": "僅能選擇同一個索引模型的知識庫",
"dataset.create_dataset_tips": "更多高級操作請前往",
"dataset_create_success": "知識庫創建成功,正在後台索引文件",
"dataset_empty_tips": "你還沒有知識庫,先創建一個吧",
"dataset_search_tool_description": "呼叫「語意搜尋」和「全文搜尋」功能,從「知識庫」中尋找可能與問題相關的參考內容。優先呼叫這個工具來協助回答使用者的問題。", "dataset_search_tool_description": "呼叫「語意搜尋」和「全文搜尋」功能,從「知識庫」中尋找可能與問題相關的參考內容。優先呼叫這個工具來協助回答使用者的問題。",
"dataset_select": "可選知識庫", "dataset_select": "可選知識庫",
"day": "日", "day": "日",

View File

@ -782,7 +782,6 @@
"dataset.Manual collection Tip": "手動資料集允許建立一個空的容器來存放資料", "dataset.Manual collection Tip": "手動資料集允許建立一個空的容器來存放資料",
"dataset.Move Failed": "移動錯誤", "dataset.Move Failed": "移動錯誤",
"dataset.Select Dataset": "選擇此知識庫", "dataset.Select Dataset": "選擇此知識庫",
"dataset.Select Dataset Tips": "僅能選擇相同索引模型的知識庫",
"dataset.Select Folder": "進入資料夾", "dataset.Select Folder": "進入資料夾",
"dataset.Training Name": "資料訓練", "dataset.Training Name": "資料訓練",
"dataset.collections.Collection Embedding": "{{total}} 個索引", "dataset.collections.Collection Embedding": "{{total}} 個索引",

View File

@ -10,9 +10,10 @@ import {
VStack, VStack,
HStack, HStack,
IconButton, IconButton,
Spacer Spacer,
useDisclosure
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { ChevronRightIcon, CloseIcon, InfoIcon } from '@chakra-ui/icons'; import { ChevronRightIcon, CloseIcon } from '@chakra-ui/icons';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import type { SelectedDatasetType } from '@fastgpt/global/core/workflow/type/io'; import type { SelectedDatasetType } from '@fastgpt/global/core/workflow/type/io';
import type { DatasetListItemType } from '@fastgpt/global/core/dataset/type'; import type { DatasetListItemType } from '@fastgpt/global/core/dataset/type';
@ -26,6 +27,7 @@ import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { useDatasetSelect } from '@/components/core/dataset/SelectModal'; import { useDatasetSelect } from '@/components/core/dataset/SelectModal';
import FolderPath from '@/components/common/folder/Path'; import FolderPath from '@/components/common/folder/Path';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip'; import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import QuickCreateDatasetModal from '@/pageComponents/app/detail/components/QuickCreateModal';
// Dataset selection modal component // Dataset selection modal component
export const DatasetSelectModal = ({ export const DatasetSelectModal = ({
@ -35,19 +37,28 @@ export const DatasetSelectModal = ({
onClose onClose
}: { }: {
isOpen: boolean; isOpen: boolean;
defaultSelectedDatasets: SelectedDatasetType; defaultSelectedDatasets: SelectedDatasetType[];
onChange: (e: SelectedDatasetType) => void; onChange: (e: SelectedDatasetType[]) => void;
onClose: () => void; onClose: () => void;
}) => { }) => {
// Translation function // Translation function
const { t } = useTranslation(); const { t } = useTranslation();
// Current selected datasets, initialized with defaultSelectedDatasets // Current selected datasets, initialized with defaultSelectedDatasets
const [selectedDatasets, setSelectedDatasets] = const [selectedDatasets, setSelectedDatasets] =
useState<SelectedDatasetType>(defaultSelectedDatasets); useState<SelectedDatasetType[]>(defaultSelectedDatasets);
const { toast } = useToast(); const { toast } = useToast();
// Use server-side search, following the logic of the dataset list page // Use server-side search, following the logic of the dataset list page
const { paths, setParentId, searchKey, setSearchKey, datasets, isFetching } = useDatasetSelect(); const {
paths,
parentId,
setParentId,
searchKey,
setSearchKey,
datasets,
isFetching,
loadDatasets
} = useDatasetSelect();
// The vector model of the first selected dataset // The vector model of the first selected dataset
const activeVectorModel = selectedDatasets[0]?.vectorModel?.model; const activeVectorModel = selectedDatasets[0]?.vectorModel?.model;
@ -98,7 +109,7 @@ export const DatasetSelectModal = ({
if (isDatasetDisabled(item)) { if (isDatasetDisabled(item)) {
return toast({ return toast({
status: 'warning', status: 'warning',
title: t('common:dataset.Select Dataset Tips') title: t('app:dataset.Select_dataset_model_tip')
}); });
} }
setSelectedDatasets((prev) => [ setSelectedDatasets((prev) => [
@ -115,6 +126,15 @@ export const DatasetSelectModal = ({
} }
}; };
const {
isOpen: isQuickCreateOpen,
onOpen: onOpenQuickCreate,
onClose: onCloseQuickCreate
} = useDisclosure();
const isRootEmpty = useMemo(() => {
return datasets.length === 0 && paths.length === 0 && !searchKey && !isFetching;
}, [datasets.length, isFetching, paths.length, searchKey]);
// Render component // Render component
return ( return (
<MyModal <MyModal
@ -124,281 +144,356 @@ export const DatasetSelectModal = ({
onClose={onClose} onClose={onClose}
minW="800px" minW="800px"
maxW={'800px'} maxW={'800px'}
maxH={'90vh'}
h={'100%'} h={'100%'}
minH={'496px'} minH={'496px'}
maxH={'90vh'}
isCentered isCentered
isLoading={isFetching} isLoading={isFetching}
> >
{/* Main vertical layout */} {/* Main vertical layout */}
<Flex h="100%" direction="column" flex={1} overflow="hidden" minH={0}> <Flex h="100%" direction="column" flex={1} overflow="hidden" minH={0}>
<ModalBody flex={1} overflow="hidden" minH={0}> <ModalBody flex={1} h={0} overflow="hidden">
{/* Two-column layout */} {isRootEmpty ? (
<Grid <VStack mt={8}>
border="1px solid" <EmptyTip text={t('app:dataset_empty_tips')} py={4} />
borderColor="myGray.200" <Button onClick={onOpenQuickCreate}>{t('common:Create')}</Button>
borderRadius="md" </VStack>
gridTemplateColumns="1fr 1fr" ) : (
h="100%" <>
overflow="hidden" {/* Two-column layout */}
minH={0} <Grid
> border="1px solid"
{/* Left: search and dataset list */} borderColor="myGray.200"
<Flex borderRadius="md"
h="100%" gridTemplateColumns="1fr 1fr"
direction="column" h="100%"
borderRight="1px solid" overflow="hidden"
borderColor="myGray.200" >
py={4} {/* Left: search and dataset list */}
overflow="hidden" <Flex
minH={0} h="100%"
> direction="column"
{/* Search box */} borderRight="1px solid"
<Box mb={2} px={4}> borderColor="myGray.200"
<SearchInput py={4}
placeholder={t('app:Search_dataset')} overflow="hidden"
value={searchKey} >
onChange={(e) => setSearchKey(e.target.value?.trim())} {/* Search box */}
size="md" <Box mb={2} px={4}>
/> <SearchInput
</Box> placeholder={t('app:Search_dataset')}
value={searchKey}
{/* Path display area - always occupies space, content changes based on search state */} onChange={(e) => setSearchKey(e.target.value?.trim())}
<Box mb={2} py={1} px={4} fontSize="sm" minH={8} display="flex" alignItems="center"> size="md"
{searchKey && ( />
</Box>
{/* Path display area - always occupies space, content changes based on search state */}
<Box <Box
w="100%" mb={2}
minH={6} py={1}
px={4}
fontSize="sm"
minH={8}
display="flex" display="flex"
alignItems="center" alignItems="center"
fontSize="sm"
color="myGray.500"
> >
{t('chat:search_results')} {searchKey && (
</Box>
)}
{!searchKey && paths.length === 0 && (
// Root directory path
<Flex flex={1} alignItems="center">
<Box
fontSize={['xs', 'sm']}
py={0.5}
px={1.5}
borderRadius="sm"
maxW={['45vw', '250px']}
className="textEllipsis"
color="myGray.700"
fontWeight="bold"
cursor="pointer"
_hover={{ bg: 'myGray.100' }}
onClick={() => setParentId('')}
>
{t('common:root_folder')}
</Box>
<MyIcon name="common/line" color="myGray.500" mx={1} w="5px" />
</Flex>
)}
{!searchKey && paths.length > 0 && (
// Subdirectory path
<FolderPath
paths={paths.map((path: ParentTreePathItemType) => ({
parentId: path.parentId,
parentName: path.parentName
}))}
FirstPathDom={t('common:root_folder')}
onClick={(e) => setParentId(e)}
/>
)}
</Box>
{/* Dataset list */}
<VStack align="stretch" spacing={1.5} flex={1} px={4} overflowY="auto" h={0} minH={0}>
{datasets.length === 0 && !isFetching && (
<EmptyTip text={t('common:folder.empty')} />
)}
{datasets.map((item: DatasetListItemType) => (
<Box key={item._id} userSelect={'none'}>
<Flex
align="center"
pr={2}
pl={4}
py={1.5}
borderRadius="md"
_hover={{ bg: 'myGray.50' }}
cursor="pointer"
onClick={() => {
if (item.type === DatasetTypeEnum.folder) {
if (searchKey) {
setSearchKey('');
}
setParentId(item._id);
} else {
onSelect(item, !isDatasetSelected(item._id));
}
}}
>
<Box <Box
w={'5'} w="100%"
onClick={(e) => e.stopPropagation()} // Prevent parent click when clicking checkbox minH={6}
display="flex"
alignItems="center"
fontSize="sm"
color="myGray.500"
> >
{item.type !== DatasetTypeEnum.folder && ( {t('chat:search_results')}
<Checkbox
isChecked={isDatasetSelected(item._id)}
isDisabled={isDatasetDisabled(item)}
onChange={(e) => {
const checked = e.target.checked;
onSelect(item, checked);
}}
colorScheme="blue"
size="sm"
/>
)}
</Box> </Box>
)}
{/* Avatar */} {!searchKey && paths.length === 0 && datasets.length > 0 && (
<Avatar src={item.avatar} w={7} h={7} borderRadius="sm" ml={3} mr={2.5} /> // Root directory path
<Flex flex={1} alignItems="center">
{/* Name and type */} <Box
<Box flex={1} minW={0}> fontSize={['xs', 'sm']}
<Box fontSize="sm" color={'myGray.900'} lineHeight={1}> py={0.5}
{item.name} px={1.5}
borderRadius="sm"
maxW={['45vw', '250px']}
className="textEllipsis"
color="myGray.700"
fontWeight="bold"
cursor="pointer"
_hover={{ bg: 'myGray.100' }}
onClick={() => setParentId('')}
>
{t('common:root_folder')}
</Box> </Box>
<Box fontSize="xs" color="myGray.500"> <MyIcon name="common/line" color="myGray.500" mx={1} w="5px" />
{item.type === DatasetTypeEnum.folder ? ( </Flex>
<>{t('common:Folder')}</> )}
) : ( {!searchKey && paths.length > 0 && (
<> // Subdirectory path
{t('app:Index')}: {item.vectorModel.name} <FolderPath
</> paths={paths.map((path: ParentTreePathItemType) => ({
)} parentId: path.parentId,
</Box> parentName: path.parentName
</Box> }))}
FirstPathDom={t('common:root_folder')}
{/* Folder expand arrow */} onClick={(e) => setParentId(e)}
{item.type === DatasetTypeEnum.folder && ( />
<Box mr={10}> )}
<ChevronRightIcon w={5} h={5} color="myGray.500" strokeWidth="1px" />
</Box>
)}
</Flex>
</Box> </Box>
))} {/* Dataset list */}
</VStack> <VStack
align="stretch"
{/* Select all / Deselect all */} spacing={1.5}
{datasets.length > 0 && ( flex={1}
<Flex mt={3} px={4} justify="space-between" align="center"> px={4}
<Checkbox overflowY="auto"
isChecked={isAllSelected} h={0}
onChange={(e) => { minH={0}
if (e.target.checked) {
const compatibleDatasets = compatibleDatasetsByModel.filter((dataset) => {
return !isDatasetSelected(dataset._id);
});
const newSelections = compatibleDatasets.map(
(item: DatasetListItemType) => ({
datasetId: item._id,
avatar: item.avatar,
name: item.name,
vectorModel: item.vectorModel
})
);
setSelectedDatasets((prev) => [...prev, ...newSelections]);
} else {
const datasetIdsToRemove = compatibleDatasetsByModel.map(
(item: DatasetListItemType) => item._id
);
setSelectedDatasets((prev) =>
prev.filter((dataset) => !datasetIdsToRemove.includes(dataset.datasetId))
);
}
}}
colorScheme="blue"
size="sm"
> >
<Box fontSize="sm">{t('common:Select_all')}</Box> {datasets.length === 0 && !isFetching && (
</Checkbox> <EmptyTip text={t('common:folder.empty')} />
)}
{datasets.map((item: DatasetListItemType) => (
<Box key={item._id} userSelect={'none'}>
<Flex
align="center"
pr={2}
pl={4}
py={1.5}
borderRadius="md"
_hover={{ bg: 'myGray.50' }}
cursor="pointer"
onClick={() => {
if (item.type === DatasetTypeEnum.folder) {
if (searchKey) {
setSearchKey('');
}
setParentId(item._id);
} else {
onSelect(item, !isDatasetSelected(item._id));
}
}}
>
<Box
w={'5'}
onClick={(e) => e.stopPropagation()} // Prevent parent click when clicking checkbox
>
{item.type !== DatasetTypeEnum.folder && (
<Checkbox
isChecked={isDatasetSelected(item._id)}
isDisabled={isDatasetDisabled(item)}
onChange={(e) => {
const checked = e.target.checked;
onSelect(item, checked);
}}
colorScheme="blue"
size="sm"
/>
)}
</Box>
{/* Avatar */}
<Avatar src={item.avatar} w={7} h={7} borderRadius="sm" ml={3} mr={2.5} />
{/* Name and type */}
<Box flex={1} minW={0}>
<Box fontSize="sm" color={'myGray.900'} lineHeight={1}>
{item.name}
</Box>
<Box fontSize="xs" color="myGray.500">
{item.type === DatasetTypeEnum.folder ? (
<>{t('common:Folder')}</>
) : (
<>
{t('app:Index')}: {item.vectorModel.name}
</>
)}
</Box>
</Box>
{/* Folder expand arrow */}
{item.type === DatasetTypeEnum.folder && (
<Box mr={10}>
<ChevronRightIcon w={5} h={5} color="myGray.500" strokeWidth="1px" />
</Box>
)}
</Flex>
</Box>
))}
</VStack>
{/* Select all / Deselect all */}
{datasets.length > 0 && (
<Flex mt={3} px={4} justify="space-between" align="center">
<Checkbox
isChecked={isAllSelected}
onChange={(e) => {
if (e.target.checked) {
const compatibleDatasets = compatibleDatasetsByModel.filter(
(dataset) => {
return !isDatasetSelected(dataset._id);
}
);
const newSelections = compatibleDatasets.map(
(item: DatasetListItemType) => ({
datasetId: item._id,
avatar: item.avatar,
name: item.name,
vectorModel: item.vectorModel
})
);
setSelectedDatasets((prev) => [...prev, ...newSelections]);
} else {
const datasetIdsToRemove = compatibleDatasetsByModel.map(
(item: DatasetListItemType) => item._id
);
setSelectedDatasets((prev) =>
prev.filter(
(dataset) => !datasetIdsToRemove.includes(dataset.datasetId)
)
);
}
}}
colorScheme="blue"
size="sm"
>
<Box fontSize="sm">{t('common:Select_all')}</Box>
</Checkbox>
</Flex>
)}
</Flex> </Flex>
)}
</Flex>
{/* Right: selected datasets display */} {/* Right: selected datasets display */}
<Flex h="100%" py={4} direction="column" overflow="hidden" minH={0}> <Flex h="100%" py={4} direction="column" overflow="hidden" minH={0}>
{/* Selected count display */} {!isRootEmpty && (
<Box mb={3} px={4} fontSize="sm" color="myGray.600"> <>
{t('app:Selected')}: {selectedDatasets.length} {t('app:dataset')} {/* Selected count display */}
</Box> <Box mb={3} px={4} fontSize="sm" color="myGray.600">
{/* Selected dataset list */} {t('app:Selected')}: {selectedDatasets.length} {t('app:dataset')}
<VStack align="stretch" spacing={1} flex={1} px={4} overflowY="auto" h={0} minH={0}> </Box>
{selectedDatasets.length === 0 && !isFetching && ( {/* Selected dataset list */}
<EmptyTip text={t('app:No_selected_dataset')} /> <VStack
)} align={'stretch'}
{selectedDatasets.map((item) => ( overflowY={'auto'}
<Flex spacing={1}
key={item.datasetId} flex={1}
px={2} px={4}
py={1.5} h={0}
borderRadius="md" minH={0}
_hover={{ bg: 'myGray.50' }} >
cursor="pointer" {selectedDatasets.length === 0 && !isFetching && (
alignItems="center" <EmptyTip text={t('app:No_selected_dataset')} />
> )}
<Avatar src={item.avatar} w={6} h={6} borderRadius="sm" mr={3} /> {selectedDatasets.map((item) => (
<Box flex={1} minW={0}> <Flex
<Box fontSize="sm">{item.name}</Box> key={item.datasetId}
</Box> px={2}
<IconButton py={1.5}
aria-label="Remove" borderRadius="md"
icon={<CloseIcon w={2.5} h={2.5} />} _hover={{ bg: 'myGray.50' }}
size="xs" cursor="pointer"
variant="ghost" alignItems="center"
color="black" >
_hover={{ bg: 'myGray.200' }} <Avatar src={item.avatar} w={6} h={6} borderRadius="sm" mr={3} />
onClick={() => <Box flex={1} minW={0}>
setSelectedDatasets((prev) => <Box fontSize="sm">{item.name}</Box>
prev.filter((dataset) => dataset.datasetId !== item.datasetId) </Box>
) <IconButton
} aria-label="Remove"
/> icon={<CloseIcon w={2.5} h={2.5} />}
</Flex> size="xs"
))} variant="ghost"
</VStack> color="black"
</Flex> _hover={{ bg: 'myGray.200' }}
</Grid> onClick={() =>
setSelectedDatasets((prev) =>
prev.filter((dataset) => dataset.datasetId !== item.datasetId)
)
}
/>
</Flex>
))}
</VStack>
</>
)}
</Flex>
</Grid>
</>
)}
</ModalBody> </ModalBody>
{/* Modal footer button area */} {/* Modal footer button area */}
<ModalFooter> <ModalFooter>
<HStack spacing={4} w="full" align="center"> <HStack spacing={4} w="full" align="center">
<Spacer /> {!isRootEmpty && (
<HStack spacing={3} align="center">
<Box
px={3}
py={2}
borderRadius="md"
bg="blue.50"
display="flex"
alignItems="center"
fontSize="xs"
color="blue.600"
>
<InfoIcon w={3.5} h={3.5} color="blue.500" mr={2} />
{t('common:dataset.Select Dataset Tips')}
</Box>
<Button <Button
colorScheme="blue" leftIcon={<MyIcon name="common/addLight" w={4} />}
variant={'transparentBase'}
color={'primary.700'}
fontSize={'mini'}
onClick={onOpenQuickCreate}
>
{t('common:new_create')}
</Button>
)}
<Spacer />
{isRootEmpty ? (
<Button
px={3.5}
maxH={8}
fontSize={'mini'}
variant={'grayBase'}
onClick={() => { onClick={() => {
// Close modal and return selected datasets
onClose(); onClose();
onChange(selectedDatasets);
}} }}
> >
{t('common:Confirm')} {t('common:Cancel')}
</Button> </Button>
</HStack> ) : (
<HStack spacing={3} align="center">
<Flex
px={3}
py={1.5}
borderRadius={'sm'}
bg={'primary.50'}
alignItems={'center'}
fontSize={'11px'}
color={'primary.600'}
gap={1}
>
<MyIcon name={'common/info'} w={3.5} />
{t('app:dataset.Select_dataset_model_tip')}
</Flex>
<Button
px={3.5}
maxH={8}
fontSize={'mini'}
onClick={() => {
// Close modal and return selected datasets
onClose();
onChange(selectedDatasets);
}}
>
{t('common:Confirm')}
</Button>
</HStack>
)}
</HStack> </HStack>
</ModalFooter> </ModalFooter>
</Flex> </Flex>
{isQuickCreateOpen && (
<QuickCreateDatasetModal
parentId={parentId}
onClose={onCloseQuickCreate}
onSuccess={(newDataset) => {
setSelectedDatasets((prev) => [...prev, newDataset]);
loadDatasets();
}}
/>
)}
</MyModal> </MyModal>
); );
}; };

View File

@ -77,7 +77,8 @@ export function useDatasetSelect() {
datasets: [], datasets: [],
paths: [] paths: []
}, },
loading: isFetching loading: isFetching,
runAsync: loadDatasets
} = useRequest2( } = useRequest2(
async () => { async () => {
const result = await Promise.all([ const result = await Promise.all([
@ -105,7 +106,8 @@ export function useDatasetSelect() {
setSearchKey, setSearchKey,
datasets: data.datasets, datasets: data.datasets,
paths: data.paths, paths: data.paths,
isFetching isFetching,
loadDatasets
}; };
} }

View File

@ -36,8 +36,8 @@ export const SelectDatasetRender = React.memo(function SelectDatasetRender({
} = useDisclosure(); } = useDisclosure();
const selectedDatasets = useMemo(() => { const selectedDatasets = useMemo(() => {
if (Array.isArray(item.value)) return item.value as SelectedDatasetType; if (Array.isArray(item.value)) return item.value as SelectedDatasetType[];
return [] as SelectedDatasetType; return [] as SelectedDatasetType[];
}, [item.value]); }, [item.value]);
useEffect(() => { useEffect(() => {

View File

@ -0,0 +1,347 @@
import React, { useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import {
Box,
Flex,
ModalBody,
ModalFooter,
Button,
FormControl,
Input,
Progress
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import Avatar from '@fastgpt/web/components/common/Avatar';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useUploadAvatar } from '@fastgpt/web/common/file/hooks/useUploadAvatar';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import {
postCreateDataset,
getDatasetById,
postCreateDatasetFileCollection
} from '@/web/core/dataset/api';
import { getUploadAvatarPresignedUrl } from '@/web/common/file/api';
import { uploadFile2DB } from '@/web/common/file/controller';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import {
ChunkSettingModeEnum,
ChunkTriggerConfigTypeEnum,
DataChunkSplitModeEnum,
DatasetCollectionDataProcessModeEnum,
DatasetTypeEnum
} from '@fastgpt/global/core/dataset/constants';
import { getWebDefaultEmbeddingModel, getWebDefaultLLMModel } from '@/web/common/system/utils';
import { BucketNameEnum } from '@fastgpt/global/common/file/constants';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { formatFileSize } from '@fastgpt/global/common/file/tools';
import { getFileIcon } from '@fastgpt/global/common/file/icon';
import type { SelectedDatasetType } from '@fastgpt/global/core/workflow/type/io';
import type { ImportSourceItemType } from '@/web/core/dataset/type';
import FileSelector, {
type SelectFileItemType
} from '@/pageComponents/dataset/detail/Import/components/FileSelector';
import { useRouter } from 'next/router';
const QuickCreateDatasetModal = ({
onClose,
onSuccess,
parentId
}: {
onClose: () => void;
onSuccess: (dataset: SelectedDatasetType) => void;
parentId: string;
}) => {
const { t } = useTranslation();
const router = useRouter();
const { defaultModels, embeddingModelList, datasetModelList } = useSystemStore();
const defaultVectorModel =
defaultModels.embedding?.model || getWebDefaultEmbeddingModel(embeddingModelList)?.model;
const defaultAgentModel =
defaultModels.datasetTextLLM?.model || getWebDefaultLLMModel(datasetModelList)?.model;
const defaultVLLM = defaultModels.datasetImageLLM?.model;
const [selectFiles, setSelectFiles] = useState<ImportSourceItemType[]>([]);
const { register, handleSubmit, watch, setValue } = useForm({
defaultValues: {
parentId,
name: '',
avatar: 'core/dataset/commonDatasetColor'
}
});
const avatar = watch('avatar');
const { Component: AvatarUploader, handleFileSelectorOpen: handleAvatarSelectorOpen } =
useUploadAvatar(getUploadAvatarPresignedUrl, {
onSuccess: (avatarUrl: string) => {
setValue('avatar', avatarUrl);
}
});
const handleSelectFiles = (files: SelectFileItemType[]) => {
setSelectFiles((state) => [
...state,
...files.map<ImportSourceItemType>((selectFile) => {
const { fileId, file } = selectFile;
return {
id: fileId,
createStatus: 'waiting',
file,
sourceName: file.name,
sourceSize: formatFileSize(file.size),
icon: getFileIcon(file.name),
uploadedFileRate: 0
};
})
]);
};
const uploadSingleFile = async (fileItem: ImportSourceItemType, datasetId: string) => {
try {
if (!fileItem.file) return;
setSelectFiles((prev) =>
prev.map((item) => (item.id === fileItem.id ? { ...item, uploadedFileRate: 0 } : item))
);
const { fileId } = await uploadFile2DB({
file: fileItem.file,
bucketName: BucketNameEnum.dataset,
data: { datasetId },
percentListen: (percent) => {
setSelectFiles((prev) =>
prev.map((item) =>
item.id === fileItem.id
? { ...item, uploadedFileRate: Math.max(percent, item.uploadedFileRate || 0) }
: item
)
);
}
});
await postCreateDatasetFileCollection({
datasetId,
fileId,
trainingType: DatasetCollectionDataProcessModeEnum.chunk,
chunkTriggerType: ChunkTriggerConfigTypeEnum.minSize,
chunkTriggerMinSize: 1000,
chunkSettingMode: ChunkSettingModeEnum.auto,
chunkSplitMode: DataChunkSplitModeEnum.paragraph,
chunkSize: 1024,
indexSize: 512,
customPdfParse: false
});
setSelectFiles((prev) =>
prev.map((item) =>
item.id === fileItem.id ? { ...item, dbFileId: fileId, uploadedFileRate: 100 } : item
)
);
} catch (error) {
setSelectFiles((prev) =>
prev.map((item) =>
item.id === fileItem.id ? { ...item, errorMsg: getErrText(error) } : item
)
);
}
};
const { runAsync: onCreate, loading: isCreating } = useRequest2(
async (data) => {
const datasetId = await postCreateDataset({
name: data.name.trim(),
avatar: data.avatar,
intro: '',
parentId,
type: DatasetTypeEnum.dataset,
vectorModel: defaultVectorModel,
agentModel: defaultAgentModel,
vlmModel: defaultVLLM
});
if (selectFiles.length > 0) {
await Promise.all(selectFiles.map((file) => uploadSingleFile(file, datasetId)));
}
const datasetDetail = await getDatasetById(datasetId);
return {
datasetId,
name: datasetDetail.name,
avatar: datasetDetail.avatar,
vectorModel: datasetDetail.vectorModel
};
},
{
manual: true,
successToast: t('app:dataset_create_success'),
onSuccess: (result) => {
onSuccess(result);
onClose();
setSelectFiles([]);
}
}
);
return (
<MyModal
isOpen={true}
onClose={onClose}
title={t('app:Create_dataset')}
minW={'800px'}
ml={'20px'}
>
<ModalBody py={6} minH={'500px'}>
<Box mb={6}>
<FormLabel mb={2}>{t('common:app_icon_and_name')}</FormLabel>
<Flex alignItems={'center'}>
<MyTooltip label={t('common:set_avatar')}>
<Avatar
src={avatar}
w={9}
h={9}
mr={4}
borderRadius={'8px'}
cursor={'pointer'}
onClick={handleAvatarSelectorOpen}
/>
</MyTooltip>
<FormControl flex={1}>
<Input
{...register('name', { required: true })}
placeholder={t('common:dataset.dataset_name')}
h={8}
autoFocus
/>
</FormControl>
</Flex>
</Box>
<Box>
<FileSelector
fileType={'.txt, .docx, .csv, .xlsx, .pdf, .md, .html, .htm, .pptx, .doc, .xls, .ppt'}
selectFiles={selectFiles}
onSelectFiles={handleSelectFiles}
/>
{selectFiles.length > 0 && (
<Flex mt={6} flexDirection={'column'} gap={1.5}>
{selectFiles.map((item) => (
<Flex
key={item.id}
px={3}
py={1.5}
h={9}
alignItems={'center'}
borderRadius={'8px'}
boxShadow={
'0 1px 2px 0 rgba(19, 51, 107, 0.05), 0 0 1px 0 rgba(19, 51, 107, 0.08)'
}
gap={2}
>
<MyIcon name={item.icon as any} w={5} />
<Flex
alignItems={'center'}
w={2 / 5}
whiteSpace={'nowrap'}
overflow={'hidden'}
textOverflow={'ellipsis'}
fontSize={'14px'}
color={'myGray.900'}
mr={4}
flexShrink={0}
>
{item.sourceName}
</Flex>
<Flex w={2 / 5} pl={2} flexShrink={0}>
{item.errorMsg ? (
<MyTooltip label={item.errorMsg}>
<Flex alignItems={'center'} color={'red.500'}>
<Box mr={1} fontSize={'sm'}>
{t('common:Error')}
</Box>
<MyIcon name={'help'} w={4} />
</Flex>
</MyTooltip>
) : !!item.uploadedFileRate ? (
<Flex alignItems={'center'} fontSize={'xs'} w={'full'}>
<Progress
value={item.uploadedFileRate}
h={'4px'}
w={'100%'}
maxW={'210px'}
size="sm"
borderRadius={'20px'}
colorScheme={item.uploadedFileRate === 100 ? 'green' : 'blue'}
bg="myGray.200"
hasStripe
isAnimated
mr={4}
/>
{`${item.uploadedFileRate}%`}
</Flex>
) : null}
</Flex>
<Flex w={1 / 5} justifyContent={'end'}>
{!item.uploadedFileRate && (
<Flex alignItems={'center'} justifyContent={'center'} w={6} h={6}>
<MyIcon
name={'delete'}
w={4}
cursor={'pointer'}
_hover={{ color: 'red.500' }}
onClick={() =>
setSelectFiles((prev) =>
prev.filter((prevItem) => prevItem.id !== item.id)
)
}
/>
</Flex>
)}
</Flex>
</Flex>
))}
</Flex>
)}
</Box>
</ModalBody>
<ModalFooter justifyContent={'space-between'} fontSize={'14px'}>
<Flex fontWeight={'medium'}>
<Box color={'myGray.500'}>{t('app:dataset.create_dataset_tips')}</Box>
<Box
px={1}
cursor={'pointer'}
color={'primary.600'}
onClick={() => {
router.push('/dataset/list');
}}
>
{t('common:core.dataset.Dataset')}
</Box>
</Flex>
<Flex gap={3}>
<Button variant={'whiteBase'} onClick={onClose}>
{t('common:Cancel')}
</Button>
<Button
isLoading={isCreating}
isDisabled={selectFiles.length === 0}
onClick={handleSubmit(onCreate)}
>
{t('common:Create')}
</Button>
</Flex>
</ModalFooter>
<AvatarUploader />
</MyModal>
);
};
export default QuickCreateDatasetModal;

View File

@ -20,7 +20,6 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { TabEnum } from '../../../../../pages/dataset/detail/index';
import { import {
postCreateDatasetApiDatasetCollection, postCreateDatasetApiDatasetCollection,
postCreateDatasetExternalFileCollection, postCreateDatasetExternalFileCollection,

View File

@ -42,7 +42,7 @@ const requestLLMPargraph = async ({
rawText: string; rawText: string;
model: string; model: string;
billId: string; billId: string;
paragraphChunkAIMode: ParagraphChunkAIModeEnum; paragraphChunkAIMode?: ParagraphChunkAIModeEnum;
}) => { }) => {
if ( if (
!global.feConfigs?.isPlus || !global.feConfigs?.isPlus ||