@lobehub/lobehub 2.0.0-next.324 → 2.0.0-next.325
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 +33 -0
- package/CLAUDE.md +4 -0
- package/apps/desktop/src/main/core/browser/Browser.ts +40 -1
- package/apps/desktop/src/main/core/infrastructure/I18nManager.ts +0 -11
- package/changelog/v1.json +5 -0
- package/package.json +2 -2
- package/packages/database/src/models/__tests__/session.test.ts +0 -29
- package/src/app/[variants]/(main)/agent/features/Conversation/AgentWelcome/OpeningQuestions.tsx +0 -2
- package/src/app/[variants]/(main)/community/(detail)/agent/features/Sidebar/TocList/index.tsx +0 -36
- package/src/app/[variants]/(main)/community/(list)/_layout/Header.tsx +0 -2
- package/src/app/[variants]/(main)/group/_layout/Sidebar/GroupConfig/GroupMember.tsx +0 -4
- package/src/app/[variants]/(main)/group/features/Conversation/ConversationArea.tsx +0 -7
- package/src/app/[variants]/(main)/group/features/Conversation/MainChatInput/GroupChat.tsx +0 -2
- package/src/app/[variants]/(main)/home/_layout/Body/index.tsx +0 -2
- package/src/app/[variants]/(main)/page/_layout/Body/List/Item/useDropdownMenu.tsx +0 -6
- package/src/app/[variants]/(main)/page/_layout/Body/useDropdownMenu.tsx +0 -15
- package/src/app/[variants]/(main)/resource/features/DndContextWrapper.tsx +0 -5
- package/src/app/[variants]/(main)/settings/provider/features/ModelList/CreateNewModelModal/index.tsx +0 -1
- package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelItem.tsx +0 -10
- package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/Checker.tsx +1 -1
- package/src/app/[variants]/(mobile)/(home)/features/SessionListContent/List/Item/Actions.tsx +0 -1
- package/src/app/[variants]/layout.tsx +0 -2
- package/src/envs/__tests__/app.test.ts +0 -6
- package/src/features/ChatInput/ActionBar/Knowledge/useControls.tsx +0 -22
- package/src/features/ChatInput/store/action.ts +0 -2
- package/src/features/Conversation/Messages/Task/TaskDetailPanel/index.tsx +1 -13
- package/src/features/DataImporter/ImportDetail.tsx +0 -20
- package/src/features/DevPanel/features/Table/TableCell.tsx +1 -36
- package/src/features/DevPanel/index.tsx +0 -9
- package/src/features/ModelSwitchPanel/__mocks__/mockEnabledChatModels.ts +159 -0
- package/src/features/ModelSwitchPanel/components/List/{VirtualItemRenderer.tsx → ListItemRenderer.tsx} +15 -25
- package/src/features/ModelSwitchPanel/components/List/MultipleProvidersModelItem.tsx +95 -69
- package/src/features/ModelSwitchPanel/components/List/index.tsx +39 -40
- package/src/features/ModelSwitchPanel/components/PanelContent.tsx +0 -8
- package/src/features/ModelSwitchPanel/hooks/{useBuildVirtualItems.ts → useBuildListItems.ts} +7 -17
- package/src/features/ModelSwitchPanel/index.tsx +24 -23
- package/src/features/ModelSwitchPanel/styles.ts +3 -0
- package/src/features/ModelSwitchPanel/types.ts +3 -8
- package/src/features/ModelSwitchPanel/utils.ts +2 -2
- package/src/features/NavPanel/SideBarDrawer.tsx +12 -2
- package/src/features/Portal/GroupThread/Body/index.tsx +0 -6
- package/src/features/ResourceManager/components/Header/AddButton.tsx +0 -16
- package/src/features/ShareModal/ShareImage/index.tsx +0 -8
- package/src/hooks/useProviderName.ts +0 -1
- package/src/layout/GlobalProvider/Locale.tsx +0 -12
- package/src/layout/GlobalProvider/index.tsx +0 -1
- package/src/libs/better-auth/sso/helpers.ts +0 -1
- package/src/libs/next/config/define-config.ts +5 -0
- package/src/locales/create.ts +0 -17
- package/src/services/aiChat.ts +0 -4
- package/src/services/debug.ts +1 -34
- package/src/services/models.ts +0 -15
- package/src/store/chat/agents/GroupOrchestration/createGroupOrchestrationExecutors.ts +0 -9
- package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +0 -3
- package/src/store/chat/slices/aiChat/actions/index.ts +1 -3
- package/src/store/file/slices/chat/action.test.ts +0 -89
- package/src/store/file/slices/chunk/selectors.ts +0 -1
- package/src/store/file/slices/fileManager/selectors.ts +0 -1
- package/src/store/file/slices/tts/selectors.ts +0 -2
- package/src/store/tool/slices/customPlugin/index.ts +0 -1
- package/src/store/tool/slices/mcpStore/index.ts +0 -1
- package/src/store/tool/slices/oldStore/index.ts +0 -1
- package/src/store/tool/slices/plugin/index.ts +0 -1
- package/src/styles/global.ts +6 -0
- package/src/utils/router.tsx +1 -7
- package/src/utils/server/parseModels.ts +0 -1
|
@@ -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 {
|
|
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
|
|
17
|
+
interface ListItemRendererProps {
|
|
18
18
|
activeKey: string;
|
|
19
|
-
|
|
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
|
|
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
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
150
|
+
<MultipleProvidersModelItem
|
|
151
|
+
activeKey={activeKey}
|
|
152
|
+
data={item.data}
|
|
153
|
+
isScrolling={isScrolling}
|
|
158
154
|
key={item.data.displayName}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
169
|
+
ListItemRenderer.displayName = 'ListItemRenderer';
|
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
import {
|
|
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,
|
|
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
|
-
|
|
28
|
-
()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
);
|
|
@@ -1,29 +1,22 @@
|
|
|
1
1
|
import { Flexbox, TooltipGroup } from '@lobehub/ui';
|
|
2
2
|
import type { FC } from 'react';
|
|
3
|
-
import { useMemo } from 'react';
|
|
3
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { Virtuoso } from 'react-virtuoso';
|
|
5
6
|
|
|
6
7
|
import { useEnabledChatModels } from '@/hooks/useEnabledChatModels';
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
INITIAL_RENDER_COUNT,
|
|
11
|
-
ITEM_HEIGHT,
|
|
12
|
-
MAX_PANEL_HEIGHT,
|
|
13
|
-
TOOLBAR_HEIGHT,
|
|
14
|
-
} from '../../const';
|
|
15
|
-
import { useBuildVirtualItems } from '../../hooks/useBuildVirtualItems';
|
|
16
|
-
import { useDelayedRender } from '../../hooks/useDelayedRender';
|
|
9
|
+
import { FOOTER_HEIGHT, ITEM_HEIGHT, MAX_PANEL_HEIGHT, TOOLBAR_HEIGHT } from '../../const';
|
|
10
|
+
import { useBuildListItems } from '../../hooks/useBuildListItems';
|
|
17
11
|
import { useModelAndProvider } from '../../hooks/useModelAndProvider';
|
|
18
12
|
import { usePanelHandlers } from '../../hooks/usePanelHandlers';
|
|
19
13
|
import { styles } from '../../styles';
|
|
20
14
|
import type { GroupMode } from '../../types';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
15
|
+
import { menuKey } from '../../utils';
|
|
16
|
+
import { ListItemRenderer } from './ListItemRenderer';
|
|
23
17
|
|
|
24
18
|
interface ListProps {
|
|
25
19
|
groupMode: GroupMode;
|
|
26
|
-
isOpen: boolean;
|
|
27
20
|
model?: string;
|
|
28
21
|
onModelChange?: (params: { model: string; provider: string }) => Promise<void>;
|
|
29
22
|
onOpenChange?: (open: boolean) => void;
|
|
@@ -33,7 +26,6 @@ interface ListProps {
|
|
|
33
26
|
|
|
34
27
|
export const List: FC<ListProps> = ({
|
|
35
28
|
groupMode,
|
|
36
|
-
isOpen,
|
|
37
29
|
model: modelProp,
|
|
38
30
|
onModelChange: onModelChangeProp,
|
|
39
31
|
onOpenChange,
|
|
@@ -43,25 +35,15 @@ export const List: FC<ListProps> = ({
|
|
|
43
35
|
const { t: tCommon } = useTranslation('common');
|
|
44
36
|
const newLabel = tCommon('new');
|
|
45
37
|
|
|
46
|
-
|
|
38
|
+
const [isScrolling, setIsScrolling] = useState(false);
|
|
47
39
|
const enabledList = useEnabledChatModels();
|
|
48
|
-
|
|
49
|
-
// Get delayed render state
|
|
50
|
-
const renderAll = useDelayedRender(isOpen);
|
|
51
|
-
|
|
52
|
-
// Get model and provider
|
|
53
40
|
const { model, provider } = useModelAndProvider(modelProp, providerProp);
|
|
54
|
-
|
|
55
|
-
// Get handlers
|
|
56
41
|
const { handleModelChange, handleClose } = usePanelHandlers({
|
|
57
42
|
onModelChange: onModelChangeProp,
|
|
58
43
|
onOpenChange,
|
|
59
44
|
});
|
|
45
|
+
const listItems = useBuildListItems(enabledList, groupMode, searchKeyword);
|
|
60
46
|
|
|
61
|
-
// Build virtual items
|
|
62
|
-
const virtualItems = useBuildVirtualItems(enabledList, groupMode, searchKeyword);
|
|
63
|
-
|
|
64
|
-
// Calculate panel height
|
|
65
47
|
const panelHeight = useMemo(
|
|
66
48
|
() =>
|
|
67
49
|
enabledList.length === 0
|
|
@@ -70,31 +52,48 @@ export const List: FC<ListProps> = ({
|
|
|
70
52
|
[enabledList.length],
|
|
71
53
|
);
|
|
72
54
|
|
|
73
|
-
// Calculate active key
|
|
74
55
|
const activeKey = menuKey(provider, model);
|
|
75
56
|
|
|
57
|
+
const handleScrollingStateChange = useCallback((scrolling: boolean) => {
|
|
58
|
+
setIsScrolling(scrolling);
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const itemContent = useCallback(
|
|
62
|
+
(index: number) => {
|
|
63
|
+
const item = listItems[index];
|
|
64
|
+
return (
|
|
65
|
+
<ListItemRenderer
|
|
66
|
+
activeKey={activeKey}
|
|
67
|
+
isScrolling={isScrolling}
|
|
68
|
+
item={item}
|
|
69
|
+
newLabel={newLabel}
|
|
70
|
+
onClose={handleClose}
|
|
71
|
+
onModelChange={handleModelChange}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
[activeKey, handleClose, handleModelChange, isScrolling, listItems, newLabel],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const listHeight = panelHeight - TOOLBAR_HEIGHT - FOOTER_HEIGHT;
|
|
79
|
+
|
|
76
80
|
return (
|
|
77
81
|
<Flexbox
|
|
78
82
|
className={styles.list}
|
|
79
83
|
flex={1}
|
|
80
84
|
style={{
|
|
81
|
-
height:
|
|
85
|
+
height: listHeight,
|
|
82
86
|
paddingBlock: groupMode === 'byModel' ? 8 : 0,
|
|
83
87
|
}}
|
|
84
88
|
>
|
|
85
89
|
<TooltipGroup>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
newLabel={newLabel}
|
|
94
|
-
onClose={handleClose}
|
|
95
|
-
onModelChange={handleModelChange}
|
|
96
|
-
/>
|
|
97
|
-
))}
|
|
90
|
+
<Virtuoso
|
|
91
|
+
isScrolling={handleScrollingStateChange}
|
|
92
|
+
itemContent={itemContent}
|
|
93
|
+
overscan={200}
|
|
94
|
+
style={{ height: listHeight }}
|
|
95
|
+
totalCount={listItems.length}
|
|
96
|
+
/>
|
|
98
97
|
</TooltipGroup>
|
|
99
98
|
</Flexbox>
|
|
100
99
|
);
|
|
@@ -13,7 +13,6 @@ import { List } from './List';
|
|
|
13
13
|
import { Toolbar } from './Toolbar';
|
|
14
14
|
|
|
15
15
|
interface PanelContentProps {
|
|
16
|
-
isOpen: boolean;
|
|
17
16
|
model?: string;
|
|
18
17
|
onModelChange?: (params: { model: string; provider: string }) => Promise<void>;
|
|
19
18
|
onOpenChange?: (open: boolean) => void;
|
|
@@ -21,19 +20,13 @@ interface PanelContentProps {
|
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
export const PanelContent: FC<PanelContentProps> = ({
|
|
24
|
-
isOpen,
|
|
25
23
|
model: modelProp,
|
|
26
24
|
onModelChange: onModelChangeProp,
|
|
27
25
|
onOpenChange,
|
|
28
26
|
provider: providerProp,
|
|
29
27
|
}) => {
|
|
30
|
-
// Get enabled models list
|
|
31
28
|
const enabledList = useEnabledChatModels();
|
|
32
|
-
|
|
33
|
-
// Search keyword state
|
|
34
29
|
const [searchKeyword, setSearchKeyword] = useState('');
|
|
35
|
-
|
|
36
|
-
// Hooks for state management
|
|
37
30
|
const { groupMode, handleGroupModeChange } = usePanelState();
|
|
38
31
|
const { panelHeight, panelWidth, handlePanelWidthChange } = usePanelSize(enabledList.length);
|
|
39
32
|
const { handleClose } = usePanelHandlers({
|
|
@@ -62,7 +55,6 @@ export const PanelContent: FC<PanelContentProps> = ({
|
|
|
62
55
|
/>
|
|
63
56
|
<List
|
|
64
57
|
groupMode={groupMode}
|
|
65
|
-
isOpen={isOpen}
|
|
66
58
|
model={modelProp}
|
|
67
59
|
onModelChange={onModelChangeProp}
|
|
68
60
|
onOpenChange={onOpenChange}
|
package/src/features/ModelSwitchPanel/hooks/{useBuildVirtualItems.ts → useBuildListItems.ts}
RENAMED
|
@@ -2,26 +2,25 @@ import { useMemo } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import type { EnabledProviderWithModels } from '@/types/aiProvider';
|
|
4
4
|
|
|
5
|
-
import type { GroupMode,
|
|
5
|
+
import type { GroupMode, ListItem, ModelWithProviders } from '../types';
|
|
6
6
|
|
|
7
|
-
export const
|
|
7
|
+
export const useBuildListItems = (
|
|
8
8
|
enabledList: EnabledProviderWithModels[],
|
|
9
9
|
groupMode: GroupMode,
|
|
10
10
|
searchKeyword: string = '',
|
|
11
|
-
):
|
|
11
|
+
): ListItem[] => {
|
|
12
12
|
return useMemo(() => {
|
|
13
13
|
if (enabledList.length === 0) {
|
|
14
|
-
return [{ type: 'no-provider' }] as
|
|
14
|
+
return [{ type: 'no-provider' }] as ListItem[];
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
// Filter function for search
|
|
18
17
|
const matchesSearch = (text: string): boolean => {
|
|
19
18
|
if (!searchKeyword.trim()) return true;
|
|
20
19
|
const keyword = searchKeyword.toLowerCase().trim();
|
|
21
20
|
return text.toLowerCase().includes(keyword);
|
|
22
21
|
};
|
|
23
22
|
|
|
24
|
-
//
|
|
23
|
+
// lobehub first, then others
|
|
25
24
|
const sortedProviders = [...enabledList].sort((a, b) => {
|
|
26
25
|
const aIsLobehub = a.id === 'lobehub';
|
|
27
26
|
const bIsLobehub = b.id === 'lobehub';
|
|
@@ -31,14 +30,12 @@ export const useBuildVirtualItems = (
|
|
|
31
30
|
});
|
|
32
31
|
|
|
33
32
|
if (groupMode === 'byModel') {
|
|
34
|
-
// Group models by display name
|
|
35
33
|
const modelMap = new Map<string, ModelWithProviders>();
|
|
36
34
|
|
|
37
35
|
for (const providerItem of sortedProviders) {
|
|
38
36
|
for (const modelItem of providerItem.children) {
|
|
39
37
|
const displayName = modelItem.displayName || modelItem.id;
|
|
40
38
|
|
|
41
|
-
// Filter by search keyword
|
|
42
39
|
if (!matchesSearch(displayName) && !matchesSearch(providerItem.name)) {
|
|
43
40
|
continue;
|
|
44
41
|
}
|
|
@@ -61,7 +58,7 @@ export const useBuildVirtualItems = (
|
|
|
61
58
|
}
|
|
62
59
|
}
|
|
63
60
|
|
|
64
|
-
//
|
|
61
|
+
// lobehub first
|
|
65
62
|
const modelArray = Array.from(modelMap.values());
|
|
66
63
|
for (const model of modelArray) {
|
|
67
64
|
model.providers.sort((a, b) => {
|
|
@@ -73,7 +70,6 @@ export const useBuildVirtualItems = (
|
|
|
73
70
|
});
|
|
74
71
|
}
|
|
75
72
|
|
|
76
|
-
// Convert to array and sort by display name
|
|
77
73
|
return modelArray
|
|
78
74
|
.sort((a, b) => a.displayName.localeCompare(b.displayName))
|
|
79
75
|
.map((data) => ({
|
|
@@ -84,27 +80,21 @@ export const useBuildVirtualItems = (
|
|
|
84
80
|
: ('model-item-multiple' as const),
|
|
85
81
|
}));
|
|
86
82
|
} else {
|
|
87
|
-
|
|
88
|
-
const items: VirtualItem[] = [];
|
|
83
|
+
const items: ListItem[] = [];
|
|
89
84
|
|
|
90
85
|
for (const providerItem of sortedProviders) {
|
|
91
|
-
// Filter models by search keyword
|
|
92
86
|
const filteredModels = providerItem.children.filter(
|
|
93
87
|
(modelItem) =>
|
|
94
88
|
matchesSearch(modelItem.displayName || modelItem.id) ||
|
|
95
89
|
matchesSearch(providerItem.name),
|
|
96
90
|
);
|
|
97
91
|
|
|
98
|
-
// Only add provider group header if there are matching models or if search is empty
|
|
99
92
|
if (filteredModels.length > 0 || !searchKeyword.trim()) {
|
|
100
|
-
// Add provider group header
|
|
101
93
|
items.push({ provider: providerItem, type: 'group-header' });
|
|
102
94
|
|
|
103
95
|
if (filteredModels.length === 0) {
|
|
104
|
-
// Add empty model placeholder
|
|
105
96
|
items.push({ provider: providerItem, type: 'empty-model' });
|
|
106
97
|
} else {
|
|
107
|
-
// Add each filtered model item
|
|
108
98
|
for (const modelItem of filteredModels) {
|
|
109
99
|
items.push({
|
|
110
100
|
model: modelItem,
|