From 74a09e27fff9130897e2f6d885711339f1b37023 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Tue, 25 Feb 2025 14:14:14 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20fix=200=20search=20result?= =?UTF-8?q?s=20with=20specific=20search=20engine=20(#6487)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update * improve plugin loading state * improve loading style * update favicon * improve search display * add search results * fix lint --- src/components/WebFavicon/index.tsx | 26 ++++ .../Assistant/Tool/Render/CustomRender.tsx | 1 + .../Messages/Assistant/Tool/Render/index.tsx | 6 + .../slices/aiChat/actions/generateAIChat.ts | 2 +- .../{action.test.ts => actions/dalle.test.ts} | 4 +- .../chat/slices/builtinTool/actions/dalle.ts | 126 +++++++++++++++++ .../chat/slices/builtinTool/actions/index.ts | 18 +++ .../{action.ts => actions/searXNG.ts} | 131 ++---------------- src/store/chat/slices/message/action.ts | 2 +- src/store/chat/store.ts | 2 +- .../ResultList/SearchItem/TitleExtra.tsx | 6 +- .../Portal/ResultList/SearchItem/Video.tsx | 7 +- .../Portal/ResultList/SearchItem/index.tsx | 11 +- .../web-browsing/Render/SearchQuery/index.tsx | 4 +- .../Render/SearchResult/SearchResultItem.tsx | 11 +- src/tools/web-browsing/Render/index.tsx | 2 +- src/tools/web-browsing/const.ts | 19 +-- 17 files changed, 231 insertions(+), 147 deletions(-) create mode 100644 src/components/WebFavicon/index.tsx rename src/store/chat/slices/builtinTool/{action.test.ts => actions/dalle.test.ts} (97%) create mode 100644 src/store/chat/slices/builtinTool/actions/dalle.ts create mode 100644 src/store/chat/slices/builtinTool/actions/index.ts rename src/store/chat/slices/builtinTool/{action.ts => actions/searXNG.ts} (50%) diff --git a/src/components/WebFavicon/index.tsx b/src/components/WebFavicon/index.tsx new file mode 100644 index 0000000000000..e49a0be876741 --- /dev/null +++ b/src/components/WebFavicon/index.tsx @@ -0,0 +1,26 @@ +import Image from 'next/image'; + +interface WebFaviconProps { + alt?: string; + size?: number; + title?: string; + url: string; +} + +const WebFavicon = ({ url, title, alt, size = 14 }: WebFaviconProps) => { + const urlObj = new URL(url); + const host = urlObj.hostname; + + return ( + {alt + ); +}; + +export default WebFavicon; diff --git a/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx b/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx index bb26212a92afc..7ead2c7ca38ee 100644 --- a/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +++ b/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx @@ -38,6 +38,7 @@ const CustomRender = memo< const { t } = useTranslation('plugin'); const theme = useTheme(); + useEffect(() => { if (!plugin?.type || loading) return; diff --git a/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx b/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx index 69e6dd2ce5ee7..5ac637cc3864e 100644 --- a/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +++ b/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx @@ -1,5 +1,6 @@ import { Suspense, memo } from 'react'; +import { LOADING_FLAT } from '@/const/message'; import ErrorResponse from '@/features/Conversation/Messages/Assistant/Tool/Render/ErrorResponse'; import { useChatStore } from '@/store/chat'; import { chatSelectors } from '@/store/chat/selectors'; @@ -28,6 +29,11 @@ const Render = memo( return ; } + // 如果是 LOADING_FLAT 则说明还在加载中 + // 而 standalone 模式的插件 content 应该始终是 LOADING_FLAT + if (toolMessage.content === LOADING_FLAT && toolMessage.plugin?.type !== 'standalone') + return ; + return ( }> { +describe('chatToolSlice - dalle', () => { describe('generateImageFromPrompts', () => { it('should generate images from prompts, update items, and upload images', async () => { const { result } = renderHook(() => useChatStore()); diff --git a/src/store/chat/slices/builtinTool/actions/dalle.ts b/src/store/chat/slices/builtinTool/actions/dalle.ts new file mode 100644 index 0000000000000..9d01aa8261b79 --- /dev/null +++ b/src/store/chat/slices/builtinTool/actions/dalle.ts @@ -0,0 +1,126 @@ +import { produce } from 'immer'; +import pMap from 'p-map'; +import { SWRResponse } from 'swr'; +import { StateCreator } from 'zustand/vanilla'; + +import { useClientDataSWR } from '@/libs/swr'; +import { fileService } from '@/services/file'; +import { imageGenerationService } from '@/services/textToImage'; +import { uploadService } from '@/services/upload'; +import { chatSelectors } from '@/store/chat/selectors'; +import { ChatStore } from '@/store/chat/store'; +import { useFileStore } from '@/store/file'; +import { DallEImageItem } from '@/types/tool/dalle'; + + +import { setNamespace } from '@/utils/storeDebug'; + +const n = setNamespace('tool'); + +const SWR_FETCH_KEY = 'FetchImageItem'; + +export interface ChatDallEAction { + generateImageFromPrompts: (items: DallEImageItem[], id: string) => Promise; + text2image: (id: string, data: DallEImageItem[]) => Promise; + toggleDallEImageLoading: (key: string, value: boolean) => void; + updateImageItem: (id: string, updater: (data: DallEImageItem[]) => void) => Promise; + useFetchDalleImageItem: (id: string) => SWRResponse; +} + +export const dalleSlice: StateCreator< + ChatStore, + [['zustand/devtools', never]], + [], + ChatDallEAction +> = (set, get) => ({ + generateImageFromPrompts: async (items, messageId) => { + const { toggleDallEImageLoading, updateImageItem } = get(); + // eslint-disable-next-line unicorn/consistent-function-scoping + const getMessageById = (id: string) => chatSelectors.getMessageById(id)(get()); + + const message = getMessageById(messageId); + if (!message) return; + + const parent = getMessageById(message!.parentId!); + const originPrompt = parent?.content; + let errorArray: any[] = []; + + await pMap(items, async (params, index) => { + toggleDallEImageLoading(messageId + params.prompt, true); + + let url = ''; + try { + url = await imageGenerationService.generateImage(params); + } catch (e) { + toggleDallEImageLoading(messageId + params.prompt, false); + errorArray[index] = e; + + await get().updatePluginState(messageId, { error: errorArray }); + } + + if (!url) return; + + await updateImageItem(messageId, (draft) => { + draft[index].previewUrl = url; + }); + + toggleDallEImageLoading(messageId + params.prompt, false); + const imageFile = await uploadService.getImageFileByUrlWithCORS( + url, + `${originPrompt || params.prompt}_${index}.png`, + ); + + const data = await useFileStore.getState().uploadWithProgress({ + file: imageFile, + }); + + if (!data) return; + + await updateImageItem(messageId, (draft) => { + draft[index].imageId = data.id; + draft[index].previewUrl = undefined; + }); + }); + }, + text2image: async (id, data) => { + // const isAutoGen = settingsSelectors.isDalleAutoGenerating(useGlobalStore.getState()); + // if (!isAutoGen) return; + + await get().generateImageFromPrompts(data, id); + }, + + toggleDallEImageLoading: (key, value) => { + set( + { dalleImageLoading: { ...get().dalleImageLoading, [key]: value } }, + false, + n('toggleDallEImageLoading'), + ); + }, + + updateImageItem: async (id, updater) => { + const message = chatSelectors.getMessageById(id)(get()); + if (!message) return; + + const data: DallEImageItem[] = JSON.parse(message.content); + + const nextContent = produce(data, updater); + await get().internal_updateMessageContent(id, JSON.stringify(nextContent)); + }, + + useFetchDalleImageItem: (id) => + useClientDataSWR([SWR_FETCH_KEY, id], async () => { + const item = await fileService.getFile(id); + + set( + produce((draft) => { + if (draft.dalleImageMap[id]) return; + + draft.dalleImageMap[id] = item; + }), + false, + n('useFetchFile'), + ); + + return item; + }), +}); diff --git a/src/store/chat/slices/builtinTool/actions/index.ts b/src/store/chat/slices/builtinTool/actions/index.ts new file mode 100644 index 0000000000000..d53eb18417456 --- /dev/null +++ b/src/store/chat/slices/builtinTool/actions/index.ts @@ -0,0 +1,18 @@ +import { StateCreator } from 'zustand/vanilla'; + +import { ChatStore } from '@/store/chat/store'; + +import { ChatDallEAction, dalleSlice } from './dalle'; +import { SearchAction, searchSlice } from './searXNG'; + +export interface ChatBuiltinToolAction extends ChatDallEAction, SearchAction {} + +export const chatToolSlice: StateCreator< + ChatStore, + [['zustand/devtools', never]], + [], + ChatBuiltinToolAction +> = (...params) => ({ + ...dalleSlice(...params), + ...searchSlice(...params), +}); diff --git a/src/store/chat/slices/builtinTool/action.ts b/src/store/chat/slices/builtinTool/actions/searXNG.ts similarity index 50% rename from src/store/chat/slices/builtinTool/action.ts rename to src/store/chat/slices/builtinTool/actions/searXNG.ts index f97a055c12505..21f9f8e1f2017 100644 --- a/src/store/chat/slices/builtinTool/action.ts +++ b/src/store/chat/slices/builtinTool/actions/searXNG.ts @@ -1,35 +1,18 @@ -import { produce } from 'immer'; -import pMap from 'p-map'; -import { SWRResponse } from 'swr'; import { StateCreator } from 'zustand/vanilla'; -import { useClientDataSWR } from '@/libs/swr'; -import { fileService } from '@/services/file'; import { searchService } from '@/services/search'; -import { imageGenerationService } from '@/services/textToImage'; -import { uploadService } from '@/services/upload'; import { chatSelectors } from '@/store/chat/selectors'; import { ChatStore } from '@/store/chat/store'; -import { useFileStore } from '@/store/file'; import { CreateMessageParams } from '@/types/message'; -import { DallEImageItem } from '@/types/tool/dalle'; import { SEARCH_SEARXNG_NOT_CONFIG, SearchContent, SearchQuery, SearchResponse, } from '@/types/tool/search'; -import { setNamespace } from '@/utils/storeDebug'; import { nanoid } from '@/utils/uuid'; -const n = setNamespace('tool'); - -const SWR_FETCH_KEY = 'FetchImageItem'; -/** - * builtin tool action - */ -export interface ChatBuiltinToolAction { - generateImageFromPrompts: (items: DallEImageItem[], id: string) => Promise; +export interface SearchAction { /** * 重新发起搜索 * @description 会更新插件的 arguments 参数,然后再次搜索 @@ -45,69 +28,15 @@ export interface ChatBuiltinToolAction { data: SearchQuery, aiSummary?: boolean, ) => Promise; - text2image: (id: string, data: DallEImageItem[]) => Promise; - - toggleDallEImageLoading: (key: string, value: boolean) => void; toggleSearchLoading: (id: string, loading: boolean) => void; - updateImageItem: (id: string, updater: (data: DallEImageItem[]) => void) => Promise; - useFetchDalleImageItem: (id: string) => SWRResponse; } -export const chatToolSlice: StateCreator< +export const searchSlice: StateCreator< ChatStore, [['zustand/devtools', never]], [], - ChatBuiltinToolAction + SearchAction > = (set, get) => ({ - generateImageFromPrompts: async (items, messageId) => { - const { toggleDallEImageLoading, updateImageItem } = get(); - // eslint-disable-next-line unicorn/consistent-function-scoping - const getMessageById = (id: string) => chatSelectors.getMessageById(id)(get()); - - const message = getMessageById(messageId); - if (!message) return; - - const parent = getMessageById(message!.parentId!); - const originPrompt = parent?.content; - let errorArray: any[] = []; - - await pMap(items, async (params, index) => { - toggleDallEImageLoading(messageId + params.prompt, true); - - let url = ''; - try { - url = await imageGenerationService.generateImage(params); - } catch (e) { - toggleDallEImageLoading(messageId + params.prompt, false); - errorArray[index] = e; - - await get().updatePluginState(messageId, { error: errorArray }); - } - - if (!url) return; - - await updateImageItem(messageId, (draft) => { - draft[index].previewUrl = url; - }); - - toggleDallEImageLoading(messageId + params.prompt, false); - const imageFile = await uploadService.getImageFileByUrlWithCORS( - url, - `${originPrompt || params.prompt}_${index}.png`, - ); - - const data = await useFileStore.getState().uploadWithProgress({ - file: imageFile, - }); - - if (!data) return; - - await updateImageItem(messageId, (draft) => { - draft[index].imageId = data.id; - draft[index].previewUrl = undefined; - }); - }); - }, reSearchWithSearXNG: async (id, data, options) => { get().toggleSearchLoading(id, true); await get().updatePluginArguments(id, data); @@ -158,6 +87,13 @@ export const chatToolSlice: StateCreator< let data: SearchResponse | undefined; try { data = await searchService.search(params.query, params.searchEngines); + + // 如果没有搜索到结果,那么尝试使用默认的搜索引擎再搜一次 + if (data?.results.length === 0 && params.searchEngines && params.searchEngines?.length > 0) { + data = await searchService.search(params.query); + get().updatePluginArguments(id, { ...params, searchEngines: undefined }); + } + await get().updatePluginState(id, data); } catch (e) { if ((e as Error).message === SEARCH_SEARXNG_NOT_CONFIG) { @@ -181,8 +117,8 @@ export const chatToolSlice: StateCreator< if (!data) return; - // 只取前 5 个结果作为上下文 - const searchContent: SearchContent[] = data.results.slice(0, 5).map((item) => ({ + // add 15 search results to message content + const searchContent: SearchContent[] = data.results.slice(0, 15).map((item) => ({ content: item.content, title: item.title, url: item.url, @@ -196,49 +132,12 @@ export const chatToolSlice: StateCreator< // 如果 aiSummary 为 true,则会自动触发总结 return aiSummary; }, - text2image: async (id, data) => { - // const isAutoGen = settingsSelectors.isDalleAutoGenerating(useGlobalStore.getState()); - // if (!isAutoGen) return; - - await get().generateImageFromPrompts(data, id); - }, - toggleDallEImageLoading: (key, value) => { + toggleSearchLoading: (id, loading) => { set( - { dalleImageLoading: { ...get().dalleImageLoading, [key]: value } }, + { searchLoading: { ...get().searchLoading, [id]: loading } }, false, - n('toggleDallEImageLoading'), + `toggleSearchLoading/${loading ? 'start' : 'end'}`, ); }, - - toggleSearchLoading: (id, loading) => { - set({ searchLoading: { ...get().searchLoading, [id]: loading } }, false, 'toggleSearchLoading'); - }, - - updateImageItem: async (id, updater) => { - const message = chatSelectors.getMessageById(id)(get()); - if (!message) return; - - const data: DallEImageItem[] = JSON.parse(message.content); - - const nextContent = produce(data, updater); - await get().internal_updateMessageContent(id, JSON.stringify(nextContent)); - }, - - useFetchDalleImageItem: (id) => - useClientDataSWR([SWR_FETCH_KEY, id], async () => { - const item = await fileService.getFile(id); - - set( - produce((draft) => { - if (draft.dalleImageMap[id]) return; - - draft.dalleImageMap[id] = item; - }), - false, - n('useFetchFile'), - ); - - return item; - }), }); diff --git a/src/store/chat/slices/message/action.ts b/src/store/chat/slices/message/action.ts index 7963d82e4fa70..8ada6c4b6b915 100644 --- a/src/store/chat/slices/message/action.ts +++ b/src/store/chat/slices/message/action.ts @@ -380,7 +380,7 @@ export const chatMessage: StateCreator< messageLoadingIds: toggleBooleanList(get().messageLoadingIds, id, loading), }, false, - 'internal_toggleMessageLoading', + `internal_toggleMessageLoading/${loading ? 'start' : 'end'}`, ); }, internal_toggleLoadingArrays: (key, loading, id, action) => { diff --git a/src/store/chat/store.ts b/src/store/chat/store.ts index 6f4602a62b375..ddb8f4deecf77 100644 --- a/src/store/chat/store.ts +++ b/src/store/chat/store.ts @@ -6,7 +6,7 @@ import { StateCreator } from 'zustand/vanilla'; import { createDevtools } from '../middleware/createDevtools'; import { ChatStoreState, initialState } from './initialState'; -import { ChatBuiltinToolAction, chatToolSlice } from './slices/builtinTool/action'; +import { ChatBuiltinToolAction, chatToolSlice } from './slices/builtinTool/actions'; import { ChatPortalAction, chatPortalSlice } from './slices/portal/action'; import { ChatTranslateAction, chatTranslate } from './slices/translate/action'; import { ChatMessageAction, chatMessage } from './slices/message/action'; diff --git a/src/tools/web-browsing/Portal/ResultList/SearchItem/TitleExtra.tsx b/src/tools/web-browsing/Portal/ResultList/SearchItem/TitleExtra.tsx index e72a6c228a4bc..2d9a0cdb4ef31 100644 --- a/src/tools/web-browsing/Portal/ResultList/SearchItem/TitleExtra.tsx +++ b/src/tools/web-browsing/Portal/ResultList/SearchItem/TitleExtra.tsx @@ -4,19 +4,23 @@ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import { EngineAvatarGroup } from '@/tools/web-browsing/components/EngineAvatar'; + import CategoryAvatar from './CategoryAvatar'; interface TitleExtraProps { category: string; + engines: string[]; highlight?: boolean; score: number; } -const TitleExtra = memo(({ category, score, highlight }) => { +const TitleExtra = memo(({ category, score, highlight, engines }) => { const { t } = useTranslation('tool'); return ( + {highlight ? ( diff --git a/src/tools/web-browsing/Portal/ResultList/SearchItem/Video.tsx b/src/tools/web-browsing/Portal/ResultList/SearchItem/Video.tsx index 84d2b1ff681b4..67e478f7232b7 100644 --- a/src/tools/web-browsing/Portal/ResultList/SearchItem/Video.tsx +++ b/src/tools/web-browsing/Portal/ResultList/SearchItem/Video.tsx @@ -113,7 +113,12 @@ const VideoItem = memo( {title} - + {url} diff --git a/src/tools/web-browsing/Portal/ResultList/SearchItem/index.tsx b/src/tools/web-browsing/Portal/ResultList/SearchItem/index.tsx index e4a5ef41c1e95..eac4de3b22fa1 100644 --- a/src/tools/web-browsing/Portal/ResultList/SearchItem/index.tsx +++ b/src/tools/web-browsing/Portal/ResultList/SearchItem/index.tsx @@ -3,9 +3,9 @@ import { createStyles } from 'antd-style'; import { memo } from 'react'; import { Flexbox } from 'react-layout-kit'; +import WebFavicon from '@/components/WebFavicon'; import { SearchResult } from '@/types/tool/search'; -import { EngineAvatarGroup } from '../../../components/EngineAvatar'; import TitleExtra from './TitleExtra'; import Video from './Video'; @@ -73,10 +73,15 @@ const SearchItem = memo((props) => { - + {title} - + {url} diff --git a/src/tools/web-browsing/Render/SearchQuery/index.tsx b/src/tools/web-browsing/Render/SearchQuery/index.tsx index 199241e3ca04f..20647705f4b97 100644 --- a/src/tools/web-browsing/Render/SearchQuery/index.tsx +++ b/src/tools/web-browsing/Render/SearchQuery/index.tsx @@ -33,8 +33,8 @@ const SearchQueryView = memo( return !pluginState ? ( - - + + ) : editing ? ( ({ @@ -52,14 +52,7 @@ const SearchResultItem = memo(({ url, title }) => {
{title}
- {title + {host.replace('www.', '')} diff --git a/src/tools/web-browsing/Render/index.tsx b/src/tools/web-browsing/Render/index.tsx index 7bbb2b54f9e4d..2878d2a2a8302 100644 --- a/src/tools/web-browsing/Render/index.tsx +++ b/src/tools/web-browsing/Render/index.tsx @@ -34,7 +34,7 @@ const WebBrowsing = memo + = { - arxiv: 'https://arxiv.org/static/browse/0.3.4/images/icons/favicon-32x32.png', - bilibili: 'https://www.bilibili.com/favicon.ico', - bing: 'https://www.bing.com/favicon.ico', - brave: 'https://brave.com/static-assets/images/brave-favicon.png', - duckduckgo: 'https://www.duckduckgo.com/favicon.ico', - google: 'https://www.google.com/favicon.ico', - npm: 'https://static-production.npmjs.com/da3ab40fb0861d15c83854c29f5f2962.png', - qwant: 'https://www.qwant.com/favicon.ico', - youtube: 'https://www.youtube.com/favicon.ico', + 'arxiv': 'https://icons.duckduckgo.com/ip3/arxiv.org.ico', + 'bilibili': 'https://icons.duckduckgo.com/ip3/bilibili.com.ico', + 'bing': 'https://icons.duckduckgo.com/ip3/www.bing.com.ico', + 'brave': 'https://icons.duckduckgo.com/ip3/brave.com.ico', + 'duckduckgo': 'https://icons.duckduckgo.com/ip3/www.duckduckgo.com.ico', + 'google': 'https://icons.duckduckgo.com/ip3/google.com.ico', + 'google scholar': 'https://icons.duckduckgo.com/ip3/scholar.google.com.ico', + 'npm': 'https://icons.duckduckgo.com/ip3/npmjs.com.ico', + 'qwant': 'https://icons.duckduckgo.com/ip3/www.qwant.com.ico', + 'youtube': 'https://icons.duckduckgo.com/ip3/youtube.com.ico', };