feature: 4.10.1 (#5201)

* add dynamic inputRender (#5127)

* dynamic input component

* fix

* fix

* fix

* perf: dynamic render input

* update doc

* perf: error catch

* num input ui

* fix form render (#5177)

* perf: i18n check

* add log

* doc

* Sync dataset  (#5181)

* perf: api dataset create (#5047)

* Sync dataset (#5120)

* add

* wait

* restructure dataset sync, update types and APIs, add sync hints, and remove legacy logic

* feat: add function to retrieve real file ID from third-party doc library and rename team permission check function for clarity

* fix come console

* refactor: rename team dataset limit check functions for clarity, update API dataset sync limit usage, and rename root directory to "ROOT_FOLDER"

* frat: update sync dataset login

* fix delete.ts

* feat: update pnpm-lock.yaml to include bullmq, fix comments in api.d.ts and type.d.ts, rename API file ID field, optimize dataset sync logic, and add website sync feature with related APIs

* feat: update CollectionCard to support site dataset sync, add API root ID constant and init sync API

* feat: add RootCollectionId constant to replace hardcoded root ID

---------

Co-authored-by: dreamer6680 <146868355@qq.com>

* perf: code

* feat: update success message for dataset sync, revise related i18n texts, and optimize file selection logic (#5166)

Co-authored-by: dreamer6680 <146868355@qq.com>

* perf: select file

* Sync dataset (#5180)

* feat: update success message for dataset sync, revise related i18n texts, and optimize file selection logic

* fix: make listfile function return rawid string

---------

Co-authored-by: dreamer6680 <146868355@qq.com>

* init sh

* fix: ts

---------

Co-authored-by: dreamer6680 <1468683855@qq.com>
Co-authored-by: dreamer6680 <146868355@qq.com>

* update doc

* i18n

---------

Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: dreamer6680 <1468683855@qq.com>
Co-authored-by: dreamer6680 <146868355@qq.com>
This commit is contained in:
Archer
2025-07-11 17:02:48 +08:00
committed by GitHub
parent 2865419952
commit 3a5d725efd
92 changed files with 2336 additions and 2235 deletions

View File

@@ -97,8 +97,10 @@ export const getNextTimeByCronStringAndTimezone = ({
};
const interval = cronParser.parseExpression(cronString, options);
const date = interval.next().toString();
return new Date(date);
} catch (error) {
return new Date('2099');
console.log('getNextTimeByCronStringAndTimezone error', error);
return new Date();
}
};

View File

@@ -17,7 +17,7 @@ export type JSONSchemaInputType = {
required?: string[];
};
const getNodeInputTypeFromSchemaInputType = ({
export const getNodeInputTypeFromSchemaInputType = ({
type,
arrayItems
}: {

View File

@@ -13,6 +13,7 @@ import type {
ParagraphChunkAIModeEnum
} from './constants';
import type { ParentIdType } from '../../common/parentFolder/type';
import type { APIFileItemType } from './apiDataset/type';
/* ================= dataset ===================== */
export type DatasetUpdateBody = {
@@ -57,6 +58,7 @@ export type CreateDatasetCollectionParams = DatasetCollectionStoreDataType & {
externalFileId?: string;
externalFileUrl?: string;
apiFileId?: string;
apiFileParentId?: string; //when file is imported by folder, the parentId is the folderId
rawTextLength?: number;
hashRawText?: string;
@@ -65,7 +67,6 @@ export type CreateDatasetCollectionParams = DatasetCollectionStoreDataType & {
createTime?: Date;
updateTime?: Date;
nextSyncTime?: Date;
};
export type ApiCreateDatasetCollectionParams = DatasetCollectionStoreDataType & {
@@ -83,6 +84,9 @@ export type ApiDatasetCreateDatasetCollectionParams = ApiCreateDatasetCollection
name: string;
apiFileId: string;
};
export type ApiDatasetCreateDatasetCollectionV2Params = ApiCreateDatasetCollectionParams & {
apiFiles: APIFileItemType[];
};
export type FileIdCreateDatasetCollectionParams = ApiCreateDatasetCollectionParams & {
fileId: string;
};
@@ -139,7 +143,7 @@ export type PushDatasetDataChunkProps = {
indexes?: Omit<DatasetDataIndexItemType, 'dataId'>[];
};
export type PostWebsiteSyncParams = {
export type PostDatasetSyncParams = {
datasetId: string;
};

View File

@@ -1,8 +1,9 @@
import { RequireOnlyOne } from '../../../common/type/utils';
import type { ParentIdType } from '../../../common/parentFolder/type';
export type APIFileItem = {
export type APIFileItemType = {
id: string;
rawId: string;
parentId: ParentIdType;
name: string;
type: 'file' | 'folder';
@@ -36,8 +37,6 @@ export type ApiDatasetServerType = {
// Api dataset api
export type APIFileListResponse = APIFileItem[];
export type ApiFileReadContentResponse = {
title?: string;
rawText: string;
@@ -47,8 +46,4 @@ export type APIFileReadResponse = {
url: string;
};
export type ApiDatasetDetailResponse = {
id: string;
name: string;
parentId: ParentIdType;
};
export type ApiDatasetDetailResponse = APIFileItemType;

View File

@@ -4,3 +4,5 @@ export enum CollectionSourcePrefixEnum {
link = 'link',
external = 'external'
}
export const RootCollectionId = 'SYSTEM_ROOT';

View File

@@ -106,13 +106,13 @@ export type DatasetCollectionSchemaType = ChunkSettingsType & {
// Status
forbid?: boolean;
nextSyncTime?: Date;
// Collection metadata
fileId?: string; // local file id
rawLink?: string; // link url
externalFileId?: string; //external file id
apiFileId?: string; // api file id
apiFileParentId?: string;
externalFileUrl?: string; // external import url
rawTextLength?: number;

View File

@@ -19,6 +19,8 @@ const defaultWorkerOpts: Omit<ConnectionOptions, 'connection'> = {
};
export enum QueueNames {
datasetSync = 'datasetSync',
// abondoned
websiteSync = 'websiteSync'
}

View File

@@ -1,5 +1,4 @@
import type {
APIFileListResponse,
ApiFileReadContentResponse,
APIFileReadResponse,
ApiDatasetDetailResponse,
@@ -19,6 +18,16 @@ type ResponseDataType = {
data: any;
};
type APIFileListResponse = {
id: string;
parentId: ParentIdType;
name: string;
type: 'file' | 'folder';
updateTime: Date;
createTime: Date;
hasChild?: boolean;
};
export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer }) => {
const instance = axios.create({
baseURL: apiServer.baseUrl,
@@ -106,6 +115,7 @@ export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer }
const formattedFiles = files.map((file) => ({
...file,
rawId: file.id,
hasChild: file.hasChild ?? file.type === 'folder'
}));
@@ -201,18 +211,27 @@ export const useApiDatasetRequest = ({ apiServer }: { apiServer: APIFileServer }
if (fileData) {
return {
id: fileData.id,
rawId: apiFileId,
name: fileData.name,
parentId: fileData.parentId === null ? '' : fileData.parentId
parentId: fileData.parentId === null ? '' : fileData.parentId,
type: fileData.type,
updateTime: fileData.updateTime,
createTime: fileData.createTime
};
}
return Promise.reject('File not found');
};
const getFileRawId = (fileId: string) => {
return fileId;
};
return {
getFileContent,
listFiles,
getFilePreviewUrl,
getFileDetail
getFileDetail,
getFileRawId
};
};

View File

@@ -1,5 +1,5 @@
import type {
APIFileItem,
APIFileItemType,
ApiFileReadContentResponse,
ApiDatasetDetailResponse,
FeishuServer
@@ -104,7 +104,11 @@ export const useFeishuDatasetRequest = ({ feishuServer }: { feishuServer: Feishu
.catch((err) => responseError(err));
};
const listFiles = async ({ parentId }: { parentId?: ParentIdType }): Promise<APIFileItem[]> => {
const listFiles = async ({
parentId
}: {
parentId?: ParentIdType;
}): Promise<APIFileItemType[]> => {
const fetchFiles = async (pageToken?: string): Promise<FeishuFileListResponse['files']> => {
const data = await request<FeishuFileListResponse>(
`/open-apis/drive/v1/files`,
@@ -130,6 +134,7 @@ export const useFeishuDatasetRequest = ({ feishuServer }: { feishuServer: Feishu
.filter((file) => ['folder', 'docx'].includes(file.type))
.map((file) => ({
id: file.token,
rawId: file.token,
parentId: file.parent_token,
name: file.name,
type: file.type === 'folder' ? ('folder' as const) : ('file' as const),
@@ -186,23 +191,33 @@ export const useFeishuDatasetRequest = ({ feishuServer }: { feishuServer: Feishu
}: {
apiFileId: string;
}): Promise<ApiDatasetDetailResponse> => {
const { document } = await request<{ document: { title: string } }>(
const { document } = await request<{ document: { title: string; type: string } }>(
`/open-apis/docx/v1/documents/${apiFileId}`,
{},
'GET'
);
return {
rawId: apiFileId,
name: document?.title,
parentId: null,
id: apiFileId
id: apiFileId,
type: document.type === 'folder' ? ('folder' as const) : ('file' as const),
hasChild: document.type === 'folder',
updateTime: new Date(),
createTime: new Date()
};
};
const getFileRawId = (fileId: string) => {
return fileId;
};
return {
getFileContent,
listFiles,
getFilePreviewUrl,
getFileDetail
getFileDetail,
getFileRawId
};
};

View File

@@ -1,5 +1,5 @@
import type {
APIFileItem,
APIFileItemType,
ApiFileReadContentResponse,
YuqueServer,
ApiDatasetDetailResponse
@@ -106,7 +106,7 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ
if (yuqueServer.basePath) parentId = yuqueServer.basePath;
}
let files: APIFileItem[] = [];
let files: APIFileItemType[] = [];
if (!parentId) {
const limit = 100;
@@ -133,7 +133,8 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ
files = allData.map((item) => {
return {
id: item.id,
id: String(item.id),
rawId: String(item.id),
name: item.name,
parentId: null,
type: 'folder',
@@ -144,7 +145,8 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ
};
});
} else {
if (typeof parentId === 'number') {
const numParentId = Number(parentId);
if (!isNaN(numParentId)) {
const data = await request<YuqueTocListResponse>(
`/api/v2/repos/${parentId}/toc`,
{},
@@ -155,6 +157,7 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ
.filter((item) => !item.parent_uuid && item.type !== 'LINK')
.map((item) => ({
id: `${parentId}-${item.id}-${item.uuid}`,
rawId: String(item.uuid),
name: item.title,
parentId: item.parent_uuid,
type: item.type === 'TITLE' ? ('folder' as const) : ('file' as const),
@@ -167,11 +170,11 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ
} else {
const [repoId, uuid, parentUuid] = parentId.split(/-(.*?)-(.*)/);
const data = await request<YuqueTocListResponse>(`/api/v2/repos/${repoId}/toc`, {}, 'GET');
return data
.filter((item) => item.parent_uuid === parentUuid)
.map((item) => ({
id: `${repoId}-${item.id}-${item.uuid}`,
rawId: String(item.uuid),
name: item.title,
parentId: item.parent_uuid,
type: item.type === 'TITLE' ? ('folder' as const) : ('file' as const),
@@ -207,6 +210,10 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ
'GET'
);
if (!data.title) {
return Promise.reject('Cannot find the file');
}
return {
title: data.title,
rawText: data.body
@@ -266,8 +273,13 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ
}
return {
id: file.id,
rawId: file.id,
name: file.name,
parentId: null
parentId: null,
type: file.type === 'TITLE' ? ('folder' as const) : ('file' as const),
updateTime: file.updated_at,
createTime: file.created_at,
hasChild: true
};
} else {
const [repoId, parentUuid, fileId] = apiFileId.split(/-(.*?)-(.*)/);
@@ -283,23 +295,43 @@ export const useYuqueDatasetRequest = ({ yuqueServer }: { yuqueServer: YuqueServ
if (file.parent_uuid) {
return {
id: file.id,
rawId: file.id,
name: file.title,
parentId: parentId
parentId: parentId,
type: file.type === 'TITLE' ? ('folder' as const) : ('file' as const),
updateTime: new Date(),
createTime: new Date(),
hasChild: !!file.child_uuid
};
} else {
return {
id: file.id,
rawId: file.id,
name: file.title,
parentId: repoId
parentId: repoId,
type: file.type === 'TITLE' ? ('folder' as const) : ('file' as const),
updateTime: new Date(),
createTime: new Date(),
hasChild: !!file.child_uuid
};
}
}
};
const getFileRawId = (fileId: string) => {
const [repoId, parentUuid, fileUuid] = fileId.split(/-(.*?)-(.*)/);
if (fileUuid) {
return `${fileUuid}`;
} else {
return `${repoId}`;
}
};
return {
getFileContent,
listFiles,
getFilePreviewUrl,
getFileDetail
getFileDetail,
getFileRawId
};
};

View File

@@ -180,18 +180,6 @@ export const createCollectionAndInsertData = async ({
hashRawText: rawText ? hashStr(rawText) : undefined,
rawTextLength: rawText?.length,
nextSyncTime: (() => {
// ignore auto collections sync for website datasets
if (!dataset.autoSync && dataset.type === DatasetTypeEnum.websiteDataset) return undefined;
if (
[DatasetCollectionTypeEnum.link, DatasetCollectionTypeEnum.apiFile].includes(
formatCreateCollectionParams.type
)
) {
return addDays(new Date(), 1);
}
return undefined;
})(),
session
});
@@ -285,7 +273,8 @@ export async function createOneCollection({ session, ...props }: CreateOneCollec
rawLink,
externalFileId,
externalFileUrl,
apiFileId
apiFileId,
apiFileParentId
} = props;
const collectionTags = await createOrGetCollectionTags({
@@ -310,7 +299,8 @@ export async function createOneCollection({ session, ...props }: CreateOneCollec
...(rawLink ? { rawLink } : {}),
...(externalFileId ? { externalFileId } : {}),
...(externalFileUrl ? { externalFileUrl } : {}),
...(apiFileId ? { apiFileId } : {})
...(apiFileId ? { apiFileId } : {}),
...(apiFileParentId ? { apiFileParentId } : {})
}
],
{ session, ordered: true }

View File

@@ -78,11 +78,10 @@ const DatasetCollectionSchema = new Schema({
},
forbid: Boolean,
// next sync time
nextSyncTime: Date,
// Parse settings
customPdfParse: Boolean,
apiFileParentId: String,
// Chunk settings
...ChunkSettings
@@ -112,16 +111,6 @@ try {
// create time filter
DatasetCollectionSchema.index({ teamId: 1, datasetId: 1, createTime: 1 });
// next sync time filter
DatasetCollectionSchema.index(
{ type: 1, nextSyncTime: -1 },
{
partialFilterExpression: {
nextSyncTime: { $exists: true }
}
}
);
// Get collection by external file id
DatasetCollectionSchema.index(
{ datasetId: 1, externalFileId: 1 },

View File

@@ -173,37 +173,39 @@ export const syncCollection = async (collection: CollectionWithDatasetType) => {
// Check if the original text is the same: skip if same
const hashRawText = hashStr(rawText);
if (collection.hashRawText && hashRawText === collection.hashRawText) {
return DatasetCollectionSyncResultEnum.sameRaw;
if (collection.hashRawText && hashRawText !== collection.hashRawText) {
await mongoSessionRun(async (session) => {
// Delete old collection
await delCollection({
collections: [collection],
delImg: false,
delFile: false,
session
});
// Create new collection
await createCollectionAndInsertData({
session,
dataset,
rawText: rawText,
createCollectionParams: {
...collection,
name: title || collection.name,
updateTime: new Date(),
tags: await collectionTagsToTagLabel({
datasetId: collection.datasetId,
tags: collection.tags
})
}
});
});
return DatasetCollectionSyncResultEnum.success;
} else if (collection.name !== title) {
await MongoDatasetCollection.updateOne({ _id: collection._id }, { $set: { name: title } });
return DatasetCollectionSyncResultEnum.success;
}
await mongoSessionRun(async (session) => {
// Delete old collection
await delCollection({
collections: [collection],
delImg: false,
delFile: false,
session
});
// Create new collection
await createCollectionAndInsertData({
session,
dataset,
rawText: rawText,
createCollectionParams: {
...collection,
name: title || collection.name,
updateTime: new Date(),
tags: await collectionTagsToTagLabel({
datasetId: collection.datasetId,
tags: collection.tags
})
}
});
});
return DatasetCollectionSyncResultEnum.success;
return DatasetCollectionSyncResultEnum.sameRaw;
};
/*

View File

@@ -2,11 +2,11 @@ import { type Processor } from 'bullmq';
import { getQueue, getWorker, QueueNames } from '../../../common/bullmq';
import { DatasetStatusEnum } from '@fastgpt/global/core/dataset/constants';
export type WebsiteSyncJobData = {
export type DatasetSyncJobData = {
datasetId: string;
};
export const websiteSyncQueue = getQueue<WebsiteSyncJobData>(QueueNames.websiteSync, {
export const datasetSyncQueue = getQueue<DatasetSyncJobData>(QueueNames.datasetSync, {
defaultJobOptions: {
attempts: 3, // retry 3 times
backoff: {
@@ -15,8 +15,8 @@ export const websiteSyncQueue = getQueue<WebsiteSyncJobData>(QueueNames.websiteS
}
}
});
export const getWebsiteSyncWorker = (processor: Processor<WebsiteSyncJobData>) => {
return getWorker<WebsiteSyncJobData>(QueueNames.websiteSync, processor, {
export const getDatasetSyncWorker = (processor: Processor<DatasetSyncJobData>) => {
return getWorker<DatasetSyncJobData>(QueueNames.datasetSync, processor, {
removeOnFail: {
age: 15 * 24 * 60 * 60, // Keep up to 15 days
count: 1000 // Keep up to 1000 jobs
@@ -25,21 +25,21 @@ export const getWebsiteSyncWorker = (processor: Processor<WebsiteSyncJobData>) =
});
};
export const addWebsiteSyncJob = (data: WebsiteSyncJobData) => {
export const addDatasetSyncJob = (data: DatasetSyncJobData) => {
const datasetId = String(data.datasetId);
// deduplication: make sure only 1 job
return websiteSyncQueue.add(datasetId, data, { deduplication: { id: datasetId } });
return datasetSyncQueue.add(datasetId, data, { deduplication: { id: datasetId } });
};
export const getWebsiteSyncDatasetStatus = async (datasetId: string) => {
const jobId = await websiteSyncQueue.getDeduplicationJobId(datasetId);
export const getDatasetSyncDatasetStatus = async (datasetId: string) => {
const jobId = await datasetSyncQueue.getDeduplicationJobId(datasetId);
if (!jobId) {
return {
status: DatasetStatusEnum.active,
errorMsg: undefined
};
}
const job = await websiteSyncQueue.getJob(jobId);
const job = await datasetSyncQueue.getJob(jobId);
if (!job) {
return {
status: DatasetStatusEnum.active,
@@ -76,10 +76,10 @@ export const getWebsiteSyncDatasetStatus = async (datasetId: string) => {
// Scheduler setting
const repeatDuration = 24 * 60 * 60 * 1000; // every day
export const upsertWebsiteSyncJobScheduler = (data: WebsiteSyncJobData, startDate?: number) => {
export const upsertDatasetSyncJobScheduler = (data: DatasetSyncJobData, startDate?: number) => {
const datasetId = String(data.datasetId);
return websiteSyncQueue.upsertJobScheduler(
return datasetSyncQueue.upsertJobScheduler(
datasetId,
{
every: repeatDuration,
@@ -92,10 +92,10 @@ export const upsertWebsiteSyncJobScheduler = (data: WebsiteSyncJobData, startDat
);
};
export const getWebsiteSyncJobScheduler = (datasetId: string) => {
return websiteSyncQueue.getJobScheduler(String(datasetId));
export const getDatasetSyncJobScheduler = (datasetId: string) => {
return datasetSyncQueue.getJobScheduler(String(datasetId));
};
export const removeWebsiteSyncJobScheduler = (datasetId: string) => {
return websiteSyncQueue.removeJobScheduler(String(datasetId));
export const removeDatasetSyncJobScheduler = (datasetId: string) => {
return datasetSyncQueue.removeJobScheduler(String(datasetId));
};

View File

@@ -119,7 +119,7 @@ export const checkTeamDatasetLimit = async (teamId: string) => {
}
};
export const checkTeamWebSyncPermission = async (teamId: string) => {
export const checkTeamDatasetSyncPermission = async (teamId: string) => {
const { standardConstants } = await getTeamStandPlan({
teamId
});

View File

@@ -41,7 +41,7 @@ export const useSelectFile = (props?: {
/>
</Box>
),
[fileType, maxCount, multiple, toast]
[fileType, maxCount, multiple, t, toast]
);
const onOpen = useCallback((sign?: any) => {

View File

@@ -192,7 +192,7 @@ const MultipleSelect = <T = any,>({
bg={'primary.100'}
color={'primary.700'}
type={'fill'}
borderRadius={'full'}
borderRadius={'lg'}
px={2}
py={0.5}
flexShrink={0}

View File

@@ -54,6 +54,9 @@ export type SelectProps<T = any> = Omit<ButtonProps, 'onChange'> & {
ScrollData?: ReturnType<typeof useScrollPagination>['ScrollData'];
customOnOpen?: () => void;
customOnClose?: () => void;
isInvalid?: boolean;
isDisabled?: boolean;
};
export const menuItemStyles: MenuItemProps = {
@@ -82,6 +85,8 @@ const MySelect = <T = any,>(
ScrollData,
customOnOpen,
customOnClose,
isInvalid,
isDisabled,
...props
}: SelectProps<T>,
ref: ForwardedRef<{
@@ -213,16 +218,31 @@ const MySelect = <T = any,>(
h={'auto'}
whiteSpace={'pre-wrap'}
wordBreak={'break-word'}
transition={'border-color 0.1s ease-in-out, box-shadow 0.1s ease-in-out'}
isDisabled={isDisabled}
_active={{
transform: 'none'
}}
{...(isOpen
? {
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)',
borderColor: 'primary.600',
color: 'primary.700'
}
: {})}
color={isOpen ? 'primary.700' : 'myGray.700'}
borderColor={isInvalid ? 'red.500' : isOpen ? 'primary.300' : 'myGray.200'}
boxShadow={
isOpen
? isInvalid
? '0px 0px 0px 2.4px rgba(255, 0, 0, 0.15)'
: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)'
: 'none'
}
_hover={
isInvalid
? {
borderColor: 'red.400',
boxShadow: '0px 0px 0px 2.4px rgba(255, 0, 0, 0.15)'
}
: {
borderColor: 'primary.300',
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)'
}
}
{...props}
>
<Flex alignItems={'center'} justifyContent="space-between" w="100%">

View File

@@ -230,12 +230,24 @@ const JSONEditor = ({
return (
<Box
borderWidth={isInvalid ? '2px' : '1px'}
borderRadius={'md'}
borderWidth={'1px'}
borderRadius={'sm'}
borderColor={isInvalid ? 'red.500' : 'myGray.200'}
py={2}
height={height}
position={'relative'}
transition={'border-color 0.1s ease-in-out, box-shadow 0.1s ease-in-out'}
_focusWithin={
isInvalid
? {
borderColor: 'red.500',
boxShadow: '0px 0px 0px 2.4px rgba(244, 69, 46, 0.15)'
}
: {
borderColor: 'primary.600',
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)'
}
}
{...props}
>
{resize && (
@@ -291,6 +303,19 @@ const JSONEditor = ({
>
{placeholder}
</Box>
{isDisabled && (
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
bg="rgba(255, 255, 255, 0.4)"
borderRadius="sm"
zIndex={1}
cursor="not-allowed"
/>
)}
</Box>
);
};

View File

@@ -21,6 +21,7 @@ import { VariableNode } from './plugins/VariablePlugin/node';
import type { EditorState, LexicalEditor } from 'lexical';
import OnBlurPlugin from './plugins/OnBlurPlugin';
import MyIcon from '../../Icon';
import type { FormPropsType } from './type.d';
import { type EditorVariableLabelPickerType, type EditorVariablePickerType } from './type.d';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import FocusPlugin from './plugins/FocusPlugin';
@@ -43,7 +44,9 @@ export default function Editor({
onBlur,
value,
placeholder = '',
bg = 'white'
bg = 'white',
isInvalid
}: {
minH?: number;
maxH?: number;
@@ -56,8 +59,9 @@ export default function Editor({
onBlur?: (editor: LexicalEditor) => void;
value?: string;
placeholder?: string;
bg?: string;
}) {
isInvalid?: boolean;
} & FormPropsType) {
const [key, setKey] = useState(getNanoid(6));
const [_, startSts] = useTransition();
const [focus, setFocus] = useState(false);
@@ -91,7 +95,7 @@ export default function Editor({
<PlainTextPlugin
contentEditable={
<ContentEditable
className={styles.contentEditable}
className={isInvalid ? styles.contentEditable_invalid : styles.contentEditable}
style={{
minHeight: `${minH}px`,
maxHeight: `${maxH}px`

View File

@@ -3,13 +3,17 @@
height: 100%;
width: 100%;
border: 1px solid rgb(232, 235, 240);
border-radius: var(--chakra-radii-md);
border-radius: var(--chakra-radii-sm);
padding: 8px 12px;
// background: #fff;
font-size: var(--chakra-fontSizes-sm);
overflow-y: auto;
transition:
border-color 0.1s ease-in-out,
box-shadow 0.1s ease-in-out;
&:hover {
border-color: var(--chakra-colors-primary-300);
}
@@ -32,6 +36,42 @@
box-shadow: 0px 0px 0px 2.4px rgba(51, 112, 255, 0.15);
}
.contentEditable_invalid {
position: relative;
height: 100%;
width: 100%;
border: 1px solid rgb(232, 235, 240);
border-radius: var(--chakra-radii-sm);
padding: 8px 12px;
font-size: var(--chakra-fontSizes-sm);
overflow-y: auto;
transition:
border-color 0.1s ease-in-out,
box-shadow 0.1s ease-in-out;
&::-webkit-scrollbar {
color: var(--chakra-colors-myGray-100);
}
&::-webkit-scrollbar-thumb {
background-color: var(--chakra-colors-myGray-200) !important;
cursor: pointer;
}
&::-webkit-scrollbar-thumb:hover {
background-color: var(--chakra-colors-myGray-250) !important;
}
border-color: var(--chakra-colors-red-500);
}
.contentEditable_invalid:focus {
outline: none;
border: 1px solid;
border-color: var(--chakra-colors-red-600);
box-shadow: 0px 0px 0px 2.4px rgba(244, 69, 46, 0.15);
}
.variable {
color: var(--chakra-colors-primary-600);
padding: 0 2px;

View File

@@ -1,10 +1,12 @@
import { Button, ModalBody, ModalFooter, useDisclosure } from '@chakra-ui/react';
import type { BoxProps } from '@chakra-ui/react';
import { Box, Button, ModalBody, ModalFooter, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import { editorStateToText } from './utils';
import Editor from './Editor';
import MyModal from '../../MyModal';
import { useTranslation } from 'next-i18next';
import type { EditorState, LexicalEditor } from 'lexical';
import type { FormPropsType } from './type.d';
import { type EditorVariableLabelPickerType, type EditorVariablePickerType } from './type.d';
import { useCallback } from 'react';
@@ -20,7 +22,9 @@ const PromptEditor = ({
maxLength,
placeholder,
title,
bg = 'white'
isInvalid,
isDisabled,
...props
}: {
showOpenModal?: boolean;
variables?: EditorVariablePickerType[];
@@ -33,8 +37,10 @@ const PromptEditor = ({
maxLength?: number;
placeholder?: string;
title?: string;
bg?: string;
}) => {
isInvalid?: boolean;
isDisabled?: boolean;
} & FormPropsType) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { t } = useTranslation();
@@ -55,20 +61,36 @@ const PromptEditor = ({
return (
<>
<Editor
showOpenModal={showOpenModal}
onOpenModal={onOpen}
variables={variables}
variableLabels={variableLabels}
minH={minH}
maxH={maxH}
maxLength={maxLength}
value={value}
onChange={onChangeInput}
onBlur={onBlurInput}
placeholder={placeholder}
bg={bg}
/>
<Box position="relative">
<Editor
showOpenModal={showOpenModal}
onOpenModal={onOpen}
variables={variables}
variableLabels={variableLabels}
minH={minH}
maxH={maxH}
maxLength={maxLength}
value={value}
onChange={onChangeInput}
onBlur={onBlurInput}
placeholder={placeholder}
isInvalid={isInvalid}
{...props}
/>
{isDisabled && (
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
bg="rgba(255, 255, 255, 0.4)"
borderRadius="md"
zIndex={1}
cursor="not-allowed"
/>
)}
</Box>
<MyModal isOpen={isOpen} onClose={onClose} iconSrc="modal/edit" title={title} w={'full'}>
<ModalBody>
<Editor

View File

@@ -21,3 +21,5 @@ export type EditorVariableLabelPickerType = {
avatar?: string;
};
};
export type FormPropsType = Omit<BoxProps, 'onChange' | 'onBlur'>;

View File

@@ -1,9 +1,10 @@
import { useBoolean } from 'ahooks';
export const useRefresh = () => {
const [_, { toggle }] = useBoolean();
const [refreshSign, { toggle }] = useBoolean();
return {
refresh: toggle
refresh: toggle,
refreshSign
};
};

View File

@@ -0,0 +1,23 @@
import { useTranslation as useNextTranslation } from 'next-i18next';
import type { I18nNsType } from '../i18n/i18next';
import { I18N_NAMESPACES_MAP } from '../i18n/constants';
export function useTranslation(ns?: I18nNsType[0] | I18nNsType) {
const { t: originalT, ...rest } = useNextTranslation(ns);
const t = (key: string | undefined, ...args: any[]): string => {
if (!key) return '';
if (!I18N_NAMESPACES_MAP[key as any]) {
return key;
}
// @ts-ignore
return originalT(key, ...args);
};
return {
t,
...rest
};
}

View File

@@ -0,0 +1,31 @@
export const I18N_NAMESPACES = [
'common',
'dataset',
'app',
'file',
'publish',
'workflow',
'user',
'chat',
'login',
'account_info',
'account_usage',
'account_bill',
'account_apikey',
'account_setting',
'account_inform',
'account_promotion',
'account_thirdParty',
'account',
'account_team',
'account_model',
'dashboard_mcp'
];
export const I18N_NAMESPACES_MAP = I18N_NAMESPACES.reduce(
(acc, namespace) => {
acc[namespace] = true;
return acc;
},
{} as Record<string, boolean>
);

View File

@@ -77,6 +77,7 @@
"select_file_img": "Upload file / image",
"select_img": "Upload Image",
"source_cronJob": "Scheduled execution",
"start_chat": "Start",
"stream_output": "Stream Output",
"unsupported_file_type": "Unsupported file types",
"upload": "Upload",

View File

@@ -77,7 +77,6 @@
"Save": "Save",
"Save_and_exit": "Save and Exit",
"Search": "Search",
"Select_all": "Select all",
"Setting": "Setting",
"Status": "Status",
"Submit": "Submit",
@@ -355,7 +354,6 @@
"core.chat.Select dataset Desc": "Select a Dataset to store the expected answer",
"core.chat.Send Message": "Send",
"core.chat.Speaking": "I'm Listening, Please Speak...",
"core.chat.Start Chat": "Start Chat",
"core.chat.Type a message": "Enter a Question, Press [Enter] to Send / Press [Ctrl(Alt/Shift) + Enter] for New Line",
"core.chat.Unpin": "Unpin",
"core.chat.You need to a chat app": "You Do Not Have an Available App",

View File

@@ -1,5 +1,6 @@
{
"Enable": "Enable",
"Select_all": "Select all files",
"add_file": "Import",
"api_file": "API Dataset",
"api_url": "API Url",
@@ -24,6 +25,7 @@
"close_auto_sync": "Are you sure you want to turn off automatic sync?",
"collection.Create update time": "Creation/Update Time",
"collection.Training type": "Training",
"collection.sync.submit": "The synchronization task has been submitted",
"collection.training_type": "Chunk type",
"collection_data_count": "Data amount",
"collection_metadata_custom_pdf_parse": "PDF enhancement analysis",
@@ -147,6 +149,7 @@
"pleaseFillUserIdAndToken": "Please fill in User ID and Token",
"preview_chunk": "Preview chunks",
"preview_chunk_empty": "File content is empty",
"preview_chunk_folder_warning": "Directory does not support preview",
"preview_chunk_intro": "A total of {{total}} blocks, up to 10",
"preview_chunk_not_selected": "Click on the file on the left to preview",
"process.Auto_Index": "Automatic index generation",
@@ -178,7 +181,7 @@
"split_sign_period": "period",
"split_sign_question": "question mark",
"split_sign_semicolon": "semicolon",
"start_sync_website_tip": "Confirm to start synchronizing data? \nThe old data will be deleted and retrieved again, please confirm!",
"start_sync_dataset_tip": "Do you really start synchronizing the entire knowledge base?",
"status_error": "Running exception",
"sync_collection_failed": "Synchronization collection error, please check whether the source file can be accessed normally",
"sync_schedule": "Timing synchronization",

65
packages/web/i18n/i18next.d.ts vendored Normal file
View File

@@ -0,0 +1,65 @@
import 'i18next';
import type account_team from './zh-CN/account_team.json';
import type account from './zh-CN/account.json';
import type account_thirdParty from './zh-CN/account_thirdParty.json';
import type account_promotion from './zh-CN/account_promotion.json';
import type account_inform from './zh-CN/account_inform.json';
import type account_setting from './zh-CN/account_setting.json';
import type account_apikey from './zh-CN/account_apikey.json';
import type account_bill from './zh-CN/account_bill.json';
import type account_usage from './zh-CN/account_usage.json';
import type account_info from './zh-CN/account_info.json';
import type common from './zh-CN/common.json';
import type dataset from './zh-CN/dataset.json';
import type app from './zh-CN/app.json';
import type file from './zh-CN/file.json';
import type publish from './zh-CN/publish.json';
import type workflow from './zh-CN/workflow.json';
import type user from './zh-CN/user.json';
import type chat from './zh-CN/chat.json';
import type login from './zh-CN/login.json';
import type account_model from './zh-CN/account_model.json';
import type dashboard_mcp from './zh-CN/dashboard_mcp.json';
import type { I18N_NAMESPACES } from './constants';
export interface I18nNamespaces {
common: typeof common;
dataset: typeof dataset;
app: typeof app;
file: typeof file;
publish: typeof publish;
workflow: typeof workflow;
user: typeof user;
chat: typeof chat;
login: typeof login;
account_info: typeof account_info;
account_usage: typeof account_usage;
account_bill: typeof account_bill;
account_apikey: typeof account_apikey;
account_setting: typeof account_setting;
account_inform: typeof account_inform;
account_promotion: typeof account_promotion;
account: typeof account;
account_team: typeof account_team;
account_thirdParty: typeof account_thirdParty;
account_model: typeof account_model;
dashboard_mcp: typeof dashboard_mcp;
}
export type I18nNsType = (keyof I18nNamespaces)[];
export type ParseKeys<Ns extends keyof I18nNamespaces = keyof I18nNamespaces> = {
[K in Ns]: `${K}:${keyof I18nNamespaces[K] & string}`;
}[Ns];
export type I18nKeyFunction = {
<Key extends ParseKeys>(key: Key): Key;
};
declare module 'i18next' {
interface CustomTypeOptions {
returnNull: false;
defaultNS: I18N_NAMESPACES;
resources: I18nNamespaces;
}
}

View File

@@ -1,3 +1,3 @@
import { type I18nKeyFunction } from '../types/i18next';
import { type I18nKeyFunction } from './i18next';
export const i18nT: I18nKeyFunction = (key) => key;

View File

@@ -109,7 +109,7 @@
"join_update_time": "加入/更新时间",
"kick_out_team": "移除成员",
"label_sync": "标签同步",
"leave": "已离职",
"leave": "离开",
"leave_team_failed": "离开团队异常",
"log_admin_add_plan": "【{{name}}】将给团队id为【{{teamId}}】的团队添加了套餐",
"log_admin_add_user": "【{{name}}】创建了一个名为【{{userName}}】的用户",
@@ -218,7 +218,7 @@
"recover_team_member": "成员恢复",
"relocate_department": "部门移动",
"remark": "备注",
"remove_tip": "确认将 {{username}} 移出团队?成员将被标记为“已离职”,不删除操作数据,账号下资源自动转让给团队所有者。",
"remove_tip": "确认将 {{username}} 移出团队?成员将被标记为“离开”,不删除操作数据,账号下资源自动转让给团队所有者。",
"restore_tip": "确认将 {{username}} 加入团队吗?仅恢复该成员账号可用性及相关权限,无法恢复账号下资源。",
"restore_tip_title": "恢复确认",
"retain_admin_permissions": "保留管理员权限",

View File

@@ -77,6 +77,7 @@
"select_file_img": "上传文件/图片",
"select_img": "上传图片",
"source_cronJob": "定时执行",
"start_chat": "开始对话",
"stream_output": "流输出",
"unsupported_file_type": "不支持的文件类型",
"upload": "上传",

View File

@@ -77,7 +77,6 @@
"Save": "保存",
"Save_and_exit": "保存并退出",
"Search": "搜索",
"Select_all": "全选",
"Setting": "设置",
"Status": "状态",
"Submit": "提交",
@@ -355,7 +354,6 @@
"core.chat.Select dataset Desc": "选择一个知识库存储预期答案",
"core.chat.Send Message": "发送",
"core.chat.Speaking": "我在听,请说...",
"core.chat.Start Chat": "开始对话",
"core.chat.Type a message": "输入问题,发送 [Enter]/换行 [Ctrl(Alt/Shift) + Enter]",
"core.chat.Unpin": "取消置顶",
"core.chat.You need to a chat app": "你没有可用的应用",
@@ -1301,7 +1299,7 @@
"user.team.role.Visitor": "访客",
"user.team.role.writer": "可写成员",
"user.type": "类型",
"user_leaved": "离开",
"user_leaved": "离开",
"value": "值",
"verification": "验证",
"xx_search_result": "{{key}} 的搜索结果",

View File

@@ -1,5 +1,6 @@
{
"Enable": "启用",
"Select_all": "选择所有文件",
"add_file": "添加文件",
"api_file": "API 文件库",
"api_url": "接口地址",
@@ -24,6 +25,7 @@
"close_auto_sync": "确认关闭自动同步功能?",
"collection.Create update time": "创建/更新时间",
"collection.Training type": "训练模式",
"collection.sync.submit": "已提交同步任务",
"collection.training_type": "处理模式",
"collection_data_count": "数据量",
"collection_metadata_custom_pdf_parse": "PDF增强解析",
@@ -147,6 +149,7 @@
"pleaseFillUserIdAndToken": "请填写 User ID 和 Token",
"preview_chunk": "分块预览",
"preview_chunk_empty": "文件内容为空",
"preview_chunk_folder_warning": "目录不支持预览",
"preview_chunk_intro": "共 {{total}} 个分块,最多展示 10 个",
"preview_chunk_not_selected": "点击左侧文件后进行预览",
"process.Auto_Index": "自动索引生成",
@@ -178,7 +181,7 @@
"split_sign_period": "句号",
"split_sign_question": "问号",
"split_sign_semicolon": "分号",
"start_sync_website_tip": "确开始同步数据?将会删除旧数据后重新获取,请确认!",
"start_sync_dataset_tip": "确开始同步整个知识库?",
"status_error": "运行异常",
"sync_collection_failed": "同步集合错误,请检查是否能正常访问源文件",
"sync_schedule": "定时同步",

View File

@@ -77,6 +77,7 @@
"select_file_img": "上傳檔案 / 圖片",
"select_img": "上傳圖片",
"source_cronJob": "定時執行",
"start_chat": "開始對話",
"stream_output": "串流輸出",
"unsupported_file_type": "不支援的檔案類型",
"upload": "上傳",

View File

@@ -77,7 +77,6 @@
"Save": "儲存",
"Save_and_exit": "儲存並離開",
"Search": "搜尋",
"Select_all": "全選",
"Setting": "設定",
"Status": "狀態",
"Submit": "送出",
@@ -355,7 +354,6 @@
"core.chat.Select dataset Desc": "選擇一個知識庫來儲存預期回答",
"core.chat.Send Message": "傳送",
"core.chat.Speaking": "我在聽,請說...",
"core.chat.Start Chat": "開始對話",
"core.chat.Type a message": "輸入問題,按 [Enter] 傳送 / 按 [Ctrl(Alt/Shift) + Enter] 換行",
"core.chat.Unpin": "取消釘選",
"core.chat.You need to a chat app": "您沒有可用的應用程式",

View File

@@ -1,5 +1,6 @@
{
"Enable": "啟用",
"Select_all": "選中所有檔案",
"add_file": "新增文件",
"api_file": "API 檔案庫",
"api_url": "介面位址",
@@ -24,6 +25,7 @@
"close_auto_sync": "確認關閉自動同步功能?",
"collection.Create update time": "建立/更新時間",
"collection.Training type": "分段模式",
"collection.sync.submit": "已提交同步任務",
"collection.training_type": "處理模式",
"collection_data_count": "資料量",
"collection_metadata_custom_pdf_parse": "PDF 增強解析",
@@ -147,6 +149,7 @@
"pleaseFillUserIdAndToken": "請填寫 User ID 和 Token",
"preview_chunk": "分塊預覽",
"preview_chunk_empty": "文件內容為空",
"preview_chunk_folder_warning": "目錄不支持預覽",
"preview_chunk_intro": "共 {{total}} 個分塊,最多展示 10 個",
"preview_chunk_not_selected": "點選左側文件後進行預覽",
"process.Auto_Index": "自動索引生成",
@@ -178,7 +181,7 @@
"split_sign_period": "句號",
"split_sign_question": "問號",
"split_sign_semicolon": "分號",
"start_sync_website_tip": "確開始同步資料?\n將會刪除舊資料後重新取得請確認",
"start_sync_dataset_tip": "確開始同步整個知識庫?",
"status_error": "執行異常",
"sync_collection_failed": "同步集合錯誤,請檢查是否能正常存取來原始檔",
"sync_schedule": "定時同步",

View File

@@ -382,14 +382,31 @@ const NumberInput = numInputMultiStyle({
bg: 'myGray.50',
border: '1px solid',
borderColor: 'myGray.200',
borderRadius: 'sm',
transition: 'border-color 0.1s ease-in-out, box-shadow 0.1s ease-in-out',
_hover: {
borderColor: 'primary.300'
},
_focus: {
borderColor: 'primary.500 !important',
borderColor: 'primary.600 !important',
boxShadow: `${shadowLight} !important`,
bg: 'white'
},
_disabled: {
color: 'myGray.400 !important',
bg: 'myWhite.300 !important'
},
_invalid: {
borderColor: 'red.500 !important',
borderWidth: '1px !important',
boxShadow: 'none !important',
_hover: {
borderColor: 'red.400 !important'
},
_focus: {
borderColor: 'red.600 !important',
boxShadow: '0px 0px 0px 2.4px rgba(244, 69, 46, 0.15) !important'
}
}
},
stepper: {

View File

@@ -1,86 +0,0 @@
import 'i18next';
import type account_team from '../i18n/zh-CN/account_team.json';
import type account from '../i18n/zh-CN/account.json';
import type account_thirdParty from '../i18n/zh-CN/account_thirdParty.json';
import type account_promotion from '../i18n/zh-CN/account_promotion.json';
import type account_inform from '../i18n/zh-CN/account_inform.json';
import type account_setting from '../i18n/zh-CN/account_setting.json';
import type account_apikey from '../i18n/zh-CN/account_apikey.json';
import type account_bill from '../i18n/zh-CN/account_bill.json';
import type account_usage from '../i18n/zh-CN/account_usage.json';
import type account_info from '../i18n/zh-CN/account_info.json';
import type common from '../i18n/zh-CN/common.json';
import type dataset from '../i18n/zh-CN/dataset.json';
import type app from '../i18n/zh-CN/app.json';
import type file from '../i18n/zh-CN/file.json';
import type publish from '../i18n/zh-CN/publish.json';
import type workflow from '../i18n/zh-CN/workflow.json';
import type user from '../i18n/zh-CN/user.json';
import type chat from '../i18n/zh-CN/chat.json';
import type login from '../i18n/zh-CN/login.json';
import type account_model from '../i18n/zh-CN/account_model.json';
import type dashboard_mcp from '../i18n/zh-CN/dashboard_mcp.json';
export interface I18nNamespaces {
common: typeof common;
dataset: typeof dataset;
app: typeof app;
file: typeof file;
publish: typeof publish;
workflow: typeof workflow;
user: typeof user;
chat: typeof chat;
login: typeof login;
account_info: typeof account_info;
account_usage: typeof account_usage;
account_bill: typeof account_bill;
account_apikey: typeof account_apikey;
account_setting: typeof account_setting;
account_inform: typeof account_inform;
account_promotion: typeof account_promotion;
account: typeof account;
account_team: typeof account_team;
account_thirdParty: typeof account_thirdParty;
account_model: typeof account_model;
dashboard_mcp: typeof dashboard_mcp;
}
export type I18nNsType = (keyof I18nNamespaces)[];
export type ParseKeys<Ns extends keyof I18nNamespaces = keyof I18nNamespaces> = {
[K in Ns]: `${K}:${keyof I18nNamespaces[K] & string}`;
}[Ns];
export type I18nKeyFunction = {
<Key extends ParseKeys>(key: Key): Key;
};
declare module 'i18next' {
interface CustomTypeOptions {
returnNull: false;
defaultNS: [
'common',
'dataset',
'app',
'file',
'publish',
'workflow',
'user',
'chat',
'login',
'account_info',
'account_usage',
'account_bill',
'account_apikey',
'account_setting',
'account_inform',
'account_promotion',
'account_thirdParty',
'account',
'account_team',
'account_model',
'dashboard_mcp'
];
resources: I18nNamespaces;
}
}