@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.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/locales/ar/models.json +9 -3
- package/locales/ar/plugin.json +12 -0
- package/locales/bg-BG/models.json +9 -3
- package/locales/bg-BG/plugin.json +12 -0
- package/locales/de-DE/models.json +9 -3
- package/locales/de-DE/plugin.json +12 -0
- package/locales/en-US/models.json +9 -3
- package/locales/en-US/plugin.json +12 -0
- package/locales/es-ES/models.json +9 -3
- package/locales/es-ES/plugin.json +12 -0
- package/locales/fa-IR/models.json +9 -3
- package/locales/fa-IR/plugin.json +12 -0
- package/locales/fr-FR/models.json +9 -3
- package/locales/fr-FR/plugin.json +12 -0
- package/locales/it-IT/models.json +9 -3
- package/locales/it-IT/plugin.json +12 -0
- package/locales/ja-JP/models.json +9 -3
- package/locales/ja-JP/plugin.json +12 -0
- package/locales/ko-KR/models.json +9 -3
- package/locales/ko-KR/plugin.json +12 -0
- package/locales/nl-NL/models.json +9 -3
- package/locales/nl-NL/plugin.json +12 -0
- package/locales/pl-PL/models.json +9 -3
- package/locales/pl-PL/plugin.json +12 -0
- package/locales/pt-BR/models.json +9 -3
- package/locales/pt-BR/plugin.json +12 -0
- package/locales/ru-RU/models.json +9 -3
- package/locales/ru-RU/plugin.json +12 -0
- package/locales/tr-TR/models.json +9 -3
- package/locales/tr-TR/plugin.json +12 -0
- package/locales/vi-VN/models.json +9 -3
- package/locales/vi-VN/plugin.json +12 -0
- package/locales/zh-CN/models.json +9 -3
- package/locales/zh-CN/plugin.json +12 -0
- package/locales/zh-TW/models.json +9 -3
- package/locales/zh-TW/plugin.json +12 -0
- package/package.json +5 -1
- package/packages/web-crawler/README.md +34 -0
- package/packages/web-crawler/package.json +13 -0
- package/packages/web-crawler/src/crawImpl/browserless.ts +62 -0
- package/packages/web-crawler/src/crawImpl/index.ts +11 -0
- package/packages/web-crawler/src/crawImpl/jina.ts +37 -0
- package/packages/web-crawler/src/crawImpl/naive.ts +84 -0
- package/packages/web-crawler/src/crawler.ts +66 -0
- package/packages/web-crawler/src/index.ts +2 -0
- package/packages/web-crawler/src/type.ts +42 -0
- package/packages/web-crawler/src/urlRules.ts +34 -0
- package/packages/web-crawler/src/utils/__snapshots__/htmlToMarkdown.test.ts.snap +638 -0
- package/packages/web-crawler/src/utils/appUrlRules.test.ts +26 -0
- package/packages/web-crawler/src/utils/appUrlRules.ts +40 -0
- package/packages/web-crawler/src/utils/errorType.ts +12 -0
- package/packages/web-crawler/src/utils/html/terms.html +1222 -0
- package/packages/web-crawler/src/utils/html/yingchao.html +1001 -0
- package/packages/web-crawler/src/utils/htmlToMarkdown.test.ts +35 -0
- package/packages/web-crawler/src/utils/htmlToMarkdown.ts +45 -0
- package/packages/web-crawler/tsconfig.json +20 -0
- package/pnpm-workspace.yaml +3 -0
- package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +4 -35
- package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +1 -1
- package/src/features/PluginsUI/Render/BuiltinType/index.tsx +3 -0
- package/src/features/PluginsUI/Render/index.tsx +1 -0
- package/src/features/Portal/Plugins/Body/ToolRender.tsx +1 -0
- package/src/locales/default/plugin.ts +12 -0
- package/src/server/routers/tools/search.ts +23 -0
- package/src/services/search.ts +8 -0
- package/src/store/chat/slices/builtinTool/actions/searXNG.ts +50 -0
- package/src/store/chat/slices/builtinTool/initialState.ts +1 -0
- package/src/tools/web-browsing/Portal/PageContent/index.tsx +190 -0
- package/src/tools/web-browsing/Portal/PageContents/index.tsx +23 -0
- package/src/tools/web-browsing/Portal/{ResultList → Search/ResultList}/SearchItem/Video.tsx +1 -1
- package/src/tools/web-browsing/Portal/Search/index.tsx +69 -0
- package/src/tools/web-browsing/Portal/index.tsx +28 -64
- package/src/tools/web-browsing/Render/PageContent/Loading.tsx +57 -0
- package/src/tools/web-browsing/Render/PageContent/Result.tsx +142 -0
- package/src/tools/web-browsing/Render/PageContent/index.tsx +41 -0
- package/src/tools/web-browsing/Render/{SearchQuery → Search/SearchQuery}/SearchView.tsx +1 -1
- package/src/tools/web-browsing/Render/{SearchQuery → Search/SearchQuery}/index.tsx +1 -1
- package/src/tools/web-browsing/Render/{SearchResult → Search/SearchResult}/ShowMore.tsx +1 -1
- package/src/tools/web-browsing/Render/Search/index.tsx +62 -0
- package/src/tools/web-browsing/Render/index.tsx +35 -44
- package/src/tools/web-browsing/index.ts +43 -47
- package/src/tools/web-browsing/systemRole.ts +109 -0
- package/src/types/tool/builtin.ts +2 -0
- package/src/types/tool/crawler.ts +19 -0
- package/src/types/tool/search.ts +1 -0
- /package/src/tools/web-browsing/Portal/{Footer.tsx → Search/Footer.tsx} +0 -0
- /package/src/tools/web-browsing/Portal/{ResultList → Search/ResultList}/SearchItem/CategoryAvatar.tsx +0 -0
- /package/src/tools/web-browsing/Portal/{ResultList → Search/ResultList}/SearchItem/TitleExtra.tsx +0 -0
- /package/src/tools/web-browsing/Portal/{ResultList → Search/ResultList}/SearchItem/index.tsx +0 -0
- /package/src/tools/web-browsing/Portal/{ResultList → Search/ResultList}/index.tsx +0 -0
- /package/src/tools/web-browsing/Render/{ConfigForm → Search/ConfigForm}/Form.tsx +0 -0
- /package/src/tools/web-browsing/Render/{ConfigForm → Search/ConfigForm}/SearchXNGIcon.tsx +0 -0
- /package/src/tools/web-browsing/Render/{ConfigForm → Search/ConfigForm}/index.tsx +0 -0
- /package/src/tools/web-browsing/Render/{ConfigForm → Search/ConfigForm}/style.tsx +0 -0
- /package/src/tools/web-browsing/Render/{SearchResult → Search/SearchResult}/SearchResultItem.tsx +0 -0
- /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
|
+
}
|
@@ -1,14 +1,9 @@
|
|
1
|
-
import {
|
2
|
-
import {
|
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 {
|
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
|
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}
|
@@ -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({
|
package/src/services/search.ts
CHANGED
@@ -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 } },
|
@@ -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 '
|
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;
|