@lobehub/chat 0.101.0 → 0.101.2

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 (26) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/package.json +1 -1
  3. package/src/app/chat/(desktop)/features/ChatInput/InputArea.tsx +5 -44
  4. package/src/app/chat/(mobile)/features/ChatInput/Files.tsx +17 -0
  5. package/src/app/chat/(mobile)/features/ChatInput/Mobile.tsx +14 -68
  6. package/src/app/chat/(mobile)/features/ChatInput/SendButton.tsx +30 -0
  7. package/src/app/chat/(mobile)/features/ChatInput/index.tsx +4 -17
  8. package/src/app/chat/components/FileList/FileItem.style.ts +3 -0
  9. package/src/app/chat/components/FileList/FileItem.tsx +56 -52
  10. package/src/app/chat/components/FileList/index.tsx +5 -1
  11. package/src/app/chat/features/ChatInput/InputAreaInner/index.tsx +57 -0
  12. package/src/app/market/(desktop)/features/AgentDetail.tsx +14 -9
  13. package/src/app/market/(desktop)/index.tsx +2 -3
  14. package/src/app/market/(mobile)/index.tsx +2 -3
  15. package/src/app/market/features/AgentCard/AgentCardItem.tsx +4 -1
  16. package/src/app/market/features/AgentCard/index.tsx +20 -31
  17. package/src/app/market/features/AgentDetailContent/AgentInfo/Header.tsx +0 -1
  18. package/src/app/market/features/AgentDetailContent/AgentInfo/index.tsx +2 -3
  19. package/src/app/market/features/AgentSearchBar/index.tsx +9 -4
  20. package/src/app/market/features/TagList/Inner.tsx +28 -0
  21. package/src/app/market/features/TagList/index.tsx +12 -23
  22. package/src/store/market/action.ts +9 -1
  23. package/src/store/market/initialState.ts +2 -0
  24. package/src/store/market/selectors.ts +10 -8
  25. package/src/types/market.ts +1 -0
  26. package/src/utils/findDuplicates.ts +0 -22
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.101.2](https://github.com/lobehub/lobe-chat/compare/v0.101.1...v0.101.2)
6
+
7
+ <sup>Released on **2023-11-14**</sup>
8
+
9
+ #### 💄 Styles
10
+
11
+ - **misc**: upload image to vision model adapting to mobile device.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Styles
19
+
20
+ - **misc**: upload image to vision model adapting to mobile device, closes [#457](https://github.com/lobehub/lobe-chat/issues/457) ([9c4f4ee](https://github.com/lobehub/lobe-chat/commit/9c4f4ee))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ### [Version 0.101.1](https://github.com/lobehub/lobe-chat/compare/v0.101.0...v0.101.1)
31
+
32
+ <sup>Released on **2023-11-14**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **misc**: Fix market search (fix.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **misc**: Fix market search (fix, closes [#437](https://github.com/lobehub/lobe-chat/issues/437) ([178b742](https://github.com/lobehub/lobe-chat/commit/178b742))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ## [Version 0.101.0](https://github.com/lobehub/lobe-chat/compare/v0.100.5...v0.101.0)
6
56
 
7
57
  <sup>Released on **2023-11-14**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.101.0",
3
+ "version": "0.101.2",
4
4
  "description": "An open-source, extensible (Function Calling), high-performance chatbot framework. It supports one-click free deployment of your private ChatGPT/LLM web application",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -1,11 +1,7 @@
1
- import { TextArea } from '@lobehub/ui';
2
1
  import { createStyles } from 'antd-style';
3
- import { memo, useRef } from 'react';
4
- import { useTranslation } from 'react-i18next';
2
+ import { memo } from 'react';
5
3
 
6
- import { useSessionStore } from '@/store/session';
7
-
8
- import { useSendMessage } from '../../../features/ChatInput/useSend';
4
+ import InputAreaInner from '@/app/chat/features/ChatInput/InputAreaInner';
9
5
 
10
6
  const useStyles = createStyles(({ css }) => {
11
7
  return {
@@ -22,46 +18,11 @@ const useStyles = createStyles(({ css }) => {
22
18
  });
23
19
 
24
20
  const InputArea = memo(() => {
25
- const { t } = useTranslation('common');
26
- const { cx, styles } = useStyles();
27
-
28
- const isChineseInput = useRef(false);
29
-
30
- const [loading, message, updateInputMessage] = useSessionStore((s) => [
31
- !!s.chatLoadingId,
32
- s.inputMessage,
33
- s.updateInputMessage,
34
- ]);
35
-
36
- const handleSend = useSendMessage();
21
+ const { styles } = useStyles();
37
22
 
38
23
  return (
39
- <div className={cx(styles.textareaContainer)}>
40
- <TextArea
41
- className={styles.textarea}
42
- onBlur={(e) => {
43
- updateInputMessage(e.target.value);
44
- }}
45
- onChange={(e) => {
46
- updateInputMessage(e.target.value);
47
- }}
48
- onCompositionEnd={() => {
49
- isChineseInput.current = false;
50
- }}
51
- onCompositionStart={() => {
52
- isChineseInput.current = true;
53
- }}
54
- onPressEnter={(e) => {
55
- if (!loading && !e.shiftKey && !isChineseInput.current) {
56
- e.preventDefault();
57
- handleSend();
58
- }
59
- }}
60
- placeholder={t('sendPlaceholder', { ns: 'chat' })}
61
- resize={false}
62
- type="pure"
63
- value={message}
64
- />
24
+ <div className={styles.textareaContainer}>
25
+ <InputAreaInner className={styles.textarea} />
65
26
  </div>
66
27
  );
67
28
  });
@@ -0,0 +1,17 @@
1
+ import { memo } from 'react';
2
+ import { Flexbox } from 'react-layout-kit';
3
+
4
+ import FileList from '@/app/chat/components/FileList';
5
+ import { useFileStore } from '@/store/files';
6
+
7
+ const Files = memo(() => {
8
+ const inputFilesList = useFileStore((s) => s.inputFilesList);
9
+
10
+ return (
11
+ <Flexbox padding={12}>
12
+ <FileList alwaysShowClose items={inputFilesList} />
13
+ </Flexbox>
14
+ );
15
+ });
16
+
17
+ export default Files;
@@ -1,79 +1,25 @@
1
- import { Icon, Input } from '@lobehub/ui';
2
- import { Button, type InputRef } from 'antd';
3
- import { Loader2, SendHorizonal } from 'lucide-react';
4
- import { forwardRef, useCallback, useRef } from 'react';
5
- import { useTranslation } from 'react-i18next';
1
+ import { memo } from 'react';
6
2
  import { Flexbox } from 'react-layout-kit';
7
- import useControlledState from 'use-merge-value';
8
3
 
9
4
  import ActionBar from '@/app/chat/features/ChatInput/ActionBar';
5
+ import InputAreaInner from '@/app/chat/features/ChatInput/InputAreaInner';
10
6
  import SaveTopic from '@/app/chat/features/ChatInput/Topic';
11
7
 
8
+ import SendButton from './SendButton';
12
9
  import { useStyles } from './style.mobile';
13
10
 
14
- export type ChatInputAreaMobile = {
15
- loading?: boolean;
16
- onChange?: (value: string) => void;
17
- onSend?: (value: string) => void;
18
- onStop?: () => void;
19
- value?: string;
20
- };
11
+ const ChatInputArea = memo(() => {
12
+ const { cx, styles } = useStyles();
21
13
 
22
- const ChatInputArea = forwardRef<InputRef, ChatInputAreaMobile>(
23
- ({ onSend, loading, onChange, onStop, value }) => {
24
- const { t } = useTranslation('chat');
25
- const [currentValue, setCurrentValue] = useControlledState<string>('', {
26
- onChange: onChange,
27
- value,
28
- });
29
- const { cx, styles } = useStyles();
30
- const isChineseInput = useRef(false);
31
-
32
- const handleSend = useCallback(() => {
33
- if (loading) return;
34
- if (onSend) onSend(currentValue);
35
- setCurrentValue('');
36
- }, [currentValue]);
37
-
38
- return (
39
- <Flexbox className={cx(styles.container)} gap={12}>
40
- <ActionBar rightAreaStartRender={<SaveTopic />} />
41
- <Flexbox className={styles.inner} gap={8} horizontal>
42
- <Input
43
- className={cx(styles.input)}
44
- onBlur={(e) => {
45
- setCurrentValue(e.target.value);
46
- }}
47
- onChange={(e) => {
48
- setCurrentValue(e.target.value);
49
- }}
50
- onCompositionEnd={() => {
51
- isChineseInput.current = false;
52
- }}
53
- onCompositionStart={() => {
54
- isChineseInput.current = true;
55
- }}
56
- onPressEnter={(e) => {
57
- if (!loading && !e.shiftKey && !isChineseInput.current) {
58
- e.preventDefault();
59
- handleSend();
60
- }
61
- }}
62
- placeholder={t('sendPlaceholder')}
63
- type={'block'}
64
- value={currentValue}
65
- />
66
- <div>
67
- {loading ? (
68
- <Button icon={loading && <Icon icon={Loader2} spin />} onClick={onStop} />
69
- ) : (
70
- <Button icon={<Icon icon={SendHorizonal} />} onClick={handleSend} type={'primary'} />
71
- )}
72
- </div>
73
- </Flexbox>
14
+ return (
15
+ <Flexbox className={cx(styles.container)} gap={12}>
16
+ <ActionBar rightAreaStartRender={<SaveTopic />} />
17
+ <Flexbox className={styles.inner} gap={8} horizontal>
18
+ <InputAreaInner mobile />
19
+ <SendButton />
74
20
  </Flexbox>
75
- );
76
- },
77
- );
21
+ </Flexbox>
22
+ );
23
+ });
78
24
 
79
25
  export default ChatInputArea;
@@ -0,0 +1,30 @@
1
+ import { Icon } from '@lobehub/ui';
2
+ import { Button } from 'antd';
3
+ import { Loader2, SendHorizonal } from 'lucide-react';
4
+ import { memo } from 'react';
5
+
6
+ import { useSendMessage } from '@/app/chat/features/ChatInput/useSend';
7
+ import { useSessionStore } from '@/store/session';
8
+
9
+ const SendButton = memo(() => {
10
+ const [loading, onStop] = useSessionStore((s) => [!!s.chatLoadingId, s.stopGenerateMessage]);
11
+
12
+ const handleSend = useSendMessage();
13
+
14
+ return loading ? (
15
+ <Button
16
+ icon={loading && <Icon icon={Loader2} spin />}
17
+ onClick={onStop}
18
+ style={{ flex: 'none' }}
19
+ />
20
+ ) : (
21
+ <Button
22
+ icon={<Icon icon={SendHorizonal} />}
23
+ onClick={handleSend}
24
+ style={{ flex: 'none' }}
25
+ type={'primary'}
26
+ />
27
+ );
28
+ });
29
+
30
+ export default SendButton;
@@ -1,10 +1,10 @@
1
1
  import { createStyles } from 'antd-style';
2
- import { memo, useState } from 'react';
2
+ import { memo } from 'react';
3
3
 
4
4
  import SafeSpacing from '@/components/SafeSpacing';
5
5
  import { CHAT_TEXTAREA_HEIGHT_MOBILE } from '@/const/layoutTokens';
6
- import { useSessionStore } from '@/store/session';
7
6
 
7
+ import Files from './Files';
8
8
  import ChatInputArea from './Mobile';
9
9
 
10
10
  const useStyles = createStyles(
@@ -23,25 +23,12 @@ const useStyles = createStyles(
23
23
  const ChatInputMobileLayout = memo(() => {
24
24
  const { styles } = useStyles();
25
25
 
26
- const [message, setMessage] = useState('');
27
-
28
- const [isLoading, sendMessage, stopGenerateMessage] = useSessionStore((s) => [
29
- !!s.chatLoadingId,
30
- s.sendMessage,
31
- s.stopGenerateMessage,
32
- ]);
33
-
34
26
  return (
35
27
  <>
28
+ <Files />
36
29
  <SafeSpacing height={CHAT_TEXTAREA_HEIGHT_MOBILE} mobile position={'bottom'} />
37
30
  <div className={styles}>
38
- <ChatInputArea
39
- loading={isLoading}
40
- onChange={setMessage}
41
- onSend={sendMessage}
42
- onStop={stopGenerateMessage}
43
- value={message}
44
- />
31
+ <ChatInputArea />
45
32
  </div>
46
33
  </>
47
34
  );
@@ -28,6 +28,9 @@ export const useStyles = createStyles(({ css, cx, token, isDarkMode }) => {
28
28
  `);
29
29
 
30
30
  return {
31
+ alwaysShowClose: css`
32
+ opacity: 1 !important;
33
+ `,
31
34
  closeIcon,
32
35
  container: css`
33
36
  cursor: pointer;
@@ -10,60 +10,64 @@ import { useFileStore } from '@/store/files';
10
10
 
11
11
  import { IMAGE_SIZE, useStyles } from './FileItem.style';
12
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]);
13
+ interface FileItemProps {
14
+ alwaysShowClose?: boolean;
15
+ editable: boolean;
16
+ id: string;
17
+ onClick: () => void;
18
+ }
19
+ const FileItem = memo<FileItemProps>(({ editable, id, onClick, alwaysShowClose }) => {
20
+ const { styles, cx } = useStyles();
21
+ const [useFetchFile, removeFile] = useFileStore((s) => [s.useFetchFile, s.removeFile]);
17
22
 
18
- const { data, isLoading } = useFetchFile(id);
23
+ const { data, isLoading } = useFetchFile(id);
19
24
 
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();
25
+ return (
26
+ <Flexbox className={styles.container} onClick={onClick}>
27
+ {isLoading ? (
28
+ <Skeleton
29
+ active
30
+ title={{
31
+ style: { borderRadius: 8, height: IMAGE_SIZE },
32
+ width: IMAGE_SIZE,
33
+ }}
34
+ />
35
+ ) : (
36
+ <Flexbox className={styles.imageCtn}>
37
+ <div className={styles.imageWrapper}>
38
+ {data ? (
39
+ <Image
40
+ alt={data.name || ''}
41
+ className={styles.image}
42
+ fetchPriority={'high'}
43
+ height={IMAGE_SIZE}
44
+ loading={'lazy'}
45
+ src={data.url}
46
+ width={IMAGE_SIZE}
47
+ />
48
+ ) : (
49
+ <Center className={styles.notFound} height={'100%'}>
50
+ <Icon icon={LucideImageOff} size={{ fontSize: 28 }} />
51
+ </Center>
52
+ )}
53
+ </div>
54
+ </Flexbox>
55
+ )}
56
+ {/* only show close icon when editable */}
57
+ {editable && (
58
+ <Center
59
+ className={cx(styles.closeIcon, alwaysShowClose && styles.alwaysShowClose)}
60
+ onClick={(e) => {
61
+ e.stopPropagation();
57
62
 
58
- removeFile(id);
59
- }}
60
- >
61
- <CloseCircleFilled />
62
- </Center>
63
- )}
64
- </Flexbox>
65
- );
66
- },
67
- );
63
+ removeFile(id);
64
+ }}
65
+ >
66
+ <CloseCircleFilled />
67
+ </Center>
68
+ )}
69
+ </Flexbox>
70
+ );
71
+ });
68
72
 
69
73
  export default FileItem;
@@ -14,11 +14,14 @@ import Lightbox from './Lightbox';
14
14
  // }));
15
15
 
16
16
  interface FileListProps {
17
+ alwaysShowClose?: boolean;
18
+
17
19
  editable?: boolean;
20
+
18
21
  items: string[];
19
22
  }
20
23
 
21
- const FileList = memo<FileListProps>(({ items, editable = true }) => {
24
+ const FileList = memo<FileListProps>(({ items, editable = true, alwaysShowClose }) => {
22
25
  // const { styles } = useStyles();
23
26
 
24
27
  const [showLightbox, setShowLightbox] = useState(false);
@@ -38,6 +41,7 @@ const FileList = memo<FileListProps>(({ items, editable = true }) => {
38
41
  >
39
42
  {items.map((i, index) => (
40
43
  <FileItem
44
+ alwaysShowClose={alwaysShowClose}
41
45
  editable={editable}
42
46
  id={i}
43
47
  key={i}
@@ -0,0 +1,57 @@
1
+ import { Input, TextArea } from '@lobehub/ui';
2
+ import { memo, useRef } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+
5
+ import { useSessionStore } from '@/store/session';
6
+
7
+ import { useSendMessage } from '../useSend';
8
+
9
+ export interface InputAreaInnerProps {
10
+ className?: string;
11
+ mobile?: boolean;
12
+ }
13
+
14
+ const InputAreaInner = memo<InputAreaInnerProps>(({ className, mobile }) => {
15
+ const { t } = useTranslation('chat');
16
+ const isChineseInput = useRef(false);
17
+
18
+ const [loading, message, updateInputMessage] = useSessionStore((s) => [
19
+ !!s.chatLoadingId,
20
+ s.inputMessage,
21
+ s.updateInputMessage,
22
+ ]);
23
+
24
+ const handleSend = useSendMessage();
25
+
26
+ const Render = mobile ? Input : TextArea;
27
+
28
+ return (
29
+ <Render
30
+ className={className}
31
+ onBlur={(e) => {
32
+ updateInputMessage(e.target.value);
33
+ }}
34
+ onChange={(e) => {
35
+ updateInputMessage(e.target.value);
36
+ }}
37
+ onCompositionEnd={() => {
38
+ isChineseInput.current = false;
39
+ }}
40
+ onCompositionStart={() => {
41
+ isChineseInput.current = true;
42
+ }}
43
+ onPressEnter={(e) => {
44
+ if (!loading && !e.shiftKey && !isChineseInput.current) {
45
+ e.preventDefault();
46
+ handleSend();
47
+ }
48
+ }}
49
+ placeholder={t('sendPlaceholder')}
50
+ resize={false}
51
+ type={mobile ? 'block' : 'pure'}
52
+ value={message}
53
+ />
54
+ );
55
+ });
56
+
57
+ export default InputAreaInner;
@@ -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
 
@@ -1,22 +1,27 @@
1
1
  import { SearchBar } from '@lobehub/ui';
2
2
  import { useResponsive } from 'antd-style';
3
- import { memo, useState } from 'react';
3
+ import { memo, useCallback, useState } from 'react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
 
6
6
  import { useMarketStore } from '@/store/market';
7
7
 
8
8
  const AgentSearchBar = memo(() => {
9
9
  const { t } = useTranslation('market');
10
- const keywords = useMarketStore((s) => s.searchKeywords);
10
+ const [keywords, setKeywords] = useMarketStore((s) => [s.searchKeywords, s.setSearchKeywords]);
11
11
  const [value, setValue] = useState(keywords);
12
12
  const { mobile } = useResponsive();
13
+
14
+ const handleSearch = useCallback(() => {
15
+ setKeywords(value);
16
+ }, [value, setKeywords]);
17
+
13
18
  return (
14
19
  <SearchBar
15
20
  allowClear
16
21
  enableShortKey={!mobile}
17
22
  onChange={(e) => setValue(e.target.value)}
18
- onPressEnter={() => useMarketStore.setState({ searchKeywords: value })}
19
- onSubmit={() => useMarketStore.setState({ searchKeywords: value })}
23
+ onPressEnter={handleSearch}
24
+ onSubmit={handleSearch}
20
25
  placeholder={t('search.placeholder')}
21
26
  shortKey={'k'}
22
27
  spotlight={!mobile}
@@ -0,0 +1,28 @@
1
+ import { Button } from 'antd';
2
+ import isEqual from 'fast-deep-equal';
3
+ import { startCase } from 'lodash-es';
4
+ import { memo } from 'react';
5
+
6
+ import { agentMarketSelectors, useMarketStore } from '@/store/market';
7
+
8
+ const Inner = memo(() => {
9
+ const agentTagList = useMarketStore(agentMarketSelectors.getAgentTagList, isEqual);
10
+ const [keywords, setSearchKeywords] = useMarketStore((s) => [
11
+ s.searchKeywords,
12
+ s.setSearchKeywords,
13
+ ]);
14
+
15
+ return agentTagList.map((item) => (
16
+ <Button
17
+ key={item}
18
+ onClick={() => setSearchKeywords(item)}
19
+ shape={'round'}
20
+ size={'small'}
21
+ type={keywords === item ? 'primary' : 'default'}
22
+ >
23
+ {startCase(item)}
24
+ </Button>
25
+ ));
26
+ });
27
+
28
+ export default Inner;
@@ -1,32 +1,21 @@
1
- import { Button, Skeleton } from 'antd';
2
- import isEqual from 'fast-deep-equal';
3
- import { startCase } from 'lodash-es';
4
- import { memo } from 'react';
1
+ import { Skeleton } from 'antd';
2
+ import { Suspense, memo } from 'react';
5
3
  import { Flexbox } from 'react-layout-kit';
6
4
 
7
- import { agentMarketSelectors, useMarketStore } from '@/store/market';
5
+ import Inner from '@/app/market/features/TagList/Inner';
8
6
 
9
7
  const TagList = memo(() => {
10
- const agentTagList = useMarketStore(agentMarketSelectors.getAgentTagList, isEqual);
11
- const keywords = useMarketStore((s) => s.searchKeywords);
12
-
13
8
  return (
14
9
  <Flexbox gap={6} horizontal style={{ flexWrap: 'wrap' }}>
15
- {agentTagList?.length > 0
16
- ? agentTagList.map((item) => (
17
- <Button
18
- key={item}
19
- onClick={() => useMarketStore.setState({ searchKeywords: item })}
20
- shape={'round'}
21
- size={'small'}
22
- type={keywords === item ? 'primary' : 'default'}
23
- >
24
- {startCase(item)}
25
- </Button>
26
- ))
27
- : Array.from({ length: 5 })
28
- .fill('')
29
- .map((_, index) => <Skeleton.Button key={index} shape={'round'} size={'small'} />)}
10
+ <Suspense
11
+ fallback={Array.from({ length: 5 })
12
+ .fill('')
13
+ .map((_, index) => (
14
+ <Skeleton.Button key={index} shape={'round'} size={'small'} />
15
+ ))}
16
+ >
17
+ <Inner />
18
+ </Suspense>
30
19
  </Flexbox>
31
20
  );
32
21
  });
@@ -12,6 +12,7 @@ import type { Store } from './store';
12
12
  export interface StoreAction {
13
13
  activateAgent: (identifier: string) => void;
14
14
  deactivateAgent: () => void;
15
+ setSearchKeywords: (keywords: string) => void;
15
16
  updateAgentMap: (key: string, value: AgentsMarketItem) => void;
16
17
  useFetchAgent: (identifier: string) => SWRResponse<AgentsMarketItem>;
17
18
  useFetchAgentList: () => SWRResponse<LobeChatAgentsMarketIndex>;
@@ -29,6 +30,9 @@ export const createMarketAction: StateCreator<
29
30
  deactivateAgent: () => {
30
31
  set({ currentIdentifier: undefined }, false, 'deactivateAgent');
31
32
  },
33
+ setSearchKeywords: (keywords) => {
34
+ set({ searchKeywords: keywords });
35
+ },
32
36
  updateAgentMap: (key, value) => {
33
37
  const { agentMap } = get();
34
38
 
@@ -56,7 +60,11 @@ export const createMarketAction: StateCreator<
56
60
  useFetchAgentList: () =>
57
61
  useSWR<LobeChatAgentsMarketIndex>(getCurrentLanguage(), getAgentList, {
58
62
  onSuccess: (agentMarketIndex) => {
59
- set({ agentList: agentMarketIndex.agents }, false, 'useFetchAgentList');
63
+ set(
64
+ { agentList: agentMarketIndex.agents, tagList: agentMarketIndex.tags },
65
+ false,
66
+ 'useFetchAgentList',
67
+ );
60
68
  },
61
69
  }),
62
70
  });
@@ -7,6 +7,7 @@ export interface StoreState {
7
7
  agentMap: MarketAgentMap;
8
8
  currentIdentifier: string;
9
9
  searchKeywords: string;
10
+ tagList: string[];
10
11
  }
11
12
 
12
13
  export const initialState: StoreState = {
@@ -14,4 +15,5 @@ export const initialState: StoreState = {
14
15
  agentMap: {},
15
16
  currentIdentifier: '',
16
17
  searchKeywords: '',
18
+ tagList: [],
17
19
  };
@@ -1,17 +1,19 @@
1
- import { flatten } from 'lodash-es';
2
-
3
1
  import { DEFAULT_AGENTS_MARKET_ITEM } from '@/const/market';
4
2
  import { AgentsMarketItem } from '@/types/market';
5
- import { findDuplicates } from '@/utils/findDuplicates';
6
3
 
7
4
  import type { Store } from './store';
8
5
 
9
- const getAgentList = (s: Store) => s.agentList;
10
- const getAgentTagList = (s: Store) => {
11
- const agentList = s.agentList;
12
- const rawAgentTagList = flatten(agentList.map((item) => item.meta.tags)) as string[];
13
- return findDuplicates(rawAgentTagList);
6
+ const getAgentList = (s: Store) => {
7
+ const { searchKeywords, agentList } = s;
8
+ if (!searchKeywords) return agentList;
9
+ return agentList.filter(({ meta }) => {
10
+ const checkMeta: string = [meta.tags, meta.title, meta.description, meta.avatar]
11
+ .filter(Boolean)
12
+ .join('');
13
+ return checkMeta.toLowerCase().includes(searchKeywords.toLowerCase());
14
+ });
14
15
  };
16
+ const getAgentTagList = (s: Store) => s.tagList;
15
17
 
16
18
  const getAgentItemById = (d: string) => (s: Store) => s.agentMap[d];
17
19
 
@@ -16,4 +16,5 @@ export type AgentsMarketItem = AgentsMarketIndexItem & LobeAgentSettings;
16
16
  export interface LobeChatAgentsMarketIndex {
17
17
  agents: AgentsMarketIndexItem[];
18
18
  schemaVersion: 1;
19
+ tags: string[];
19
20
  }
@@ -1,22 +0,0 @@
1
- export const findDuplicates = (arr: string[]): string[] => {
2
- const duplicates: { [key: string]: number } = {};
3
-
4
- // 统计每个项目出现的次数
5
- for (const item of arr) {
6
- if (duplicates[item]) {
7
- duplicates[item]++;
8
- } else {
9
- duplicates[item] = 1;
10
- }
11
- }
12
-
13
- // 挑出重复出现 3 次以上的项目
14
- const COUNT = 3;
15
-
16
- const result = Object.keys(duplicates).filter((item) => duplicates[item] >= COUNT);
17
-
18
- // 按重复次数从多到少排序
19
- result.sort((a, b) => duplicates[b] - duplicates[a]);
20
-
21
- return result;
22
- };