@lobehub/chat 1.66.6 → 1.67.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 (98) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/ar/models.json +9 -3
  4. package/locales/ar/plugin.json +12 -0
  5. package/locales/bg-BG/models.json +9 -3
  6. package/locales/bg-BG/plugin.json +12 -0
  7. package/locales/de-DE/models.json +9 -3
  8. package/locales/de-DE/plugin.json +12 -0
  9. package/locales/en-US/models.json +9 -3
  10. package/locales/en-US/plugin.json +12 -0
  11. package/locales/es-ES/models.json +9 -3
  12. package/locales/es-ES/plugin.json +12 -0
  13. package/locales/fa-IR/models.json +9 -3
  14. package/locales/fa-IR/plugin.json +12 -0
  15. package/locales/fr-FR/models.json +9 -3
  16. package/locales/fr-FR/plugin.json +12 -0
  17. package/locales/it-IT/models.json +9 -3
  18. package/locales/it-IT/plugin.json +12 -0
  19. package/locales/ja-JP/models.json +9 -3
  20. package/locales/ja-JP/plugin.json +12 -0
  21. package/locales/ko-KR/models.json +9 -3
  22. package/locales/ko-KR/plugin.json +12 -0
  23. package/locales/nl-NL/models.json +9 -3
  24. package/locales/nl-NL/plugin.json +12 -0
  25. package/locales/pl-PL/models.json +9 -3
  26. package/locales/pl-PL/plugin.json +12 -0
  27. package/locales/pt-BR/models.json +9 -3
  28. package/locales/pt-BR/plugin.json +12 -0
  29. package/locales/ru-RU/models.json +9 -3
  30. package/locales/ru-RU/plugin.json +12 -0
  31. package/locales/tr-TR/models.json +9 -3
  32. package/locales/tr-TR/plugin.json +12 -0
  33. package/locales/vi-VN/models.json +9 -3
  34. package/locales/vi-VN/plugin.json +12 -0
  35. package/locales/zh-CN/models.json +9 -3
  36. package/locales/zh-CN/plugin.json +12 -0
  37. package/locales/zh-TW/models.json +9 -3
  38. package/locales/zh-TW/plugin.json +12 -0
  39. package/package.json +5 -1
  40. package/packages/web-crawler/README.md +34 -0
  41. package/packages/web-crawler/package.json +13 -0
  42. package/packages/web-crawler/src/crawImpl/browserless.ts +62 -0
  43. package/packages/web-crawler/src/crawImpl/index.ts +11 -0
  44. package/packages/web-crawler/src/crawImpl/jina.ts +37 -0
  45. package/packages/web-crawler/src/crawImpl/naive.ts +84 -0
  46. package/packages/web-crawler/src/crawler.ts +66 -0
  47. package/packages/web-crawler/src/index.ts +2 -0
  48. package/packages/web-crawler/src/type.ts +42 -0
  49. package/packages/web-crawler/src/urlRules.ts +34 -0
  50. package/packages/web-crawler/src/utils/__snapshots__/htmlToMarkdown.test.ts.snap +638 -0
  51. package/packages/web-crawler/src/utils/appUrlRules.test.ts +26 -0
  52. package/packages/web-crawler/src/utils/appUrlRules.ts +40 -0
  53. package/packages/web-crawler/src/utils/errorType.ts +12 -0
  54. package/packages/web-crawler/src/utils/html/terms.html +1222 -0
  55. package/packages/web-crawler/src/utils/html/yingchao.html +1001 -0
  56. package/packages/web-crawler/src/utils/htmlToMarkdown.test.ts +35 -0
  57. package/packages/web-crawler/src/utils/htmlToMarkdown.ts +45 -0
  58. package/packages/web-crawler/tsconfig.json +20 -0
  59. package/pnpm-workspace.yaml +3 -0
  60. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +4 -35
  61. package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +1 -1
  62. package/src/features/PluginsUI/Render/BuiltinType/index.tsx +3 -0
  63. package/src/features/PluginsUI/Render/index.tsx +1 -0
  64. package/src/features/Portal/Plugins/Body/ToolRender.tsx +1 -0
  65. package/src/locales/default/plugin.ts +12 -0
  66. package/src/server/routers/tools/search.ts +23 -0
  67. package/src/services/search.ts +8 -0
  68. package/src/store/chat/slices/builtinTool/actions/searXNG.ts +50 -0
  69. package/src/store/chat/slices/builtinTool/initialState.ts +1 -0
  70. package/src/tools/web-browsing/Portal/PageContent/index.tsx +190 -0
  71. package/src/tools/web-browsing/Portal/PageContents/index.tsx +23 -0
  72. package/src/tools/web-browsing/Portal/{ResultList → Search/ResultList}/SearchItem/Video.tsx +1 -1
  73. package/src/tools/web-browsing/Portal/Search/index.tsx +69 -0
  74. package/src/tools/web-browsing/Portal/index.tsx +28 -64
  75. package/src/tools/web-browsing/Render/PageContent/Loading.tsx +57 -0
  76. package/src/tools/web-browsing/Render/PageContent/Result.tsx +142 -0
  77. package/src/tools/web-browsing/Render/PageContent/index.tsx +41 -0
  78. package/src/tools/web-browsing/Render/{SearchQuery → Search/SearchQuery}/SearchView.tsx +1 -1
  79. package/src/tools/web-browsing/Render/{SearchQuery → Search/SearchQuery}/index.tsx +1 -1
  80. package/src/tools/web-browsing/Render/{SearchResult → Search/SearchResult}/ShowMore.tsx +1 -1
  81. package/src/tools/web-browsing/Render/Search/index.tsx +62 -0
  82. package/src/tools/web-browsing/Render/index.tsx +35 -44
  83. package/src/tools/web-browsing/index.ts +43 -47
  84. package/src/tools/web-browsing/systemRole.ts +109 -0
  85. package/src/types/tool/builtin.ts +2 -0
  86. package/src/types/tool/crawler.ts +19 -0
  87. package/src/types/tool/search.ts +1 -0
  88. /package/src/tools/web-browsing/Portal/{Footer.tsx → Search/Footer.tsx} +0 -0
  89. /package/src/tools/web-browsing/Portal/{ResultList → Search/ResultList}/SearchItem/CategoryAvatar.tsx +0 -0
  90. /package/src/tools/web-browsing/Portal/{ResultList → Search/ResultList}/SearchItem/TitleExtra.tsx +0 -0
  91. /package/src/tools/web-browsing/Portal/{ResultList → Search/ResultList}/SearchItem/index.tsx +0 -0
  92. /package/src/tools/web-browsing/Portal/{ResultList → Search/ResultList}/index.tsx +0 -0
  93. /package/src/tools/web-browsing/Render/{ConfigForm → Search/ConfigForm}/Form.tsx +0 -0
  94. /package/src/tools/web-browsing/Render/{ConfigForm → Search/ConfigForm}/SearchXNGIcon.tsx +0 -0
  95. /package/src/tools/web-browsing/Render/{ConfigForm → Search/ConfigForm}/index.tsx +0 -0
  96. /package/src/tools/web-browsing/Render/{ConfigForm → Search/ConfigForm}/style.tsx +0 -0
  97. /package/src/tools/web-browsing/Render/{SearchResult → Search/SearchResult}/SearchResultItem.tsx +0 -0
  98. /package/src/tools/web-browsing/Render/{SearchResult → Search/SearchResult}/index.tsx +0 -0
@@ -0,0 +1,35 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { expect } from 'vitest';
4
+
5
+ import { FilterOptions } from '../type';
6
+ import { htmlToMarkdown } from './htmlToMarkdown';
7
+
8
+ interface TestItem {
9
+ file: string;
10
+ url: string;
11
+ filterOptions?: FilterOptions;
12
+ }
13
+ const list: TestItem[] = [
14
+ {
15
+ file: 'terms.html',
16
+ url: 'https://lobehub.com/terms',
17
+ },
18
+ {
19
+ file: 'yingchao.html',
20
+ url: 'https://www.qiumiwu.com/standings/yingchao',
21
+ filterOptions: { pureText: true, enableReadability: false },
22
+ },
23
+ ];
24
+
25
+ describe('htmlToMarkdown', () => {
26
+ list.forEach((item) => {
27
+ it(`should transform ${item.file} to markdown`, () => {
28
+ const html = readFileSync(path.join(__dirname, `./html/${item.file}`), { encoding: 'utf8' });
29
+
30
+ const data = htmlToMarkdown(html, { url: item.url, filterOptions: item.filterOptions || {} });
31
+
32
+ expect(data).toMatchSnapshot();
33
+ });
34
+ });
35
+ });
@@ -0,0 +1,45 @@
1
+ import { Readability } from '@mozilla/readability';
2
+ import { Window } from 'happy-dom';
3
+ import { NodeHtmlMarkdown, type TranslatorConfigObject } from 'node-html-markdown';
4
+
5
+ import { FilterOptions } from '../type';
6
+
7
+ export const htmlToMarkdown = (
8
+ html: string,
9
+ { url, filterOptions }: { filterOptions: FilterOptions; url: string },
10
+ ) => {
11
+ const window = new Window({ url });
12
+
13
+ const document = window.document;
14
+ document.body.innerHTML = html;
15
+
16
+ // @ts-expect-error reason: Readability expects a Document type
17
+ const parsedContent = new Readability(document).parse();
18
+
19
+ const useReadability = filterOptions.enableReadability ?? true;
20
+
21
+ let htmlNode = html;
22
+
23
+ if (useReadability && parsedContent?.content) {
24
+ htmlNode = parsedContent?.content;
25
+ }
26
+
27
+ const customTranslators = (
28
+ filterOptions.pureText
29
+ ? {
30
+ a: {
31
+ postprocess: (_: string, content: string) => content,
32
+ },
33
+ img: {
34
+ ignore: true,
35
+ },
36
+ }
37
+ : {}
38
+ ) as TranslatorConfigObject;
39
+
40
+ const nodeHtmlMarkdown = new NodeHtmlMarkdown({}, customTranslators);
41
+
42
+ const content = nodeHtmlMarkdown.translate(htmlNode);
43
+
44
+ return { ...parsedContent, content };
45
+ };
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "CommonJS",
4
+ "target": "ESNext",
5
+ "lib": ["dom", "dom.iterable", "esnext"],
6
+ "sourceMap": true,
7
+ "skipDefaultLibCheck": true,
8
+ "jsx": "react-jsx",
9
+ "baseUrl": ".",
10
+ "allowSyntheticDefaultImports": true,
11
+ "moduleResolution": "node",
12
+ "forceConsistentCasingInFileNames": true,
13
+ "noImplicitReturns": true,
14
+ "noUnusedLocals": true,
15
+ "resolveJsonModule": true,
16
+ "skipLibCheck": true,
17
+ "strict": true,
18
+ "types": ["vitest/globals"]
19
+ }
20
+ }
@@ -0,0 +1,3 @@
1
+ packages:
2
+ - 'packages/**'
3
+ - '.'
@@ -1,14 +1,9 @@
1
- import { Icon } from '@lobehub/ui';
2
- import { ConfigProvider, Empty } from 'antd';
3
- import { useTheme } from 'antd-style';
4
- import { LucideSquareArrowLeft, LucideSquareArrowRight } from 'lucide-react';
5
- import { memo, useContext, useEffect } from 'react';
6
- import { useTranslation } from 'react-i18next';
7
- import { Center, Flexbox } from 'react-layout-kit';
1
+ import { memo, useEffect } from 'react';
2
+ import { Flexbox } from 'react-layout-kit';
8
3
 
9
4
  import PluginRender from '@/features/PluginsUI/Render';
10
5
  import { useChatStore } from '@/store/chat';
11
- import { chatPortalSelectors, chatSelectors } from '@/store/chat/selectors';
6
+ import { chatSelectors } from '@/store/chat/selectors';
12
7
  import { ChatMessage } from '@/types/message';
13
8
 
14
9
  import Arguments from './Arguments';
@@ -30,14 +25,7 @@ const CustomRender = memo<CustomRenderProps>(
30
25
  setShowPluginRender,
31
26
  pluginError,
32
27
  }) => {
33
- const [loading, isMessageToolUIOpen] = useChatStore((s) => [
34
- chatSelectors.isPluginApiInvoking(id)(s),
35
- chatPortalSelectors.isPluginUIOpen(id)(s),
36
- ]);
37
- const { direction } = useContext(ConfigProvider.ConfigContext);
38
- const { t } = useTranslation('plugin');
39
-
40
- const theme = useTheme();
28
+ const [loading] = useChatStore((s) => [chatSelectors.isPluginApiInvoking(id)(s)]);
41
29
 
42
30
  useEffect(() => {
43
31
  if (!plugin?.type || loading) return;
@@ -45,25 +33,6 @@ const CustomRender = memo<CustomRenderProps>(
45
33
  setShowPluginRender(plugin?.type !== 'default');
46
34
  }, [plugin?.type, loading]);
47
35
 
48
- if (isMessageToolUIOpen)
49
- return (
50
- <Center paddingBlock={8} style={{ background: theme.colorFillQuaternary, borderRadius: 4 }}>
51
- <Empty
52
- description={t('showInPortal')}
53
- image={
54
- <Icon
55
- color={theme.colorTextQuaternary}
56
- icon={direction === 'rtl' ? LucideSquareArrowLeft : LucideSquareArrowRight}
57
- size={'large'}
58
- />
59
- }
60
- styles={{
61
- image: { height: 24 },
62
- }}
63
- />
64
- </Center>
65
- );
66
-
67
36
  if (loading) return <Arguments arguments={requestArgs} shine />;
68
37
 
69
38
  return (
@@ -1,12 +1,12 @@
1
1
  import { Suspense, memo } from 'react';
2
2
 
3
3
  import { LOADING_FLAT } from '@/const/message';
4
- import ErrorResponse from '@/features/Conversation/Messages/Assistant/Tool/Render/ErrorResponse';
5
4
  import { useChatStore } from '@/store/chat';
6
5
  import { chatSelectors } from '@/store/chat/selectors';
7
6
 
8
7
  import Arguments from './Arguments';
9
8
  import CustomRender from './CustomRender';
9
+ import ErrorResponse from './ErrorResponse';
10
10
 
11
11
  interface RenderProps {
12
12
  messageId: string;
@@ -7,6 +7,7 @@ import Loading from '../Loading';
7
7
  import { useParseContent } from '../useParseContent';
8
8
 
9
9
  export interface BuiltinTypeProps {
10
+ apiName?: string;
10
11
  arguments?: string;
11
12
  content: string;
12
13
  id: string;
@@ -25,6 +26,7 @@ const BuiltinType = memo<BuiltinTypeProps>(
25
26
  identifier,
26
27
  loading,
27
28
  pluginError,
29
+ apiName,
28
30
  }) => {
29
31
  const { isJSON, data } = useParseContent(content);
30
32
 
@@ -40,6 +42,7 @@ const BuiltinType = memo<BuiltinTypeProps>(
40
42
 
41
43
  return (
42
44
  <Render
45
+ apiName={apiName}
43
46
  args={args}
44
47
  content={data}
45
48
  identifier={identifier}
@@ -40,6 +40,7 @@ const PluginRender = memo<PluginRenderProps>(
40
40
  case 'builtin': {
41
41
  return (
42
42
  <BuiltinType
43
+ apiName={payload?.apiName}
43
44
  arguments={argumentsStr}
44
45
  content={content}
45
46
  id={id}
@@ -40,6 +40,7 @@ const ToolRender = memo(() => {
40
40
 
41
41
  return (
42
42
  <Render
43
+ apiName={plugin.apiName}
43
44
  arguments={args}
44
45
  identifier={plugin.identifier}
45
46
  messageId={messageId}
@@ -141,6 +141,18 @@ export default {
141
141
  close: '删除',
142
142
  confirm: '已完成配置并重试',
143
143
  },
144
+ crawPages: {
145
+ crawling: '链接识别中',
146
+ detail: {
147
+ preview: '预览',
148
+ raw: '原始文本',
149
+ tooLong: '文本内容过长,对话上下文仅保留前 10000 字符,超过部分不计入会话上下文',
150
+ },
151
+ meta: {
152
+ crawler: '抓取模式',
153
+ words: '字符数',
154
+ },
155
+ },
144
156
  searchxng: {
145
157
  baseURL: '请输入',
146
158
  description: '请输入 SearchXNG 的网址,即可开始联网搜索',
@@ -1,4 +1,6 @@
1
+ import { Crawler } from '@lobechat/web-crawler';
1
2
  import { TRPCError } from '@trpc/server';
3
+ import pMap from 'p-map';
2
4
  import { z } from 'zod';
3
5
 
4
6
  import { toolsEnv } from '@/config/tools';
@@ -10,6 +12,27 @@ import { SEARCH_SEARXNG_NOT_CONFIG } from '@/types/tool/search';
10
12
  const searchProcedure = isServerMode ? authedProcedure : passwordProcedure;
11
13
 
12
14
  export const searchRouter = router({
15
+ crawlPages: searchProcedure
16
+ .input(
17
+ z.object({
18
+ impls: z.string().array().optional(),
19
+ urls: z.string().array(),
20
+ }),
21
+ )
22
+ .mutation(async ({ input }) => {
23
+ const crawler = new Crawler();
24
+
25
+ const results = await pMap(
26
+ input.urls,
27
+ async (url) => {
28
+ return await crawler.crawl({ impls: input.impls, url });
29
+ },
30
+ { concurrency: 10 },
31
+ );
32
+
33
+ return { results };
34
+ }),
35
+
13
36
  query: searchProcedure
14
37
  .input(
15
38
  z.object({
@@ -4,6 +4,14 @@ class SearchService {
4
4
  search(query: string, searchEngine?: string[]) {
5
5
  return toolsClient.search.query.query({ query, searchEngine });
6
6
  }
7
+
8
+ crawlPage(url: string) {
9
+ return toolsClient.search.crawlPages.mutate({ urls: [url] });
10
+ }
11
+
12
+ crawlPages(urls: string[]) {
13
+ return toolsClient.search.crawlPages.mutate({ urls });
14
+ }
7
15
  }
8
16
 
9
17
  export const searchService = new SearchService();
@@ -13,6 +13,16 @@ import {
13
13
  import { nanoid } from '@/utils/uuid';
14
14
 
15
15
  export interface SearchAction {
16
+ crawlMultiPages: (
17
+ id: string,
18
+ params: { urls: string[] },
19
+ aiSummary?: boolean,
20
+ ) => Promise<boolean | undefined>;
21
+ crawlSinglePage: (
22
+ id: string,
23
+ params: { url: string },
24
+ aiSummary?: boolean,
25
+ ) => Promise<boolean | undefined>;
16
26
  /**
17
27
  * 重新发起搜索
18
28
  * @description 会更新插件的 arguments 参数,然后再次搜索
@@ -28,6 +38,7 @@ export interface SearchAction {
28
38
  data: SearchQuery,
29
39
  aiSummary?: boolean,
30
40
  ) => Promise<void | boolean>;
41
+ togglePageContent: (url: string) => void;
31
42
  toggleSearchLoading: (id: string, loading: boolean) => void;
32
43
  }
33
44
 
@@ -37,12 +48,47 @@ export const searchSlice: StateCreator<
37
48
  [],
38
49
  SearchAction
39
50
  > = (set, get) => ({
51
+ crawlMultiPages: async (id, params, aiSummary = true) => {
52
+ const { internal_updateMessageContent } = get();
53
+ get().toggleSearchLoading(id, true);
54
+ const response = await searchService.crawlPages(params.urls);
55
+
56
+ await get().updatePluginState(id, response);
57
+ get().toggleSearchLoading(id, false);
58
+ const { results } = response;
59
+
60
+ if (!results) return;
61
+
62
+ const content = results.map((item) =>
63
+ 'errorMessage' in item
64
+ ? item
65
+ : {
66
+ ...item.data,
67
+ // if crawl too many content
68
+ // slice the top 10000 char
69
+ content: item.data.content?.slice(0, 10_000),
70
+ },
71
+ );
72
+
73
+ await internal_updateMessageContent(id, JSON.stringify(content));
74
+
75
+ // if aiSummary is true, then trigger ai message
76
+ return aiSummary;
77
+ },
78
+
79
+ crawlSinglePage: async (id, params, aiSummary) => {
80
+ const { crawlMultiPages } = get();
81
+
82
+ return await crawlMultiPages(id, { urls: [params.url] }, aiSummary);
83
+ },
84
+
40
85
  reSearchWithSearXNG: async (id, data, options) => {
41
86
  get().toggleSearchLoading(id, true);
42
87
  await get().updatePluginArguments(id, data);
43
88
 
44
89
  await get().searchWithSearXNG(id, data, options?.aiSummary);
45
90
  },
91
+
46
92
  saveSearXNGSearchResult: async (id) => {
47
93
  const message = chatSelectors.getMessageById(id)(get());
48
94
  if (!message || !message.plugin) return;
@@ -133,6 +179,10 @@ export const searchSlice: StateCreator<
133
179
  return aiSummary;
134
180
  },
135
181
 
182
+ togglePageContent: (url) => {
183
+ set({ activePageContentUrl: url });
184
+ },
185
+
136
186
  toggleSearchLoading: (id, loading) => {
137
187
  set(
138
188
  { searchLoading: { ...get().searchLoading, [id]: loading } },
@@ -1,6 +1,7 @@
1
1
  import { FileItem } from '@/types/files';
2
2
 
3
3
  export interface ChatToolState {
4
+ activePageContentUrl?: string;
4
5
  dalleImageLoading: Record<string, boolean>;
5
6
  dalleImageMap: Record<string, FileItem>;
6
7
  searchLoading: Record<string, boolean>;
@@ -0,0 +1,190 @@
1
+ import { Alert, CopyButton, Icon, Markdown } from '@lobehub/ui';
2
+ import { Descriptions, Segmented, Typography } from 'antd';
3
+ import { createStyles } from 'antd-style';
4
+ import { ExternalLink } from 'lucide-react';
5
+ import Link from 'next/link';
6
+ import { memo, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Flexbox } from 'react-layout-kit';
9
+
10
+ import { CrawlResult } from '@/types/tool/crawler';
11
+
12
+ const useStyles = createStyles(({ token, css }) => {
13
+ return {
14
+ cardBody: css`
15
+ padding-block: 12px 8px;
16
+ padding-inline: 16px;
17
+ `,
18
+ container: css`
19
+ cursor: pointer;
20
+
21
+ overflow: hidden;
22
+
23
+ max-width: 360px;
24
+ border: 1px solid ${token.colorBorderSecondary};
25
+ border-radius: 12px;
26
+
27
+ transition: border-color 0.2s;
28
+
29
+ :hover {
30
+ border-color: ${token.colorPrimary};
31
+ }
32
+ `,
33
+ description: css`
34
+ margin-block: 0 4px !important;
35
+ color: ${token.colorTextSecondary};
36
+ `,
37
+ detailsSection: css`
38
+ padding-block: ${token.paddingSM}px;
39
+ `,
40
+ externalLink: css`
41
+ color: ${token.colorPrimary};
42
+ `,
43
+ footer: css`
44
+ padding: ${token.paddingXS}px;
45
+ border-radius: 6px;
46
+ text-align: center;
47
+ background-color: ${token.colorFillQuaternary};
48
+ `,
49
+ footerText: css`
50
+ font-size: ${token.fontSizeSM}px;
51
+ color: ${token.colorTextTertiary} !important;
52
+ `,
53
+ metaInfo: css`
54
+ display: flex;
55
+ align-items: center;
56
+ color: ${token.colorTextSecondary};
57
+ `,
58
+ sliced: css`
59
+ color: ${token.colorTextQuaternary};
60
+ `,
61
+ title: css`
62
+ overflow: hidden;
63
+ display: -webkit-box;
64
+ -webkit-box-orient: vertical;
65
+ -webkit-line-clamp: 2;
66
+
67
+ margin-block-end: 0;
68
+
69
+ font-size: 16px;
70
+ font-weight: bold;
71
+ `,
72
+ titleRow: css`
73
+ color: ${token.colorText};
74
+ `,
75
+
76
+ url: css`
77
+ color: ${token.colorTextTertiary};
78
+ `,
79
+ };
80
+ });
81
+
82
+ interface PageContentProps {
83
+ messageId: string;
84
+ result?: CrawlResult;
85
+ }
86
+
87
+ const SLICED_LIMITED = 10_000;
88
+
89
+ const PageContent = memo<PageContentProps>(({ result }) => {
90
+ const { t } = useTranslation('plugin');
91
+ const { styles } = useStyles();
92
+ const [display, setDisplay] = useState('render');
93
+
94
+ if (!result) return undefined;
95
+
96
+ const { url, title, description, content } = result.data;
97
+ return (
98
+ <Flexbox gap={24}>
99
+ <Flexbox gap={8}>
100
+ <Flexbox
101
+ align={'center'}
102
+ className={styles.titleRow}
103
+ gap={24}
104
+ horizontal
105
+ justify={'space-between'}
106
+ >
107
+ <Flexbox>
108
+ <div className={styles.title}>{title || result.originalUrl}</div>
109
+ </Flexbox>
110
+ </Flexbox>
111
+ {description && (
112
+ <Typography.Paragraph
113
+ className={styles.description}
114
+ ellipsis={{ expandable: false, rows: 4 }}
115
+ >
116
+ {description}
117
+ </Typography.Paragraph>
118
+ )}
119
+ <Flexbox align={'center'} className={styles.url} gap={4} horizontal>
120
+ {result.data.siteName && <div>{result.data.siteName} · </div>}
121
+ <Link
122
+ className={styles.url}
123
+ href={url}
124
+ onClick={(e) => e.stopPropagation()}
125
+ rel={'nofollow'}
126
+ style={{ display: 'flex', gap: 4 }}
127
+ target={'_blank'}
128
+ >
129
+ {result.originalUrl}
130
+ <Icon icon={ExternalLink} />
131
+ </Link>
132
+ </Flexbox>
133
+
134
+ <div className={styles.footer}>
135
+ <Descriptions
136
+ classNames={{
137
+ content: styles.footerText,
138
+ }}
139
+ column={2}
140
+ items={[
141
+ {
142
+ children: result.data.content?.length,
143
+ label: t('search.crawPages.meta.words'),
144
+ },
145
+ {
146
+ children: result.crawler,
147
+ label: t('search.crawPages.meta.crawler'),
148
+ },
149
+ ]}
150
+ size="small"
151
+ />
152
+ </div>
153
+ </Flexbox>
154
+ {content && (
155
+ <Flexbox gap={12} paddingBlock={'0 12px'}>
156
+ <Flexbox horizontal justify={'space-between'}>
157
+ <Segmented
158
+ onChange={(value) => setDisplay(value)}
159
+ options={[
160
+ { label: t('search.crawPages.detail.preview'), value: 'render' },
161
+ { label: t('search.crawPages.detail.raw'), value: 'raw' },
162
+ ]}
163
+ value={display}
164
+ />
165
+ <CopyButton content={content} />
166
+ </Flexbox>
167
+ {content.length > SLICED_LIMITED && (
168
+ <Alert message={t('search.crawPages.detail.tooLong')} variant={'pure'} />
169
+ )}
170
+ {display === 'render' ? (
171
+ <Markdown variant={'chat'}>{content}</Markdown>
172
+ ) : (
173
+ <div style={{ paddingBlock: '0 12px' }}>
174
+ {content.length < SLICED_LIMITED ? (
175
+ content
176
+ ) : (
177
+ <>
178
+ <span>{content.slice(0, SLICED_LIMITED)}</span>
179
+ <span className={styles.sliced}>{content.slice(SLICED_LIMITED, -1)}</span>
180
+ </>
181
+ )}
182
+ </div>
183
+ )}
184
+ </Flexbox>
185
+ )}
186
+ </Flexbox>
187
+ );
188
+ });
189
+
190
+ export default PageContent;
@@ -0,0 +1,23 @@
1
+ import { memo } from 'react';
2
+
3
+ import { useChatStore } from '@/store/chat';
4
+ import { CrawlResult } from '@/types/tool/crawler';
5
+
6
+ import PageContent from '../PageContent';
7
+
8
+ interface PageContentProps {
9
+ messageId: string;
10
+ results: CrawlResult[];
11
+ urls: string[];
12
+ }
13
+
14
+ const PageContents = memo<PageContentProps>(({ urls, messageId, results }) => {
15
+ const activePageContentUrl = useChatStore((s) => s.activePageContentUrl);
16
+
17
+ const url = urls.find((u) => u === activePageContentUrl);
18
+ const result = results.find((result) => result.originalUrl === url);
19
+
20
+ return <PageContent messageId={messageId} result={result} />;
21
+ });
22
+
23
+ export default PageContents;
@@ -5,7 +5,7 @@ import { Flexbox } from 'react-layout-kit';
5
5
 
6
6
  import { SearchResult } from '@/types/tool/search';
7
7
 
8
- import { ENGINE_ICON_MAP } from '../../../const';
8
+ import { ENGINE_ICON_MAP } from '../../../../const';
9
9
  import TitleExtra from './TitleExtra';
10
10
 
11
11
  const useStyles = createStyles(({ css, token }) => {
@@ -0,0 +1,69 @@
1
+ import { Skeleton } from 'antd';
2
+ import { uniq } from 'lodash-es';
3
+ import { memo } from 'react';
4
+ import { Flexbox } from 'react-layout-kit';
5
+
6
+ import { useChatStore } from '@/store/chat';
7
+ import { chatToolSelectors } from '@/store/chat/selectors';
8
+ import { SearchQuery, SearchResponse } from '@/types/tool/search';
9
+
10
+ import SearchBar from '../../components/SearchBar';
11
+ import Footer from './Footer';
12
+ import ResultList from './ResultList';
13
+
14
+ interface InspectorUIProps {
15
+ messageId: string;
16
+ query: SearchQuery;
17
+ response: SearchResponse;
18
+ }
19
+
20
+ const Inspector = memo<InspectorUIProps>(({ query: args, messageId, response }) => {
21
+ const engines = uniq((response.results || []).map((result) => result.engine));
22
+ const defaultEngines = engines.length > 0 ? engines : args.searchEngines || [];
23
+ const loading = useChatStore(chatToolSelectors.isSearXNGSearching(messageId));
24
+
25
+ if (loading) {
26
+ return (
27
+ <Flexbox gap={12} height={'100%'}>
28
+ <SearchBar
29
+ aiSummary={false}
30
+ defaultEngines={defaultEngines}
31
+ defaultQuery={args.query}
32
+ messageId={messageId}
33
+ tooltip={false}
34
+ />
35
+
36
+ <Flexbox gap={16} paddingBlock={16} paddingInline={12}>
37
+ {[1, 2, 3, 4, 6].map((id) => (
38
+ <Skeleton
39
+ active
40
+ key={id}
41
+ paragraph={{ rows: 3, width: `${(id % 4) + 5}0%` }}
42
+ title={false}
43
+ />
44
+ ))}
45
+ </Flexbox>
46
+ </Flexbox>
47
+ );
48
+ }
49
+
50
+ return (
51
+ <Flexbox gap={0} height={'100%'}>
52
+ <Flexbox gap={12} height={'100%'}>
53
+ <SearchBar
54
+ aiSummary={false}
55
+ defaultEngines={defaultEngines}
56
+ defaultQuery={args.query}
57
+ messageId={messageId}
58
+ tooltip={false}
59
+ />
60
+ <Flexbox height={'100%'} width={'100%'}>
61
+ <ResultList dataSources={response.results} />
62
+ </Flexbox>
63
+ </Flexbox>
64
+ <Footer />
65
+ </Flexbox>
66
+ );
67
+ });
68
+
69
+ export default Inspector;