@lobehub/lobehub 2.0.0-next.38 → 2.0.0-next.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/apps/desktop/src/main/modules/networkProxy/__tests__/dispatcher.test.ts +401 -0
- package/apps/desktop/src/main/modules/networkProxy/__tests__/tester.test.ts +531 -0
- package/apps/desktop/src/main/modules/networkProxy/__tests__/urlBuilder.test.ts +349 -0
- package/apps/desktop/src/main/modules/networkProxy/__tests__/validator.test.ts +492 -0
- package/changelog/v1.json +5 -0
- package/locales/ar/auth.json +45 -1
- package/locales/bg-BG/auth.json +45 -1
- package/locales/de-DE/auth.json +45 -1
- package/locales/en-US/auth.json +45 -1
- package/locales/es-ES/auth.json +45 -1
- package/locales/fa-IR/auth.json +45 -1
- package/locales/fr-FR/auth.json +45 -1
- package/locales/it-IT/auth.json +45 -1
- package/locales/ja-JP/auth.json +45 -1
- package/locales/ko-KR/auth.json +45 -1
- package/locales/nl-NL/auth.json +45 -1
- package/locales/pl-PL/auth.json +45 -1
- package/locales/pt-BR/auth.json +45 -1
- package/locales/ru-RU/auth.json +45 -1
- package/locales/tr-TR/auth.json +45 -1
- package/locales/vi-VN/auth.json +45 -1
- package/locales/zh-CN/auth.json +45 -1
- package/locales/zh-TW/auth.json +45 -1
- package/package.json +1 -1
- package/packages/context-engine/src/processors/MessageCleanup.ts +1 -0
- package/packages/context-engine/src/processors/__tests__/MessageCleanup.test.ts +28 -0
- package/packages/obervability-otel/package.json +3 -1
- package/packages/obervability-otel/src/api.ts +2 -0
- package/packages/obervability-otel/src/trpc/convention.ts +16 -0
- package/packages/obervability-otel/src/trpc/index.test.ts +38 -0
- package/packages/obervability-otel/src/trpc/index.ts +62 -0
- package/packages/obervability-otel/src/trpc/metrics.ts +31 -0
- package/packages/types/src/usage/usageRecord.ts +54 -0
- package/src/app/[variants]/(main)/profile/hooks/useCategory.tsx +10 -1
- package/src/app/[variants]/(main)/profile/usage/Client.tsx +114 -0
- package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/ModelTable.tsx +175 -0
- package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/index.tsx +126 -0
- package/src/app/[variants]/(main)/profile/usage/features/UsageCards/MonthSpend.tsx +53 -0
- package/src/app/[variants]/(main)/profile/usage/features/UsageCards/TodaySpend.tsx +67 -0
- package/src/app/[variants]/(main)/profile/usage/features/UsageCards/index.tsx +19 -0
- package/src/app/[variants]/(main)/profile/usage/features/UsageTable.tsx +145 -0
- package/src/app/[variants]/(main)/profile/usage/features/UsageTrends.tsx +107 -0
- package/src/app/[variants]/(main)/profile/usage/features/components/UsageBarChart.tsx +48 -0
- package/src/app/[variants]/(main)/profile/usage/page.tsx +23 -0
- package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +3 -3
- package/src/features/Conversation/Messages/Group/Actions/WithoutContentId.tsx +37 -14
- package/src/features/Conversation/Messages/Group/Error/index.tsx +1 -1
- package/src/features/Conversation/Messages/Group/GroupChildren.tsx +13 -35
- package/src/features/Conversation/Messages/Group/GroupItem.tsx +43 -0
- package/src/features/Conversation/Messages/Group/Tool/Inspector/index.tsx +1 -2
- package/src/features/Conversation/Messages/Group/Tool/Render/CustomRender.tsx +1 -1
- package/src/features/Conversation/Messages/Group/Tool/index.tsx +0 -2
- package/src/features/Conversation/Messages/Group/index.tsx +7 -2
- package/src/features/Conversation/hooks/useChatListActionsBar.tsx +21 -7
- package/src/features/PluginsUI/Render/BuiltinType/index.tsx +1 -1
- package/src/features/PluginsUI/Render/MCPType/index.tsx +52 -0
- package/src/features/PluginsUI/Render/StandaloneType/Iframe.tsx +2 -2
- package/src/features/PluginsUI/Render/index.tsx +17 -0
- package/src/libs/mcp/client.ts +3 -2
- package/src/libs/mcp/types.ts +71 -0
- package/src/libs/trpc/lambda/index.ts +5 -2
- package/src/libs/trpc/middleware/openTelemetry.ts +141 -0
- package/src/locales/default/auth.ts +44 -0
- package/src/locales/default/chat.ts +1 -0
- package/src/server/routers/desktop/mcp.ts +1 -3
- package/src/server/routers/lambda/index.ts +2 -0
- package/src/server/routers/lambda/usage.ts +36 -0
- package/src/server/routers/tools/mcp.ts +1 -3
- package/src/server/services/mcp/index.test.ts +28 -15
- package/src/server/services/mcp/index.ts +29 -18
- package/src/server/services/usage/index.test.ts +310 -0
- package/src/server/services/usage/index.ts +164 -0
- package/src/services/chat/contextEngineering.test.ts +4 -0
- package/src/services/mcp.test.ts +7 -1
- package/src/services/mcp.ts +13 -12
- package/src/services/usage.ts +13 -0
- package/src/store/chat/agents/createAgentExecutors.ts +2 -3
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +40 -1
- package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +13 -5
- package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +3 -3
- package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +6 -6
- package/src/store/chat/slices/builtinTool/actions/interpreter.ts +2 -2
- package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
- package/src/store/chat/slices/builtinTool/actions/search.ts +6 -6
- package/src/store/chat/slices/message/actions/publicApi.ts +19 -1
- package/src/store/chat/slices/message/initialState.ts +5 -0
- package/src/store/chat/slices/message/selectors/chat.test.ts +22 -602
- package/src/store/chat/slices/message/selectors/chat.ts +0 -2
- package/src/store/chat/slices/message/selectors/dbMessage.test.ts +51 -0
- package/src/store/chat/slices/message/selectors/displayMessage.test.ts +818 -0
- package/src/store/chat/slices/message/selectors/displayMessage.ts +52 -1
- package/src/store/chat/slices/message/selectors/messageState.ts +2 -0
- package/src/store/chat/slices/plugin/action.test.ts +4 -4
- package/src/store/chat/slices/plugin/actions/index.ts +39 -0
- package/src/store/chat/slices/plugin/actions/internals.ts +83 -0
- package/src/store/chat/slices/plugin/actions/optimisticUpdate.ts +188 -0
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +213 -0
- package/src/store/chat/slices/plugin/actions/publicApi.ts +115 -0
- package/src/store/chat/slices/plugin/actions/workflow.ts +121 -0
- package/src/store/chat/store.ts +1 -1
- package/src/store/global/initialState.ts +1 -0
- package/src/store/chat/slices/plugin/action.ts +0 -539
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { ModelIcon, ProviderIcon } from '@lobehub/icons';
|
|
2
|
+
import { ActionIcon, Modal } from '@lobehub/ui';
|
|
3
|
+
import { useTheme } from 'antd-style';
|
|
4
|
+
import { MaximizeIcon } from 'lucide-react';
|
|
5
|
+
import { memo, useMemo, useState } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { Flexbox } from 'react-layout-kit';
|
|
8
|
+
|
|
9
|
+
import StatisticCard from '@/components/StatisticCard';
|
|
10
|
+
import TitleWithPercentage from '@/components/StatisticCard/TitleWithPercentage';
|
|
11
|
+
import { UsageLog } from '@/types/usage/usageRecord';
|
|
12
|
+
import { formatNumber } from '@/utils/format';
|
|
13
|
+
|
|
14
|
+
import { GroupBy, UsageChartProps } from '../../../Client';
|
|
15
|
+
import ModelTable from './ModelTable';
|
|
16
|
+
|
|
17
|
+
const computeList = (data: UsageLog[], groupBy: GroupBy): string[] => {
|
|
18
|
+
if (!data || data?.length === 0) return [];
|
|
19
|
+
|
|
20
|
+
return Array.from(
|
|
21
|
+
data.reduce((acc, log) => {
|
|
22
|
+
if (log.records) {
|
|
23
|
+
for (const item of log.records) {
|
|
24
|
+
if (groupBy === GroupBy.Model && item.model?.length !== 0) {
|
|
25
|
+
acc.add(item.model);
|
|
26
|
+
}
|
|
27
|
+
if (groupBy === GroupBy.Provider && item.provider?.length !== 0) {
|
|
28
|
+
acc.add(item.provider);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return acc;
|
|
33
|
+
}, new Set<string>()),
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const ActiveModels = memo<UsageChartProps>(({ data, isLoading, groupBy }) => {
|
|
38
|
+
const { t } = useTranslation('auth');
|
|
39
|
+
const theme = useTheme();
|
|
40
|
+
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
42
|
+
|
|
43
|
+
const iconList = useMemo(
|
|
44
|
+
() => computeList(data || [], groupBy || GroupBy.Model),
|
|
45
|
+
[data, groupBy],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
<StatisticCard
|
|
51
|
+
extra={
|
|
52
|
+
<ActionIcon
|
|
53
|
+
icon={MaximizeIcon}
|
|
54
|
+
onClick={() => setOpen(true)}
|
|
55
|
+
title={
|
|
56
|
+
groupBy === GroupBy.Model
|
|
57
|
+
? t('usage.activeModels.modelTable')
|
|
58
|
+
: t('usage.activeModels.providerTable')
|
|
59
|
+
}
|
|
60
|
+
/>
|
|
61
|
+
}
|
|
62
|
+
key={groupBy}
|
|
63
|
+
loading={isLoading}
|
|
64
|
+
statistic={{
|
|
65
|
+
description: (
|
|
66
|
+
<Flexbox horizontal wrap={'wrap'}>
|
|
67
|
+
{iconList.map((item, i) => {
|
|
68
|
+
if (!item) return null;
|
|
69
|
+
return groupBy === GroupBy.Model ? (
|
|
70
|
+
<ModelIcon
|
|
71
|
+
key={item}
|
|
72
|
+
model={item}
|
|
73
|
+
size={18}
|
|
74
|
+
style={{
|
|
75
|
+
border: `2px solid ${theme.colorBgContainer}`,
|
|
76
|
+
boxSizing: 'content-box',
|
|
77
|
+
marginRight: -8,
|
|
78
|
+
zIndex: i + 1,
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
) : (
|
|
82
|
+
<ProviderIcon
|
|
83
|
+
key={item}
|
|
84
|
+
provider={item}
|
|
85
|
+
size={18}
|
|
86
|
+
style={{
|
|
87
|
+
border: `2px solid ${theme.colorBgContainer}`,
|
|
88
|
+
boxSizing: 'content-box',
|
|
89
|
+
marginRight: -8,
|
|
90
|
+
zIndex: i + 1,
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
})}
|
|
95
|
+
</Flexbox>
|
|
96
|
+
),
|
|
97
|
+
precision: 0,
|
|
98
|
+
value: formatNumber(iconList?.length ?? 0),
|
|
99
|
+
}}
|
|
100
|
+
title={
|
|
101
|
+
<TitleWithPercentage
|
|
102
|
+
title={
|
|
103
|
+
groupBy === GroupBy.Model
|
|
104
|
+
? t('usage.activeModels.models')
|
|
105
|
+
: t('usage.activeModels.providers')
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
}
|
|
109
|
+
/>
|
|
110
|
+
<Modal
|
|
111
|
+
footer={null}
|
|
112
|
+
onCancel={() => setOpen(false)}
|
|
113
|
+
open={open}
|
|
114
|
+
title={
|
|
115
|
+
groupBy === GroupBy.Model
|
|
116
|
+
? t('usage.activeModels.modelTable')
|
|
117
|
+
: t('usage.activeModels.providerTable')
|
|
118
|
+
}
|
|
119
|
+
>
|
|
120
|
+
<ModelTable data={data} groupBy={groupBy} isLoading={isLoading} />
|
|
121
|
+
</Modal>
|
|
122
|
+
</>
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
export default ActiveModels;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useTheme } from 'antd-style';
|
|
4
|
+
import { memo } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
|
|
7
|
+
import Statistic from '@/components/Statistic';
|
|
8
|
+
import StatisticCard from '@/components/StatisticCard';
|
|
9
|
+
import TitleWithPercentage from '@/components/StatisticCard/TitleWithPercentage';
|
|
10
|
+
import { UsageLog } from '@/types/usage/usageRecord';
|
|
11
|
+
import { formatNumber } from '@/utils/format';
|
|
12
|
+
|
|
13
|
+
import { UsageChartProps } from '../../Client';
|
|
14
|
+
|
|
15
|
+
const computeMonth = (
|
|
16
|
+
data: UsageLog[],
|
|
17
|
+
): {
|
|
18
|
+
calls: number | string;
|
|
19
|
+
spend: number | string;
|
|
20
|
+
} => {
|
|
21
|
+
if (!data || data?.length === 0) return { calls: 0, spend: 0 };
|
|
22
|
+
|
|
23
|
+
const spend = data.reduce((acc, log) => acc + (log.totalSpend || 0), 0);
|
|
24
|
+
const calls = data.reduce((acc, log) => acc + (log.records?.length ?? 0), 0);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
calls: formatNumber(calls),
|
|
28
|
+
spend: formatNumber(spend),
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const MonthSpend = memo<UsageChartProps>(({ data, isLoading }) => {
|
|
33
|
+
const { t } = useTranslation('auth');
|
|
34
|
+
const theme = useTheme();
|
|
35
|
+
|
|
36
|
+
const { spend, calls } = computeMonth(data || []);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<StatisticCard
|
|
40
|
+
highlight={theme.blue}
|
|
41
|
+
loading={isLoading}
|
|
42
|
+
statistic={{
|
|
43
|
+
description: <Statistic title={t('usage.cards.month.modelCalls')} value={calls} />,
|
|
44
|
+
precision: 2,
|
|
45
|
+
prefix: '$',
|
|
46
|
+
value: spend,
|
|
47
|
+
}}
|
|
48
|
+
title={<TitleWithPercentage title={t('usage.cards.month.title')} />}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export default MonthSpend;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useTheme } from 'antd-style';
|
|
4
|
+
import dayjs from 'dayjs';
|
|
5
|
+
import utc from 'dayjs/plugin/utc';
|
|
6
|
+
import isToday from 'dayjs/plugin/isToday';
|
|
7
|
+
import isYesterday from 'dayjs/plugin/isYesterday';
|
|
8
|
+
import { memo } from 'react';
|
|
9
|
+
import { useTranslation } from 'react-i18next';
|
|
10
|
+
|
|
11
|
+
import Statistic from '@/components/Statistic';
|
|
12
|
+
import StatisticCard from '@/components/StatisticCard';
|
|
13
|
+
import TitleWithPercentage from '@/components/StatisticCard/TitleWithPercentage';
|
|
14
|
+
import { UsageLog } from '@/types/usage/usageRecord';
|
|
15
|
+
import { formatNumber } from '@/utils/format';
|
|
16
|
+
|
|
17
|
+
import { UsageChartProps } from '../../Client';
|
|
18
|
+
|
|
19
|
+
dayjs.extend(utc);
|
|
20
|
+
dayjs.extend(isToday);
|
|
21
|
+
dayjs.extend(isYesterday);
|
|
22
|
+
|
|
23
|
+
const computeSpend = (
|
|
24
|
+
data: UsageLog[],
|
|
25
|
+
): {
|
|
26
|
+
today: number | string;
|
|
27
|
+
yesterday: number | string;
|
|
28
|
+
} => {
|
|
29
|
+
if (!data || data?.length === 0) return { today: 0, yesterday: 0 };
|
|
30
|
+
|
|
31
|
+
const today = data.find((log) => dayjs.utc(log.day).isToday())?.totalSpend ?? 0;
|
|
32
|
+
const yesterday = data.find((log) => dayjs.utc(log.day).isYesterday())?.totalSpend ?? 0;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
today: formatNumber(today),
|
|
36
|
+
yesterday: formatNumber(yesterday),
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const TodaySpend = memo<UsageChartProps>(({ data, isLoading }) => {
|
|
41
|
+
const { t } = useTranslation('auth');
|
|
42
|
+
const theme = useTheme();
|
|
43
|
+
|
|
44
|
+
const { today, yesterday } = computeSpend(data || []);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<StatisticCard
|
|
48
|
+
highlight={theme.green}
|
|
49
|
+
loading={isLoading}
|
|
50
|
+
statistic={{
|
|
51
|
+
description: <Statistic title={t('usage.cards.today.yesterday')} value={yesterday} />,
|
|
52
|
+
precision: 2,
|
|
53
|
+
prefix: '$',
|
|
54
|
+
value: today,
|
|
55
|
+
}}
|
|
56
|
+
title={
|
|
57
|
+
<TitleWithPercentage
|
|
58
|
+
count={typeof today === 'number' ? today : 0}
|
|
59
|
+
prvCount={typeof yesterday === 'number' ? yesterday : 0}
|
|
60
|
+
title={t('usage.cards.today.title')}
|
|
61
|
+
/>
|
|
62
|
+
}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export default TodaySpend;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { memo } from 'react';
|
|
2
|
+
import { Flexbox } from 'react-layout-kit';
|
|
3
|
+
|
|
4
|
+
import { UsageChartProps } from '../../Client';
|
|
5
|
+
import ActiveModels from './ActiveModels';
|
|
6
|
+
import MonthSpend from './MonthSpend';
|
|
7
|
+
import TodaySpend from './TodaySpend';
|
|
8
|
+
|
|
9
|
+
const UsageCards = memo<UsageChartProps>(({ isLoading, data, groupBy }) => {
|
|
10
|
+
return (
|
|
11
|
+
<Flexbox gap={16} horizontal>
|
|
12
|
+
<TodaySpend data={data} isLoading={isLoading} />
|
|
13
|
+
<MonthSpend data={data} isLoading={isLoading} />
|
|
14
|
+
<ActiveModels data={data} groupBy={groupBy} isLoading={isLoading} />
|
|
15
|
+
</Flexbox>
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export default UsageCards;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { ProviderIcon } from '@lobehub/icons';
|
|
2
|
+
import { Tag } from '@lobehub/ui';
|
|
3
|
+
import { Table, TableColumnType, Typography } from 'antd';
|
|
4
|
+
import { useTheme } from 'antd-style';
|
|
5
|
+
import { parseAsInteger, useQueryState } from 'nuqs';
|
|
6
|
+
import { memo, useEffect } from 'react';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
import { Flexbox } from 'react-layout-kit';
|
|
9
|
+
|
|
10
|
+
import { useClientDataSWR } from '@/libs/swr';
|
|
11
|
+
import { usageService } from '@/services/usage';
|
|
12
|
+
import { formatDate, formatNumber } from '@/utils/format';
|
|
13
|
+
|
|
14
|
+
import { UsageChartProps } from '../Client';
|
|
15
|
+
|
|
16
|
+
const UsageTable = memo<UsageChartProps>(({ dateStrings }) => {
|
|
17
|
+
const theme = useTheme();
|
|
18
|
+
const { t } = useTranslation('auth');
|
|
19
|
+
|
|
20
|
+
const { data, isLoading, mutate } = useClientDataSWR('usage-logs', async () =>
|
|
21
|
+
usageService.findByMonth(dateStrings),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const [currentPage, setCurrentPage] = useQueryState(
|
|
25
|
+
'current',
|
|
26
|
+
parseAsInteger.withDefault(1).withOptions({ clearOnDefault: true }),
|
|
27
|
+
);
|
|
28
|
+
const [pageSize, setPageSize] = useQueryState(
|
|
29
|
+
'pageSize',
|
|
30
|
+
parseAsInteger.withDefault(5).withOptions({ clearOnDefault: true }),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (dateStrings) {
|
|
35
|
+
mutate();
|
|
36
|
+
}
|
|
37
|
+
}, [dateStrings]);
|
|
38
|
+
|
|
39
|
+
const columns: TableColumnType<any>[] = [
|
|
40
|
+
{
|
|
41
|
+
hidden: true,
|
|
42
|
+
key: 'id',
|
|
43
|
+
title: 'ID',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
dataIndex: 'model',
|
|
47
|
+
key: 'model',
|
|
48
|
+
render: (value, record) => (
|
|
49
|
+
<Flexbox align={'start'} gap={16} horizontal>
|
|
50
|
+
<ProviderIcon
|
|
51
|
+
provider={record.provider}
|
|
52
|
+
size={18}
|
|
53
|
+
style={{
|
|
54
|
+
border: `2px solid ${theme.colorBgContainer}`,
|
|
55
|
+
boxSizing: 'content-box',
|
|
56
|
+
marginRight: -8,
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
<Typography.Text>
|
|
60
|
+
{value?.length > 12 ? `${value.slice(0, 12)}...` : value}
|
|
61
|
+
</Typography.Text>
|
|
62
|
+
</Flexbox>
|
|
63
|
+
),
|
|
64
|
+
title: t('usage.table.model'),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
dataIndex: 'type',
|
|
68
|
+
filters: [
|
|
69
|
+
{
|
|
70
|
+
text: 'Chat',
|
|
71
|
+
value: 'chat',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
key: 'type',
|
|
75
|
+
onFilter: (value, record) => record.callType === value,
|
|
76
|
+
render: (value) => {
|
|
77
|
+
return <Tag>{value}</Tag>;
|
|
78
|
+
},
|
|
79
|
+
title: t('usage.table.type'),
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
dataIndex: 'totalInputTokens',
|
|
83
|
+
key: 'inputTokens',
|
|
84
|
+
title: t('usage.table.inputTokens'),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
dataIndex: 'totalOutputTokens',
|
|
88
|
+
key: 'outputTokens',
|
|
89
|
+
title: t('usage.table.outputTokens'),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
dataIndex: 'tps',
|
|
93
|
+
key: 'tps',
|
|
94
|
+
render: (value) => formatNumber(value, 2),
|
|
95
|
+
title: t('usage.table.tps'),
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
dataIndex: 'ttft',
|
|
99
|
+
key: 'ttft',
|
|
100
|
+
render: (value) => formatNumber(value / 1000, 2),
|
|
101
|
+
title: t('usage.table.ttft'),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
dataIndex: 'spend',
|
|
105
|
+
key: 'spend',
|
|
106
|
+
render: (value) => {
|
|
107
|
+
return `$${formatNumber(value, 6)}`;
|
|
108
|
+
},
|
|
109
|
+
title: t('usage.table.spend'),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
dataIndex: 'createdAt',
|
|
113
|
+
key: 'createdAt',
|
|
114
|
+
render: (value) => {
|
|
115
|
+
return formatDate(new Date(value));
|
|
116
|
+
},
|
|
117
|
+
sortDirections: ['descend'],
|
|
118
|
+
sorter: (a, b) => a.createdAt - b.createdAt,
|
|
119
|
+
title: t('usage.table.createdAt'),
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<Table
|
|
125
|
+
columns={columns}
|
|
126
|
+
dataSource={data}
|
|
127
|
+
key="id"
|
|
128
|
+
loading={isLoading}
|
|
129
|
+
pagination={{
|
|
130
|
+
current: currentPage,
|
|
131
|
+
onChange: (page) => {
|
|
132
|
+
setCurrentPage(page);
|
|
133
|
+
},
|
|
134
|
+
onShowSizeChange: (current, size) => {
|
|
135
|
+
setCurrentPage(current);
|
|
136
|
+
setPageSize(size);
|
|
137
|
+
},
|
|
138
|
+
pageSize,
|
|
139
|
+
}}
|
|
140
|
+
size="small"
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export default UsageTable;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { type BarChartProps } from '@lobehub/charts';
|
|
2
|
+
import { Segmented } from '@lobehub/ui';
|
|
3
|
+
import { memo, useState } from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
6
|
+
import StatisticCard from '@/components/StatisticCard';
|
|
7
|
+
import { UsageLog } from '@/types/usage/usageRecord';
|
|
8
|
+
import { formatNumber } from '@/utils/format';
|
|
9
|
+
|
|
10
|
+
import { GroupBy, UsageChartProps } from '../Client';
|
|
11
|
+
import { UsageBarChart } from './components/UsageBarChart';
|
|
12
|
+
|
|
13
|
+
const groupByType = (
|
|
14
|
+
data: UsageLog[],
|
|
15
|
+
type: 'spend' | 'token',
|
|
16
|
+
groupBy: GroupBy,
|
|
17
|
+
): { categories: string[]; data: BarChartProps['data'] } => {
|
|
18
|
+
if (!data || data?.length === 0) return { categories: [], data: [] };
|
|
19
|
+
let formattedData: BarChartProps['data'] = [];
|
|
20
|
+
let cate: Map<string, number> = data.reduce((acc, log) => {
|
|
21
|
+
if (log.records) {
|
|
22
|
+
for (const item of log.records) {
|
|
23
|
+
if (groupBy === GroupBy.Model && item.model) {
|
|
24
|
+
acc.set(item.model, 0);
|
|
25
|
+
} else if (groupBy === GroupBy.Provider && item.provider) {
|
|
26
|
+
acc.set(item.provider, 0);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return acc;
|
|
31
|
+
}, new Map<string, number>());
|
|
32
|
+
const categories: string[] = Array.from(cate.keys());
|
|
33
|
+
formattedData = data.map((log) => {
|
|
34
|
+
const totalObj = {
|
|
35
|
+
day: log.day,
|
|
36
|
+
total: type === 'spend' ? log.totalSpend : log.totalTokens,
|
|
37
|
+
};
|
|
38
|
+
let todayCate = new Map<string, number>(cate);
|
|
39
|
+
for (const item of log.records) {
|
|
40
|
+
const value = type === 'spend' ? item.spend || 0 : item.totalTokens || 0;
|
|
41
|
+
const key = groupBy === GroupBy.Model ? item.model : item.provider;
|
|
42
|
+
let displayValue = (todayCate.get(key) || 0) + value;
|
|
43
|
+
if (type === 'spend') {
|
|
44
|
+
const formattedNum = formatNumber((todayCate.get(key) || 0) + value, 2);
|
|
45
|
+
if (typeof formattedNum !== 'string') {
|
|
46
|
+
displayValue = formattedNum;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
todayCate.set(key, displayValue);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
...totalObj,
|
|
53
|
+
...Object.fromEntries(todayCate.entries()),
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
categories,
|
|
58
|
+
data: formattedData,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
enum ShowType {
|
|
63
|
+
Spend = 'spend',
|
|
64
|
+
Token = 'token',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const UsageTrends = memo<UsageChartProps>(({ isLoading, data, groupBy }) => {
|
|
68
|
+
const { t } = useTranslation('auth');
|
|
69
|
+
|
|
70
|
+
const [type, setType] = useState<ShowType>(ShowType.Spend);
|
|
71
|
+
|
|
72
|
+
const { categories: spendCate, data: spendData } = groupByType(
|
|
73
|
+
data || [],
|
|
74
|
+
'spend',
|
|
75
|
+
groupBy || GroupBy.Model,
|
|
76
|
+
);
|
|
77
|
+
const { categories: tokenCate, data: tokenData } = groupByType(
|
|
78
|
+
data || [],
|
|
79
|
+
'token',
|
|
80
|
+
groupBy || GroupBy.Model,
|
|
81
|
+
);
|
|
82
|
+
return (
|
|
83
|
+
<StatisticCard
|
|
84
|
+
chart={
|
|
85
|
+
data &&
|
|
86
|
+
(type === ShowType.Spend ? (
|
|
87
|
+
<UsageBarChart categories={spendCate} data={spendData} index="day" />
|
|
88
|
+
) : (
|
|
89
|
+
<UsageBarChart categories={tokenCate} data={tokenData} index="day" />
|
|
90
|
+
))
|
|
91
|
+
}
|
|
92
|
+
extra={
|
|
93
|
+
<Segmented
|
|
94
|
+
onChange={(value) => setType(value as ShowType)}
|
|
95
|
+
options={[
|
|
96
|
+
{ label: t('usage.trends.spend'), value: ShowType.Spend },
|
|
97
|
+
{ label: t('usage.trends.tokens'), value: ShowType.Token },
|
|
98
|
+
]}
|
|
99
|
+
value={type}
|
|
100
|
+
/>
|
|
101
|
+
}
|
|
102
|
+
loading={isLoading}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
export default UsageTrends;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Divider, Typography } from 'antd'
|
|
2
|
+
import { Flexbox } from 'react-layout-kit';
|
|
3
|
+
import { BarChart, ChartTooltipFrame, ChartTooltipRow, type BarChartProps } from '@lobehub/charts';
|
|
4
|
+
|
|
5
|
+
export const UsageBarChart = ({ ...props }: BarChartProps) => (
|
|
6
|
+
<BarChart
|
|
7
|
+
{...props}
|
|
8
|
+
customTooltip={({ active, payload, label, valueFormatter }) => {
|
|
9
|
+
if (active && payload) {
|
|
10
|
+
return (
|
|
11
|
+
<ChartTooltipFrame>
|
|
12
|
+
<Flexbox
|
|
13
|
+
horizontal
|
|
14
|
+
justify={'space-between'}
|
|
15
|
+
paddingBlock={8}
|
|
16
|
+
paddingInline={16}
|
|
17
|
+
>
|
|
18
|
+
<Typography.Paragraph ellipsis style={{ margin: 0 }}>
|
|
19
|
+
{label}
|
|
20
|
+
</Typography.Paragraph>
|
|
21
|
+
<span style={{ fontWeight: 'bold' }}>
|
|
22
|
+
{payload.reduce((acc: number, cur: any) => acc + cur.value, 0)}
|
|
23
|
+
</span>
|
|
24
|
+
</Flexbox>
|
|
25
|
+
<Divider style={{ margin: 0 }} />
|
|
26
|
+
<Flexbox
|
|
27
|
+
gap={4}
|
|
28
|
+
paddingBlock={8}
|
|
29
|
+
paddingInline={16}
|
|
30
|
+
style={{ flexDirection: 'column-reverse', marginTop: 4 }}
|
|
31
|
+
>
|
|
32
|
+
{payload.map(({ value, color, name }: any, idx: number) => (
|
|
33
|
+
typeof value === 'number' && value > 0 ?
|
|
34
|
+
<ChartTooltipRow
|
|
35
|
+
color={color}
|
|
36
|
+
key={`id-${idx}`}
|
|
37
|
+
name={name}
|
|
38
|
+
value={(valueFormatter as any)?.(value)}
|
|
39
|
+
/> : null
|
|
40
|
+
))}
|
|
41
|
+
</Flexbox>
|
|
42
|
+
</ChartTooltipFrame>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { metadataModule } from '@/server/metadata';
|
|
2
|
+
import { translation } from '@/server/translation';
|
|
3
|
+
import { DynamicLayoutProps } from '@/types/next';
|
|
4
|
+
import { RouteVariants } from '@/utils/server/routeVariants';
|
|
5
|
+
|
|
6
|
+
import Client from './Client';
|
|
7
|
+
|
|
8
|
+
export const generateMetadata = async (props: DynamicLayoutProps) => {
|
|
9
|
+
const locale = await RouteVariants.getLocale(props);
|
|
10
|
+
const { t } = await translation('auth', locale);
|
|
11
|
+
return metadataModule.generate({
|
|
12
|
+
description: t('header.desc'),
|
|
13
|
+
title: t('tab.usage'),
|
|
14
|
+
url: '/profile/usage',
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const Page = async (props: DynamicLayoutProps) => {
|
|
19
|
+
const mobile = await RouteVariants.getIsMobile(props);
|
|
20
|
+
return <Client mobile={mobile} />;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default Page;
|
|
@@ -46,8 +46,8 @@ const CustomRender = memo<CustomRenderProps>(
|
|
|
46
46
|
const [loading] = useChatStore((s) => [messageStateSelectors.isPluginApiInvoking(id)(s)]);
|
|
47
47
|
const [isEditing, setIsEditing] = useState(false);
|
|
48
48
|
const { message } = App.useApp();
|
|
49
|
-
const [
|
|
50
|
-
s.
|
|
49
|
+
const [optimisticUpdatePluginArguments, reInvokeToolMessage] = useChatStore((s) => [
|
|
50
|
+
s.optimisticUpdatePluginArguments,
|
|
51
51
|
s.reInvokeToolMessage,
|
|
52
52
|
]);
|
|
53
53
|
const handleCancel = useCallback(() => {
|
|
@@ -62,7 +62,7 @@ const CustomRender = memo<CustomRenderProps>(
|
|
|
62
62
|
const newArgsString = JSON.stringify(editedObject, null, 2);
|
|
63
63
|
|
|
64
64
|
if (newArgsString !== requestArgs) {
|
|
65
|
-
await
|
|
65
|
+
await optimisticUpdatePluginArguments(id, editedObject, true);
|
|
66
66
|
await reInvokeToolMessage(id);
|
|
67
67
|
}
|
|
68
68
|
setIsEditing(false);
|