@lobehub/lobehub 2.0.0-next.220 → 2.0.0-next.222

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 (123) hide show
  1. package/.github/workflows/claude-auto-testing.yml +6 -3
  2. package/.github/workflows/claude-dedupe-issues.yml +8 -1
  3. package/.github/workflows/claude-issue-triage.yml +8 -14
  4. package/.github/workflows/claude-translate-comments.yml +6 -3
  5. package/.github/workflows/claude-translator.yml +12 -14
  6. package/.github/workflows/claude.yml +10 -20
  7. package/.github/workflows/test.yml +26 -0
  8. package/.i18nrc.js +4 -2
  9. package/CHANGELOG.md +66 -0
  10. package/apps/desktop/src/main/core/browser/Browser.ts +48 -20
  11. package/apps/desktop/src/main/core/browser/__tests__/Browser.test.ts +1 -0
  12. package/changelog/v1.json +21 -0
  13. package/docs/glossary.md +11 -0
  14. package/locales/zh-CN/components.json +1 -0
  15. package/locales/zh-CN/topic.json +5 -5
  16. package/package.json +3 -3
  17. package/packages/const/src/index.ts +0 -1
  18. package/packages/memory-user-memory/package.json +1 -0
  19. package/packages/memory-user-memory/src/extractors/context.test.ts +3 -2
  20. package/packages/memory-user-memory/src/extractors/experience.test.ts +3 -2
  21. package/packages/memory-user-memory/src/extractors/identity.test.ts +23 -6
  22. package/packages/memory-user-memory/src/extractors/preference.test.ts +3 -2
  23. package/packages/memory-user-memory/vitest.config.ts +4 -0
  24. package/packages/model-runtime/src/providers/replicate/index.ts +1 -1
  25. package/packages/ssrf-safe-fetch/index.test.ts +2 -2
  26. package/packages/ssrf-safe-fetch/package.json +3 -2
  27. package/packages/types/src/serverConfig.ts +2 -0
  28. package/packages/utils/package.json +1 -1
  29. package/packages/utils/src/client/xor-obfuscation.test.ts +32 -32
  30. package/packages/utils/src/client/xor-obfuscation.ts +3 -4
  31. package/packages/utils/src/imageToBase64.ts +1 -1
  32. package/packages/utils/src/server/__tests__/auth.test.ts +1 -1
  33. package/packages/utils/src/server/auth.ts +1 -1
  34. package/packages/utils/src/server/xor.test.ts +9 -7
  35. package/packages/utils/src/server/xor.ts +1 -1
  36. package/packages/web-crawler/package.json +1 -1
  37. package/packages/web-crawler/src/crawImpl/__tests__/naive.test.ts +1 -1
  38. package/packages/web-crawler/src/crawImpl/naive.ts +1 -1
  39. package/scripts/prebuild.mts +58 -1
  40. package/src/app/(backend)/api/auth/[...all]/route.ts +2 -1
  41. package/src/app/(backend)/middleware/auth/index.ts +3 -3
  42. package/src/app/(backend)/middleware/auth/utils.test.ts +1 -1
  43. package/src/app/(backend)/middleware/auth/utils.ts +1 -1
  44. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +2 -2
  45. package/src/app/(backend)/webapi/models/[provider]/route.test.ts +1 -1
  46. package/src/app/(backend)/webapi/plugin/gateway/route.ts +1 -1
  47. package/src/app/(backend)/webapi/proxy/route.ts +1 -1
  48. package/src/app/[variants]/(auth)/login/[[...login]]/page.tsx +1 -1
  49. package/src/app/[variants]/(auth)/reset-password/layout.tsx +1 -1
  50. package/src/app/[variants]/(auth)/signin/layout.tsx +1 -1
  51. package/src/app/[variants]/(auth)/signin/useSignIn.ts +2 -2
  52. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +1 -1
  53. package/src/app/[variants]/(auth)/signup/[[...signup]]/useSignUp.tsx +12 -6
  54. package/src/app/[variants]/(auth)/verify-email/layout.tsx +1 -1
  55. package/src/app/[variants]/(main)/settings/profile/features/AvatarRow.tsx +1 -1
  56. package/src/app/[variants]/(main)/settings/security/index.tsx +1 -1
  57. package/src/app/[variants]/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +1 -1
  58. package/src/app/[variants]/(mobile)/me/(home)/__tests__/useCategory.test.tsx +1 -1
  59. package/src/app/[variants]/(mobile)/me/(home)/features/UserBanner.tsx +1 -1
  60. package/src/app/[variants]/(mobile)/settings/_layout/Header.tsx +1 -1
  61. package/src/components/ModelSelect/index.tsx +103 -72
  62. package/src/envs/auth.ts +30 -9
  63. package/src/features/Conversation/Messages/AssistantGroup/components/EditState.tsx +15 -32
  64. package/src/features/Conversation/Messages/AssistantGroup/index.tsx +9 -7
  65. package/src/features/EditorModal/EditorCanvas.tsx +31 -50
  66. package/src/features/EditorModal/TextareCanvas.tsx +3 -1
  67. package/src/features/EditorModal/index.tsx +14 -4
  68. package/src/features/ModelSwitchPanel/components/Footer.tsx +42 -0
  69. package/src/features/ModelSwitchPanel/components/List/MultipleProvidersModelItem.tsx +103 -0
  70. package/src/features/ModelSwitchPanel/components/List/SingleProviderModelItem.tsx +24 -0
  71. package/src/features/ModelSwitchPanel/components/List/VirtualItemRenderer.tsx +180 -0
  72. package/src/features/ModelSwitchPanel/components/List/index.tsx +99 -0
  73. package/src/features/ModelSwitchPanel/components/PanelContent.tsx +77 -0
  74. package/src/features/ModelSwitchPanel/components/Toolbar.tsx +54 -0
  75. package/src/features/ModelSwitchPanel/const.ts +29 -0
  76. package/src/features/ModelSwitchPanel/hooks/useBuildVirtualItems.ts +122 -0
  77. package/src/features/ModelSwitchPanel/hooks/useCurrentModelName.ts +18 -0
  78. package/src/features/ModelSwitchPanel/hooks/useDelayedRender.ts +18 -0
  79. package/src/features/ModelSwitchPanel/hooks/useModelAndProvider.ts +14 -0
  80. package/src/features/ModelSwitchPanel/hooks/usePanelHandlers.ts +33 -0
  81. package/src/features/ModelSwitchPanel/hooks/usePanelSize.ts +33 -0
  82. package/src/features/ModelSwitchPanel/hooks/usePanelState.ts +20 -0
  83. package/src/features/ModelSwitchPanel/index.tsx +25 -706
  84. package/src/features/ModelSwitchPanel/styles.ts +58 -0
  85. package/src/features/ModelSwitchPanel/types.ts +73 -0
  86. package/src/features/ModelSwitchPanel/utils.ts +24 -0
  87. package/src/features/User/UserPanel/PanelContent.tsx +1 -1
  88. package/src/features/User/__tests__/PanelContent.test.tsx +1 -1
  89. package/src/features/User/__tests__/UserAvatar.test.tsx +1 -1
  90. package/src/features/User/__tests__/useMenu.test.tsx +1 -1
  91. package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -1
  92. package/src/libs/better-auth/auth-client.ts +7 -3
  93. package/src/libs/better-auth/define-config.ts +2 -2
  94. package/src/libs/next/proxy/define-config.ts +1 -2
  95. package/src/libs/oidc-provider/provider.test.ts +1 -1
  96. package/src/libs/trpc/async/context.ts +1 -1
  97. package/src/libs/trpc/lambda/context.ts +7 -8
  98. package/src/libs/trpc/middleware/userAuth.ts +1 -1
  99. package/src/libs/trusted-client/getSessionUser.ts +1 -1
  100. package/src/locales/default/components.ts +1 -0
  101. package/src/server/globalConfig/index.ts +2 -0
  102. package/src/server/routers/async/caller.ts +1 -1
  103. package/src/server/routers/lambda/__tests__/user.test.ts +2 -2
  104. package/src/server/routers/lambda/notebook.ts +4 -2
  105. package/src/server/routers/lambda/user.ts +2 -1
  106. package/src/services/_auth.ts +3 -3
  107. package/src/services/chat/index.ts +1 -1
  108. package/src/services/chat/mecha/contextEngineering.ts +1 -1
  109. package/src/services/notebook.ts +2 -0
  110. package/src/store/global/initialState.ts +10 -0
  111. package/src/store/global/selectors/systemStatus.ts +5 -0
  112. package/src/store/serverConfig/selectors.ts +5 -1
  113. package/src/store/tool/slices/builtin/executors/lobe-web-browsing.ts +2 -0
  114. package/src/store/tool/slices/mcpStore/action.ts +74 -75
  115. package/src/store/user/slices/auth/action.test.ts +1 -1
  116. package/src/store/user/slices/auth/action.ts +1 -1
  117. package/src/store/user/slices/auth/initialState.ts +1 -1
  118. package/src/store/user/slices/auth/selectors.test.ts +1 -1
  119. package/src/store/user/slices/auth/selectors.ts +1 -1
  120. package/src/store/user/slices/common/action.ts +1 -1
  121. package/src/store/userMemory/slices/context/action.ts +6 -6
  122. package/glossary.json +0 -8
  123. package/packages/const/src/auth.ts +0 -14
@@ -2,7 +2,8 @@
2
2
 
3
3
  import type { AssistantContentBlock } from '@lobechat/types';
4
4
  import isEqual from 'fast-deep-equal';
5
- import { type MouseEventHandler, memo, useCallback, useMemo } from 'react';
5
+ import dynamic from 'next/dynamic';
6
+ import { type MouseEventHandler, Suspense, memo, useCallback, useMemo } from 'react';
6
7
 
7
8
  import { MESSAGE_ACTION_BAR_PORTAL_ATTRIBUTES } from '@/const/messageActionPortal';
8
9
  import { ChatItem } from '@/features/Conversation/ChatItem';
@@ -21,9 +22,12 @@ import {
21
22
  import FileListViewer from '../User/components/FileListViewer';
22
23
  import Usage from '../components/Extras/Usage';
23
24
  import MessageBranch from '../components/MessageBranch';
24
- import EditState from './components/EditState';
25
25
  import Group from './components/Group';
26
26
 
27
+ const EditState = dynamic(() => import('./components/EditState'), {
28
+ ssr: false,
29
+ });
30
+
27
31
  const actionBarHolder = (
28
32
  <div
29
33
  {...{ [MESSAGE_ACTION_BAR_PORTAL_ATTRIBUTES.assistantGroup]: '' }}
@@ -92,11 +96,6 @@ const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing, isLat
92
96
  }
93
97
  }, [isInbox]);
94
98
 
95
- // If editing, show edit state
96
- if (editing && contentId) {
97
- return <EditState content={lastAssistantMsg?.content} id={contentId} />;
98
- }
99
-
100
99
  return (
101
100
  <ChatItem
102
101
  actions={
@@ -139,6 +138,9 @@ const GroupMessage = memo<GroupMessageProps>(({ id, index, disableEditing, isLat
139
138
  {model && (
140
139
  <Usage model={model} performance={performance} provider={provider!} usage={usage} />
141
140
  )}
141
+ <Suspense fallback={null}>
142
+ {editing && contentId && <EditState content={lastAssistantMsg?.content} id={contentId} />}
143
+ </Suspense>
142
144
  </ChatItem>
143
145
  );
144
146
  }, isEqual);
@@ -1,4 +1,5 @@
1
1
  import {
2
+ IEditor,
2
3
  ReactCodePlugin,
3
4
  ReactCodemirrorPlugin,
4
5
  ReactHRPlugin,
@@ -7,72 +8,52 @@ import {
7
8
  ReactMathPlugin,
8
9
  ReactTablePlugin,
9
10
  } from '@lobehub/editor';
10
- import { Editor, useEditor } from '@lobehub/editor/react';
11
+ import { Editor } from '@lobehub/editor/react';
11
12
  import { Flexbox } from '@lobehub/ui';
12
13
  import { FC } from 'react';
13
14
 
14
15
  import TypoBar from './Typobar';
15
16
 
16
17
  interface EditorCanvasProps {
17
- onChange?: (value: string) => void;
18
- value?: string;
18
+ defaultValue?: string;
19
+ editor?: IEditor;
19
20
  }
20
21
 
21
- const EditorCanvas: FC<EditorCanvasProps> = ({ value, onChange }) => {
22
- const editor = useEditor();
22
+ const EditorCanvas: FC<EditorCanvasProps> = ({ defaultValue, editor }) => {
23
23
  return (
24
24
  <>
25
25
  <TypoBar editor={editor} />
26
26
  <Flexbox
27
- onClick={() => {
28
- editor?.focus();
29
- }}
30
27
  padding={16}
31
28
  style={{ cursor: 'text', maxHeight: '80vh', minHeight: '50vh', overflowY: 'auto' }}
32
29
  >
33
- <div
34
- onClick={(e) => {
35
- e.stopPropagation();
36
- e.preventDefault();
30
+ <Editor
31
+ autoFocus
32
+ content={''}
33
+ editor={editor}
34
+ onInit={(editor) => {
35
+ if (!editor || !defaultValue) return;
36
+ try {
37
+ editor?.setDocument('markdown', defaultValue);
38
+ } catch (e) {
39
+ console.error('setDocument error:', e);
40
+ }
37
41
  }}
38
- >
39
- <Editor
40
- autoFocus
41
- content={''}
42
- editor={editor}
43
- onInit={(editor) => {
44
- if (!editor || !value) return;
45
- try {
46
- editor?.setDocument('markdown', value);
47
- } catch (e) {
48
- console.error('setDocument error:', e);
49
- }
50
- }}
51
- onTextChange={(editor) => {
52
- try {
53
- const newValue = editor.getDocument('markdown') as unknown as string;
54
- onChange?.(newValue);
55
- } catch (e) {
56
- console.error('getDocument error:', e);
57
- onChange?.('');
58
- }
59
- }}
60
- plugins={[
61
- ReactListPlugin,
62
- ReactCodePlugin,
63
- ReactCodemirrorPlugin,
64
- ReactHRPlugin,
65
- ReactLinkPlugin,
66
- ReactTablePlugin,
67
- ReactMathPlugin,
68
- ]}
69
- style={{
70
- paddingBottom: 120,
71
- }}
72
- type={'text'}
73
- variant={'chat'}
74
- />
75
- </div>
42
+ plugins={[
43
+ ReactListPlugin,
44
+ ReactCodePlugin,
45
+ ReactCodemirrorPlugin,
46
+ ReactHRPlugin,
47
+ ReactLinkPlugin,
48
+ ReactTablePlugin,
49
+ ReactMathPlugin,
50
+ ]}
51
+ style={{
52
+ paddingBottom: 120,
53
+ }}
54
+ type={'text'}
55
+ variant={'chat'}
56
+ />
76
57
  </Flexbox>
77
58
  </>
78
59
  );
@@ -2,13 +2,15 @@ import { TextArea } from '@lobehub/ui';
2
2
  import { FC } from 'react';
3
3
 
4
4
  interface EditorCanvasProps {
5
+ defaultValue?: string;
5
6
  onChange?: (value: string) => void;
6
7
  value?: string;
7
8
  }
8
9
 
9
- const EditorCanvas: FC<EditorCanvasProps> = ({ value, onChange }) => {
10
+ const EditorCanvas: FC<EditorCanvasProps> = ({ defaultValue, value, onChange }) => {
10
11
  return (
11
12
  <TextArea
13
+ defaultValue={defaultValue}
12
14
  onChange={(e) => {
13
15
  onChange?.(e.target.value);
14
16
  }}
@@ -1,3 +1,4 @@
1
+ import { useEditor } from '@lobehub/editor/react';
1
2
  import { Modal, ModalProps, createRawModal } from '@lobehub/ui';
2
3
  import { memo, useState } from 'react';
3
4
  import { useTranslation } from 'react-i18next';
@@ -18,8 +19,7 @@ export const EditorModal = memo<EditorModalProps>(({ value, onConfirm, ...rest }
18
19
  const { t } = useTranslation('common');
19
20
  const [v, setV] = useState(value);
20
21
  const enableRichRender = useUserStore(labPreferSelectors.enableInputMarkdown);
21
-
22
- const Canvas = enableRichRender ? EditorCanvas : TextareCanvas;
22
+ const editor = useEditor();
23
23
 
24
24
  return (
25
25
  <Modal
@@ -30,7 +30,13 @@ export const EditorModal = memo<EditorModalProps>(({ value, onConfirm, ...rest }
30
30
  okText={t('ok')}
31
31
  onOk={async () => {
32
32
  setConfirmLoading(true);
33
- await onConfirm?.(v || '');
33
+ let finalValue;
34
+ if (enableRichRender) {
35
+ finalValue = editor?.getDocument('markdown') as unknown as string;
36
+ } else {
37
+ finalValue = v;
38
+ }
39
+ await onConfirm?.(finalValue || '');
34
40
  setConfirmLoading(false);
35
41
  }}
36
42
  styles={{
@@ -43,7 +49,11 @@ export const EditorModal = memo<EditorModalProps>(({ value, onConfirm, ...rest }
43
49
  width={'min(90vw, 920px)'}
44
50
  {...rest}
45
51
  >
46
- <Canvas onChange={(v) => setV(v)} value={v} />
52
+ {enableRichRender ? (
53
+ <EditorCanvas defaultValue={value} editor={editor} />
54
+ ) : (
55
+ <TextareCanvas defaultValue={value} onChange={(v) => setV(v)} value={v} />
56
+ )}
47
57
  </Modal>
48
58
  );
49
59
  });
@@ -0,0 +1,42 @@
1
+ import { Block, Flexbox, Icon } from '@lobehub/ui';
2
+ import { cssVar } from 'antd-style';
3
+ import { LucideArrowRight, LucideBolt } from 'lucide-react';
4
+ import type { FC } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { useNavigate } from 'react-router-dom';
7
+
8
+ import { styles } from '../styles';
9
+
10
+ interface FooterProps {
11
+ onClose: () => void;
12
+ }
13
+
14
+ export const Footer: FC<FooterProps> = ({ onClose }) => {
15
+ const { t } = useTranslation('components');
16
+ const navigate = useNavigate();
17
+
18
+ return (
19
+ <Flexbox className={styles.footer} padding={4}>
20
+ <Block
21
+ clickable
22
+ gap={8}
23
+ horizontal
24
+ onClick={() => {
25
+ navigate('/settings/provider/all');
26
+ onClose();
27
+ }}
28
+ paddingBlock={8}
29
+ paddingInline={12}
30
+ variant={'borderless'}
31
+ >
32
+ <Flexbox align={'center'} gap={8} horizontal style={{ flex: 1 }}>
33
+ <Icon icon={LucideBolt} size={'small'} />
34
+ {t('ModelSwitchPanel.manageProvider')}
35
+ </Flexbox>
36
+ <Icon color={cssVar.colorTextDescription} icon={LucideArrowRight} size={'small'} />
37
+ </Block>
38
+ </Flexbox>
39
+ );
40
+ };
41
+
42
+ Footer.displayName = 'Footer';
@@ -0,0 +1,103 @@
1
+ import { ActionIcon, Dropdown } from '@lobehub/ui';
2
+ import { MenuItemType } from 'antd/es/menu/interface';
3
+ import { LucideBolt } from 'lucide-react';
4
+ import { memo, useMemo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { useNavigate } from 'react-router-dom';
7
+ import urlJoin from 'url-join';
8
+
9
+ import { ModelItemRender, ProviderItemRender } from '@/components/ModelSelect';
10
+
11
+ import { styles } from '../../styles';
12
+ import type { ModelWithProviders } from '../../types';
13
+ import { menuKey } from '../../utils';
14
+
15
+ interface MultipleProvidersModelItemProps {
16
+ activeKey: string;
17
+ data: ModelWithProviders;
18
+ newLabel: string;
19
+ onClose: () => void;
20
+ onModelChange: (modelId: string, providerId: string) => Promise<void>;
21
+ }
22
+
23
+ export const MultipleProvidersModelItem = memo<MultipleProvidersModelItemProps>(
24
+ ({ activeKey, data, newLabel, onModelChange, onClose }) => {
25
+ const { t } = useTranslation('components');
26
+ const navigate = useNavigate();
27
+
28
+ const items = useMemo(
29
+ () =>
30
+ [
31
+ {
32
+ key: 'header',
33
+ label: t('ModelSwitchPanel.useModelFrom'),
34
+ type: 'group',
35
+ },
36
+ ...data.providers.map((p) => {
37
+ return {
38
+ extra: (
39
+ <ActionIcon
40
+ className={'settings-icon'}
41
+ icon={LucideBolt}
42
+ onClick={(e) => {
43
+ e.preventDefault();
44
+ e.stopPropagation();
45
+ const url = urlJoin('/settings/provider', p.id || 'all');
46
+ if (e.ctrlKey || e.metaKey) {
47
+ window.open(url, '_blank');
48
+ } else {
49
+ navigate(url);
50
+ }
51
+ }}
52
+ size={'small'}
53
+ title={t('ModelSwitchPanel.goToSettings')}
54
+ />
55
+ ),
56
+ key: menuKey(p.id, data.model.id),
57
+ label: (
58
+ <ProviderItemRender
59
+ logo={p.logo}
60
+ name={p.name}
61
+ provider={p.id}
62
+ size={20}
63
+ source={p.source}
64
+ type={'avatar'}
65
+ />
66
+ ),
67
+ onClick: async () => {
68
+ onModelChange(data.model.id, p.id);
69
+ onClose();
70
+ },
71
+ };
72
+ }),
73
+ ] as MenuItemType[],
74
+ [data.model.id, data.providers, navigate, onModelChange, onClose, t],
75
+ );
76
+
77
+ return (
78
+ <Dropdown
79
+ align={{ offset: [12, -48] }}
80
+ arrow={false}
81
+ classNames={{
82
+ item: styles.menuItem,
83
+ }}
84
+ menu={{
85
+ items,
86
+ selectedKeys: [activeKey],
87
+ }}
88
+ // @ts-ignore
89
+ placement="rightTop"
90
+ >
91
+ <ModelItemRender
92
+ {...data.model}
93
+ {...data.model.abilities}
94
+ infoTagTooltip={false}
95
+ newBadgeLabel={newLabel}
96
+ showInfoTag={true}
97
+ />
98
+ </Dropdown>
99
+ );
100
+ },
101
+ );
102
+
103
+ MultipleProvidersModelItem.displayName = 'MultipleProvidersModelItem';
@@ -0,0 +1,24 @@
1
+ import { memo } from 'react';
2
+
3
+ import { ModelItemRender } from '@/components/ModelSelect';
4
+
5
+ import type { ModelWithProviders } from '../../types';
6
+
7
+ interface SingleProviderModelItemProps {
8
+ data: ModelWithProviders;
9
+ newLabel: string;
10
+ }
11
+
12
+ export const SingleProviderModelItem = memo<SingleProviderModelItemProps>(({ data, newLabel }) => {
13
+ return (
14
+ <ModelItemRender
15
+ {...data.model}
16
+ {...data.model.abilities}
17
+ infoTagTooltip={false}
18
+ newBadgeLabel={newLabel}
19
+ showInfoTag={true}
20
+ />
21
+ );
22
+ });
23
+
24
+ SingleProviderModelItem.displayName = 'SingleProviderModelItem';
@@ -0,0 +1,180 @@
1
+ import { ActionIcon, Block, Flexbox, Icon } from '@lobehub/ui';
2
+ import { cssVar } from 'antd-style';
3
+ import { LucideArrowRight, LucideBolt } from 'lucide-react';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { useNavigate } from 'react-router-dom';
7
+ import urlJoin from 'url-join';
8
+
9
+ import { ModelItemRender, ProviderItemRender } from '@/components/ModelSelect';
10
+
11
+ import { styles } from '../../styles';
12
+ import type { VirtualItem } from '../../types';
13
+ import { menuKey } from '../../utils';
14
+ import { MultipleProvidersModelItem } from './MultipleProvidersModelItem';
15
+ import { SingleProviderModelItem } from './SingleProviderModelItem';
16
+
17
+ interface VirtualItemRendererProps {
18
+ activeKey: string;
19
+ item: VirtualItem;
20
+ newLabel: string;
21
+ onClose: () => void;
22
+ onModelChange: (modelId: string, providerId: string) => Promise<void>;
23
+ }
24
+
25
+ export const VirtualItemRenderer = memo<VirtualItemRendererProps>(
26
+ ({ activeKey, item, newLabel, onModelChange, onClose }) => {
27
+ const { t } = useTranslation('components');
28
+ const navigate = useNavigate();
29
+
30
+ switch (item.type) {
31
+ case 'no-provider': {
32
+ return (
33
+ <Block
34
+ className={styles.menuItem}
35
+ clickable
36
+ gap={8}
37
+ horizontal
38
+ key="no-provider"
39
+ onClick={() => navigate('/settings/provider/all')}
40
+ style={{ color: cssVar.colorTextTertiary }}
41
+ variant={'borderless'}
42
+ >
43
+ {t('ModelSwitchPanel.emptyProvider')}
44
+ <Icon icon={LucideArrowRight} />
45
+ </Block>
46
+ );
47
+ }
48
+
49
+ case 'group-header': {
50
+ return (
51
+ <Flexbox
52
+ className={styles.groupHeader}
53
+ horizontal
54
+ justify="space-between"
55
+ key={`header-${item.provider.id}`}
56
+ paddingBlock={'12px 4px'}
57
+ paddingInline={'12px 8px'}
58
+ >
59
+ <ProviderItemRender
60
+ logo={item.provider.logo}
61
+ name={item.provider.name}
62
+ provider={item.provider.id}
63
+ source={item.provider.source}
64
+ />
65
+ <ActionIcon
66
+ className="settings-icon"
67
+ icon={LucideBolt}
68
+ onClick={(e) => {
69
+ e.preventDefault();
70
+ e.stopPropagation();
71
+ const url = urlJoin('/settings/provider', item.provider.id || 'all');
72
+ if (e.ctrlKey || e.metaKey) {
73
+ window.open(url, '_blank');
74
+ } else {
75
+ navigate(url);
76
+ }
77
+ }}
78
+ size={'small'}
79
+ title={t('ModelSwitchPanel.goToSettings')}
80
+ />
81
+ </Flexbox>
82
+ );
83
+ }
84
+
85
+ case 'empty-model': {
86
+ return (
87
+ <Flexbox
88
+ className={styles.menuItem}
89
+ gap={8}
90
+ horizontal
91
+ key={`empty-${item.provider.id}`}
92
+ onClick={() => navigate(`/settings/provider/${item.provider.id}`)}
93
+ style={{ color: cssVar.colorTextTertiary }}
94
+ >
95
+ {t('ModelSwitchPanel.emptyModel')}
96
+ <Icon icon={LucideArrowRight} />
97
+ </Flexbox>
98
+ );
99
+ }
100
+
101
+ case 'provider-model-item': {
102
+ const key = menuKey(item.provider.id, item.model.id);
103
+ const isActive = key === activeKey;
104
+
105
+ return (
106
+ <Block
107
+ className={styles.menuItem}
108
+ clickable
109
+ key={key}
110
+ onClick={async () => {
111
+ onModelChange(item.model.id, item.provider.id);
112
+ onClose();
113
+ }}
114
+ variant={isActive ? 'filled' : 'borderless'}
115
+ >
116
+ <ModelItemRender
117
+ {...item.model}
118
+ {...item.model.abilities}
119
+ infoTagTooltip={false}
120
+ newBadgeLabel={newLabel}
121
+ showInfoTag
122
+ />
123
+ </Block>
124
+ );
125
+ }
126
+
127
+ case 'model-item-single': {
128
+ const singleProvider = item.data.providers[0];
129
+ const key = menuKey(singleProvider.id, item.data.model.id);
130
+ const isActive = key === activeKey;
131
+
132
+ return (
133
+ <Block
134
+ className={styles.menuItem}
135
+ clickable
136
+ key={key}
137
+ onClick={async () => {
138
+ onModelChange(item.data.model.id, singleProvider.id);
139
+ onClose();
140
+ }}
141
+ variant={isActive ? 'filled' : 'borderless'}
142
+ >
143
+ <SingleProviderModelItem data={item.data} newLabel={newLabel} />
144
+ </Block>
145
+ );
146
+ }
147
+
148
+ case 'model-item-multiple': {
149
+ // Check if any provider of this model is active
150
+ const activeProvider = item.data.providers.find(
151
+ (p) => menuKey(p.id, item.data.model.id) === activeKey,
152
+ );
153
+ const isActive = !!activeProvider;
154
+
155
+ return (
156
+ <Block
157
+ className={styles.menuItem}
158
+ clickable
159
+ key={item.data.displayName}
160
+ variant={isActive ? 'filled' : 'borderless'}
161
+ >
162
+ <MultipleProvidersModelItem
163
+ activeKey={activeKey}
164
+ data={item.data}
165
+ newLabel={newLabel}
166
+ onClose={onClose}
167
+ onModelChange={onModelChange}
168
+ />
169
+ </Block>
170
+ );
171
+ }
172
+
173
+ default: {
174
+ return null;
175
+ }
176
+ }
177
+ },
178
+ );
179
+
180
+ VirtualItemRenderer.displayName = 'VirtualItemRenderer';
@@ -0,0 +1,99 @@
1
+ import { Flexbox } from '@lobehub/ui';
2
+ import type { FC } from 'react';
3
+ import { useMemo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import { useEnabledChatModels } from '@/hooks/useEnabledChatModels';
7
+
8
+ import {
9
+ FOOTER_HEIGHT,
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';
17
+ import { useModelAndProvider } from '../../hooks/useModelAndProvider';
18
+ import { usePanelHandlers } from '../../hooks/usePanelHandlers';
19
+ import { styles } from '../../styles';
20
+ import type { GroupMode } from '../../types';
21
+ import { getVirtualItemKey, menuKey } from '../../utils';
22
+ import { VirtualItemRenderer } from './VirtualItemRenderer';
23
+
24
+ interface ListProps {
25
+ groupMode: GroupMode;
26
+ isOpen: boolean;
27
+ model?: string;
28
+ onModelChange?: (params: { model: string; provider: string }) => Promise<void>;
29
+ onOpenChange?: (open: boolean) => void;
30
+ provider?: string;
31
+ searchKeyword?: string;
32
+ }
33
+
34
+ export const List: FC<ListProps> = ({
35
+ groupMode,
36
+ isOpen,
37
+ model: modelProp,
38
+ onModelChange: onModelChangeProp,
39
+ onOpenChange,
40
+ provider: providerProp,
41
+ searchKeyword = '',
42
+ }) => {
43
+ const { t: tCommon } = useTranslation('common');
44
+ const newLabel = tCommon('new');
45
+
46
+ // Get enabled models list
47
+ const enabledList = useEnabledChatModels();
48
+
49
+ // Get delayed render state
50
+ const renderAll = useDelayedRender(isOpen);
51
+
52
+ // Get model and provider
53
+ const { model, provider } = useModelAndProvider(modelProp, providerProp);
54
+
55
+ // Get handlers
56
+ const { handleModelChange, handleClose } = usePanelHandlers({
57
+ onModelChange: onModelChangeProp,
58
+ onOpenChange,
59
+ });
60
+
61
+ // Build virtual items
62
+ const virtualItems = useBuildVirtualItems(enabledList, groupMode, searchKeyword);
63
+
64
+ // Calculate panel height
65
+ const panelHeight = useMemo(
66
+ () =>
67
+ enabledList.length === 0
68
+ ? TOOLBAR_HEIGHT + ITEM_HEIGHT['no-provider'] + FOOTER_HEIGHT
69
+ : MAX_PANEL_HEIGHT,
70
+ [enabledList.length],
71
+ );
72
+
73
+ // Calculate active key
74
+ const activeKey = menuKey(provider, model);
75
+
76
+ return (
77
+ <Flexbox
78
+ className={styles.list}
79
+ flex={1}
80
+ style={{
81
+ height: panelHeight - TOOLBAR_HEIGHT - FOOTER_HEIGHT,
82
+ paddingBlock: groupMode === 'byModel' ? 8 : 0,
83
+ }}
84
+ >
85
+ {virtualItems.slice(0, renderAll ? virtualItems.length : INITIAL_RENDER_COUNT).map((item) => (
86
+ <VirtualItemRenderer
87
+ activeKey={activeKey}
88
+ item={item}
89
+ key={getVirtualItemKey(item)}
90
+ newLabel={newLabel}
91
+ onClose={handleClose}
92
+ onModelChange={handleModelChange}
93
+ />
94
+ ))}
95
+ </Flexbox>
96
+ );
97
+ };
98
+
99
+ List.displayName = 'List';