@lobehub/lobehub 2.0.0-next.324 → 2.0.0-next.326

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 (69) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/CLAUDE.md +4 -0
  3. package/apps/desktop/src/main/core/browser/Browser.ts +40 -1
  4. package/apps/desktop/src/main/core/infrastructure/I18nManager.ts +0 -11
  5. package/apps/desktop/src/main/core/infrastructure/UpdaterManager.ts +52 -2
  6. package/apps/desktop/src/main/core/infrastructure/__tests__/UpdaterManager.test.ts +41 -0
  7. package/changelog/v1.json +10 -0
  8. package/package.json +2 -2
  9. package/packages/database/src/models/__tests__/session.test.ts +0 -29
  10. package/src/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/OpeningQuestions.tsx +0 -2
  11. package/src/app/[variants]/(main)/community/(detail)/agent/features/Sidebar/TocList/index.tsx +0 -36
  12. package/src/app/[variants]/(main)/community/(list)/_layout/Header.tsx +0 -2
  13. package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/GroupMember.tsx +0 -4
  14. package/src/app/[variants]/(main)/group/features/Conversation/ConversationArea.tsx +0 -7
  15. package/src/app/[variants]/(main)/group/features/Conversation/MainChatInput/GroupChat.tsx +0 -2
  16. package/src/app/[variants]/(main)/home/_layout/Body/index.tsx +0 -2
  17. package/src/app/[variants]/(main)/page/_layout/Body/List/Item/useDropdownMenu.tsx +0 -6
  18. package/src/app/[variants]/(main)/page/_layout/Body/useDropdownMenu.tsx +0 -15
  19. package/src/app/[variants]/(main)/resource/features/DndContextWrapper.tsx +0 -5
  20. package/src/app/[variants]/(main)/settings/provider/features/ModelList/CreateNewModelModal/index.tsx +0 -1
  21. package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelItem.tsx +0 -10
  22. package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/Checker.tsx +1 -1
  23. package/src/app/[variants]/(mobile)/(home)/features/SessionListContent/List/Item/Actions.tsx +0 -1
  24. package/src/app/[variants]/layout.tsx +0 -2
  25. package/src/envs/__tests__/app.test.ts +0 -6
  26. package/src/features/ChatInput/ActionBar/Knowledge/useControls.tsx +0 -22
  27. package/src/features/ChatInput/store/action.ts +0 -2
  28. package/src/features/Conversation/Messages/Task/TaskDetailPanel/index.tsx +1 -15
  29. package/src/features/DataImporter/ImportDetail.tsx +0 -20
  30. package/src/features/DevPanel/features/Table/TableCell.tsx +1 -36
  31. package/src/features/DevPanel/index.tsx +0 -9
  32. package/src/features/ModelSwitchPanel/__mocks__/mockEnabledChatModels.ts +159 -0
  33. package/src/features/ModelSwitchPanel/components/List/{VirtualItemRenderer.tsx → ListItemRenderer.tsx} +15 -25
  34. package/src/features/ModelSwitchPanel/components/List/MultipleProvidersModelItem.tsx +95 -69
  35. package/src/features/ModelSwitchPanel/components/List/index.tsx +39 -40
  36. package/src/features/ModelSwitchPanel/components/PanelContent.tsx +0 -8
  37. package/src/features/ModelSwitchPanel/hooks/{useBuildVirtualItems.ts → useBuildListItems.ts} +7 -17
  38. package/src/features/ModelSwitchPanel/index.tsx +24 -23
  39. package/src/features/ModelSwitchPanel/styles.ts +3 -0
  40. package/src/features/ModelSwitchPanel/types.ts +3 -8
  41. package/src/features/ModelSwitchPanel/utils.ts +2 -2
  42. package/src/features/NavPanel/SideBarDrawer.tsx +12 -2
  43. package/src/features/NavPanel/SideBarHeaderLayout.tsx +3 -1
  44. package/src/features/Portal/GroupThread/Body/index.tsx +0 -6
  45. package/src/features/ResourceManager/components/Header/AddButton.tsx +0 -16
  46. package/src/features/ShareModal/ShareImage/index.tsx +0 -8
  47. package/src/hooks/useProviderName.ts +0 -1
  48. package/src/layout/GlobalProvider/Locale.tsx +0 -12
  49. package/src/layout/GlobalProvider/index.tsx +0 -1
  50. package/src/libs/better-auth/sso/helpers.ts +0 -1
  51. package/src/libs/next/config/define-config.ts +5 -0
  52. package/src/locales/create.ts +0 -17
  53. package/src/services/aiChat.ts +0 -4
  54. package/src/services/debug.ts +1 -34
  55. package/src/services/models.ts +0 -15
  56. package/src/store/chat/agents/GroupOrchestration/createGroupOrchestrationExecutors.ts +0 -9
  57. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +0 -3
  58. package/src/store/chat/slices/aiChat/actions/index.ts +1 -3
  59. package/src/store/file/slices/chat/action.test.ts +0 -89
  60. package/src/store/file/slices/chunk/selectors.ts +0 -1
  61. package/src/store/file/slices/fileManager/selectors.ts +0 -1
  62. package/src/store/file/slices/tts/selectors.ts +0 -2
  63. package/src/store/tool/slices/customPlugin/index.ts +0 -1
  64. package/src/store/tool/slices/mcpStore/index.ts +0 -1
  65. package/src/store/tool/slices/oldStore/index.ts +0 -1
  66. package/src/store/tool/slices/plugin/index.ts +0 -1
  67. package/src/styles/global.ts +6 -0
  68. package/src/utils/router.tsx +1 -7
  69. package/src/utils/server/parseModels.ts +0 -1
@@ -33,28 +33,6 @@ export const useControls = ({
33
33
  ]);
34
34
 
35
35
  const items: ItemType[] = [
36
- // {
37
- // children: [
38
- // {
39
- // icon: <RepoIcon />,
40
- // key: 'allFiles',
41
- // label: <KnowledgeBaseItem id={'all'} label={t('knowledgeBase.allFiles')} />,
42
- // },
43
- // {
44
- // icon: <RepoIcon />,
45
- // key: 'allRepos',
46
- // label: <KnowledgeBaseItem id={'all'} label={t('knowledgeBase.allLibraries')} />,
47
- // },
48
- // ],
49
- // key: 'all',
50
- // label: (
51
- // <Flexbox horizontal justify={'space-between'}>
52
- // {t('knowledgeBase.all')}
53
- // {/*<Link href={'/files'}>{t('knowledgeBase.more')}</Link>*/}
54
- // </Flexbox>
55
- // ),
56
- // type: 'group',
57
- // },
58
36
  {
59
37
  children: [
60
38
  // first the files
@@ -15,8 +15,6 @@ export interface Action {
15
15
 
16
16
  export type Store = Action & State;
17
17
 
18
- // const t = setNamespace('ChatInput');
19
-
20
18
  type CreateStore = (
21
19
  initState?: Partial<PublicState>,
22
20
  ) => StateCreator<Store, [['zustand/devtools', never]]>;
@@ -17,21 +17,7 @@ interface TaskDetailPanelProps {
17
17
  }
18
18
 
19
19
  const TaskDetailPanel = memo<TaskDetailPanelProps>(({ taskDetail, content, messageId }) => {
20
- return (
21
- <>
22
- {/* Instruction Header */}
23
- {/*{instruction && (*/}
24
- {/* <Block padding={12}>*/}
25
- {/* <Text fontSize={13} type={'secondary'}>*/}
26
- {/* {instruction}*/}
27
- {/* </Text>*/}
28
- {/* </Block>*/}
29
- {/*)}*/}
30
-
31
- {/* Status Content */}
32
- <StatusContent content={content} messageId={messageId} taskDetail={taskDetail} />
33
- </>
34
- );
20
+ return <StatusContent content={content} messageId={messageId} taskDetail={taskDetail} />;
35
21
  });
36
22
 
37
23
  TaskDetailPanel.displayName = 'TaskDetailPanel';
@@ -170,26 +170,6 @@ const ImportPreviewModal = ({
170
170
  size="small"
171
171
  />
172
172
  </div>
173
-
174
- {/*<Flexbox>*/}
175
- {/* 重复数据处理方式:*/}
176
- {/* <div className={styles.duplicateOptions}>*/}
177
- {/* <Radio.Group*/}
178
- {/* onChange={(e) => setDuplicateAction(e.target.value)}*/}
179
- {/* value={duplicateAction}*/}
180
- {/* >*/}
181
- {/* <Space>*/}
182
- {/* <Radio value="skip">跳过</Radio>*/}
183
- {/* <Radio value="overwrite">覆盖</Radio>*/}
184
- {/* </Space>*/}
185
- {/* </Radio.Group>*/}
186
- {/* </div>*/}
187
- {/* <div className={styles.duplicateDescription}>*/}
188
- {/* {duplicateAction === 'skip'*/}
189
- {/* ? '选择跳过将仅导入不重复的数据,保留现有数据不变。'*/}
190
- {/* : '选择覆盖将使用导入数据替换系统中具有相同 ID 的现有记录。'}*/}
191
- {/* </div>*/}
192
- {/*</Flexbox>*/}
193
173
  </Flexbox>
194
174
  </div>
195
175
  </Modal>
@@ -2,27 +2,6 @@ import dayjs from 'dayjs';
2
2
  import { get, isDate } from 'es-toolkit/compat';
3
3
  import React, { useMemo } from 'react';
4
4
 
5
- // import TooltipContent from './TooltipContent';
6
-
7
- // const { Text } = Typography;
8
-
9
- // const useStyles = createStyles(({ token, css }) => ({
10
- // cell: css`
11
- // font-family: ${token.fontFamilyCode};
12
- // font-size: ${token.fontSizeSM}px;
13
- // `,
14
- // tooltip: css`
15
- // border: 1px solid ${token.colorBorder};
16
- //
17
- // font-family: ${token.fontFamilyCode};
18
- // font-size: ${token.fontSizeSM}px;
19
- // color: ${token.colorText} !important;
20
- // word-break: break-all;
21
- //
22
- // background: ${token.colorBgElevated} !important;
23
- // `,
24
- // }));
25
-
26
5
  interface TableCellProps {
27
6
  column: string;
28
7
  dataItem: any;
@@ -30,7 +9,6 @@ interface TableCellProps {
30
9
  }
31
10
 
32
11
  const TableCell = ({ dataItem, column, rowIndex }: TableCellProps) => {
33
- // const { styles } = useStyles();
34
12
  const data = get(dataItem, column);
35
13
  const content = useMemo(() => {
36
14
  if (isDate(data)) return dayjs(data).format('YYYY-MM-DD HH:mm:ss');
@@ -52,21 +30,8 @@ const TableCell = ({ dataItem, column, rowIndex }: TableCellProps) => {
52
30
 
53
31
  return (
54
32
  <td key={column} onDoubleClick={() => console.log('Edit cell:', rowIndex, column)}>
55
- {content}
56
-
57
33
  {/* 不能使用 antd 的 Text, 会有大量的重渲染导致滚动极其卡顿 */}
58
- {/*<Text*/}
59
- {/* className={styles.cell}*/}
60
- {/* ellipsis={{*/}
61
- {/* tooltip: {*/}
62
- {/* arrow: false,*/}
63
- {/* classNames: { body: styles.tooltip },*/}
64
- {/* title: <TooltipContent>{content}</TooltipContent>,*/}
65
- {/* },*/}
66
- {/* }}*/}
67
- {/*>*/}
68
- {/* {content}*/}
69
- {/*</Text>*/}
34
+ {content}
70
35
  </td>
71
36
  );
72
37
  };
@@ -15,15 +15,6 @@ const FloatPanel = dynamic(() => import('./features/FloatPanel'), {
15
15
  const DevPanel = () => (
16
16
  <FloatPanel
17
17
  items={[
18
- // ...(isDesktop
19
- // ? [
20
- // {
21
- // children: <PostgresViewer />,
22
- // icon: <DatabaseIcon size={16} />,
23
- // key: 'Postgres Viewer',
24
- // },
25
- // ]
26
- // : []),
27
18
  {
28
19
  children: <MetadataViewer />,
29
20
  icon: <BookText size={16} />,
@@ -0,0 +1,159 @@
1
+ import type { AiModelForSelect } from 'model-bank';
2
+
3
+ import type { EnabledProviderWithModels } from '@/types/aiProvider';
4
+
5
+ /**
6
+ * Mock data for testing ModelSwitchPanel
7
+ *
8
+ * This data includes:
9
+ * - Multiple providers (OpenAI, Azure, Ollama)
10
+ * - Same model provided by multiple providers (gpt-4o -> model-item-multiple)
11
+ * - Single provider model (llama3 -> model-item-single)
12
+ */
13
+ export const mockEnabledChatModels: EnabledProviderWithModels[] = [
14
+ {
15
+ children: [
16
+ {
17
+ abilities: {
18
+ functionCall: true,
19
+ reasoning: false,
20
+ vision: true,
21
+ },
22
+ contextWindowTokens: 128_000,
23
+ displayName: 'GPT-4o',
24
+ id: 'gpt-4o',
25
+ maxOutput: 16_384,
26
+ releasedAt: '2024-05-13',
27
+ type: 'chat',
28
+ } as AiModelForSelect,
29
+ {
30
+ abilities: {
31
+ functionCall: true,
32
+ reasoning: false,
33
+ vision: true,
34
+ },
35
+ contextWindowTokens: 128_000,
36
+ displayName: 'GPT-4o Mini',
37
+ id: 'gpt-4o-mini',
38
+ maxOutput: 16_384,
39
+ releasedAt: '2024-07-18',
40
+ type: 'chat',
41
+ } as AiModelForSelect,
42
+ {
43
+ abilities: {
44
+ functionCall: true,
45
+ reasoning: true,
46
+ vision: false,
47
+ },
48
+ contextWindowTokens: 200_000,
49
+ displayName: 'o1',
50
+ id: 'o1',
51
+ maxOutput: 100_000,
52
+ releasedAt: '2024-12-17',
53
+ type: 'chat',
54
+ } as AiModelForSelect,
55
+ ],
56
+ id: 'openai',
57
+ logo: 'https://registry.npmmirror.com/@lobehub/icons-static-png/1.45.0/files/dark/openai.png',
58
+ name: 'OpenAI',
59
+ source: 'builtin',
60
+ },
61
+ {
62
+ children: [
63
+ {
64
+ // Same displayName as OpenAI's gpt-4o -> will create model-item-multiple
65
+ abilities: {
66
+ functionCall: true,
67
+ reasoning: false,
68
+ vision: true,
69
+ },
70
+ contextWindowTokens: 128_000,
71
+ displayName: 'GPT-4o',
72
+ id: 'gpt-4o',
73
+ maxOutput: 16_384,
74
+ type: 'chat',
75
+ } as AiModelForSelect,
76
+ {
77
+ // Same displayName as OpenAI's gpt-4o-mini -> will create model-item-multiple
78
+ abilities: {
79
+ functionCall: true,
80
+ reasoning: false,
81
+ vision: true,
82
+ },
83
+ contextWindowTokens: 128_000,
84
+ displayName: 'GPT-4o Mini',
85
+ id: 'gpt-4o-mini',
86
+ maxOutput: 16_384,
87
+ type: 'chat',
88
+ } as AiModelForSelect,
89
+ ],
90
+ id: 'azure',
91
+ logo: 'https://registry.npmmirror.com/@lobehub/icons-static-png/1.45.0/files/dark/azure.png',
92
+ name: 'Azure OpenAI',
93
+ source: 'builtin',
94
+ },
95
+ {
96
+ children: [
97
+ {
98
+ // Unique model -> will create model-item-single
99
+ abilities: {
100
+ functionCall: true,
101
+ reasoning: false,
102
+ vision: false,
103
+ },
104
+ contextWindowTokens: 128_000,
105
+ displayName: 'Llama 3.3 70B',
106
+ id: 'llama3.3:70b',
107
+ maxOutput: 8192,
108
+ type: 'chat',
109
+ } as AiModelForSelect,
110
+ {
111
+ abilities: {
112
+ functionCall: false,
113
+ reasoning: false,
114
+ vision: true,
115
+ },
116
+ contextWindowTokens: 128_000,
117
+ displayName: 'Llava',
118
+ id: 'llava:latest',
119
+ maxOutput: 4096,
120
+ type: 'chat',
121
+ } as AiModelForSelect,
122
+ ],
123
+ id: 'ollama',
124
+ logo: 'https://registry.npmmirror.com/@lobehub/icons-static-png/1.45.0/files/dark/ollama.png',
125
+ name: 'Ollama',
126
+ source: 'builtin',
127
+ },
128
+ {
129
+ children: [
130
+ {
131
+ // Same as OpenAI's o1 -> will create model-item-multiple
132
+ abilities: {
133
+ functionCall: true,
134
+ reasoning: true,
135
+ vision: false,
136
+ },
137
+ contextWindowTokens: 200_000,
138
+ displayName: 'o1',
139
+ id: 'o1',
140
+ maxOutput: 100_000,
141
+ type: 'chat',
142
+ } as AiModelForSelect,
143
+ ],
144
+ id: 'openrouter',
145
+ logo: 'https://registry.npmmirror.com/@lobehub/icons-static-png/1.45.0/files/dark/openrouter.png',
146
+ name: 'OpenRouter',
147
+ source: 'builtin',
148
+ },
149
+ ];
150
+
151
+ /**
152
+ * Expected result when groupMode = 'byModel':
153
+ *
154
+ * - GPT-4o (model-item-multiple) -> OpenAI, Azure
155
+ * - GPT-4o Mini (model-item-multiple) -> OpenAI, Azure
156
+ * - Llama 3.3 70B (model-item-single) -> Ollama
157
+ * - Llava (model-item-single) -> Ollama
158
+ * - o1 (model-item-multiple) -> OpenAI, OpenRouter
159
+ */
@@ -9,21 +9,22 @@ import urlJoin from 'url-join';
9
9
  import { ModelItemRender, ProviderItemRender } from '@/components/ModelSelect';
10
10
 
11
11
  import { styles } from '../../styles';
12
- import type { VirtualItem } from '../../types';
12
+ import type { ListItem } from '../../types';
13
13
  import { menuKey } from '../../utils';
14
14
  import { MultipleProvidersModelItem } from './MultipleProvidersModelItem';
15
15
  import { SingleProviderModelItem } from './SingleProviderModelItem';
16
16
 
17
- interface VirtualItemRendererProps {
17
+ interface ListItemRendererProps {
18
18
  activeKey: string;
19
- item: VirtualItem;
19
+ isScrolling: boolean;
20
+ item: ListItem;
20
21
  newLabel: string;
21
22
  onClose: () => void;
22
23
  onModelChange: (modelId: string, providerId: string) => Promise<void>;
23
24
  }
24
25
 
25
- export const VirtualItemRenderer = memo<VirtualItemRendererProps>(
26
- ({ activeKey, item, newLabel, onModelChange, onClose }) => {
26
+ export const ListItemRenderer = memo<ListItemRendererProps>(
27
+ ({ activeKey, isScrolling, item, newLabel, onModelChange, onClose }) => {
27
28
  const { t } = useTranslation('components');
28
29
  const navigate = useNavigate();
29
30
 
@@ -145,27 +146,16 @@ export const VirtualItemRenderer = memo<VirtualItemRendererProps>(
145
146
  }
146
147
 
147
148
  case 'model-item-multiple': {
148
- // Check if any provider of this model is active
149
- const activeProvider = item.data.providers.find(
150
- (p) => menuKey(p.id, item.data.model.id) === activeKey,
151
- );
152
- const isActive = !!activeProvider;
153
-
154
149
  return (
155
- <Block
156
- className={styles.menuItem}
157
- clickable
150
+ <MultipleProvidersModelItem
151
+ activeKey={activeKey}
152
+ data={item.data}
153
+ isScrolling={isScrolling}
158
154
  key={item.data.displayName}
159
- variant={isActive ? 'filled' : 'borderless'}
160
- >
161
- <MultipleProvidersModelItem
162
- activeKey={activeKey}
163
- data={item.data}
164
- newLabel={newLabel}
165
- onClose={onClose}
166
- onModelChange={onModelChange}
167
- />
168
- </Block>
155
+ newLabel={newLabel}
156
+ onClose={onClose}
157
+ onModelChange={onModelChange}
158
+ />
169
159
  );
170
160
  }
171
161
 
@@ -176,4 +166,4 @@ export const VirtualItemRenderer = memo<VirtualItemRendererProps>(
176
166
  },
177
167
  );
178
168
 
179
- VirtualItemRenderer.displayName = 'VirtualItemRenderer';
169
+ ListItemRenderer.displayName = 'ListItemRenderer';
@@ -1,6 +1,21 @@
1
- import { ActionIcon, type DropdownItem, DropdownMenu } from '@lobehub/ui';
1
+ import {
2
+ ActionIcon,
3
+ DropdownMenuGroup,
4
+ DropdownMenuGroupLabel,
5
+ DropdownMenuItem,
6
+ DropdownMenuItemExtra,
7
+ DropdownMenuItemIcon,
8
+ DropdownMenuItemLabel,
9
+ DropdownMenuPopup,
10
+ DropdownMenuPortal,
11
+ DropdownMenuPositioner,
12
+ DropdownMenuSubmenuRoot,
13
+ DropdownMenuSubmenuTrigger,
14
+ menuSharedStyles,
15
+ } from '@lobehub/ui';
16
+ import { cx } from 'antd-style';
2
17
  import { Check, LucideBolt } from 'lucide-react';
3
- import { memo, useMemo } from 'react';
18
+ import { memo, useEffect, useState } from 'react';
4
19
  import { useTranslation } from 'react-i18next';
5
20
  import { useNavigate } from 'react-router-dom';
6
21
  import urlJoin from 'url-join';
@@ -14,85 +29,96 @@ import { menuKey } from '../../utils';
14
29
  interface MultipleProvidersModelItemProps {
15
30
  activeKey: string;
16
31
  data: ModelWithProviders;
32
+ isScrolling: boolean;
17
33
  newLabel: string;
18
34
  onClose: () => void;
19
35
  onModelChange: (modelId: string, providerId: string) => Promise<void>;
20
36
  }
21
37
 
22
38
  export const MultipleProvidersModelItem = memo<MultipleProvidersModelItemProps>(
23
- ({ activeKey, data, newLabel, onModelChange, onClose }) => {
39
+ ({ activeKey, data, isScrolling, newLabel, onModelChange, onClose }) => {
24
40
  const { t } = useTranslation('components');
25
41
  const navigate = useNavigate();
42
+ const [submenuOpen, setSubmenuOpen] = useState(false);
26
43
 
27
- const items = useMemo(
28
- () =>
29
- [
30
- {
31
- key: 'header',
32
- label: t('ModelSwitchPanel.useModelFrom'),
33
- type: 'group',
34
- },
35
- ...data.providers.map((p) => {
36
- const key = menuKey(p.id, data.model.id);
44
+ useEffect(() => {
45
+ if (isScrolling) {
46
+ setSubmenuOpen(false);
47
+ }
48
+ }, [isScrolling]);
37
49
 
38
- return {
39
- extra: (
40
- <ActionIcon
41
- className={'settings-icon'}
42
- icon={LucideBolt}
43
- onClick={(e) => {
44
- e.preventDefault();
45
- e.stopPropagation();
46
- const url = urlJoin('/settings/provider', p.id || 'all');
47
- if (e.ctrlKey || e.metaKey) {
48
- window.open(url, '_blank');
49
- } else {
50
- navigate(url);
51
- }
52
- }}
53
- size={'small'}
54
- title={t('ModelSwitchPanel.goToSettings')}
55
- />
56
- ),
57
- icon: activeKey === key ? Check : undefined,
58
- key,
59
- label: (
60
- <ProviderItemRender
61
- logo={p.logo}
62
- name={p.name}
63
- provider={p.id}
64
- size={20}
65
- source={p.source}
66
- type={'avatar'}
67
- />
68
- ),
69
- onClick: async () => {
70
- onModelChange(data.model.id, p.id);
71
- onClose();
72
- },
73
- };
74
- }),
75
- ] as DropdownItem[],
76
- [activeKey, data.model.id, data.providers, navigate, onModelChange, onClose, t],
77
- );
50
+ const isActive = data.providers.some((p) => menuKey(p.id, data.model.id) === activeKey);
78
51
 
79
52
  return (
80
- <DropdownMenu
81
- items={items}
82
- placement="rightTop"
83
- popupProps={{ className: styles.dropdownMenu }}
84
- positionerProps={{
85
- alignOffset: -48,
86
- sideOffset: 12,
87
- }}
88
- >
89
- <ModelItemRender
90
- {...data.model}
91
- {...data.model.abilities}
92
- newBadgeLabel={newLabel}
93
- showInfoTag={true}
94
- />
95
- </DropdownMenu>
53
+ <DropdownMenuSubmenuRoot onOpenChange={setSubmenuOpen} open={submenuOpen}>
54
+ <DropdownMenuSubmenuTrigger
55
+ className={cx(menuSharedStyles.item, isActive && styles.menuItemActive)}
56
+ >
57
+ <ModelItemRender
58
+ {...data.model}
59
+ {...data.model.abilities}
60
+ newBadgeLabel={newLabel}
61
+ showInfoTag={true}
62
+ />
63
+ </DropdownMenuSubmenuTrigger>
64
+ <DropdownMenuPortal>
65
+ <DropdownMenuPositioner anchor={null} placement="rightTop" sideOffset={-4}>
66
+ <DropdownMenuPopup className={styles.dropdownMenu}>
67
+ <DropdownMenuGroup>
68
+ <DropdownMenuGroupLabel>
69
+ {t('ModelSwitchPanel.useModelFrom')}
70
+ </DropdownMenuGroupLabel>
71
+ {data.providers.map((p) => {
72
+ const key = menuKey(p.id, data.model.id);
73
+ const isProviderActive = activeKey === key;
74
+
75
+ return (
76
+ <DropdownMenuItem
77
+ key={key}
78
+ onClick={async () => {
79
+ await onModelChange(data.model.id, p.id);
80
+ onClose();
81
+ }}
82
+ >
83
+ <DropdownMenuItemIcon>
84
+ {isProviderActive ? <Check size={16} /> : null}
85
+ </DropdownMenuItemIcon>
86
+ <DropdownMenuItemLabel>
87
+ <ProviderItemRender
88
+ logo={p.logo}
89
+ name={p.name}
90
+ provider={p.id}
91
+ size={20}
92
+ source={p.source}
93
+ type={'avatar'}
94
+ />
95
+ </DropdownMenuItemLabel>
96
+ <DropdownMenuItemExtra>
97
+ <ActionIcon
98
+ className={'settings-icon'}
99
+ icon={LucideBolt}
100
+ onClick={(e) => {
101
+ e.preventDefault();
102
+ e.stopPropagation();
103
+ const url = urlJoin('/settings/provider', p.id || 'all');
104
+ if (e.ctrlKey || e.metaKey) {
105
+ window.open(url, '_blank');
106
+ } else {
107
+ navigate(url);
108
+ }
109
+ }}
110
+ size={'small'}
111
+ title={t('ModelSwitchPanel.goToSettings')}
112
+ />
113
+ </DropdownMenuItemExtra>
114
+ </DropdownMenuItem>
115
+ );
116
+ })}
117
+ </DropdownMenuGroup>
118
+ </DropdownMenuPopup>
119
+ </DropdownMenuPositioner>
120
+ </DropdownMenuPortal>
121
+ </DropdownMenuSubmenuRoot>
96
122
  );
97
123
  },
98
124
  );