@lobehub/chat 0.100.5 → 0.101.1

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 (83) hide show
  1. package/.eslintrc.js +1 -0
  2. package/CHANGELOG.md +50 -0
  3. package/README.md +2 -2
  4. package/README.zh-CN.md +2 -2
  5. package/__mocks__/zustand/traditional.ts +25 -0
  6. package/package.json +7 -3
  7. package/src/app/api/files/image/imgur.ts +72 -0
  8. package/src/app/api/files/image/route.ts +42 -0
  9. package/src/app/api/openai/chat/createChatCompletion.ts +2 -1
  10. package/src/app/chat/(desktop)/features/ChatInput/DragUpload.tsx +166 -0
  11. package/src/app/chat/(desktop)/features/ChatInput/Footer/LocalFiles.tsx +10 -0
  12. package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +64 -0
  13. package/src/app/chat/(desktop)/features/ChatInput/InputArea.tsx +3 -3
  14. package/src/app/chat/(desktop)/features/ChatInput/index.tsx +38 -30
  15. package/src/app/chat/components/FileList/FileItem.style.ts +81 -0
  16. package/src/app/chat/components/FileList/FileItem.tsx +69 -0
  17. package/src/app/chat/components/FileList/Lightbox.tsx +46 -0
  18. package/src/app/chat/components/FileList/index.tsx +63 -0
  19. package/src/app/chat/features/ChatInput/ActionBar/Clear.tsx +13 -4
  20. package/src/app/chat/features/ChatInput/ActionBar/FileUpload.tsx +53 -0
  21. package/src/app/chat/features/ChatInput/ActionBar/config.ts +3 -1
  22. package/src/app/chat/{(desktop)/features → features}/ChatInput/useSend.ts +5 -1
  23. package/src/app/chat/features/Conversation/ChatList/Messages/User.tsx +23 -0
  24. package/src/app/chat/features/Conversation/ChatList/Messages/index.ts +2 -0
  25. package/src/app/market/(desktop)/features/AgentDetail.tsx +14 -9
  26. package/src/app/market/(desktop)/index.tsx +2 -3
  27. package/src/app/market/(mobile)/index.tsx +2 -3
  28. package/src/app/market/features/AgentCard/AgentCardItem.tsx +4 -1
  29. package/src/app/market/features/AgentCard/index.tsx +20 -31
  30. package/src/app/market/features/AgentDetailContent/AgentInfo/Header.tsx +0 -1
  31. package/src/app/market/features/AgentDetailContent/AgentInfo/index.tsx +2 -3
  32. package/src/app/market/features/AgentSearchBar/index.tsx +9 -4
  33. package/src/app/market/features/TagList/Inner.tsx +28 -0
  34. package/src/app/market/features/TagList/index.tsx +12 -23
  35. package/src/config/__tests__/client.test.ts +76 -0
  36. package/src/config/__tests__/server.test.ts +52 -0
  37. package/src/config/server.ts +8 -0
  38. package/src/const/layoutTokens.ts +1 -1
  39. package/src/const/llm.ts +3 -0
  40. package/src/const/settings.ts +2 -0
  41. package/src/database/core/__tests__/db.test.ts +77 -0
  42. package/src/database/core/__tests__/model.test.ts +55 -0
  43. package/src/database/core/db.ts +25 -0
  44. package/src/database/core/index.ts +1 -0
  45. package/src/database/core/model.ts +49 -0
  46. package/src/database/core/schema.ts +9 -0
  47. package/src/database/models/file.test.ts +71 -0
  48. package/src/database/models/file.ts +26 -0
  49. package/src/locales/default/chat.ts +5 -0
  50. package/src/services/__tests__/file.test.ts +88 -0
  51. package/src/services/chatModel.ts +11 -6
  52. package/src/services/file.ts +36 -0
  53. package/src/store/files/index.ts +2 -0
  54. package/src/store/files/initialState.ts +7 -0
  55. package/src/store/files/selectors.test.ts +75 -0
  56. package/src/store/files/selectors.ts +34 -0
  57. package/src/store/files/slices/images/action.test.ts +186 -0
  58. package/src/store/files/slices/images/action.ts +83 -0
  59. package/src/store/files/slices/images/index.ts +3 -0
  60. package/src/store/files/slices/images/initialState.ts +11 -0
  61. package/src/store/files/store.ts +27 -0
  62. package/src/store/global/selectors/settings.test.ts +1 -3
  63. package/src/store/market/action.ts +9 -1
  64. package/src/store/market/initialState.ts +2 -0
  65. package/src/store/market/selectors.ts +10 -8
  66. package/src/store/session/slices/agentConfig/selectors.ts +7 -1
  67. package/src/store/session/slices/chat/actions/message.ts +78 -17
  68. package/src/store/session/slices/chat/reducers/files.test.ts +38 -0
  69. package/src/store/session/slices/chat/reducers/files.ts +37 -0
  70. package/src/store/session/slices/session/initialState.ts +1 -0
  71. package/src/store/session/slices/session/reducers/session.test.ts +98 -1
  72. package/src/store/session/slices/session/reducers/session.ts +26 -1
  73. package/src/types/chatMessage.ts +3 -2
  74. package/src/types/database/db.ts +4 -0
  75. package/src/types/database/files.ts +38 -0
  76. package/src/types/files.ts +8 -0
  77. package/src/types/market.ts +1 -0
  78. package/src/types/openai/chat.ts +15 -1
  79. package/src/types/session.ts +1 -0
  80. package/tests/setup.ts +8 -0
  81. package/vitest.config.ts +2 -0
  82. package/src/app/chat/(desktop)/features/ChatInput/Footer.tsx +0 -63
  83. package/src/utils/findDuplicates.ts +0 -22
@@ -0,0 +1,81 @@
1
+ import { createStyles } from 'antd-style';
2
+
3
+ export const IMAGE_SIZE = 64;
4
+ const imageBorderRaidus = 8;
5
+
6
+ export const useStyles = createStyles(({ css, cx, token, isDarkMode }) => {
7
+ const closeIcon = cx(css`
8
+ cursor: pointer;
9
+
10
+ position: absolute;
11
+ top: -6px;
12
+ right: -6px;
13
+
14
+ width: 16px;
15
+ height: 16px;
16
+
17
+ color: ${isDarkMode ? token.colorTextQuaternary : token.colorTextTertiary};
18
+
19
+ opacity: 0;
20
+ background: ${isDarkMode ? token.colorTextSecondary : token.colorBgContainer};
21
+ border-radius: 50%;
22
+
23
+ transition: all 0.2s ease-in;
24
+
25
+ &:hover {
26
+ scale: 1.2;
27
+ }
28
+ `);
29
+
30
+ return {
31
+ closeIcon,
32
+ container: css`
33
+ cursor: pointer;
34
+
35
+ position: relative;
36
+
37
+ width: ${IMAGE_SIZE}px;
38
+ height: ${IMAGE_SIZE}px;
39
+
40
+ border-radius: ${imageBorderRaidus}px;
41
+
42
+ &:hover {
43
+ .${closeIcon} {
44
+ opacity: 1;
45
+ }
46
+ }
47
+ `,
48
+ image: css`
49
+ opacity: ${isDarkMode ? 0.6 : 1};
50
+ object-fit: cover;
51
+ border-radius: 8px;
52
+ animation: fade-in 0.3s ease-in;
53
+
54
+ @keyframes fade-in {
55
+ from {
56
+ scale: 1.2;
57
+ opacity: 1;
58
+ }
59
+
60
+ to {
61
+ scale: 1;
62
+ opacity: 1;
63
+ }
64
+ }
65
+ `,
66
+ imageCtn: css`
67
+ position: relative;
68
+ `,
69
+ imageWrapper: css`
70
+ overflow: hidden;
71
+ width: ${IMAGE_SIZE}px;
72
+ height: ${IMAGE_SIZE}px;
73
+ border-radius: ${imageBorderRaidus}px;
74
+ `,
75
+ notFound: css`
76
+ color: ${token.colorTextSecondary};
77
+ background: ${token.colorFillTertiary};
78
+ border-radius: ${imageBorderRaidus}px;
79
+ `,
80
+ };
81
+ });
@@ -0,0 +1,69 @@
1
+ import { CloseCircleFilled } from '@ant-design/icons';
2
+ import { Icon } from '@lobehub/ui';
3
+ import { Skeleton } from 'antd';
4
+ import { LucideImageOff } from 'lucide-react';
5
+ import Image from 'next/image';
6
+ import { memo } from 'react';
7
+ import { Center, Flexbox } from 'react-layout-kit';
8
+
9
+ import { useFileStore } from '@/store/files';
10
+
11
+ import { IMAGE_SIZE, useStyles } from './FileItem.style';
12
+
13
+ const FileItem = memo<{ editable: boolean; id: string; onClick: () => void }>(
14
+ ({ editable, id, onClick }) => {
15
+ const { styles } = useStyles();
16
+ const [useFetchFile, removeFile] = useFileStore((s) => [s.useFetchFile, s.removeFile]);
17
+
18
+ const { data, isLoading } = useFetchFile(id);
19
+
20
+ return (
21
+ <Flexbox className={styles.container} onClick={onClick}>
22
+ {isLoading ? (
23
+ <Skeleton
24
+ active
25
+ title={{
26
+ style: { borderRadius: 8, height: IMAGE_SIZE },
27
+ width: IMAGE_SIZE,
28
+ }}
29
+ />
30
+ ) : (
31
+ <Flexbox className={styles.imageCtn}>
32
+ <div className={styles.imageWrapper}>
33
+ {data ? (
34
+ <Image
35
+ alt={data.name || ''}
36
+ className={styles.image}
37
+ fetchPriority={'high'}
38
+ height={IMAGE_SIZE}
39
+ loading={'lazy'}
40
+ src={data.url}
41
+ width={IMAGE_SIZE}
42
+ />
43
+ ) : (
44
+ <Center className={styles.notFound} height={'100%'}>
45
+ <Icon icon={LucideImageOff} size={{ fontSize: 28 }} />
46
+ </Center>
47
+ )}
48
+ </div>
49
+ </Flexbox>
50
+ )}
51
+ {/* only show close icon when editable */}
52
+ {editable && (
53
+ <Center
54
+ className={styles.closeIcon}
55
+ onClick={(e) => {
56
+ e.stopPropagation();
57
+
58
+ removeFile(id);
59
+ }}
60
+ >
61
+ <CloseCircleFilled />
62
+ </Center>
63
+ )}
64
+ </Flexbox>
65
+ );
66
+ },
67
+ );
68
+
69
+ export default FileItem;
@@ -0,0 +1,46 @@
1
+ import { createStyles } from 'antd-style';
2
+ import { memo } from 'react';
3
+ import Lightbox from 'react-spring-lightbox';
4
+
5
+ import { filesSelectors, useFileStore } from '@/store/files';
6
+
7
+ const useStyles = createStyles(({ css, token }) => ({
8
+ wrapper: css`
9
+ background: ${token.colorBgMask};
10
+ backdrop-filter: blur(4px);
11
+ `,
12
+ }));
13
+
14
+ interface LightBoxProps {
15
+ index: number;
16
+ items: string[];
17
+ onNext: () => void;
18
+ onOpenChange: (open: boolean) => void;
19
+ onPrev: () => void;
20
+ open: boolean;
21
+ }
22
+
23
+ const LightBox = memo<LightBoxProps>(({ onOpenChange, open, items, index, onNext, onPrev }) => {
24
+ const { styles } = useStyles();
25
+
26
+ const list = useFileStore(filesSelectors.getImageDetailByList(items));
27
+ return (
28
+ <Lightbox
29
+ className={styles.wrapper}
30
+ currentIndex={index}
31
+ images={list.map((i) => ({
32
+ alt: i.name,
33
+ loading: 'lazy',
34
+ src: i.url,
35
+ }))}
36
+ isOpen={open}
37
+ onClose={() => {
38
+ onOpenChange(false);
39
+ }}
40
+ onNext={onNext}
41
+ onPrev={onPrev}
42
+ />
43
+ );
44
+ });
45
+
46
+ export default LightBox;
@@ -0,0 +1,63 @@
1
+ import { memo, useState } from 'react';
2
+ import { Flexbox } from 'react-layout-kit';
3
+
4
+ import FileItem from './FileItem';
5
+ import Lightbox from './Lightbox';
6
+
7
+ // TODO:后续看情况是否做图片过多的滚动功能
8
+ // const useStyles = createStyles(({ css }) => ({
9
+ // container: css`
10
+ // width: ${IMAGE_SIZE * 5 + 4 * 8 + IMAGE_SIZE / 2}px;
11
+ // overflow-x: scroll;
12
+ // height: ${IMAGE_SIZE + 12}px;
13
+ // `,
14
+ // }));
15
+
16
+ interface FileListProps {
17
+ editable?: boolean;
18
+ items: string[];
19
+ }
20
+
21
+ const FileList = memo<FileListProps>(({ items, editable = true }) => {
22
+ // const { styles } = useStyles();
23
+
24
+ const [showLightbox, setShowLightbox] = useState(false);
25
+ const [currentImageIndex, setCurrentIndex] = useState(0);
26
+
27
+ const gotoPrevious = () => currentImageIndex > 0 && setCurrentIndex(currentImageIndex - 1);
28
+ const gotoNext = () =>
29
+ currentImageIndex + 1 < items.length && setCurrentIndex(currentImageIndex + 1);
30
+
31
+ return (
32
+ <>
33
+ <Flexbox
34
+ align={'flex-end'}
35
+ // className={styles.container}
36
+ gap={8}
37
+ horizontal
38
+ >
39
+ {items.map((i, index) => (
40
+ <FileItem
41
+ editable={editable}
42
+ id={i}
43
+ key={i}
44
+ onClick={() => {
45
+ setCurrentIndex(index);
46
+ setShowLightbox(true);
47
+ }}
48
+ />
49
+ ))}
50
+ </Flexbox>
51
+ <Lightbox
52
+ index={currentImageIndex}
53
+ items={items}
54
+ onNext={gotoNext}
55
+ onOpenChange={setShowLightbox}
56
+ onPrev={gotoPrevious}
57
+ open={showLightbox}
58
+ />
59
+ </>
60
+ );
61
+ });
62
+
63
+ export default FileList;
@@ -1,20 +1,27 @@
1
1
  import { ActionIcon } from '@lobehub/ui';
2
2
  import { Popconfirm } from 'antd';
3
3
  import { Eraser } from 'lucide-react';
4
- import { memo } from 'react';
4
+ import { memo, useCallback } from 'react';
5
5
  import { useHotkeys } from 'react-hotkeys-hook';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import HotKeys from '@/components/HotKeys';
9
9
  import { CLEAN_MESSAGE_KEY, PREFIX_KEY } from '@/const/hotkeys';
10
+ import { useFileStore } from '@/store/files';
10
11
  import { useSessionStore } from '@/store/session';
11
12
 
12
13
  const Clear = memo(() => {
13
14
  const { t } = useTranslation('setting');
14
- const [clearMessage] = useSessionStore((s) => [s.clearMessage, s.updateAgentConfig]);
15
+ const [clearMessage] = useSessionStore((s) => [s.clearMessage]);
16
+ const [clearImageList] = useFileStore((s) => [s.clearImageList]);
15
17
  const hotkeys = [PREFIX_KEY, CLEAN_MESSAGE_KEY].join('+');
16
18
 
17
- useHotkeys(hotkeys, clearMessage, {
19
+ const resetConversation = useCallback(() => {
20
+ clearMessage();
21
+ clearImageList();
22
+ }, []);
23
+
24
+ useHotkeys(hotkeys, resetConversation, {
18
25
  preventDefault: true,
19
26
  });
20
27
 
@@ -23,7 +30,9 @@ const Clear = memo(() => {
23
30
  cancelText={t('cancel', { ns: 'common' })}
24
31
  okButtonProps={{ danger: true }}
25
32
  okText={t('ok', { ns: 'common' })}
26
- onConfirm={() => clearMessage()}
33
+ onConfirm={() => {
34
+ resetConversation();
35
+ }}
27
36
  placement={'topRight'}
28
37
  title={t('confirmClearCurrentMessages', { ns: 'chat' })}
29
38
  >
@@ -0,0 +1,53 @@
1
+ import { ActionIcon, Icon } from '@lobehub/ui';
2
+ import { Upload } from 'antd';
3
+ import { useTheme } from 'antd-style';
4
+ import { LucideImage, LucideLoader2 } from 'lucide-react';
5
+ import { memo, useState } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Center } from 'react-layout-kit';
8
+
9
+ import { useFileStore } from '@/store/files';
10
+ import { useSessionStore } from '@/store/session';
11
+ import { agentSelectors } from '@/store/session/slices/agentConfig';
12
+
13
+ const FileUpload = memo(() => {
14
+ const { t } = useTranslation('chat');
15
+ const [loading, setLoading] = useState(false);
16
+ const theme = useTheme();
17
+ const upload = useFileStore((s) => s.uploadFile);
18
+
19
+ const canUpload = useSessionStore(agentSelectors.modelHasVisionAbility);
20
+
21
+ if (!canUpload) return null;
22
+
23
+ return (
24
+ <Upload
25
+ accept="image/*"
26
+ beforeUpload={async (file) => {
27
+ setLoading(true);
28
+
29
+ await upload(file);
30
+
31
+ setLoading(false);
32
+ return false;
33
+ }}
34
+ multiple={true}
35
+ showUploadList={false}
36
+ >
37
+ {loading ? (
38
+ <Center height={36} width={36}>
39
+ <Icon
40
+ color={theme.colorTextSecondary}
41
+ icon={LucideLoader2}
42
+ size={{ fontSize: 18 }}
43
+ spin
44
+ ></Icon>
45
+ </Center>
46
+ ) : (
47
+ <ActionIcon icon={LucideImage} placement={'bottom'} title={t('upload.actionTooltip')} />
48
+ )}
49
+ </Upload>
50
+ );
51
+ });
52
+
53
+ export default FileUpload;
@@ -1,6 +1,7 @@
1
1
  import { FC } from 'react';
2
2
 
3
3
  import Clear from './Clear';
4
+ import FileUpload from './FileUpload';
4
5
  import History from './History';
5
6
  import ModelSwitch from './ModelSwitch';
6
7
  import Temperature from './Temperature';
@@ -8,6 +9,7 @@ import Token from './Token';
8
9
 
9
10
  export const actionMap: Record<string, FC> = {
10
11
  clear: Clear,
12
+ fileUpload: FileUpload,
11
13
  history: History,
12
14
  model: ModelSwitch,
13
15
  temperature: Temperature,
@@ -15,5 +17,5 @@ export const actionMap: Record<string, FC> = {
15
17
  };
16
18
 
17
19
  // we can make these action lists configurable in the future
18
- export const leftActionList = ['model', 'temperature', 'history', 'token'];
20
+ export const leftActionList = ['model', 'fileUpload', 'temperature', 'history', 'token'];
19
21
  export const rightActionList = ['clear'];
@@ -1,5 +1,6 @@
1
1
  import { useCallback } from 'react';
2
2
 
3
+ import { filesSelectors, useFileStore } from '@/store/files';
3
4
  import { useSessionStore } from '@/store/session';
4
5
 
5
6
  export const useSendMessage = () => {
@@ -11,7 +12,10 @@ export const useSendMessage = () => {
11
12
  return useCallback(() => {
12
13
  const store = useSessionStore.getState();
13
14
  if (!!store.chatLoadingId) return;
14
- sendMessage(store.inputMessage);
15
+ const imageList = filesSelectors.imageUrlOrBase64List(useFileStore.getState());
16
+
17
+ sendMessage(store.inputMessage, imageList);
15
18
  updateInputMessage('');
19
+ useFileStore.getState().clearImageList();
16
20
  }, []);
17
21
  };
@@ -0,0 +1,23 @@
1
+ import { ReactNode, memo } from 'react';
2
+ import { Flexbox } from 'react-layout-kit';
3
+
4
+ import FileList from '@/app/chat/components/FileList';
5
+ import { LOADING_FLAT } from '@/const/message';
6
+ import { ChatMessage } from '@/types/chatMessage';
7
+
8
+ import BubblesLoading from '../Loading';
9
+
10
+ export const UserMessage = memo<
11
+ ChatMessage & {
12
+ editableContent: ReactNode;
13
+ }
14
+ >(({ id, editableContent, content, ...res }) => {
15
+ if (content === LOADING_FLAT) return <BubblesLoading />;
16
+
17
+ return (
18
+ <Flexbox gap={8} id={id}>
19
+ {editableContent}
20
+ {res.files && <FileList editable={false} items={res.files} />}
21
+ </Flexbox>
22
+ );
23
+ });
@@ -10,11 +10,13 @@ import { pathString } from '@/utils/url';
10
10
  import { AssistantMessage } from './Assistant';
11
11
  import { DefaultMessage } from './Default';
12
12
  import { FunctionMessage } from './Function';
13
+ import { UserMessage } from './User';
13
14
 
14
15
  export const renderMessages: ChatListProps['renderMessages'] = {
15
16
  assistant: AssistantMessage,
16
17
  default: DefaultMessage,
17
18
  function: FunctionMessage,
19
+ user: UserMessage,
18
20
  };
19
21
 
20
22
  export const useAvatarsClick = (): ChatListProps['onAvatarsClick'] => {
@@ -1,6 +1,6 @@
1
1
  import { DraggablePanel, DraggablePanelBody, DraggablePanelContainer } from '@lobehub/ui';
2
2
  import { createStyles } from 'antd-style';
3
- import { memo, useState } from 'react';
3
+ import { memo, useCallback, useState } from 'react';
4
4
 
5
5
  import SafeSpacing from '@/components/SafeSpacing';
6
6
  import { MARKET_SIDEBAR_WIDTH } from '@/const/layoutTokens';
@@ -31,6 +31,18 @@ const SideBar = memo(() => {
31
31
  s.activateAgent,
32
32
  ]);
33
33
 
34
+ const handleExpandChange = useCallback(
35
+ (show: boolean) => {
36
+ if (!show) {
37
+ setTempId(useMarketStore.getState().currentIdentifier);
38
+ deactivateAgent();
39
+ } else if (tempId) {
40
+ activateAgent(tempId);
41
+ }
42
+ },
43
+ [deactivateAgent, activateAgent, tempId],
44
+ );
45
+
34
46
  return (
35
47
  <DraggablePanel
36
48
  className={styles.drawer}
@@ -40,14 +52,7 @@ const SideBar = memo(() => {
40
52
  expand={showAgentSidebar}
41
53
  minWidth={MARKET_SIDEBAR_WIDTH}
42
54
  mode={'fixed'}
43
- onExpandChange={(show) => {
44
- if (!show) {
45
- setTempId(useMarketStore.getState().currentIdentifier);
46
- deactivateAgent();
47
- } else if (tempId) {
48
- activateAgent(tempId);
49
- }
50
- }}
55
+ onExpandChange={handleExpandChange}
51
56
  placement={'right'}
52
57
  >
53
58
  <DraggablePanelContainer
@@ -6,18 +6,17 @@ import { FC, memo } from 'react';
6
6
 
7
7
  import AgentCard from '@/app/market/features/AgentCard';
8
8
  import ResponsiveIndex from '@/components/ResponsiveIndex';
9
- import { AgentsMarketIndexItem } from '@/types/market';
10
9
 
11
10
  import Index from '../index';
12
11
  import Layout from './layout.desktop';
13
12
 
14
13
  const Mobile: FC = dynamic(() => import('../(mobile)'), { ssr: false }) as FC;
15
14
 
16
- export default memo<{ defaultAgents?: AgentsMarketIndexItem[] }>(({ defaultAgents }) => (
15
+ export default memo(() => (
17
16
  <ResponsiveIndex Mobile={Mobile}>
18
17
  <Layout>
19
18
  <Index />
20
- <AgentCard CardRender={SpotlightCard} defaultAgents={defaultAgents} />
19
+ <AgentCard CardRender={SpotlightCard} />
21
20
  </Layout>
22
21
  </ResponsiveIndex>
23
22
  ));
@@ -3,15 +3,14 @@
3
3
  import { memo } from 'react';
4
4
 
5
5
  import AgentCard from '@/app/market/features/AgentCard';
6
- import { AgentsMarketIndexItem } from '@/types/market';
7
6
 
8
7
  import Index from '../index';
9
8
  import CardRender from './features/AgentCard';
10
9
  import Layout from './layout.mobile';
11
10
 
12
- export default memo<{ defaultAgents?: AgentsMarketIndexItem[] }>(({ defaultAgents }) => (
11
+ export default memo(() => (
13
12
  <Layout>
14
13
  <Index />
15
- <AgentCard CardRender={CardRender} defaultAgents={defaultAgents} mobile />
14
+ <AgentCard CardRender={CardRender} mobile />
16
15
  </Layout>
17
16
  ));
@@ -13,13 +13,16 @@ import AgentCardBanner from './AgentCardBanner';
13
13
  import { useStyles } from './style';
14
14
 
15
15
  const { Paragraph } = Typography;
16
+
16
17
  const AgentCardItem = memo<AgentsMarketIndexItem>(({ meta, identifier }) => {
17
18
  const ref = useRef(null);
18
19
  const isHovering = useHover(ref);
19
20
  const onAgentCardClick = useMarketStore((s) => s.activateAgent);
20
- const { avatar, title, description, tags, backgroundColor } = meta;
21
21
  const { styles, theme } = useStyles();
22
22
  const { isDarkMode } = useThemeMode();
23
+
24
+ const { avatar, title, description, tags, backgroundColor } = meta;
25
+
23
26
  return (
24
27
  <Flexbox className={styles.container} onClick={() => onAgentCardClick(identifier)}>
25
28
  <AgentCardBanner meta={meta} style={{ opacity: isDarkMode ? 0.9 : 0.4 }} />
@@ -1,71 +1,60 @@
1
1
  import { SpotlightCardProps } from '@lobehub/ui';
2
- import { FC, memo, useMemo } from 'react';
2
+ import isEqual from 'fast-deep-equal';
3
+ import { FC, memo, useCallback } from 'react';
3
4
  import { useTranslation } from 'react-i18next';
4
5
  import { Flexbox } from 'react-layout-kit';
5
6
  import LazyLoad from 'react-lazy-load';
6
7
 
7
8
  import { agentMarketSelectors, useMarketStore } from '@/store/market';
8
- import { AgentsMarketIndexItem } from '@/types/market';
9
9
 
10
10
  import TagList from '../TagList';
11
11
  import AgentCardItem from './AgentCardItem';
12
12
  import Loading from './Loading';
13
13
  import { useStyles } from './style';
14
14
 
15
- const gridRender: SpotlightCardProps['renderItem'] = (item) => {
16
- // eslint-disable-next-line react-hooks/rules-of-hooks
17
- const { styles } = useStyles();
18
- return (
19
- <LazyLoad className={styles.lazy}>
20
- <AgentCardItem {...item} />
21
- </LazyLoad>
22
- );
23
- };
24
-
25
15
  export interface AgentCardProps {
26
16
  CardRender: FC<SpotlightCardProps>;
27
- defaultAgents?: AgentsMarketIndexItem[];
28
17
  mobile?: boolean;
29
18
  }
30
19
 
31
- const AgentCard = memo<AgentCardProps>(({ defaultAgents, CardRender, mobile }) => {
20
+ const AgentCard = memo<AgentCardProps>(({ CardRender, mobile }) => {
32
21
  const { t } = useTranslation('market');
33
-
34
- const { styles } = useStyles();
35
-
36
22
  const [useFetchAgentList, keywords] = useMarketStore((s) => [
37
23
  s.useFetchAgentList,
38
24
  s.searchKeywords,
39
25
  ]);
40
- useFetchAgentList();
41
-
42
- const clientAgentList = useMarketStore(agentMarketSelectors.getAgentList);
43
- const agentList = clientAgentList.length === 0 && defaultAgents ? defaultAgents : clientAgentList;
26
+ const { isLoading } = useFetchAgentList();
27
+ const agentList = useMarketStore(agentMarketSelectors.getAgentList, isEqual);
28
+ const { styles } = useStyles();
44
29
 
45
- const agentListData = useMemo(() => {
46
- if (!keywords) return agentList;
47
- return agentList.filter(({ meta }) => JSON.stringify(meta).toLowerCase().includes(keywords));
48
- }, [agentList, keywords]);
30
+ const GridRender: SpotlightCardProps['renderItem'] = useCallback(
31
+ (props: any) => (
32
+ <LazyLoad className={styles.lazy}>
33
+ <AgentCardItem {...props} />
34
+ </LazyLoad>
35
+ ),
36
+ [styles.lazy],
37
+ );
49
38
 
50
- if (agentList.length === 0) return <Loading />;
39
+ if (isLoading) return <Loading />;
51
40
 
52
41
  return (
53
42
  <Flexbox gap={mobile ? 16 : 24}>
54
43
  <TagList />
55
44
  {keywords ? (
56
45
  <CardRender
57
- items={agentListData}
58
- renderItem={gridRender}
46
+ items={agentList}
47
+ renderItem={GridRender}
59
48
  spotlight={mobile ? undefined : false}
60
49
  />
61
50
  ) : (
62
51
  <>
63
52
  <div className={styles.subTitle}>{t('title.recentSubmits')}</div>
64
- <CardRender items={agentListData.slice(0, 3)} renderItem={gridRender} />
53
+ <CardRender items={agentList.slice(0, 3)} renderItem={GridRender} />
65
54
  <div className={styles.subTitle}>{t('title.allAgents')}</div>
66
55
  <CardRender
67
- items={agentListData.slice(3)}
68
- renderItem={gridRender}
56
+ items={agentList.slice(3)}
57
+ renderItem={GridRender}
69
58
  spotlight={mobile ? undefined : false}
70
59
  />
71
60
  </>
@@ -17,7 +17,6 @@ const { Link } = Typography;
17
17
  const Header = memo(() => {
18
18
  const { t } = useTranslation('market');
19
19
  const { styles, theme } = useStyles();
20
-
21
20
  const createSession = useSessionStore((s) => s.createSession);
22
21
  const switchSideBar = useGlobalStore((s) => s.switchSideBar);
23
22
  const agentItem = useMarketStore(agentMarketSelectors.currentAgentItem);
@@ -22,11 +22,10 @@ const AgentModalInner = memo(() => {
22
22
  s.useFetchAgent,
23
23
  s.currentIdentifier,
24
24
  ]);
25
- const { styles } = useStyles();
26
-
27
- const { data, isLoading } = useFetchAgent(currentIdentifier);
28
25
  const { t } = useTranslation('market');
29
26
  const [tab, setTab] = useState<string>(InfoTabs.prompt);
27
+ const { data, isLoading } = useFetchAgent(currentIdentifier);
28
+ const { styles } = useStyles();
30
29
 
31
30
  if (isLoading || !data?.meta) return <Loading />;
32
31