perf: usages list;perf: move components (#3654)

* perf: usages list

* team sub plan load

* perf: usage dashboard code

* perf: dashboard ui

* perf: move components
This commit is contained in:
Archer
2025-01-23 17:29:39 +08:00
committed by GitHub
parent 0c05add8b2
commit 34b510cba1
271 changed files with 611 additions and 921 deletions

View File

@@ -2,12 +2,22 @@ import { ErrType } from '../errorCode';
import { i18nT } from '../../../../web/i18n/utils';
/* team: 503000 */
export enum UserErrEnum {
notUser = 'notUser',
userExist = 'userExist',
unAuthRole = 'unAuthRole',
account_psw_error = 'account_psw_error',
balanceNotEnough = 'balanceNotEnough',
unAuthSso = 'unAuthSso'
}
const errList = [
{
statusText: UserErrEnum.notUser,
message: i18nT('common:code_error.account_not_found')
},
{
statusText: UserErrEnum.userExist,
message: i18nT('common:code_error.account_exist')
},
{
statusText: UserErrEnum.account_psw_error,
message: i18nT('common:code_error.account_error')

View File

@@ -6,21 +6,20 @@ export type CreateTrainingUsageProps = {
datasetId: string;
};
export type GetTotalPointsProps = {
dateStart: Date;
dateEnd: Date;
teamMemberIds: string[];
sources: UsageSourceEnum[];
unit: 'day' | 'week' | 'month';
};
export type GetUsageProps = {
dateStart: Date;
dateEnd: Date;
sources?: UsageSourceEnum[];
teamMemberIds?: string[];
projectName?: string;
isSelectAllTmb?: boolean;
};
export type GetUsageDashboardProps = GetUsageProps & {
unit: 'day' | 'month';
};
export type GetUsageDashboardResponseItem = {
date: Date;
totalPoints: number;
};
export type ConcatUsageProps = UsageListItemCountType & {

View File

@@ -9,7 +9,7 @@ import { jsonRes } from '../response';
// unit: times/s
// how to use?
// export default NextAPI(useQPSLimit(10), handler); // limit 10 times per second for a ip
export function useReqFrequencyLimit(seconds: number, limit: number, force = false) {
export function useIPFrequencyLimit(seconds: number, limit: number, force = false) {
return async (req: ApiRequestProps, res: NextApiResponse) => {
const ip = requestIp.getClientIp(req);
if (!ip || (process.env.USE_IP_LIMIT !== 'true' && !force)) {

View File

@@ -348,119 +348,6 @@ export async function searchDatasetData(props: SearchDatasetDataProps) {
};
}
const searchResults = (
await Promise.all(
datasetIds.map(async (id) => {
return MongoDatasetData.aggregate(
[
{
$match: {
teamId: new Types.ObjectId(teamId),
datasetId: new Types.ObjectId(id),
$text: { $search: jiebaSplit({ text: query }) },
...(filterCollectionIdList
? {
collectionId: {
$in: filterCollectionIdList.map((id) => new Types.ObjectId(id))
}
}
: {}),
...(forbidCollectionIdList && forbidCollectionIdList.length > 0
? {
collectionId: {
$nin: forbidCollectionIdList.map((id) => new Types.ObjectId(id))
}
}
: {})
}
},
{
$sort: {
score: { $meta: 'textScore' }
}
},
{
$limit: limit
},
{
$project: {
_id: 1,
datasetId: 1,
collectionId: 1,
updateTime: 1,
q: 1,
a: 1,
chunkIndex: 1,
score: { $meta: 'textScore' }
}
}
],
{
...readFromSecondary
}
);
})
)
).flat() as (DatasetDataSchemaType & { score: number })[];
// Get data and collections
const collections = await MongoDatasetCollection.find(
{
_id: { $in: searchResults.map((item) => item.collectionId) }
},
'_id name fileId rawLink externalFileId externalFileUrl',
{ ...readFromSecondary }
).lean();
return {
fullTextRecallResults: searchResults
.map((data, index) => {
const collection = collections.find(
(col) => String(col._id) === String(data.collectionId)
);
if (!collection) {
console.log('Collection is not found', data);
return;
}
return {
id: String(data._id),
datasetId: String(data.datasetId),
collectionId: String(data.collectionId),
updateTime: data.updateTime,
q: data.q,
a: data.a,
chunkIndex: data.chunkIndex,
indexes: data.indexes,
...getCollectionSourceData(collection),
score: [{ type: SearchScoreTypeEnum.fullText, value: data.score ?? 0, index }]
};
})
.filter(Boolean) as SearchDataResponseItemType[],
tokenLen: 0
};
};
const fullTextRecall2 = async ({
query,
limit,
filterCollectionIdList,
forbidCollectionIdList
}: {
query: string;
limit: number;
filterCollectionIdList?: string[];
forbidCollectionIdList: string[];
}): Promise<{
fullTextRecallResults: SearchDataResponseItemType[];
tokenLen: number;
}> => {
if (limit === 0) {
return {
fullTextRecallResults: [],
tokenLen: 0
};
}
const searchResults = (
await Promise.all(
datasetIds.map(async (id) => {
@@ -637,7 +524,7 @@ export async function searchDatasetData(props: SearchDatasetDataProps) {
filterCollectionIdList
}),
// FullText tmp
fullTextRecall2({
fullTextRecall({
query,
limit: fullTextLimit,
filterCollectionIdList,

View File

@@ -61,7 +61,8 @@ const UsageSchema = new Schema({
});
try {
UsageSchema.index({ teamId: 1, tmbId: 1, source: 1, time: -1 });
UsageSchema.index({ teamId: 1, time: 1, tmbId: 1, source: 1 });
UsageSchema.index({ teamId: 1, time: 1, appName: 1 });
// timer task. clear dead team
// UsageSchema.index({ teamId: 1, time: -1 });

View File

@@ -101,7 +101,7 @@ const DateRangePicker = ({
date.to = date.from;
}
setRange(date);
onChange && onChange(date);
onChange?.(date);
}}
footer={
<Flex justifyContent={'flex-end'}>
@@ -116,7 +116,7 @@ const DateRangePicker = ({
<Button
size={'sm'}
onClick={() => {
onSuccess && onSuccess(range || defaultDate);
onSuccess?.(range || defaultDate);
setShowSelected(false);
}}
>

View File

@@ -10,29 +10,30 @@ import {
MenuList,
useDisclosure
} from '@chakra-ui/react';
import React, { useMemo, useRef } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import MyTag from '../Tag/index';
import MyIcon from '../Icon';
import MyAvatar from '../Avatar';
import { useTranslation } from 'next-i18next';
import { useScrollPagination } from '../../../hooks/useScrollPagination';
import MyDivider from '../MyDivider';
export type SelectProps<T = any> = {
value: T[];
placeholder?: string;
list: {
icon?: string;
label: string | React.ReactNode;
value: T;
}[];
value: T[];
isSelectAll: boolean;
setIsSelectAll: React.Dispatch<React.SetStateAction<boolean>>;
placeholder?: string;
maxH?: number;
itemWrap?: boolean;
onSelect: (val: T[]) => void;
closeable?: boolean;
showCheckedIcon?: boolean;
ScrollData?: ReturnType<typeof useScrollPagination>['ScrollData'];
isSelectAll?: boolean;
setIsSelectAll?: React.Dispatch<React.SetStateAction<boolean>>;
} & Omit<ButtonProps, 'onSelect'>;
const MultipleSelect = <T = any,>({
@@ -42,7 +43,6 @@ const MultipleSelect = <T = any,>({
maxH = 400,
onSelect,
closeable = false,
showCheckedIcon = true,
itemWrap = true,
ScrollData,
isSelectAll,
@@ -65,79 +65,66 @@ const MultipleSelect = <T = any,>({
}
};
const onclickItem = (val: T) => {
if (value.includes(val)) {
onSelect(value.filter((i) => i !== val));
} else {
onSelect([...value, val]);
}
};
const onclickItem = useCallback(
(val: T) => {
// 全选状态下value 实际上上空。
if (isSelectAll) {
onSelect(list.map((item) => item.value).filter((i) => i !== val));
setIsSelectAll(false);
return;
}
const onSelectAll = () => {
if (!setIsSelectAll) {
onSelect(value.length === list.length ? [] : list.map((item) => item.value));
return;
}
if (value.includes(val)) {
onSelect(value.filter((i) => i !== val));
} else {
onSelect([...value, val]);
}
},
[value, isSelectAll, onSelect, setIsSelectAll]
);
const onSelectAll = useCallback(() => {
const hasSelected = isSelectAll || value.length > 0;
onSelect(hasSelected ? [] : list.map((item) => item.value));
if (isSelectAll) {
onSelect([]);
}
setIsSelectAll((state) => !state);
};
}, [value, list, setIsSelectAll, onSelect]);
const ListRender = useMemo(() => {
return (
<>
{list.map((item, i) => (
<MenuItem
key={i}
{...menuItemStyles}
{...((isSelectAll && !value.includes(item.value)) ||
(!isSelectAll && value.includes(item.value))
? {
color: 'primary.600'
}
: {
color: 'myGray.900'
})}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onclickItem(item.value);
}}
whiteSpace={'pre-wrap'}
fontSize={'sm'}
gap={2}
>
{!showCheckedIcon && (
<Checkbox
isChecked={
(isSelectAll && !value.includes(item.value)) ||
(!isSelectAll && value.includes(item.value))
}
/>
)}
{item.icon && <MyAvatar src={item.icon} w={'1rem'} borderRadius={'0'} />}
<Box flex={'1 0 0'}>{item.label}</Box>
{showCheckedIcon && (
<Box w={'0.8rem'} lineHeight={1}>
{(isSelectAll && !value.includes(item.value)) ||
(!isSelectAll && value.includes(item.value) && (
<MyIcon name={'price/right'} w={'1rem'} />
))}
</Box>
)}
</MenuItem>
))}
{list.map((item, i) => {
const isSelected = isSelectAll || value.includes(item.value);
return (
<MenuItem
key={i}
{...menuItemStyles}
{...(isSelected
? {
color: 'primary.600'
}
: {
color: 'myGray.900'
})}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onclickItem(item.value);
}}
whiteSpace={'pre-wrap'}
fontSize={'sm'}
gap={2}
>
<Checkbox isChecked={isSelected} />
{item.icon && <MyAvatar src={item.icon} w={'1rem'} borderRadius={'0'} />}
<Box flex={'1 0 0'}>{item.label}</Box>
</MenuItem>
);
})}
</>
);
}, [value, list, isSelectAll]);
const isAllSelected = useMemo(
() => (isSelectAll && value.length === 0) || (!isSelectAll && value.length === list.length),
[isSelectAll, value, list]
);
return (
<Box>
<Menu
@@ -186,44 +173,43 @@ const MultipleSelect = <T = any,>({
overflow={'hidden'}
flex={1}
>
{isAllSelected ? (
{isSelectAll ? (
<Box fontSize={'mini'} color={'myGray.900'}>
{t('common:common.All')}
</Box>
) : (
(isSelectAll
? list.filter((item) => !value.includes(item.value))
: list.filter((item) => value.includes(item.value))
).map((item, i) => (
<MyTag
className="tag-icon"
key={i}
bg={'primary.100'}
color={'primary.700'}
type={'fill'}
borderRadius={'full'}
px={2}
py={0.5}
flexShrink={0}
>
{item.label}
{closeable && (
<MyIcon
name={'common/closeLight'}
ml={1}
w="0.8rem"
cursor={'pointer'}
_hover={{
color: 'red.500'
}}
onClick={(e) => {
e.stopPropagation();
onclickItem(item.value);
}}
/>
)}
</MyTag>
))
list
.filter((item) => value.includes(item.value))
.map((item, i) => (
<MyTag
className="tag-icon"
key={i}
bg={'primary.100'}
color={'primary.700'}
type={'fill'}
borderRadius={'full'}
px={2}
py={0.5}
flexShrink={0}
>
{item.label}
{closeable && (
<MyIcon
name={'common/closeLight'}
ml={1}
w="0.8rem"
cursor={'pointer'}
_hover={{
color: 'red.500'
}}
onClick={(e) => {
e.stopPropagation();
onclickItem(item.value);
}}
/>
)}
</MyTag>
))
)}
</Flex>
<MyIcon name={'core/chat/chevronDown'} color={'myGray.600'} w={4} h={4} />
@@ -245,7 +231,7 @@ const MultipleSelect = <T = any,>({
>
<MenuItem
{...menuItemStyles}
color={isAllSelected ? 'primary.600' : 'myGray.900'}
color={isSelectAll ? 'primary.600' : 'myGray.900'}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
@@ -256,15 +242,12 @@ const MultipleSelect = <T = any,>({
gap={2}
mb={1}
>
{!showCheckedIcon && <Checkbox isChecked={isAllSelected} />}
<Checkbox isChecked={isSelectAll} />
<Box flex={'1 0 0'}>{t('common:common.All')}</Box>
{showCheckedIcon && (
<Box w={'0.8rem'} lineHeight={1}>
{isAllSelected && <MyIcon name={'price/right'} w={'1rem'} />}
</Box>
)}
</MenuItem>
<MyDivider my={1} />
{ScrollData ? <ScrollData>{ListRender}</ScrollData> : ListRender}
</MenuList>
</Menu>
@@ -273,3 +256,9 @@ const MultipleSelect = <T = any,>({
};
export default MultipleSelect;
export const useMultipleSelect = <T = any,>(defaultValue: T[] = [], defaultSelectAll = false) => {
const [value, setValue] = useState<T[]>(defaultValue);
const [isSelectAll, setIsSelectAll] = useState<boolean>(defaultSelectAll);
return { value, setValue, isSelectAll, setIsSelectAll };
};

View File

@@ -0,0 +1,24 @@
import { useCopyData } from '../../../hooks/useCopyData';
import React from 'react';
import MyTooltip from '../MyTooltip';
import { useTranslation } from 'next-i18next';
import { Box, BoxProps } from '@chakra-ui/react';
const CopyBox = ({
value,
children,
...props
}: { value: string; children: React.ReactNode } & BoxProps) => {
const { copyData } = useCopyData();
const { t } = useTranslation();
return (
<MyTooltip label={t('common:click_to_copy')}>
<Box cursor={'pointer'} onClick={() => copyData(value)} {...props}>
{children}
</Box>
</MyTooltip>
);
};
export default CopyBox;

View File

@@ -0,0 +1,64 @@
import { useTranslation } from 'next-i18next';
import { useToast } from './useToast';
import { useCallback } from 'react';
import { hasHttps } from '../common/system/utils';
import { isProduction } from '@fastgpt/global/common/system/constants';
/**
* copy text data
*/
export const useCopyData = () => {
const { t } = useTranslation();
const { toast } = useToast();
const copyData = useCallback(
async (
data: string,
title: string | null = t('common:common.Copy Successful'),
duration = 1000
) => {
data = data.trim();
try {
if ((hasHttps() || !isProduction) && navigator.clipboard) {
await navigator.clipboard.writeText(data);
} else {
throw new Error('');
}
} catch (error) {
// console.log(error);
const textarea = document.createElement('textarea');
textarea.value = data;
textarea.style.position = 'absolute';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
const res = document.execCommand('copy');
document.body.removeChild(textarea);
if (!res) {
return toast({
title: t('common:common.Copy_failed'),
status: 'error',
duration
});
}
}
if (title) {
toast({
title,
status: 'success',
duration
});
}
},
[t, toast]
);
return {
copyData
};
};

View File

@@ -9,7 +9,10 @@
"details": "Details",
"dingtalk": "DingTalk",
"duration_seconds": "Duration (seconds)",
"every_day": "Day",
"every_month": "Moon",
"export_confirm": "Export confirmation",
"export_confirm_tip": "There are currently {{total}} usage records in total. Are you sure to export?",
"feishu": "Feishu",
"generation_time": "Generation time",
"input_token_length": "input tokens",
@@ -26,7 +29,6 @@
"select_member_and_source_first": "Please select members and types first",
"share": "Share Link",
"source": "source",
"start_export": "Export started",
"text_length": "text length",
"token_length": "token length",
"total_points": "AI points consumption",

View File

@@ -37,9 +37,12 @@
"chose_condition": "Choose Condition",
"chosen": "Chosen",
"classification": "Classification",
"click_to_copy": "Click to copy",
"click_to_resume": "Click to Resume",
"code_editor": "Code Editor",
"code_error.account_error": "Incorrect account name or password",
"code_error.account_exist": "Account has been registered",
"code_error.account_not_found": "User is not registered",
"code_error.app_error.invalid_app_type": "Invalid Application Type",
"code_error.app_error.invalid_owner": "Unauthorized Application Owner",
"code_error.app_error.not_exist": "Application Does Not Exist",
@@ -932,14 +935,14 @@
"model_doubao": "Doubao",
"model_ernie": "Ernie",
"model_hunyuan": "Hunyuan",
"model_intern": "Intern",
"model_moka": "Moka-AI",
"model_moonshot": "Moonshot",
"model_other": "Other",
"model_qwen": "Qwen",
"model_sparkdesk": "SprkDesk",
"model_stepfun": "StepFun",
"model_yi": "Yi",
"model_intern": "Intern",
"model_moka": "Moka-AI",
"move.confirm": "Confirm move",
"navbar.Account": "Account",
"navbar.Chat": "Chat",

View File

@@ -9,10 +9,11 @@
"details": "详情",
"dingtalk": "钉钉",
"duration_seconds": "时长(秒)",
"every_day": "天",
"every_month": "月",
"every_day": "天",
"every_month": "月",
"every_week": "每周",
"export_confirm": "导出确认",
"export_confirm_tip": "当前共 {{total}} 条使用记录,确认导出?",
"export_success": "导出成功",
"feishu": "飞书",
"generation_time": "生成时间",
@@ -30,7 +31,6 @@
"select_member_and_source_first": "请先选中成员和类型",
"share": "分享链接",
"source": "来源",
"start_export": "已开始导出",
"text_length": "文本长度",
"token_length": "token 长度",
"total_points": "AI 积分消耗",

View File

@@ -41,9 +41,12 @@
"chose_condition": "选择条件",
"chosen": "已选",
"classification": "分类",
"click_to_copy": "点击复制",
"click_to_resume": "点击恢复",
"code_editor": "代码编辑",
"code_error.account_error": "账号名或密码错误",
"code_error.account_exist": "账号已注册",
"code_error.account_not_found": "用户未注册",
"code_error.app_error.invalid_app_type": "错误的应用类型",
"code_error.app_error.invalid_owner": "非法的应用所有者",
"code_error.app_error.not_exist": "应用不存在",
@@ -935,14 +938,14 @@
"model_doubao": "豆包",
"model_ernie": "文心一言",
"model_hunyuan": "腾讯混元",
"model_intern": "书生",
"model_moka": "Moka-AI",
"model_moonshot": "月之暗面",
"model_other": "其他",
"model_qwen": "阿里千问",
"model_sparkdesk": "讯飞星火",
"model_stepfun": "阶跃星辰",
"model_yi": "零一万物",
"model_intern": "书生",
"model_moka": "Moka-AI",
"move.confirm": "确认移动",
"navbar.Account": "账号",
"navbar.Chat": "聊天",

View File

@@ -9,7 +9,10 @@
"details": "詳情",
"dingtalk": "釘釘",
"duration_seconds": "時長(秒)",
"every_day": "天",
"every_month": "月",
"export_confirm": "導出確認",
"export_confirm_tip": "當前共 {{total}} 筆使用記錄,確認導出?",
"feishu": "飛書",
"generation_time": "生成時間",
"input_token_length": "輸入 tokens",
@@ -26,7 +29,6 @@
"select_member_and_source_first": "請先選取成員和類型",
"share": "分享連結",
"source": "來源",
"start_export": "已開始匯出",
"text_length": "文字長度",
"token_length": "token 長度",
"total_points": "AI 積分消耗",

View File

@@ -37,9 +37,11 @@
"chose_condition": "選擇條件",
"chosen": "已選擇",
"classification": "分類",
"click_to_copy": "點選複製",
"click_to_resume": "點選繼續",
"code_editor": "程式碼編輯器",
"code_error.account_error": "帳號名稱或密碼錯誤",
"code_error.account_not_found": "用戶未註冊",
"code_error.app_error.invalid_app_type": "無效的應用程式類型",
"code_error.app_error.invalid_owner": "非法的應用程式擁有者",
"code_error.app_error.not_exist": "應用程式不存在",
@@ -932,14 +934,14 @@
"model_doubao": "豆包",
"model_ernie": "文心一言",
"model_hunyuan": "騰訊混元",
"model_intern": "書生",
"model_moka": "Moka-AI",
"model_moonshot": "月之暗面",
"model_other": "其他",
"model_qwen": "阿里千問",
"model_sparkdesk": "訊飛星火",
"model_stepfun": "階躍星辰",
"model_yi": "零一萬物",
"model_intern": "書生",
"model_moka": "Moka-AI",
"move.confirm": "確認移動",
"navbar.Account": "帳戶",
"navbar.Chat": "對話",