@lobehub/lobehub 2.0.0-next.323 → 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 +58 -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 +14 -0
- package/package.json +2 -2
- package/packages/database/src/models/__tests__/session.test.ts +0 -29
- package/src/app/[variants]/(desktop)/desktop-onboarding/components/LobeMessage.tsx +5 -0
- package/src/app/[variants]/(desktop)/desktop-onboarding/features/WelcomeStep.tsx +3 -1
- 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)/home/features/WelcomeText/index.tsx +3 -1
- 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/app/[variants]/onboarding/components/LobeMessage.tsx +5 -0
- package/src/app/[variants]/onboarding/features/TelemetryStep.tsx +3 -1
- 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
|
@@ -18,19 +18,7 @@ interface TaskDetailPanelProps {
|
|
|
18
18
|
|
|
19
19
|
const TaskDetailPanel = memo<TaskDetailPanelProps>(({ taskDetail, content, messageId }) => {
|
|
20
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
|
-
</>
|
|
21
|
+
<StatusContent content={content} messageId={messageId} taskDetail={taskDetail} />
|
|
34
22
|
);
|
|
35
23
|
});
|
|
36
24
|
|
|
@@ -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
|
-
{
|
|
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 {
|
|
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
|
);
|