@lobehub/lobehub 2.0.0-next.37 → 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 +50 -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 +14 -0
- package/locales/ar/auth.json +45 -1
- package/locales/ar/modelProvider.json +13 -1
- package/locales/bg-BG/auth.json +45 -1
- package/locales/bg-BG/modelProvider.json +13 -1
- package/locales/de-DE/auth.json +45 -1
- package/locales/de-DE/modelProvider.json +13 -1
- package/locales/en-US/auth.json +45 -1
- package/locales/en-US/modelProvider.json +13 -1
- package/locales/es-ES/auth.json +45 -1
- package/locales/es-ES/modelProvider.json +13 -1
- package/locales/fa-IR/auth.json +45 -1
- package/locales/fa-IR/modelProvider.json +13 -1
- package/locales/fr-FR/auth.json +45 -1
- package/locales/fr-FR/modelProvider.json +13 -1
- package/locales/it-IT/auth.json +45 -1
- package/locales/it-IT/modelProvider.json +13 -1
- package/locales/ja-JP/auth.json +45 -1
- package/locales/ja-JP/modelProvider.json +13 -1
- package/locales/ko-KR/auth.json +45 -1
- package/locales/ko-KR/modelProvider.json +13 -1
- package/locales/nl-NL/auth.json +45 -1
- package/locales/nl-NL/modelProvider.json +13 -1
- package/locales/pl-PL/auth.json +45 -1
- package/locales/pl-PL/modelProvider.json +13 -1
- package/locales/pt-BR/auth.json +45 -1
- package/locales/pt-BR/modelProvider.json +13 -1
- package/locales/ru-RU/auth.json +45 -1
- package/locales/ru-RU/modelProvider.json +13 -1
- package/locales/tr-TR/auth.json +45 -1
- package/locales/tr-TR/modelProvider.json +13 -1
- package/locales/vi-VN/auth.json +45 -1
- package/locales/vi-VN/modelProvider.json +13 -1
- package/locales/zh-CN/auth.json +45 -1
- package/locales/zh-CN/modelProvider.json +13 -1
- package/locales/zh-TW/auth.json +45 -1
- package/locales/zh-TW/modelProvider.json +13 -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/packages/web-crawler/src/crawImpl/browserless.ts +1 -1
- package/packages/web-crawler/src/crawImpl/naive.ts +9 -9
- package/packages/web-crawler/src/crawler.ts +5 -5
- package/packages/web-crawler/src/urlRules.ts +13 -13
- package/packages/web-crawler/src/utils/appUrlRules.ts +5 -5
- 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/components/Extras/Usage/UsageDetail/index.tsx +3 -0
- 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
package/src/app/[variants]/(main)/profile/usage/features/UsageCards/ActiveModels/ModelTable.tsx
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { CategoryBar, useThemeColorRange } from '@lobehub/charts';
|
|
2
|
+
import { ModelIcon, ProviderIcon } from '@lobehub/icons';
|
|
3
|
+
import { Collapse, Tag } from '@lobehub/ui';
|
|
4
|
+
import { Skeleton } from 'antd';
|
|
5
|
+
import { useTheme } from 'antd-style';
|
|
6
|
+
import { memo, useMemo } from 'react';
|
|
7
|
+
import { useTranslation } from 'react-i18next';
|
|
8
|
+
import { Flexbox } from 'react-layout-kit';
|
|
9
|
+
|
|
10
|
+
import InlineTable from '@/components/InlineTable';
|
|
11
|
+
import { UsageLog } from '@/types/usage/usageRecord';
|
|
12
|
+
import { formatPrice } from '@/utils/format';
|
|
13
|
+
|
|
14
|
+
import { GroupBy, UsageChartProps } from '../../../Client';
|
|
15
|
+
|
|
16
|
+
interface WeightGroup {
|
|
17
|
+
id: string;
|
|
18
|
+
spend: number | string;
|
|
19
|
+
weight: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const formatData = (
|
|
23
|
+
data: UsageLog[],
|
|
24
|
+
groupBy: GroupBy,
|
|
25
|
+
): {
|
|
26
|
+
childrens: WeightGroup[];
|
|
27
|
+
id: string;
|
|
28
|
+
totalSpend: number;
|
|
29
|
+
}[] => {
|
|
30
|
+
if (!data || data?.length === 0) return [];
|
|
31
|
+
|
|
32
|
+
const requestLogs = data.flatMap((log) => log.records);
|
|
33
|
+
const groupedLogs = requestLogs.reduce((acc, log) => {
|
|
34
|
+
const key = groupBy === GroupBy.Model ? log.model : log.provider;
|
|
35
|
+
if (!acc.has(key)) {
|
|
36
|
+
acc.set(key, []);
|
|
37
|
+
}
|
|
38
|
+
acc.get(key)?.push(log);
|
|
39
|
+
return acc;
|
|
40
|
+
}, new Map<string, UsageLog['records']>());
|
|
41
|
+
|
|
42
|
+
return Array.from(groupedLogs.entries())
|
|
43
|
+
.map(([key, logs]) => {
|
|
44
|
+
// 此处的 logs 为多日的 log,需要进行 sum
|
|
45
|
+
// 如果当前的 groupBy 是 Model,则 logs 应该按照 Provider 进行分组
|
|
46
|
+
const spend = logs.reduce((acc, log) => {
|
|
47
|
+
const key = groupBy === GroupBy.Model ? log.provider : log.model;
|
|
48
|
+
acc.set(key, (acc.get(key) || 0) + log.spend);
|
|
49
|
+
return acc;
|
|
50
|
+
}, new Map<string, number>());
|
|
51
|
+
|
|
52
|
+
const totalSpend = logs.reduce((total, log) => total + (log.spend || 0), 0);
|
|
53
|
+
|
|
54
|
+
const spendWithWeight = Array.from(
|
|
55
|
+
spend.entries().map(([key, value]) => {
|
|
56
|
+
return {
|
|
57
|
+
id: key,
|
|
58
|
+
spend: value,
|
|
59
|
+
weight: totalSpend > 0 ? value / totalSpend : 0,
|
|
60
|
+
};
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
childrens: spendWithWeight.sort((a, b) => b.weight - a.weight),
|
|
66
|
+
id: key,
|
|
67
|
+
totalSpend: totalSpend,
|
|
68
|
+
};
|
|
69
|
+
})
|
|
70
|
+
.sort((a, b) => b.totalSpend - a.totalSpend);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const ModelTable = memo<UsageChartProps>(({ data, isLoading, groupBy }) => {
|
|
74
|
+
const { t } = useTranslation('auth');
|
|
75
|
+
const theme = useTheme();
|
|
76
|
+
const themeColorRange = useThemeColorRange();
|
|
77
|
+
|
|
78
|
+
const formattedData = useMemo(
|
|
79
|
+
() => formatData(data || [], groupBy || GroupBy.Model),
|
|
80
|
+
[data, groupBy],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
console.log('ModelTable', groupBy, formattedData);
|
|
84
|
+
|
|
85
|
+
return isLoading ? (
|
|
86
|
+
<Skeleton active paragraph={{ rows: 8 }} title={false} />
|
|
87
|
+
) : (
|
|
88
|
+
<Collapse
|
|
89
|
+
defaultActiveKey={formattedData.map((item) => item.id)}
|
|
90
|
+
expandIconPosition={'end'}
|
|
91
|
+
gap={16}
|
|
92
|
+
items={formattedData.map((item) => {
|
|
93
|
+
const key = item.id;
|
|
94
|
+
return {
|
|
95
|
+
children: (
|
|
96
|
+
<Flexbox>
|
|
97
|
+
<CategoryBar
|
|
98
|
+
colors={themeColorRange}
|
|
99
|
+
showLabels={false}
|
|
100
|
+
size={2}
|
|
101
|
+
values={item.childrens.map((item) => item.weight)}
|
|
102
|
+
/>
|
|
103
|
+
<InlineTable
|
|
104
|
+
columns={[
|
|
105
|
+
{
|
|
106
|
+
dataIndex: 'id',
|
|
107
|
+
key: 'id',
|
|
108
|
+
render: (value, record, index) => {
|
|
109
|
+
return (
|
|
110
|
+
<Flexbox align={'center'} gap={12} horizontal key={value}>
|
|
111
|
+
{groupBy === GroupBy.Provider ? (
|
|
112
|
+
<ProviderIcon
|
|
113
|
+
provider={record.id}
|
|
114
|
+
style={{
|
|
115
|
+
boxShadow: `0 0 0 2px ${theme.colorBgContainer}, 0 0 0 4px ${themeColorRange[index]}`,
|
|
116
|
+
boxSizing: 'content-box',
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
) : (
|
|
120
|
+
<ModelIcon
|
|
121
|
+
model={record.id}
|
|
122
|
+
style={{
|
|
123
|
+
boxShadow: `0 0 0 2px ${theme.colorBgContainer}, 0 0 0 4px ${themeColorRange[index]}`,
|
|
124
|
+
boxSizing: 'content-box',
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
{value}
|
|
129
|
+
</Flexbox>
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
title:
|
|
133
|
+
groupBy === GroupBy.Model
|
|
134
|
+
? t('usage.activeModels.table.provider')
|
|
135
|
+
: t('usage.activeModels.table.model'),
|
|
136
|
+
width: 200,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
dataIndex: 'spend',
|
|
140
|
+
key: 'spend',
|
|
141
|
+
render: (value) => {
|
|
142
|
+
return `$${formatPrice(value)}`;
|
|
143
|
+
},
|
|
144
|
+
title: t('usage.activeModels.table.spend'),
|
|
145
|
+
},
|
|
146
|
+
]}
|
|
147
|
+
dataSource={item.childrens}
|
|
148
|
+
hoverToActive={false}
|
|
149
|
+
loading={isLoading}
|
|
150
|
+
rowKey={(record) => record.id}
|
|
151
|
+
/>
|
|
152
|
+
</Flexbox>
|
|
153
|
+
),
|
|
154
|
+
extra: <Tag>{item?.childrens?.length ?? 0}</Tag>,
|
|
155
|
+
key,
|
|
156
|
+
label: (
|
|
157
|
+
<Flexbox align={'center'} gap={8} horizontal>
|
|
158
|
+
{groupBy === GroupBy.Model ? (
|
|
159
|
+
<ModelIcon model={key} size={24} />
|
|
160
|
+
) : (
|
|
161
|
+
<ProviderIcon provider={key} size={24} />
|
|
162
|
+
)}
|
|
163
|
+
{key}
|
|
164
|
+
</Flexbox>
|
|
165
|
+
),
|
|
166
|
+
};
|
|
167
|
+
})}
|
|
168
|
+
padding={{
|
|
169
|
+
body: 0,
|
|
170
|
+
}}
|
|
171
|
+
/>
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
export default ModelTable;
|
|
@@ -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;
|