@lobehub/chat 1.133.1 → 1.133.3
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/.cursor/rules/project-introduce.mdc +19 -25
- package/.cursor/rules/project-structure.mdc +102 -221
- package/.cursor/rules/{rules-attach.mdc → rules-index.mdc} +2 -11
- package/.cursor/rules/typescript.mdc +3 -53
- package/.vscode/settings.json +2 -1
- package/AGENTS.md +33 -54
- package/CHANGELOG.md +58 -0
- package/CLAUDE.md +1 -26
- package/changelog/v1.json +21 -0
- package/locales/ar/chat.json +5 -0
- package/locales/ar/image.json +7 -0
- package/locales/ar/models.json +2 -2
- package/locales/bg-BG/chat.json +5 -0
- package/locales/bg-BG/image.json +7 -0
- package/locales/de-DE/chat.json +5 -0
- package/locales/de-DE/image.json +7 -0
- package/locales/en-US/chat.json +5 -0
- package/locales/en-US/image.json +7 -0
- package/locales/es-ES/chat.json +5 -0
- package/locales/es-ES/image.json +7 -0
- package/locales/es-ES/tool.json +1 -1
- package/locales/fa-IR/chat.json +5 -0
- package/locales/fa-IR/image.json +7 -0
- package/locales/fa-IR/models.json +2 -2
- package/locales/fr-FR/chat.json +5 -0
- package/locales/fr-FR/image.json +7 -0
- package/locales/fr-FR/models.json +2 -2
- package/locales/it-IT/chat.json +5 -0
- package/locales/it-IT/image.json +7 -0
- package/locales/ja-JP/chat.json +5 -0
- package/locales/ja-JP/image.json +7 -0
- package/locales/ko-KR/chat.json +5 -0
- package/locales/ko-KR/image.json +7 -0
- package/locales/nl-NL/chat.json +5 -0
- package/locales/nl-NL/image.json +7 -0
- package/locales/pl-PL/chat.json +5 -0
- package/locales/pl-PL/image.json +7 -0
- package/locales/pt-BR/chat.json +5 -0
- package/locales/pt-BR/image.json +7 -0
- package/locales/ru-RU/chat.json +5 -0
- package/locales/ru-RU/image.json +7 -0
- package/locales/ru-RU/tool.json +1 -1
- package/locales/tr-TR/chat.json +5 -0
- package/locales/tr-TR/image.json +7 -0
- package/locales/tr-TR/models.json +2 -2
- package/locales/vi-VN/chat.json +5 -0
- package/locales/vi-VN/image.json +7 -0
- package/locales/zh-CN/chat.json +5 -0
- package/locales/zh-CN/image.json +7 -0
- package/locales/zh-TW/chat.json +5 -0
- package/locales/zh-TW/image.json +7 -0
- package/package.json +4 -5
- package/packages/const/package.json +4 -0
- package/packages/const/src/currency.ts +2 -0
- package/packages/const/src/index.ts +1 -0
- package/packages/model-bank/package.json +2 -1
- package/packages/model-bank/src/aiModels/google.ts +6 -0
- package/packages/model-bank/src/aiModels/openai.ts +6 -22
- package/packages/model-bank/src/standard-parameters/index.ts +56 -46
- package/packages/model-runtime/package.json +1 -0
- package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +4 -2
- package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.ts +12 -2
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +16 -5
- package/packages/model-runtime/src/core/streams/anthropic.ts +25 -36
- package/packages/model-runtime/src/core/streams/google/google-ai.test.ts +1 -1
- package/packages/model-runtime/src/core/streams/google/index.ts +18 -42
- package/packages/model-runtime/src/core/streams/openai/openai.test.ts +7 -10
- package/packages/model-runtime/src/core/streams/openai/openai.ts +14 -11
- package/packages/model-runtime/src/core/streams/openai/responsesStream.ts +11 -5
- package/packages/model-runtime/src/core/streams/protocol.ts +25 -6
- package/packages/model-runtime/src/core/streams/qwen.ts +2 -2
- package/packages/model-runtime/src/core/streams/spark.ts +3 -3
- package/packages/model-runtime/src/core/streams/vertex-ai.test.ts +2 -2
- package/packages/model-runtime/src/core/streams/vertex-ai.ts +14 -23
- package/packages/model-runtime/src/core/usageConverters/anthropic.test.ts +99 -0
- package/packages/model-runtime/src/core/usageConverters/anthropic.ts +73 -0
- package/packages/model-runtime/src/core/usageConverters/google-ai.test.ts +88 -0
- package/packages/model-runtime/src/core/usageConverters/google-ai.ts +55 -0
- package/packages/model-runtime/src/core/usageConverters/index.ts +4 -0
- package/packages/model-runtime/src/core/usageConverters/openai.test.ts +429 -0
- package/packages/model-runtime/src/core/usageConverters/openai.ts +152 -0
- package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.test.ts +455 -0
- package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.ts +293 -0
- package/packages/model-runtime/src/core/usageConverters/utils/computeImageCost.test.ts +47 -0
- package/packages/model-runtime/src/core/usageConverters/utils/computeImageCost.ts +121 -0
- package/packages/model-runtime/src/core/usageConverters/utils/index.ts +11 -0
- package/packages/model-runtime/src/core/usageConverters/utils/withUsageCost.ts +19 -0
- package/packages/model-runtime/src/index.ts +2 -0
- package/packages/model-runtime/src/providers/anthropic/index.ts +48 -1
- package/packages/model-runtime/src/providers/google/createImage.ts +11 -2
- package/packages/model-runtime/src/providers/google/index.ts +8 -1
- package/packages/model-runtime/src/providers/openai/__snapshots__/index.test.ts.snap +7 -0
- package/packages/model-runtime/src/providers/zhipu/index.ts +3 -1
- package/packages/model-runtime/src/types/chat.ts +5 -3
- package/packages/model-runtime/src/types/image.ts +20 -9
- package/packages/model-runtime/src/utils/getModelPricing.ts +36 -0
- package/packages/obervability-otel/package.json +2 -2
- package/packages/ssrf-safe-fetch/index.test.ts +343 -0
- package/packages/ssrf-safe-fetch/index.ts +37 -0
- package/packages/ssrf-safe-fetch/package.json +17 -0
- package/packages/ssrf-safe-fetch/vitest.config.mts +10 -0
- package/packages/types/src/message/base.ts +43 -17
- package/packages/utils/src/client/apiKeyManager.test.ts +70 -0
- package/packages/utils/src/client/apiKeyManager.ts +41 -0
- package/packages/utils/src/client/index.ts +2 -0
- package/packages/utils/src/fetch/fetchSSE.ts +4 -4
- package/packages/utils/src/index.ts +1 -0
- package/packages/utils/src/toolManifest.ts +2 -1
- package/src/app/(backend)/webapi/proxy/route.ts +2 -13
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/default.tsx +2 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatMinimap/index.tsx +335 -0
- package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/TopicPanel.tsx +4 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/QualitySelect.tsx +23 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +9 -0
- package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.test.ts +13 -13
- package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +1 -1
- package/src/features/Conversation/components/ChatItem/index.tsx +56 -2
- package/src/features/Conversation/components/VirtualizedList/VirtuosoContext.ts +88 -0
- package/src/features/Conversation/components/VirtualizedList/index.tsx +15 -1
- package/src/locales/default/chat.ts +5 -0
- package/src/locales/default/image.ts +7 -0
- package/src/server/modules/EdgeConfig/index.ts +1 -1
- package/src/server/routers/async/image.ts +9 -1
- package/src/services/_auth.ts +12 -12
- package/src/services/chat/contextEngineering.ts +2 -3
- package/.cursor/rules/backend-architecture.mdc +0 -176
- package/.cursor/rules/code-review.mdc +0 -58
- package/.cursor/rules/cursor-ux.mdc +0 -32
- package/.cursor/rules/define-database-model.mdc +0 -8
- package/.cursor/rules/system-role.mdc +0 -31
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
interface KeyStore {
|
|
2
|
+
index: number;
|
|
3
|
+
keyLen: number;
|
|
4
|
+
keys: string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class ClientApiKeyManager {
|
|
8
|
+
private _cache: Map<string, KeyStore> = new Map();
|
|
9
|
+
|
|
10
|
+
private _mode: string = 'random';
|
|
11
|
+
|
|
12
|
+
private getKeyStore(apiKeys: string) {
|
|
13
|
+
let store = this._cache.get(apiKeys);
|
|
14
|
+
|
|
15
|
+
if (!store) {
|
|
16
|
+
const keys = apiKeys
|
|
17
|
+
.split(',')
|
|
18
|
+
.map((_) => _.trim())
|
|
19
|
+
.filter((_) => !!_);
|
|
20
|
+
|
|
21
|
+
store = { index: 0, keyLen: keys.length, keys } as KeyStore;
|
|
22
|
+
this._cache.set(apiKeys, store);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return store;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pick(apiKeys: string = '') {
|
|
29
|
+
if (!apiKeys) return undefined;
|
|
30
|
+
|
|
31
|
+
const store = this.getKeyStore(apiKeys);
|
|
32
|
+
let index = 0;
|
|
33
|
+
|
|
34
|
+
if (this._mode === 'turn') index = store.index++ % store.keyLen;
|
|
35
|
+
if (this._mode === 'random') index = Math.floor(Math.random() * store.keyLen);
|
|
36
|
+
|
|
37
|
+
return store.keys[index];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const clientApiKeyManager = new ClientApiKeyManager();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
export * from './apiKeyManager';
|
|
1
2
|
export * from './clipboard';
|
|
2
3
|
export * from './downloadFile';
|
|
3
4
|
export * from './exportFile';
|
|
4
5
|
export * from './fetchEventSource';
|
|
6
|
+
export * from './parserPlaceholder';
|
|
5
7
|
export * from './sanitize';
|
|
6
8
|
export * from './videoValidation';
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
MessageToolCallSchema,
|
|
11
11
|
ModelReasoning,
|
|
12
12
|
ModelSpeed,
|
|
13
|
-
|
|
13
|
+
ModelUsage,
|
|
14
14
|
ResponseAnimation,
|
|
15
15
|
ResponseAnimationStyle,
|
|
16
16
|
} from '@lobechat/types';
|
|
@@ -32,13 +32,13 @@ export type OnFinishHandler = (
|
|
|
32
32
|
toolCalls?: MessageToolCall[];
|
|
33
33
|
traceId?: string | null;
|
|
34
34
|
type?: SSEFinishType;
|
|
35
|
-
usage?:
|
|
35
|
+
usage?: ModelUsage;
|
|
36
36
|
},
|
|
37
37
|
) => Promise<void>;
|
|
38
38
|
|
|
39
39
|
export interface MessageUsageChunk {
|
|
40
40
|
type: 'usage';
|
|
41
|
-
usage:
|
|
41
|
+
usage: ModelUsage;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export interface MessageSpeedChunk {
|
|
@@ -379,7 +379,7 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
|
379
379
|
});
|
|
380
380
|
|
|
381
381
|
let grounding: GroundingSearch | undefined = undefined;
|
|
382
|
-
let usage:
|
|
382
|
+
let usage: ModelUsage | undefined = undefined;
|
|
383
383
|
let images: ChatImageChunk[] = [];
|
|
384
384
|
let speed: ModelSpeed | undefined = undefined;
|
|
385
385
|
|
|
@@ -3,7 +3,8 @@ import { LobeChatPluginManifest, pluginManifestSchema } from '@lobehub/chat-plug
|
|
|
3
3
|
import { uniqBy } from 'lodash-es';
|
|
4
4
|
|
|
5
5
|
import { API_ENDPOINTS } from '@/services/_url';
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
import { genToolCallingName } from './toolCall';
|
|
7
8
|
|
|
8
9
|
const fetchJSON = async <T = any>(url: string, proxy = false): Promise<T> => {
|
|
9
10
|
// 2. 发送请求
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
-
import
|
|
3
|
-
import { RequestFilteringAgentOptions, useAgent as ssrfAgent } from 'request-filtering-agent';
|
|
4
|
-
|
|
5
|
-
import { appEnv } from '@/envs/app';
|
|
2
|
+
import { ssrfSafeFetch } from 'ssrf-safe-fetch';
|
|
6
3
|
|
|
7
4
|
/**
|
|
8
5
|
* just for a proxy
|
|
@@ -11,15 +8,7 @@ export const POST = async (req: Request) => {
|
|
|
11
8
|
const url = await req.text();
|
|
12
9
|
|
|
13
10
|
try {
|
|
14
|
-
|
|
15
|
-
const options: RequestFilteringAgentOptions = {
|
|
16
|
-
allowIPAddressList: appEnv.SSRF_ALLOW_IP_ADDRESS_LIST?.split(',') || [],
|
|
17
|
-
allowMetaIPAddress: appEnv.SSRF_ALLOW_PRIVATE_IP_ADDRESS,
|
|
18
|
-
allowPrivateIPAddress: appEnv.SSRF_ALLOW_PRIVATE_IP_ADDRESS,
|
|
19
|
-
denyIPAddressList: [],
|
|
20
|
-
};
|
|
21
|
-
const res = await fetch(url, { agent: ssrfAgent(url, options) });
|
|
22
|
-
|
|
11
|
+
const res = await ssrfSafeFetch(url);
|
|
23
12
|
return new Response(await res.arrayBuffer(), { headers: { ...res.headers } });
|
|
24
13
|
} catch (err) {
|
|
25
14
|
console.error(err); // DNS lookup 127.0.0.1(family:4, host:127.0.0.1.nip.io) is not allowed. Because, It is private IP address.
|
|
@@ -4,6 +4,7 @@ import { RouteVariants } from '@/utils/server/routeVariants';
|
|
|
4
4
|
import ChatHydration from './features/ChatHydration';
|
|
5
5
|
import ChatInput from './features/ChatInput';
|
|
6
6
|
import ChatList from './features/ChatList';
|
|
7
|
+
import ChatMinimap from './features/ChatMinimap';
|
|
7
8
|
import ThreadHydration from './features/ThreadHydration';
|
|
8
9
|
import ZenModeToast from './features/ZenModeToast';
|
|
9
10
|
|
|
@@ -17,6 +18,7 @@ const ChatConversation = async (props: DynamicLayoutProps) => {
|
|
|
17
18
|
<ChatInput mobile={isMobile} />
|
|
18
19
|
<ChatHydration />
|
|
19
20
|
<ThreadHydration />
|
|
21
|
+
{!isMobile && <ChatMinimap />}
|
|
20
22
|
</>
|
|
21
23
|
);
|
|
22
24
|
};
|
package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatMinimap/index.tsx
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Icon } from '@lobehub/ui';
|
|
4
|
+
import { Tooltip } from 'antd';
|
|
5
|
+
import { createStyles, useTheme } from 'antd-style';
|
|
6
|
+
import { ChevronDown, ChevronUp } from 'lucide-react';
|
|
7
|
+
import { memo, useCallback, useMemo, useState, useSyncExternalStore } from 'react';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { Flexbox } from 'react-layout-kit';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
getVirtuosoActiveIndex,
|
|
13
|
+
getVirtuosoGlobalRef,
|
|
14
|
+
subscribeVirtuosoActiveIndex,
|
|
15
|
+
subscribeVirtuosoGlobalRef,
|
|
16
|
+
} from '@/features/Conversation/components/VirtualizedList/VirtuosoContext';
|
|
17
|
+
import { useChatStore } from '@/store/chat';
|
|
18
|
+
import { chatSelectors } from '@/store/chat/selectors';
|
|
19
|
+
|
|
20
|
+
const MIN_WIDTH = 16;
|
|
21
|
+
const MAX_WIDTH = 30;
|
|
22
|
+
const MAX_CONTENT_LENGTH = 320;
|
|
23
|
+
const MIN_MESSAGES = 6;
|
|
24
|
+
|
|
25
|
+
const useStyles = createStyles(({ css, token }) => ({
|
|
26
|
+
arrow: css`
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
|
|
29
|
+
transform: translateX(4px);
|
|
30
|
+
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
|
|
35
|
+
width: 24px;
|
|
36
|
+
height: 24px;
|
|
37
|
+
padding: 0;
|
|
38
|
+
border: none;
|
|
39
|
+
border-radius: 6px;
|
|
40
|
+
|
|
41
|
+
color: ${token.colorTextTertiary};
|
|
42
|
+
|
|
43
|
+
opacity: 0;
|
|
44
|
+
background: none;
|
|
45
|
+
|
|
46
|
+
transition: opacity 0.2s ease;
|
|
47
|
+
|
|
48
|
+
&:hover {
|
|
49
|
+
color: ${token.colorText};
|
|
50
|
+
background: ${token.colorFill};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&:focus-visible {
|
|
54
|
+
outline: none;
|
|
55
|
+
box-shadow: 0 0 0 2px ${token.colorPrimaryBorder};
|
|
56
|
+
}
|
|
57
|
+
`,
|
|
58
|
+
arrowVisible: css`
|
|
59
|
+
opacity: 1;
|
|
60
|
+
`,
|
|
61
|
+
container: css`
|
|
62
|
+
pointer-events: none;
|
|
63
|
+
|
|
64
|
+
position: absolute;
|
|
65
|
+
z-index: 1;
|
|
66
|
+
inset-block: 16px 120px;
|
|
67
|
+
inset-inline-end: 8px;
|
|
68
|
+
|
|
69
|
+
width: 32px;
|
|
70
|
+
`,
|
|
71
|
+
indicator: css`
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
|
|
74
|
+
flex-shrink: 0;
|
|
75
|
+
|
|
76
|
+
min-width: ${MIN_WIDTH}px;
|
|
77
|
+
height: 12px;
|
|
78
|
+
padding-block: 4px;
|
|
79
|
+
padding-inline: 4px;
|
|
80
|
+
border: none;
|
|
81
|
+
border-radius: 3px;
|
|
82
|
+
|
|
83
|
+
background: none;
|
|
84
|
+
|
|
85
|
+
transition:
|
|
86
|
+
transform 0.2s ease,
|
|
87
|
+
background-color 0.2s ease,
|
|
88
|
+
box-shadow 0.2s ease;
|
|
89
|
+
|
|
90
|
+
&:hover {
|
|
91
|
+
transform: scaleX(1.05);
|
|
92
|
+
background: ${token.colorFill};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
&:focus-visible {
|
|
96
|
+
outline: none;
|
|
97
|
+
box-shadow: 0 0 0 2px ${token.colorPrimaryBorder};
|
|
98
|
+
}
|
|
99
|
+
`,
|
|
100
|
+
indicatorActive: css`
|
|
101
|
+
transform: scaleX(1.1);
|
|
102
|
+
background: ${token.colorPrimary};
|
|
103
|
+
box-shadow: 0 0 0 1px ${token.colorPrimaryHover};
|
|
104
|
+
`,
|
|
105
|
+
indicatorContent: css`
|
|
106
|
+
width: 100%;
|
|
107
|
+
height: 100%;
|
|
108
|
+
border-radius: 3px;
|
|
109
|
+
background: ${token.colorFillSecondary};
|
|
110
|
+
`,
|
|
111
|
+
indicatorContentActive: css`
|
|
112
|
+
background: ${token.colorPrimary};
|
|
113
|
+
`,
|
|
114
|
+
rail: css`
|
|
115
|
+
pointer-events: auto;
|
|
116
|
+
|
|
117
|
+
display: flex;
|
|
118
|
+
flex-direction: column;
|
|
119
|
+
gap: 0;
|
|
120
|
+
align-items: end;
|
|
121
|
+
justify-content: space-between;
|
|
122
|
+
|
|
123
|
+
width: 100%;
|
|
124
|
+
height: fit-content;
|
|
125
|
+
margin-block: 0;
|
|
126
|
+
margin-inline: auto;
|
|
127
|
+
|
|
128
|
+
&:hover .arrow {
|
|
129
|
+
opacity: 1;
|
|
130
|
+
}
|
|
131
|
+
`,
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
const getIndicatorWidth = (content: string | undefined) => {
|
|
135
|
+
if (!content) return MIN_WIDTH;
|
|
136
|
+
|
|
137
|
+
const ratio = Math.min(content.length / MAX_CONTENT_LENGTH, 1);
|
|
138
|
+
|
|
139
|
+
return MIN_WIDTH + (MAX_WIDTH - MIN_WIDTH) * ratio;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const getPreviewText = (content: string | undefined) => {
|
|
143
|
+
if (!content) return '';
|
|
144
|
+
|
|
145
|
+
const normalized = content.replaceAll(/\s+/g, ' ').trim();
|
|
146
|
+
if (!normalized) return '';
|
|
147
|
+
|
|
148
|
+
return normalized.slice(0, 100) + (normalized.length > 100 ? '…' : '');
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
interface MinimapIndicator {
|
|
152
|
+
id: string;
|
|
153
|
+
preview: string;
|
|
154
|
+
virtuosoIndex: number;
|
|
155
|
+
width: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const ChatMinimap = () => {
|
|
159
|
+
const { t } = useTranslation('chat');
|
|
160
|
+
const { styles, cx } = useStyles();
|
|
161
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
162
|
+
const virtuosoRef = useSyncExternalStore(
|
|
163
|
+
subscribeVirtuosoGlobalRef,
|
|
164
|
+
getVirtuosoGlobalRef,
|
|
165
|
+
() => null,
|
|
166
|
+
);
|
|
167
|
+
const activeIndex = useSyncExternalStore(
|
|
168
|
+
subscribeVirtuosoActiveIndex,
|
|
169
|
+
getVirtuosoActiveIndex,
|
|
170
|
+
() => null,
|
|
171
|
+
);
|
|
172
|
+
const messages = useChatStore(chatSelectors.mainDisplayChats);
|
|
173
|
+
|
|
174
|
+
const theme = useTheme();
|
|
175
|
+
|
|
176
|
+
const indicators = useMemo<MinimapIndicator[]>(() => {
|
|
177
|
+
return messages.reduce<MinimapIndicator[]>((acc, message, virtuosoIndex) => {
|
|
178
|
+
if (message.role !== 'user' && message.role !== 'assistant') return acc;
|
|
179
|
+
|
|
180
|
+
acc.push({
|
|
181
|
+
id: message.id,
|
|
182
|
+
preview: getPreviewText(message.content),
|
|
183
|
+
virtuosoIndex,
|
|
184
|
+
width: getIndicatorWidth(message.content),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return acc;
|
|
188
|
+
}, []);
|
|
189
|
+
}, [messages]);
|
|
190
|
+
|
|
191
|
+
const indicatorIndexMap = useMemo(() => {
|
|
192
|
+
const map = new Map<number, number>();
|
|
193
|
+
indicators.forEach(({ virtuosoIndex }, position) => {
|
|
194
|
+
map.set(virtuosoIndex, position);
|
|
195
|
+
});
|
|
196
|
+
return map;
|
|
197
|
+
}, [indicators]);
|
|
198
|
+
|
|
199
|
+
const activeIndicatorPosition = useMemo(() => {
|
|
200
|
+
if (activeIndex === null) return null;
|
|
201
|
+
|
|
202
|
+
console.log('> activeIndex', activeIndex);
|
|
203
|
+
console.log('> indicatorIndexMap', indicatorIndexMap);
|
|
204
|
+
|
|
205
|
+
return indicatorIndexMap.get(activeIndex) ?? null;
|
|
206
|
+
}, [activeIndex, indicatorIndexMap]);
|
|
207
|
+
|
|
208
|
+
const handleJump = useCallback(
|
|
209
|
+
(virtIndex: number) => {
|
|
210
|
+
virtuosoRef?.current?.scrollToIndex({
|
|
211
|
+
align: 'start',
|
|
212
|
+
behavior: 'smooth',
|
|
213
|
+
index: virtIndex,
|
|
214
|
+
// The current index detection will be off by 1, so we need to add 1 here
|
|
215
|
+
offset: 6,
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
[virtuosoRef],
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const handleStep = useCallback(
|
|
222
|
+
(direction: 'prev' | 'next') => {
|
|
223
|
+
const ref = virtuosoRef?.current;
|
|
224
|
+
if (!ref || indicators.length === 0) return;
|
|
225
|
+
|
|
226
|
+
let targetPosition: number;
|
|
227
|
+
|
|
228
|
+
if (activeIndicatorPosition !== null) {
|
|
229
|
+
console.log('activeIndicatorPosition', activeIndicatorPosition);
|
|
230
|
+
// We're on an indicator, move to prev/next
|
|
231
|
+
const delta = direction === 'prev' ? -1 : 1;
|
|
232
|
+
targetPosition = Math.min(
|
|
233
|
+
Math.max(activeIndicatorPosition + delta, 0),
|
|
234
|
+
Math.max(indicators.length - 1, 0),
|
|
235
|
+
);
|
|
236
|
+
} else if (activeIndex !== null) {
|
|
237
|
+
// We're not on an indicator, find the nearest one in the direction
|
|
238
|
+
if (direction === 'prev') {
|
|
239
|
+
let matched = -1;
|
|
240
|
+
for (let pos = indicators.length - 1; pos >= 0; pos -= 1) {
|
|
241
|
+
if (indicators[pos].virtuosoIndex < activeIndex) {
|
|
242
|
+
matched = pos;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
targetPosition = matched === -1 ? 0 : matched;
|
|
247
|
+
} else {
|
|
248
|
+
console.log('activeIndex', activeIndex);
|
|
249
|
+
console.log('indicators', indicators);
|
|
250
|
+
let matched = indicators.length - 1;
|
|
251
|
+
for (const [pos, indicator] of indicators.entries()) {
|
|
252
|
+
if (indicator.virtuosoIndex > activeIndex) {
|
|
253
|
+
matched = pos;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
targetPosition = matched;
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
// No active index, go to first/last
|
|
261
|
+
targetPosition = direction === 'prev' ? indicators.length - 1 : 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const targetIndicator = indicators[targetPosition];
|
|
265
|
+
|
|
266
|
+
if (!targetIndicator) return;
|
|
267
|
+
|
|
268
|
+
ref.scrollToIndex({
|
|
269
|
+
align: 'start',
|
|
270
|
+
behavior: 'smooth',
|
|
271
|
+
index: targetIndicator.virtuosoIndex,
|
|
272
|
+
offset: 6,
|
|
273
|
+
});
|
|
274
|
+
},
|
|
275
|
+
[activeIndex, activeIndicatorPosition, indicators, virtuosoRef],
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
if (indicators.length <= MIN_MESSAGES) return null;
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<Flexbox align={'center'} className={styles.container} justify={'center'}>
|
|
282
|
+
<Flexbox
|
|
283
|
+
className={styles.rail}
|
|
284
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
285
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
286
|
+
role={'group'}
|
|
287
|
+
>
|
|
288
|
+
<Tooltip mouseEnterDelay={0.1} placement={'left'} title={t('minimap.previousMessage')}>
|
|
289
|
+
<button
|
|
290
|
+
aria-label={t('minimap.previousMessage')}
|
|
291
|
+
className={cx(styles.arrow, isHovered && styles.arrowVisible)}
|
|
292
|
+
onClick={() => handleStep('prev')}
|
|
293
|
+
type={'button'}
|
|
294
|
+
>
|
|
295
|
+
<Icon color={theme.colorTextTertiary} icon={ChevronUp} size={16} />
|
|
296
|
+
</button>
|
|
297
|
+
</Tooltip>
|
|
298
|
+
{indicators.map(({ id, width, preview, virtuosoIndex }, position) => {
|
|
299
|
+
const isActive = activeIndicatorPosition === position;
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<Tooltip key={id} mouseEnterDelay={0.1} placement={'left'} title={preview || undefined}>
|
|
303
|
+
<button
|
|
304
|
+
aria-current={isActive ? 'true' : undefined}
|
|
305
|
+
aria-label={t('minimap.jumpToMessage', { index: position + 1 })}
|
|
306
|
+
className={styles.indicator}
|
|
307
|
+
onClick={() => handleJump(virtuosoIndex)}
|
|
308
|
+
style={{
|
|
309
|
+
width,
|
|
310
|
+
}}
|
|
311
|
+
type={'button'}
|
|
312
|
+
>
|
|
313
|
+
<div
|
|
314
|
+
className={cx(styles.indicatorContent, isActive && styles.indicatorContentActive)}
|
|
315
|
+
/>
|
|
316
|
+
</button>
|
|
317
|
+
</Tooltip>
|
|
318
|
+
);
|
|
319
|
+
})}
|
|
320
|
+
<Tooltip mouseEnterDelay={0.1} placement={'left'} title={t('minimap.nextMessage')}>
|
|
321
|
+
<button
|
|
322
|
+
aria-label={t('minimap.nextMessage')}
|
|
323
|
+
className={cx(styles.arrow, isHovered && styles.arrowVisible)}
|
|
324
|
+
onClick={() => handleStep('next')}
|
|
325
|
+
type={'button'}
|
|
326
|
+
>
|
|
327
|
+
<Icon icon={ChevronDown} size={16} />
|
|
328
|
+
</button>
|
|
329
|
+
</Tooltip>
|
|
330
|
+
</Flexbox>
|
|
331
|
+
</Flexbox>
|
|
332
|
+
);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
export default memo(ChatMinimap);
|
|
@@ -59,7 +59,11 @@ const TopicPanel = memo(({ children }: PropsWithChildren) => {
|
|
|
59
59
|
mode={md ? 'fixed' : 'float'}
|
|
60
60
|
onExpandChange={handleExpand}
|
|
61
61
|
placement={'right'}
|
|
62
|
+
showHandleWhenCollapsed={false}
|
|
62
63
|
showHandleWideArea={false}
|
|
64
|
+
styles={{
|
|
65
|
+
handle: { display: 'none' },
|
|
66
|
+
}}
|
|
63
67
|
>
|
|
64
68
|
<DraggablePanelContainer
|
|
65
69
|
style={{
|
package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/QualitySelect.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Select } from '@lobehub/ui';
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
|
|
5
|
+
import { useGenerationConfigParam } from '@/store/image/slices/generationConfig/hooks';
|
|
6
|
+
|
|
7
|
+
const QualitySelect = memo(() => {
|
|
8
|
+
const { t } = useTranslation('image');
|
|
9
|
+
const { value, setValue, enumValues } = useGenerationConfigParam('quality');
|
|
10
|
+
|
|
11
|
+
const options =
|
|
12
|
+
enumValues?.map((quality) => ({
|
|
13
|
+
label:
|
|
14
|
+
quality === 'standard'
|
|
15
|
+
? t('config.quality.options.standard')
|
|
16
|
+
: t('config.quality.options.hd'),
|
|
17
|
+
value: quality,
|
|
18
|
+
})) ?? [];
|
|
19
|
+
|
|
20
|
+
return <Select onChange={setValue} options={options} style={{ width: '100%' }} value={value} />;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export default QualitySelect;
|
|
@@ -18,6 +18,7 @@ import ImageNum from './components/ImageNum';
|
|
|
18
18
|
import ImageUrl from './components/ImageUrl';
|
|
19
19
|
import ImageUrlsUpload from './components/ImageUrlsUpload';
|
|
20
20
|
import ModelSelect from './components/ModelSelect';
|
|
21
|
+
import QualitySelect from './components/QualitySelect';
|
|
21
22
|
import SeedNumberInput from './components/SeedNumberInput';
|
|
22
23
|
import SizeSelect from './components/SizeSelect';
|
|
23
24
|
import StepsSliderInput from './components/StepsSliderInput';
|
|
@@ -52,6 +53,7 @@ const ConfigPanel = memo(() => {
|
|
|
52
53
|
const isInit = useImageStore((s) => s.isInit);
|
|
53
54
|
const isSupportImageUrl = useImageStore(isSupportedParamSelector('imageUrl'));
|
|
54
55
|
const isSupportSize = useImageStore(isSupportedParamSelector('size'));
|
|
56
|
+
const isSupportQuality = useImageStore(isSupportedParamSelector('quality'));
|
|
55
57
|
const isSupportSeed = useImageStore(isSupportedParamSelector('seed'));
|
|
56
58
|
const isSupportSteps = useImageStore(isSupportedParamSelector('steps'));
|
|
57
59
|
const isSupportCfg = useImageStore(isSupportedParamSelector('cfg'));
|
|
@@ -75,6 +77,7 @@ const ConfigPanel = memo(() => {
|
|
|
75
77
|
checkScrollable,
|
|
76
78
|
isSupportImageUrl,
|
|
77
79
|
isSupportSize,
|
|
80
|
+
isSupportQuality,
|
|
78
81
|
isSupportSeed,
|
|
79
82
|
isSupportSteps,
|
|
80
83
|
isSupportCfg,
|
|
@@ -159,6 +162,12 @@ const ConfigPanel = memo(() => {
|
|
|
159
162
|
</ConfigItemLayout>
|
|
160
163
|
)}
|
|
161
164
|
|
|
165
|
+
{isSupportQuality && (
|
|
166
|
+
<ConfigItemLayout label={t('config.quality.label')}>
|
|
167
|
+
<QualitySelect />
|
|
168
|
+
</ConfigItemLayout>
|
|
169
|
+
)}
|
|
170
|
+
|
|
162
171
|
{showDimensionControl && <DimensionControlGroup />}
|
|
163
172
|
|
|
164
173
|
{isSupportSteps && (
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { ModelTokensUsage } from '@/types/message';
|
|
3
|
+
import { ModelUsage } from '@/types/message';
|
|
5
4
|
|
|
5
|
+
import { LobeDefaultAiModelListItem } from '../../../../../../packages/model-bank/src/types/aiModel';
|
|
6
6
|
import { getDetailsToken } from './tokens';
|
|
7
7
|
|
|
8
8
|
describe('getDetailsToken', () => {
|
|
@@ -20,7 +20,7 @@ describe('getDetailsToken', () => {
|
|
|
20
20
|
} as LobeDefaultAiModelListItem;
|
|
21
21
|
|
|
22
22
|
it('should return empty object when usage is empty', () => {
|
|
23
|
-
const usage:
|
|
23
|
+
const usage: ModelUsage = {};
|
|
24
24
|
const result = getDetailsToken(usage);
|
|
25
25
|
|
|
26
26
|
expect(result).toEqual({
|
|
@@ -38,7 +38,7 @@ describe('getDetailsToken', () => {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
it('should handle inputTextTokens correctly', () => {
|
|
41
|
-
const usage:
|
|
41
|
+
const usage: ModelUsage = {
|
|
42
42
|
inputTextTokens: 100,
|
|
43
43
|
};
|
|
44
44
|
|
|
@@ -67,7 +67,7 @@ describe('getDetailsToken', () => {
|
|
|
67
67
|
const usage = {
|
|
68
68
|
totalInputTokens: 200,
|
|
69
69
|
cachedTokens: 50,
|
|
70
|
-
} as
|
|
70
|
+
} as ModelUsage;
|
|
71
71
|
|
|
72
72
|
const result = getDetailsToken(usage, mockModelCard);
|
|
73
73
|
|
|
@@ -83,7 +83,7 @@ describe('getDetailsToken', () => {
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
it('should handle outputTokens correctly', () => {
|
|
86
|
-
const usage = { outputTokens: 150 } as
|
|
86
|
+
const usage = { outputTokens: 150 } as ModelUsage;
|
|
87
87
|
|
|
88
88
|
const result = getDetailsToken(usage, mockModelCard);
|
|
89
89
|
|
|
@@ -102,7 +102,7 @@ describe('getDetailsToken', () => {
|
|
|
102
102
|
const usage = {
|
|
103
103
|
outputTokens: 200,
|
|
104
104
|
reasoningTokens: 50,
|
|
105
|
-
} as
|
|
105
|
+
} as ModelUsage;
|
|
106
106
|
|
|
107
107
|
const result = getDetailsToken(usage, mockModelCard);
|
|
108
108
|
|
|
@@ -122,7 +122,7 @@ describe('getDetailsToken', () => {
|
|
|
122
122
|
inputAudioTokens: 100,
|
|
123
123
|
outputAudioTokens: 50,
|
|
124
124
|
outputTokens: 150,
|
|
125
|
-
} as
|
|
125
|
+
} as ModelUsage;
|
|
126
126
|
|
|
127
127
|
const result = getDetailsToken(usage, mockModelCard);
|
|
128
128
|
|
|
@@ -150,7 +150,7 @@ describe('getDetailsToken', () => {
|
|
|
150
150
|
outputReasoningTokens: 30,
|
|
151
151
|
totalOutputTokens: 200,
|
|
152
152
|
totalTokens: 300,
|
|
153
|
-
} as
|
|
153
|
+
} as ModelUsage;
|
|
154
154
|
|
|
155
155
|
const result = getDetailsToken(usage, mockModelCard);
|
|
156
156
|
|
|
@@ -182,7 +182,7 @@ describe('getDetailsToken', () => {
|
|
|
182
182
|
});
|
|
183
183
|
|
|
184
184
|
it('should handle inputCitationTokens correctly', () => {
|
|
185
|
-
const usage:
|
|
185
|
+
const usage: ModelUsage = {
|
|
186
186
|
inputCitationTokens: 75,
|
|
187
187
|
};
|
|
188
188
|
|
|
@@ -200,7 +200,7 @@ describe('getDetailsToken', () => {
|
|
|
200
200
|
totalInputTokens: 200,
|
|
201
201
|
inputCachedTokens: 50,
|
|
202
202
|
outputTokens: 300,
|
|
203
|
-
} as
|
|
203
|
+
} as ModelUsage;
|
|
204
204
|
|
|
205
205
|
const result = getDetailsToken(usage, mockModelCard);
|
|
206
206
|
|
|
@@ -216,7 +216,7 @@ describe('getDetailsToken', () => {
|
|
|
216
216
|
});
|
|
217
217
|
|
|
218
218
|
it('should handle missing pricing information', () => {
|
|
219
|
-
const usage = { inputTextTokens: 100, outputTokens: 200 } as
|
|
219
|
+
const usage = { inputTextTokens: 100, outputTokens: 200 } as ModelUsage;
|
|
220
220
|
|
|
221
221
|
const result = getDetailsToken(usage);
|
|
222
222
|
|
|
@@ -232,7 +232,7 @@ describe('getDetailsToken', () => {
|
|
|
232
232
|
});
|
|
233
233
|
|
|
234
234
|
it('should handle complex scenario with all token types', () => {
|
|
235
|
-
const usage:
|
|
235
|
+
const usage: ModelUsage = {
|
|
236
236
|
totalTokens: 1000,
|
|
237
237
|
totalInputTokens: 400,
|
|
238
238
|
inputTextTokens: 300,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ModelTokensUsage } from '@lobechat/model-runtime';
|
|
2
1
|
import { LobeDefaultAiModelListItem } from 'model-bank';
|
|
3
2
|
|
|
3
|
+
import type { ModelTokensUsage } from '@/types/message';
|
|
4
4
|
import { getAudioInputUnitRate, getAudioOutputUnitRate } from '@/utils/pricing';
|
|
5
5
|
|
|
6
6
|
import { getPrice } from './pricing';
|