@lobehub/lobehub 2.0.0-next.204 → 2.0.0-next.206
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/changelog/v1.json +14 -0
- package/locales/ar/components.json +4 -0
- package/locales/ar/models.json +25 -126
- package/locales/bg-BG/components.json +4 -0
- package/locales/bg-BG/models.json +2 -2
- package/locales/de-DE/components.json +4 -0
- package/locales/de-DE/models.json +21 -120
- package/locales/en-US/components.json +4 -0
- package/locales/es-ES/components.json +4 -0
- package/locales/es-ES/models.json +24 -180
- package/locales/fa-IR/components.json +4 -0
- package/locales/fa-IR/models.json +2 -2
- package/locales/fr-FR/components.json +4 -0
- package/locales/fr-FR/models.json +2 -108
- package/locales/it-IT/components.json +4 -0
- package/locales/it-IT/models.json +22 -51
- package/locales/ja-JP/components.json +4 -0
- package/locales/ja-JP/models.json +16 -133
- package/locales/ko-KR/components.json +4 -0
- package/locales/ko-KR/models.json +26 -148
- package/locales/nl-NL/components.json +4 -0
- package/locales/nl-NL/models.json +2 -2
- package/locales/pl-PL/components.json +4 -0
- package/locales/pl-PL/models.json +2 -2
- package/locales/pt-BR/components.json +4 -0
- package/locales/pt-BR/models.json +49 -125
- package/locales/ru-RU/components.json +4 -0
- package/locales/ru-RU/models.json +17 -96
- package/locales/tr-TR/components.json +4 -0
- package/locales/tr-TR/models.json +28 -57
- package/locales/vi-VN/components.json +4 -0
- package/locales/vi-VN/models.json +1 -92
- package/locales/zh-CN/components.json +4 -0
- package/locales/zh-CN/models.json +31 -165
- package/locales/zh-TW/components.json +4 -0
- package/locales/zh-TW/models.json +2 -2
- package/package.json +1 -1
- package/packages/builtin-tool-gtd/src/executor/index.ts +7 -4
- package/packages/utils/src/object.test.ts +10 -2
- package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/index.tsx +1 -1
- package/src/features/ModelSwitchPanel/index.tsx +392 -41
- package/src/locales/default/components.ts +4 -0
- package/src/store/aiInfra/slices/aiModel/selectors.test.ts +1 -0
- package/src/store/aiInfra/slices/aiProvider/__tests__/selectors.test.ts +34 -11
- package/src/store/aiInfra/slices/aiProvider/action.ts +9 -1
- package/src/store/aiInfra/slices/aiProvider/initialState.ts +6 -1
- package/src/store/aiInfra/slices/aiProvider/selectors.ts +17 -3
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import { ActionIcon, Flexbox, Icon, TooltipGroup } from '@lobehub/ui';
|
|
1
|
+
import { ActionIcon, Flexbox, Icon, Segmented, TooltipGroup } from '@lobehub/ui';
|
|
2
|
+
import { ProviderIcon } from '@lobehub/ui/icons';
|
|
2
3
|
import { Dropdown } from 'antd';
|
|
3
4
|
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
Brain,
|
|
7
|
+
LucideArrowRight,
|
|
8
|
+
LucideBolt,
|
|
9
|
+
LucideCheck,
|
|
10
|
+
LucideChevronRight,
|
|
11
|
+
LucideSettings,
|
|
12
|
+
} from 'lucide-react';
|
|
5
13
|
import { type AiModelForSelect } from 'model-bank';
|
|
6
14
|
import { type ReactNode, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
|
7
15
|
import { useTranslation } from 'react-i18next';
|
|
@@ -16,21 +24,26 @@ import { agentSelectors } from '@/store/agent/selectors';
|
|
|
16
24
|
import { type EnabledProviderWithModels } from '@/types/aiProvider';
|
|
17
25
|
|
|
18
26
|
const STORAGE_KEY = 'MODEL_SWITCH_PANEL_WIDTH';
|
|
27
|
+
const STORAGE_KEY_MODE = 'MODEL_SWITCH_PANEL_MODE';
|
|
19
28
|
const DEFAULT_WIDTH = 320;
|
|
20
29
|
const MIN_WIDTH = 280;
|
|
21
30
|
const MAX_WIDTH = 600;
|
|
22
|
-
const MAX_PANEL_HEIGHT =
|
|
31
|
+
const MAX_PANEL_HEIGHT = 460;
|
|
32
|
+
const TOOLBAR_HEIGHT = 40;
|
|
33
|
+
const FOOTER_HEIGHT = 48;
|
|
23
34
|
|
|
24
35
|
const INITIAL_RENDER_COUNT = 15;
|
|
25
36
|
const RENDER_ALL_DELAY_MS = 500;
|
|
26
37
|
|
|
27
38
|
const ITEM_HEIGHT = {
|
|
28
|
-
'empty-model':
|
|
29
|
-
'group-header':
|
|
30
|
-
'model-item':
|
|
31
|
-
'no-provider':
|
|
39
|
+
'empty-model': 32,
|
|
40
|
+
'group-header': 32,
|
|
41
|
+
'model-item': 32,
|
|
42
|
+
'no-provider': 32,
|
|
32
43
|
} as const;
|
|
33
44
|
|
|
45
|
+
type GroupMode = 'byModel' | 'byProvider';
|
|
46
|
+
|
|
34
47
|
const ENABLE_RESIZING = {
|
|
35
48
|
bottom: false,
|
|
36
49
|
bottomLeft: false,
|
|
@@ -54,9 +67,44 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
|
54
67
|
background: ${cssVar.colorBgElevated};
|
|
55
68
|
box-shadow: ${cssVar.boxShadowSecondary};
|
|
56
69
|
`,
|
|
70
|
+
footer: css`
|
|
71
|
+
position: sticky;
|
|
72
|
+
z-index: 10;
|
|
73
|
+
inset-block-end: 0;
|
|
74
|
+
|
|
75
|
+
padding-block: 6px;
|
|
76
|
+
padding-inline: 0;
|
|
77
|
+
border-block-start: 1px solid ${cssVar.colorBorderSecondary};
|
|
78
|
+
|
|
79
|
+
background: ${cssVar.colorBgElevated};
|
|
80
|
+
`,
|
|
81
|
+
footerButton: css`
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
|
|
84
|
+
display: flex;
|
|
85
|
+
gap: 8px;
|
|
86
|
+
align-items: center;
|
|
87
|
+
justify-content: space-between;
|
|
88
|
+
|
|
89
|
+
box-sizing: border-box;
|
|
90
|
+
margin-inline: 8px;
|
|
91
|
+
padding-block: 6px;
|
|
92
|
+
padding-inline: 8px;
|
|
93
|
+
border-radius: ${cssVar.borderRadiusSM};
|
|
94
|
+
|
|
95
|
+
color: ${cssVar.colorTextSecondary};
|
|
96
|
+
|
|
97
|
+
transition: all 0.2s;
|
|
98
|
+
|
|
99
|
+
&:hover {
|
|
100
|
+
color: ${cssVar.colorText};
|
|
101
|
+
background: ${cssVar.colorFillTertiary};
|
|
102
|
+
}
|
|
103
|
+
`,
|
|
57
104
|
groupHeader: css`
|
|
58
|
-
|
|
59
|
-
padding-
|
|
105
|
+
margin-inline: 8px;
|
|
106
|
+
padding-block: 6px;
|
|
107
|
+
padding-inline: 8px;
|
|
60
108
|
color: ${cssVar.colorTextSecondary};
|
|
61
109
|
`,
|
|
62
110
|
menuItem: css`
|
|
@@ -67,26 +115,84 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
|
67
115
|
align-items: center;
|
|
68
116
|
|
|
69
117
|
box-sizing: border-box;
|
|
70
|
-
|
|
71
|
-
padding-
|
|
118
|
+
margin-inline: 8px;
|
|
119
|
+
padding-block: 6px;
|
|
120
|
+
padding-inline: 8px;
|
|
121
|
+
border-radius: ${cssVar.borderRadiusSM};
|
|
72
122
|
|
|
73
123
|
white-space: nowrap;
|
|
74
124
|
|
|
75
125
|
&:hover {
|
|
76
126
|
background: ${cssVar.colorFillTertiary};
|
|
127
|
+
|
|
128
|
+
.settings-icon {
|
|
129
|
+
opacity: 1;
|
|
130
|
+
}
|
|
77
131
|
}
|
|
78
132
|
`,
|
|
79
133
|
menuItemActive: css`
|
|
80
134
|
background: ${cssVar.colorFillTertiary};
|
|
81
135
|
`,
|
|
136
|
+
settingsIcon: css`
|
|
137
|
+
opacity: 0;
|
|
138
|
+
`,
|
|
139
|
+
submenu: css`
|
|
140
|
+
.ant-dropdown-menu {
|
|
141
|
+
padding: 4px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.ant-dropdown-menu-item {
|
|
145
|
+
margin-inline: 0;
|
|
146
|
+
padding-block: 6px;
|
|
147
|
+
padding-inline: 8px;
|
|
148
|
+
border-radius: ${cssVar.borderRadiusSM};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.ant-dropdown-menu-item-group-title {
|
|
152
|
+
padding-block: 6px;
|
|
153
|
+
padding-inline: 8px;
|
|
154
|
+
font-size: 12px;
|
|
155
|
+
color: ${cssVar.colorTextSecondary};
|
|
156
|
+
}
|
|
157
|
+
`,
|
|
82
158
|
tag: css`
|
|
83
159
|
cursor: pointer;
|
|
84
160
|
`,
|
|
161
|
+
toolbar: css`
|
|
162
|
+
position: sticky;
|
|
163
|
+
z-index: 10;
|
|
164
|
+
inset-block-start: 0;
|
|
165
|
+
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
justify-content: flex-end;
|
|
169
|
+
|
|
170
|
+
padding-block: 6px;
|
|
171
|
+
padding-inline: 8px;
|
|
172
|
+
border-block-end: 1px solid ${cssVar.colorBorderSecondary};
|
|
173
|
+
|
|
174
|
+
background: ${cssVar.colorBgElevated};
|
|
175
|
+
`,
|
|
85
176
|
}));
|
|
86
177
|
|
|
87
178
|
const menuKey = (provider: string, model: string) => `${provider}-${model}`;
|
|
88
179
|
|
|
180
|
+
interface ModelWithProviders {
|
|
181
|
+
displayName: string;
|
|
182
|
+
model: AiModelForSelect;
|
|
183
|
+
providers: Array<{
|
|
184
|
+
id: string;
|
|
185
|
+
logo?: string;
|
|
186
|
+
name: string;
|
|
187
|
+
source?: EnabledProviderWithModels['source'];
|
|
188
|
+
}>;
|
|
189
|
+
}
|
|
190
|
+
|
|
89
191
|
type VirtualItem =
|
|
192
|
+
| {
|
|
193
|
+
data: ModelWithProviders;
|
|
194
|
+
type: 'model-item';
|
|
195
|
+
}
|
|
90
196
|
| {
|
|
91
197
|
provider: EnabledProviderWithModels;
|
|
92
198
|
type: 'group-header';
|
|
@@ -94,7 +200,7 @@ type VirtualItem =
|
|
|
94
200
|
| {
|
|
95
201
|
model: AiModelForSelect;
|
|
96
202
|
provider: EnabledProviderWithModels;
|
|
97
|
-
type: 'model-item';
|
|
203
|
+
type: 'provider-model-item';
|
|
98
204
|
}
|
|
99
205
|
| {
|
|
100
206
|
provider: EnabledProviderWithModels;
|
|
@@ -148,6 +254,12 @@ const ModelSwitchPanel = memo<ModelSwitchPanelProps>(
|
|
|
148
254
|
return stored ? Number(stored) : DEFAULT_WIDTH;
|
|
149
255
|
});
|
|
150
256
|
|
|
257
|
+
const [groupMode, setGroupMode] = useState<GroupMode>(() => {
|
|
258
|
+
if (typeof window === 'undefined') return 'byModel';
|
|
259
|
+
const stored = localStorage.getItem(STORAGE_KEY_MODE);
|
|
260
|
+
return (stored as GroupMode) || 'byModel';
|
|
261
|
+
});
|
|
262
|
+
|
|
151
263
|
const [renderAll, setRenderAll] = useState(false);
|
|
152
264
|
const [internalOpen, setInternalOpen] = useState(false);
|
|
153
265
|
|
|
@@ -198,41 +310,74 @@ const ModelSwitchPanel = memo<ModelSwitchPanelProps>(
|
|
|
198
310
|
[onModelChange, updateAgentConfig],
|
|
199
311
|
);
|
|
200
312
|
|
|
201
|
-
//
|
|
202
|
-
const
|
|
313
|
+
// Build virtual items based on group mode
|
|
314
|
+
const virtualItems = useMemo(() => {
|
|
203
315
|
if (enabledList.length === 0) {
|
|
204
|
-
return {
|
|
205
|
-
panelHeight: ITEM_HEIGHT['no-provider'],
|
|
206
|
-
virtualItems: [{ type: 'no-provider' }] as VirtualItem[],
|
|
207
|
-
};
|
|
316
|
+
return [{ type: 'no-provider' }] as VirtualItem[];
|
|
208
317
|
}
|
|
209
318
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
for (const providerItem of enabledList) {
|
|
214
|
-
// Add provider group header
|
|
215
|
-
items.push({ provider: providerItem, type: 'group-header' });
|
|
216
|
-
totalHeight += ITEM_HEIGHT['group-header'];
|
|
319
|
+
if (groupMode === 'byModel') {
|
|
320
|
+
// Group models by display name
|
|
321
|
+
const modelMap = new Map<string, ModelWithProviders>();
|
|
217
322
|
|
|
218
|
-
|
|
219
|
-
// Add empty model placeholder
|
|
220
|
-
items.push({ provider: providerItem, type: 'empty-model' });
|
|
221
|
-
totalHeight += ITEM_HEIGHT['empty-model'];
|
|
222
|
-
} else {
|
|
223
|
-
// Add each model item
|
|
323
|
+
for (const providerItem of enabledList) {
|
|
224
324
|
for (const modelItem of providerItem.children) {
|
|
225
|
-
|
|
226
|
-
|
|
325
|
+
const displayName = modelItem.displayName || modelItem.id;
|
|
326
|
+
|
|
327
|
+
if (!modelMap.has(displayName)) {
|
|
328
|
+
modelMap.set(displayName, {
|
|
329
|
+
displayName,
|
|
330
|
+
model: modelItem,
|
|
331
|
+
providers: [],
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const entry = modelMap.get(displayName)!;
|
|
336
|
+
entry.providers.push({
|
|
337
|
+
id: providerItem.id,
|
|
338
|
+
logo: providerItem.logo,
|
|
339
|
+
name: providerItem.name,
|
|
340
|
+
source: providerItem.source,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Convert to array and sort by display name
|
|
346
|
+
return Array.from(modelMap.values())
|
|
347
|
+
.sort((a, b) => a.displayName.localeCompare(b.displayName))
|
|
348
|
+
.map((data) => ({ data, type: 'model-item' as const }));
|
|
349
|
+
} else {
|
|
350
|
+
// Group by provider (original structure)
|
|
351
|
+
const items: VirtualItem[] = [];
|
|
352
|
+
|
|
353
|
+
for (const providerItem of enabledList) {
|
|
354
|
+
// Add provider group header
|
|
355
|
+
items.push({ provider: providerItem, type: 'group-header' });
|
|
356
|
+
|
|
357
|
+
if (providerItem.children.length === 0) {
|
|
358
|
+
// Add empty model placeholder
|
|
359
|
+
items.push({ provider: providerItem, type: 'empty-model' });
|
|
360
|
+
} else {
|
|
361
|
+
// Add each model item
|
|
362
|
+
for (const modelItem of providerItem.children) {
|
|
363
|
+
items.push({
|
|
364
|
+
model: modelItem,
|
|
365
|
+
provider: providerItem,
|
|
366
|
+
type: 'provider-model-item',
|
|
367
|
+
});
|
|
368
|
+
}
|
|
227
369
|
}
|
|
228
370
|
}
|
|
371
|
+
|
|
372
|
+
return items;
|
|
229
373
|
}
|
|
374
|
+
}, [enabledList, groupMode]);
|
|
230
375
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
376
|
+
// Use a fixed panel height to prevent shifting when switching modes
|
|
377
|
+
const panelHeight =
|
|
378
|
+
enabledList.length === 0
|
|
379
|
+
? TOOLBAR_HEIGHT + ITEM_HEIGHT['no-provider'] + FOOTER_HEIGHT
|
|
380
|
+
: MAX_PANEL_HEIGHT;
|
|
236
381
|
|
|
237
382
|
const activeKey = menuKey(provider, model);
|
|
238
383
|
|
|
@@ -299,7 +444,7 @@ const ModelSwitchPanel = memo<ModelSwitchPanelProps>(
|
|
|
299
444
|
);
|
|
300
445
|
}
|
|
301
446
|
|
|
302
|
-
case 'model-item': {
|
|
447
|
+
case 'provider-model-item': {
|
|
303
448
|
const key = menuKey(item.provider.id, item.model.id);
|
|
304
449
|
const isActive = key === activeKey;
|
|
305
450
|
|
|
@@ -315,7 +460,7 @@ const ModelSwitchPanel = memo<ModelSwitchPanelProps>(
|
|
|
315
460
|
<ModelItemRender
|
|
316
461
|
{...item.model}
|
|
317
462
|
{...item.model.abilities}
|
|
318
|
-
|
|
463
|
+
infoTagTooltip={false}
|
|
319
464
|
newBadgeLabel={newLabel}
|
|
320
465
|
showInfoTag
|
|
321
466
|
/>
|
|
@@ -323,6 +468,167 @@ const ModelSwitchPanel = memo<ModelSwitchPanelProps>(
|
|
|
323
468
|
);
|
|
324
469
|
}
|
|
325
470
|
|
|
471
|
+
case 'model-item': {
|
|
472
|
+
const { data } = item;
|
|
473
|
+
const hasSingleProvider = data.providers.length === 1;
|
|
474
|
+
|
|
475
|
+
// Check if this model is currently active and find active provider
|
|
476
|
+
const activeProvider = data.providers.find(
|
|
477
|
+
(p) => menuKey(p.id, data.model.id) === activeKey,
|
|
478
|
+
);
|
|
479
|
+
const isActive = !!activeProvider;
|
|
480
|
+
// Use active provider if found, otherwise use first provider for settings link
|
|
481
|
+
const settingsProvider = activeProvider || data.providers[0];
|
|
482
|
+
|
|
483
|
+
// Single provider - direct click without submenu
|
|
484
|
+
if (hasSingleProvider) {
|
|
485
|
+
const singleProvider = data.providers[0];
|
|
486
|
+
const key = menuKey(singleProvider.id, data.model.id);
|
|
487
|
+
|
|
488
|
+
return (
|
|
489
|
+
<div className={cx(styles.menuItem, isActive && styles.menuItemActive)} key={key}>
|
|
490
|
+
<Flexbox
|
|
491
|
+
align={'center'}
|
|
492
|
+
gap={8}
|
|
493
|
+
horizontal
|
|
494
|
+
justify={'space-between'}
|
|
495
|
+
onClick={async () => {
|
|
496
|
+
await handleModelChange(data.model.id, singleProvider.id);
|
|
497
|
+
handleOpenChange(false);
|
|
498
|
+
}}
|
|
499
|
+
style={{ flex: 1, minWidth: 0 }}
|
|
500
|
+
>
|
|
501
|
+
<ModelItemRender
|
|
502
|
+
{...data.model}
|
|
503
|
+
{...data.model.abilities}
|
|
504
|
+
infoTagTooltip={false}
|
|
505
|
+
newBadgeLabel={newLabel}
|
|
506
|
+
showInfoTag={false}
|
|
507
|
+
/>
|
|
508
|
+
</Flexbox>
|
|
509
|
+
<div className={cx(styles.settingsIcon, 'settings-icon')}>
|
|
510
|
+
<ActionIcon
|
|
511
|
+
icon={LucideBolt}
|
|
512
|
+
onClick={(e) => {
|
|
513
|
+
e.preventDefault();
|
|
514
|
+
e.stopPropagation();
|
|
515
|
+
const url = urlJoin('/settings/provider', settingsProvider.id || 'all');
|
|
516
|
+
if (e.ctrlKey || e.metaKey) {
|
|
517
|
+
window.open(url, '_blank');
|
|
518
|
+
} else {
|
|
519
|
+
navigate(url);
|
|
520
|
+
}
|
|
521
|
+
}}
|
|
522
|
+
size={'small'}
|
|
523
|
+
title={t('ModelSwitchPanel.goToSettings')}
|
|
524
|
+
/>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Multiple providers - show submenu on hover
|
|
531
|
+
return (
|
|
532
|
+
<Dropdown
|
|
533
|
+
align={{ offset: [4, 0] }}
|
|
534
|
+
arrow={false}
|
|
535
|
+
dropdownRender={(menu) => (
|
|
536
|
+
<div className={styles.submenu} style={{ minWidth: 240 }}>
|
|
537
|
+
{menu}
|
|
538
|
+
</div>
|
|
539
|
+
)}
|
|
540
|
+
key={data.displayName}
|
|
541
|
+
menu={{
|
|
542
|
+
items: [
|
|
543
|
+
{
|
|
544
|
+
key: 'header',
|
|
545
|
+
label: t('ModelSwitchPanel.useModelFrom'),
|
|
546
|
+
type: 'group',
|
|
547
|
+
},
|
|
548
|
+
...data.providers.map((p) => {
|
|
549
|
+
const isCurrentProvider = menuKey(p.id, data.model.id) === activeKey;
|
|
550
|
+
return {
|
|
551
|
+
key: menuKey(p.id, data.model.id),
|
|
552
|
+
label: (
|
|
553
|
+
<Flexbox
|
|
554
|
+
align={'center'}
|
|
555
|
+
gap={8}
|
|
556
|
+
horizontal
|
|
557
|
+
justify={'space-between'}
|
|
558
|
+
style={{ minWidth: 0 }}
|
|
559
|
+
>
|
|
560
|
+
<Flexbox align={'center'} gap={8} horizontal style={{ minWidth: 0 }}>
|
|
561
|
+
<div style={{ flexShrink: 0, width: 16 }}>
|
|
562
|
+
{isCurrentProvider && (
|
|
563
|
+
<Icon
|
|
564
|
+
icon={LucideCheck}
|
|
565
|
+
size={16}
|
|
566
|
+
style={{ color: cssVar.colorPrimary }}
|
|
567
|
+
/>
|
|
568
|
+
)}
|
|
569
|
+
</div>
|
|
570
|
+
<ProviderItemRender
|
|
571
|
+
logo={p.logo}
|
|
572
|
+
name={p.name}
|
|
573
|
+
provider={p.id}
|
|
574
|
+
source={p.source}
|
|
575
|
+
/>
|
|
576
|
+
</Flexbox>
|
|
577
|
+
<ActionIcon
|
|
578
|
+
icon={LucideBolt}
|
|
579
|
+
onClick={(e) => {
|
|
580
|
+
e.preventDefault();
|
|
581
|
+
e.stopPropagation();
|
|
582
|
+
const url = urlJoin('/settings/provider', p.id || 'all');
|
|
583
|
+
if (e.ctrlKey || e.metaKey) {
|
|
584
|
+
window.open(url, '_blank');
|
|
585
|
+
} else {
|
|
586
|
+
navigate(url);
|
|
587
|
+
}
|
|
588
|
+
}}
|
|
589
|
+
size={'small'}
|
|
590
|
+
title={t('ModelSwitchPanel.goToSettings')}
|
|
591
|
+
/>
|
|
592
|
+
</Flexbox>
|
|
593
|
+
),
|
|
594
|
+
onClick: async () => {
|
|
595
|
+
await handleModelChange(data.model.id, p.id);
|
|
596
|
+
handleOpenChange(false);
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
}),
|
|
600
|
+
],
|
|
601
|
+
}}
|
|
602
|
+
// @ts-ignore
|
|
603
|
+
placement="rightTop"
|
|
604
|
+
trigger={['hover']}
|
|
605
|
+
>
|
|
606
|
+
<div className={cx(styles.menuItem, isActive && styles.menuItemActive)}>
|
|
607
|
+
<Flexbox
|
|
608
|
+
align={'center'}
|
|
609
|
+
gap={8}
|
|
610
|
+
horizontal
|
|
611
|
+
justify={'space-between'}
|
|
612
|
+
style={{ width: '100%' }}
|
|
613
|
+
>
|
|
614
|
+
<ModelItemRender
|
|
615
|
+
{...data.model}
|
|
616
|
+
{...data.model.abilities}
|
|
617
|
+
infoTagTooltip={false}
|
|
618
|
+
newBadgeLabel={newLabel}
|
|
619
|
+
showInfoTag={false}
|
|
620
|
+
/>
|
|
621
|
+
<Icon
|
|
622
|
+
icon={LucideChevronRight}
|
|
623
|
+
size={16}
|
|
624
|
+
style={{ color: cssVar.colorTextSecondary, flexShrink: 0 }}
|
|
625
|
+
/>
|
|
626
|
+
</Flexbox>
|
|
627
|
+
</div>
|
|
628
|
+
</Dropdown>
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
326
632
|
default: {
|
|
327
633
|
return null;
|
|
328
634
|
}
|
|
@@ -354,11 +660,56 @@ const ModelSwitchPanel = memo<ModelSwitchPanelProps>(
|
|
|
354
660
|
size={{ height: panelHeight, width: panelWidth }}
|
|
355
661
|
style={{ position: 'relative' }}
|
|
356
662
|
>
|
|
357
|
-
<div
|
|
663
|
+
<div className={styles.toolbar}>
|
|
664
|
+
<Segmented
|
|
665
|
+
onChange={(value) => {
|
|
666
|
+
const mode = value as GroupMode;
|
|
667
|
+
setGroupMode(mode);
|
|
668
|
+
localStorage.setItem(STORAGE_KEY_MODE, mode);
|
|
669
|
+
}}
|
|
670
|
+
options={[
|
|
671
|
+
{
|
|
672
|
+
icon: <Icon icon={Brain} />,
|
|
673
|
+
title: t('ModelSwitchPanel.byModel'),
|
|
674
|
+
value: 'byModel',
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
icon: <Icon icon={ProviderIcon} />,
|
|
678
|
+
title: t('ModelSwitchPanel.byProvider'),
|
|
679
|
+
value: 'byProvider',
|
|
680
|
+
},
|
|
681
|
+
]}
|
|
682
|
+
size="small"
|
|
683
|
+
value={groupMode}
|
|
684
|
+
/>
|
|
685
|
+
</div>
|
|
686
|
+
<div
|
|
687
|
+
style={{
|
|
688
|
+
height: panelHeight - TOOLBAR_HEIGHT - FOOTER_HEIGHT,
|
|
689
|
+
overflow: 'auto',
|
|
690
|
+
paddingBlock: groupMode === 'byModel' ? 8 : 0,
|
|
691
|
+
width: '100%',
|
|
692
|
+
}}
|
|
693
|
+
>
|
|
358
694
|
{(renderAll ? virtualItems : virtualItems.slice(0, INITIAL_RENDER_COUNT)).map(
|
|
359
695
|
renderVirtualItem,
|
|
360
696
|
)}
|
|
361
697
|
</div>
|
|
698
|
+
<div className={styles.footer}>
|
|
699
|
+
<div
|
|
700
|
+
className={styles.footerButton}
|
|
701
|
+
onClick={() => {
|
|
702
|
+
navigate('/settings/provider/all');
|
|
703
|
+
handleOpenChange(false);
|
|
704
|
+
}}
|
|
705
|
+
>
|
|
706
|
+
<Flexbox align={'center'} gap={8} horizontal style={{ flex: 1 }}>
|
|
707
|
+
<Icon icon={LucideSettings} size={16} />
|
|
708
|
+
{t('ModelSwitchPanel.manageProvider')}
|
|
709
|
+
</Flexbox>
|
|
710
|
+
<Icon icon={LucideArrowRight} size={16} />
|
|
711
|
+
</div>
|
|
712
|
+
</div>
|
|
362
713
|
</Rnd>
|
|
363
714
|
)}
|
|
364
715
|
>
|
|
@@ -107,11 +107,15 @@ export default {
|
|
|
107
107
|
'ModelSelect.featureTag.vision': 'This model supports visual recognition.',
|
|
108
108
|
'ModelSelect.removed':
|
|
109
109
|
'The model is not in the list. It will be automatically removed if deselected.',
|
|
110
|
+
'ModelSwitchPanel.byModel': 'By Model',
|
|
111
|
+
'ModelSwitchPanel.byProvider': 'By Provider',
|
|
110
112
|
'ModelSwitchPanel.emptyModel': 'No enabled model. Please go to settings to enable.',
|
|
111
113
|
'ModelSwitchPanel.emptyProvider': 'No enabled providers. Please go to settings to enable one.',
|
|
112
114
|
'ModelSwitchPanel.goToSettings': 'Go to settings',
|
|
115
|
+
'ModelSwitchPanel.manageProvider': 'Manage Provider',
|
|
113
116
|
'ModelSwitchPanel.provider': 'Provider',
|
|
114
117
|
'ModelSwitchPanel.title': 'Model',
|
|
118
|
+
'ModelSwitchPanel.useModelFrom': 'Use this model from:',
|
|
115
119
|
'MultiImagesUpload.actions.uploadMore': 'Click or drag to upload more',
|
|
116
120
|
'MultiImagesUpload.modal.complete': 'Done',
|
|
117
121
|
'MultiImagesUpload.modal.newFileIndicator': 'New',
|
|
@@ -10,11 +10,13 @@ describe('aiProviderSelectors', () => {
|
|
|
10
10
|
{ id: 'provider3', enabled: true, sort: 0, source: 'builtin' },
|
|
11
11
|
{ id: 'custom1', enabled: false, sort: 3, source: 'custom' },
|
|
12
12
|
],
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
aiProviderDetailMap: {
|
|
14
|
+
provider1: {
|
|
15
|
+
id: 'provider1',
|
|
16
|
+
keyVaults: {
|
|
17
|
+
baseURL: 'https://api.example.com',
|
|
18
|
+
apiKey: 'test-key',
|
|
19
|
+
},
|
|
18
20
|
},
|
|
19
21
|
},
|
|
20
22
|
aiProviderLoadingIds: ['loading-provider'],
|
|
@@ -97,20 +99,37 @@ describe('aiProviderSelectors', () => {
|
|
|
97
99
|
});
|
|
98
100
|
});
|
|
99
101
|
|
|
102
|
+
describe('providerDetailById', () => {
|
|
103
|
+
it('should return provider detail by id', () => {
|
|
104
|
+
expect(aiProviderSelectors.providerDetailById('provider1')(mockState)).toEqual(
|
|
105
|
+
mockState.aiProviderDetailMap.provider1,
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should return undefined for non-existing provider', () => {
|
|
110
|
+
expect(aiProviderSelectors.providerDetailById('non-existing')(mockState)).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
100
114
|
describe('activeProviderConfig', () => {
|
|
101
|
-
it('should return active provider config', () => {
|
|
115
|
+
it('should return active provider config from map', () => {
|
|
102
116
|
expect(aiProviderSelectors.activeProviderConfig(mockState)).toEqual(
|
|
103
|
-
mockState.
|
|
117
|
+
mockState.aiProviderDetailMap.provider1,
|
|
104
118
|
);
|
|
105
119
|
});
|
|
120
|
+
|
|
121
|
+
it('should return undefined when no active provider', () => {
|
|
122
|
+
const stateWithoutActive = { ...mockState, activeAiProvider: undefined };
|
|
123
|
+
expect(aiProviderSelectors.activeProviderConfig(stateWithoutActive)).toBeUndefined();
|
|
124
|
+
});
|
|
106
125
|
});
|
|
107
126
|
|
|
108
127
|
describe('isAiProviderConfigLoading', () => {
|
|
109
|
-
it('should return true if provider
|
|
128
|
+
it('should return true if provider is not in detail map (not loaded)', () => {
|
|
110
129
|
expect(aiProviderSelectors.isAiProviderConfigLoading('provider2')(mockState)).toBe(true);
|
|
111
130
|
});
|
|
112
131
|
|
|
113
|
-
it('should return false if provider
|
|
132
|
+
it('should return false if provider is in detail map (loaded)', () => {
|
|
114
133
|
expect(aiProviderSelectors.isAiProviderConfigLoading('provider1')(mockState)).toBe(false);
|
|
115
134
|
});
|
|
116
135
|
});
|
|
@@ -123,7 +142,9 @@ describe('aiProviderSelectors', () => {
|
|
|
123
142
|
it('should return false when no endpoint info exists', () => {
|
|
124
143
|
const stateWithoutEndpoint = {
|
|
125
144
|
...mockState,
|
|
126
|
-
|
|
145
|
+
aiProviderDetailMap: {
|
|
146
|
+
provider1: { id: 'provider1', keyVaults: {} },
|
|
147
|
+
},
|
|
127
148
|
};
|
|
128
149
|
expect(aiProviderSelectors.isActiveProviderEndpointNotEmpty(stateWithoutEndpoint)).toBe(
|
|
129
150
|
false,
|
|
@@ -139,7 +160,9 @@ describe('aiProviderSelectors', () => {
|
|
|
139
160
|
it('should return false when no api key exists', () => {
|
|
140
161
|
const stateWithoutApiKey = {
|
|
141
162
|
...mockState,
|
|
142
|
-
|
|
163
|
+
aiProviderDetailMap: {
|
|
164
|
+
provider1: { id: 'provider1', keyVaults: {} },
|
|
165
|
+
},
|
|
143
166
|
};
|
|
144
167
|
expect(aiProviderSelectors.isActiveProviderApiKeyNotEmpty(stateWithoutApiKey)).toBe(false);
|
|
145
168
|
});
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type UpdateAiProviderParams,
|
|
30
30
|
} from '@/types/aiProvider';
|
|
31
31
|
|
|
32
|
+
|
|
32
33
|
export type ProviderModelListItem = {
|
|
33
34
|
abilities: ModelAbilities;
|
|
34
35
|
approximatePricePerImage?: number;
|
|
@@ -293,7 +294,14 @@ export const createAiProviderSlice: StateCreator<
|
|
|
293
294
|
onSuccess: (data) => {
|
|
294
295
|
if (!data) return;
|
|
295
296
|
|
|
296
|
-
set(
|
|
297
|
+
set(
|
|
298
|
+
(state) => ({
|
|
299
|
+
activeAiProvider: id,
|
|
300
|
+
aiProviderDetailMap: { ...state.aiProviderDetailMap, [id]: data },
|
|
301
|
+
}),
|
|
302
|
+
false,
|
|
303
|
+
'useFetchAiProviderItem',
|
|
304
|
+
);
|
|
297
305
|
},
|
|
298
306
|
},
|
|
299
307
|
),
|