@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.
Files changed (130) hide show
  1. package/.github/workflows/claude-auto-testing.yml +6 -3
  2. package/.github/workflows/claude-dedupe-issues.yml +8 -1
  3. package/.github/workflows/claude-issue-triage.yml +8 -14
  4. package/.github/workflows/claude-translate-comments.yml +6 -3
  5. package/.github/workflows/claude-translator.yml +12 -14
  6. package/.github/workflows/claude.yml +10 -20
  7. package/.github/workflows/test.yml +26 -0
  8. package/CHANGELOG.md +58 -0
  9. package/changelog/v1.json +18 -0
  10. package/e2e/package.json +1 -1
  11. package/e2e/src/mocks/index.ts +2 -2
  12. package/e2e/src/steps/{discover → community}/detail-pages.steps.ts +8 -8
  13. package/e2e/src/steps/{discover → community}/interactions.steps.ts +4 -4
  14. package/locales/zh-CN/components.json +1 -0
  15. package/package.json +3 -3
  16. package/packages/const/src/index.ts +0 -1
  17. package/packages/memory-user-memory/package.json +1 -0
  18. package/packages/memory-user-memory/src/extractors/context.test.ts +3 -2
  19. package/packages/memory-user-memory/src/extractors/experience.test.ts +3 -2
  20. package/packages/memory-user-memory/src/extractors/identity.test.ts +23 -6
  21. package/packages/memory-user-memory/src/extractors/preference.test.ts +3 -2
  22. package/packages/memory-user-memory/vitest.config.ts +4 -0
  23. package/packages/model-runtime/src/providers/replicate/index.ts +1 -1
  24. package/packages/ssrf-safe-fetch/index.test.ts +2 -2
  25. package/packages/ssrf-safe-fetch/package.json +3 -2
  26. package/packages/types/src/serverConfig.ts +2 -0
  27. package/packages/utils/package.json +1 -1
  28. package/packages/utils/src/client/xor-obfuscation.test.ts +32 -32
  29. package/packages/utils/src/client/xor-obfuscation.ts +3 -4
  30. package/packages/utils/src/imageToBase64.ts +1 -1
  31. package/packages/utils/src/server/__tests__/auth.test.ts +1 -1
  32. package/packages/utils/src/server/auth.ts +1 -1
  33. package/packages/utils/src/server/correctOIDCUrl.test.ts +80 -19
  34. package/packages/utils/src/server/correctOIDCUrl.ts +89 -24
  35. package/packages/utils/src/server/index.ts +1 -0
  36. package/packages/utils/src/server/xor.test.ts +9 -7
  37. package/packages/utils/src/server/xor.ts +1 -1
  38. package/packages/web-crawler/package.json +1 -1
  39. package/packages/web-crawler/src/crawImpl/__tests__/naive.test.ts +1 -1
  40. package/packages/web-crawler/src/crawImpl/naive.ts +1 -1
  41. package/scripts/prebuild.mts +58 -1
  42. package/src/app/(backend)/api/auth/[...all]/route.ts +2 -1
  43. package/src/app/(backend)/middleware/auth/index.ts +3 -3
  44. package/src/app/(backend)/middleware/auth/utils.test.ts +1 -1
  45. package/src/app/(backend)/middleware/auth/utils.ts +1 -1
  46. package/src/app/(backend)/oidc/callback/desktop/route.ts +7 -36
  47. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +2 -2
  48. package/src/app/(backend)/webapi/models/[provider]/route.test.ts +1 -1
  49. package/src/app/(backend)/webapi/plugin/gateway/route.ts +1 -1
  50. package/src/app/(backend)/webapi/proxy/route.ts +1 -1
  51. package/src/app/[variants]/(auth)/login/[[...login]]/page.tsx +1 -1
  52. package/src/app/[variants]/(auth)/reset-password/layout.tsx +1 -1
  53. package/src/app/[variants]/(auth)/signin/layout.tsx +1 -1
  54. package/src/app/[variants]/(auth)/signin/useSignIn.ts +2 -2
  55. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +1 -1
  56. package/src/app/[variants]/(auth)/signup/[[...signup]]/useSignUp.tsx +12 -6
  57. package/src/app/[variants]/(auth)/verify-email/layout.tsx +1 -1
  58. package/src/app/[variants]/(main)/settings/profile/features/AvatarRow.tsx +1 -1
  59. package/src/app/[variants]/(main)/settings/security/index.tsx +1 -1
  60. package/src/app/[variants]/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +1 -1
  61. package/src/app/[variants]/(mobile)/me/(home)/__tests__/useCategory.test.tsx +1 -1
  62. package/src/app/[variants]/(mobile)/me/(home)/features/UserBanner.tsx +1 -1
  63. package/src/app/[variants]/(mobile)/settings/_layout/Header.tsx +1 -1
  64. package/src/components/ModelSelect/index.tsx +103 -72
  65. package/src/envs/auth.ts +30 -9
  66. package/src/features/Conversation/Messages/AssistantGroup/components/EditState.tsx +15 -32
  67. package/src/features/Conversation/Messages/AssistantGroup/index.tsx +9 -7
  68. package/src/features/EditorModal/EditorCanvas.tsx +31 -50
  69. package/src/features/EditorModal/TextareCanvas.tsx +3 -1
  70. package/src/features/EditorModal/index.tsx +14 -4
  71. package/src/features/ModelSwitchPanel/components/Footer.tsx +42 -0
  72. package/src/features/ModelSwitchPanel/components/List/MultipleProvidersModelItem.tsx +103 -0
  73. package/src/features/ModelSwitchPanel/components/List/SingleProviderModelItem.tsx +24 -0
  74. package/src/features/ModelSwitchPanel/components/List/VirtualItemRenderer.tsx +180 -0
  75. package/src/features/ModelSwitchPanel/components/List/index.tsx +99 -0
  76. package/src/features/ModelSwitchPanel/components/PanelContent.tsx +77 -0
  77. package/src/features/ModelSwitchPanel/components/Toolbar.tsx +54 -0
  78. package/src/features/ModelSwitchPanel/const.ts +29 -0
  79. package/src/features/ModelSwitchPanel/hooks/useBuildVirtualItems.ts +122 -0
  80. package/src/features/ModelSwitchPanel/hooks/useCurrentModelName.ts +18 -0
  81. package/src/features/ModelSwitchPanel/hooks/useDelayedRender.ts +18 -0
  82. package/src/features/ModelSwitchPanel/hooks/useModelAndProvider.ts +14 -0
  83. package/src/features/ModelSwitchPanel/hooks/usePanelHandlers.ts +33 -0
  84. package/src/features/ModelSwitchPanel/hooks/usePanelSize.ts +33 -0
  85. package/src/features/ModelSwitchPanel/hooks/usePanelState.ts +20 -0
  86. package/src/features/ModelSwitchPanel/index.tsx +25 -706
  87. package/src/features/ModelSwitchPanel/styles.ts +58 -0
  88. package/src/features/ModelSwitchPanel/types.ts +73 -0
  89. package/src/features/ModelSwitchPanel/utils.ts +24 -0
  90. package/src/features/User/UserPanel/PanelContent.tsx +1 -1
  91. package/src/features/User/__tests__/PanelContent.test.tsx +1 -1
  92. package/src/features/User/__tests__/UserAvatar.test.tsx +1 -1
  93. package/src/features/User/__tests__/useMenu.test.tsx +1 -1
  94. package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -1
  95. package/src/libs/better-auth/auth-client.ts +7 -3
  96. package/src/libs/better-auth/define-config.ts +2 -2
  97. package/src/libs/next/proxy/define-config.ts +9 -6
  98. package/src/libs/oidc-provider/provider.test.ts +1 -1
  99. package/src/libs/trpc/async/context.ts +1 -1
  100. package/src/libs/trpc/lambda/context.ts +7 -8
  101. package/src/libs/trpc/middleware/userAuth.ts +1 -1
  102. package/src/libs/trusted-client/getSessionUser.ts +1 -1
  103. package/src/locales/default/components.ts +1 -0
  104. package/src/server/globalConfig/index.ts +2 -0
  105. package/src/server/routers/async/caller.ts +1 -1
  106. package/src/server/routers/lambda/__tests__/user.test.ts +2 -2
  107. package/src/server/routers/lambda/user.ts +2 -1
  108. package/src/services/_auth.ts +3 -3
  109. package/src/services/chat/index.ts +1 -1
  110. package/src/services/chat/mecha/contextEngineering.ts +1 -1
  111. package/src/store/global/initialState.ts +10 -0
  112. package/src/store/global/selectors/systemStatus.ts +5 -0
  113. package/src/store/serverConfig/selectors.ts +5 -1
  114. package/src/store/tool/slices/mcpStore/action.ts +74 -75
  115. package/src/store/user/slices/auth/action.test.ts +1 -1
  116. package/src/store/user/slices/auth/action.ts +1 -1
  117. package/src/store/user/slices/auth/initialState.ts +1 -1
  118. package/src/store/user/slices/auth/selectors.test.ts +1 -1
  119. package/src/store/user/slices/auth/selectors.ts +1 -1
  120. package/src/store/user/slices/common/action.ts +1 -1
  121. package/src/store/userMemory/slices/context/action.ts +6 -6
  122. package/packages/const/src/auth.ts +0 -14
  123. /package/e2e/src/features/{discover → community}/detail-pages.feature +0 -0
  124. /package/e2e/src/features/{discover → community}/interactions.feature +0 -0
  125. /package/e2e/src/features/{discover → community}/smoke.feature +0 -0
  126. /package/e2e/src/mocks/{discover → community}/data.ts +0 -0
  127. /package/e2e/src/mocks/{discover → community}/handlers.ts +0 -0
  128. /package/e2e/src/mocks/{discover → community}/index.ts +0 -0
  129. /package/e2e/src/mocks/{discover → community}/types.ts +0 -0
  130. /package/e2e/src/steps/{discover → community}/smoke.steps.ts +0 -0
@@ -1,246 +1,10 @@
1
- import { ActionIcon, Flexbox, Icon, Segmented, TooltipGroup } from '@lobehub/ui';
2
- import { ProviderIcon } from '@lobehub/ui/icons';
3
- import { Dropdown } from 'antd';
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 { ModelItemRender, ProviderItemRender } from '@/components/ModelSelect';
21
- import { useEnabledChatModels } from '@/hooks/useEnabledChatModels';
22
- import { useAgentStore } from '@/store/agent';
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
- <Dropdown
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
- <div className={styles.tag}>{children}</div>
737
- </Dropdown>
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
- export type { ModelSwitchPanelProps };
62
+
63
+ export { type ModelSwitchPanelProps } from './types';