@lobehub/lobehub 2.0.0-next.95 → 2.0.0-next.96

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 (53) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/ar/common.json +21 -0
  4. package/locales/ar/hotkey.json +4 -0
  5. package/locales/bg-BG/common.json +21 -0
  6. package/locales/bg-BG/hotkey.json +4 -0
  7. package/locales/de-DE/common.json +21 -0
  8. package/locales/de-DE/hotkey.json +4 -0
  9. package/locales/en-US/common.json +21 -0
  10. package/locales/en-US/hotkey.json +4 -0
  11. package/locales/es-ES/common.json +21 -0
  12. package/locales/es-ES/hotkey.json +4 -0
  13. package/locales/fa-IR/common.json +21 -0
  14. package/locales/fa-IR/hotkey.json +4 -0
  15. package/locales/fr-FR/common.json +21 -0
  16. package/locales/fr-FR/hotkey.json +4 -0
  17. package/locales/it-IT/common.json +21 -0
  18. package/locales/it-IT/hotkey.json +4 -0
  19. package/locales/ja-JP/common.json +21 -0
  20. package/locales/ja-JP/hotkey.json +4 -0
  21. package/locales/ko-KR/common.json +21 -0
  22. package/locales/ko-KR/hotkey.json +4 -0
  23. package/locales/nl-NL/common.json +21 -0
  24. package/locales/nl-NL/hotkey.json +4 -0
  25. package/locales/pl-PL/common.json +21 -0
  26. package/locales/pl-PL/hotkey.json +4 -0
  27. package/locales/pt-BR/common.json +21 -0
  28. package/locales/pt-BR/hotkey.json +4 -0
  29. package/locales/ru-RU/common.json +21 -0
  30. package/locales/ru-RU/hotkey.json +4 -0
  31. package/locales/tr-TR/common.json +21 -0
  32. package/locales/tr-TR/hotkey.json +4 -0
  33. package/locales/vi-VN/common.json +21 -0
  34. package/locales/vi-VN/hotkey.json +4 -0
  35. package/locales/zh-CN/common.json +21 -0
  36. package/locales/zh-CN/hotkey.json +4 -0
  37. package/locales/zh-TW/common.json +21 -0
  38. package/locales/zh-TW/hotkey.json +4 -0
  39. package/package.json +3 -1
  40. package/packages/const/src/hotkeys.ts +6 -0
  41. package/packages/conversation-flow/src/__tests__/indexing.test.ts +513 -0
  42. package/packages/conversation-flow/src/__tests__/structuring.test.ts +600 -0
  43. package/packages/types/src/hotkey.ts +1 -0
  44. package/src/app/[variants]/(main)/settings/_layout/Desktop/index.tsx +41 -8
  45. package/src/app/[variants]/(main)/settings/provider/(list)/ProviderGrid/Card.tsx +6 -4
  46. package/src/app/[variants]/(main)/settings/provider/(list)/ProviderGrid/index.tsx +16 -4
  47. package/src/app/[variants]/(main)/settings/provider/(list)/index.tsx +15 -3
  48. package/src/app/[variants]/(main)/settings/provider/detail/index.tsx +23 -15
  49. package/src/layout/GlobalProvider/Cmdk.tsx +470 -0
  50. package/src/layout/GlobalProvider/CmdkLazy.tsx +17 -0
  51. package/src/layout/GlobalProvider/index.tsx +2 -0
  52. package/src/locales/default/common.ts +21 -0
  53. package/src/locales/default/hotkey.ts +4 -0
@@ -5,7 +5,6 @@ import { memo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { Flexbox } from 'react-layout-kit';
7
7
 
8
- import Link from '@/components/Link';
9
8
  import { AiProviderListItem } from '@/types/aiProvider';
10
9
 
11
10
  import EnableSwitch from './EnableSwitch';
@@ -13,9 +12,10 @@ import { useStyles } from './style';
13
12
 
14
13
  interface ProviderCardProps extends AiProviderListItem {
15
14
  loading?: boolean;
15
+ onProviderSelect: (provider: string) => void;
16
16
  }
17
17
  const ProviderCard = memo<ProviderCardProps>(
18
- ({ id, description, name, enabled, source, logo, loading }) => {
18
+ ({ id, description, name, enabled, source, logo, loading, onProviderSelect }) => {
19
19
  const { t } = useTranslation('providers');
20
20
  const { cx, styles, theme } = useStyles();
21
21
 
@@ -33,7 +33,9 @@ const ProviderCard = memo<ProviderCardProps>(
33
33
  return (
34
34
  <Flexbox className={cx(styles.container)} gap={24}>
35
35
  <Flexbox gap={12} padding={16} width={'100%'}>
36
- <Link href={`/settings?active=provider&provider=${id}`}>
36
+ <div onClick={() => {
37
+ onProviderSelect(id);
38
+ }} style={{ cursor: 'pointer' }}>
37
39
  <Flexbox gap={12} width={'100%'}>
38
40
  <Flexbox align={'center'} horizontal justify={'space-between'}>
39
41
  {source === 'builtin' ? (
@@ -68,7 +70,7 @@ const ProviderCard = memo<ProviderCardProps>(
68
70
  {source === 'custom' ? description : t(`${id}.description`)}
69
71
  </Text>
70
72
  </Flexbox>
71
- </Link>
73
+ </div>
72
74
  <Divider style={{ margin: '4px 0' }} />
73
75
  <Flexbox horizontal justify={'space-between'}>
74
76
  <div />
@@ -14,7 +14,12 @@ const loadingArr = Array.from({ length: 12 })
14
14
  .fill('-')
15
15
  .map((item, index) => `${index}x${item}`);
16
16
 
17
- const List = memo(() => {
17
+ type ListProps = {
18
+ onProviderSelect: (provider: string) => void;
19
+ };
20
+
21
+ const List = memo((props: ListProps) => {
22
+ const { onProviderSelect } = props;
18
23
  const { t } = useTranslation('modelProvider');
19
24
  const enabledList = useAiInfraStore(aiProviderSelectors.enabledAiProviderList, isEqual);
20
25
  const disabledList = useAiInfraStore(aiProviderSelectors.disabledAiProviderList, isEqual);
@@ -30,7 +35,14 @@ const List = memo(() => {
30
35
  </Flexbox>
31
36
  <Grid gap={16} rows={3}>
32
37
  {loadingArr.map((item) => (
33
- <Card enabled={false} id={item} key={item} loading source={'builtin'} />
38
+ <Card
39
+ enabled={false}
40
+ id={item}
41
+ key={item}
42
+ loading
43
+ onProviderSelect={onProviderSelect}
44
+ source={'builtin'}
45
+ />
34
46
  ))}
35
47
  </Grid>
36
48
  </Flexbox>
@@ -47,7 +59,7 @@ const List = memo(() => {
47
59
  </Flexbox>
48
60
  <Grid gap={16} rows={3}>
49
61
  {enabledList.map((item) => (
50
- <Card {...item} key={item.id} />
62
+ <Card {...item} key={item.id} onProviderSelect={onProviderSelect} />
51
63
  ))}
52
64
  </Grid>
53
65
  </Flexbox>
@@ -60,7 +72,7 @@ const List = memo(() => {
60
72
  </Flexbox>
61
73
  <Grid gap={16} rows={3}>
62
74
  {disabledList.map((item) => (
63
- <Card {...item} key={item.id} />
75
+ <Card {...item} key={item.id} onProviderSelect={onProviderSelect} />
64
76
  ))}
65
77
  </Grid>
66
78
  </Flexbox>
@@ -1,20 +1,32 @@
1
1
  'use client';
2
2
 
3
3
  import { isCustomBranding } from '@/const/version';
4
- import { parseAsString, useQueryParam } from '@/hooks/useQueryParam';
5
4
 
6
5
  import DesktopLayout from '../_layout/Desktop';
7
6
  import MobileLayout from '../_layout/Mobile';
8
7
  import ProviderDetailPage from '../detail';
9
8
  import Footer from './Footer';
9
+ import { useMemo, useState } from 'react';
10
+ import { useSearchParams } from 'react-router-dom';
10
11
 
11
12
  const Page = (props: { mobile?: boolean }) => {
12
- const [Provider, setProvider] = useQueryParam('provider', parseAsString.withDefault('all'));
13
+ const [SearchParams, setSearchParams] = useSearchParams();
14
+ const [provider, setProviderState] = useState(SearchParams.get('provider') || 'all');
15
+ const setProvider = (provider: string) => {
16
+ setSearchParams({ active: 'provider', provider });
17
+ setProviderState(provider);
18
+ };
19
+
13
20
  const { mobile } = props;
14
21
  const ProviderLayout = mobile ? MobileLayout : DesktopLayout;
22
+
23
+ const ProviderListPage = useMemo(() => {
24
+ return <ProviderDetailPage id={provider} onProviderSelect={setProvider} />;
25
+ }, [provider]);
26
+
15
27
  return (
16
28
  <ProviderLayout onProviderSelect={setProvider}>
17
- <ProviderDetailPage id={Provider} />
29
+ {ProviderListPage}
18
30
  {!isCustomBranding && <Footer />}
19
31
  </ProviderLayout>
20
32
  );
@@ -1,22 +1,30 @@
1
- import NewAPI from './newapi';
2
- import ProviderGrid from '../(list)/ProviderGrid';
3
- import Azure from './azure';
4
- import AzureAI from './azureai';
5
- import Bedrock from './bedrock';
6
- import Cloudflare from './cloudflare';
7
- import ComfyUI from './comfyui';
8
- import DefaultPage from './default/ProviderDetialPage';
9
- import GitHub from './github';
10
- import Ollama from './ollama';
11
- import OpenAI from './openai';
12
- import VertexAI from './vertexai';
1
+ import dynamic from 'next/dynamic';
2
+ import Loading from '@/components/Loading/BrandTextLoading';
13
3
 
14
- const ProviderDetailPage = (props: { id?: string | null }) => {
15
- const { id } = props;
4
+ const NewAPI = dynamic(() => import('./newapi'), { loading: () => <Loading />, ssr: false });
5
+ const OpenAI = dynamic(() => import('./openai'), { loading: () => <Loading />, ssr: false });
6
+ const VertexAI = dynamic(() => import('./vertexai'), { loading: () => <Loading />, ssr: false });
7
+ const GitHub = dynamic(() => import('./github'), { loading: () => <Loading />, ssr: false });
8
+ const Ollama = dynamic(() => import('./ollama'), { loading: () => <Loading />, ssr: false });
9
+ const ComfyUI = dynamic(() => import('./comfyui'), { loading: () => <Loading />, ssr: false });
10
+ const Cloudflare = dynamic(() => import('./cloudflare'), { loading: () => <Loading />, ssr: false });
11
+ const Bedrock = dynamic(() => import('./bedrock'), { loading: () => <Loading />, ssr: false });
12
+ const AzureAI = dynamic(() => import('./azureai'), { loading: () => <Loading />, ssr: false });
13
+ const Azure = dynamic(() => import('./azure'), { loading: () => <Loading />, ssr: false });
14
+ const ProviderGrid = dynamic(() => import('../(list)/ProviderGrid'), { loading: () => <Loading />, ssr: false });
15
+ const DefaultPage = dynamic(() => import('./default/ProviderDetialPage'), { loading: () => <Loading />, ssr: false });
16
+
17
+ type ProviderDetailPageProps = {
18
+ id?: string | null;
19
+ onProviderSelect: (provider: string) => void;
20
+ }
21
+
22
+ const ProviderDetailPage = (props: ProviderDetailPageProps) => {
23
+ const { id, onProviderSelect } = props;
16
24
 
17
25
  switch (id) {
18
26
  case 'all': {
19
- return <ProviderGrid />;
27
+ return <ProviderGrid onProviderSelect={onProviderSelect} />;
20
28
  }
21
29
  case 'azure': {
22
30
  return <Azure />;
@@ -0,0 +1,470 @@
1
+ 'use client';
2
+
3
+ import { Tag } from '@lobehub/ui';
4
+ import { createStyles } from 'antd-style';
5
+ import { Command } from 'cmdk';
6
+ import {
7
+ ArrowLeft,
8
+ ArrowUpDown,
9
+ BookOpen,
10
+ Bot,
11
+ Compass,
12
+ CornerDownLeft,
13
+ Github,
14
+ MessageCircle,
15
+ Monitor,
16
+ Moon,
17
+ Palette,
18
+ Settings,
19
+ Star,
20
+ Sun,
21
+ } from 'lucide-react';
22
+ import { usePathname, useRouter } from 'next/navigation';
23
+ import { memo, useEffect, useState } from 'react';
24
+ import { createPortal } from 'react-dom';
25
+ import { useTranslation } from 'react-i18next';
26
+
27
+ import { useHotkeyById } from '@/hooks/useHotkeys/useHotkeyById';
28
+ import { useGlobalStore } from '@/store/global';
29
+ import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
30
+ import { useSessionStore } from '@/store/session';
31
+ import { HotkeyEnum } from '@/types/hotkey';
32
+
33
+ const useStyles = createStyles(({ css, token }) => ({
34
+ backTag: css`
35
+ cursor: pointer;
36
+
37
+ &:hover {
38
+ opacity: 0.8;
39
+ }
40
+ `,
41
+ commandFooter: css`
42
+ display: flex;
43
+ gap: 16px;
44
+ align-items: center;
45
+ justify-content: flex-end;
46
+
47
+ padding-block: 8px;
48
+ padding-inline: 16px;
49
+ border-block-start: 1px solid ${token.colorBorderSecondary};
50
+
51
+ background: ${token.colorBgContainer};
52
+ `,
53
+ commandRoot: css`
54
+ overflow: hidden;
55
+ display: flex;
56
+ flex-direction: column;
57
+
58
+ width: min(640px, 90vw);
59
+ max-height: min(500px, 70vh);
60
+ border-radius: ${token.borderRadiusLG}px;
61
+
62
+ background: ${token.colorBgElevated};
63
+ box-shadow: ${token.boxShadowSecondary};
64
+
65
+ animation: slide-down 0.12s ease-out;
66
+
67
+ @keyframes slide-down {
68
+ from {
69
+ transform: translateY(-20px) scale(0.96);
70
+ opacity: 0;
71
+ }
72
+
73
+ to {
74
+ transform: translateY(0) scale(1);
75
+ opacity: 1;
76
+ }
77
+ }
78
+
79
+ [cmdk-input] {
80
+ flex: 1;
81
+
82
+ min-width: 0;
83
+ padding: 0;
84
+ border: none;
85
+
86
+ font-family: inherit;
87
+ font-size: 16px;
88
+ color: ${token.colorText};
89
+
90
+ background: transparent;
91
+ outline: none;
92
+
93
+ &::placeholder {
94
+ color: ${token.colorTextPlaceholder};
95
+ }
96
+ }
97
+
98
+ [cmdk-list] {
99
+ overflow-y: auto;
100
+ max-height: 400px;
101
+ padding: 8px;
102
+ }
103
+
104
+ [cmdk-empty] {
105
+ padding-block: 32px;
106
+ padding-inline: 16px;
107
+
108
+ font-size: 14px;
109
+ color: ${token.colorTextTertiary};
110
+ text-align: center;
111
+ }
112
+
113
+ [cmdk-item] {
114
+ cursor: pointer;
115
+ user-select: none;
116
+
117
+ display: flex;
118
+ gap: 12px;
119
+ align-items: center;
120
+
121
+ padding-block: 12px;
122
+ padding-inline: 16px;
123
+ border-radius: ${token.borderRadius}px;
124
+
125
+ color: ${token.colorText};
126
+
127
+ transition: all 0.15s ease;
128
+
129
+ &[aria-selected='true'] {
130
+ background: ${token.colorBgTextHover};
131
+ }
132
+
133
+ &:hover {
134
+ background: ${token.colorBgTextHover};
135
+ }
136
+ }
137
+
138
+ [cmdk-group-heading] {
139
+ user-select: none;
140
+
141
+ padding-block: 8px;
142
+ padding-inline: 16px;
143
+
144
+ font-size: 12px;
145
+ font-weight: 500;
146
+ color: ${token.colorTextSecondary};
147
+ }
148
+
149
+ [cmdk-separator] {
150
+ height: 1px;
151
+ margin-block: 4px;
152
+ background: ${token.colorBorderSecondary};
153
+ }
154
+ `,
155
+ icon: css`
156
+ flex-shrink: 0;
157
+ width: 20px;
158
+ height: 20px;
159
+ color: ${token.colorTextSecondary};
160
+ `,
161
+ inputWrapper: css`
162
+ display: flex;
163
+ gap: 8px;
164
+ align-items: center;
165
+
166
+ padding: 16px;
167
+ border-block-end: 1px solid ${token.colorBorderSecondary};
168
+ `,
169
+ itemContent: css`
170
+ flex: 1;
171
+ min-width: 0;
172
+ `,
173
+ itemDescription: css`
174
+ margin-block-start: 2px;
175
+ font-size: 12px;
176
+ line-height: 1.4;
177
+ color: ${token.colorTextTertiary};
178
+ `,
179
+ itemLabel: css`
180
+ font-size: 14px;
181
+ font-weight: 500;
182
+ line-height: 1.4;
183
+ `,
184
+ kbd: css`
185
+ display: inline-flex;
186
+ gap: 4px;
187
+ align-items: center;
188
+
189
+ padding-block: 2px;
190
+ padding-inline: 6px;
191
+ border-radius: ${token.borderRadiusSM}px;
192
+
193
+ font-size: 11px;
194
+ font-weight: 500;
195
+ line-height: 1.2;
196
+ color: ${token.colorTextSecondary};
197
+
198
+ background: ${token.colorFillQuaternary};
199
+ `,
200
+ kbdIcon: css`
201
+ width: 12px;
202
+ height: 12px;
203
+ `,
204
+ overlay: css`
205
+ position: fixed;
206
+ z-index: 9999;
207
+ inset: 0;
208
+
209
+ display: flex;
210
+ justify-content: center;
211
+
212
+ padding-block-start: 15vh;
213
+
214
+ background: ${token.colorBgMask};
215
+
216
+ animation: fade-in 0.1s ease-in-out;
217
+
218
+ @keyframes fade-in {
219
+ from {
220
+ opacity: 0;
221
+ }
222
+
223
+ to {
224
+ opacity: 1;
225
+ }
226
+ }
227
+ `,
228
+ }));
229
+
230
+ const Cmdk = memo(() => {
231
+ const [open, setOpen] = useState(false);
232
+ const [mounted, setMounted] = useState(false);
233
+ const [search, setSearch] = useState('');
234
+ const [pages, setPages] = useState<string[]>([]);
235
+ const router = useRouter();
236
+ const pathname = usePathname();
237
+ const { t } = useTranslation('common');
238
+ const { styles } = useStyles();
239
+ const switchThemeMode = useGlobalStore((s) => s.switchThemeMode);
240
+ const createSession = useSessionStore((s) => s.createSession);
241
+ const { showCreateSession } = useServerConfigStore(featureFlagsSelectors);
242
+
243
+ const page = pages.at(-1);
244
+
245
+ // Ensure we're mounted on the client
246
+ useEffect(() => {
247
+ setMounted(true);
248
+ }, []);
249
+
250
+ // Register Cmd+K / Ctrl+K hotkey
251
+ useHotkeyById(HotkeyEnum.CommandPalette, () => {
252
+ setOpen((prev) => !prev);
253
+ });
254
+
255
+ // Close on Escape key and prevent body scroll
256
+ useEffect(() => {
257
+ if (open) {
258
+ const originalStyle = window.getComputedStyle(document.body).overflow;
259
+ document.body.style.overflow = 'hidden';
260
+
261
+ return () => {
262
+ document.body.style.overflow = originalStyle;
263
+ };
264
+ }
265
+ }, [open]);
266
+
267
+ // Reset pages and search when opening/closing
268
+ useEffect(() => {
269
+ if (open) {
270
+ setPages([]);
271
+ setSearch('');
272
+ }
273
+ }, [open]);
274
+
275
+ const handleNavigate = (path: string) => {
276
+ router.push(path);
277
+ setOpen(false);
278
+ };
279
+
280
+ const handleExternalLink = (url: string) => {
281
+ window.open(url, '_blank', 'noopener,noreferrer');
282
+ setOpen(false);
283
+ };
284
+
285
+ const handleThemeChange = (theme: 'light' | 'dark' | 'auto') => {
286
+ switchThemeMode(theme);
287
+ setOpen(false);
288
+ };
289
+
290
+ if (!mounted || !open) return null;
291
+
292
+ return createPortal(
293
+ <div className={styles.overlay} onClick={() => setOpen(false)}>
294
+ <div onClick={(e) => e.stopPropagation()}>
295
+ <Command
296
+ className={styles.commandRoot}
297
+ onKeyDown={(e) => {
298
+ // Escape goes to previous page or closes
299
+ if (e.key === 'Escape') {
300
+ e.preventDefault();
301
+ if (pages.length > 0) {
302
+ setPages((prev) => prev.slice(0, -1));
303
+ } else {
304
+ setOpen(false);
305
+ }
306
+ }
307
+ // Backspace goes to previous page when search is empty
308
+ if (e.key === 'Backspace' && !search && pages.length > 0) {
309
+ e.preventDefault();
310
+ setPages((prev) => prev.slice(0, -1));
311
+ }
312
+ }}
313
+ shouldFilter={true}
314
+ >
315
+ <div className={styles.inputWrapper}>
316
+ {pages.length > 0 && (
317
+ <Tag
318
+ className={styles.backTag}
319
+ icon={<ArrowLeft size={12} />}
320
+ onClick={() => setPages((prev) => prev.slice(0, -1))}
321
+ />
322
+ )}
323
+ <Command.Input
324
+ autoFocus
325
+ onValueChange={setSearch}
326
+ placeholder={t('cmdk.searchPlaceholder')}
327
+ value={search}
328
+ />
329
+ <Tag>ESC</Tag>
330
+ </div>
331
+ <Command.List>
332
+ <Command.Empty>{t('cmdk.noResults')}</Command.Empty>
333
+
334
+ {!page && (
335
+ <>
336
+ {showCreateSession && (
337
+ <Command.Item
338
+ onSelect={() => {
339
+ createSession();
340
+ setOpen(false);
341
+ }}
342
+ value="new-agent"
343
+ >
344
+ <Bot className={styles.icon} />
345
+ <div className={styles.itemContent}>
346
+ <div className={styles.itemLabel}>{t('cmdk.newAgent')}</div>
347
+ </div>
348
+ </Command.Item>
349
+ )}
350
+
351
+ {!pathname?.startsWith('/settings') && (
352
+ <Command.Item onSelect={() => handleNavigate('/settings')} value="settings">
353
+ <Settings className={styles.icon} />
354
+ <div className={styles.itemContent}>
355
+ <div className={styles.itemLabel}>{t('cmdk.settings')}</div>
356
+ </div>
357
+ </Command.Item>
358
+ )}
359
+
360
+ <Command.Item onSelect={() => setPages([...pages, 'theme'])} value="theme">
361
+ <Monitor className={styles.icon} />
362
+ <div className={styles.itemContent}>
363
+ <div className={styles.itemLabel}>{t('cmdk.theme')}</div>
364
+ </div>
365
+ </Command.Item>
366
+
367
+ <Command.Group heading={t('cmdk.navigate')}>
368
+ {!pathname?.startsWith('/discover') && (
369
+ <Command.Item onSelect={() => handleNavigate('/discover')} value="discover">
370
+ <Compass className={styles.icon} />
371
+ <div className={styles.itemContent}>
372
+ <div className={styles.itemLabel}>{t('cmdk.discover')}</div>
373
+ </div>
374
+ </Command.Item>
375
+ )}
376
+ {!pathname?.startsWith('/image') && (
377
+ <Command.Item onSelect={() => handleNavigate('/image')} value="painting">
378
+ <Palette className={styles.icon} />
379
+ <div className={styles.itemContent}>
380
+ <div className={styles.itemLabel}>{t('cmdk.painting')}</div>
381
+ </div>
382
+ </Command.Item>
383
+ )}
384
+ {!pathname?.startsWith('/knowledge') && (
385
+ <Command.Item onSelect={() => handleNavigate('/knowledge')} value="knowledge">
386
+ <BookOpen className={styles.icon} />
387
+ <div className={styles.itemContent}>
388
+ <div className={styles.itemLabel}>{t('cmdk.knowledgeBase')}</div>
389
+ </div>
390
+ </Command.Item>
391
+ )}
392
+ </Command.Group>
393
+
394
+ <Command.Group heading={t('cmdk.about')}>
395
+ <Command.Item
396
+ onSelect={() =>
397
+ handleExternalLink('https://github.com/lobehub/lobe-chat/issues/new/choose')
398
+ }
399
+ value="submit-issue"
400
+ >
401
+ <Github className={styles.icon} />
402
+ <div className={styles.itemContent}>
403
+ <div className={styles.itemLabel}>{t('cmdk.submitIssue')}</div>
404
+ </div>
405
+ </Command.Item>
406
+ <Command.Item
407
+ onSelect={() => handleExternalLink('https://github.com/lobehub/lobe-chat')}
408
+ value="star-github"
409
+ >
410
+ <Star className={styles.icon} />
411
+ <div className={styles.itemContent}>
412
+ <div className={styles.itemLabel}>{t('cmdk.starOnGitHub')}</div>
413
+ </div>
414
+ </Command.Item>
415
+ <Command.Item
416
+ onSelect={() => handleExternalLink('https://discord.gg/AYFPHvv2jT')}
417
+ value="discord"
418
+ >
419
+ <MessageCircle className={styles.icon} />
420
+ <div className={styles.itemContent}>
421
+ <div className={styles.itemLabel}>{t('cmdk.communitySupport')}</div>
422
+ </div>
423
+ </Command.Item>
424
+ </Command.Group>
425
+ </>
426
+ )}
427
+
428
+ {page === 'theme' && (
429
+ <>
430
+ <Command.Item onSelect={() => handleThemeChange('light')} value="theme-light">
431
+ <Sun className={styles.icon} />
432
+ <div className={styles.itemContent}>
433
+ <div className={styles.itemLabel}>{t('cmdk.themeLight')}</div>
434
+ </div>
435
+ </Command.Item>
436
+ <Command.Item onSelect={() => handleThemeChange('dark')} value="theme-dark">
437
+ <Moon className={styles.icon} />
438
+ <div className={styles.itemContent}>
439
+ <div className={styles.itemLabel}>{t('cmdk.themeDark')}</div>
440
+ </div>
441
+ </Command.Item>
442
+ <Command.Item onSelect={() => handleThemeChange('auto')} value="theme-auto">
443
+ <Monitor className={styles.icon} />
444
+ <div className={styles.itemContent}>
445
+ <div className={styles.itemLabel}>{t('cmdk.themeAuto')}</div>
446
+ </div>
447
+ </Command.Item>
448
+ </>
449
+ )}
450
+ </Command.List>
451
+ <div className={styles.commandFooter}>
452
+ <div className={styles.kbd}>
453
+ <CornerDownLeft className={styles.kbdIcon} />
454
+ <span>{t('cmdk.toOpen')}</span>
455
+ </div>
456
+ <div className={styles.kbd}>
457
+ <ArrowUpDown className={styles.kbdIcon} />
458
+ <span>{t('cmdk.toSelect')}</span>
459
+ </div>
460
+ </div>
461
+ </Command>
462
+ </div>
463
+ </div>,
464
+ document.body,
465
+ );
466
+ });
467
+
468
+ Cmdk.displayName = 'Cmdk';
469
+
470
+ export default Cmdk;
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+
3
+ import dynamic from 'next/dynamic';
4
+ import { memo } from 'react';
5
+
6
+ // Lazy load the CMDK component with Next.js dynamic import
7
+ // This splits the CMDK code into a separate chunk that only loads when needed
8
+ // ssr: false ensures it only loads on the client side
9
+ const CmdkComponent = dynamic(() => import('./Cmdk'), {
10
+ ssr: false,
11
+ });
12
+
13
+ const CmdkLazy = memo(() => <CmdkComponent />);
14
+
15
+ CmdkLazy.displayName = 'CmdkLazy';
16
+
17
+ export default CmdkLazy;
@@ -10,6 +10,7 @@ import { getAntdLocale } from '@/utils/locale';
10
10
 
11
11
  import AntdV5MonkeyPatch from './AntdV5MonkeyPatch';
12
12
  import AppTheme from './AppTheme';
13
+ import CmdkLazy from './CmdkLazy';
13
14
  import ImportSettings from './ImportSettings';
14
15
  import Locale from './Locale';
15
16
  import QueryProvider from './Query';
@@ -65,6 +66,7 @@ const GlobalLayout = async ({
65
66
  <ImportSettings />
66
67
  {process.env.NODE_ENV === 'development' && <DevPanel />}
67
68
  </Suspense>
69
+ <CmdkLazy />
68
70
  </ServerConfigStoreProvider>
69
71
  </AppTheme>
70
72
  </Locale>