@lobehub/lobehub 2.0.0-next.221 → 2.0.0-next.223
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/.github/workflows/claude-auto-testing.yml +6 -3
- package/.github/workflows/claude-dedupe-issues.yml +8 -1
- package/.github/workflows/claude-issue-triage.yml +8 -14
- package/.github/workflows/claude-translate-comments.yml +6 -3
- package/.github/workflows/claude-translator.yml +12 -14
- package/.github/workflows/claude.yml +10 -20
- package/.github/workflows/test.yml +26 -0
- package/CHANGELOG.md +58 -0
- package/changelog/v1.json +18 -0
- package/e2e/package.json +1 -1
- package/e2e/src/mocks/index.ts +2 -2
- package/e2e/src/steps/{discover → community}/detail-pages.steps.ts +8 -8
- package/e2e/src/steps/{discover → community}/interactions.steps.ts +4 -4
- package/locales/zh-CN/components.json +1 -0
- package/package.json +3 -3
- package/packages/const/src/index.ts +0 -1
- package/packages/memory-user-memory/package.json +1 -0
- package/packages/memory-user-memory/src/extractors/context.test.ts +3 -2
- package/packages/memory-user-memory/src/extractors/experience.test.ts +3 -2
- package/packages/memory-user-memory/src/extractors/identity.test.ts +23 -6
- package/packages/memory-user-memory/src/extractors/preference.test.ts +3 -2
- package/packages/memory-user-memory/vitest.config.ts +4 -0
- package/packages/model-runtime/src/providers/replicate/index.ts +1 -1
- package/packages/ssrf-safe-fetch/index.test.ts +2 -2
- package/packages/ssrf-safe-fetch/package.json +3 -2
- package/packages/types/src/serverConfig.ts +2 -0
- package/packages/utils/package.json +1 -1
- package/packages/utils/src/client/xor-obfuscation.test.ts +32 -32
- package/packages/utils/src/client/xor-obfuscation.ts +3 -4
- package/packages/utils/src/imageToBase64.ts +1 -1
- package/packages/utils/src/server/__tests__/auth.test.ts +1 -1
- package/packages/utils/src/server/auth.ts +1 -1
- package/packages/utils/src/server/correctOIDCUrl.test.ts +80 -19
- package/packages/utils/src/server/correctOIDCUrl.ts +89 -24
- package/packages/utils/src/server/index.ts +1 -0
- package/packages/utils/src/server/xor.test.ts +9 -7
- package/packages/utils/src/server/xor.ts +1 -1
- package/packages/web-crawler/package.json +1 -1
- package/packages/web-crawler/src/crawImpl/__tests__/naive.test.ts +1 -1
- package/packages/web-crawler/src/crawImpl/naive.ts +1 -1
- package/scripts/prebuild.mts +58 -1
- package/src/app/(backend)/api/auth/[...all]/route.ts +2 -1
- package/src/app/(backend)/middleware/auth/index.ts +3 -3
- package/src/app/(backend)/middleware/auth/utils.test.ts +1 -1
- package/src/app/(backend)/middleware/auth/utils.ts +1 -1
- package/src/app/(backend)/oidc/callback/desktop/route.ts +7 -36
- package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +2 -2
- package/src/app/(backend)/webapi/models/[provider]/route.test.ts +1 -1
- package/src/app/(backend)/webapi/plugin/gateway/route.ts +1 -1
- package/src/app/(backend)/webapi/proxy/route.ts +1 -1
- package/src/app/[variants]/(auth)/login/[[...login]]/page.tsx +1 -1
- package/src/app/[variants]/(auth)/reset-password/layout.tsx +1 -1
- package/src/app/[variants]/(auth)/signin/layout.tsx +1 -1
- package/src/app/[variants]/(auth)/signin/useSignIn.ts +2 -2
- package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +1 -1
- package/src/app/[variants]/(auth)/signup/[[...signup]]/useSignUp.tsx +12 -6
- package/src/app/[variants]/(auth)/verify-email/layout.tsx +1 -1
- package/src/app/[variants]/(main)/settings/profile/features/AvatarRow.tsx +1 -1
- package/src/app/[variants]/(main)/settings/security/index.tsx +1 -1
- package/src/app/[variants]/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +1 -1
- package/src/app/[variants]/(mobile)/me/(home)/__tests__/useCategory.test.tsx +1 -1
- package/src/app/[variants]/(mobile)/me/(home)/features/UserBanner.tsx +1 -1
- package/src/app/[variants]/(mobile)/settings/_layout/Header.tsx +1 -1
- package/src/components/ModelSelect/index.tsx +103 -72
- package/src/envs/auth.ts +30 -9
- package/src/features/Conversation/Messages/AssistantGroup/components/EditState.tsx +15 -32
- package/src/features/Conversation/Messages/AssistantGroup/index.tsx +9 -7
- package/src/features/EditorModal/EditorCanvas.tsx +31 -50
- package/src/features/EditorModal/TextareCanvas.tsx +3 -1
- package/src/features/EditorModal/index.tsx +14 -4
- package/src/features/ModelSwitchPanel/components/Footer.tsx +42 -0
- package/src/features/ModelSwitchPanel/components/List/MultipleProvidersModelItem.tsx +103 -0
- package/src/features/ModelSwitchPanel/components/List/SingleProviderModelItem.tsx +24 -0
- package/src/features/ModelSwitchPanel/components/List/VirtualItemRenderer.tsx +180 -0
- package/src/features/ModelSwitchPanel/components/List/index.tsx +99 -0
- package/src/features/ModelSwitchPanel/components/PanelContent.tsx +77 -0
- package/src/features/ModelSwitchPanel/components/Toolbar.tsx +54 -0
- package/src/features/ModelSwitchPanel/const.ts +29 -0
- package/src/features/ModelSwitchPanel/hooks/useBuildVirtualItems.ts +122 -0
- package/src/features/ModelSwitchPanel/hooks/useCurrentModelName.ts +18 -0
- package/src/features/ModelSwitchPanel/hooks/useDelayedRender.ts +18 -0
- package/src/features/ModelSwitchPanel/hooks/useModelAndProvider.ts +14 -0
- package/src/features/ModelSwitchPanel/hooks/usePanelHandlers.ts +33 -0
- package/src/features/ModelSwitchPanel/hooks/usePanelSize.ts +33 -0
- package/src/features/ModelSwitchPanel/hooks/usePanelState.ts +20 -0
- package/src/features/ModelSwitchPanel/index.tsx +25 -706
- package/src/features/ModelSwitchPanel/styles.ts +58 -0
- package/src/features/ModelSwitchPanel/types.ts +73 -0
- package/src/features/ModelSwitchPanel/utils.ts +24 -0
- package/src/features/User/UserPanel/PanelContent.tsx +1 -1
- package/src/features/User/__tests__/PanelContent.test.tsx +1 -1
- package/src/features/User/__tests__/UserAvatar.test.tsx +1 -1
- package/src/features/User/__tests__/useMenu.test.tsx +1 -1
- package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -1
- package/src/libs/better-auth/auth-client.ts +7 -3
- package/src/libs/better-auth/define-config.ts +2 -2
- package/src/libs/next/proxy/define-config.ts +9 -6
- package/src/libs/oidc-provider/provider.test.ts +1 -1
- package/src/libs/trpc/async/context.ts +1 -1
- package/src/libs/trpc/lambda/context.ts +7 -8
- package/src/libs/trpc/middleware/userAuth.ts +1 -1
- package/src/libs/trusted-client/getSessionUser.ts +1 -1
- package/src/locales/default/components.ts +1 -0
- package/src/server/globalConfig/index.ts +2 -0
- package/src/server/routers/async/caller.ts +1 -1
- package/src/server/routers/lambda/__tests__/user.test.ts +2 -2
- package/src/server/routers/lambda/user.ts +2 -1
- package/src/services/_auth.ts +3 -3
- package/src/services/chat/index.ts +1 -1
- package/src/services/chat/mecha/contextEngineering.ts +1 -1
- package/src/store/global/initialState.ts +10 -0
- package/src/store/global/selectors/systemStatus.ts +5 -0
- package/src/store/serverConfig/selectors.ts +5 -1
- package/src/store/tool/slices/mcpStore/action.ts +74 -75
- package/src/store/user/slices/auth/action.test.ts +1 -1
- package/src/store/user/slices/auth/action.ts +1 -1
- package/src/store/user/slices/auth/initialState.ts +1 -1
- package/src/store/user/slices/auth/selectors.test.ts +1 -1
- package/src/store/user/slices/auth/selectors.ts +1 -1
- package/src/store/user/slices/common/action.ts +1 -1
- package/src/store/userMemory/slices/context/action.ts +6 -6
- package/packages/const/src/auth.ts +0 -14
- /package/e2e/src/features/{discover → community}/detail-pages.feature +0 -0
- /package/e2e/src/features/{discover → community}/interactions.feature +0 -0
- /package/e2e/src/features/{discover → community}/smoke.feature +0 -0
- /package/e2e/src/mocks/{discover → community}/data.ts +0 -0
- /package/e2e/src/mocks/{discover → community}/handlers.ts +0 -0
- /package/e2e/src/mocks/{discover → community}/index.ts +0 -0
- /package/e2e/src/mocks/{discover → community}/types.ts +0 -0
- /package/e2e/src/steps/{discover → community}/smoke.steps.ts +0 -0
|
@@ -1,246 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { createStaticStyles, cssVar, cx } from 'antd-style';
|
|
5
|
-
import {
|
|
6
|
-
Brain,
|
|
7
|
-
LucideArrowRight,
|
|
8
|
-
LucideBolt,
|
|
9
|
-
LucideCheck,
|
|
10
|
-
LucideChevronRight,
|
|
11
|
-
LucideSettings,
|
|
12
|
-
} from 'lucide-react';
|
|
13
|
-
import { type AiModelForSelect } from 'model-bank';
|
|
14
|
-
import { type ReactNode, memo, useCallback, useEffect, useMemo, useState } from 'react';
|
|
15
|
-
import { useTranslation } from 'react-i18next';
|
|
16
|
-
import { Rnd } from 'react-rnd';
|
|
17
|
-
import { useNavigate } from 'react-router-dom';
|
|
18
|
-
import urlJoin from 'url-join';
|
|
1
|
+
import { TooltipGroup } from '@lobehub/ui';
|
|
2
|
+
import { Popover } from 'antd';
|
|
3
|
+
import { memo, useCallback, useState } from 'react';
|
|
19
4
|
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { agentSelectors } from '@/store/agent/selectors';
|
|
24
|
-
import { type EnabledProviderWithModels } from '@/types/aiProvider';
|
|
25
|
-
|
|
26
|
-
const STORAGE_KEY = 'MODEL_SWITCH_PANEL_WIDTH';
|
|
27
|
-
const STORAGE_KEY_MODE = 'MODEL_SWITCH_PANEL_MODE';
|
|
28
|
-
const DEFAULT_WIDTH = 430;
|
|
29
|
-
const MIN_WIDTH = 280;
|
|
30
|
-
const MAX_WIDTH = 600;
|
|
31
|
-
const MAX_PANEL_HEIGHT = 460;
|
|
32
|
-
const TOOLBAR_HEIGHT = 40;
|
|
33
|
-
const FOOTER_HEIGHT = 48;
|
|
34
|
-
|
|
35
|
-
const INITIAL_RENDER_COUNT = 15;
|
|
36
|
-
const RENDER_ALL_DELAY_MS = 500;
|
|
37
|
-
|
|
38
|
-
const ITEM_HEIGHT = {
|
|
39
|
-
'empty-model': 32,
|
|
40
|
-
'group-header': 32,
|
|
41
|
-
'model-item': 32,
|
|
42
|
-
'no-provider': 32,
|
|
43
|
-
} as const;
|
|
44
|
-
|
|
45
|
-
type GroupMode = 'byModel' | 'byProvider';
|
|
46
|
-
|
|
47
|
-
const ENABLE_RESIZING = {
|
|
48
|
-
bottom: false,
|
|
49
|
-
bottomLeft: false,
|
|
50
|
-
bottomRight: false,
|
|
51
|
-
left: false,
|
|
52
|
-
right: true,
|
|
53
|
-
top: false,
|
|
54
|
-
topLeft: false,
|
|
55
|
-
topRight: false,
|
|
56
|
-
} as const;
|
|
57
|
-
|
|
58
|
-
const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
59
|
-
dropdown: css`
|
|
60
|
-
overflow: hidden;
|
|
61
|
-
|
|
62
|
-
width: 100%;
|
|
63
|
-
height: 100%;
|
|
64
|
-
border: 1px solid ${cssVar.colorBorderSecondary};
|
|
65
|
-
border-radius: ${cssVar.borderRadiusLG};
|
|
66
|
-
|
|
67
|
-
background: ${cssVar.colorBgElevated};
|
|
68
|
-
box-shadow: ${cssVar.boxShadowSecondary};
|
|
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
|
-
`,
|
|
104
|
-
groupHeader: css`
|
|
105
|
-
margin-inline: 8px;
|
|
106
|
-
padding-block: 6px;
|
|
107
|
-
padding-inline: 8px;
|
|
108
|
-
color: ${cssVar.colorTextSecondary};
|
|
109
|
-
`,
|
|
110
|
-
menuItem: css`
|
|
111
|
-
cursor: pointer;
|
|
112
|
-
|
|
113
|
-
display: flex;
|
|
114
|
-
gap: 8px;
|
|
115
|
-
align-items: center;
|
|
116
|
-
|
|
117
|
-
box-sizing: border-box;
|
|
118
|
-
margin-inline: 8px;
|
|
119
|
-
padding-block: 6px;
|
|
120
|
-
padding-inline: 8px;
|
|
121
|
-
border-radius: ${cssVar.borderRadiusSM};
|
|
122
|
-
|
|
123
|
-
white-space: nowrap;
|
|
124
|
-
|
|
125
|
-
&:hover {
|
|
126
|
-
background: ${cssVar.colorFillTertiary};
|
|
127
|
-
|
|
128
|
-
.settings-icon {
|
|
129
|
-
opacity: 1;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
`,
|
|
133
|
-
menuItemActive: css`
|
|
134
|
-
background: ${cssVar.colorFillTertiary};
|
|
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
|
-
`,
|
|
158
|
-
tag: css`
|
|
159
|
-
cursor: pointer;
|
|
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: space-between;
|
|
169
|
-
|
|
170
|
-
padding-block: 6px;
|
|
171
|
-
padding-inline: 8px;
|
|
172
|
-
border-block-end: 1px solid ${cssVar.colorBorderSecondary};
|
|
173
|
-
|
|
174
|
-
background: ${cssVar.colorBgElevated};
|
|
175
|
-
`,
|
|
176
|
-
toolbarModelName: css`
|
|
177
|
-
overflow: hidden;
|
|
178
|
-
|
|
179
|
-
font-size: 12px;
|
|
180
|
-
color: ${cssVar.colorTextSecondary};
|
|
181
|
-
text-overflow: ellipsis;
|
|
182
|
-
white-space: nowrap;
|
|
183
|
-
`,
|
|
184
|
-
}));
|
|
185
|
-
|
|
186
|
-
const menuKey = (provider: string, model: string) => `${provider}-${model}`;
|
|
187
|
-
|
|
188
|
-
interface ModelWithProviders {
|
|
189
|
-
displayName: string;
|
|
190
|
-
model: AiModelForSelect;
|
|
191
|
-
providers: Array<{
|
|
192
|
-
id: string;
|
|
193
|
-
logo?: string;
|
|
194
|
-
name: string;
|
|
195
|
-
source?: EnabledProviderWithModels['source'];
|
|
196
|
-
}>;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
type VirtualItem =
|
|
200
|
-
| {
|
|
201
|
-
data: ModelWithProviders;
|
|
202
|
-
type: 'model-item';
|
|
203
|
-
}
|
|
204
|
-
| {
|
|
205
|
-
provider: EnabledProviderWithModels;
|
|
206
|
-
type: 'group-header';
|
|
207
|
-
}
|
|
208
|
-
| {
|
|
209
|
-
model: AiModelForSelect;
|
|
210
|
-
provider: EnabledProviderWithModels;
|
|
211
|
-
type: 'provider-model-item';
|
|
212
|
-
}
|
|
213
|
-
| {
|
|
214
|
-
provider: EnabledProviderWithModels;
|
|
215
|
-
type: 'empty-model';
|
|
216
|
-
}
|
|
217
|
-
| {
|
|
218
|
-
type: 'no-provider';
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
type DropdownPlacement = 'bottom' | 'bottomLeft' | 'bottomRight' | 'top' | 'topLeft' | 'topRight';
|
|
222
|
-
|
|
223
|
-
interface ModelSwitchPanelProps {
|
|
224
|
-
children?: ReactNode;
|
|
225
|
-
/**
|
|
226
|
-
* Current model ID. If not provided, uses currentAgentModel from store.
|
|
227
|
-
*/
|
|
228
|
-
model?: string;
|
|
229
|
-
/**
|
|
230
|
-
* Callback when model changes. If not provided, uses updateAgentConfig from store.
|
|
231
|
-
*/
|
|
232
|
-
onModelChange?: (params: { model: string; provider: string }) => Promise<void>;
|
|
233
|
-
onOpenChange?: (open: boolean) => void;
|
|
234
|
-
open?: boolean;
|
|
235
|
-
/**
|
|
236
|
-
* Dropdown placement. Defaults to 'topLeft'.
|
|
237
|
-
*/
|
|
238
|
-
placement?: DropdownPlacement;
|
|
239
|
-
/**
|
|
240
|
-
* Current provider ID. If not provided, uses currentAgentModelProvider from store.
|
|
241
|
-
*/
|
|
242
|
-
provider?: string;
|
|
243
|
-
}
|
|
5
|
+
import { PanelContent } from './components/PanelContent';
|
|
6
|
+
import { styles } from './styles';
|
|
7
|
+
import type { ModelSwitchPanelProps } from './types';
|
|
244
8
|
|
|
245
9
|
const ModelSwitchPanel = memo<ModelSwitchPanelProps>(
|
|
246
10
|
({
|
|
@@ -252,38 +16,11 @@ const ModelSwitchPanel = memo<ModelSwitchPanelProps>(
|
|
|
252
16
|
placement = 'topLeft',
|
|
253
17
|
provider: providerProp,
|
|
254
18
|
}) => {
|
|
255
|
-
const { t } = useTranslation('components');
|
|
256
|
-
const { t: tCommon } = useTranslation('common');
|
|
257
|
-
const newLabel = tCommon('new');
|
|
258
|
-
|
|
259
|
-
const [panelWidth, setPanelWidth] = useState(() => {
|
|
260
|
-
if (typeof window === 'undefined') return DEFAULT_WIDTH;
|
|
261
|
-
const stored = localStorage.getItem(STORAGE_KEY);
|
|
262
|
-
return stored ? Number(stored) : DEFAULT_WIDTH;
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
const [groupMode, setGroupMode] = useState<GroupMode>(() => {
|
|
266
|
-
if (typeof window === 'undefined') return 'byModel';
|
|
267
|
-
const stored = localStorage.getItem(STORAGE_KEY_MODE);
|
|
268
|
-
return (stored as GroupMode) || 'byModel';
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
const [renderAll, setRenderAll] = useState(false);
|
|
272
19
|
const [internalOpen, setInternalOpen] = useState(false);
|
|
273
20
|
|
|
274
21
|
// Use controlled open if provided, otherwise use internal state
|
|
275
22
|
const isOpen = open ?? internalOpen;
|
|
276
23
|
|
|
277
|
-
// Only delay render all items on first open, then keep cached
|
|
278
|
-
useEffect(() => {
|
|
279
|
-
if (isOpen && !renderAll) {
|
|
280
|
-
const timer = setTimeout(() => {
|
|
281
|
-
setRenderAll(true);
|
|
282
|
-
}, RENDER_ALL_DELAY_MS);
|
|
283
|
-
return () => clearTimeout(timer);
|
|
284
|
-
}
|
|
285
|
-
}, [isOpen, renderAll]);
|
|
286
|
-
|
|
287
24
|
const handleOpenChange = useCallback(
|
|
288
25
|
(nextOpen: boolean) => {
|
|
289
26
|
setInternalOpen(nextOpen);
|
|
@@ -292,453 +29,35 @@ const ModelSwitchPanel = memo<ModelSwitchPanelProps>(
|
|
|
292
29
|
[onOpenChange],
|
|
293
30
|
);
|
|
294
31
|
|
|
295
|
-
// Get values from store for fallback when props are not provided
|
|
296
|
-
const [storeModel, storeProvider, updateAgentConfig] = useAgentStore((s) => [
|
|
297
|
-
agentSelectors.currentAgentModel(s),
|
|
298
|
-
agentSelectors.currentAgentModelProvider(s),
|
|
299
|
-
s.updateAgentConfig,
|
|
300
|
-
]);
|
|
301
|
-
|
|
302
|
-
// Use props if provided, otherwise fallback to store values
|
|
303
|
-
const model = modelProp ?? storeModel;
|
|
304
|
-
const provider = providerProp ?? storeProvider;
|
|
305
|
-
|
|
306
|
-
const navigate = useNavigate();
|
|
307
|
-
const enabledList = useEnabledChatModels();
|
|
308
|
-
|
|
309
|
-
const handleModelChange = useCallback(
|
|
310
|
-
async (modelId: string, providerId: string) => {
|
|
311
|
-
const params = { model: modelId, provider: providerId };
|
|
312
|
-
if (onModelChange) {
|
|
313
|
-
await onModelChange(params);
|
|
314
|
-
} else {
|
|
315
|
-
await updateAgentConfig(params);
|
|
316
|
-
}
|
|
317
|
-
},
|
|
318
|
-
[onModelChange, updateAgentConfig],
|
|
319
|
-
);
|
|
320
|
-
|
|
321
|
-
// Build virtual items based on group mode
|
|
322
|
-
const virtualItems = useMemo(() => {
|
|
323
|
-
if (enabledList.length === 0) {
|
|
324
|
-
return [{ type: 'no-provider' }] as VirtualItem[];
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (groupMode === 'byModel') {
|
|
328
|
-
// Group models by display name
|
|
329
|
-
const modelMap = new Map<string, ModelWithProviders>();
|
|
330
|
-
|
|
331
|
-
for (const providerItem of enabledList) {
|
|
332
|
-
for (const modelItem of providerItem.children) {
|
|
333
|
-
const displayName = modelItem.displayName || modelItem.id;
|
|
334
|
-
|
|
335
|
-
if (!modelMap.has(displayName)) {
|
|
336
|
-
modelMap.set(displayName, {
|
|
337
|
-
displayName,
|
|
338
|
-
model: modelItem,
|
|
339
|
-
providers: [],
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const entry = modelMap.get(displayName)!;
|
|
344
|
-
entry.providers.push({
|
|
345
|
-
id: providerItem.id,
|
|
346
|
-
logo: providerItem.logo,
|
|
347
|
-
name: providerItem.name,
|
|
348
|
-
source: providerItem.source,
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Convert to array and sort by display name
|
|
354
|
-
return Array.from(modelMap.values())
|
|
355
|
-
.sort((a, b) => a.displayName.localeCompare(b.displayName))
|
|
356
|
-
.map((data) => ({ data, type: 'model-item' as const }));
|
|
357
|
-
} else {
|
|
358
|
-
// Group by provider (original structure)
|
|
359
|
-
const items: VirtualItem[] = [];
|
|
360
|
-
|
|
361
|
-
for (const providerItem of enabledList) {
|
|
362
|
-
// Add provider group header
|
|
363
|
-
items.push({ provider: providerItem, type: 'group-header' });
|
|
364
|
-
|
|
365
|
-
if (providerItem.children.length === 0) {
|
|
366
|
-
// Add empty model placeholder
|
|
367
|
-
items.push({ provider: providerItem, type: 'empty-model' });
|
|
368
|
-
} else {
|
|
369
|
-
// Add each model item
|
|
370
|
-
for (const modelItem of providerItem.children) {
|
|
371
|
-
items.push({
|
|
372
|
-
model: modelItem,
|
|
373
|
-
provider: providerItem,
|
|
374
|
-
type: 'provider-model-item',
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return items;
|
|
381
|
-
}
|
|
382
|
-
}, [enabledList, groupMode]);
|
|
383
|
-
|
|
384
|
-
// Use a fixed panel height to prevent shifting when switching modes
|
|
385
|
-
const panelHeight =
|
|
386
|
-
enabledList.length === 0
|
|
387
|
-
? TOOLBAR_HEIGHT + ITEM_HEIGHT['no-provider'] + FOOTER_HEIGHT
|
|
388
|
-
: MAX_PANEL_HEIGHT;
|
|
389
|
-
|
|
390
|
-
const activeKey = menuKey(provider, model);
|
|
391
|
-
|
|
392
|
-
// Find current model's display name
|
|
393
|
-
const currentModelName = useMemo(() => {
|
|
394
|
-
for (const providerItem of enabledList) {
|
|
395
|
-
const modelItem = providerItem.children.find((m) => m.id === model);
|
|
396
|
-
if (modelItem) {
|
|
397
|
-
return modelItem.displayName || modelItem.id;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
return model;
|
|
401
|
-
}, [enabledList, model]);
|
|
402
|
-
|
|
403
|
-
const renderVirtualItem = useCallback(
|
|
404
|
-
(item: VirtualItem) => {
|
|
405
|
-
switch (item.type) {
|
|
406
|
-
case 'no-provider': {
|
|
407
|
-
return (
|
|
408
|
-
<div
|
|
409
|
-
className={styles.menuItem}
|
|
410
|
-
key="no-provider"
|
|
411
|
-
onClick={() => navigate('/settings/provider/all')}
|
|
412
|
-
>
|
|
413
|
-
<Flexbox gap={8} horizontal style={{ color: cssVar.colorTextTertiary }}>
|
|
414
|
-
{t('ModelSwitchPanel.emptyProvider')}
|
|
415
|
-
<Icon icon={LucideArrowRight} />
|
|
416
|
-
</Flexbox>
|
|
417
|
-
</div>
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
case 'group-header': {
|
|
422
|
-
return (
|
|
423
|
-
<div className={styles.groupHeader} key={`header-${item.provider.id}`}>
|
|
424
|
-
<Flexbox horizontal justify="space-between">
|
|
425
|
-
<ProviderItemRender
|
|
426
|
-
logo={item.provider.logo}
|
|
427
|
-
name={item.provider.name}
|
|
428
|
-
provider={item.provider.id}
|
|
429
|
-
source={item.provider.source}
|
|
430
|
-
/>
|
|
431
|
-
<ActionIcon
|
|
432
|
-
icon={LucideBolt}
|
|
433
|
-
onClick={(e) => {
|
|
434
|
-
e.preventDefault();
|
|
435
|
-
e.stopPropagation();
|
|
436
|
-
const url = urlJoin('/settings/provider', item.provider.id || 'all');
|
|
437
|
-
if (e.ctrlKey || e.metaKey) {
|
|
438
|
-
window.open(url, '_blank');
|
|
439
|
-
} else {
|
|
440
|
-
navigate(url);
|
|
441
|
-
}
|
|
442
|
-
}}
|
|
443
|
-
size={'small'}
|
|
444
|
-
title={t('ModelSwitchPanel.goToSettings')}
|
|
445
|
-
/>
|
|
446
|
-
</Flexbox>
|
|
447
|
-
</div>
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
case 'empty-model': {
|
|
452
|
-
return (
|
|
453
|
-
<div
|
|
454
|
-
className={styles.menuItem}
|
|
455
|
-
key={`empty-${item.provider.id}`}
|
|
456
|
-
onClick={() => navigate(`/settings/provider/${item.provider.id}`)}
|
|
457
|
-
>
|
|
458
|
-
<Flexbox gap={8} horizontal style={{ color: cssVar.colorTextTertiary }}>
|
|
459
|
-
{t('ModelSwitchPanel.emptyModel')}
|
|
460
|
-
<Icon icon={LucideArrowRight} />
|
|
461
|
-
</Flexbox>
|
|
462
|
-
</div>
|
|
463
|
-
);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
case 'provider-model-item': {
|
|
467
|
-
const key = menuKey(item.provider.id, item.model.id);
|
|
468
|
-
const isActive = key === activeKey;
|
|
469
|
-
|
|
470
|
-
return (
|
|
471
|
-
<div
|
|
472
|
-
className={cx(styles.menuItem, isActive && styles.menuItemActive)}
|
|
473
|
-
key={key}
|
|
474
|
-
onClick={async () => {
|
|
475
|
-
await handleModelChange(item.model.id, item.provider.id);
|
|
476
|
-
handleOpenChange(false);
|
|
477
|
-
}}
|
|
478
|
-
>
|
|
479
|
-
<ModelItemRender
|
|
480
|
-
{...item.model}
|
|
481
|
-
{...item.model.abilities}
|
|
482
|
-
infoTagTooltip={false}
|
|
483
|
-
newBadgeLabel={newLabel}
|
|
484
|
-
showInfoTag
|
|
485
|
-
/>
|
|
486
|
-
</div>
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
case 'model-item': {
|
|
491
|
-
const { data } = item;
|
|
492
|
-
const hasSingleProvider = data.providers.length === 1;
|
|
493
|
-
|
|
494
|
-
// Check if this model is currently active and find active provider
|
|
495
|
-
const activeProvider = data.providers.find(
|
|
496
|
-
(p) => menuKey(p.id, data.model.id) === activeKey,
|
|
497
|
-
);
|
|
498
|
-
const isActive = !!activeProvider;
|
|
499
|
-
// Use active provider if found, otherwise use first provider for settings link
|
|
500
|
-
const settingsProvider = activeProvider || data.providers[0];
|
|
501
|
-
|
|
502
|
-
// Single provider - direct click without submenu
|
|
503
|
-
if (hasSingleProvider) {
|
|
504
|
-
const singleProvider = data.providers[0];
|
|
505
|
-
const key = menuKey(singleProvider.id, data.model.id);
|
|
506
|
-
|
|
507
|
-
return (
|
|
508
|
-
<div className={cx(styles.menuItem, isActive && styles.menuItemActive)} key={key}>
|
|
509
|
-
<Flexbox
|
|
510
|
-
align={'center'}
|
|
511
|
-
gap={8}
|
|
512
|
-
horizontal
|
|
513
|
-
justify={'space-between'}
|
|
514
|
-
onClick={async () => {
|
|
515
|
-
await handleModelChange(data.model.id, singleProvider.id);
|
|
516
|
-
handleOpenChange(false);
|
|
517
|
-
}}
|
|
518
|
-
style={{ flex: 1, minWidth: 0 }}
|
|
519
|
-
>
|
|
520
|
-
<ModelItemRender
|
|
521
|
-
{...data.model}
|
|
522
|
-
{...data.model.abilities}
|
|
523
|
-
infoTagTooltip={false}
|
|
524
|
-
newBadgeLabel={newLabel}
|
|
525
|
-
showInfoTag={false}
|
|
526
|
-
/>
|
|
527
|
-
</Flexbox>
|
|
528
|
-
<div className={cx(styles.settingsIcon, 'settings-icon')}>
|
|
529
|
-
<ActionIcon
|
|
530
|
-
icon={LucideBolt}
|
|
531
|
-
onClick={(e) => {
|
|
532
|
-
e.preventDefault();
|
|
533
|
-
e.stopPropagation();
|
|
534
|
-
const url = urlJoin('/settings/provider', settingsProvider.id || 'all');
|
|
535
|
-
if (e.ctrlKey || e.metaKey) {
|
|
536
|
-
window.open(url, '_blank');
|
|
537
|
-
} else {
|
|
538
|
-
navigate(url);
|
|
539
|
-
}
|
|
540
|
-
}}
|
|
541
|
-
size={'small'}
|
|
542
|
-
title={t('ModelSwitchPanel.goToSettings')}
|
|
543
|
-
/>
|
|
544
|
-
</div>
|
|
545
|
-
</div>
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Multiple providers - show submenu on hover
|
|
550
|
-
return (
|
|
551
|
-
<Dropdown
|
|
552
|
-
align={{ offset: [4, 0] }}
|
|
553
|
-
arrow={false}
|
|
554
|
-
dropdownRender={(menu) => (
|
|
555
|
-
<div className={styles.submenu} style={{ minWidth: 240 }}>
|
|
556
|
-
{menu}
|
|
557
|
-
</div>
|
|
558
|
-
)}
|
|
559
|
-
key={data.displayName}
|
|
560
|
-
menu={{
|
|
561
|
-
items: [
|
|
562
|
-
{
|
|
563
|
-
key: 'header',
|
|
564
|
-
label: t('ModelSwitchPanel.useModelFrom'),
|
|
565
|
-
type: 'group',
|
|
566
|
-
},
|
|
567
|
-
...data.providers.map((p) => {
|
|
568
|
-
const isCurrentProvider = menuKey(p.id, data.model.id) === activeKey;
|
|
569
|
-
return {
|
|
570
|
-
key: menuKey(p.id, data.model.id),
|
|
571
|
-
label: (
|
|
572
|
-
<Flexbox
|
|
573
|
-
align={'center'}
|
|
574
|
-
gap={8}
|
|
575
|
-
horizontal
|
|
576
|
-
justify={'space-between'}
|
|
577
|
-
style={{ minWidth: 0 }}
|
|
578
|
-
>
|
|
579
|
-
<Flexbox align={'center'} gap={8} horizontal style={{ minWidth: 0 }}>
|
|
580
|
-
<div style={{ flexShrink: 0, width: 16 }}>
|
|
581
|
-
{isCurrentProvider && (
|
|
582
|
-
<Icon
|
|
583
|
-
icon={LucideCheck}
|
|
584
|
-
size={16}
|
|
585
|
-
style={{ color: cssVar.colorPrimary }}
|
|
586
|
-
/>
|
|
587
|
-
)}
|
|
588
|
-
</div>
|
|
589
|
-
<ProviderItemRender
|
|
590
|
-
logo={p.logo}
|
|
591
|
-
name={p.name}
|
|
592
|
-
provider={p.id}
|
|
593
|
-
source={p.source}
|
|
594
|
-
/>
|
|
595
|
-
</Flexbox>
|
|
596
|
-
<ActionIcon
|
|
597
|
-
icon={LucideBolt}
|
|
598
|
-
onClick={(e) => {
|
|
599
|
-
e.preventDefault();
|
|
600
|
-
e.stopPropagation();
|
|
601
|
-
const url = urlJoin('/settings/provider', p.id || 'all');
|
|
602
|
-
if (e.ctrlKey || e.metaKey) {
|
|
603
|
-
window.open(url, '_blank');
|
|
604
|
-
} else {
|
|
605
|
-
navigate(url);
|
|
606
|
-
}
|
|
607
|
-
}}
|
|
608
|
-
size={'small'}
|
|
609
|
-
title={t('ModelSwitchPanel.goToSettings')}
|
|
610
|
-
/>
|
|
611
|
-
</Flexbox>
|
|
612
|
-
),
|
|
613
|
-
onClick: async () => {
|
|
614
|
-
await handleModelChange(data.model.id, p.id);
|
|
615
|
-
handleOpenChange(false);
|
|
616
|
-
},
|
|
617
|
-
};
|
|
618
|
-
}),
|
|
619
|
-
],
|
|
620
|
-
}}
|
|
621
|
-
// @ts-ignore
|
|
622
|
-
placement="rightTop"
|
|
623
|
-
trigger={['hover']}
|
|
624
|
-
>
|
|
625
|
-
<div className={cx(styles.menuItem, isActive && styles.menuItemActive)}>
|
|
626
|
-
<Flexbox
|
|
627
|
-
align={'center'}
|
|
628
|
-
gap={8}
|
|
629
|
-
horizontal
|
|
630
|
-
justify={'space-between'}
|
|
631
|
-
style={{ width: '100%' }}
|
|
632
|
-
>
|
|
633
|
-
<ModelItemRender
|
|
634
|
-
{...data.model}
|
|
635
|
-
{...data.model.abilities}
|
|
636
|
-
infoTagTooltip={false}
|
|
637
|
-
newBadgeLabel={newLabel}
|
|
638
|
-
showInfoTag={false}
|
|
639
|
-
/>
|
|
640
|
-
<Icon
|
|
641
|
-
icon={LucideChevronRight}
|
|
642
|
-
size={16}
|
|
643
|
-
style={{ color: cssVar.colorTextSecondary, flexShrink: 0 }}
|
|
644
|
-
/>
|
|
645
|
-
</Flexbox>
|
|
646
|
-
</div>
|
|
647
|
-
</Dropdown>
|
|
648
|
-
);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
default: {
|
|
652
|
-
return null;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
},
|
|
656
|
-
[activeKey, cx, handleModelChange, handleOpenChange, navigate, newLabel, styles, t],
|
|
657
|
-
);
|
|
658
|
-
|
|
659
32
|
return (
|
|
660
33
|
<TooltipGroup>
|
|
661
|
-
<
|
|
34
|
+
<Popover
|
|
662
35
|
arrow={false}
|
|
36
|
+
classNames={{
|
|
37
|
+
container: styles.container,
|
|
38
|
+
}}
|
|
39
|
+
content={
|
|
40
|
+
<PanelContent
|
|
41
|
+
isOpen={isOpen}
|
|
42
|
+
model={modelProp}
|
|
43
|
+
onModelChange={onModelChange}
|
|
44
|
+
onOpenChange={handleOpenChange}
|
|
45
|
+
provider={providerProp}
|
|
46
|
+
/>
|
|
47
|
+
}
|
|
663
48
|
onOpenChange={handleOpenChange}
|
|
664
49
|
open={isOpen}
|
|
665
50
|
placement={placement}
|
|
666
|
-
popupRender={() => (
|
|
667
|
-
<Rnd
|
|
668
|
-
className={styles.dropdown}
|
|
669
|
-
disableDragging
|
|
670
|
-
enableResizing={ENABLE_RESIZING}
|
|
671
|
-
maxWidth={MAX_WIDTH}
|
|
672
|
-
minWidth={MIN_WIDTH}
|
|
673
|
-
onResizeStop={(_e, _direction, ref) => {
|
|
674
|
-
const newWidth = ref.offsetWidth;
|
|
675
|
-
setPanelWidth(newWidth);
|
|
676
|
-
localStorage.setItem(STORAGE_KEY, String(newWidth));
|
|
677
|
-
}}
|
|
678
|
-
position={{ x: 0, y: 0 }}
|
|
679
|
-
size={{ height: panelHeight, width: panelWidth }}
|
|
680
|
-
style={{ position: 'relative' }}
|
|
681
|
-
>
|
|
682
|
-
<div className={styles.toolbar}>
|
|
683
|
-
<div className={styles.toolbarModelName}>{currentModelName}</div>
|
|
684
|
-
<Segmented
|
|
685
|
-
onChange={(value) => {
|
|
686
|
-
const mode = value as GroupMode;
|
|
687
|
-
setGroupMode(mode);
|
|
688
|
-
localStorage.setItem(STORAGE_KEY_MODE, mode);
|
|
689
|
-
}}
|
|
690
|
-
options={[
|
|
691
|
-
{
|
|
692
|
-
icon: <Icon icon={Brain} />,
|
|
693
|
-
title: t('ModelSwitchPanel.byModel'),
|
|
694
|
-
value: 'byModel',
|
|
695
|
-
},
|
|
696
|
-
{
|
|
697
|
-
icon: <Icon icon={ProviderIcon} />,
|
|
698
|
-
title: t('ModelSwitchPanel.byProvider'),
|
|
699
|
-
value: 'byProvider',
|
|
700
|
-
},
|
|
701
|
-
]}
|
|
702
|
-
size="small"
|
|
703
|
-
value={groupMode}
|
|
704
|
-
/>
|
|
705
|
-
</div>
|
|
706
|
-
<div
|
|
707
|
-
style={{
|
|
708
|
-
height: panelHeight - TOOLBAR_HEIGHT - FOOTER_HEIGHT,
|
|
709
|
-
overflow: 'auto',
|
|
710
|
-
paddingBlock: groupMode === 'byModel' ? 8 : 0,
|
|
711
|
-
width: '100%',
|
|
712
|
-
}}
|
|
713
|
-
>
|
|
714
|
-
{(renderAll ? virtualItems : virtualItems.slice(0, INITIAL_RENDER_COUNT)).map(
|
|
715
|
-
renderVirtualItem,
|
|
716
|
-
)}
|
|
717
|
-
</div>
|
|
718
|
-
<div className={styles.footer}>
|
|
719
|
-
<div
|
|
720
|
-
className={styles.footerButton}
|
|
721
|
-
onClick={() => {
|
|
722
|
-
navigate('/settings/provider/all');
|
|
723
|
-
handleOpenChange(false);
|
|
724
|
-
}}
|
|
725
|
-
>
|
|
726
|
-
<Flexbox align={'center'} gap={8} horizontal style={{ flex: 1 }}>
|
|
727
|
-
<Icon icon={LucideSettings} size={16} />
|
|
728
|
-
{t('ModelSwitchPanel.manageProvider')}
|
|
729
|
-
</Flexbox>
|
|
730
|
-
<Icon icon={LucideArrowRight} size={16} />
|
|
731
|
-
</div>
|
|
732
|
-
</div>
|
|
733
|
-
</Rnd>
|
|
734
|
-
)}
|
|
735
51
|
>
|
|
736
|
-
|
|
737
|
-
</
|
|
52
|
+
{children}
|
|
53
|
+
</Popover>
|
|
738
54
|
</TooltipGroup>
|
|
739
55
|
);
|
|
740
56
|
},
|
|
741
57
|
);
|
|
742
58
|
|
|
59
|
+
ModelSwitchPanel.displayName = 'ModelSwitchPanel';
|
|
60
|
+
|
|
743
61
|
export default ModelSwitchPanel;
|
|
744
|
-
|
|
62
|
+
|
|
63
|
+
export { type ModelSwitchPanelProps } from './types';
|