@lobehub/chat 1.63.2 → 1.64.0

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 (151) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/models.json +25 -16
  4. package/locales/ar/plugin.json +16 -0
  5. package/locales/ar/portal.json +0 -5
  6. package/locales/ar/tool.json +18 -0
  7. package/locales/bg-BG/models.json +25 -16
  8. package/locales/bg-BG/plugin.json +16 -0
  9. package/locales/bg-BG/portal.json +0 -5
  10. package/locales/bg-BG/tool.json +18 -0
  11. package/locales/de-DE/models.json +25 -16
  12. package/locales/de-DE/plugin.json +16 -0
  13. package/locales/de-DE/portal.json +0 -5
  14. package/locales/de-DE/tool.json +18 -0
  15. package/locales/en-US/models.json +24 -15
  16. package/locales/en-US/plugin.json +16 -0
  17. package/locales/en-US/portal.json +0 -5
  18. package/locales/en-US/tool.json +18 -0
  19. package/locales/es-ES/models.json +25 -16
  20. package/locales/es-ES/plugin.json +16 -0
  21. package/locales/es-ES/portal.json +0 -5
  22. package/locales/es-ES/tool.json +18 -0
  23. package/locales/fa-IR/models.json +25 -16
  24. package/locales/fa-IR/plugin.json +16 -0
  25. package/locales/fa-IR/portal.json +0 -5
  26. package/locales/fa-IR/tool.json +18 -0
  27. package/locales/fr-FR/models.json +25 -16
  28. package/locales/fr-FR/plugin.json +16 -0
  29. package/locales/fr-FR/portal.json +0 -5
  30. package/locales/fr-FR/tool.json +18 -0
  31. package/locales/it-IT/models.json +25 -16
  32. package/locales/it-IT/plugin.json +16 -0
  33. package/locales/it-IT/portal.json +0 -5
  34. package/locales/it-IT/tool.json +18 -0
  35. package/locales/ja-JP/models.json +24 -15
  36. package/locales/ja-JP/plugin.json +16 -0
  37. package/locales/ja-JP/portal.json +0 -5
  38. package/locales/ja-JP/tool.json +18 -0
  39. package/locales/ko-KR/models.json +25 -16
  40. package/locales/ko-KR/plugin.json +16 -0
  41. package/locales/ko-KR/portal.json +0 -5
  42. package/locales/ko-KR/tool.json +18 -0
  43. package/locales/nl-NL/models.json +25 -16
  44. package/locales/nl-NL/plugin.json +16 -0
  45. package/locales/nl-NL/portal.json +0 -5
  46. package/locales/nl-NL/tool.json +18 -0
  47. package/locales/pl-PL/models.json +25 -16
  48. package/locales/pl-PL/plugin.json +16 -0
  49. package/locales/pl-PL/portal.json +0 -5
  50. package/locales/pl-PL/tool.json +18 -0
  51. package/locales/pt-BR/models.json +24 -15
  52. package/locales/pt-BR/plugin.json +16 -0
  53. package/locales/pt-BR/portal.json +0 -5
  54. package/locales/pt-BR/tool.json +18 -0
  55. package/locales/ru-RU/models.json +25 -16
  56. package/locales/ru-RU/plugin.json +16 -0
  57. package/locales/ru-RU/portal.json +0 -5
  58. package/locales/ru-RU/tool.json +18 -0
  59. package/locales/tr-TR/models.json +25 -16
  60. package/locales/tr-TR/plugin.json +16 -0
  61. package/locales/tr-TR/portal.json +0 -5
  62. package/locales/tr-TR/tool.json +18 -0
  63. package/locales/vi-VN/models.json +24 -15
  64. package/locales/vi-VN/plugin.json +16 -0
  65. package/locales/vi-VN/portal.json +0 -5
  66. package/locales/vi-VN/tool.json +18 -0
  67. package/locales/zh-CN/models.json +30 -21
  68. package/locales/zh-CN/plugin.json +16 -0
  69. package/locales/zh-CN/portal.json +1 -6
  70. package/locales/zh-CN/tool.json +19 -1
  71. package/locales/zh-TW/models.json +23 -14
  72. package/locales/zh-TW/plugin.json +16 -0
  73. package/locales/zh-TW/portal.json +0 -5
  74. package/locales/zh-TW/tool.json +18 -0
  75. package/package.json +1 -1
  76. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/index.tsx +1 -0
  77. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/SearchTags.tsx +17 -0
  78. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Tags.tsx +8 -2
  79. package/src/config/tools.ts +16 -0
  80. package/src/database/repositories/aiInfra/index.test.ts +29 -0
  81. package/src/features/ChatInput/ActionBar/Search/index.tsx +6 -15
  82. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/ToolTitle.tsx +76 -0
  83. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/index.tsx +8 -21
  84. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +62 -50
  85. package/src/features/PluginsUI/Render/BuiltinType/index.tsx +11 -1
  86. package/src/features/PluginsUI/Render/index.tsx +3 -0
  87. package/src/features/Portal/Plugins/Body/index.tsx +3 -7
  88. package/src/features/Portal/Plugins/Header.tsx +14 -2
  89. package/src/hooks/useAgentEnableSearch.ts +27 -0
  90. package/src/libs/agent-runtime/perplexity/index.test.ts +26 -0
  91. package/src/libs/agent-runtime/utils/streams/openai.ts +1 -1
  92. package/src/libs/trpc/client/index.ts +1 -0
  93. package/src/libs/trpc/client/tools.ts +20 -0
  94. package/src/locales/default/plugin.ts +16 -0
  95. package/src/locales/default/portal.ts +0 -5
  96. package/src/locales/default/tool.ts +18 -0
  97. package/src/server/modules/SearXNG.ts +33 -0
  98. package/src/server/routers/lambda/message.ts +11 -0
  99. package/src/server/routers/tools/__tests__/fixtures/searXNG.ts +668 -0
  100. package/src/server/routers/tools/__tests__/search.test.ts +47 -0
  101. package/src/server/routers/tools/index.ts +3 -0
  102. package/src/server/routers/tools/search.ts +38 -0
  103. package/src/services/__tests__/__snapshots__/chat.test.ts.snap +1 -0
  104. package/src/services/_auth.ts +4 -4
  105. package/src/services/chat.ts +31 -10
  106. package/src/services/message/_deprecated.ts +4 -0
  107. package/src/services/message/client.ts +4 -0
  108. package/src/services/message/server.ts +5 -5
  109. package/src/services/message/type.ts +2 -0
  110. package/src/services/search.ts +9 -0
  111. package/src/store/aiInfra/slices/aiModel/selectors.ts +12 -5
  112. package/src/store/chat/slices/builtinTool/action.ts +121 -0
  113. package/src/store/chat/slices/builtinTool/initialState.ts +2 -0
  114. package/src/store/chat/slices/builtinTool/selectors.ts +3 -0
  115. package/src/store/chat/slices/message/action.ts +11 -0
  116. package/src/store/chat/slices/plugin/action.test.ts +2 -2
  117. package/src/store/chat/slices/plugin/action.ts +2 -2
  118. package/src/store/tool/selectors/tool.ts +5 -12
  119. package/src/store/tool/slices/builtin/selectors.ts +1 -1
  120. package/src/store/user/slices/modelList/action.ts +6 -0
  121. package/src/store/user/slices/modelList/selectors/keyVaults.ts +1 -0
  122. package/src/tools/index.ts +7 -0
  123. package/src/tools/portals.ts +6 -1
  124. package/src/tools/renders.ts +3 -0
  125. package/src/{features/Portal/Plugins → tools/web-browsing/Portal}/Footer.tsx +13 -10
  126. package/src/tools/web-browsing/Portal/ResultList/SearchItem/CategoryAvatar.tsx +70 -0
  127. package/src/tools/web-browsing/Portal/ResultList/SearchItem/TitleExtra.tsx +38 -0
  128. package/src/tools/web-browsing/Portal/ResultList/SearchItem/Video.tsx +135 -0
  129. package/src/tools/web-browsing/Portal/ResultList/SearchItem/index.tsx +91 -0
  130. package/src/tools/web-browsing/Portal/ResultList/index.tsx +21 -0
  131. package/src/tools/web-browsing/Portal/index.tsx +65 -0
  132. package/src/tools/web-browsing/Render/ConfigForm/Form.tsx +110 -0
  133. package/src/tools/web-browsing/Render/ConfigForm/SearchXNGIcon.tsx +20 -0
  134. package/src/tools/web-browsing/Render/ConfigForm/index.tsx +67 -0
  135. package/src/tools/web-browsing/Render/ConfigForm/style.tsx +63 -0
  136. package/src/tools/web-browsing/Render/SearchQuery/SearchView.tsx +88 -0
  137. package/src/tools/web-browsing/Render/SearchQuery/index.tsx +61 -0
  138. package/src/tools/web-browsing/Render/SearchResult/SearchResultItem.tsx +72 -0
  139. package/src/tools/web-browsing/Render/SearchResult/ShowMore.tsx +68 -0
  140. package/src/tools/web-browsing/Render/SearchResult/index.tsx +105 -0
  141. package/src/tools/web-browsing/Render/index.tsx +57 -0
  142. package/src/tools/web-browsing/components/EngineAvatar.tsx +32 -0
  143. package/src/tools/web-browsing/components/SearchBar.tsx +134 -0
  144. package/src/tools/web-browsing/const.ts +11 -0
  145. package/src/tools/web-browsing/index.ts +102 -0
  146. package/src/types/message/chat.ts +1 -0
  147. package/src/types/message/tools.ts +10 -0
  148. package/src/types/tool/builtin.ts +2 -0
  149. package/src/types/tool/search.ts +38 -0
  150. package/src/types/user/settings/keyVaults.ts +8 -1
  151. package/src/utils/toolManifest.ts +20 -0
@@ -0,0 +1,67 @@
1
+ import { Button } from 'antd';
2
+ import { memo, useMemo, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Center, Flexbox } from 'react-layout-kit';
5
+
6
+ import { useChatStore } from '@/store/chat';
7
+
8
+ import SearchXNGIcon from './SearchXNGIcon';
9
+ import { FormAction } from './style';
10
+
11
+ interface ConfigAlertProps {
12
+ id: string;
13
+ provider: string;
14
+ }
15
+
16
+ const ConfigAlert = memo<ConfigAlertProps>(({ provider, id }) => {
17
+ const { t } = useTranslation('plugin');
18
+
19
+ const [resend, deleteMessage] = useChatStore((s) => [s.reInvokeToolMessage, s.deleteMessage]);
20
+
21
+ const [loading, setLoading] = useState(false);
22
+
23
+ const avatar = useMemo(() => {
24
+ switch (provider) {
25
+ default: {
26
+ return <SearchXNGIcon />;
27
+ }
28
+ }
29
+ }, [provider]);
30
+
31
+ return (
32
+ <Center gap={16} style={{ width: 400 }}>
33
+ <FormAction
34
+ avatar={avatar}
35
+ description={t('search.searchxng.unconfiguredDesc')}
36
+ title={t('search.searchxng.unconfiguredTitle')}
37
+ >
38
+ <Flexbox gap={12} width={'100%'}>
39
+ <Button
40
+ block
41
+ disabled={loading}
42
+ onClick={async () => {
43
+ setLoading(true);
44
+ resend(id).then(() => {
45
+ setLoading(false);
46
+ });
47
+ // deleteMessage(id);
48
+ }}
49
+ style={{ marginTop: 8 }}
50
+ type={'primary'}
51
+ >
52
+ {t('search.config.confirm')}
53
+ </Button>
54
+ <Button
55
+ onClick={() => {
56
+ deleteMessage(id);
57
+ }}
58
+ >
59
+ {t('search.config.close')}
60
+ </Button>
61
+ </Flexbox>
62
+ </FormAction>
63
+ </Center>
64
+ );
65
+ });
66
+
67
+ export default ConfigAlert;
@@ -0,0 +1,63 @@
1
+ import { Avatar } from '@lobehub/ui';
2
+ import { createStyles } from 'antd-style';
3
+ import { ReactNode, memo } from 'react';
4
+ import { Center, CenterProps, Flexbox } from 'react-layout-kit';
5
+
6
+ export const useStyles = createStyles(({ css, token }) => ({
7
+ container: css`
8
+ border-radius: 8px;
9
+ color: ${token.colorText};
10
+ `,
11
+ desc: css`
12
+ color: ${token.colorTextTertiary};
13
+ text-align: center;
14
+ `,
15
+ form: css`
16
+ width: 100%;
17
+ max-width: 300px;
18
+ padding-block: 12px;
19
+ `,
20
+ }));
21
+
22
+ export const FormAction = memo<
23
+ {
24
+ animation?: boolean;
25
+ avatar: ReactNode;
26
+ background?: string;
27
+ description: string;
28
+ title: string;
29
+ } & CenterProps
30
+ >(
31
+ ({
32
+ children,
33
+ background,
34
+ title,
35
+ description,
36
+ avatar,
37
+ animation,
38
+ className,
39
+ gap = 16,
40
+ ...rest
41
+ }) => {
42
+ const { cx, styles, theme } = useStyles();
43
+
44
+ return (
45
+ <Center className={cx(styles.form, className)} gap={gap} {...rest}>
46
+ <Avatar
47
+ animation={animation}
48
+ avatar={avatar}
49
+ background={background ?? theme.colorFillContent}
50
+ gap={12}
51
+ size={80}
52
+ />
53
+ <Flexbox gap={8} width={'100%'}>
54
+ <Flexbox style={{ fontSize: 18, fontWeight: 'bold', textAlign: 'center' }}>
55
+ {title}
56
+ </Flexbox>
57
+ <Flexbox className={styles.desc}>{description}</Flexbox>
58
+ </Flexbox>
59
+ {children}
60
+ </Center>
61
+ );
62
+ },
63
+ );
@@ -0,0 +1,88 @@
1
+ import { Icon } from '@lobehub/ui';
2
+ import { Divider, Skeleton } from 'antd';
3
+ import { createStyles } from 'antd-style';
4
+ import { SearchIcon } from 'lucide-react';
5
+ import { memo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { useIsMobile } from '@/hooks/useIsMobile';
10
+
11
+ import { EngineAvatarGroup } from '../../components/EngineAvatar';
12
+
13
+ const useStyles = createStyles(({ css, token }) => ({
14
+ font: css`
15
+ font-size: 12px;
16
+ color: ${token.colorTextTertiary};
17
+ `,
18
+ query: css`
19
+ cursor: pointer;
20
+
21
+ padding-block: 4px;
22
+ padding-inline: 8px;
23
+ border-radius: 8px;
24
+
25
+ font-size: 12px;
26
+ color: ${token.colorTextSecondary};
27
+
28
+ &:hover {
29
+ background: ${token.colorFillTertiary};
30
+ }
31
+ `,
32
+ }));
33
+
34
+ interface SearchBarProps {
35
+ defaultEngines: string[];
36
+ defaultQuery: string;
37
+ onEditingChange: (editing: boolean) => void;
38
+ resultsNumber: number;
39
+ searching?: boolean;
40
+ }
41
+
42
+ const SearchBar = memo<SearchBarProps>(
43
+ ({ defaultEngines, defaultQuery, resultsNumber, onEditingChange, searching }) => {
44
+ const { t } = useTranslation('tool');
45
+ const isMobile = useIsMobile();
46
+ const { styles } = useStyles();
47
+ return (
48
+ <Flexbox
49
+ align={isMobile ? 'flex-start' : 'center'}
50
+ distribution={'space-between'}
51
+ gap={isMobile ? 8 : 40}
52
+ height={isMobile ? undefined : 32}
53
+ horizontal={!isMobile}
54
+ >
55
+ <Flexbox
56
+ align={'center'}
57
+ className={styles.query}
58
+ gap={8}
59
+ horizontal
60
+ onClick={() => {
61
+ onEditingChange(true);
62
+ }}
63
+ >
64
+ <Icon icon={SearchIcon} />
65
+ {defaultQuery}
66
+ </Flexbox>
67
+
68
+ <Flexbox align={'center'} horizontal>
69
+ <div className={styles.font}>{t('search.searchEngine')}</div>
70
+ {searching ? (
71
+ <Skeleton.Button active size={'small'} />
72
+ ) : (
73
+ <EngineAvatarGroup engines={defaultEngines} />
74
+ )}
75
+
76
+ {!isMobile && (
77
+ <>
78
+ <Divider type={'vertical'} />
79
+ <div className={styles.font}>{t('search.searchResult')}</div>
80
+ {searching ? <Skeleton.Button active size={'small'} /> : resultsNumber}
81
+ </>
82
+ )}
83
+ </Flexbox>
84
+ </Flexbox>
85
+ );
86
+ },
87
+ );
88
+ export default SearchBar;
@@ -0,0 +1,61 @@
1
+ import { ActionIcon } from '@lobehub/ui';
2
+ import { Skeleton } from 'antd';
3
+ import { uniq } from 'lodash-es';
4
+ import { XIcon } from 'lucide-react';
5
+ import { memo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { useChatStore } from '@/store/chat';
10
+ import { chatToolSelectors } from '@/store/chat/selectors';
11
+ import { SearchQuery, SearchResponse } from '@/types/tool/search';
12
+
13
+ import SearchBar from '../../components/SearchBar';
14
+ import SearchView from './SearchView';
15
+
16
+ interface SearchQueryViewProps {
17
+ args: SearchQuery;
18
+ editing: boolean;
19
+ messageId: string;
20
+ pluginState?: SearchResponse;
21
+ setEditing: (editing: boolean) => void;
22
+ }
23
+
24
+ const SearchQueryView = memo<SearchQueryViewProps>(
25
+ ({ messageId, args, pluginState, setEditing, editing }) => {
26
+ const loading = useChatStore(chatToolSelectors.isSearXNGSearching(messageId));
27
+ const searchResults = pluginState?.results || [];
28
+
29
+ const { t } = useTranslation('common');
30
+
31
+ const engines = uniq(searchResults.map((result) => result.engine));
32
+ const defaultEngines = engines.length > 0 ? engines : args.searchEngines || [];
33
+
34
+ return !pluginState ? (
35
+ <Flexbox align={'center'} distribution={'space-between'} height={32} horizontal>
36
+ <Skeleton.Button active style={{ borderRadius: 8, height: 32, width: 180 }} />
37
+ <Skeleton.Button active style={{ borderRadius: 8, height: 32, width: 220 }} />
38
+ </Flexbox>
39
+ ) : editing ? (
40
+ <SearchBar
41
+ defaultEngines={defaultEngines}
42
+ defaultQuery={args?.query}
43
+ messageId={messageId}
44
+ onSearch={() => setEditing(false)}
45
+ searchAddon={
46
+ <ActionIcon icon={XIcon} onClick={() => setEditing(false)} title={t('cancel')} />
47
+ }
48
+ />
49
+ ) : (
50
+ <SearchView
51
+ defaultEngines={defaultEngines}
52
+ defaultQuery={args?.query}
53
+ onEditingChange={setEditing}
54
+ resultsNumber={searchResults.length}
55
+ searching={loading}
56
+ />
57
+ );
58
+ },
59
+ );
60
+
61
+ export default SearchQueryView;
@@ -0,0 +1,72 @@
1
+ import { Typography } from 'antd';
2
+ import { createStyles } from 'antd-style';
3
+ import Image from 'next/image';
4
+ import Link from 'next/link';
5
+ import { memo } from 'react';
6
+ import { Flexbox } from 'react-layout-kit';
7
+
8
+ import { SearchResult } from '@/types/tool/search';
9
+
10
+ const useStyles = createStyles(({ css, token }) => ({
11
+ container: css`
12
+ cursor: pointer;
13
+
14
+ height: 100%;
15
+ padding: 8px;
16
+ border-radius: 8px;
17
+
18
+ font-size: 12px;
19
+ color: initial;
20
+
21
+ background: ${token.colorFillQuaternary};
22
+
23
+ &:hover {
24
+ background: ${token.colorFillTertiary};
25
+ }
26
+ `,
27
+ title: css`
28
+ overflow: hidden;
29
+ display: -webkit-box;
30
+ -webkit-box-orient: vertical;
31
+ -webkit-line-clamp: 2;
32
+
33
+ text-overflow: ellipsis;
34
+ `,
35
+ url: css`
36
+ overflow: hidden;
37
+ display: -webkit-box;
38
+ -webkit-box-orient: vertical;
39
+ -webkit-line-clamp: 1;
40
+
41
+ text-overflow: ellipsis;
42
+ `,
43
+ }));
44
+
45
+ const SearchResultItem = memo<SearchResult>(({ url, title }) => {
46
+ const { styles } = useStyles();
47
+
48
+ const urlObj = new URL(url);
49
+ const host = urlObj.hostname;
50
+ return (
51
+ <Link href={url} target={'_blank'}>
52
+ <Flexbox className={styles.container} gap={2} justify={'space-between'} key={url}>
53
+ <div className={styles.title}>{title}</div>
54
+ <Flexbox align={'center'} gap={4} horizontal>
55
+ <Image
56
+ alt={title || url}
57
+ height={14}
58
+ src={`https://icons.duckduckgo.com/ip3/${host}.ico`}
59
+ style={{ borderRadius: 4 }}
60
+ unoptimized
61
+ width={14}
62
+ />
63
+ <Typography.Text className={styles.url} type={'secondary'}>
64
+ {host.replace('www.', '')}
65
+ </Typography.Text>
66
+ </Flexbox>
67
+ </Flexbox>
68
+ </Link>
69
+ );
70
+ });
71
+
72
+ export default SearchResultItem;
@@ -0,0 +1,68 @@
1
+ import { createStyles } from 'antd-style';
2
+ import { memo } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Flexbox } from 'react-layout-kit';
5
+
6
+ import { useChatStore } from '@/store/chat';
7
+ import { WebBrowsingManifest } from '@/tools/web-browsing';
8
+
9
+ import { EngineAvatarGroup } from '../../components/EngineAvatar';
10
+
11
+ const useStyles = createStyles(({ css, token }) => ({
12
+ container: css`
13
+ cursor: pointer;
14
+
15
+ height: 100%;
16
+ padding: 8px;
17
+
18
+ font-size: 12px;
19
+ color: initial;
20
+
21
+ background: ${token.colorFillQuaternary};
22
+ border-radius: 8px;
23
+
24
+ &:hover {
25
+ background: ${token.colorFillTertiary};
26
+ }
27
+ `,
28
+ title: css`
29
+ overflow: hidden;
30
+ { /* stylelint-disable-line */ }
31
+ display: -webkit-box;
32
+ -webkit-box-orient: vertical;
33
+
34
+ text-overflow: ellipsis;
35
+
36
+ -webkit-line-clamp: 2;
37
+ `,
38
+ }));
39
+
40
+ interface ShowMoreProps {
41
+ engines: string[];
42
+ messageId: string;
43
+ resultsNumber: number;
44
+ }
45
+ const ShowMore = memo<ShowMoreProps>(({ messageId, engines, resultsNumber }) => {
46
+ const { styles } = useStyles();
47
+ const [openToolUI] = useChatStore((s) => [s.openToolUI]);
48
+
49
+ const { t } = useTranslation('tool');
50
+
51
+ return (
52
+ <Flexbox
53
+ className={styles.container}
54
+ gap={2}
55
+ justify={'space-between'}
56
+ onClick={() => {
57
+ openToolUI(messageId, WebBrowsingManifest.identifier);
58
+ }}
59
+ >
60
+ <div className={styles.title}>{t('search.viewMoreResults', { results: resultsNumber })}</div>
61
+ <Flexbox align={'center'} gap={4} horizontal>
62
+ <EngineAvatarGroup engines={engines} />
63
+ </Flexbox>
64
+ </Flexbox>
65
+ );
66
+ });
67
+
68
+ export default ShowMore;
@@ -0,0 +1,105 @@
1
+ import { Icon } from '@lobehub/ui';
2
+ import { Button, Empty, Skeleton } from 'antd';
3
+ import { uniq } from 'lodash-es';
4
+ import { Edit2Icon } from 'lucide-react';
5
+ import { memo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Center, Flexbox } from 'react-layout-kit';
8
+
9
+ import { useIsMobile } from '@/hooks/useIsMobile';
10
+ import { useChatStore } from '@/store/chat';
11
+ import { chatToolSelectors } from '@/store/chat/selectors';
12
+ import { SearchQuery, SearchResponse } from '@/types/tool/search';
13
+
14
+ import SearchResultItem from './SearchResultItem';
15
+ import ShowMore from './ShowMore';
16
+
17
+ const ITEM_HEIGHT = 80;
18
+ const ITEM_WIDTH = 160;
19
+
20
+ interface SearchResultProps {
21
+ args: SearchQuery;
22
+ editing: boolean;
23
+ messageId: string;
24
+ pluginState?: SearchResponse;
25
+ setEditing: (editing: boolean) => void;
26
+ }
27
+
28
+ const SearchResult = memo<SearchResultProps>(
29
+ ({ messageId, args, pluginState, setEditing, editing }) => {
30
+ const loading = useChatStore(chatToolSelectors.isSearXNGSearching(messageId));
31
+ const searchResults = pluginState?.results || [];
32
+ const { t } = useTranslation(['tool', 'common']);
33
+
34
+ const engines = uniq(searchResults.map((result) => result.engine));
35
+ const defaultEngines = engines.length > 0 ? engines : args.searchEngines || [];
36
+ const isMobile = useIsMobile();
37
+
38
+ if (loading || !pluginState)
39
+ return (
40
+ <Flexbox gap={12} horizontal>
41
+ {['1', '2', '3', '4'].map((id) => (
42
+ <Skeleton.Button
43
+ active
44
+ key={id}
45
+ style={{ borderRadius: 8, height: ITEM_HEIGHT, width: ITEM_WIDTH }}
46
+ />
47
+ ))}
48
+ </Flexbox>
49
+ );
50
+
51
+ if (searchResults.length === 0)
52
+ return (
53
+ <Center>
54
+ <Empty
55
+ description={
56
+ <Flexbox gap={8}>
57
+ <div>{t('search.emptyResult')}</div>
58
+ {!editing && (
59
+ <div>
60
+ <Button
61
+ icon={<Icon icon={Edit2Icon} />}
62
+ onClick={() => {
63
+ setEditing(true);
64
+ }}
65
+ type={'primary'}
66
+ >
67
+ {t('edit', { ns: 'common' })}
68
+ </Button>
69
+ </div>
70
+ )}
71
+ </Flexbox>
72
+ }
73
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
74
+ />
75
+ </Center>
76
+ );
77
+
78
+ return (
79
+ <Flexbox gap={8}>
80
+ <Flexbox
81
+ gap={12}
82
+ horizontal
83
+ style={{ minHeight: ITEM_HEIGHT, overflowX: 'scroll', width: '100%' }}
84
+ >
85
+ {searchResults.slice(0, 5).map((result) => (
86
+ <div key={result.url} style={{ minWidth: ITEM_WIDTH, width: ITEM_WIDTH }}>
87
+ <SearchResultItem {...result} />
88
+ </div>
89
+ ))}
90
+ {!isMobile && searchResults.length > 5 && (
91
+ <div style={{ minWidth: ITEM_WIDTH }}>
92
+ <ShowMore
93
+ engines={defaultEngines}
94
+ messageId={messageId}
95
+ resultsNumber={searchResults.length - 5}
96
+ />
97
+ </div>
98
+ )}
99
+ </Flexbox>
100
+ </Flexbox>
101
+ );
102
+ },
103
+ );
104
+
105
+ export default SearchResult;
@@ -0,0 +1,57 @@
1
+ import { Alert, Highlighter } from '@lobehub/ui';
2
+ import { memo, useState } from 'react';
3
+ import { Flexbox } from 'react-layout-kit';
4
+
5
+ import { BuiltinRenderProps } from '@/types/tool';
6
+ import { SearchContent, SearchQuery, SearchResponse } from '@/types/tool/search';
7
+
8
+ import ConfigForm from './ConfigForm';
9
+ import SearchQueryView from './SearchQuery';
10
+ import SearchResult from './SearchResult';
11
+
12
+ const WebBrowsing = memo<BuiltinRenderProps<SearchContent[], SearchQuery, SearchResponse>>(
13
+ ({ messageId, args, pluginState, pluginError }) => {
14
+ const [editing, setEditing] = useState(false);
15
+
16
+ if (pluginError) {
17
+ if (pluginError?.type === 'PluginSettingsInvalid') {
18
+ return <ConfigForm id={messageId} provider={pluginError.body?.provider} />;
19
+ }
20
+
21
+ return (
22
+ <Alert
23
+ extra={
24
+ <Flexbox>
25
+ <Highlighter copyButtonSize={'small'} language={'json'} type={'pure'}>
26
+ {JSON.stringify(pluginError.body?.data || pluginError.body, null, 2)}
27
+ </Highlighter>
28
+ </Flexbox>
29
+ }
30
+ message={pluginError?.message}
31
+ type={'error'}
32
+ />
33
+ );
34
+ }
35
+
36
+ return (
37
+ <Flexbox gap={16}>
38
+ <SearchQueryView
39
+ args={args}
40
+ editing={editing}
41
+ messageId={messageId}
42
+ pluginState={pluginState}
43
+ setEditing={setEditing}
44
+ />
45
+ <SearchResult
46
+ args={args}
47
+ editing={editing}
48
+ messageId={messageId}
49
+ pluginState={pluginState}
50
+ setEditing={setEditing}
51
+ />
52
+ </Flexbox>
53
+ );
54
+ },
55
+ );
56
+
57
+ export default WebBrowsing;
@@ -0,0 +1,32 @@
1
+ import { Avatar } from 'antd';
2
+ import { useTheme } from 'antd-style';
3
+ import { memo } from 'react';
4
+
5
+ import { ENGINE_ICON_MAP } from '../const';
6
+
7
+ interface EngineAvatarGroupProps {
8
+ engines: string[];
9
+ }
10
+
11
+ interface EngineAvatarProps {
12
+ engine: string;
13
+ size?: number;
14
+ }
15
+ export const EngineAvatar = memo<EngineAvatarProps>(({ engine }) => (
16
+ <Avatar alt={engine} src={ENGINE_ICON_MAP[engine]} style={{ height: 16, width: 16 }} />
17
+ ));
18
+
19
+ export const EngineAvatarGroup = memo<EngineAvatarGroupProps>(({ engines }) => {
20
+ const theme = useTheme();
21
+ return (
22
+ <Avatar.Group>
23
+ {engines.map((engine) => (
24
+ <Avatar
25
+ key={engine}
26
+ src={ENGINE_ICON_MAP[engine]}
27
+ style={{ background: theme.colorBgLayout, height: 20, padding: 3, width: 20 }}
28
+ />
29
+ ))}
30
+ </Avatar.Group>
31
+ );
32
+ });