@lobehub/chat 1.136.9 → 1.136.10
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/package.json +1 -1
- package/packages/prompts/src/prompts/index.ts +1 -0
- package/packages/prompts/src/prompts/search/crawlResults.test.ts +172 -0
- package/packages/prompts/src/prompts/search/crawlResults.ts +82 -0
- package/packages/prompts/src/prompts/search/index.ts +2 -0
- package/packages/prompts/src/prompts/search/searchResults.test.ts +138 -0
- package/packages/prompts/src/prompts/search/searchResults.ts +58 -0
- package/packages/prompts/src/prompts/search/xmlEscape.ts +21 -0
- package/packages/types/src/tool/builtin.ts +8 -0
- package/packages/types/src/tool/index.ts +2 -0
- package/src/features/Conversation/Messages/Assistant/Tool/Inspector/BuiltinPluginTitle.tsx +17 -13
- package/src/features/Conversation/Messages/Assistant/Tool/Inspector/ToolTitle.tsx +1 -8
- package/src/features/Conversation/Messages/Assistant/Tool/Render/Arguments/index.tsx +12 -16
- package/src/features/Conversation/Messages/Assistant/Tool/Render/LoadingPlaceholder/index.tsx +29 -0
- package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +26 -7
- package/src/features/Conversation/Messages/Assistant/Tool/index.tsx +2 -0
- package/src/features/PluginsUI/Render/BuiltinType/index.test.tsx +5 -5
- package/src/features/PluginsUI/Render/BuiltinType/index.tsx +1 -7
- package/src/store/chat/slices/builtinTool/actions/search.test.ts +4 -3
- package/src/store/chat/slices/builtinTool/actions/search.ts +23 -15
- package/src/store/chat/slices/message/selectors.ts +10 -0
- package/src/styles/text.ts +10 -7
- package/src/tools/placeholders.ts +8 -0
- package/src/tools/web-browsing/Placeholder/PageContent.tsx +27 -0
- package/src/tools/web-browsing/Placeholder/Search.tsx +65 -0
- package/src/tools/web-browsing/Placeholder/index.tsx +40 -0
- package/src/tools/web-browsing/Render/PageContent/Loading.tsx +14 -5
- package/src/tools/web-browsing/Render/PageContent/Result.tsx +14 -13
- package/src/tools/web-browsing/Render/PageContent/index.tsx +15 -6
- package/src/tools/web-browsing/Render/Search/SearchQuery/SearchView.tsx +13 -19
- package/src/tools/web-browsing/Render/Search/SearchResult/index.tsx +21 -26
- package/src/tools/web-browsing/components/EngineAvatar.tsx +8 -2
- package/src/tools/web-browsing/const.ts +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 1.136.10](https://github.com/lobehub/lobe-chat/compare/v1.136.9...v1.136.10)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2025-10-11**</sup>
|
|
8
|
+
|
|
9
|
+
#### 💄 Styles
|
|
10
|
+
|
|
11
|
+
- **misc**: Improve search experience.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### Styles
|
|
19
|
+
|
|
20
|
+
- **misc**: Improve search experience, closes [#9661](https://github.com/lobehub/lobe-chat/issues/9661) ([8624f84](https://github.com/lobehub/lobe-chat/commit/8624f84))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
### [Version 1.136.9](https://github.com/lobehub/lobe-chat/compare/v1.136.8...v1.136.9)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2025-10-11**</sup>
|
package/changelog/v1.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "1.136.
|
|
3
|
+
"version": "1.136.10",
|
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { crawlResultsPrompt } from './crawlResults';
|
|
4
|
+
|
|
5
|
+
describe('crawlResultsPrompt', () => {
|
|
6
|
+
it('should return empty XML for empty results', () => {
|
|
7
|
+
const result = crawlResultsPrompt([]);
|
|
8
|
+
expect(result).toBe('<no_crawl_results />');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should convert basic crawl result to compact XML format', () => {
|
|
12
|
+
const results = [
|
|
13
|
+
{
|
|
14
|
+
url: 'https://example.com',
|
|
15
|
+
title: 'Example Page',
|
|
16
|
+
content: 'Page content here',
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const xml = crawlResultsPrompt(results);
|
|
21
|
+
|
|
22
|
+
expect(xml).toEqual(`<crawlResults>
|
|
23
|
+
<page url="https://example.com" title="Example Page">Page content here</page>
|
|
24
|
+
</crawlResults>`);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should include all optional metadata fields', () => {
|
|
28
|
+
const results = [
|
|
29
|
+
{
|
|
30
|
+
url: 'http://arxiv.org/abs/2509.09734v1',
|
|
31
|
+
title: 'MCP-AgentBench: Evaluating Real-World Language Agent Performance',
|
|
32
|
+
contentType: 'text' as const,
|
|
33
|
+
description: 'Abstract page for arXiv paper 2509.09734v1',
|
|
34
|
+
length: 10187,
|
|
35
|
+
content: 'Full paper content...',
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const xml = crawlResultsPrompt(results);
|
|
40
|
+
|
|
41
|
+
expect(xml).toEqual(`<crawlResults>
|
|
42
|
+
<page url="http://arxiv.org/abs/2509.09734v1" title="MCP-AgentBench: Evaluating Real-World Language Agent Performance" contentType="text" description="Abstract page for arXiv paper 2509.09734v1" length="10187">Full paper content...</page>
|
|
43
|
+
</crawlResults>`);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should handle page without content', () => {
|
|
47
|
+
const results = [
|
|
48
|
+
{
|
|
49
|
+
url: 'https://example.com',
|
|
50
|
+
title: 'Empty Page',
|
|
51
|
+
contentType: 'text' as const,
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const xml = crawlResultsPrompt(results);
|
|
56
|
+
|
|
57
|
+
expect(xml).toEqual(`<crawlResults>
|
|
58
|
+
<page url="https://example.com" title="Empty Page" contentType="text" />
|
|
59
|
+
</crawlResults>`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should handle error items', () => {
|
|
63
|
+
const results = [
|
|
64
|
+
{
|
|
65
|
+
errorType: 'NetworkError',
|
|
66
|
+
errorMessage: 'Failed to fetch the page',
|
|
67
|
+
url: 'https://failed.com',
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const xml = crawlResultsPrompt(results);
|
|
72
|
+
|
|
73
|
+
expect(xml).toEqual(`<crawlResults>
|
|
74
|
+
<error errorType="NetworkError" errorMessage="Failed to fetch the page" url="https://failed.com" />
|
|
75
|
+
</crawlResults>`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should escape XML special characters in attributes', () => {
|
|
79
|
+
const results = [
|
|
80
|
+
{
|
|
81
|
+
url: 'https://example.com?foo=bar&baz=qux',
|
|
82
|
+
title: 'Title with <tags> & "quotes"',
|
|
83
|
+
description: 'Description with special chars & <html>',
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const xml = crawlResultsPrompt(results);
|
|
88
|
+
|
|
89
|
+
expect(xml).toEqual(`<crawlResults>
|
|
90
|
+
<page url="https://example.com?foo=bar&baz=qux" title="Title with <tags> & "quotes"" description="Description with special chars & <html>" />
|
|
91
|
+
</crawlResults>`);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should escape XML special characters in content', () => {
|
|
95
|
+
const results = [
|
|
96
|
+
{
|
|
97
|
+
url: 'https://example.com',
|
|
98
|
+
title: 'Test',
|
|
99
|
+
content: 'Content with <html> tags & special chars',
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
const xml = crawlResultsPrompt(results);
|
|
104
|
+
|
|
105
|
+
expect(xml).toEqual(`<crawlResults>
|
|
106
|
+
<page url="https://example.com" title="Test">Content with <html> tags & special chars</page>
|
|
107
|
+
</crawlResults>`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle multiple pages with mixed success and errors', () => {
|
|
111
|
+
const results = [
|
|
112
|
+
{
|
|
113
|
+
url: 'https://success1.com',
|
|
114
|
+
title: 'First Page',
|
|
115
|
+
content: 'First content',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
errorType: 'TimeoutError',
|
|
119
|
+
errorMessage: 'Request timeout',
|
|
120
|
+
url: 'https://failed.com',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
url: 'https://success2.com',
|
|
124
|
+
title: 'Second Page',
|
|
125
|
+
content: 'Second content',
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
const xml = crawlResultsPrompt(results);
|
|
130
|
+
|
|
131
|
+
expect(xml).toEqual(`<crawlResults>
|
|
132
|
+
<page url="https://success1.com" title="First Page">First content</page>
|
|
133
|
+
<error errorType="TimeoutError" errorMessage="Request timeout" url="https://failed.com" />
|
|
134
|
+
<page url="https://success2.com" title="Second Page">Second content</page>
|
|
135
|
+
</crawlResults>`);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should handle error without url', () => {
|
|
139
|
+
const results = [
|
|
140
|
+
{
|
|
141
|
+
errorType: 'UnknownError',
|
|
142
|
+
errorMessage: 'Unknown error occurred',
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
const xml = crawlResultsPrompt(results);
|
|
147
|
+
|
|
148
|
+
expect(xml).toEqual(`<crawlResults>
|
|
149
|
+
<error errorType="UnknownError" errorMessage="Unknown error occurred" />
|
|
150
|
+
</crawlResults>`);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should handle real arXiv example', () => {
|
|
154
|
+
const results = [
|
|
155
|
+
{
|
|
156
|
+
url: 'http://arxiv.org/abs/2508.01780v1',
|
|
157
|
+
title: 'LiveMCPBench: Can Agents Navigate an Ocean of MCP Tools?',
|
|
158
|
+
contentType: 'text' as const,
|
|
159
|
+
description: 'Abstract page for arXiv paper 2508.01780v1',
|
|
160
|
+
length: 10512,
|
|
161
|
+
content:
|
|
162
|
+
'With the rapid development of Model Context Protocol (MCP), the number of MCP servers has surpassed 10,000...',
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const xml = crawlResultsPrompt(results);
|
|
167
|
+
|
|
168
|
+
expect(xml).toEqual(`<crawlResults>
|
|
169
|
+
<page url="http://arxiv.org/abs/2508.01780v1" title="LiveMCPBench: Can Agents Navigate an Ocean of MCP Tools?" contentType="text" description="Abstract page for arXiv paper 2508.01780v1" length="10512">With the rapid development of Model Context Protocol (MCP), the number of MCP servers has surpassed 10,000...</page>
|
|
170
|
+
</crawlResults>`);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { escapeXmlAttr, escapeXmlContent } from './xmlEscape';
|
|
2
|
+
|
|
3
|
+
export interface CrawlResultItem {
|
|
4
|
+
content?: string;
|
|
5
|
+
contentType?: 'text' | 'json';
|
|
6
|
+
description?: string;
|
|
7
|
+
length?: number;
|
|
8
|
+
siteName?: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
url: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CrawlErrorItem {
|
|
14
|
+
content?: string;
|
|
15
|
+
errorMessage: string;
|
|
16
|
+
errorType: string;
|
|
17
|
+
url?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Convert crawl results array to compact XML format for token efficiency
|
|
22
|
+
* Uses attributes for metadata and element content for main text
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const results = [
|
|
27
|
+
* { title: "Page Title", url: "https://example.com", content: "..." }
|
|
28
|
+
* ];
|
|
29
|
+
* const xml = crawlResultsPrompt(results);
|
|
30
|
+
* // Output:
|
|
31
|
+
* // <crawlResults>
|
|
32
|
+
* // <page title="Page Title" url="https://example.com">...</page>
|
|
33
|
+
* // </crawlResults>
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const crawlResultsPrompt = (results: Array<CrawlResultItem | CrawlErrorItem>): string => {
|
|
37
|
+
if (results.length === 0) return '<no_crawl_results />';
|
|
38
|
+
|
|
39
|
+
const items = results
|
|
40
|
+
.map((item) => {
|
|
41
|
+
// Handle error items
|
|
42
|
+
if ('errorMessage' in item) {
|
|
43
|
+
const attrs: string[] = [
|
|
44
|
+
`errorType="${escapeXmlAttr(item.errorType)}"`,
|
|
45
|
+
`errorMessage="${escapeXmlAttr(item.errorMessage)}"`,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
if (item.url) {
|
|
49
|
+
attrs.push(`url="${escapeXmlAttr(item.url)}"`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return ` <error ${attrs.join(' ')} />`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Handle successful crawl items
|
|
56
|
+
const attrs: string[] = [`url="${escapeXmlAttr(item.url)}"`];
|
|
57
|
+
|
|
58
|
+
if (item.title) {
|
|
59
|
+
attrs.push(`title="${escapeXmlAttr(item.title)}"`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (item.contentType) {
|
|
63
|
+
attrs.push(`contentType="${escapeXmlAttr(item.contentType)}"`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (item.description) {
|
|
67
|
+
attrs.push(`description="${escapeXmlAttr(item.description)}"`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (item.length !== undefined) {
|
|
71
|
+
attrs.push(`length="${item.length}"`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const attrString = attrs.join(' ');
|
|
75
|
+
const content = item.content ? escapeXmlContent(item.content) : '';
|
|
76
|
+
|
|
77
|
+
return content ? ` <page ${attrString}>${content}</page>` : ` <page ${attrString} />`;
|
|
78
|
+
})
|
|
79
|
+
.join('\n');
|
|
80
|
+
|
|
81
|
+
return `<crawlResults>\n${items}\n</crawlResults>`;
|
|
82
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { searchResultsPrompt } from './searchResults';
|
|
4
|
+
|
|
5
|
+
describe('searchResultsPrompt', () => {
|
|
6
|
+
it('should return empty XML for empty results', () => {
|
|
7
|
+
const result = searchResultsPrompt([]);
|
|
8
|
+
expect(result).toBe('<searchResults />');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should convert basic search results to compact XML format', () => {
|
|
12
|
+
const results = [
|
|
13
|
+
{
|
|
14
|
+
title: 'Example Title',
|
|
15
|
+
url: 'https://example.com',
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const xml = searchResultsPrompt(results);
|
|
20
|
+
|
|
21
|
+
expect(xml).toEqual(`<searchResults>
|
|
22
|
+
<item title="Example Title" url="https://example.com" />
|
|
23
|
+
</searchResults>`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should include content as element text when present', () => {
|
|
27
|
+
const results = [
|
|
28
|
+
{
|
|
29
|
+
title: 'Research Paper',
|
|
30
|
+
url: 'http://arxiv.org/abs/2108.11510v1',
|
|
31
|
+
content: 'Deep reinforcement learning survey',
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const xml = searchResultsPrompt(results);
|
|
36
|
+
|
|
37
|
+
expect(xml).toEqual(`<searchResults>
|
|
38
|
+
<item title="Research Paper" url="http://arxiv.org/abs/2108.11510v1">Deep reinforcement learning survey</item>
|
|
39
|
+
</searchResults>`);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should include optional fields as attributes when present', () => {
|
|
43
|
+
const results = [
|
|
44
|
+
{
|
|
45
|
+
title: 'Research Paper',
|
|
46
|
+
url: 'http://arxiv.org/abs/2108.11510v1',
|
|
47
|
+
content: 'Deep reinforcement learning survey',
|
|
48
|
+
publishedDate: '2021-08-25T23:01:48',
|
|
49
|
+
imgSrc: 'https://example.com/image.jpg',
|
|
50
|
+
thumbnail: 'https://example.com/thumb.jpg',
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const xml = searchResultsPrompt(results);
|
|
55
|
+
|
|
56
|
+
expect(xml).toEqual(`<searchResults>
|
|
57
|
+
<item title="Research Paper" url="http://arxiv.org/abs/2108.11510v1" publishedDate="2021-08-25T23:01:48" imgSrc="https://example.com/image.jpg" thumbnail="https://example.com/thumb.jpg">Deep reinforcement learning survey</item>
|
|
58
|
+
</searchResults>`);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should omit optional fields when not present', () => {
|
|
62
|
+
const results = [
|
|
63
|
+
{
|
|
64
|
+
title: 'Simple Result',
|
|
65
|
+
url: 'https://example.com',
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const xml = searchResultsPrompt(results);
|
|
70
|
+
|
|
71
|
+
expect(xml).toEqual(`<searchResults>
|
|
72
|
+
<item title="Simple Result" url="https://example.com" />
|
|
73
|
+
</searchResults>`);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should escape XML special characters in attributes', () => {
|
|
77
|
+
const results = [
|
|
78
|
+
{
|
|
79
|
+
title: 'Title with <tags> & "quotes"',
|
|
80
|
+
url: 'https://example.com?foo=bar&baz=qux',
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const xml = searchResultsPrompt(results);
|
|
85
|
+
|
|
86
|
+
expect(xml).toEqual(`<searchResults>
|
|
87
|
+
<item title="Title with <tags> & "quotes"" url="https://example.com?foo=bar&baz=qux" />
|
|
88
|
+
</searchResults>`);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should escape XML special characters in content', () => {
|
|
92
|
+
const results = [
|
|
93
|
+
{
|
|
94
|
+
title: 'Test',
|
|
95
|
+
url: 'https://example.com',
|
|
96
|
+
content: 'Content with <html> & special chars',
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const xml = searchResultsPrompt(results);
|
|
101
|
+
|
|
102
|
+
expect(xml).toEqual(`<searchResults>
|
|
103
|
+
<item title="Test" url="https://example.com">Content with <html> & special chars</item>
|
|
104
|
+
</searchResults>`);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should handle multiple search results', () => {
|
|
108
|
+
const results = [
|
|
109
|
+
{ title: 'First', url: 'https://first.com', content: 'First content' },
|
|
110
|
+
{ title: 'Second', url: 'https://second.com', content: 'Second content' },
|
|
111
|
+
{ title: 'Third', url: 'https://third.com' },
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const xml = searchResultsPrompt(results);
|
|
115
|
+
|
|
116
|
+
expect(xml).toEqual(`<searchResults>
|
|
117
|
+
<item title="First" url="https://first.com">First content</item>
|
|
118
|
+
<item title="Second" url="https://second.com">Second content</item>
|
|
119
|
+
<item title="Third" url="https://third.com" />
|
|
120
|
+
</searchResults>`);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should create compact single-line items', () => {
|
|
124
|
+
const results = [
|
|
125
|
+
{
|
|
126
|
+
title: 'Test',
|
|
127
|
+
url: 'https://test.com',
|
|
128
|
+
content: 'Content',
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const xml = searchResultsPrompt(results);
|
|
133
|
+
|
|
134
|
+
expect(xml).toEqual(`<searchResults>
|
|
135
|
+
<item title="Test" url="https://test.com">Content</item>
|
|
136
|
+
</searchResults>`);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { escapeXmlAttr, escapeXmlContent } from './xmlEscape';
|
|
2
|
+
|
|
3
|
+
export interface SearchResultItem {
|
|
4
|
+
content?: string;
|
|
5
|
+
imgSrc?: string;
|
|
6
|
+
publishedDate?: string | null;
|
|
7
|
+
thumbnail?: string | null;
|
|
8
|
+
title: string;
|
|
9
|
+
url: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Convert search results array to compact XML format for token efficiency
|
|
14
|
+
* Uses attributes for title/url and element content for main text
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const results = [
|
|
19
|
+
* { title: "Example", url: "https://example.com", content: "..." }
|
|
20
|
+
* ];
|
|
21
|
+
* const xml = searchResultsPrompt(results);
|
|
22
|
+
* // Output:
|
|
23
|
+
* // <searchResults>
|
|
24
|
+
* // <item title="Example" url="https://example.com">...</item>
|
|
25
|
+
* // </searchResults>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export const searchResultsPrompt = (results: SearchResultItem[]): string => {
|
|
29
|
+
if (results.length === 0) return '<searchResults />';
|
|
30
|
+
|
|
31
|
+
const items = results
|
|
32
|
+
.map((item) => {
|
|
33
|
+
const attrs: string[] = [
|
|
34
|
+
`title="${escapeXmlAttr(item.title)}"`,
|
|
35
|
+
`url="${escapeXmlAttr(item.url)}"`,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
if (item.publishedDate) {
|
|
39
|
+
attrs.push(`publishedDate="${escapeXmlAttr(item.publishedDate)}"`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (item.imgSrc) {
|
|
43
|
+
attrs.push(`imgSrc="${escapeXmlAttr(item.imgSrc)}"`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (item.thumbnail) {
|
|
47
|
+
attrs.push(`thumbnail="${escapeXmlAttr(item.thumbnail)}"`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const attrString = attrs.join(' ');
|
|
51
|
+
const content = item.content ? escapeXmlContent(item.content) : '';
|
|
52
|
+
|
|
53
|
+
return content ? ` <item ${attrString}>${content}</item>` : ` <item ${attrString} />`;
|
|
54
|
+
})
|
|
55
|
+
.join('\n');
|
|
56
|
+
|
|
57
|
+
return `<searchResults>\n${items}\n</searchResults>`;
|
|
58
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape special characters for XML attributes
|
|
3
|
+
* Includes: & " < >
|
|
4
|
+
*/
|
|
5
|
+
export const escapeXmlAttr = (text: string | undefined | null): string => {
|
|
6
|
+
if (!text) return '';
|
|
7
|
+
return text
|
|
8
|
+
.replaceAll('&', '&')
|
|
9
|
+
.replaceAll('"', '"')
|
|
10
|
+
.replaceAll('<', '<')
|
|
11
|
+
.replaceAll('>', '>');
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Escape special characters for XML content
|
|
16
|
+
* Includes: & < >
|
|
17
|
+
*/
|
|
18
|
+
export const escapeXmlContent = (text: string | undefined | null): string => {
|
|
19
|
+
if (!text) return '';
|
|
20
|
+
return text.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
21
|
+
};
|
|
@@ -49,3 +49,11 @@ export interface BuiltinPortalProps<Arguments = Record<string, any>, State = any
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
export type BuiltinPortal = <T = any>(props: BuiltinPortalProps<T>) => ReactNode;
|
|
52
|
+
|
|
53
|
+
export interface BuiltinPlaceholderProps {
|
|
54
|
+
apiName: string;
|
|
55
|
+
args?: Record<string, any>;
|
|
56
|
+
identifier: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type BuiltinPlaceholder = (props: BuiltinPlaceholderProps) => ReactNode;
|
|
@@ -33,18 +33,22 @@ interface BuiltinPluginTitleProps {
|
|
|
33
33
|
toolCallId: string;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const BuiltinPluginTitle = memo<BuiltinPluginTitleProps>(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
36
|
+
const BuiltinPluginTitle = memo<BuiltinPluginTitleProps>(
|
|
37
|
+
({ messageId, index, apiName, title, toolCallId }) => {
|
|
38
|
+
const { styles } = useStyles();
|
|
39
|
+
|
|
40
|
+
const isLoading = useChatStore(
|
|
41
|
+
chatSelectors.isToolApiNameShining(messageId, index, toolCallId),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Flexbox align={'center'} className={isLoading ? styles.shinyText : ''} gap={4} horizontal>
|
|
46
|
+
<div>{title}</div>
|
|
47
|
+
<Icon icon={ChevronRight} />
|
|
48
|
+
<span className={styles.apiName}>{apiName}</span>
|
|
49
|
+
</Flexbox>
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
);
|
|
49
53
|
|
|
50
54
|
export default BuiltinPluginTitle;
|
|
@@ -43,14 +43,7 @@ const ToolTitle = memo<ToolTitleProps>(({ identifier, messageId, index, apiName,
|
|
|
43
43
|
const { t } = useTranslation('plugin');
|
|
44
44
|
const { styles } = useStyles();
|
|
45
45
|
|
|
46
|
-
const isLoading = useChatStore((
|
|
47
|
-
const toolMessageId = chatSelectors.getMessageByToolCallId(toolCallId)(s)?.id;
|
|
48
|
-
const isToolCallStreaming = chatSelectors.isToolCallStreaming(messageId, index)(s);
|
|
49
|
-
const isPluginApiInvoking = !toolMessageId
|
|
50
|
-
? true
|
|
51
|
-
: chatSelectors.isPluginApiInvoking(toolMessageId)(s);
|
|
52
|
-
return isToolCallStreaming || isPluginApiInvoking;
|
|
53
|
-
});
|
|
46
|
+
const isLoading = useChatStore(chatSelectors.isToolApiNameShining(messageId, index, toolCallId));
|
|
54
47
|
|
|
55
48
|
const pluginMeta = useToolStore(toolSelectors.getMetaById(identifier), isEqual);
|
|
56
49
|
|
|
@@ -115,22 +115,18 @@ const Arguments = memo<ArgumentsProps>(({ arguments: args = '', shine, actions }
|
|
|
115
115
|
{actions}
|
|
116
116
|
</Flexbox>
|
|
117
117
|
)}
|
|
118
|
-
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
/>
|
|
131
|
-
);
|
|
132
|
-
})
|
|
133
|
-
)}
|
|
118
|
+
{Object.entries(displayArgs).map(([key, value]) => {
|
|
119
|
+
return (
|
|
120
|
+
<ObjectEntity
|
|
121
|
+
editable={false}
|
|
122
|
+
hasMinWidth={hasMinWidth}
|
|
123
|
+
key={key}
|
|
124
|
+
objectKey={key}
|
|
125
|
+
shine={shine}
|
|
126
|
+
value={value}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
})}
|
|
134
130
|
</div>
|
|
135
131
|
);
|
|
136
132
|
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { safeParseJSON } from '@lobechat/utils';
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
|
|
4
|
+
import { BuiltinToolPlaceholders } from '@/tools/placeholders';
|
|
5
|
+
|
|
6
|
+
import Arguments from '../Arguments';
|
|
7
|
+
|
|
8
|
+
interface LoadingPlaceholderProps {
|
|
9
|
+
apiName: string;
|
|
10
|
+
identifier: string;
|
|
11
|
+
loading?: boolean;
|
|
12
|
+
requestArgs?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const LoadingPlaceholder = memo<LoadingPlaceholderProps>(
|
|
16
|
+
({ identifier, requestArgs, apiName, loading }) => {
|
|
17
|
+
const Render = BuiltinToolPlaceholders[identifier || ''];
|
|
18
|
+
|
|
19
|
+
if (identifier) {
|
|
20
|
+
return (
|
|
21
|
+
<Render apiName={apiName} args={safeParseJSON(requestArgs) || {}} identifier={identifier} />
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return <Arguments arguments={requestArgs} shine={loading} />;
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export default LoadingPlaceholder;
|