@lobehub/chat 1.77.17 → 1.78.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 (56) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/changelog/v1.json +18 -0
  5. package/contributing/Basic/Architecture.md +1 -1
  6. package/contributing/Basic/Architecture.zh-CN.md +1 -1
  7. package/contributing/Basic/Chat-API.md +326 -108
  8. package/contributing/Basic/Chat-API.zh-CN.md +313 -133
  9. package/contributing/Basic/Contributing-Guidelines.md +7 -4
  10. package/contributing/Basic/Contributing-Guidelines.zh-CN.md +7 -6
  11. package/contributing/Home.md +5 -5
  12. package/contributing/State-Management/State-Management-Intro.md +1 -1
  13. package/contributing/State-Management/State-Management-Intro.zh-CN.md +1 -1
  14. package/docs/self-hosting/advanced/auth/next-auth/keycloak.mdx +119 -0
  15. package/docs/self-hosting/advanced/auth/next-auth/keycloak.zh-CN.mdx +116 -0
  16. package/docs/self-hosting/advanced/auth.mdx +3 -0
  17. package/docs/self-hosting/advanced/auth.zh-CN.mdx +3 -0
  18. package/locales/ar/tool.json +21 -1
  19. package/locales/bg-BG/tool.json +21 -1
  20. package/locales/de-DE/tool.json +21 -1
  21. package/locales/en-US/tool.json +21 -1
  22. package/locales/es-ES/tool.json +21 -1
  23. package/locales/fa-IR/tool.json +21 -1
  24. package/locales/fr-FR/tool.json +21 -1
  25. package/locales/it-IT/tool.json +21 -1
  26. package/locales/ja-JP/tool.json +21 -1
  27. package/locales/ko-KR/tool.json +21 -1
  28. package/locales/nl-NL/tool.json +21 -1
  29. package/locales/pl-PL/tool.json +21 -1
  30. package/locales/pt-BR/tool.json +21 -1
  31. package/locales/ru-RU/tool.json +21 -1
  32. package/locales/tr-TR/tool.json +21 -1
  33. package/locales/vi-VN/tool.json +21 -1
  34. package/locales/zh-CN/tool.json +30 -1
  35. package/locales/zh-TW/tool.json +21 -1
  36. package/package.json +1 -1
  37. package/src/libs/next-auth/sso-providers/index.ts +2 -0
  38. package/src/libs/next-auth/sso-providers/keycloak.ts +25 -0
  39. package/src/locales/default/tool.ts +30 -1
  40. package/src/server/modules/SearXNG.ts +10 -2
  41. package/src/server/routers/tools/__test__/search.test.ts +3 -1
  42. package/src/server/routers/tools/search.ts +10 -2
  43. package/src/services/search.ts +2 -2
  44. package/src/store/chat/slices/builtinTool/actions/searXNG.test.ts +28 -8
  45. package/src/store/chat/slices/builtinTool/actions/searXNG.ts +22 -5
  46. package/src/tools/web-browsing/Portal/Search/index.tsx +1 -1
  47. package/src/tools/web-browsing/Render/Search/SearchQuery/SearchView.tsx +1 -1
  48. package/src/tools/web-browsing/Render/Search/SearchQuery/index.tsx +1 -1
  49. package/src/tools/web-browsing/Render/Search/SearchResult/index.tsx +1 -1
  50. package/src/tools/web-browsing/components/CategoryAvatar.tsx +27 -0
  51. package/src/tools/web-browsing/components/SearchBar.tsx +84 -4
  52. package/src/tools/web-browsing/const.ts +26 -0
  53. package/src/tools/web-browsing/index.ts +58 -28
  54. package/src/tools/web-browsing/systemRole.ts +62 -1
  55. package/src/types/tool/search.ts +10 -1
  56. package/src/helpers/url.ts +0 -17
@@ -19,8 +19,28 @@
19
19
  "placeholder": "Palavras-chave",
20
20
  "tooltip": "Isso irá recuperar os resultados da pesquisa novamente e criar uma nova mensagem de resumo"
21
21
  },
22
- "searchEngine": "Motor de busca:",
22
+ "searchCategory": {
23
+ "placeholder": "Pesquisar categoria",
24
+ "title": "Categoria de pesquisa:",
25
+ "value": {
26
+ "files": "Arquivos",
27
+ "general": "Geral",
28
+ "images": "Imagens",
29
+ "it": "Tecnologia da Informação",
30
+ "map": "Mapa",
31
+ "music": "Música",
32
+ "news": "Notícias",
33
+ "science": "Ciência",
34
+ "social_media": "Mídias sociais",
35
+ "videos": "Vídeos"
36
+ }
37
+ },
38
+ "searchEngine": {
39
+ "placeholder": "Motor de busca",
40
+ "title": "Motor de busca:"
41
+ },
23
42
  "searchResult": "Número de pesquisas:",
43
+ "searchTimeRange": "Intervalo de tempo:",
24
44
  "summary": "Resumo",
25
45
  "summaryTooltip": "Resumir o conteúdo atual",
26
46
  "viewMoreResults": "Ver mais {{results}} resultados"
@@ -19,8 +19,28 @@
19
19
  "placeholder": "Ключевые слова",
20
20
  "tooltip": "Будет повторно получен результат поиска и создано новое резюме сообщения"
21
21
  },
22
- "searchEngine": "Поисковая система:",
22
+ "searchCategory": {
23
+ "placeholder": "Поиск категории",
24
+ "title": "Категория поиска:",
25
+ "value": {
26
+ "files": "Файлы",
27
+ "general": "Общее",
28
+ "images": "Изображения",
29
+ "it": "Информационные технологии",
30
+ "map": "Карта",
31
+ "music": "Музыка",
32
+ "news": "Новости",
33
+ "science": "Наука",
34
+ "social_media": "Социальные медиа",
35
+ "videos": "Видео"
36
+ }
37
+ },
38
+ "searchEngine": {
39
+ "placeholder": "Поисковая система",
40
+ "title": "Поисковая система:"
41
+ },
23
42
  "searchResult": "Количество результатов:",
43
+ "searchTimeRange": "Временной диапазон:",
24
44
  "summary": "Резюме",
25
45
  "summaryTooltip": "Суммировать текущее содержимое",
26
46
  "viewMoreResults": "Посмотреть еще {{results}} результатов"
@@ -19,8 +19,28 @@
19
19
  "placeholder": "Anahtar kelime",
20
20
  "tooltip": "Arama sonuçları yeniden alınacak ve yeni bir özet mesajı oluşturulacaktır"
21
21
  },
22
- "searchEngine": "Arama motoru:",
22
+ "searchCategory": {
23
+ "placeholder": "Kategori Ara",
24
+ "title": "Kategori Ara:",
25
+ "value": {
26
+ "files": "Dosyalar",
27
+ "general": "Genel",
28
+ "images": "Görüntüler",
29
+ "it": "Bilgi Teknolojisi",
30
+ "map": "Harita",
31
+ "music": "Müzik",
32
+ "news": "Haberler",
33
+ "science": "Bilim",
34
+ "social_media": "Sosyal Medya",
35
+ "videos": "Videolar"
36
+ }
37
+ },
38
+ "searchEngine": {
39
+ "placeholder": "Arama Motoru",
40
+ "title": "Arama Motoru:"
41
+ },
23
42
  "searchResult": "Arama sayısı:",
43
+ "searchTimeRange": "Zaman aralığı:",
24
44
  "summary": "Özet",
25
45
  "summaryTooltip": "Mevcut içeriği özetle",
26
46
  "viewMoreResults": "Daha fazla {{results}} sonuç görüntüle"
@@ -19,8 +19,28 @@
19
19
  "placeholder": "Từ khóa",
20
20
  "tooltip": "Sẽ lấy lại kết quả tìm kiếm và tạo một tin nhắn tóm tắt mới"
21
21
  },
22
- "searchEngine": "Công cụ tìm kiếm:",
22
+ "searchCategory": {
23
+ "placeholder": "Tìm kiếm danh mục",
24
+ "title": "Danh mục tìm kiếm:",
25
+ "value": {
26
+ "files": "Tài liệu",
27
+ "general": "Chung",
28
+ "images": "Hình ảnh",
29
+ "it": "Công nghệ thông tin",
30
+ "map": "Bản đồ",
31
+ "music": "Âm nhạc",
32
+ "news": "Tin tức",
33
+ "science": "Khoa học",
34
+ "social_media": "Mạng xã hội",
35
+ "videos": "Video"
36
+ }
37
+ },
38
+ "searchEngine": {
39
+ "placeholder": "Công cụ tìm kiếm",
40
+ "title": "Công cụ tìm kiếm:"
41
+ },
23
42
  "searchResult": "Số lượng tìm kiếm:",
43
+ "searchTimeRange": "Khoảng thời gian:",
24
44
  "summary": "Tóm tắt",
25
45
  "summaryTooltip": "Tóm tắt nội dung hiện tại",
26
46
  "viewMoreResults": "Xem thêm {{results}} kết quả"
@@ -19,8 +19,37 @@
19
19
  "placeholder": "关键词",
20
20
  "tooltip": "将会重新获取搜索结果,并创建一条新的总结消息"
21
21
  },
22
- "searchEngine": "搜索引擎:",
22
+ "searchCategory": {
23
+ "placeholder": "搜索类别",
24
+ "title": "搜索类别:",
25
+ "value": {
26
+ "files": "文件",
27
+ "general": "通用",
28
+ "images": "图片",
29
+ "it": "信息技术",
30
+ "map": "地图",
31
+ "music": "音乐",
32
+ "news": "新闻",
33
+ "science": "科学",
34
+ "social_media": "社交媒体",
35
+ "videos": "视频"
36
+ }
37
+ },
38
+ "searchEngine": {
39
+ "placeholder": "搜索引擎",
40
+ "title": "搜索引擎:"
41
+ },
23
42
  "searchResult": "搜索数量:",
43
+ "searchTimeRange": {
44
+ "title": "时间范围:",
45
+ "value": {
46
+ "anytime": "时间不限",
47
+ "day": "一天内",
48
+ "month": "一月内",
49
+ "week": "一周内",
50
+ "year": "一年内"
51
+ }
52
+ },
24
53
  "summary": "总结",
25
54
  "summaryTooltip": "总结当前内容",
26
55
  "viewMoreResults": "查看更多 {{results}} 个结果"
@@ -19,8 +19,28 @@
19
19
  "placeholder": "關鍵字",
20
20
  "tooltip": "將會重新獲取搜尋結果,並建立一條新的總結消息"
21
21
  },
22
- "searchEngine": "搜尋引擎:",
22
+ "searchCategory": {
23
+ "placeholder": "搜尋類別",
24
+ "title": "搜尋類別:",
25
+ "value": {
26
+ "files": "文件",
27
+ "general": "通用",
28
+ "images": "圖片",
29
+ "it": "資訊科技",
30
+ "map": "地圖",
31
+ "music": "音樂",
32
+ "news": "新聞",
33
+ "science": "科學",
34
+ "social_media": "社交媒體",
35
+ "videos": "影片"
36
+ }
37
+ },
38
+ "searchEngine": {
39
+ "placeholder": "搜尋引擎",
40
+ "title": "搜尋引擎:"
41
+ },
23
42
  "searchResult": "搜尋數量:",
43
+ "searchTimeRange": "時間範圍:",
24
44
  "summary": "總結",
25
45
  "summaryTooltip": "總結當前內容",
26
46
  "viewMoreResults": "查看更多 {{results}} 個結果"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.77.17",
3
+ "version": "1.78.0",
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",
@@ -6,6 +6,7 @@ import Casdoor from './casdoor';
6
6
  import CloudflareZeroTrust from './cloudflare-zero-trust';
7
7
  import GenericOIDC from './generic-oidc';
8
8
  import Github from './github';
9
+ import Keycloak from './keycloak';
9
10
  import Logto from './logto';
10
11
  import MicrosoftEntraID from './microsoft-entra-id';
11
12
  import WeChat from './wechat';
@@ -24,4 +25,5 @@ export const ssoProviders = [
24
25
  Casdoor,
25
26
  MicrosoftEntraID,
26
27
  WeChat,
28
+ Keycloak,
27
29
  ];
@@ -0,0 +1,25 @@
1
+ import Keycloak from 'next-auth/providers/keycloak';
2
+
3
+ import { CommonProviderConfig } from './sso.config';
4
+
5
+ const provider = {
6
+ id: 'keycloak',
7
+ provider: Keycloak({
8
+ ...CommonProviderConfig,
9
+ // Specify auth scope, at least include 'openid email'
10
+ authorization: { params: { scope: 'openid email profile' } },
11
+ clientId: process.env.AUTH_KEYCLOAK_ID,
12
+ clientSecret: process.env.AUTH_KEYCLOAK_SECRET,
13
+ issuer: process.env.AUTH_KEYCLOAK_ISSUER,
14
+ profile(profile) {
15
+ return {
16
+ email: profile.email,
17
+ id: profile.sub,
18
+ name: profile.name,
19
+ providerAccountId: profile.sub,
20
+ };
21
+ },
22
+ }),
23
+ };
24
+
25
+ export default provider;
@@ -19,8 +19,37 @@ export default {
19
19
  placeholder: '关键词',
20
20
  tooltip: '将会重新获取搜索结果,并创建一条新的总结消息',
21
21
  },
22
- searchEngine: '搜索引擎:',
22
+ searchCategory: {
23
+ placeholder: '搜索类别',
24
+ title: '搜索类别:',
25
+ value: {
26
+ 'files': '文件',
27
+ 'general': '通用',
28
+ 'images': '图片',
29
+ 'it': '信息技术',
30
+ 'map': '地图',
31
+ 'music': '音乐',
32
+ 'news': '新闻',
33
+ 'science': '科学',
34
+ 'social_media': '社交媒体',
35
+ 'videos': '视频',
36
+ },
37
+ },
38
+ searchEngine: {
39
+ placeholder: '搜索引擎',
40
+ title: '搜索引擎:',
41
+ },
23
42
  searchResult: '搜索数量:',
43
+ searchTimeRange: {
44
+ title: '时间范围:',
45
+ value: {
46
+ anytime: '时间不限',
47
+ day: '一天内',
48
+ month: '一月内',
49
+ week: '一周内',
50
+ year: '一年内',
51
+ },
52
+ },
24
53
  summary: '总结',
25
54
  summaryTooltip: '总结当前内容',
26
55
  viewMoreResults: '查看更多 {{results}} 个结果',
@@ -10,10 +10,18 @@ export class SearXNGClient {
10
10
  this.baseUrl = baseUrl;
11
11
  }
12
12
 
13
- async search(query: string, engines?: string[]): Promise<SearchResponse> {
13
+ async search(query: string, optionalParams: Record<string, any> = {}): Promise<SearchResponse> {
14
14
  try {
15
+ const { time_range, ...otherParams } = optionalParams;
16
+
17
+ const processedParams = Object.entries(otherParams).reduce<Record<string, any>>((acc, [key, value]) => {
18
+ acc[key] = Array.isArray(value) ? value.join(',') : value;
19
+ return acc;
20
+ }, {});
21
+
15
22
  const searchParams = qs.stringify({
16
- engines: engines?.join(','),
23
+ ...processedParams,
24
+ ...(time_range !== 'anytime' && { time_range }),
17
25
  format: 'json',
18
26
  q: query,
19
27
  });
@@ -98,8 +98,10 @@ describe('searchRouter', () => {
98
98
  const caller = searchRouter.createCaller(mockContext as any);
99
99
 
100
100
  const result = await caller.query({
101
+ optionalParams: {
102
+ searchEngines: ['google'],
103
+ },
101
104
  query: 'test query',
102
- searchEngine: ['google'],
103
105
  });
104
106
 
105
107
  expect(result).toEqual(mockSearchResult);
@@ -43,8 +43,12 @@ export const searchRouter = router({
43
43
  query: searchProcedure
44
44
  .input(
45
45
  z.object({
46
+ optionalParams: z.object({
47
+ searchCategories: z.array(z.string()).optional(),
48
+ searchEngines: z.array(z.string()).optional(),
49
+ searchTimeRange: z.string().optional(),
50
+ }).optional(),
46
51
  query: z.string(),
47
- searchEngine: z.array(z.string()).optional(),
48
52
  }),
49
53
  )
50
54
  .query(async ({ input }) => {
@@ -55,7 +59,11 @@ export const searchRouter = router({
55
59
  const client = new SearXNGClient(toolsEnv.SEARXNG_URL);
56
60
 
57
61
  try {
58
- return await client.search(input.query, input.searchEngine);
62
+ return await client.search(input.query, {
63
+ categories: input.optionalParams?.searchCategories,
64
+ engines: input.optionalParams?.searchEngines,
65
+ time_range: input.optionalParams?.searchTimeRange,
66
+ });
59
67
  } catch (e) {
60
68
  console.error(e);
61
69
 
@@ -1,8 +1,8 @@
1
1
  import { toolsClient } from '@/libs/trpc/client';
2
2
 
3
3
  class SearchService {
4
- search(query: string, searchEngine?: string[]) {
5
- return toolsClient.search.query.query({ query, searchEngine });
4
+ search(query: string, optionalParams?: object) {
5
+ return toolsClient.search.query.query({ optionalParams, query});
6
6
  }
7
7
 
8
8
  crawlPage(url: string) {
@@ -72,8 +72,10 @@ describe('searXNG actions', () => {
72
72
 
73
73
  const messageId = 'test-message-id';
74
74
  const query: SearchQuery = {
75
+ optionalParams: {
76
+ searchEngines: ['google'],
77
+ },
75
78
  query: 'test query',
76
- searchEngines: ['google'],
77
79
  };
78
80
 
79
81
  await act(async () => {
@@ -82,13 +84,15 @@ describe('searXNG actions', () => {
82
84
 
83
85
  const expectedContent: SearchContent[] = [
84
86
  {
85
- content: 'Test Content',
86
87
  title: 'Test Result',
87
88
  url: 'https://test.com',
89
+ content: 'Test Content',
88
90
  },
89
91
  ];
90
92
 
91
- expect(searchService.search).toHaveBeenCalledWith('test query', ['google']);
93
+ expect( searchService.search ).toHaveBeenCalledWith('test query', {
94
+ searchEngines: [ 'google' ]
95
+ });
92
96
  expect(result.current.searchLoading[messageId]).toBe(false);
93
97
  expect(result.current.internal_updateMessageContent).toHaveBeenCalledWith(
94
98
  messageId,
@@ -133,6 +137,7 @@ describe('searXNG actions', () => {
133
137
  };
134
138
 
135
139
  (searchService.search as Mock)
140
+ .mockResolvedValueOnce(emptyResponse)
136
141
  .mockResolvedValueOnce(emptyResponse)
137
142
  .mockResolvedValueOnce(retryResponse);
138
143
 
@@ -141,20 +146,35 @@ describe('searXNG actions', () => {
141
146
 
142
147
  const messageId = 'test-message-id';
143
148
  const query: SearchQuery = {
149
+ optionalParams: {
150
+ searchEngines: ['custom-engine'],
151
+ searchTimeRange: 'year',
152
+ },
144
153
  query: 'test query',
145
- searchEngines: ['custom-engine'],
146
154
  };
147
155
 
148
156
  await act(async () => {
149
157
  await searchWithSearXNG(messageId, query);
150
158
  });
151
159
 
152
- expect(searchService.search).toHaveBeenCalledTimes(2);
153
- expect(searchService.search).toHaveBeenNthCalledWith(1, 'test query', ['custom-engine']);
154
- expect(searchService.search).toHaveBeenNthCalledWith(2, 'test query');
160
+ expect(searchService.search).toHaveBeenCalledTimes(3);
161
+ expect(searchService.search).toHaveBeenNthCalledWith(1, "test query", {
162
+ "searchEngines": [ "custom-engine" ],
163
+ "searchTimeRange": "year",
164
+ });
165
+ expect(searchService.search).toHaveBeenNthCalledWith(2, "test query", {
166
+ "searchTimeRange": "year",
167
+ });
168
+ expect(result.current.updatePluginArguments).toHaveBeenCalledWith(messageId, {
169
+ optionalParams: {
170
+ "searchTimeRange": "year",
171
+ },
172
+ query: 'test query',
173
+ });
174
+ expect(searchService.search).toHaveBeenNthCalledWith(3, "test query");
155
175
  expect(result.current.updatePluginArguments).toHaveBeenCalledWith(messageId, {
176
+ optionalParams: undefined,
156
177
  query: 'test query',
157
- searchEngines: undefined,
158
178
  });
159
179
  });
160
180
 
@@ -142,12 +142,26 @@ export const searchSlice: StateCreator<
142
142
  get().toggleSearchLoading(id, true);
143
143
  let data: SearchResponse | undefined;
144
144
  try {
145
- data = await searchService.search(params.query, params.searchEngines);
145
+ // 首次查询
146
+ data = await searchService.search(params.query, params.optionalParams);
147
+
148
+ // 如果没有搜索到结果,则执行第一次重试(移除搜索引擎限制)
149
+ if (data?.results.length === 0 && params.optionalParams?.searchEngines && params.optionalParams?.searchEngines?.length > 0) {
150
+ const paramsExcludeSearchEngines = {
151
+ ...params,
152
+ optionalParams: {
153
+ ...params.optionalParams,
154
+ searchEngines: undefined
155
+ }
156
+ };
157
+ data = await searchService.search(params.query, paramsExcludeSearchEngines.optionalParams);
158
+ get().updatePluginArguments(id, paramsExcludeSearchEngines);
159
+ }
146
160
 
147
- // 如果没有搜索到结果,那么尝试使用默认的搜索引擎再搜一次
148
- if (data?.results.length === 0 && params.searchEngines && params.searchEngines?.length > 0) {
161
+ // 如果仍然没有搜索到结果,则执行第二次重试(移除所有限制)
162
+ if (data?.results.length === 0) {
149
163
  data = await searchService.search(params.query);
150
- get().updatePluginArguments(id, { ...params, searchEngines: undefined });
164
+ get().updatePluginArguments(id, { ...params, optionalParams: undefined });
151
165
  }
152
166
 
153
167
  await get().updatePluginState(id, data);
@@ -175,9 +189,12 @@ export const searchSlice: StateCreator<
175
189
 
176
190
  // add 15 search results to message content
177
191
  const searchContent: SearchContent[] = data.results.slice(0, 15).map((item) => ({
178
- content: item.content,
179
192
  title: item.title,
180
193
  url: item.url,
194
+ ...(item.content && { content: item.content }),
195
+ ...(item.publishedDate && { publishedDate: item.publishedDate }),
196
+ ...(item.img_src && { img_src: item.img_src }),
197
+ ...(item.thumbnail && { thumbnail: item.thumbnail }),
181
198
  }));
182
199
 
183
200
  await get().internal_updateMessageContent(id, JSON.stringify(searchContent));
@@ -19,7 +19,7 @@ interface InspectorUIProps {
19
19
 
20
20
  const Inspector = memo<InspectorUIProps>(({ query: args, messageId, response }) => {
21
21
  const engines = uniq((response.results || []).map((result) => result.engine));
22
- const defaultEngines = engines.length > 0 ? engines : args.searchEngines || [];
22
+ const defaultEngines = engines.length > 0 ? engines : args.optionalParams?.searchEngines || [];
23
23
  const loading = useChatStore(chatToolSelectors.isSearXNGSearching(messageId));
24
24
 
25
25
  if (loading) {
@@ -66,7 +66,7 @@ const SearchBar = memo<SearchBarProps>(
66
66
  </Flexbox>
67
67
 
68
68
  <Flexbox align={'center'} horizontal>
69
- <div className={styles.font}>{t('search.searchEngine')}</div>
69
+ <div className={styles.font}>{t('search.searchEngine.title')}</div>
70
70
  {searching ? (
71
71
  <Skeleton.Button active size={'small'} />
72
72
  ) : (
@@ -29,7 +29,7 @@ const SearchQueryView = memo<SearchQueryViewProps>(
29
29
  const { t } = useTranslation('common');
30
30
 
31
31
  const engines = uniq(searchResults.map((result) => result.engine));
32
- const defaultEngines = engines.length > 0 ? engines : args.searchEngines || [];
32
+ const defaultEngines = engines.length > 0 ? engines : args.optionalParams?.searchEngines || [];
33
33
 
34
34
  return !pluginState ? (
35
35
  <Flexbox align={'center'} distribution={'space-between'} height={32} horizontal>
@@ -32,7 +32,7 @@ const SearchResult = memo<SearchResultProps>(
32
32
  const { t } = useTranslation(['tool', 'common']);
33
33
 
34
34
  const engines = uniq(searchResults.map((result) => result.engine));
35
- const defaultEngines = engines.length > 0 ? engines : args.searchEngines || [];
35
+ const defaultEngines = engines.length > 0 ? engines : args.optionalParams?.searchEngines || [];
36
36
  const isMobile = useIsMobile();
37
37
 
38
38
  if (loading || !pluginState)
@@ -0,0 +1,27 @@
1
+ import { Avatar } from 'antd';
2
+ import { useTheme } from 'antd-style';
3
+ import { memo } from 'react';
4
+
5
+ import { CATEGORY_ICON_MAP } from '../const';
6
+
7
+ interface CategoryAvatarProps {
8
+ category: string;
9
+ }
10
+
11
+ export const CategoryAvatar = memo<CategoryAvatarProps>(({ category }) => {
12
+ const theme = useTheme();
13
+ const IconComponent = CATEGORY_ICON_MAP[category];
14
+
15
+ return (
16
+ <Avatar
17
+ alt={category}
18
+ icon={<IconComponent />}
19
+ style={{
20
+ backgroundColor: 'transparent',
21
+ color: theme.colorTextSecondary,
22
+ height: 16,
23
+ width: 16
24
+ }}
25
+ />
26
+ );
27
+ });