@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.
Files changed (130) hide show
  1. package/.cursor/rules/project-introduce.mdc +19 -25
  2. package/.cursor/rules/project-structure.mdc +102 -221
  3. package/.cursor/rules/{rules-attach.mdc → rules-index.mdc} +2 -11
  4. package/.cursor/rules/typescript.mdc +3 -53
  5. package/.vscode/settings.json +2 -1
  6. package/AGENTS.md +33 -54
  7. package/CHANGELOG.md +58 -0
  8. package/CLAUDE.md +1 -26
  9. package/changelog/v1.json +21 -0
  10. package/locales/ar/chat.json +5 -0
  11. package/locales/ar/image.json +7 -0
  12. package/locales/ar/models.json +2 -2
  13. package/locales/bg-BG/chat.json +5 -0
  14. package/locales/bg-BG/image.json +7 -0
  15. package/locales/de-DE/chat.json +5 -0
  16. package/locales/de-DE/image.json +7 -0
  17. package/locales/en-US/chat.json +5 -0
  18. package/locales/en-US/image.json +7 -0
  19. package/locales/es-ES/chat.json +5 -0
  20. package/locales/es-ES/image.json +7 -0
  21. package/locales/es-ES/tool.json +1 -1
  22. package/locales/fa-IR/chat.json +5 -0
  23. package/locales/fa-IR/image.json +7 -0
  24. package/locales/fa-IR/models.json +2 -2
  25. package/locales/fr-FR/chat.json +5 -0
  26. package/locales/fr-FR/image.json +7 -0
  27. package/locales/fr-FR/models.json +2 -2
  28. package/locales/it-IT/chat.json +5 -0
  29. package/locales/it-IT/image.json +7 -0
  30. package/locales/ja-JP/chat.json +5 -0
  31. package/locales/ja-JP/image.json +7 -0
  32. package/locales/ko-KR/chat.json +5 -0
  33. package/locales/ko-KR/image.json +7 -0
  34. package/locales/nl-NL/chat.json +5 -0
  35. package/locales/nl-NL/image.json +7 -0
  36. package/locales/pl-PL/chat.json +5 -0
  37. package/locales/pl-PL/image.json +7 -0
  38. package/locales/pt-BR/chat.json +5 -0
  39. package/locales/pt-BR/image.json +7 -0
  40. package/locales/ru-RU/chat.json +5 -0
  41. package/locales/ru-RU/image.json +7 -0
  42. package/locales/ru-RU/tool.json +1 -1
  43. package/locales/tr-TR/chat.json +5 -0
  44. package/locales/tr-TR/image.json +7 -0
  45. package/locales/tr-TR/models.json +2 -2
  46. package/locales/vi-VN/chat.json +5 -0
  47. package/locales/vi-VN/image.json +7 -0
  48. package/locales/zh-CN/chat.json +5 -0
  49. package/locales/zh-CN/image.json +7 -0
  50. package/locales/zh-TW/chat.json +5 -0
  51. package/locales/zh-TW/image.json +7 -0
  52. package/package.json +4 -5
  53. package/packages/const/package.json +4 -0
  54. package/packages/const/src/currency.ts +2 -0
  55. package/packages/const/src/index.ts +1 -0
  56. package/packages/model-bank/package.json +2 -1
  57. package/packages/model-bank/src/aiModels/google.ts +6 -0
  58. package/packages/model-bank/src/aiModels/openai.ts +6 -22
  59. package/packages/model-bank/src/standard-parameters/index.ts +56 -46
  60. package/packages/model-runtime/package.json +1 -0
  61. package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +4 -2
  62. package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.ts +12 -2
  63. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +16 -5
  64. package/packages/model-runtime/src/core/streams/anthropic.ts +25 -36
  65. package/packages/model-runtime/src/core/streams/google/google-ai.test.ts +1 -1
  66. package/packages/model-runtime/src/core/streams/google/index.ts +18 -42
  67. package/packages/model-runtime/src/core/streams/openai/openai.test.ts +7 -10
  68. package/packages/model-runtime/src/core/streams/openai/openai.ts +14 -11
  69. package/packages/model-runtime/src/core/streams/openai/responsesStream.ts +11 -5
  70. package/packages/model-runtime/src/core/streams/protocol.ts +25 -6
  71. package/packages/model-runtime/src/core/streams/qwen.ts +2 -2
  72. package/packages/model-runtime/src/core/streams/spark.ts +3 -3
  73. package/packages/model-runtime/src/core/streams/vertex-ai.test.ts +2 -2
  74. package/packages/model-runtime/src/core/streams/vertex-ai.ts +14 -23
  75. package/packages/model-runtime/src/core/usageConverters/anthropic.test.ts +99 -0
  76. package/packages/model-runtime/src/core/usageConverters/anthropic.ts +73 -0
  77. package/packages/model-runtime/src/core/usageConverters/google-ai.test.ts +88 -0
  78. package/packages/model-runtime/src/core/usageConverters/google-ai.ts +55 -0
  79. package/packages/model-runtime/src/core/usageConverters/index.ts +4 -0
  80. package/packages/model-runtime/src/core/usageConverters/openai.test.ts +429 -0
  81. package/packages/model-runtime/src/core/usageConverters/openai.ts +152 -0
  82. package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.test.ts +455 -0
  83. package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.ts +293 -0
  84. package/packages/model-runtime/src/core/usageConverters/utils/computeImageCost.test.ts +47 -0
  85. package/packages/model-runtime/src/core/usageConverters/utils/computeImageCost.ts +121 -0
  86. package/packages/model-runtime/src/core/usageConverters/utils/index.ts +11 -0
  87. package/packages/model-runtime/src/core/usageConverters/utils/withUsageCost.ts +19 -0
  88. package/packages/model-runtime/src/index.ts +2 -0
  89. package/packages/model-runtime/src/providers/anthropic/index.ts +48 -1
  90. package/packages/model-runtime/src/providers/google/createImage.ts +11 -2
  91. package/packages/model-runtime/src/providers/google/index.ts +8 -1
  92. package/packages/model-runtime/src/providers/openai/__snapshots__/index.test.ts.snap +7 -0
  93. package/packages/model-runtime/src/providers/zhipu/index.ts +3 -1
  94. package/packages/model-runtime/src/types/chat.ts +5 -3
  95. package/packages/model-runtime/src/types/image.ts +20 -9
  96. package/packages/model-runtime/src/utils/getModelPricing.ts +36 -0
  97. package/packages/obervability-otel/package.json +2 -2
  98. package/packages/ssrf-safe-fetch/index.test.ts +343 -0
  99. package/packages/ssrf-safe-fetch/index.ts +37 -0
  100. package/packages/ssrf-safe-fetch/package.json +17 -0
  101. package/packages/ssrf-safe-fetch/vitest.config.mts +10 -0
  102. package/packages/types/src/message/base.ts +43 -17
  103. package/packages/utils/src/client/apiKeyManager.test.ts +70 -0
  104. package/packages/utils/src/client/apiKeyManager.ts +41 -0
  105. package/packages/utils/src/client/index.ts +2 -0
  106. package/packages/utils/src/fetch/fetchSSE.ts +4 -4
  107. package/packages/utils/src/index.ts +1 -0
  108. package/packages/utils/src/toolManifest.ts +2 -1
  109. package/src/app/(backend)/webapi/proxy/route.ts +2 -13
  110. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/default.tsx +2 -0
  111. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatMinimap/index.tsx +335 -0
  112. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/TopicPanel.tsx +4 -0
  113. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/QualitySelect.tsx +23 -0
  114. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +9 -0
  115. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.test.ts +13 -13
  116. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +1 -1
  117. package/src/features/Conversation/components/ChatItem/index.tsx +56 -2
  118. package/src/features/Conversation/components/VirtualizedList/VirtuosoContext.ts +88 -0
  119. package/src/features/Conversation/components/VirtualizedList/index.tsx +15 -1
  120. package/src/locales/default/chat.ts +5 -0
  121. package/src/locales/default/image.ts +7 -0
  122. package/src/server/modules/EdgeConfig/index.ts +1 -1
  123. package/src/server/routers/async/image.ts +9 -1
  124. package/src/services/_auth.ts +12 -12
  125. package/src/services/chat/contextEngineering.ts +2 -3
  126. package/.cursor/rules/backend-architecture.mdc +0 -176
  127. package/.cursor/rules/code-review.mdc +0 -58
  128. package/.cursor/rules/cursor-ux.mdc +0 -32
  129. package/.cursor/rules/define-database-model.mdc +0 -8
  130. 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
- ModelTokensUsage,
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?: ModelTokensUsage;
35
+ usage?: ModelUsage;
36
36
  },
37
37
  ) => Promise<void>;
38
38
 
39
39
  export interface MessageUsageChunk {
40
40
  type: 'usage';
41
- usage: ModelTokensUsage;
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: ModelTokensUsage | undefined = undefined;
382
+ let usage: ModelUsage | undefined = undefined;
383
383
  let images: ChatImageChunk[] = [];
384
384
  let speed: ModelSpeed | undefined = undefined;
385
385
 
@@ -8,6 +8,7 @@ export * from './parseModels';
8
8
  export * from './pricing';
9
9
  export * from './safeParseJSON';
10
10
  export * from './sleep';
11
+ export * from './toolCall';
11
12
  export * from './uriParser';
12
13
  export * from './url';
13
14
  export * from './uuid';
@@ -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
- import { genToolCallingName } from '@/utils/toolCall';
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 fetch from 'node-fetch';
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
- // https://www.npmjs.com/package/request-filtering-agent
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
  };
@@ -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={{
@@ -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 { LobeDefaultAiModelListItem } from '../../../../../../packages/model-bank/src/types/aiModel';
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: ModelTokensUsage = {};
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: ModelTokensUsage = {
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 ModelTokensUsage;
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 ModelTokensUsage;
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 ModelTokensUsage;
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 ModelTokensUsage;
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 ModelTokensUsage;
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: ModelTokensUsage = {
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 ModelTokensUsage;
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 ModelTokensUsage;
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: ModelTokensUsage = {
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';