@lobehub/lobehub 2.0.0-next.221 → 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 (114) 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/CHANGELOG.md +33 -0
  9. package/changelog/v1.json +9 -0
  10. package/locales/zh-CN/components.json +1 -0
  11. package/package.json +3 -3
  12. package/packages/const/src/index.ts +0 -1
  13. package/packages/memory-user-memory/package.json +1 -0
  14. package/packages/memory-user-memory/src/extractors/context.test.ts +3 -2
  15. package/packages/memory-user-memory/src/extractors/experience.test.ts +3 -2
  16. package/packages/memory-user-memory/src/extractors/identity.test.ts +23 -6
  17. package/packages/memory-user-memory/src/extractors/preference.test.ts +3 -2
  18. package/packages/memory-user-memory/vitest.config.ts +4 -0
  19. package/packages/model-runtime/src/providers/replicate/index.ts +1 -1
  20. package/packages/ssrf-safe-fetch/index.test.ts +2 -2
  21. package/packages/ssrf-safe-fetch/package.json +3 -2
  22. package/packages/types/src/serverConfig.ts +2 -0
  23. package/packages/utils/package.json +1 -1
  24. package/packages/utils/src/client/xor-obfuscation.test.ts +32 -32
  25. package/packages/utils/src/client/xor-obfuscation.ts +3 -4
  26. package/packages/utils/src/imageToBase64.ts +1 -1
  27. package/packages/utils/src/server/__tests__/auth.test.ts +1 -1
  28. package/packages/utils/src/server/auth.ts +1 -1
  29. package/packages/utils/src/server/xor.test.ts +9 -7
  30. package/packages/utils/src/server/xor.ts +1 -1
  31. package/packages/web-crawler/package.json +1 -1
  32. package/packages/web-crawler/src/crawImpl/__tests__/naive.test.ts +1 -1
  33. package/packages/web-crawler/src/crawImpl/naive.ts +1 -1
  34. package/scripts/prebuild.mts +58 -1
  35. package/src/app/(backend)/api/auth/[...all]/route.ts +2 -1
  36. package/src/app/(backend)/middleware/auth/index.ts +3 -3
  37. package/src/app/(backend)/middleware/auth/utils.test.ts +1 -1
  38. package/src/app/(backend)/middleware/auth/utils.ts +1 -1
  39. package/src/app/(backend)/webapi/chat/[provider]/route.test.ts +2 -2
  40. package/src/app/(backend)/webapi/models/[provider]/route.test.ts +1 -1
  41. package/src/app/(backend)/webapi/plugin/gateway/route.ts +1 -1
  42. package/src/app/(backend)/webapi/proxy/route.ts +1 -1
  43. package/src/app/[variants]/(auth)/login/[[...login]]/page.tsx +1 -1
  44. package/src/app/[variants]/(auth)/reset-password/layout.tsx +1 -1
  45. package/src/app/[variants]/(auth)/signin/layout.tsx +1 -1
  46. package/src/app/[variants]/(auth)/signin/useSignIn.ts +2 -2
  47. package/src/app/[variants]/(auth)/signup/[[...signup]]/page.tsx +1 -1
  48. package/src/app/[variants]/(auth)/signup/[[...signup]]/useSignUp.tsx +12 -6
  49. package/src/app/[variants]/(auth)/verify-email/layout.tsx +1 -1
  50. package/src/app/[variants]/(main)/settings/profile/features/AvatarRow.tsx +1 -1
  51. package/src/app/[variants]/(main)/settings/security/index.tsx +1 -1
  52. package/src/app/[variants]/(mobile)/me/(home)/__tests__/UserBanner.test.tsx +1 -1
  53. package/src/app/[variants]/(mobile)/me/(home)/__tests__/useCategory.test.tsx +1 -1
  54. package/src/app/[variants]/(mobile)/me/(home)/features/UserBanner.tsx +1 -1
  55. package/src/app/[variants]/(mobile)/settings/_layout/Header.tsx +1 -1
  56. package/src/components/ModelSelect/index.tsx +103 -72
  57. package/src/envs/auth.ts +30 -9
  58. package/src/features/Conversation/Messages/AssistantGroup/components/EditState.tsx +15 -32
  59. package/src/features/Conversation/Messages/AssistantGroup/index.tsx +9 -7
  60. package/src/features/EditorModal/EditorCanvas.tsx +31 -50
  61. package/src/features/EditorModal/TextareCanvas.tsx +3 -1
  62. package/src/features/EditorModal/index.tsx +14 -4
  63. package/src/features/ModelSwitchPanel/components/Footer.tsx +42 -0
  64. package/src/features/ModelSwitchPanel/components/List/MultipleProvidersModelItem.tsx +103 -0
  65. package/src/features/ModelSwitchPanel/components/List/SingleProviderModelItem.tsx +24 -0
  66. package/src/features/ModelSwitchPanel/components/List/VirtualItemRenderer.tsx +180 -0
  67. package/src/features/ModelSwitchPanel/components/List/index.tsx +99 -0
  68. package/src/features/ModelSwitchPanel/components/PanelContent.tsx +77 -0
  69. package/src/features/ModelSwitchPanel/components/Toolbar.tsx +54 -0
  70. package/src/features/ModelSwitchPanel/const.ts +29 -0
  71. package/src/features/ModelSwitchPanel/hooks/useBuildVirtualItems.ts +122 -0
  72. package/src/features/ModelSwitchPanel/hooks/useCurrentModelName.ts +18 -0
  73. package/src/features/ModelSwitchPanel/hooks/useDelayedRender.ts +18 -0
  74. package/src/features/ModelSwitchPanel/hooks/useModelAndProvider.ts +14 -0
  75. package/src/features/ModelSwitchPanel/hooks/usePanelHandlers.ts +33 -0
  76. package/src/features/ModelSwitchPanel/hooks/usePanelSize.ts +33 -0
  77. package/src/features/ModelSwitchPanel/hooks/usePanelState.ts +20 -0
  78. package/src/features/ModelSwitchPanel/index.tsx +25 -706
  79. package/src/features/ModelSwitchPanel/styles.ts +58 -0
  80. package/src/features/ModelSwitchPanel/types.ts +73 -0
  81. package/src/features/ModelSwitchPanel/utils.ts +24 -0
  82. package/src/features/User/UserPanel/PanelContent.tsx +1 -1
  83. package/src/features/User/__tests__/PanelContent.test.tsx +1 -1
  84. package/src/features/User/__tests__/UserAvatar.test.tsx +1 -1
  85. package/src/features/User/__tests__/useMenu.test.tsx +1 -1
  86. package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -1
  87. package/src/libs/better-auth/auth-client.ts +7 -3
  88. package/src/libs/better-auth/define-config.ts +2 -2
  89. package/src/libs/next/proxy/define-config.ts +1 -2
  90. package/src/libs/oidc-provider/provider.test.ts +1 -1
  91. package/src/libs/trpc/async/context.ts +1 -1
  92. package/src/libs/trpc/lambda/context.ts +7 -8
  93. package/src/libs/trpc/middleware/userAuth.ts +1 -1
  94. package/src/libs/trusted-client/getSessionUser.ts +1 -1
  95. package/src/locales/default/components.ts +1 -0
  96. package/src/server/globalConfig/index.ts +2 -0
  97. package/src/server/routers/async/caller.ts +1 -1
  98. package/src/server/routers/lambda/__tests__/user.test.ts +2 -2
  99. package/src/server/routers/lambda/user.ts +2 -1
  100. package/src/services/_auth.ts +3 -3
  101. package/src/services/chat/index.ts +1 -1
  102. package/src/services/chat/mecha/contextEngineering.ts +1 -1
  103. package/src/store/global/initialState.ts +10 -0
  104. package/src/store/global/selectors/systemStatus.ts +5 -0
  105. package/src/store/serverConfig/selectors.ts +5 -1
  106. package/src/store/tool/slices/mcpStore/action.ts +74 -75
  107. package/src/store/user/slices/auth/action.test.ts +1 -1
  108. package/src/store/user/slices/auth/action.ts +1 -1
  109. package/src/store/user/slices/auth/initialState.ts +1 -1
  110. package/src/store/user/slices/auth/selectors.test.ts +1 -1
  111. package/src/store/user/slices/auth/selectors.ts +1 -1
  112. package/src/store/user/slices/common/action.ts +1 -1
  113. package/src/store/userMemory/slices/context/action.ts +6 -6
  114. package/packages/const/src/auth.ts +0 -14
@@ -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';
@@ -0,0 +1,77 @@
1
+ import type { FC } from 'react';
2
+ import { useState } from 'react';
3
+ import { Rnd } from 'react-rnd';
4
+
5
+ import { useEnabledChatModels } from '@/hooks/useEnabledChatModels';
6
+
7
+ import { ENABLE_RESIZING, MAX_WIDTH, MIN_WIDTH } from '../const';
8
+ import { usePanelHandlers } from '../hooks/usePanelHandlers';
9
+ import { usePanelSize } from '../hooks/usePanelSize';
10
+ import { usePanelState } from '../hooks/usePanelState';
11
+ import { Footer } from './Footer';
12
+ import { List } from './List';
13
+ import { Toolbar } from './Toolbar';
14
+
15
+ interface PanelContentProps {
16
+ isOpen: boolean;
17
+ model?: string;
18
+ onModelChange?: (params: { model: string; provider: string }) => Promise<void>;
19
+ onOpenChange?: (open: boolean) => void;
20
+ provider?: string;
21
+ }
22
+
23
+ export const PanelContent: FC<PanelContentProps> = ({
24
+ isOpen,
25
+ model: modelProp,
26
+ onModelChange: onModelChangeProp,
27
+ onOpenChange,
28
+ provider: providerProp,
29
+ }) => {
30
+ // Get enabled models list
31
+ const enabledList = useEnabledChatModels();
32
+
33
+ // Search keyword state
34
+ const [searchKeyword, setSearchKeyword] = useState('');
35
+
36
+ // Hooks for state management
37
+ const { groupMode, handleGroupModeChange } = usePanelState();
38
+ const { panelHeight, panelWidth, handlePanelWidthChange } = usePanelSize(enabledList.length);
39
+ const { handleClose } = usePanelHandlers({
40
+ onModelChange: onModelChangeProp,
41
+ onOpenChange,
42
+ });
43
+
44
+ return (
45
+ <Rnd
46
+ disableDragging
47
+ enableResizing={ENABLE_RESIZING}
48
+ maxWidth={MAX_WIDTH}
49
+ minWidth={MIN_WIDTH}
50
+ onResizeStop={(_e, _direction, ref) => {
51
+ handlePanelWidthChange(ref.offsetWidth);
52
+ }}
53
+ position={{ x: 0, y: 0 }}
54
+ size={{ height: panelHeight, width: panelWidth }}
55
+ style={{ display: 'flex', flexDirection: 'column', position: 'relative' }}
56
+ >
57
+ <Toolbar
58
+ groupMode={groupMode}
59
+ onGroupModeChange={handleGroupModeChange}
60
+ onSearchKeywordChange={setSearchKeyword}
61
+ searchKeyword={searchKeyword}
62
+ />
63
+ <List
64
+ groupMode={groupMode}
65
+ isOpen={isOpen}
66
+ model={modelProp}
67
+ onModelChange={onModelChangeProp}
68
+ onOpenChange={onOpenChange}
69
+ provider={providerProp}
70
+ searchKeyword={searchKeyword}
71
+ />
72
+ <Footer onClose={handleClose} />
73
+ </Rnd>
74
+ );
75
+ };
76
+
77
+ PanelContent.displayName = 'PanelContent';