Added Web search DuckDuckGo
This commit is contained in:
		
							parent
							
								
									317011a6d2
								
							
						
					
					
						commit
						9ced6469ce
					
				| @ -28,6 +28,7 @@ | |||||||
|     "@vitejs/plugin-react": "^4.2.1", |     "@vitejs/plugin-react": "^4.2.1", | ||||||
|     "antd": "^5.13.3", |     "antd": "^5.13.3", | ||||||
|     "axios": "^1.6.7", |     "axios": "^1.6.7", | ||||||
|  |     "cheerio": "^1.0.0-rc.12", | ||||||
|     "dayjs": "^1.11.10", |     "dayjs": "^1.11.10", | ||||||
|     "html-to-text": "^9.0.5", |     "html-to-text": "^9.0.5", | ||||||
|     "i18next": "^23.10.1", |     "i18next": "^23.10.1", | ||||||
|  | |||||||
| @ -1,8 +1,8 @@ | |||||||
| { | { | ||||||
|     "generalSettings": { |     "generalSettings": { | ||||||
|         "title": "General Settings", |         "title": "General Settings", | ||||||
|         "heading": "Web UI Settings", |  | ||||||
|         "settings": { |         "settings": { | ||||||
|  |             "heading": "Web UI Settings", | ||||||
|             "speechRecognitionLang": { |             "speechRecognitionLang": { | ||||||
|                 "label": "Speech Recognition Language", |                 "label": "Speech Recognition Language", | ||||||
|                 "placeholder": "Select a language" |                 "placeholder": "Select a language" | ||||||
| @ -18,14 +18,25 @@ | |||||||
|                     "dark": "Dark" |                     "dark": "Dark" | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             "searchMode": { |  | ||||||
|                 "label": "Perform Simple Internet Search" |  | ||||||
|             }, |  | ||||||
|             "deleteChatHistory": { |             "deleteChatHistory": { | ||||||
|                 "label": "Delete Chat History", |                 "label": "Delete Chat History", | ||||||
|                 "button": "Delete", |                 "button": "Delete", | ||||||
|                 "confirm": "Are you sure you want to delete your chat history? This action cannot be undone." |                 "confirm": "Are you sure you want to delete your chat history? This action cannot be undone." | ||||||
|             } |             } | ||||||
|  |         }, | ||||||
|  |         "webSearch": { | ||||||
|  |             "heading": "Manage Web Search", | ||||||
|  |             "searchMode": { | ||||||
|  |                 "label": "Perform Simple Internet Search" | ||||||
|  |             }, | ||||||
|  |             "provider": { | ||||||
|  |                 "label": "Search Engine", | ||||||
|  |                 "placeholder": "Select a search engine" | ||||||
|  |             }, | ||||||
|  |             "totalSearchResults": { | ||||||
|  |                 "label": "Total Search Results", | ||||||
|  |                 "placeholder": "Enter Total Search Results" | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|     "manageModels": { |     "manageModels": { | ||||||
|  | |||||||
| @ -1,8 +1,8 @@ | |||||||
| { | { | ||||||
|     "generalSettings": { |     "generalSettings": { | ||||||
|         "title": "一般設定", |         "title": "一般設定", | ||||||
|         "heading": "Web UIの設定", |  | ||||||
|         "settings": { |         "settings": { | ||||||
|  |             "heading": "Web UIの設定", | ||||||
|             "speechRecognitionLang": { |             "speechRecognitionLang": { | ||||||
|                 "label": "音声認識の言語", |                 "label": "音声認識の言語", | ||||||
|                 "placeholder": "言語を選択" |                 "placeholder": "言語を選択" | ||||||
| @ -26,6 +26,20 @@ | |||||||
|                 "button": "削除", |                 "button": "削除", | ||||||
|                 "confirm": "チャット履歴を削除してもよろしいですか?この操作は元に戻せません。" |                 "confirm": "チャット履歴を削除してもよろしいですか?この操作は元に戻せません。" | ||||||
|             } |             } | ||||||
|  |         }, | ||||||
|  |         "webSearch": { | ||||||
|  |             "heading": "ウェブ検索を管理する", | ||||||
|  |             "searchMode": { | ||||||
|  |               "label": "簡単なインターネット検索を実行する" | ||||||
|  |             }, | ||||||
|  |             "provider": { | ||||||
|  |               "label": "検索エンジン", | ||||||
|  |               "placeholder": "検索エンジンを選択する" | ||||||
|  |             }, | ||||||
|  |             "totalSearchResults": { | ||||||
|  |               "label": "合計検索結果", | ||||||
|  |               "placeholder": "合計検索結果を入力する" | ||||||
|  |             } | ||||||
|           } |           } | ||||||
|     }, |     }, | ||||||
|     "manageModels": { |     "manageModels": { | ||||||
|  | |||||||
| @ -1,8 +1,8 @@ | |||||||
| { | { | ||||||
|     "generalSettings": { |     "generalSettings": { | ||||||
|         "title": "പൊതുവായ സെറ്റിംഗുകൾ", |         "title": "പൊതുവായ സെറ്റിംഗുകൾ", | ||||||
|         "heading": "വെബ് UI സെറ്റിംഗുകൾ", |  | ||||||
|         "settings": { |         "settings": { | ||||||
|  |             "heading": "വെബ് UI സെറ്റിംഗുകൾ", | ||||||
|             "speechRecognitionLang": { |             "speechRecognitionLang": { | ||||||
|                 "label": "സംഭാഷണ തിരിച്ചറിയല് ഭാഷ", |                 "label": "സംഭാഷണ തിരിച്ചറിയല് ഭാഷ", | ||||||
|                 "placeholder": "ഒരു ഭാഷ തിരഞ്ഞെടുക്കുക" |                 "placeholder": "ഒരു ഭാഷ തിരഞ്ഞെടുക്കുക" | ||||||
| @ -26,6 +26,20 @@ | |||||||
|                 "button": "ഇല്ലാതാക്കുക", |                 "button": "ഇല്ലാതാക്കുക", | ||||||
|                 "confirm": "നിങ്ങളുടെ ചാറ്റ് ചരിത്രം ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ? ഈ പ്രവർത്തനം പിന്നീട് പിൻവലിക്കാനാകില്ല." |                 "confirm": "നിങ്ങളുടെ ചാറ്റ് ചരിത്രം ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ? ഈ പ്രവർത്തനം പിന്നീട് പിൻവലിക്കാനാകില്ല." | ||||||
|             } |             } | ||||||
|  |         }, | ||||||
|  |         "webSearch": { | ||||||
|  |             "heading": "വെബ്ബ് തിരച്ചിൽ നിയന്ത്രിക്കുക", | ||||||
|  |             "searchMode": { | ||||||
|  |                 "label": "സരളമായ ഇന്റർനെറ്റ് തിരച്ചിൽ നടത്തുക" | ||||||
|  |             }, | ||||||
|  |             "provider": { | ||||||
|  |                 "label": "തിരച്ചിൽ എഞ്ചിൻ", | ||||||
|  |                 "placeholder": "തിരച്ചിൽ എഞ്ചിൻ തിരഞ്ഞെടുക്കുക" | ||||||
|  |             }, | ||||||
|  |             "totalSearchResults": { | ||||||
|  |                 "label": "ആകെ തിരച്ചിൽ ഫലങ്ങൾ", | ||||||
|  |                 "placeholder": "ആകെ തിരച്ചിൽ ഫലങ്ങളുടെ എണ്ണം നൽകുക" | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     }, |     }, | ||||||
|     "manageModels": { |     "manageModels": { | ||||||
|  | |||||||
| @ -1,8 +1,8 @@ | |||||||
| { | { | ||||||
|     "generalSettings": { |     "generalSettings": { | ||||||
|         "title": "一般设置", |         "title": "一般设置", | ||||||
|         "heading": "Web UI 设置", |  | ||||||
|         "settings": { |         "settings": { | ||||||
|  |             "heading": "Web UI 设置", | ||||||
|             "speechRecognitionLang": { |             "speechRecognitionLang": { | ||||||
|                 "label": "语音识别语言", |                 "label": "语音识别语言", | ||||||
|                 "placeholder": "选择一种语言" |                 "placeholder": "选择一种语言" | ||||||
| @ -26,6 +26,20 @@ | |||||||
|                 "button": "删除", |                 "button": "删除", | ||||||
|                 "confirm": "您确定要删除聊天历史记录吗?这个操作不能撤销。" |                 "confirm": "您确定要删除聊天历史记录吗?这个操作不能撤销。" | ||||||
|             } |             } | ||||||
|  |         }, | ||||||
|  |         "webSearch": { | ||||||
|  |             "heading": "管理网络搜索", | ||||||
|  |             "searchMode": { | ||||||
|  |               "label": "执行简单的网际网路搜索" | ||||||
|  |             }, | ||||||
|  |             "provider": { | ||||||
|  |               "label": "搜索引擎", | ||||||
|  |               "placeholder": "选择一个搜索引擎" | ||||||
|  |             }, | ||||||
|  |             "totalSearchResults": { | ||||||
|  |               "label": "总搜索结果", | ||||||
|  |               "placeholder": "输入总搜索结果" | ||||||
|  |             } | ||||||
|           } |           } | ||||||
|     }, |     }, | ||||||
|     "manageModels": { |     "manageModels": { | ||||||
|  | |||||||
| @ -17,18 +17,13 @@ export const SettingOther = () => { | |||||||
| 
 | 
 | ||||||
|   const { mode, toggleDarkMode } = useDarkMode() |   const { mode, toggleDarkMode } = useDarkMode() | ||||||
|   const { t } = useTranslation("settings") |   const { t } = useTranslation("settings") | ||||||
|   const { |   const { changeLocale, locale, supportLanguage } = useI18n() | ||||||
|     changeLocale, |  | ||||||
|     locale, |  | ||||||
|     supportLanguage |  | ||||||
|   }= useI18n() |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <dl className="flex flex-col space-y-6 text-sm"> |     <dl className="flex flex-col space-y-6 text-sm"> | ||||||
|       <div> |       <div> | ||||||
|         <h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white"> |         <h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white"> | ||||||
|           {t("generalSettings.heading")} |           {t("generalSettings.settings.heading")} | ||||||
|         </h2> |         </h2> | ||||||
|         <div className="border border-b border-gray-200 dark:border-gray-600 mt-3"></div> |         <div className="border border-b border-gray-200 dark:border-gray-600 mt-3"></div> | ||||||
|       </div> |       </div> | ||||||
| @ -38,7 +33,9 @@ export const SettingOther = () => { | |||||||
|         </span> |         </span> | ||||||
| 
 | 
 | ||||||
|         <Select |         <Select | ||||||
|           placeholder={t("generalSettings.settings.speechRecognitionLang.placeholder")} |           placeholder={t( | ||||||
|  |             "generalSettings.settings.speechRecognitionLang.placeholder" | ||||||
|  |           )} | ||||||
|           allowClear |           allowClear | ||||||
|           showSearch |           showSearch | ||||||
|           options={SUPPORTED_LANGUAGES} |           options={SUPPORTED_LANGUAGES} | ||||||
| @ -86,7 +83,9 @@ export const SettingOther = () => { | |||||||
|           ) : ( |           ) : ( | ||||||
|             <MoonIcon className="w-4 h-4 mr-2" /> |             <MoonIcon className="w-4 h-4 mr-2" /> | ||||||
|           )} |           )} | ||||||
|           {mode === "dark" ? t("generalSettings.settings.darkMode.options.light") : t("generalSettings.settings.darkMode.options.dark")} |           {mode === "dark" | ||||||
|  |             ? t("generalSettings.settings.darkMode.options.light") | ||||||
|  |             : t("generalSettings.settings.darkMode.options.dark")} | ||||||
|         </button> |         </button> | ||||||
|       </div> |       </div> | ||||||
|       <SearchModeSettings /> |       <SearchModeSettings /> | ||||||
|  | |||||||
| @ -1,40 +1,92 @@ | |||||||
|  | import { SaveButton } from "@/components/Common/SaveButton" | ||||||
|  | import { getSearchSettings, setSearchSettings } from "@/services/search" | ||||||
|  | import { SUPPORTED_SERACH_PROVIDERS } from "@/utils/search-provider" | ||||||
|  | import { useForm } from "@mantine/form" | ||||||
| import { useQuery, useQueryClient } from "@tanstack/react-query" | import { useQuery, useQueryClient } from "@tanstack/react-query" | ||||||
| import { Skeleton, Switch } from "antd" | import { Select, Skeleton, Switch, InputNumber } from "antd" | ||||||
| import { useTranslation } from "react-i18next" | import { useTranslation } from "react-i18next" | ||||||
| import { |  | ||||||
|   getIsSimpleInternetSearch, |  | ||||||
|   setIsSimpleInternetSearch |  | ||||||
| } from "~/services/ollama" |  | ||||||
| 
 | 
 | ||||||
| export const SearchModeSettings = () => { | export const SearchModeSettings = () => { | ||||||
|   const { t } = useTranslation("settings") |   const { t } = useTranslation("settings") | ||||||
|  |   const queryClient = useQueryClient() | ||||||
| 
 | 
 | ||||||
|   const { data, status } = useQuery({ |   const form = useForm({ | ||||||
|     queryKey: ["fetchIsSimpleInternetSearch"], |     initialValues: { | ||||||
|     queryFn: () => getIsSimpleInternetSearch() |       isSimpleInternetSearch: false, | ||||||
|  |       searchProvider: "", | ||||||
|  |       totalSearchResults: 0 | ||||||
|  |     } | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   const queryClient = useQueryClient() |   const { status } = useQuery({ | ||||||
|  |     queryKey: ["fetchSearchSettings"], | ||||||
|  |     queryFn: async () => { | ||||||
|  |       const data = await getSearchSettings() | ||||||
|  |       form.setValues(data) | ||||||
|  |       return data | ||||||
|  |     } | ||||||
|  |   }) | ||||||
| 
 | 
 | ||||||
|   if (status === "pending" || status === "error") { |   if (status === "pending" || status === "error") { | ||||||
|     return <Skeleton active /> |     return <Skeleton active /> | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|  |     <div> | ||||||
|  |       <div className="mb-5"> | ||||||
|  |         <h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white"> | ||||||
|  |           {t("generalSettings.webSearch.heading")} | ||||||
|  |         </h2> | ||||||
|  |         <div className="border border-b border-gray-200 dark:border-gray-600 mt-3"></div> | ||||||
|  |       </div> | ||||||
|  |       <form | ||||||
|  |         onSubmit={form.onSubmit(async (values) => { | ||||||
|  |           await setSearchSettings(values) | ||||||
|  |         })} | ||||||
|  |         className="space-y-4"> | ||||||
|         <div className="flex flex-row justify-between"> |         <div className="flex flex-row justify-between"> | ||||||
|           <span className="text-gray-500 dark:text-neutral-50 "> |           <span className="text-gray-500 dark:text-neutral-50 "> | ||||||
|         {t("generalSettings.settings.searchMode.label")} |             {t("generalSettings.webSearch.provider.label")} | ||||||
|           </span> |           </span> | ||||||
| 
 |           <Select | ||||||
|       <Switch |             placeholder={t("generalSettings.webSearch.provider.placeholder")} | ||||||
|         checked={data} |             showSearch | ||||||
|         onChange={(checked) => { |             style={{ width: "200px" }} | ||||||
|           setIsSimpleInternetSearch(checked) |             options={SUPPORTED_SERACH_PROVIDERS} | ||||||
|           queryClient.invalidateQueries({ |             filterOption={(input, option) => | ||||||
|             queryKey: ["fetchIsSimpleInternetSearch"] |               option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 || | ||||||
|           }) |               option!.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 | ||||||
|         }} |             } | ||||||
|  |             {...form.getInputProps("searchProvider")} | ||||||
|           /> |           /> | ||||||
|         </div> |         </div> | ||||||
|  |         <div className="flex flex-row justify-between"> | ||||||
|  |           <span className="text-gray-500 dark:text-neutral-50 "> | ||||||
|  |             {t("generalSettings.webSearch.searchMode.label")} | ||||||
|  |           </span> | ||||||
|  |           <Switch | ||||||
|  |             {...form.getInputProps("isSimpleInternetSearch", { | ||||||
|  |               type: "checkbox" | ||||||
|  |             })} | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |         <div className="flex flex-row justify-between"> | ||||||
|  |           <span className="text-gray-500 dark:text-neutral-50 "> | ||||||
|  |             {t("generalSettings.webSearch.totalSearchResults.label")} | ||||||
|  |           </span> | ||||||
|  |           <InputNumber | ||||||
|  |             placeholder={t( | ||||||
|  |               "generalSettings.webSearch.totalSearchResults.placeholder" | ||||||
|  |             )} | ||||||
|  |             {...form.getInputProps("totalSearchResults")} | ||||||
|  |             style={{ width: "200px" }} | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <div className="flex justify-end"> | ||||||
|  |           <SaveButton btnType="submit" /> | ||||||
|  |         </div> | ||||||
|  |       </form> | ||||||
|  |     </div> | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
|  | |||||||
| @ -296,19 +296,6 @@ export const setWebPrompts = async (prompt: string, followUpPrompt: string) => { | |||||||
|   await setWebSearchFollowUpPrompt(followUpPrompt) |   await setWebSearchFollowUpPrompt(followUpPrompt) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const getIsSimpleInternetSearch = async () => { |  | ||||||
|   const isSimpleInternetSearch = await storage.get("isSimpleInternetSearch") |  | ||||||
|   if (!isSimpleInternetSearch || isSimpleInternetSearch.length === 0) { |  | ||||||
|     return true |  | ||||||
|   } |  | ||||||
|   return isSimpleInternetSearch === "true" |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| export const setIsSimpleInternetSearch = async (isSimpleInternetSearch: boolean) => { |  | ||||||
|   await storage.set("isSimpleInternetSearch", isSimpleInternetSearch.toString()) |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export const getPageShareUrl = async () => { | export const getPageShareUrl = async () => { | ||||||
|   const pageShareUrl = await storage.get("pageShareUrl") |   const pageShareUrl = await storage.get("pageShareUrl") | ||||||
|  | |||||||
							
								
								
									
										79
									
								
								src/services/search.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/services/search.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,79 @@ | |||||||
|  | import { Storage } from "@plasmohq/storage" | ||||||
|  | 
 | ||||||
|  | const storage = new Storage() | ||||||
|  | 
 | ||||||
|  | const TOTAL_SEARCH_RESULTS = 2 | ||||||
|  | const DEFAULT_PROVIDER = "google" | ||||||
|  | 
 | ||||||
|  | const AVAILABLE_PROVIDERS = ["google", "duckduckgo"] as const | ||||||
|  | 
 | ||||||
|  | export const getIsSimpleInternetSearch = async () => { | ||||||
|  |   const isSimpleInternetSearch = await storage.get("isSimpleInternetSearch") | ||||||
|  |   if (!isSimpleInternetSearch || isSimpleInternetSearch.length === 0) { | ||||||
|  |     return true | ||||||
|  |   } | ||||||
|  |   return isSimpleInternetSearch === "true" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const setIsSimpleInternetSearch = async ( | ||||||
|  |   isSimpleInternetSearch: boolean | ||||||
|  | ) => { | ||||||
|  |   await storage.set("isSimpleInternetSearch", isSimpleInternetSearch.toString()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const getSearchProvider = async (): Promise< | ||||||
|  |   (typeof AVAILABLE_PROVIDERS)[number] | ||||||
|  | > => { | ||||||
|  |   const searchProvider = await storage.get("searchProvider") | ||||||
|  |   if (!searchProvider || searchProvider.length === 0) { | ||||||
|  |     return DEFAULT_PROVIDER | ||||||
|  |   } | ||||||
|  |   return searchProvider as (typeof AVAILABLE_PROVIDERS)[number] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const setSearchProvider = async (searchProvider: string) => { | ||||||
|  |   await storage.set("searchProvider", searchProvider) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const totalSearchResults = async () => { | ||||||
|  |   const totalSearchResults = await storage.get("totalSearchResults") | ||||||
|  |   if (!totalSearchResults || totalSearchResults.length === 0) { | ||||||
|  |     return TOTAL_SEARCH_RESULTS | ||||||
|  |   } | ||||||
|  |   return parseInt(totalSearchResults) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const setTotalSearchResults = async (totalSearchResults: number) => { | ||||||
|  |   await storage.set("totalSearchResults", totalSearchResults.toString()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const getSearchSettings = async () => { | ||||||
|  |   const [isSimpleInternetSearch, searchProvider, totalSearchResult] = | ||||||
|  |     await Promise.all([ | ||||||
|  |       getIsSimpleInternetSearch(), | ||||||
|  |       getSearchProvider(), | ||||||
|  |       totalSearchResults() | ||||||
|  |     ]) | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     isSimpleInternetSearch, | ||||||
|  |     searchProvider, | ||||||
|  |     totalSearchResults: totalSearchResult | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const setSearchSettings = async ({ | ||||||
|  |   isSimpleInternetSearch, | ||||||
|  |   searchProvider, | ||||||
|  |   totalSearchResults | ||||||
|  | }: { | ||||||
|  |   isSimpleInternetSearch: boolean | ||||||
|  |   searchProvider: string | ||||||
|  |   totalSearchResults: number | ||||||
|  | }) => { | ||||||
|  |   await Promise.all([ | ||||||
|  |     setIsSimpleInternetSearch(isSimpleInternetSearch), | ||||||
|  |     setSearchProvider(searchProvider), | ||||||
|  |     setTotalSearchResults(totalSearchResults) | ||||||
|  |   ]) | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								src/utils/search-provider.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/utils/search-provider.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | export const SUPPORTED_SERACH_PROVIDERS = [ | ||||||
|  |     { | ||||||
|  |         label: "Google", | ||||||
|  |         value: "google" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         label: "DuckDuckGo", | ||||||
|  |         value: "duckduckgo" | ||||||
|  |     } | ||||||
|  | ] | ||||||
							
								
								
									
										114
									
								
								src/web/local-duckduckgo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/web/local-duckduckgo.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,114 @@ | |||||||
|  | import { cleanUrl } from "@/libs/clean-url" | ||||||
|  | import { chromeRunTime } from "@/libs/runtime" | ||||||
|  | import { PageAssistHtmlLoader } from "@/loader/html" | ||||||
|  | import { | ||||||
|  |   defaultEmbeddingChunkOverlap, | ||||||
|  |   defaultEmbeddingChunkSize, | ||||||
|  |   defaultEmbeddingModelForRag, | ||||||
|  |   getOllamaURL | ||||||
|  | } from "@/services/ollama" | ||||||
|  | import { | ||||||
|  |   getIsSimpleInternetSearch, | ||||||
|  |   totalSearchResults | ||||||
|  | } from "@/services/search" | ||||||
|  | import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama" | ||||||
|  | import type { Document } from "@langchain/core/documents" | ||||||
|  | import * as cheerio from "cheerio" | ||||||
|  | import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" | ||||||
|  | import { MemoryVectorStore } from "langchain/vectorstores/memory" | ||||||
|  | 
 | ||||||
|  | export const localDuckDuckGoSearch = async (query: string) => { | ||||||
|  |   await chromeRunTime(cleanUrl("https://html.duckduckgo.com/html/?q=" + query)) | ||||||
|  | 
 | ||||||
|  |   const abortController = new AbortController() | ||||||
|  |   setTimeout(() => abortController.abort(), 10000) | ||||||
|  | 
 | ||||||
|  |   const htmlString = await fetch( | ||||||
|  |     "https://html.duckduckgo.com/html/?q=" + query, | ||||||
|  |     { | ||||||
|  |       signal: abortController.signal | ||||||
|  |     } | ||||||
|  |   ) | ||||||
|  |     .then((response) => response.text()) | ||||||
|  |     .catch() | ||||||
|  | 
 | ||||||
|  |   const $ = cheerio.load(htmlString) | ||||||
|  | 
 | ||||||
|  |   const searchResults = Array.from($("div.results_links_deep")).map( | ||||||
|  |     (result) => { | ||||||
|  |       const title = $(result).find("a.result__a").text() | ||||||
|  |       const link = $(result) | ||||||
|  |         .find("a.result__snippet") | ||||||
|  |         .attr("href") | ||||||
|  |         .replace("//duckduckgo.com/l/?uddg=", "") | ||||||
|  |       const content = $(result).find("a.result__snippet").text() | ||||||
|  |       const decodedLink = decodeURIComponent(link) | ||||||
|  |       return { title, link: decodedLink, content } | ||||||
|  |     } | ||||||
|  |   ) | ||||||
|  | 
 | ||||||
|  |   return searchResults | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const webDuckDuckGoSearch = async (query: string) => { | ||||||
|  |   const results = await localDuckDuckGoSearch(query) | ||||||
|  |   const TOTAL_SEARCH_RESULTS = await totalSearchResults() | ||||||
|  |   const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS) | ||||||
|  | 
 | ||||||
|  |   const isSimpleMode = await getIsSimpleInternetSearch() | ||||||
|  | 
 | ||||||
|  |   if (isSimpleMode) { | ||||||
|  |     await getOllamaURL() | ||||||
|  |     return searchResults.map((result) => { | ||||||
|  |       return { | ||||||
|  |         url: result.link, | ||||||
|  |         content: result.content | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const docs: Document<Record<string, any>>[] = [] | ||||||
|  |   for (const result of searchResults) { | ||||||
|  |     const loader = new PageAssistHtmlLoader({ | ||||||
|  |       html: "", | ||||||
|  |       url: result.link | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     const documents = await loader.loadByURL() | ||||||
|  | 
 | ||||||
|  |     documents.forEach((doc) => { | ||||||
|  |       docs.push(doc) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |   const ollamaUrl = await getOllamaURL() | ||||||
|  | 
 | ||||||
|  |   const embeddingModle = await defaultEmbeddingModelForRag() | ||||||
|  |   const ollamaEmbedding = new OllamaEmbeddings({ | ||||||
|  |     model: embeddingModle || "", | ||||||
|  |     baseUrl: cleanUrl(ollamaUrl) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   const chunkSize = await defaultEmbeddingChunkSize() | ||||||
|  |   const chunkOverlap = await defaultEmbeddingChunkOverlap() | ||||||
|  |   const textSplitter = new RecursiveCharacterTextSplitter({ | ||||||
|  |     chunkSize, | ||||||
|  |     chunkOverlap | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   const chunks = await textSplitter.splitDocuments(docs) | ||||||
|  | 
 | ||||||
|  |   const store = new MemoryVectorStore(ollamaEmbedding) | ||||||
|  | 
 | ||||||
|  |   await store.addDocuments(chunks) | ||||||
|  | 
 | ||||||
|  |   const resultsWithEmbeddings = await store.similaritySearch(query, 3) | ||||||
|  | 
 | ||||||
|  |   const searchResult = resultsWithEmbeddings.map((result) => { | ||||||
|  |     return { | ||||||
|  |       url: result.metadata.url, | ||||||
|  |       content: result.pageContent | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   return searchResult | ||||||
|  | } | ||||||
| @ -1,3 +1,7 @@ | |||||||
|  | import { | ||||||
|  |   getIsSimpleInternetSearch, | ||||||
|  |   totalSearchResults | ||||||
|  | } from "@/services/search" | ||||||
| import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama" | import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama" | ||||||
| import type { Document } from "@langchain/core/documents" | import type { Document } from "@langchain/core/documents" | ||||||
| import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" | import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" | ||||||
| @ -5,16 +9,13 @@ import { MemoryVectorStore } from "langchain/vectorstores/memory" | |||||||
| import { cleanUrl } from "~/libs/clean-url" | import { cleanUrl } from "~/libs/clean-url" | ||||||
| import { chromeRunTime } from "~/libs/runtime" | import { chromeRunTime } from "~/libs/runtime" | ||||||
| import { PageAssistHtmlLoader } from "~/loader/html" | import { PageAssistHtmlLoader } from "~/loader/html" | ||||||
| import { defaultEmbeddingChunkOverlap, defaultEmbeddingChunkSize, defaultEmbeddingModelForRag, getIsSimpleInternetSearch, getOllamaURL } from "~/services/ollama" | import { | ||||||
|  |   defaultEmbeddingChunkOverlap, | ||||||
|  |   defaultEmbeddingChunkSize, | ||||||
|  |   defaultEmbeddingModelForRag, | ||||||
|  |   getOllamaURL | ||||||
|  | } from "~/services/ollama" | ||||||
| 
 | 
 | ||||||
| const BLOCKED_HOSTS = [ |  | ||||||
|   "google.com", |  | ||||||
|   "youtube.com", |  | ||||||
|   "twitter.com", |  | ||||||
|   "linkedin.com", |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| const TOTAL_SEARCH_RESULTS = 2 |  | ||||||
| 
 | 
 | ||||||
| export const localGoogleSearch = async (query: string) => { | export const localGoogleSearch = async (query: string) => { | ||||||
|   await chromeRunTime( |   await chromeRunTime( | ||||||
| @ -40,23 +41,18 @@ export const localGoogleSearch = async (query: string) => { | |||||||
|     (result) => { |     (result) => { | ||||||
|       const title = result.querySelector("h3")?.textContent |       const title = result.querySelector("h3")?.textContent | ||||||
|       const link = result.querySelector("a")?.getAttribute("href") |       const link = result.querySelector("a")?.getAttribute("href") | ||||||
|       const content = Array.from(result.querySelectorAll("span")).map((span) => span.textContent).join(" ") |       const content = Array.from(result.querySelectorAll("span")) | ||||||
|  |         .map((span) => span.textContent) | ||||||
|  |         .join(" ") | ||||||
|       return { title, link, content } |       return { title, link, content } | ||||||
|     } |     } | ||||||
|   ) |   ) | ||||||
|   const filteredSearchResults = searchResults |   return searchResults | ||||||
|     .filter( |  | ||||||
|       (result) => |  | ||||||
|         !result.link || |  | ||||||
|         !BLOCKED_HOSTS.some((host) => result.link.includes(host)) |  | ||||||
|     ) |  | ||||||
|     .filter((result) => result.title && result.link) |  | ||||||
|   return filteredSearchResults |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | export const webGoogleSearch = async (query: string) => { | ||||||
| export const webSearch = async (query: string) => { |  | ||||||
|   const results = await localGoogleSearch(query) |   const results = await localGoogleSearch(query) | ||||||
|  |   const TOTAL_SEARCH_RESULTS = await totalSearchResults() | ||||||
|   const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS) |   const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS) | ||||||
| 
 | 
 | ||||||
|   const isSimpleMode = await getIsSimpleInternetSearch() |   const isSimpleMode = await getIsSimpleInternetSearch() | ||||||
| @ -71,7 +67,7 @@ export const webSearch = async (query: string) => { | |||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const docs: Document<Record<string, any>>[] = []; |   const docs: Document<Record<string, any>>[] = [] | ||||||
|   for (const result of searchResults) { |   for (const result of searchResults) { | ||||||
|     const loader = new PageAssistHtmlLoader({ |     const loader = new PageAssistHtmlLoader({ | ||||||
|       html: "", |       html: "", | ||||||
| @ -89,14 +85,14 @@ export const webSearch = async (query: string) => { | |||||||
|   const embeddingModle = await defaultEmbeddingModelForRag() |   const embeddingModle = await defaultEmbeddingModelForRag() | ||||||
|   const ollamaEmbedding = new OllamaEmbeddings({ |   const ollamaEmbedding = new OllamaEmbeddings({ | ||||||
|     model: embeddingModle || "", |     model: embeddingModle || "", | ||||||
|     baseUrl: cleanUrl(ollamaUrl), |     baseUrl: cleanUrl(ollamaUrl) | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   const chunkSize = await defaultEmbeddingChunkSize(); |   const chunkSize = await defaultEmbeddingChunkSize() | ||||||
|   const chunkOverlap = await defaultEmbeddingChunkOverlap(); |   const chunkOverlap = await defaultEmbeddingChunkOverlap() | ||||||
|   const textSplitter = new RecursiveCharacterTextSplitter({ |   const textSplitter = new RecursiveCharacterTextSplitter({ | ||||||
|     chunkSize, |     chunkSize, | ||||||
|     chunkOverlap, |     chunkOverlap | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   const chunks = await textSplitter.splitDocuments(docs) |   const chunks = await textSplitter.splitDocuments(docs) | ||||||
| @ -105,7 +101,6 @@ export const webSearch = async (query: string) => { | |||||||
| 
 | 
 | ||||||
|   await store.addDocuments(chunks) |   await store.addDocuments(chunks) | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|   const resultsWithEmbeddings = await store.similaritySearch(query, 3) |   const resultsWithEmbeddings = await store.similaritySearch(query, 3) | ||||||
| 
 | 
 | ||||||
|   const searchResult = resultsWithEmbeddings.map((result) => { |   const searchResult = resultsWithEmbeddings.map((result) => { | ||||||
|  | |||||||
| @ -1,5 +1,7 @@ | |||||||
| import { getWebSearchPrompt } from "~/services/ollama" | import { getWebSearchPrompt } from "~/services/ollama" | ||||||
| import { webSearch } from "./local-google" | import { webGoogleSearch } from "./local-google" | ||||||
|  | import { webDuckDuckGoSearch } from "./local-duckduckgo" | ||||||
|  | import { getSearchProvider } from "@/services/search" | ||||||
| 
 | 
 | ||||||
| const getHostName = (url: string) => { | const getHostName = (url: string) => { | ||||||
|   try { |   try { | ||||||
| @ -10,17 +12,34 @@ const getHostName = (url: string) => { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | const searchWeb = (provider: string, query: string) => { | ||||||
|  |   switch (provider) { | ||||||
|  |     case "duckduckgo": | ||||||
|  |       return webDuckDuckGoSearch(query) | ||||||
|  |     default: | ||||||
|  |       return webGoogleSearch(query) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export const getSystemPromptForWeb = async (query: string) => { | export const getSystemPromptForWeb = async (query: string) => { | ||||||
|   try { |   try { | ||||||
|         const search = await webSearch(query) |     const searchProvider = await getSearchProvider() | ||||||
|  |     const search = await searchWeb(searchProvider, query) | ||||||
| 
 | 
 | ||||||
|         const search_results = search.map((result, idx) => `<result source="${result.url}" id="${idx}">${result.content}</result>`).join("\n") |     const search_results = search | ||||||
|  |       .map( | ||||||
|  |         (result, idx) => | ||||||
|  |           `<result source="${result.url}" id="${idx}">${result.content}</result>` | ||||||
|  |       ) | ||||||
|  |       .join("\n") | ||||||
| 
 | 
 | ||||||
|     const current_date_time = new Date().toLocaleString() |     const current_date_time = new Date().toLocaleString() | ||||||
| 
 | 
 | ||||||
|         const system = await getWebSearchPrompt(); |     const system = await getWebSearchPrompt() | ||||||
| 
 | 
 | ||||||
|         const prompt = system.replace("{current_date_time}", current_date_time).replace("{search_results}", search_results) |     const prompt = system | ||||||
|  |       .replace("{current_date_time}", current_date_time) | ||||||
|  |       .replace("{search_results}", search_results) | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|       prompt, |       prompt, | ||||||
| @ -28,7 +47,7 @@ export const getSystemPromptForWeb = async (query: string) => { | |||||||
|         return { |         return { | ||||||
|           url: result.url, |           url: result.url, | ||||||
|           name: getHostName(result.url), |           name: getHostName(result.url), | ||||||
|                     type: "url", |           type: "url" | ||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
| @ -36,7 +55,7 @@ export const getSystemPromptForWeb = async (query: string) => { | |||||||
|     console.error(e) |     console.error(e) | ||||||
|     return { |     return { | ||||||
|       prompt: "", |       prompt: "", | ||||||
|             source: [], |       source: [] | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
							
								
								
									
										38
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								yarn.lock
									
									
									
									
									
								
							| @ -1884,6 +1884,31 @@ charenc@0.0.2: | |||||||
|   resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" |   resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" | ||||||
|   integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== |   integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== | ||||||
| 
 | 
 | ||||||
|  | cheerio-select@^2.1.0: | ||||||
|  |   version "2.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" | ||||||
|  |   integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== | ||||||
|  |   dependencies: | ||||||
|  |     boolbase "^1.0.0" | ||||||
|  |     css-select "^5.1.0" | ||||||
|  |     css-what "^6.1.0" | ||||||
|  |     domelementtype "^2.3.0" | ||||||
|  |     domhandler "^5.0.3" | ||||||
|  |     domutils "^3.0.1" | ||||||
|  | 
 | ||||||
|  | cheerio@^1.0.0-rc.12: | ||||||
|  |   version "1.0.0-rc.12" | ||||||
|  |   resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" | ||||||
|  |   integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== | ||||||
|  |   dependencies: | ||||||
|  |     cheerio-select "^2.1.0" | ||||||
|  |     dom-serializer "^2.0.0" | ||||||
|  |     domhandler "^5.0.3" | ||||||
|  |     domutils "^3.0.1" | ||||||
|  |     htmlparser2 "^8.0.1" | ||||||
|  |     parse5 "^7.0.0" | ||||||
|  |     parse5-htmlparser2-tree-adapter "^7.0.0" | ||||||
|  | 
 | ||||||
| chokidar@^3.5.3, chokidar@^3.6.0: | chokidar@^3.5.3, chokidar@^3.6.0: | ||||||
|   version "3.6.0" |   version "3.6.0" | ||||||
|   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" |   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" | ||||||
| @ -3194,7 +3219,7 @@ html-to-text@^9.0.5: | |||||||
|     htmlparser2 "^8.0.2" |     htmlparser2 "^8.0.2" | ||||||
|     selderee "^0.11.0" |     selderee "^0.11.0" | ||||||
| 
 | 
 | ||||||
| htmlparser2@^8.0.2: | htmlparser2@^8.0.1, htmlparser2@^8.0.2: | ||||||
|   version "8.0.2" |   version "8.0.2" | ||||||
|   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" |   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" | ||||||
|   integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== |   integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== | ||||||
| @ -4998,6 +5023,14 @@ parse5-htmlparser2-tree-adapter@^6.0.0: | |||||||
|   dependencies: |   dependencies: | ||||||
|     parse5 "^6.0.1" |     parse5 "^6.0.1" | ||||||
| 
 | 
 | ||||||
|  | parse5-htmlparser2-tree-adapter@^7.0.0: | ||||||
|  |   version "7.0.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" | ||||||
|  |   integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== | ||||||
|  |   dependencies: | ||||||
|  |     domhandler "^5.0.2" | ||||||
|  |     parse5 "^7.0.0" | ||||||
|  | 
 | ||||||
| parse5@^5.1.1: | parse5@^5.1.1: | ||||||
|   version "5.1.1" |   version "5.1.1" | ||||||
|   resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" |   resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" | ||||||
| @ -5008,7 +5041,7 @@ parse5@^6.0.1: | |||||||
|   resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" |   resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" | ||||||
|   integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== |   integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== | ||||||
| 
 | 
 | ||||||
| parse5@^7.1.1: | parse5@^7.0.0, parse5@^7.1.1: | ||||||
|   version "7.1.2" |   version "7.1.2" | ||||||
|   resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" |   resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" | ||||||
|   integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== |   integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== | ||||||
| @ -6974,6 +7007,7 @@ winreg@0.0.12: | |||||||
|   integrity sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ== |   integrity sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ== | ||||||
| 
 | 
 | ||||||
| "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: | "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: | ||||||
|  |   name wrap-ansi-cjs | ||||||
|   version "7.0.0" |   version "7.0.0" | ||||||
|   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" |   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" | ||||||
|   integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== |   integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user