@lobehub/lobehub 2.0.0-next.33 → 2.0.0-next.35

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 (148) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/package.json +1 -1
  4. package/packages/model-bank/src/aiModels/google.ts +1 -1
  5. package/src/app/[variants]/(main)/chat/ChatRouter.tsx +83 -0
  6. package/src/app/[variants]/(main)/chat/_layout/ChatLayout.tsx +22 -0
  7. package/src/app/[variants]/(main)/chat/_layout/Desktop/SessionPanel.tsx +12 -7
  8. package/src/app/[variants]/(main)/chat/_layout/Desktop/index.tsx +2 -2
  9. package/src/app/[variants]/(main)/chat/_layout/FeatureFlagsProvider.tsx +24 -0
  10. package/src/app/[variants]/(main)/chat/_layout/Mobile.tsx +3 -2
  11. package/src/app/[variants]/(main)/chat/_layout/type.ts +0 -1
  12. package/src/app/[variants]/(main)/chat/components/ConversationArea.tsx +29 -0
  13. package/src/app/[variants]/(main)/chat/components/MainChatPage.tsx +25 -0
  14. package/src/app/[variants]/(main)/chat/components/PortalPanel.tsx +28 -0
  15. package/src/app/[variants]/(main)/chat/components/SessionPanel.tsx +33 -0
  16. package/src/app/[variants]/(main)/chat/{settings/page.tsx → components/SettingsPage.tsx} +35 -3
  17. package/src/app/[variants]/(main)/chat/components/TopicSidebar.tsx +30 -0
  18. package/src/app/[variants]/(main)/chat/components/WorkspaceLayout.tsx +73 -0
  19. package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/ChatItem/index.tsx +1 -1
  20. package/src/app/[variants]/(main)/chat/{layout.ts → layout.tsx} +0 -1
  21. package/src/app/[variants]/(main)/chat/page.tsx +12 -0
  22. package/src/app/[variants]/(main)/settings/provider/ProviderMenu/List.tsx +97 -7
  23. package/src/app/[variants]/(main)/settings/provider/features/ModelList/DisabledModels.tsx +144 -8
  24. package/src/features/Portal/GroupThread/Body/index.tsx +1 -1
  25. package/src/hooks/useHotkeys/chatScope.ts +1 -1
  26. package/src/locales/default/modelProvider.ts +15 -1
  27. package/src/server/services/mcp/deps/checkers/ManualInstallationChecker.test.ts +162 -0
  28. package/src/server/services/mcp/deps/checkers/NpmInstallationChecker.test.ts +374 -0
  29. package/src/server/services/mcp/deps/checkers/PythonInstallationChecker.test.ts +368 -0
  30. package/src/store/global/initialState.ts +4 -0
  31. package/src/store/global/selectors/systemStatus.ts +6 -0
  32. package/src/app/[variants]/(main)/chat/(workspace)/layout.ts +0 -11
  33. package/src/app/[variants]/(main)/chat/(workspace)/page.tsx +0 -53
  34. package/src/app/[variants]/(main)/chat/@session/default.tsx +0 -31
  35. package/src/app/[variants]/(main)/chat/settings/layout.tsx +0 -21
  36. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/default.tsx +0 -0
  37. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatHydration/index.tsx +0 -0
  38. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Desktop/ClassicChat.tsx +0 -0
  39. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Desktop/GroupChat.tsx +0 -0
  40. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Desktop/MessageFromUrl.tsx +0 -0
  41. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Desktop/index.tsx +0 -0
  42. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Desktop/useSendMenuItems.tsx +0 -0
  43. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Mobile/MentionedUsers/MentionedUserItem.tsx +0 -0
  44. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Mobile/MentionedUsers/index.tsx +0 -0
  45. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Mobile/index.tsx +0 -0
  46. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/ActionBar.tsx +0 -0
  47. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/Files/index.tsx +0 -0
  48. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/InputArea/Container.tsx +0 -0
  49. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/InputArea/index.tsx +0 -0
  50. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/Send.tsx +0 -0
  51. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/index.tsx +0 -0
  52. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/useSend.ts +0 -0
  53. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/index.tsx +0 -0
  54. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/useSend.ts +0 -0
  55. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/ChatItem/OrchestratorThinking.tsx +0 -0
  56. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/ChatItem/Thread.tsx +0 -0
  57. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/ChatItem/ThreadItem.tsx +0 -0
  58. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/Content.tsx +0 -0
  59. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/AgentWelcome/AddButton.tsx +0 -0
  60. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/AgentWelcome/OpeningQuestions.tsx +0 -0
  61. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/AgentWelcome/index.tsx +0 -0
  62. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/GroupWelcome/GroupUsageSuggest.tsx +0 -0
  63. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/GroupWelcome/index.tsx +0 -0
  64. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/GroupWelcome/useTemplateMatching.ts +0 -0
  65. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/index.tsx +0 -0
  66. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/index.tsx +0 -0
  67. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatMinimap/index.tsx +0 -0
  68. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ThreadHydration.tsx +0 -0
  69. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ZenModeToast/Toast.tsx +0 -0
  70. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ZenModeToast/index.tsx +0 -0
  71. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/AgentSettings/index.tsx +0 -0
  72. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/AgentTeamSettings/index.tsx +0 -0
  73. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/ChangelogModal.tsx +0 -0
  74. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/SettingButton.tsx +0 -0
  75. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/ShareButton/index.tsx +0 -0
  76. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/TelemetryNotification.tsx +0 -0
  77. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/HeaderAction.tsx +0 -0
  78. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Main.tsx +0 -0
  79. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Tags/HistoryLimitTags.tsx +0 -0
  80. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Tags/KnowledgeTag.tsx +0 -0
  81. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Tags/MemberCountTag.tsx +0 -0
  82. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Tags/SearchTags.tsx +0 -0
  83. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Tags/index.tsx +0 -0
  84. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/index.tsx +0 -0
  85. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/Portal.tsx +0 -0
  86. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/TopicPanel.tsx +0 -0
  87. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/index.tsx +0 -0
  88. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Mobile/ChatHeader/ChatHeaderTitle.tsx +0 -0
  89. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Mobile/ChatHeader/index.tsx +0 -0
  90. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Mobile/TopicModal.tsx +0 -0
  91. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Mobile/index.tsx +0 -0
  92. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/type.ts +0 -0
  93. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/_layout/Desktop.tsx +0 -0
  94. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/_layout/Mobile.tsx +0 -0
  95. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/default.tsx +0 -0
  96. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/error.tsx +0 -0
  97. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/features/Body.tsx +0 -0
  98. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/loading.tsx +0 -0
  99. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/_layout/Desktop.tsx +0 -0
  100. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/_layout/Mobile.tsx +0 -0
  101. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/default.tsx +0 -0
  102. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/AgentConfig/SystemRole.tsx +0 -0
  103. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/AgentConfig/index.tsx +0 -0
  104. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/ConfigLayout.tsx +0 -0
  105. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/ConfigSwitcher.tsx +0 -0
  106. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/GroupConfig/GroupMember.tsx +0 -0
  107. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/GroupConfig/GroupMemberItem.tsx +0 -0
  108. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/GroupConfig/GroupRole.tsx +0 -0
  109. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/GroupConfig/index.tsx +0 -0
  110. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/GroupConfig/style.ts +0 -0
  111. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/SkeletonList.tsx +0 -0
  112. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/Header.tsx +0 -0
  113. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/ByTimeMode/GroupItem.tsx +0 -0
  114. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/ByTimeMode/index.tsx +0 -0
  115. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/FlatMode/index.tsx +0 -0
  116. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/SearchResult/index.tsx +0 -0
  117. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/ThreadItem/Content.tsx +0 -0
  118. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/ThreadItem/index.tsx +0 -0
  119. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/ThreadList/index.tsx +0 -0
  120. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/TopicItem/DefaultContent.tsx +0 -0
  121. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/TopicItem/TopicContent.tsx +0 -0
  122. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/TopicItem/index.tsx +0 -0
  123. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/index.tsx +0 -0
  124. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicSearchBar/index.tsx +0 -0
  125. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/index.tsx +0 -0
  126. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionHydration.tsx +0 -0
  127. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/CollapseGroup/Actions.tsx +0 -0
  128. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/CollapseGroup/index.tsx +0 -0
  129. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/DefaultMode.tsx +0 -0
  130. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/Inbox/index.tsx +0 -0
  131. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/List/AddButton.tsx +0 -0
  132. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/List/Item/Actions.tsx +0 -0
  133. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/List/Item/index.tsx +0 -0
  134. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/List/index.tsx +0 -0
  135. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/ListItem/index.tsx +0 -0
  136. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/Modals/ConfigGroupModal/GroupItem.tsx +0 -0
  137. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/Modals/ConfigGroupModal/index.tsx +0 -0
  138. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/Modals/CreateGroupModal.tsx +0 -0
  139. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/Modals/RenameGroupModal.tsx +0 -0
  140. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/SearchMode.tsx +0 -0
  141. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/index.tsx +0 -0
  142. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionSearchBar.tsx +0 -0
  143. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SkeletonList.tsx +0 -0
  144. /package/src/app/[variants]/(main)/chat/{@session/_layout → session/layout}/Desktop/PanelBody.tsx +0 -0
  145. /package/src/app/[variants]/(main)/chat/{@session/_layout → session/layout}/Desktop/SessionHeader.tsx +0 -0
  146. /package/src/app/[variants]/(main)/chat/{@session/_layout → session/layout}/Desktop/index.tsx +0 -0
  147. /package/src/app/[variants]/(main)/chat/{@session/_layout → session/layout}/Mobile/SessionHeader.tsx +0 -0
  148. /package/src/app/[variants]/(main)/chat/{@session/_layout → session/layout}/Mobile/index.tsx +0 -0
@@ -1,19 +1,29 @@
1
1
  'use client';
2
2
 
3
- import { ActionIcon, ScrollShadow, Text } from '@lobehub/ui';
3
+ import { ActionIcon, Dropdown, Icon, ScrollShadow, Text } from '@lobehub/ui';
4
+ import type { ItemType } from 'antd/es/menu/interface';
4
5
  import isEqual from 'fast-deep-equal';
5
- import { ArrowDownUpIcon } from 'lucide-react';
6
- import { useState } from 'react';
6
+ import { ArrowDownUpIcon, LucideCheck } from 'lucide-react';
7
+ import { useCallback, useMemo, useState } from 'react';
7
8
  import { useTranslation } from 'react-i18next';
8
9
  import { Flexbox } from 'react-layout-kit';
9
10
 
10
11
  import { aiProviderSelectors } from '@/store/aiInfra';
11
12
  import { useAiInfraStore } from '@/store/aiInfra/store';
13
+ import { useGlobalStore } from '@/store/global';
14
+ import { systemStatusSelectors } from '@/store/global/selectors';
12
15
 
13
16
  import All from './All';
14
17
  import ProviderItem from './Item';
15
18
  import SortProviderModal from './SortProviderModal';
16
19
 
20
+ // Sort type enumeration
21
+ enum SortType {
22
+ Alphabetical = 'alphabetical',
23
+ AlphabeticalDesc = 'alphabeticalDesc',
24
+ Default = 'default',
25
+ }
26
+
17
27
  const ProviderList = (props: {
18
28
  mobile?: boolean;
19
29
  onProviderSelect: (providerKey: string) => void;
@@ -21,6 +31,19 @@ const ProviderList = (props: {
21
31
  const { onProviderSelect, mobile } = props;
22
32
  const { t } = useTranslation('modelProvider');
23
33
  const [open, setOpen] = useState(false);
34
+
35
+ const [sortType, updateSystemStatus] = useGlobalStore((s) => [
36
+ systemStatusSelectors.disabledModelProvidersSortType(s),
37
+ s.updateSystemStatus,
38
+ ]);
39
+
40
+ const updateSortType = useCallback(
41
+ (newSortType: SortType) => {
42
+ updateSystemStatus({ disabledModelProvidersSortType: newSortType });
43
+ },
44
+ [updateSystemStatus],
45
+ );
46
+
24
47
  const enabledModelProviderList = useAiInfraStore(
25
48
  aiProviderSelectors.enabledAiProviderList,
26
49
  isEqual,
@@ -30,6 +53,34 @@ const ProviderList = (props: {
30
53
  aiProviderSelectors.disabledAiProviderList,
31
54
  isEqual,
32
55
  );
56
+
57
+ // Sort model providers based on sort type
58
+ const sortedDisabledProviders = useMemo(() => {
59
+ const providers = [...disabledModelProviderList];
60
+ switch (sortType) {
61
+ case SortType.Alphabetical: {
62
+ return providers.sort((a, b) => {
63
+ const cmpDisplay = (a.name || a.id).localeCompare(b.name || b.id);
64
+ if (cmpDisplay !== 0) return cmpDisplay;
65
+ return a.id.localeCompare(b.id);
66
+ });
67
+ }
68
+ case SortType.AlphabeticalDesc: {
69
+ return providers.sort((a, b) => {
70
+ const cmpDisplay = (b.name || a.id).localeCompare(a.name || b.id);
71
+ if (cmpDisplay !== 0) return cmpDisplay;
72
+ return b.id.localeCompare(a.id);
73
+ });
74
+ }
75
+ case SortType.Default: {
76
+ return providers;
77
+ }
78
+ default: {
79
+ return providers;
80
+ }
81
+ }
82
+ }, [disabledModelProviderList, sortType]);
83
+
33
84
  return (
34
85
  <ScrollShadow gap={4} height={'100%'} paddingInline={12} size={4} style={{ paddingBottom: 32 }}>
35
86
  {!mobile && <All onClick={onProviderSelect} />}
@@ -63,10 +114,49 @@ const ProviderList = (props: {
63
114
  {enabledModelProviderList.map((item) => (
64
115
  <ProviderItem {...item} key={item.id} onClick={onProviderSelect} />
65
116
  ))}
66
- <Text style={{ fontSize: 12, marginTop: 8 }} type={'secondary'}>
67
- {t('menu.list.disabled')}
68
- </Text>
69
- {disabledModelProviderList.map((item) => (
117
+ <Flexbox align={'center'} horizontal justify={'space-between'}>
118
+ <Text style={{ fontSize: 12, marginTop: 8 }} type={'secondary'}>
119
+ {t('menu.list.disabled')}
120
+ </Text>
121
+ {disabledModelProviderList.length > 1 && (
122
+ <Dropdown
123
+ menu={{
124
+ items: [
125
+ {
126
+ icon: sortType === SortType.Default ? <Icon icon={LucideCheck} /> : <div />,
127
+ key: 'default',
128
+ label: t('menu.list.disabledActions.sortDefault'),
129
+ onClick: () => updateSortType(SortType.Default),
130
+ },
131
+ {
132
+ type: 'divider',
133
+ },
134
+ {
135
+ icon: sortType === SortType.Alphabetical ? <Icon icon={LucideCheck} /> : <div />,
136
+ key: 'alphabetical',
137
+ label: t('menu.list.disabledActions.sortAlphabetical'),
138
+ onClick: () => updateSortType(SortType.Alphabetical),
139
+ },
140
+ {
141
+ icon:
142
+ sortType === SortType.AlphabeticalDesc ? <Icon icon={LucideCheck} /> : <div />,
143
+ key: 'alphabeticalDesc',
144
+ label: t('menu.list.disabledActions.sortAlphabeticalDesc'),
145
+ onClick: () => updateSortType(SortType.AlphabeticalDesc),
146
+ },
147
+ ] as ItemType[],
148
+ }}
149
+ trigger={['click']}
150
+ >
151
+ <ActionIcon
152
+ icon={ArrowDownUpIcon}
153
+ size={'small'}
154
+ title={t('menu.list.disabledActions.sort')}
155
+ />
156
+ </Dropdown>
157
+ )}
158
+ </Flexbox>
159
+ {sortedDisabledProviders.map((item) => (
70
160
  <ProviderItem {...item} key={item.id} onClick={onProviderSelect} />
71
161
  ))}
72
162
  </ScrollShadow>
@@ -1,12 +1,15 @@
1
- import { Button, Text } from '@lobehub/ui';
1
+ import { ActionIcon, Button, Dropdown, Icon, Text } from '@lobehub/ui';
2
+ import type { ItemType } from 'antd/es/menu/interface';
2
3
  import isEqual from 'fast-deep-equal';
3
- import { ChevronDown } from 'lucide-react';
4
- import { memo, useMemo, useState } from 'react';
4
+ import { ArrowDownUpIcon, ChevronDown, LucideCheck } from 'lucide-react';
5
+ import { memo, useCallback, useMemo, useState } from 'react';
5
6
  import { useTranslation } from 'react-i18next';
6
7
  import { Flexbox } from 'react-layout-kit';
7
8
 
8
9
  import { useAiInfraStore } from '@/store/aiInfra';
9
10
  import { aiModelSelectors } from '@/store/aiInfra/selectors';
11
+ import { useGlobalStore } from '@/store/global';
12
+ import { systemStatusSelectors } from '@/store/global/selectors';
10
13
 
11
14
  import ModelItem from './ModelItem';
12
15
 
@@ -14,10 +17,32 @@ interface DisabledModelsProps {
14
17
  activeTab: string;
15
18
  }
16
19
 
20
+ // Sort type enumeration
21
+ enum SortType {
22
+ Alphabetical = 'alphabetical',
23
+ AlphabeticalDesc = 'alphabeticalDesc',
24
+ Default = 'default',
25
+ ReleasedAt = 'releasedAt',
26
+ ReleasedAtDesc = 'releasedAtDesc',
27
+ }
28
+
17
29
  const DisabledModels = memo<DisabledModelsProps>(({ activeTab }) => {
18
30
  const { t } = useTranslation('modelProvider');
19
31
 
20
32
  const [showMore, setShowMore] = useState(false);
33
+
34
+ const [sortType, updateSystemStatus] = useGlobalStore((s) => [
35
+ systemStatusSelectors.disabledModelsSortType(s),
36
+ s.updateSystemStatus,
37
+ ]);
38
+
39
+ const updateSortType = useCallback(
40
+ (newSortType: SortType) => {
41
+ updateSystemStatus({ disabledModelsSortType: newSortType });
42
+ },
43
+ [updateSystemStatus],
44
+ );
45
+
21
46
  const disabledModels = useAiInfraStore(aiModelSelectors.disabledAiProviderModelList, isEqual);
22
47
 
23
48
  // Filter models based on active tab
@@ -26,18 +51,129 @@ const DisabledModels = memo<DisabledModelsProps>(({ activeTab }) => {
26
51
  return disabledModels.filter((model) => model.type === activeTab);
27
52
  }, [disabledModels, activeTab]);
28
53
 
29
- const displayModels = showMore ? filteredDisabledModels : filteredDisabledModels.slice(0, 10);
54
+ // Sort models based on sort type
55
+ const sortedDisabledModels = useMemo(() => {
56
+ const models = [...filteredDisabledModels];
57
+ switch (sortType) {
58
+ case SortType.Alphabetical: {
59
+ return models.sort((a, b) => {
60
+ const cmpDisplay = (a.displayName || a.id).localeCompare(b.displayName || b.id);
61
+ if (cmpDisplay !== 0) return cmpDisplay;
62
+ return a.id.localeCompare(b.id);
63
+ });
64
+ }
65
+ case SortType.AlphabeticalDesc: {
66
+ return models.sort((a, b) => {
67
+ const cmpDisplay = (b.displayName || b.id).localeCompare(a.displayName || a.id);
68
+ if (cmpDisplay !== 0) return cmpDisplay;
69
+ return b.id.localeCompare(a.id);
70
+ });
71
+ }
72
+ case SortType.ReleasedAt: {
73
+ return models.sort((a, b) => {
74
+ const aHasDate = !!a.releasedAt;
75
+ const bHasDate = !!b.releasedAt;
76
+
77
+ if (aHasDate && !bHasDate) return -1;
78
+ if (!aHasDate && bHasDate) return 1;
79
+ if (!aHasDate && !bHasDate) return 0;
80
+
81
+ return a.releasedAt!.localeCompare(b.releasedAt!);
82
+ });
83
+ }
84
+ case SortType.ReleasedAtDesc: {
85
+ return models.sort((a, b) => {
86
+ const aHasDate = !!a.releasedAt;
87
+ const bHasDate = !!b.releasedAt;
88
+
89
+ if (aHasDate && !bHasDate) return -1;
90
+ if (!aHasDate && bHasDate) return 1;
91
+ if (!aHasDate && !bHasDate) return 0;
92
+
93
+ return b.releasedAt!.localeCompare(a.releasedAt!);
94
+ });
95
+ }
96
+ case SortType.Default: {
97
+ return models;
98
+ }
99
+ default: {
100
+ return models;
101
+ }
102
+ }
103
+ }, [filteredDisabledModels, sortType]);
104
+
105
+ const displayModels = showMore ? sortedDisabledModels : sortedDisabledModels.slice(0, 10);
30
106
 
31
107
  return (
32
108
  filteredDisabledModels.length > 0 && (
33
109
  <Flexbox>
34
- <Text style={{ fontSize: 12, marginTop: 8 }} type={'secondary'}>
35
- {t('providerModels.list.disabled')}
36
- </Text>
110
+ <Flexbox align="center" horizontal justify="space-between">
111
+ <Text style={{ fontSize: 12, marginTop: 8 }} type={'secondary'}>
112
+ {t('providerModels.list.disabled')}
113
+ </Text>
114
+ {filteredDisabledModels.length > 1 && (
115
+ <Dropdown
116
+ menu={{
117
+ items: [
118
+ {
119
+ icon: sortType === SortType.Default ? <Icon icon={LucideCheck} /> : <div />,
120
+ key: 'default',
121
+ label: t('providerModels.list.disabledActions.sortDefault'),
122
+ onClick: () => updateSortType(SortType.Default),
123
+ },
124
+ {
125
+ type: 'divider',
126
+ },
127
+ {
128
+ icon:
129
+ sortType === SortType.Alphabetical ? <Icon icon={LucideCheck} /> : <div />,
130
+ key: 'alphabetical',
131
+ label: t('providerModels.list.disabledActions.sortAlphabetical'),
132
+ onClick: () => updateSortType(SortType.Alphabetical),
133
+ },
134
+ {
135
+ icon:
136
+ sortType === SortType.AlphabeticalDesc ? (
137
+ <Icon icon={LucideCheck} />
138
+ ) : (
139
+ <div />
140
+ ),
141
+ key: 'alphabeticalDesc',
142
+ label: t('providerModels.list.disabledActions.sortAlphabeticalDesc'),
143
+ onClick: () => updateSortType(SortType.AlphabeticalDesc),
144
+ },
145
+ {
146
+ type: 'divider',
147
+ },
148
+ {
149
+ icon: sortType === SortType.ReleasedAt ? <Icon icon={LucideCheck} /> : <div />,
150
+ key: 'releasedAt',
151
+ label: t('providerModels.list.disabledActions.sortReleasedAt'),
152
+ onClick: () => updateSortType(SortType.ReleasedAt),
153
+ },
154
+ {
155
+ icon:
156
+ sortType === SortType.ReleasedAtDesc ? <Icon icon={LucideCheck} /> : <div />,
157
+ key: 'releasedAtDesc',
158
+ label: t('providerModels.list.disabledActions.sortReleasedAtDesc'),
159
+ onClick: () => updateSortType(SortType.ReleasedAtDesc),
160
+ },
161
+ ] as ItemType[],
162
+ }}
163
+ trigger={['click']}
164
+ >
165
+ <ActionIcon
166
+ icon={ArrowDownUpIcon}
167
+ size={'small'}
168
+ title={t('providerModels.list.disabledActions.sort')}
169
+ />
170
+ </Dropdown>
171
+ )}
172
+ </Flexbox>
37
173
  {displayModels.map((item) => (
38
174
  <ModelItem {...item} key={item.id} />
39
175
  ))}
40
- {!showMore && filteredDisabledModels.length > 10 && (
176
+ {!showMore && sortedDisabledModels.length > 10 && (
41
177
  <Button
42
178
  block
43
179
  icon={ChevronDown}
@@ -3,7 +3,7 @@
3
3
  import { memo } from 'react';
4
4
  import { Flexbox } from 'react-layout-kit';
5
5
 
6
- import ChatInput from '@/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput';
6
+ import ChatInput from '@/app/[variants]/(main)/chat/components/conversation/features/ChatInput';
7
7
  import { useChatGroupStore } from '@/store/chatGroup';
8
8
 
9
9
  import ThreadChatList from './ThreadChatList';
@@ -2,7 +2,7 @@ import isEqual from 'fast-deep-equal';
2
2
  import { useEffect } from 'react';
3
3
  import { useHotkeysContext } from 'react-hotkeys-hook';
4
4
 
5
- import { useSend } from '@/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/useSend';
5
+ import { useSend } from '@/app/[variants]/(main)/chat/components/conversation/features/ChatInput/useSend';
6
6
  import { useClearCurrentMessages } from '@/features/ChatInput/ActionBar/Clear';
7
7
  import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
8
8
  import { useActionSWR } from '@/libs/swr';
@@ -202,6 +202,12 @@ export default {
202
202
  all: '全部',
203
203
  list: {
204
204
  disabled: '未启用',
205
+ disabledActions: {
206
+ sort: '排序方式',
207
+ sortAlphabetical: '按字母排序',
208
+ sortAlphabeticalDesc: '按字母倒序排序',
209
+ sortDefault: '默认排序',
210
+ },
205
211
  enabled: '已启用',
206
212
  },
207
213
  notFound: '未找到搜索结果',
@@ -399,7 +405,15 @@ export default {
399
405
  list: {
400
406
  addNew: '添加模型',
401
407
  disabled: '未启用',
402
- disabledActions: { showMore: '显示全部' },
408
+ disabledActions: {
409
+ showMore: '显示全部',
410
+ sort: '排序方式',
411
+ sortAlphabetical: '按字母排序',
412
+ sortAlphabeticalDesc: '按字母倒序排序',
413
+ sortDefault: '默认排序',
414
+ sortReleasedAt: '按最早发布时间排序',
415
+ sortReleasedAtDesc: '按最新发布时间排序',
416
+ },
403
417
  empty: {
404
418
  desc: '请创建自定义模型或拉取模型后开始使用吧',
405
419
  title: '暂无可用模型',
@@ -0,0 +1,162 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { ManualInstallationChecker } from './ManualInstallationChecker';
4
+
5
+ describe('ManualInstallationChecker', () => {
6
+ let checker: ManualInstallationChecker;
7
+
8
+ beforeEach(() => {
9
+ checker = new ManualInstallationChecker();
10
+ });
11
+
12
+ describe('checkPackageInstalled', () => {
13
+ it('should always return not installed for manual packages', async () => {
14
+ const result = await checker.checkPackageInstalled({ packageName: 'manual-package' });
15
+
16
+ expect(result).toEqual({
17
+ installed: false,
18
+ packageName: 'manual-package',
19
+ });
20
+ });
21
+
22
+ it('should handle packageName with repository URL', async () => {
23
+ const result = await checker.checkPackageInstalled({
24
+ packageName: 'my-custom-tool',
25
+ repositoryUrlToClone: 'https://github.com/user/repo.git',
26
+ });
27
+
28
+ expect(result.installed).toBe(false);
29
+ expect(result.packageName).toBe('my-custom-tool');
30
+ });
31
+
32
+ it('should use repositoryUrlToClone as packageName when packageName is not provided', async () => {
33
+ const result = await checker.checkPackageInstalled({
34
+ repositoryUrlToClone: 'https://github.com/user/custom-tool.git',
35
+ });
36
+
37
+ expect(result).toEqual({
38
+ installed: false,
39
+ packageName: 'https://github.com/user/custom-tool.git',
40
+ });
41
+ });
42
+
43
+ it('should return empty packageName when neither packageName nor repositoryUrlToClone provided', async () => {
44
+ const result = await checker.checkPackageInstalled({});
45
+
46
+ expect(result).toEqual({
47
+ installed: false,
48
+ packageName: '',
49
+ });
50
+ });
51
+
52
+ it('should handle undefined packageName', async () => {
53
+ const result = await checker.checkPackageInstalled({ packageName: undefined });
54
+
55
+ expect(result).toEqual({
56
+ installed: false,
57
+ packageName: '',
58
+ });
59
+ });
60
+
61
+ it('should handle empty string packageName', async () => {
62
+ const result = await checker.checkPackageInstalled({ packageName: '' });
63
+
64
+ expect(result).toEqual({
65
+ installed: false,
66
+ packageName: '',
67
+ });
68
+ });
69
+
70
+ it('should handle empty string repositoryUrlToClone as fallback', async () => {
71
+ const result = await checker.checkPackageInstalled({
72
+ packageName: '',
73
+ repositoryUrlToClone: '',
74
+ });
75
+
76
+ expect(result).toEqual({
77
+ installed: false,
78
+ packageName: '',
79
+ });
80
+ });
81
+
82
+ it('should prioritize packageName over repositoryUrlToClone', async () => {
83
+ const result = await checker.checkPackageInstalled({
84
+ packageName: 'my-tool',
85
+ repositoryUrlToClone: 'https://github.com/user/repo.git',
86
+ });
87
+
88
+ expect(result.packageName).toBe('my-tool');
89
+ expect(result.installed).toBe(false);
90
+ });
91
+
92
+ it('should handle complex package names', async () => {
93
+ const result = await checker.checkPackageInstalled({
94
+ packageName: '@scope/package-name-with-special.chars_123',
95
+ });
96
+
97
+ expect(result).toEqual({
98
+ installed: false,
99
+ packageName: '@scope/package-name-with-special.chars_123',
100
+ });
101
+ });
102
+
103
+ it('should handle SSH repository URLs', async () => {
104
+ const result = await checker.checkPackageInstalled({
105
+ repositoryUrlToClone: 'git@github.com:user/repo.git',
106
+ });
107
+
108
+ expect(result).toEqual({
109
+ installed: false,
110
+ packageName: 'git@github.com:user/repo.git',
111
+ });
112
+ });
113
+
114
+ it('should handle local file paths', async () => {
115
+ const result = await checker.checkPackageInstalled({
116
+ packageName: '/usr/local/custom-tool',
117
+ });
118
+
119
+ expect(result).toEqual({
120
+ installed: false,
121
+ packageName: '/usr/local/custom-tool',
122
+ });
123
+ });
124
+
125
+ it('should handle Windows-style paths', async () => {
126
+ const result = await checker.checkPackageInstalled({
127
+ packageName: 'C:\\Program Files\\CustomTool',
128
+ });
129
+
130
+ expect(result).toEqual({
131
+ installed: false,
132
+ packageName: 'C:\\Program Files\\CustomTool',
133
+ });
134
+ });
135
+
136
+ it('should handle package names with unicode characters', async () => {
137
+ const result = await checker.checkPackageInstalled({
138
+ packageName: 'my-tool-日本語',
139
+ });
140
+
141
+ expect(result).toEqual({
142
+ installed: false,
143
+ packageName: 'my-tool-日本語',
144
+ });
145
+ });
146
+
147
+ it('should always return false for installed regardless of input', async () => {
148
+ const testCases = [
149
+ { packageName: 'package1' },
150
+ { packageName: 'package2', repositoryUrlToClone: 'https://example.com/repo.git' },
151
+ { repositoryUrlToClone: 'https://example.com/repo2.git' },
152
+ {},
153
+ { packageName: '' },
154
+ ];
155
+
156
+ for (const testCase of testCases) {
157
+ const result = await checker.checkPackageInstalled(testCase);
158
+ expect(result.installed).toBe(false);
159
+ }
160
+ });
161
+ });
162
+ });