@lobehub/chat 1.68.3 → 1.68.5
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/README.md +3 -3
- package/README.zh-CN.md +14 -17
- package/changelog/v1.json +18 -0
- package/docs/usage/providers/azureai.mdx +69 -0
- package/docs/usage/providers/azureai.zh-CN.mdx +69 -0
- package/docs/usage/providers/deepseek.mdx +3 -3
- package/docs/usage/providers/deepseek.zh-CN.mdx +5 -4
- package/docs/usage/providers/jina.mdx +51 -0
- package/docs/usage/providers/jina.zh-CN.mdx +51 -0
- package/docs/usage/providers/lmstudio.mdx +75 -0
- package/docs/usage/providers/lmstudio.zh-CN.mdx +75 -0
- package/docs/usage/providers/nvidia.mdx +55 -0
- package/docs/usage/providers/nvidia.zh-CN.mdx +55 -0
- package/docs/usage/providers/ppio.mdx +7 -7
- package/docs/usage/providers/ppio.zh-CN.mdx +6 -6
- package/docs/usage/providers/sambanova.mdx +50 -0
- package/docs/usage/providers/sambanova.zh-CN.mdx +50 -0
- package/docs/usage/providers/tencentcloud.mdx +49 -0
- package/docs/usage/providers/tencentcloud.zh-CN.mdx +49 -0
- package/docs/usage/providers/vertexai.mdx +59 -0
- package/docs/usage/providers/vertexai.zh-CN.mdx +59 -0
- package/docs/usage/providers/vllm.mdx +98 -0
- package/docs/usage/providers/vllm.zh-CN.mdx +98 -0
- package/docs/usage/providers/volcengine.mdx +47 -0
- package/docs/usage/providers/volcengine.zh-CN.mdx +48 -0
- package/locales/ar/chat.json +29 -0
- package/locales/ar/models.json +48 -0
- package/locales/ar/providers.json +3 -0
- package/locales/bg-BG/chat.json +29 -0
- package/locales/bg-BG/models.json +48 -0
- package/locales/bg-BG/providers.json +3 -0
- package/locales/de-DE/chat.json +29 -0
- package/locales/de-DE/models.json +48 -0
- package/locales/de-DE/providers.json +3 -0
- package/locales/en-US/chat.json +29 -0
- package/locales/en-US/models.json +48 -0
- package/locales/en-US/providers.json +3 -3
- package/locales/es-ES/chat.json +29 -0
- package/locales/es-ES/models.json +48 -0
- package/locales/es-ES/providers.json +3 -0
- package/locales/fa-IR/chat.json +29 -0
- package/locales/fa-IR/models.json +48 -0
- package/locales/fa-IR/providers.json +3 -0
- package/locales/fr-FR/chat.json +29 -0
- package/locales/fr-FR/models.json +48 -0
- package/locales/fr-FR/providers.json +3 -0
- package/locales/it-IT/chat.json +29 -0
- package/locales/it-IT/models.json +48 -0
- package/locales/it-IT/providers.json +3 -0
- package/locales/ja-JP/chat.json +29 -0
- package/locales/ja-JP/models.json +48 -0
- package/locales/ja-JP/providers.json +3 -0
- package/locales/ko-KR/chat.json +29 -0
- package/locales/ko-KR/models.json +48 -0
- package/locales/ko-KR/providers.json +3 -0
- package/locales/nl-NL/chat.json +29 -0
- package/locales/nl-NL/models.json +48 -0
- package/locales/nl-NL/providers.json +3 -0
- package/locales/pl-PL/chat.json +29 -0
- package/locales/pl-PL/models.json +48 -0
- package/locales/pl-PL/providers.json +3 -0
- package/locales/pt-BR/chat.json +29 -0
- package/locales/pt-BR/models.json +48 -0
- package/locales/pt-BR/providers.json +3 -0
- package/locales/ru-RU/chat.json +29 -0
- package/locales/ru-RU/models.json +48 -0
- package/locales/ru-RU/providers.json +3 -0
- package/locales/tr-TR/chat.json +29 -0
- package/locales/tr-TR/models.json +48 -0
- package/locales/tr-TR/providers.json +3 -0
- package/locales/vi-VN/chat.json +29 -0
- package/locales/vi-VN/models.json +48 -0
- package/locales/vi-VN/providers.json +3 -0
- package/locales/zh-CN/chat.json +29 -0
- package/locales/zh-CN/models.json +51 -3
- package/locales/zh-CN/providers.json +3 -4
- package/locales/zh-TW/chat.json +29 -0
- package/locales/zh-TW/models.json +48 -0
- package/locales/zh-TW/providers.json +3 -0
- package/package.json +1 -1
- package/packages/web-crawler/src/crawImpl/__test__/jina.test.ts +169 -0
- package/packages/web-crawler/src/crawImpl/naive.ts +29 -3
- package/packages/web-crawler/src/utils/errorType.ts +7 -0
- package/scripts/serverLauncher/startServer.js +11 -7
- package/src/config/modelProviders/index.ts +1 -1
- package/src/config/modelProviders/ppio.ts +1 -1
- package/src/features/Conversation/Extras/Assistant.tsx +12 -20
- package/src/features/Conversation/Extras/Usage/UsageDetail/ModelCard.tsx +130 -0
- package/src/features/Conversation/Extras/Usage/UsageDetail/TokenProgress.tsx +71 -0
- package/src/features/Conversation/Extras/Usage/UsageDetail/index.tsx +146 -0
- package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +94 -0
- package/src/features/Conversation/Extras/Usage/index.tsx +40 -0
- package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +14 -0
- package/src/libs/agent-runtime/utils/streams/anthropic.ts +25 -0
- package/src/libs/agent-runtime/utils/streams/openai.test.ts +100 -10
- package/src/libs/agent-runtime/utils/streams/openai.ts +30 -4
- package/src/libs/agent-runtime/utils/streams/protocol.ts +4 -0
- package/src/locales/default/chat.ts +30 -1
- package/src/server/routers/tools/search.ts +1 -1
- package/src/store/aiInfra/slices/aiModel/initialState.ts +3 -1
- package/src/store/aiInfra/slices/aiModel/selectors.test.ts +1 -0
- package/src/store/aiInfra/slices/aiModel/selectors.ts +5 -0
- package/src/store/aiInfra/slices/aiProvider/action.ts +3 -1
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +5 -1
- package/src/store/chat/slices/message/action.ts +3 -0
- package/src/store/global/initialState.ts +1 -0
- package/src/store/global/selectors/systemStatus.ts +2 -0
- package/src/types/message/base.ts +18 -0
- package/src/types/message/chat.ts +4 -3
- package/src/utils/fetch/fetchSSE.ts +24 -1
- package/src/utils/format.ts +3 -1
@@ -0,0 +1,169 @@
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import { jina } from '../jina';
|
4
|
+
|
5
|
+
describe('jina crawler', () => {
|
6
|
+
const mockFetch = vi.fn();
|
7
|
+
global.fetch = mockFetch;
|
8
|
+
|
9
|
+
beforeEach(() => {
|
10
|
+
vi.resetAllMocks();
|
11
|
+
});
|
12
|
+
|
13
|
+
it('should crawl url successfully', async () => {
|
14
|
+
const mockResponse = {
|
15
|
+
ok: true,
|
16
|
+
json: () =>
|
17
|
+
Promise.resolve({
|
18
|
+
code: 200,
|
19
|
+
data: {
|
20
|
+
content: 'test content',
|
21
|
+
description: 'test description',
|
22
|
+
siteName: 'test site',
|
23
|
+
title: 'test title',
|
24
|
+
},
|
25
|
+
}),
|
26
|
+
};
|
27
|
+
|
28
|
+
mockFetch.mockResolvedValue(mockResponse);
|
29
|
+
|
30
|
+
const result = await jina('https://example.com', {
|
31
|
+
apiKey: 'test-key',
|
32
|
+
filterOptions: {},
|
33
|
+
});
|
34
|
+
|
35
|
+
expect(mockFetch).toHaveBeenCalledWith('https://r.jina.ai/https://example.com', {
|
36
|
+
headers: {
|
37
|
+
'Accept': 'application/json',
|
38
|
+
'Authorization': 'Bearer test-key',
|
39
|
+
'x-send-from': 'LobeChat Community',
|
40
|
+
},
|
41
|
+
});
|
42
|
+
|
43
|
+
expect(result).toEqual({
|
44
|
+
content: 'test content',
|
45
|
+
contentType: 'text',
|
46
|
+
description: 'test description',
|
47
|
+
length: 12,
|
48
|
+
siteName: 'test site',
|
49
|
+
title: 'test title',
|
50
|
+
url: 'https://example.com',
|
51
|
+
});
|
52
|
+
});
|
53
|
+
|
54
|
+
it('should use JINA_READER_API_KEY from env if apiKey not provided', async () => {
|
55
|
+
process.env.JINA_READER_API_KEY = 'env-reader-key';
|
56
|
+
|
57
|
+
const mockResponse = {
|
58
|
+
ok: true,
|
59
|
+
json: () =>
|
60
|
+
Promise.resolve({
|
61
|
+
code: 200,
|
62
|
+
data: {
|
63
|
+
content: 'test content',
|
64
|
+
},
|
65
|
+
}),
|
66
|
+
};
|
67
|
+
|
68
|
+
mockFetch.mockResolvedValue(mockResponse);
|
69
|
+
|
70
|
+
await jina('https://example.com', { filterOptions: {} });
|
71
|
+
|
72
|
+
expect(mockFetch).toHaveBeenCalledWith('https://r.jina.ai/https://example.com', {
|
73
|
+
headers: {
|
74
|
+
'Accept': 'application/json',
|
75
|
+
'Authorization': 'Bearer env-reader-key',
|
76
|
+
'x-send-from': 'LobeChat Community',
|
77
|
+
},
|
78
|
+
});
|
79
|
+
|
80
|
+
delete process.env.JINA_READER_API_KEY;
|
81
|
+
});
|
82
|
+
|
83
|
+
it('should use JINA_API_KEY from env if apiKey and JINA_READER_API_KEY not provided', async () => {
|
84
|
+
process.env.JINA_API_KEY = 'env-key';
|
85
|
+
|
86
|
+
const mockResponse = {
|
87
|
+
ok: true,
|
88
|
+
json: () =>
|
89
|
+
Promise.resolve({
|
90
|
+
code: 200,
|
91
|
+
data: {
|
92
|
+
content: 'test content',
|
93
|
+
},
|
94
|
+
}),
|
95
|
+
};
|
96
|
+
|
97
|
+
mockFetch.mockResolvedValue(mockResponse);
|
98
|
+
|
99
|
+
await jina('https://example.com', { filterOptions: {} });
|
100
|
+
|
101
|
+
expect(mockFetch).toHaveBeenCalledWith('https://r.jina.ai/https://example.com', {
|
102
|
+
headers: {
|
103
|
+
'Accept': 'application/json',
|
104
|
+
'Authorization': 'Bearer env-key',
|
105
|
+
'x-send-from': 'LobeChat Community',
|
106
|
+
},
|
107
|
+
});
|
108
|
+
|
109
|
+
delete process.env.JINA_API_KEY;
|
110
|
+
});
|
111
|
+
|
112
|
+
it('should send empty Authorization header if no api key provided', async () => {
|
113
|
+
const mockResponse = {
|
114
|
+
ok: true,
|
115
|
+
json: () =>
|
116
|
+
Promise.resolve({
|
117
|
+
code: 200,
|
118
|
+
data: {
|
119
|
+
content: 'test content',
|
120
|
+
},
|
121
|
+
}),
|
122
|
+
};
|
123
|
+
|
124
|
+
mockFetch.mockResolvedValue(mockResponse);
|
125
|
+
|
126
|
+
await jina('https://example.com', { filterOptions: {} });
|
127
|
+
|
128
|
+
expect(mockFetch).toHaveBeenCalledWith('https://r.jina.ai/https://example.com', {
|
129
|
+
headers: {
|
130
|
+
'Accept': 'application/json',
|
131
|
+
'Authorization': '',
|
132
|
+
'x-send-from': 'LobeChat Community',
|
133
|
+
},
|
134
|
+
});
|
135
|
+
});
|
136
|
+
|
137
|
+
it('should return undefined if response is not ok', async () => {
|
138
|
+
mockFetch.mockResolvedValue({ ok: false });
|
139
|
+
|
140
|
+
const result = await jina('https://example.com', { filterOptions: {} });
|
141
|
+
|
142
|
+
expect(result).toBeUndefined();
|
143
|
+
});
|
144
|
+
|
145
|
+
it('should return undefined if response code is not 200', async () => {
|
146
|
+
const mockResponse = {
|
147
|
+
ok: true,
|
148
|
+
json: () =>
|
149
|
+
Promise.resolve({
|
150
|
+
code: 400,
|
151
|
+
message: 'Bad Request',
|
152
|
+
}),
|
153
|
+
};
|
154
|
+
|
155
|
+
mockFetch.mockResolvedValue(mockResponse);
|
156
|
+
|
157
|
+
const result = await jina('https://example.com', { filterOptions: {} });
|
158
|
+
|
159
|
+
expect(result).toBeUndefined();
|
160
|
+
});
|
161
|
+
|
162
|
+
it('should return undefined if fetch throws error', async () => {
|
163
|
+
mockFetch.mockRejectedValue(new Error('Network error'));
|
164
|
+
|
165
|
+
const result = await jina('https://example.com', { filterOptions: {} });
|
166
|
+
|
167
|
+
expect(result).toBeUndefined();
|
168
|
+
});
|
169
|
+
});
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { CrawlImpl, CrawlSuccessResult } from '../type';
|
2
|
-
import { NetworkConnectionError, PageNotFoundError } from '../utils/errorType';
|
2
|
+
import { NetworkConnectionError, PageNotFoundError, TimeoutError } from '../utils/errorType';
|
3
3
|
import { htmlToMarkdown } from '../utils/htmlToMarkdown';
|
4
4
|
|
5
5
|
const mixinHeaders = {
|
@@ -31,15 +31,41 @@ const mixinHeaders = {
|
|
31
31
|
'sec-fetch-user': '?1',
|
32
32
|
};
|
33
33
|
|
34
|
+
const TIMEOUT_CONTROL = 10_000;
|
35
|
+
|
36
|
+
const withTimeout = <T>(promise: Promise<T>, ms: number): Promise<T> => {
|
37
|
+
const controller = new AbortController();
|
38
|
+
const timeoutPromise = new Promise<T>((_, reject) => {
|
39
|
+
setTimeout(() => {
|
40
|
+
controller.abort();
|
41
|
+
reject(new TimeoutError(`Request timeout after ${ms}ms`));
|
42
|
+
}, ms);
|
43
|
+
});
|
44
|
+
|
45
|
+
return Promise.race([promise, timeoutPromise]);
|
46
|
+
};
|
47
|
+
|
34
48
|
export const naive: CrawlImpl = async (url, { filterOptions }) => {
|
35
49
|
let res: Response;
|
36
50
|
|
37
51
|
try {
|
38
|
-
res = await
|
52
|
+
res = await withTimeout(
|
53
|
+
fetch(url, {
|
54
|
+
headers: mixinHeaders,
|
55
|
+
signal: new AbortController().signal,
|
56
|
+
}),
|
57
|
+
TIMEOUT_CONTROL,
|
58
|
+
);
|
39
59
|
} catch (e) {
|
40
|
-
|
60
|
+
const error = e as Error;
|
61
|
+
if (error.message === 'fetch failed') {
|
41
62
|
throw new NetworkConnectionError();
|
42
63
|
}
|
64
|
+
|
65
|
+
if (error instanceof TimeoutError) {
|
66
|
+
throw error;
|
67
|
+
}
|
68
|
+
|
43
69
|
throw e;
|
44
70
|
}
|
45
71
|
|
@@ -74,20 +74,24 @@ const runProxyChainsConfGenerator = async (url) => {
|
|
74
74
|
|
75
75
|
let ip = isValidIP(host, 4) ? host : await resolveHostIP(host, 4);
|
76
76
|
|
77
|
-
const
|
78
|
-
localnet 127.0.0.0/255.0.0.0
|
79
|
-
localnet 10.0.0.0/255.0.0.0
|
80
|
-
localnet 172.16.0.0/255.240.0.0
|
81
|
-
localnet 192.168.0.0/255.255.0.0
|
82
|
-
localnet ::1/128
|
77
|
+
const proxyDNSConfig = process.env.ENABLE_PROXY_DNS === '1' ? `
|
83
78
|
proxy_dns
|
84
79
|
remote_dns_subnet 224
|
80
|
+
`.trim() : '';
|
81
|
+
|
82
|
+
const configContent = `
|
83
|
+
localnet 127.0.0.0/8
|
84
|
+
localnet 10.0.0.0/8
|
85
|
+
localnet 172.16.0.0/12
|
86
|
+
localnet 192.168.0.0/16
|
87
|
+
localnet ::/127
|
88
|
+
${proxyDNSConfig}
|
85
89
|
strict_chain
|
86
90
|
tcp_connect_time_out 8000
|
87
91
|
tcp_read_time_out 15000
|
88
92
|
[ProxyList]
|
89
93
|
${protocol} ${ip} ${port} ${user} ${pass}
|
90
|
-
`.trim();
|
94
|
+
`.replace(/\n{2,}/g, '\n').trim();
|
91
95
|
|
92
96
|
await fs.writeFile(PROXYCHAINS_CONF_PATH, configContent);
|
93
97
|
console.log(`✅ ProxyChains: All outgoing traffic routed via ${url}.`);
|
@@ -107,12 +107,12 @@ export const DEFAULT_MODEL_PROVIDER_LIST = [
|
|
107
107
|
GoogleProvider,
|
108
108
|
VertexAIProvider,
|
109
109
|
DeepSeekProvider,
|
110
|
-
PPIOProvider,
|
111
110
|
HuggingFaceProvider,
|
112
111
|
OpenRouterProvider,
|
113
112
|
CloudflareProvider,
|
114
113
|
GithubProvider,
|
115
114
|
NovitaProvider,
|
115
|
+
PPIOProvider,
|
116
116
|
NvidiaProvider,
|
117
117
|
TogetherAIProvider,
|
118
118
|
FireworksAIProvider,
|
@@ -243,7 +243,7 @@ const PPIO: ModelProviderCard = {
|
|
243
243
|
sdkType: 'openai',
|
244
244
|
showModelFetcher: true,
|
245
245
|
},
|
246
|
-
url: 'https://ppinfra.com
|
246
|
+
url: 'https://ppinfra.com/user/register?invited_by=RQIMOC',
|
247
247
|
};
|
248
248
|
|
249
249
|
export default PPIO;
|
@@ -1,9 +1,7 @@
|
|
1
|
-
import { ModelTag } from '@lobehub/icons';
|
2
1
|
import { memo } from 'react';
|
3
2
|
import { Flexbox } from 'react-layout-kit';
|
4
3
|
|
5
|
-
import {
|
6
|
-
import { agentSelectors } from '@/store/agent/slices/chat';
|
4
|
+
import { LOADING_FLAT } from '@/const/message';
|
7
5
|
import { useChatStore } from '@/store/chat';
|
8
6
|
import { chatSelectors } from '@/store/chat/selectors';
|
9
7
|
import { ChatMessage } from '@/types/message';
|
@@ -12,34 +10,28 @@ import { RenderMessageExtra } from '../types';
|
|
12
10
|
import ExtraContainer from './ExtraContainer';
|
13
11
|
import TTS from './TTS';
|
14
12
|
import Translate from './Translate';
|
13
|
+
import Usage from './Usage';
|
15
14
|
|
16
15
|
export const AssistantMessageExtra: RenderMessageExtra = memo<ChatMessage>(
|
17
|
-
({ extra, id, content }) => {
|
18
|
-
const model = useAgentStore(agentSelectors.currentAgentModel);
|
16
|
+
({ extra, id, content, metadata, tools }) => {
|
19
17
|
const loading = useChatStore(chatSelectors.isMessageGenerating(id));
|
20
18
|
|
21
|
-
const showModelTag = extra?.fromModel && model !== extra?.fromModel;
|
22
|
-
const showTranslate = !!extra?.translate;
|
23
|
-
const showTTS = !!extra?.tts;
|
24
|
-
|
25
|
-
const showExtra = showModelTag || showTranslate || showTTS;
|
26
|
-
|
27
|
-
if (!showExtra) return;
|
28
|
-
|
29
19
|
return (
|
30
|
-
<Flexbox gap={8} style={{ marginTop: 8 }}>
|
31
|
-
{
|
32
|
-
<
|
33
|
-
|
34
|
-
|
20
|
+
<Flexbox gap={8} style={{ marginTop: !!tools?.length ? 8 : 4 }}>
|
21
|
+
{content !== LOADING_FLAT && extra?.fromModel && (
|
22
|
+
<Usage
|
23
|
+
metadata={metadata || {}}
|
24
|
+
model={extra?.fromModel}
|
25
|
+
provider={extra.fromProvider!}
|
26
|
+
/>
|
35
27
|
)}
|
36
28
|
<>
|
37
|
-
{extra?.tts && (
|
29
|
+
{!!extra?.tts && (
|
38
30
|
<ExtraContainer>
|
39
31
|
<TTS content={content} id={id} loading={loading} {...extra?.tts} />
|
40
32
|
</ExtraContainer>
|
41
33
|
)}
|
42
|
-
{extra?.translate && (
|
34
|
+
{!!extra?.translate && (
|
43
35
|
<ExtraContainer>
|
44
36
|
<Translate id={id} loading={loading} {...extra?.translate} />
|
45
37
|
</ExtraContainer>
|
@@ -0,0 +1,130 @@
|
|
1
|
+
import { ModelIcon } from '@lobehub/icons';
|
2
|
+
import { Icon, Tooltip } from '@lobehub/ui';
|
3
|
+
import { Segmented } from 'antd';
|
4
|
+
import { createStyles } from 'antd-style';
|
5
|
+
import { ArrowDownToDot, ArrowUpFromDot, CircleFadingArrowUp } from 'lucide-react';
|
6
|
+
import { memo } from 'react';
|
7
|
+
import { useTranslation } from 'react-i18next';
|
8
|
+
import { Flexbox } from 'react-layout-kit';
|
9
|
+
|
10
|
+
import { useGlobalStore } from '@/store/global';
|
11
|
+
import { systemStatusSelectors } from '@/store/global/selectors';
|
12
|
+
import { LobeDefaultAiModelListItem } from '@/types/aiModel';
|
13
|
+
import { ModelPriceCurrency } from '@/types/llm';
|
14
|
+
import { formatPriceByCurrency } from '@/utils/format';
|
15
|
+
|
16
|
+
export const useStyles = createStyles(({ css, token }) => {
|
17
|
+
return {
|
18
|
+
container: css`
|
19
|
+
font-size: 12px;
|
20
|
+
`,
|
21
|
+
desc: css`
|
22
|
+
line-height: 12px;
|
23
|
+
color: ${token.colorTextDescription};
|
24
|
+
`,
|
25
|
+
pricing: css`
|
26
|
+
font-size: 12px;
|
27
|
+
color: ${token.colorTextSecondary};
|
28
|
+
`,
|
29
|
+
};
|
30
|
+
});
|
31
|
+
|
32
|
+
interface ModelCardProps extends LobeDefaultAiModelListItem {
|
33
|
+
provider: string;
|
34
|
+
}
|
35
|
+
|
36
|
+
const ModelCard = memo<ModelCardProps>(({ pricing, id, provider, displayName }) => {
|
37
|
+
const { t } = useTranslation('chat');
|
38
|
+
const { styles } = useStyles();
|
39
|
+
|
40
|
+
const isShowCredit = useGlobalStore(systemStatusSelectors.isShowCredit) && !!pricing;
|
41
|
+
const updateSystemStatus = useGlobalStore((s) => s.updateSystemStatus);
|
42
|
+
|
43
|
+
const inputPrice = formatPriceByCurrency(pricing?.input, pricing?.currency as ModelPriceCurrency);
|
44
|
+
const cachedInputPrice = formatPriceByCurrency(
|
45
|
+
pricing?.cachedInput,
|
46
|
+
pricing?.currency as ModelPriceCurrency,
|
47
|
+
);
|
48
|
+
const outputPrice = formatPriceByCurrency(
|
49
|
+
pricing?.output,
|
50
|
+
pricing?.currency as ModelPriceCurrency,
|
51
|
+
);
|
52
|
+
return (
|
53
|
+
<>
|
54
|
+
<Flexbox
|
55
|
+
align={'center'}
|
56
|
+
className={styles.container}
|
57
|
+
flex={1}
|
58
|
+
gap={40}
|
59
|
+
horizontal
|
60
|
+
justify={'space-between'}
|
61
|
+
>
|
62
|
+
<Flexbox align={'center'} gap={8} horizontal>
|
63
|
+
<ModelIcon model={id} size={22} />
|
64
|
+
<Flexbox flex={1} gap={2} style={{ minWidth: 0 }}>
|
65
|
+
<Flexbox align={'center'} gap={8} horizontal style={{ lineHeight: '12px' }}>
|
66
|
+
{displayName || id}
|
67
|
+
</Flexbox>
|
68
|
+
<span className={styles.desc}>{provider}</span>
|
69
|
+
</Flexbox>
|
70
|
+
</Flexbox>
|
71
|
+
{!!pricing && (
|
72
|
+
<Flexbox>
|
73
|
+
<Segmented
|
74
|
+
onChange={(value) => {
|
75
|
+
updateSystemStatus({ isShowCredit: value === 'credit' });
|
76
|
+
}}
|
77
|
+
options={[
|
78
|
+
{ label: 'Token', value: 'token' },
|
79
|
+
{
|
80
|
+
label: (
|
81
|
+
<Tooltip title={t('messages.modelCard.creditTooltip')}>
|
82
|
+
{t('messages.modelCard.credit')}
|
83
|
+
</Tooltip>
|
84
|
+
),
|
85
|
+
value: 'credit',
|
86
|
+
},
|
87
|
+
]}
|
88
|
+
size={'small'}
|
89
|
+
value={isShowCredit ? 'credit' : 'token'}
|
90
|
+
/>
|
91
|
+
</Flexbox>
|
92
|
+
)}
|
93
|
+
</Flexbox>
|
94
|
+
{isShowCredit && (
|
95
|
+
<Flexbox horizontal justify={'space-between'}>
|
96
|
+
<div />
|
97
|
+
<Flexbox align={'center'} className={styles.pricing} gap={8} horizontal>
|
98
|
+
{t('messages.modelCard.creditPricing')}:
|
99
|
+
{pricing?.cachedInput && (
|
100
|
+
<Tooltip
|
101
|
+
title={t('messages.modelCard.pricing.inputCachedTokens', {
|
102
|
+
amount: cachedInputPrice,
|
103
|
+
})}
|
104
|
+
>
|
105
|
+
<Flexbox gap={2} horizontal>
|
106
|
+
<Icon icon={CircleFadingArrowUp} />
|
107
|
+
{cachedInputPrice}
|
108
|
+
</Flexbox>
|
109
|
+
</Tooltip>
|
110
|
+
)}
|
111
|
+
<Tooltip title={t('messages.modelCard.pricing.inputTokens', { amount: inputPrice })}>
|
112
|
+
<Flexbox gap={2} horizontal>
|
113
|
+
<Icon icon={ArrowUpFromDot} />
|
114
|
+
{inputPrice}
|
115
|
+
</Flexbox>
|
116
|
+
</Tooltip>
|
117
|
+
<Tooltip title={t('messages.modelCard.pricing.outputTokens', { amount: outputPrice })}>
|
118
|
+
<Flexbox gap={2} horizontal>
|
119
|
+
<Icon icon={ArrowDownToDot} />
|
120
|
+
{outputPrice}
|
121
|
+
</Flexbox>
|
122
|
+
</Tooltip>
|
123
|
+
</Flexbox>
|
124
|
+
</Flexbox>
|
125
|
+
)}
|
126
|
+
</>
|
127
|
+
);
|
128
|
+
});
|
129
|
+
|
130
|
+
export default ModelCard;
|
@@ -0,0 +1,71 @@
|
|
1
|
+
import { useTheme } from 'antd-style';
|
2
|
+
import numeral from 'numeral';
|
3
|
+
import { memo } from 'react';
|
4
|
+
import { Flexbox } from 'react-layout-kit';
|
5
|
+
|
6
|
+
|
7
|
+
export interface TokenProgressItem {
|
8
|
+
color: string;
|
9
|
+
id: string;
|
10
|
+
title: string;
|
11
|
+
value: number;
|
12
|
+
}
|
13
|
+
|
14
|
+
interface TokenProgressProps {
|
15
|
+
data: TokenProgressItem[];
|
16
|
+
showIcon?: boolean;
|
17
|
+
}
|
18
|
+
|
19
|
+
const format = (number: number) => numeral(number).format('0,0');
|
20
|
+
|
21
|
+
const TokenProgress = memo<TokenProgressProps>(({ data, showIcon }) => {
|
22
|
+
const theme = useTheme();
|
23
|
+
const total = data.reduce((acc, item) => acc + item.value, 0);
|
24
|
+
|
25
|
+
return (
|
26
|
+
<Flexbox gap={8} style={{ position: 'relative' }} width={'100%'}>
|
27
|
+
<Flexbox
|
28
|
+
height={6}
|
29
|
+
horizontal
|
30
|
+
style={{
|
31
|
+
background: total === 0 ? theme.colorFill : undefined,
|
32
|
+
borderRadius: 3,
|
33
|
+
overflow: 'hidden',
|
34
|
+
position: 'relative',
|
35
|
+
}}
|
36
|
+
width={'100%'}
|
37
|
+
>
|
38
|
+
{data.map((item) => (
|
39
|
+
<Flexbox
|
40
|
+
height={'100%'}
|
41
|
+
key={item.id}
|
42
|
+
style={{ background: item.color, flex: item.value }}
|
43
|
+
/>
|
44
|
+
))}
|
45
|
+
</Flexbox>
|
46
|
+
<Flexbox>
|
47
|
+
{data.map((item) => (
|
48
|
+
<Flexbox align={'center'} gap={4} horizontal justify={'space-between'} key={item.id}>
|
49
|
+
<Flexbox align={'center'} gap={4} horizontal>
|
50
|
+
{showIcon && (
|
51
|
+
<div
|
52
|
+
style={{
|
53
|
+
background: item.color,
|
54
|
+
borderRadius: '50%',
|
55
|
+
flex: 'none',
|
56
|
+
height: 6,
|
57
|
+
width: 6,
|
58
|
+
}}
|
59
|
+
/>
|
60
|
+
)}
|
61
|
+
<div style={{ color: theme.colorTextSecondary }}>{item.title}</div>
|
62
|
+
</Flexbox>
|
63
|
+
<div style={{ fontWeight: 500 }}>{format(item.value)}</div>
|
64
|
+
</Flexbox>
|
65
|
+
))}
|
66
|
+
</Flexbox>
|
67
|
+
</Flexbox>
|
68
|
+
);
|
69
|
+
});
|
70
|
+
|
71
|
+
export default TokenProgress;
|
@@ -0,0 +1,146 @@
|
|
1
|
+
import { Icon } from '@lobehub/ui';
|
2
|
+
import { Divider, Popover } from 'antd';
|
3
|
+
import { useTheme } from 'antd-style';
|
4
|
+
import { BadgeCent, CoinsIcon } from 'lucide-react';
|
5
|
+
import { memo } from 'react';
|
6
|
+
import { useTranslation } from 'react-i18next';
|
7
|
+
import { Center, Flexbox } from 'react-layout-kit';
|
8
|
+
|
9
|
+
import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
|
10
|
+
import { useGlobalStore } from '@/store/global';
|
11
|
+
import { systemStatusSelectors } from '@/store/global/selectors';
|
12
|
+
import { ModelTokensUsage } from '@/types/message';
|
13
|
+
import { formatNumber } from '@/utils/format';
|
14
|
+
|
15
|
+
import ModelCard from './ModelCard';
|
16
|
+
import TokenProgress, { TokenProgressItem } from './TokenProgress';
|
17
|
+
import { getDetailsToken } from './tokens';
|
18
|
+
|
19
|
+
interface TokenDetailProps {
|
20
|
+
model: string;
|
21
|
+
provider: string;
|
22
|
+
usage: ModelTokensUsage;
|
23
|
+
}
|
24
|
+
|
25
|
+
const TokenDetail = memo<TokenDetailProps>(({ usage, model, provider }) => {
|
26
|
+
const { t } = useTranslation('chat');
|
27
|
+
const theme = useTheme();
|
28
|
+
|
29
|
+
const modelCard = useAiInfraStore(aiModelSelectors.getModelCard(model, provider));
|
30
|
+
const isShowCredit = useGlobalStore(systemStatusSelectors.isShowCredit) && !!modelCard?.pricing;
|
31
|
+
|
32
|
+
const detailTokens = getDetailsToken(usage, modelCard);
|
33
|
+
const inputDetails = [
|
34
|
+
!!detailTokens.inputAudio && {
|
35
|
+
color: theme.cyan9,
|
36
|
+
id: 'reasoning',
|
37
|
+
title: t('messages.tokenDetails.inputAudio'),
|
38
|
+
value: isShowCredit ? detailTokens.inputAudio.credit : detailTokens.inputAudio.token,
|
39
|
+
},
|
40
|
+
!!detailTokens.inputText && {
|
41
|
+
color: theme.green,
|
42
|
+
id: 'inputText',
|
43
|
+
title: t('messages.tokenDetails.inputText'),
|
44
|
+
value: isShowCredit ? detailTokens.inputText.credit : detailTokens.inputText.token,
|
45
|
+
},
|
46
|
+
].filter(Boolean) as TokenProgressItem[];
|
47
|
+
|
48
|
+
const outputDetails = [
|
49
|
+
!!detailTokens.reasoning && {
|
50
|
+
color: theme.pink,
|
51
|
+
id: 'reasoning',
|
52
|
+
title: t('messages.tokenDetails.reasoning'),
|
53
|
+
value: isShowCredit ? detailTokens.reasoning.credit : detailTokens.reasoning.token,
|
54
|
+
},
|
55
|
+
!!detailTokens.outputAudio && {
|
56
|
+
color: theme.cyan9,
|
57
|
+
id: 'outputAudio',
|
58
|
+
title: t('messages.tokenDetails.outputAudio'),
|
59
|
+
value: isShowCredit ? detailTokens.outputAudio.credit : detailTokens.outputAudio.token,
|
60
|
+
},
|
61
|
+
!!detailTokens.outputText && {
|
62
|
+
color: theme.green,
|
63
|
+
id: 'outputText',
|
64
|
+
title: t('messages.tokenDetails.outputText'),
|
65
|
+
value: isShowCredit ? detailTokens.outputText.credit : detailTokens.outputText.token,
|
66
|
+
},
|
67
|
+
].filter(Boolean) as TokenProgressItem[];
|
68
|
+
|
69
|
+
const totalDetail = [
|
70
|
+
!!detailTokens.cachedInput && {
|
71
|
+
color: theme.orange,
|
72
|
+
id: 'cachedInput',
|
73
|
+
title: t('messages.tokenDetails.inputCached'),
|
74
|
+
value: isShowCredit ? detailTokens.cachedInput.credit : detailTokens.cachedInput.token,
|
75
|
+
},
|
76
|
+
!!detailTokens.uncachedInput && {
|
77
|
+
color: theme.colorFill,
|
78
|
+
|
79
|
+
id: 'uncachedInput',
|
80
|
+
title: t('messages.tokenDetails.inputUncached'),
|
81
|
+
value: isShowCredit ? detailTokens.uncachedInput.credit : detailTokens.uncachedInput.token,
|
82
|
+
},
|
83
|
+
!!detailTokens.totalOutput && {
|
84
|
+
color: theme.colorSuccess,
|
85
|
+
id: 'output',
|
86
|
+
title: t('messages.tokenDetails.output'),
|
87
|
+
value: isShowCredit ? detailTokens.totalOutput.credit : detailTokens.totalOutput.token,
|
88
|
+
},
|
89
|
+
].filter(Boolean) as TokenProgressItem[];
|
90
|
+
|
91
|
+
const displayTotal =
|
92
|
+
isShowCredit && !!detailTokens.totalTokens
|
93
|
+
? formatNumber(detailTokens.totalTokens.credit)
|
94
|
+
: formatNumber(usage.totalTokens);
|
95
|
+
|
96
|
+
return (
|
97
|
+
<Popover
|
98
|
+
arrow={false}
|
99
|
+
content={
|
100
|
+
<Flexbox gap={20} style={{ minWidth: 200 }}>
|
101
|
+
{modelCard && <ModelCard {...modelCard} provider={provider} />}
|
102
|
+
{inputDetails.length > 1 && (
|
103
|
+
<>
|
104
|
+
<Flexbox align={'center'} gap={4} horizontal justify={'space-between'} width={'100%'}>
|
105
|
+
<div style={{ color: theme.colorTextDescription }}>
|
106
|
+
{t('messages.tokenDetails.inputTitle')}
|
107
|
+
</div>
|
108
|
+
</Flexbox>
|
109
|
+
<TokenProgress data={inputDetails} showIcon />
|
110
|
+
</>
|
111
|
+
)}
|
112
|
+
{outputDetails.length > 1 && (
|
113
|
+
<>
|
114
|
+
<Flexbox align={'center'} gap={4} horizontal justify={'space-between'} width={'100%'}>
|
115
|
+
<div style={{ color: theme.colorTextDescription }}>
|
116
|
+
{t('messages.tokenDetails.outputTitle')}
|
117
|
+
</div>
|
118
|
+
</Flexbox>
|
119
|
+
<TokenProgress data={outputDetails} showIcon />
|
120
|
+
</>
|
121
|
+
)}
|
122
|
+
|
123
|
+
<Flexbox>
|
124
|
+
<TokenProgress data={totalDetail} showIcon />
|
125
|
+
<Divider style={{ marginBlock: 8 }} />
|
126
|
+
<Flexbox align={'center'} gap={4} horizontal justify={'space-between'}>
|
127
|
+
<div style={{ color: theme.colorTextSecondary }}>
|
128
|
+
{t('messages.tokenDetails.total')}
|
129
|
+
</div>
|
130
|
+
<div style={{ fontWeight: 500 }}>{displayTotal}</div>
|
131
|
+
</Flexbox>
|
132
|
+
</Flexbox>
|
133
|
+
</Flexbox>
|
134
|
+
}
|
135
|
+
placement={'top'}
|
136
|
+
trigger={['hover', 'click']}
|
137
|
+
>
|
138
|
+
<Center gap={2} horizontal style={{ cursor: 'default' }}>
|
139
|
+
<Icon icon={isShowCredit ? BadgeCent : CoinsIcon} />
|
140
|
+
{displayTotal}
|
141
|
+
</Center>
|
142
|
+
</Popover>
|
143
|
+
);
|
144
|
+
});
|
145
|
+
|
146
|
+
export default TokenDetail;
|