diff --git a/package.json b/package.json index eb13514..8f62d6c 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@vitejs/plugin-react": "^4.2.1", "antd": "^5.13.3", "axios": "^1.6.7", + "cheerio": "^1.0.0-rc.12", "dayjs": "^1.11.10", "html-to-text": "^9.0.5", "i18next": "^23.10.1", diff --git a/src/assets/locale/en/settings.json b/src/assets/locale/en/settings.json index 40e5c58..2aa6083 100644 --- a/src/assets/locale/en/settings.json +++ b/src/assets/locale/en/settings.json @@ -1,8 +1,8 @@ { "generalSettings": { "title": "General Settings", - "heading": "Web UI Settings", "settings": { + "heading": "Web UI Settings", "speechRecognitionLang": { "label": "Speech Recognition Language", "placeholder": "Select a language" @@ -17,14 +17,39 @@ "light": "Light", "dark": "Dark" } - }, + } + }, + "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" + } + }, + "system": { + "heading": "System Settings", "deleteChatHistory": { "label": "Delete Chat History", "button": "Delete", "confirm": "Are you sure you want to delete your chat history? This action cannot be undone." + }, + "export": { + "label": "Export Chat History, Settings, and Prompts", + "button": "Export Data", + "success": "Export Success" + }, + "import": { + "label": "Import Chat History, Settings, and Prompts", + "button": "Import Data", + "success": "Import Success", + "error": "Import Error" } } }, @@ -203,5 +228,19 @@ "webSearchFollowUpPromptPlaceholder": "Your Web Search Follow Up Prompt" } } + }, + "manageSearch": { + "title": "Manage Web Search", + "heading": "Configure Web Search" + }, + "about": { + "title": "About", + "heading": "About", + "chromeVersion": "Page Assist Version", + "ollamaVersion": "Ollama Version", + "support": "You can support the Page Assist project by donating or sponsoring through the following platforms:", + "koFi": "Support on Ko-fi", + "githubSponsor": "Sponsor on GitHub", + "githubRepo": "GitHub Repository" } } \ No newline at end of file diff --git a/src/assets/locale/ja-JP/common.json b/src/assets/locale/ja-JP/common.json new file mode 100644 index 0000000..569e669 --- /dev/null +++ b/src/assets/locale/ja-JP/common.json @@ -0,0 +1,52 @@ +{ + "pageAssist": "ページアシスト", + "selectAModel": "モデルを選択", + "save": "保存", + "saved": "保存済み", + "cancel": "キャンセル", + "retry": "再試行", + "share": { + "tooltip": { + "share": "共有" + }, + "modal": { + "title": "チャットリンクを共有" + }, + "form": { + "defaultValue": { + "name": "匿名", + "title": "無題のチャット" + }, + "title": { + "label": "チャットタイトル", + "placeholder": "チャットタイトルを入力", + "required": "チャットタイトルは必須です" + }, + "name": { + "label": "あなたの名前", + "placeholder": "名前を入力", + "required": "名前は必須です" + }, + "btn": { + "save": "リンクを生成", + "saving": "リンクを生成中..." + } + }, + "notification": { + "successGenerate": "リンクがクリップボードにコピーされました", + "failGenerate": "リンクの生成に失敗しました" + } + }, + "copyToClipboard": "クリップボードにコピー", + "webSearch": "ウェブを検索中", + "regenerate": "再生成", + "edit": "編集", + "saveAndSubmit": "保存して送信", + "editMessage": { + "placeholder": "メッセージを入力..." + }, + "submit": "送信", + "noData": "データがありません", + "noHistory": "チャット履歴がありません", + "chatWithCurrentPage": "現在のページでチャット" + } \ No newline at end of file diff --git a/src/assets/locale/ja-JP/option.json b/src/assets/locale/ja-JP/option.json new file mode 100644 index 0000000..57b023b --- /dev/null +++ b/src/assets/locale/ja-JP/option.json @@ -0,0 +1,12 @@ +{ + "newChat": "新しいチャット", + "selectAPrompt": "プロンプトを選択", + "githubRepository": "GitHubリポジトリ", + "settings": "設定", + "sidebarTitle": "チャット履歴", + "error": "エラー", + "somethingWentWrong": "何かが間違っています", + "validationSelectModel": "続行するにはモデルを選択してください", + "deleteHistoryConfirmation": "この履歴を削除しますか?", + "editHistoryTitle": "新しいタイトルを入力" + } \ No newline at end of file diff --git a/src/assets/locale/ja-JP/playground.json b/src/assets/locale/ja-JP/playground.json new file mode 100644 index 0000000..d26dd66 --- /dev/null +++ b/src/assets/locale/ja-JP/playground.json @@ -0,0 +1,27 @@ +{ + "ollamaState": { + "searching": "Ollamaを検索中 🦙", + "running": "Ollamaが実行中 🦙", + "notRunning": "Ollamaに接続できません 🦙" + }, + "formError": { + "noModel": "モデルを選択してください", + "noEmbeddingModel": "設定 > Ollamaページでembeddingモデルを設定してください" + }, + "form": { + "textarea": { + "placeholder": "メッセージを入力..." + }, + "webSearch": { + "on": "オン", + "off": "オフ" + } + }, + "tooltip": { + "searchInternet": "インターネットを検索", + "speechToText": "音声入力", + "uploadImage": "画像をアップロード", + "stopStreaming": "ストリーミングを停止" + }, + "sendWhenEnter": "Enterキーを押すと送信" +} \ No newline at end of file diff --git a/src/assets/locale/ja-JP/settings.json b/src/assets/locale/ja-JP/settings.json new file mode 100644 index 0000000..607532e --- /dev/null +++ b/src/assets/locale/ja-JP/settings.json @@ -0,0 +1,249 @@ +{ + "generalSettings": { + "title": "一般設定", + "settings": { + "heading": "Web UIの設定", + "speechRecognitionLang": { + "label": "音声認識の言語", + "placeholder": "言語を選択" + }, + "language": { + "label": "言語", + "placeholder": "言語を選択" + }, + "darkMode": { + "label": "テーマを変更", + "options": { + "light": "ライト", + "dark": "ダーク" + } + }, + "searchMode": { + "label": "簡易インターネット検索を実行" + } + }, + "webSearch": { + "heading": "ウェブ検索を管理する", + "searchMode": { + "label": "簡単なインターネット検索を実行する" + }, + "provider": { + "label": "検索エンジン", + "placeholder": "検索エンジンを選択する" + }, + "totalSearchResults": { + "label": "合計検索結果", + "placeholder": "合計検索結果を入力する" + } + }, + "system": { + "heading": "システム設定", + "deleteChatHistory": { + "label": "チャット履歴を削除する", + "button": "削除", + "confirm": "チャット履歴を削除してもよろしいですか?この操作は元に戻せません。" + }, + "export": { + "label": "チャット履歴、設定、プロンプトをエクスポートする", + "button": "データをエクスポート", + "success": "エクスポート成功" + }, + "import": { + "label": "チャット履歴、設定、プロンプトをインポートする", + "button": "データをインポート", + "success": "インポート成功", + "error": "インポートエラー" + } + } + }, + "manageModels": { + "title": "モデルを管理", + "addBtn": "新しいモデルを追加", + "columns": { + "name": "名前", + "digest": "ダイジェスト", + "modifiedAt": "修正日時", + "size": "サイズ", + "actions": "アクション" + }, + "expandedColumns": { + "parentModel": "親モデル", + "format": "フォーマット", + "family": "ファミリー", + "parameterSize": "パラメータサイズ", + "quantizationLevel": "量子化レベル" + }, + "tooltip": { + "delete": "モデルを削除", + "repull": "モデルを再取得" + }, + "confirm": { + "delete": "本当にこのモデルを削除しますか?", + "repull": "本当にこのモデルを再取得しますか?" + }, + "modal": { + "title": "新しいモデルを追加", + "placeholder": "モデル名を入力", + "pull": "モデルを取得" + }, + "notification": { + "pullModel": "モデルを取得中", + "pullModelDescription": "{{modelName}}モデルを取得中。詳細は拡張機能のアイコンをご確認ください。", + "success": "成功", + "error": "エラー", + "successDescription": "モデルの取得が完了しました", + "successDeleteDescription": "モデルの削除が完了しました", + "someError": "問題が発生しました。後ほど再度お試しください。" + } + }, + "managePrompts": { + "title": "プロンプトを管理", + "addBtn": "新しいプロンプトを追加", + "option1": "通常", + "option2": "RAG", + "questionPrompt": "質問プロンプト", + "columns": { + "title": "タイトル", + "prompt": "プロンプト", + "type": "プロンプトタイプ", + "actions": "アクション" + }, + "systemPrompt": "システムプロンプト", + "quickPrompt": "クイックプロンプト", + "tooltip": { + "delete": "プロンプトを削除", + "edit": "プロンプトを編集" + }, + "confirm": { + "delete": "本当にこのプロンプトを削除しますか?この操作は元に戻せません。" + }, + "modal": { + "addTitle": "新しいプロンプトを追加", + "editTitle": "プロンプトを編集" + }, + "form": { + "title": { + "label": "タイトル", + "placeholder": "素晴らしいプロンプト", + "required": "タイトルを入力してください" + }, + "prompt": { + "label": "プロンプト", + "placeholder": "プロンプトを入力", + "required": "プロンプトを入力してください", + "help": "プロンプト内で{key}を変数として使用できます。" + }, + "isSystem": { + "label": "システムプロンプト" + }, + "btnSave": { + "saving": "プロンプトを追加中...", + "save": "プロンプトを追加" + }, + "btnEdit": { + "saving": "プロンプトを更新中...", + "save": "プロンプトを更新" + } + }, + "notification": { + "addSuccess": "プロンプトが追加されました", + "addSuccessDesc": "プロンプトが正常に追加されました", + "error": "エラー", + "someError": "問題が発生しました。後ほど再度お試しください。", + "updatedSuccess": "プロンプトが更新されました", + "updatedSuccessDesc": "プロンプトが正常に更新されました", + "deletedSuccess": "プロンプトが削除されました", + "deletedSuccessDesc": "プロンプトが正常に削除されました" + } + }, + "manageShare": { + "title": "共有を管理", + "heading": "ページ共有URLを設定", + "form": { + "url": { + "label": "ページ共有URL", + "placeholder": "ページ共有URLを入力", + "required": "ページ共有URLを入力してください!", + "help": "プライバシー保護のため、ページ共有を自身でホストし、そのURLをここに入力することができます。詳細" + } + }, + "webshare": { + "heading": "ウェブ共有", + "columns": { + "title": "タイトル", + "url": "URL", + "actions": "アクション" + }, + "tooltip": { + "delete": "共有を削除" + }, + "confirm": { + "delete": "本当にこの共有を削除しますか?この操作は元に戻せません。" + } + }, + "notification": { + "pageShareSuccess": "ページ共有URLが正常に更新されました", + "someError": "問題が発生しました。後ほど再度お試しください。", + "webShareDeleteSuccess": "ウェブ共有が正常に削除されました" + } + }, + "ollamaSettings": { + "title": "Ollamaの設定", + "heading": "Ollamaを設定", + "settings": { + "ollamaUrl": { + "label": "OllamaのURL", + "placeholder": "OllamaのURLを入力" + }, + "ragSettings": { + "label": "RAGの設定", + "model": { + "label": "エンベディングモデル", + "required": "モデルを選択してください", + "help": "`nomic-embed-text`などのエンベディングモデルの使用を強くおすすめします。", + "placeholder": "モデルを選択" + }, + "chunkSize": { + "label": "チャンクサイズ", + "placeholder": "チャンクサイズを入力", + "required": "チャンクサイズを入力してください" + }, + "chunkOverlap": { + "label": "チャンクオーバーラップ", + "placeholder": "チャンクオーバーラップを入力", + "required": "チャンクオーバーラップを入力してください" + } + }, + "prompt": { + "label": "RAGプロンプトを設定", + "option1": "通常", + "option2": "Web", + "alert": "ここでシステムプロンプトを設定することは非推奨となりました。プロンプトの追加や編集には「プロンプトを管理」セクションをご利用ください。このセクションは今後のリリースで削除される予定です。", + "systemPrompt": "システムプロンプト", + "systemPromptPlaceholder": "システムプロンプトを入力", + "webSearchPrompt": "Web検索プロンプト", + "webSearchPromptHelp": "プロンプトから`{search_results}`を削除しないでください。", + "webSearchPromptError": "Web検索プロンプトを入力してください", + "webSearchPromptPlaceholder": "Web検索プロンプトを入力", + "webSearchFollowUpPrompt": "Web検索フォローアッププロンプト", + "webSearchFollowUpPromptHelp": "プロンプトから`{chat_history}`と`{question}`を削除しないでください。", + "webSearchFollowUpPromptError": "Web検索フォローアッププロンプトを入力してください!", + "webSearchFollowUpPromptPlaceholder": "Web検索フォローアッププロンプト" + } + } + }, + "manageSearch": { + "title": "Web検索の管理", + "heading": "Web検索を設定する" + }, + "about": { + "title": "About", + "heading": "About", + "chromeVersion": "Page Assistのバージョン", + "ollamaVersion": "Ollamaのバージョン", + "support": "Page Assistプロジェクトは、以下のプラットフォームで寄付やスポンサーシップをすることで支援できます:", + "koFi": "Ko-fiで支援する", + "githubSponsor": "GitHubでスポンサーする", + "githubRepo": "GitHubリポジトリ" + } +} \ No newline at end of file diff --git a/src/assets/locale/ja-JP/sidepanel.json b/src/assets/locale/ja-JP/sidepanel.json new file mode 100644 index 0000000..ff20f3f --- /dev/null +++ b/src/assets/locale/ja-JP/sidepanel.json @@ -0,0 +1,5 @@ +{ + "tooltip": { + "embed": "ページを埋め込むのに数分かかる場合があります。しばらくお待ちください..." + } + } \ No newline at end of file diff --git a/src/assets/locale/ml/settings.json b/src/assets/locale/ml/settings.json index d896f45..84ab01e 100644 --- a/src/assets/locale/ml/settings.json +++ b/src/assets/locale/ml/settings.json @@ -1,8 +1,8 @@ { "generalSettings": { "title": "പൊതുവായ സെറ്റിംഗുകൾ", - "heading": "വെബ് UI സെറ്റിംഗുകൾ", "settings": { + "heading": "വെബ് UI സെറ്റിംഗുകൾ", "speechRecognitionLang": { "label": "സംഭാഷണ തിരിച്ചറിയല്‍ ഭാഷ", "placeholder": "ഒരു ഭാഷ തിരഞ്ഞെടുക്കുക" @@ -20,11 +20,39 @@ }, "searchMode": { "label": "സാധാരണ ഇന്റർനെറ്റ് അന്വേഷണം നടത്തുക" + } + }, + "webSearch": { + "heading": "വെബ്ബ് തിരച്ചിൽ നിയന്ത്രിക്കുക", + "searchMode": { + "label": "സരളമായ ഇന്റർനെറ്റ് തിരച്ചിൽ നടത്തുക" }, + "provider": { + "label": "തിരച്ചിൽ എഞ്ചിൻ", + "placeholder": "തിരച്ചിൽ എഞ്ചിൻ തിരഞ്ഞെടുക്കുക" + }, + "totalSearchResults": { + "label": "ആകെ തിരച്ചിൽ ഫലങ്ങൾ", + "placeholder": "ആകെ തിരച്ചിൽ ഫലങ്ങളുടെ എണ്ണം നൽകുക" + } + }, + "system": { + "heading": "സിസ്റ്റം ക്രമീകരണങ്ങൾ", "deleteChatHistory": { "label": "ചാറ്റ് ചരിത്രം ഇല്ലാതാക്കുക", "button": "ഇല്ലാതാക്കുക", - "confirm": "നിങ്ങളുടെ ചാറ്റ് ചരിത്രം ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ? ഈ പ്രവർത്തനം പിന്നീട് പിൻവലിക്കാനാകില്ല." + "confirm": "നിങ്ങളുടെ ചാറ്റ് ചരിത്രം ഇല്ലാതാക്കണമെന്ന് ഉറപ്പാണോ? ഈ പ്രവർത്തനം പിന്നീട് പിന്വലിക്കാനാവില്ല." + }, + "export": { + "label": "ചാറ്റ് ചരിത്രം, ക്രമീകരണങ്ങൾ, പ്രോംപ്റ്റുകൾ എക്സ്പോർട്ട് ചെയ്യുക", + "button": "ഡാറ്റ എക്സ്പോർട്ട് ചെയ്യുക", + "success": "എക്സ്പോർട്ട് വിജയകരമായി" + }, + "import": { + "label": "ചാറ്റ് ചരിത്രം, ക്രമീകരണങ്ങൾ, പ്രോംപ്റ്റുകൾ ഇമ്പോർട്ട് ചെയ്യുക", + "button": "ഡാറ്റ ഇമ്പോർട്ട് ചെയ്യുക", + "success": "ഇമ്പോർട്ട് വിജയകരമായി", + "error": "ഇമ്പോർട്ട് പരാജയപ്പെട്ടു" } } }, @@ -203,5 +231,20 @@ "webSearchFollowUpPromptPlaceholder": "നിങ്ങളുടെ വെബ് തിരയല്‍ തുടര്‍പ്രോംപ്റ്റ്" } } - } + }, + "manageSearch": { + "heading": "Web തിരയൽ സജ്ജമാക്കുക", + "title": "Web തിരയൽ നിയന്ത്രിക്കുക" + }, + "about": { + "title": "വിവരങ്ങൾ", + "heading": "വിവരങ്ങൾ", + "chromeVersion": "പേജ് അസിസ്റ്റ് വേർഷൻ", + "ollamaVersion": "ഓളാമ വേർഷൻ", + "support": "താഴെ പറയുന്ന പ്ലാറ്റ്ഫോമുകളിലൂടെ ദാനം ചെയ്യുകയോ സ്പോൺസർ ചെയ്യുകയോ ചെയ്ത് പേജ് അസിസ്റ്റ് പ്രോജക്റ്റിനെ പിന്തുണയ്ക്കാവുന്നതാണ്:", + "koFi": "കോഫിയിൽ പിന്തുണയ്ക്കുക", + "githubSponsor": "ഗിറ്റ്ഹബ്ബിൽ സ്പോൺസർ ചെയ്യുക", + "githubRepo": "ഗിറ്റ്ഹബ്ബ് റെപ്പോസിറ്ററി" + } + } \ No newline at end of file diff --git a/src/assets/locale/zh/settings.json b/src/assets/locale/zh/settings.json index 251b07e..b99fead 100644 --- a/src/assets/locale/zh/settings.json +++ b/src/assets/locale/zh/settings.json @@ -1,8 +1,8 @@ { "generalSettings": { "title": "一般设置", - "heading": "Web UI 设置", "settings": { + "heading": "Web UI 设置", "speechRecognitionLang": { "label": "语音识别语言", "placeholder": "选择一种语言" @@ -20,11 +20,39 @@ }, "searchMode": { "label": "使用简化的互联网搜索" + } + }, + "webSearch": { + "heading": "管理网络搜索", + "searchMode": { + "label": "执行简单的网际网路搜索" }, + "provider": { + "label": "搜索引擎", + "placeholder": "选择一个搜索引擎" + }, + "totalSearchResults": { + "label": "总搜索结果", + "placeholder": "输入总搜索结果" + } + }, + "system": { + "heading": "系统设置", "deleteChatHistory": { - "label": "删除聊天历史记录", + "label": "删除聊天记录", "button": "删除", - "confirm": "您确定要删除聊天历史记录吗?这个操作不能撤销。" + "confirm": "您确定要删除聊天记录吗?此操作无法撤销。" + }, + "export": { + "label": "导出聊天记录、设置和提示", + "button": "导出数据", + "success": "导出成功" + }, + "import": { + "label": "导入聊天记录、设置和提示", + "button": "导入数据", + "success": "导入成功", + "error": "导入错误" } } }, @@ -204,5 +232,19 @@ "webSearchFollowUpPromptPlaceholder": "您的网页搜索追问提示词" } } - } + }, + "manageSearch": { + "heading": "配置网络搜索", + "title": "管理网络搜索" + }, + "about": { + "title": "关于", + "heading": "关于", + "chromeVersion": "Page Assist版本", + "ollamaVersion": "Ollama版本", + "support": "您可以通过以下平台捐赠或赞助Page Assist项目:", + "koFi": "在Ko-fi上支持", + "githubSponsor": "在GitHub上赞助", + "githubRepo": "GitHub仓库" + } } \ No newline at end of file diff --git a/src/components/Common/PageAssistProvider.tsx b/src/components/Common/PageAssistProvider.tsx new file mode 100644 index 0000000..fb75f83 --- /dev/null +++ b/src/components/Common/PageAssistProvider.tsx @@ -0,0 +1,27 @@ +import { PageAssistContext } from "@/context" +import { Message } from "@/types/message" +import React from "react" + +export const PageAssistProvider = ({ + children +}: { + children: React.ReactNode +}) => { + const [messages, setMessages] = React.useState([]) + const [controller, setController] = React.useState( + null + ) + + return ( + + {children} + + ) +} diff --git a/src/components/Icons/Ollama.tsx b/src/components/Icons/Ollama.tsx new file mode 100644 index 0000000..e0e0dab --- /dev/null +++ b/src/components/Icons/Ollama.tsx @@ -0,0 +1,20 @@ +import React from "react" + +export const OllamaIcon = React.forwardRef< + SVGSVGElement, + React.SVGProps +>((props, ref) => { + return ( + + + + + + ) +}) diff --git a/src/components/Layouts/Layout.tsx b/src/components/Layouts/Layout.tsx index 26ac2a5..23e36db 100644 --- a/src/components/Layouts/Layout.tsx +++ b/src/components/Layouts/Layout.tsx @@ -18,6 +18,7 @@ import { import { getAllPrompts } from "~/libs/db" import { ShareBtn } from "~/components/Common/ShareBtn" import { useTranslation } from "react-i18next" +import { OllamaIcon } from "../Icons/Ollama" export default function OptionLayout({ children @@ -73,7 +74,7 @@ export default function OptionLayout({
-
+
{pathname !== "/" && (
@@ -94,7 +95,7 @@ export default function OptionLayout({
@@ -109,16 +110,22 @@ export default function OptionLayout({ size="large" loading={isModelsLoading || isModelsFetching} filterOption={(input, option) => - option!.label.toLowerCase().indexOf(input.toLowerCase()) >= - 0 || - option!.value.toLowerCase().indexOf(input.toLowerCase()) >= - 0 + option.label.key + .toLowerCase() + .indexOf(input.toLowerCase()) >= 0 } showSearch placeholder={t("common:selectAModel")} className="w-64 " options={models?.map((model) => ({ - label: model.name, + label: ( + + + {model.name} + + ), value: model.model }))} /> @@ -146,13 +153,13 @@ export default function OptionLayout({ label: ( - {prompt.title} + className="flex flex-row gap-3 items-center"> {prompt.is_system ? ( ) : ( )} + {prompt.title} ), value: prompt.id @@ -166,8 +173,7 @@ export default function OptionLayout({ {pathname === "/" && messages.length > 0 && !streaming && ( )} - + - + diff --git a/src/components/Layouts/SettingsOptionLayout.tsx b/src/components/Layouts/SettingsOptionLayout.tsx index 3b50161..372dc0d 100644 --- a/src/components/Layouts/SettingsOptionLayout.tsx +++ b/src/components/Layouts/SettingsOptionLayout.tsx @@ -1,12 +1,7 @@ -import { - Book, - BrainCircuit, - CircuitBoardIcon, - Orbit, - Share -} from "lucide-react" +import { Book, BrainCircuit, Orbit, Share, BlocksIcon , InfoIcon} from "lucide-react" import { useTranslation } from "react-i18next" import { Link, useLocation } from "react-router-dom" +import { OllamaIcon } from "../Icons/Ollama" function classNames(...classes: string[]) { return classes.filter(Boolean).join(" ") @@ -64,7 +59,7 @@ export const SettingsLayout = ({ children }: { children: React.ReactNode }) => { { current={location.pathname} icon={BrainCircuit} /> + {/* */} { icon={Share} current={location.pathname} /> + diff --git a/src/components/Option/Playground/PlaygroundEmpty.tsx b/src/components/Option/Playground/PlaygroundEmpty.tsx index 81771da..6636605 100644 --- a/src/components/Option/Playground/PlaygroundEmpty.tsx +++ b/src/components/Option/Playground/PlaygroundEmpty.tsx @@ -37,10 +37,10 @@ export const PlaygroundEmpty = () => { return (
-
+
{(ollamaStatus === "pending" || isRefetching) && (
-
+

{t("ollamaState.searching")}

@@ -49,7 +49,7 @@ export const PlaygroundEmpty = () => { {!isRefetching && ollamaStatus === "success" ? ( ollamaInfo.isOk ? (
-
+

{t("ollamaState.running")}

@@ -57,7 +57,7 @@ export const PlaygroundEmpty = () => { ) : (
-
+

{t("ollamaState.notRunning")}

diff --git a/src/components/Option/Playground/PlaygroundForm.tsx b/src/components/Option/Playground/PlaygroundForm.tsx index 6776732..87c6005 100644 --- a/src/components/Option/Playground/PlaygroundForm.tsx +++ b/src/components/Option/Playground/PlaygroundForm.tsx @@ -153,7 +153,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => { } } return ( -
+
{
-
+
{ if (!selectedModel || selectedModel.length === 0) { diff --git a/src/components/Option/Settings/about.tsx b/src/components/Option/Settings/about.tsx new file mode 100644 index 0000000..b30f39d --- /dev/null +++ b/src/components/Option/Settings/about.tsx @@ -0,0 +1,102 @@ +import { getOllamaURL } from "~/services/ollama" +import { useTranslation } from "react-i18next" +import { useQuery } from "@tanstack/react-query" +import { Skeleton } from "antd" +import { cleanUrl } from "@/libs/clean-url" + +export const AboutApp = () => { + const { t } = useTranslation("settings") + + const { data, status } = useQuery({ + queryKey: ["fetchOllamURL"], + queryFn: async () => { + const chromeVersion = chrome.runtime.getManifest().version + try { + const url = await getOllamaURL() + const req = await fetch(`${cleanUrl(url)}/api/version`) + + if (!req.ok) { + return { + ollama: "N/A", + chromeVersion + } + } + + const res = (await req.json()) as { version: string } + return { + ollama: res.version, + chromeVersion + } + } catch { + return { + ollama: "N/A", + chromeVersion + } + } + } + }) + + return ( +
+ {status === "pending" && } + {status === "success" && ( +
+
+
+

+ {t("about.heading")} +

+
+
+
+ +
+
+
+ + {t("about.chromeVersion")} + + + {data.chromeVersion} + +
+ +
+ + {t("about.ollamaVersion")} + + + {data.ollama} + +
+
+
+ +
+

+ {t("about.support")} +

+ + +
+
+ )} +
+ ) +} diff --git a/src/components/Option/Settings/other.tsx b/src/components/Option/Settings/other.tsx index 5a61adb..a608f98 100644 --- a/src/components/Option/Settings/other.tsx +++ b/src/components/Option/Settings/other.tsx @@ -17,18 +17,13 @@ export const SettingOther = () => { const { mode, toggleDarkMode } = useDarkMode() const { t } = useTranslation("settings") - const { - changeLocale, - locale, - supportLanguage - }= useI18n() - + const { changeLocale, locale, supportLanguage } = useI18n() return (

- {t("generalSettings.heading")} + {t("generalSettings.settings.heading")}

@@ -38,7 +33,9 @@ export const SettingOther = () => { + option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 || + option!.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 + } + {...form.getInputProps("searchProvider")} + /> +
+
+ + {t("generalSettings.webSearch.searchMode.label")} + + +
+
+ + {t("generalSettings.webSearch.totalSearchResults.label")} + + +
- { - setIsSimpleInternetSearch(checked) - queryClient.invalidateQueries({ - queryKey: ["fetchIsSimpleInternetSearch"] - }) - }} - /> +
+ +
+
) } diff --git a/src/context/index.tsx b/src/context/index.tsx new file mode 100644 index 0000000..7e91bae --- /dev/null +++ b/src/context/index.tsx @@ -0,0 +1,26 @@ +import { Message } from "@/types/message" +import React, { Dispatch, SetStateAction, createContext } from "react" + +interface PageAssistContext { + messages: Message[] + setMessages: Dispatch> + + controller: AbortController | null + setController: Dispatch> +} + +export const PageAssistContext = createContext({ + messages: [], + setMessages: () => {}, + + controller: null, + setController: () => {} +}) + +export const usePageAssist = () => { + const context = React.useContext(PageAssistContext) + if (!context) { + throw new Error("usePageAssist must be used within a PageAssistContext") + } + return context +} diff --git a/src/entries/background.ts b/src/entries/background.ts index af5c1e3..d4f075b 100644 --- a/src/entries/background.ts +++ b/src/entries/background.ts @@ -81,7 +81,8 @@ export default defineBackground({ chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { const tab = tabs[0] chrome.sidePanel.open({ - tabId: tab.id! + // tabId: tab.id!, + windowId: tab.windowId!, }) }) } else if (message.type === "pull_model") { @@ -113,7 +114,7 @@ export default defineBackground({ chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { const tab = tabs[0] chrome.sidePanel.open({ - tabId: tab.id! + windowId: tab.windowId! }) }) break @@ -133,7 +134,7 @@ export default defineBackground({ chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { const tab = tabs[0] await chrome.sidePanel.open({ - tabId: tab.id! + windowId: tab.windowId!, }) }) } diff --git a/src/entries/options/App.tsx b/src/entries/options/App.tsx index b0221ef..f78a803 100644 --- a/src/entries/options/App.tsx +++ b/src/entries/options/App.tsx @@ -1,7 +1,5 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { MemoryRouter } from "react-router-dom" -import { ToastContainer } from "react-toastify" -import "react-toastify/dist/ReactToastify.css" const queryClient = new QueryClient() import { ConfigProvider, Empty, theme } from "antd" import { StyleProvider } from "@ant-design/cssinjs" @@ -9,6 +7,7 @@ import { useDarkMode } from "~/hooks/useDarkmode" import { OptionRouting } from "~/routes" import "~/i18n" import { useTranslation } from "react-i18next" +import { PageAssistProvider } from "@/components/Common/PageAssistProvider" function IndexOption() { const { mode } = useDarkMode() @@ -27,12 +26,12 @@ function IndexOption() { }} description={t("common:noData")} /> - )} - > + )}> - - + + + diff --git a/src/entries/sidepanel/App.tsx b/src/entries/sidepanel/App.tsx index 26ce815..495d6fd 100644 --- a/src/entries/sidepanel/App.tsx +++ b/src/entries/sidepanel/App.tsx @@ -1,14 +1,13 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { MemoryRouter } from "react-router-dom" import { SidepanelRouting } from "~/routes" -import { ToastContainer } from "react-toastify" -import "react-toastify/dist/ReactToastify.css" const queryClient = new QueryClient() import { ConfigProvider, Empty, theme } from "antd" import { StyleProvider } from "@ant-design/cssinjs" import { useDarkMode } from "~/hooks/useDarkmode" import "~/i18n" import { useTranslation } from "react-i18next" +import { PageAssistProvider } from "@/components/Common/PageAssistProvider" function IndexSidepanel() { const { mode } = useDarkMode() @@ -28,12 +27,12 @@ function IndexSidepanel() { }} description={t("common:noData")} /> - )} - > + )}> - - + + + diff --git a/src/hooks/chat-helper/index.ts b/src/hooks/chat-helper/index.ts new file mode 100644 index 0000000..51d320c --- /dev/null +++ b/src/hooks/chat-helper/index.ts @@ -0,0 +1,160 @@ +import { saveHistory, saveMessage } from "@/libs/db" +import { ChatHistory } from "@/store/option" + +export const saveMessageOnError = async ({ + e, + history, + setHistory, + image, + userMessage, + botMessage, + historyId, + selectedModel, + setHistoryId, + isRegenerating +}: { + e: any + setHistory: (history: ChatHistory) => void + history: ChatHistory + userMessage: string + image: string + botMessage: string + historyId: string | null + selectedModel: string + setHistoryId: (historyId: string) => void + isRegenerating: boolean +}) => { + if ( + e?.name === "AbortError" || + e?.message === "AbortError" || + e?.name?.includes("AbortError") || + e?.message?.includes("AbortError") + ) { + setHistory([ + ...history, + { + role: "user", + content: userMessage, + image + }, + { + role: "assistant", + content: botMessage + } + ]) + + if (historyId) { + if (!isRegenerating) { + await saveMessage( + historyId, + selectedModel, + "user", + userMessage, + [image], + [], + 1 + ) + } + await saveMessage( + historyId, + selectedModel, + "assistant", + botMessage, + [], + [], + 2 + ) + } else { + const newHistoryId = await saveHistory(userMessage) + if (!isRegenerating) { + await saveMessage( + newHistoryId.id, + selectedModel, + "user", + userMessage, + [image], + [], + 1 + ) + } + await saveMessage( + newHistoryId.id, + selectedModel, + "assistant", + botMessage, + [], + [], + 2 + ) + setHistoryId(newHistoryId.id) + } + + return true + } + + return false +} + +export const saveMessageOnSuccess = async ({ + historyId, + setHistoryId, + isRegenerate, + selectedModel, + message, + image, + fullText, + source +}: { + historyId: string | null + setHistoryId: (historyId: string) => void + isRegenerate: boolean + selectedModel: string | null + message: string + image: string + fullText: string + source: any[] +}) => { + if (historyId) { + if (!isRegenerate) { + await saveMessage( + historyId, + selectedModel, + "user", + message, + [image], + [], + 1 + ) + } + await saveMessage( + historyId, + selectedModel!, + "assistant", + fullText, + [], + source, + 2 + ) + } else { + const newHistoryId = await saveHistory(message) + await saveMessage( + newHistoryId.id, + selectedModel, + "user", + message, + [image], + [], + 1 + ) + await saveMessage( + newHistoryId.id, + selectedModel!, + "assistant", + fullText, + [], + source, + 2 + ) + setHistoryId(newHistoryId.id) + } +} diff --git a/src/hooks/useMessageOption.tsx b/src/hooks/useMessageOption.tsx index d5e49b1..f0b0d63 100644 --- a/src/hooks/useMessageOption.tsx +++ b/src/hooks/useMessageOption.tsx @@ -11,10 +11,9 @@ import { HumanMessage, SystemMessage } from "@langchain/core/messages" import { useStoreMessageOption } from "~/store/option" import { deleteChatForEdit, + generateID, getPromptById, removeMessageUsingHistoryId, - saveHistory, - saveMessage, updateMessageByIndex } from "~/libs/db" import { useNavigate } from "react-router-dom" @@ -22,13 +21,19 @@ import { notification } from "antd" import { getSystemPromptForWeb } from "~/web/web" import { generateHistory } from "@/utils/generate-history" import { useTranslation } from "react-i18next" +import { saveMessageOnError, saveMessageOnSuccess } from "./chat-helper" +import { usePageAssist } from "@/context" export const useMessageOption = () => { const { - history, + controller: abortController, + setController: setAbortController, messages, + setMessages + } = usePageAssist() + const { + history, setHistory, - setMessages, setStreaming, streaming, setIsFirstMessage, @@ -59,8 +64,6 @@ export const useMessageOption = () => { const navigate = useNavigate() const textareaRef = React.useRef(null) - const abortControllerRef = React.useRef(null) - const clearChat = () => { navigate("/") setMessages([]) @@ -78,14 +81,14 @@ export const useMessageOption = () => { image: string, isRegenerate: boolean, messages: Message[], - history: ChatHistory + history: ChatHistory, + signal: AbortSignal ) => { const url = await getOllamaURL() if (image.length > 0) { image = `data:image/jpeg;base64,${image.split(",")[1]}` } - abortControllerRef.current = new AbortController() const ollama = new ChatOllama({ model: selectedModel!, @@ -93,6 +96,8 @@ export const useMessageOption = () => { }) let newMessage: Message[] = [] + let generateMessageId = generateID() + if (!isRegenerate) { newMessage = [ ...messages, @@ -107,7 +112,8 @@ export const useMessageOption = () => { isBot: true, name: selectedModel, message: "▋", - sources: [] + sources: [], + id: generateMessageId } ] } else { @@ -117,12 +123,14 @@ export const useMessageOption = () => { isBot: true, name: selectedModel, message: "▋", - sources: [] + sources: [], + id: generateMessageId } ] } setMessages(newMessage) - const appendingIndex = newMessage.length - 1 + let fullText = "" + let contentToSave = "" try { setIsSearchingInternet(true) @@ -195,138 +203,93 @@ export const useMessageOption = () => { const chunks = await ollama.stream( [...applicationChatHistory, humanMessage], { - signal: abortControllerRef.current.signal + signal: signal } ) let count = 0 for await (const chunk of chunks) { + contentToSave += chunk.content + fullText += chunk.content if (count === 0) { setIsProcessing(true) - newMessage[appendingIndex].message = chunk.content + "▋" - setMessages(newMessage) - } else { - newMessage[appendingIndex].message = - newMessage[appendingIndex].message.slice(0, -1) + - chunk.content + - "▋" - setMessages(newMessage) } - + setMessages((prev) => { + return prev.map((message) => { + if (message.id === generateMessageId) { + return { + ...message, + message: fullText.slice(0, -1) + "▋" + } + } + return message + }) + }) count++ } - - newMessage[appendingIndex].message = newMessage[ - appendingIndex - ].message.slice(0, -1) - - newMessage[appendingIndex].sources = source - - if (!isRegenerate) { - setHistory([ - ...history, - { - role: "user", - content: message, - image - }, - { - role: "assistant", - content: newMessage[appendingIndex].message + // update the message with the full text + setMessages((prev) => { + return prev.map((message) => { + if (message.id === generateMessageId) { + return { + ...message, + message: fullText, + sources: source + } } - ]) - } else { - setHistory([ - ...history, - { - role: "assistant", - content: newMessage[appendingIndex].message - } - ]) - } + return message + }) + }) - if (historyId) { - if (!isRegenerate) { - await saveMessage(historyId, selectedModel!, "user", message, [image]) - } - await saveMessage( - historyId, - selectedModel!, - "assistant", - newMessage[appendingIndex].message, - [], - source - ) - } else { - const newHistoryId = await saveHistory(message) - await saveMessage(newHistoryId.id, selectedModel!, "user", message, [ + setHistory([ + ...history, + { + role: "user", + content: message, image - ]) - await saveMessage( - newHistoryId.id, - selectedModel!, - "assistant", - newMessage[appendingIndex].message, - [], - source - ) - setHistoryId(newHistoryId.id) - } + }, + { + role: "assistant", + content: fullText + } + ]) + + await saveMessageOnSuccess({ + historyId, + setHistoryId, + isRegenerate, + selectedModel: selectedModel, + message, + image, + fullText, + source + }) setIsProcessing(false) setStreaming(false) } catch (e) { - //@ts-ignore - if (e?.name === "AbortError") { - newMessage[appendingIndex].message = newMessage[ - appendingIndex - ].message.slice(0, -1) + const errorSave = await saveMessageOnError({ + e, + botMessage: fullText, + history, + historyId, + image, + selectedModel, + setHistory, + setHistoryId, + userMessage: message, + isRegenerating: isRegenerate + }) - setHistory([ - ...history, - { - role: "user", - content: message, - image - }, - { - role: "assistant", - content: newMessage[appendingIndex].message - } - ]) - - if (historyId) { - await saveMessage(historyId, selectedModel!, "user", message, [image]) - await saveMessage( - historyId, - selectedModel!, - "assistant", - newMessage[appendingIndex].message, - [] - ) - } else { - const newHistoryId = await saveHistory(message) - await saveMessage(newHistoryId.id, selectedModel!, "user", message, [ - image - ]) - await saveMessage( - newHistoryId.id, - selectedModel!, - "assistant", - newMessage[appendingIndex].message, - [] - ) - setHistoryId(newHistoryId.id) - } - } else { - //@ts-ignore + if (!errorSave) { notification.error({ message: t("error"), description: e?.message || t("somethingWentWrong") }) } - setIsProcessing(false) setStreaming(false) + } finally { + setAbortController(null) } } @@ -335,14 +298,14 @@ export const useMessageOption = () => { image: string, isRegenerate: boolean, messages: Message[], - history: ChatHistory + history: ChatHistory, + signal: AbortSignal ) => { const url = await getOllamaURL() if (image.length > 0) { image = `data:image/jpeg;base64,${image.split(",")[1]}` } - abortControllerRef.current = new AbortController() const ollama = new ChatOllama({ model: selectedModel!, @@ -350,6 +313,8 @@ export const useMessageOption = () => { }) let newMessage: Message[] = [] + let generateMessageId = generateID() + if (!isRegenerate) { newMessage = [ ...messages, @@ -364,7 +329,8 @@ export const useMessageOption = () => { isBot: true, name: selectedModel, message: "▋", - sources: [] + sources: [], + id: generateMessageId } ] } else { @@ -374,12 +340,14 @@ export const useMessageOption = () => { isBot: true, name: selectedModel, message: "▋", - sources: [] + sources: [], + id: generateMessageId } ] } setMessages(newMessage) - const appendingIndex = newMessage.length - 1 + let fullText = "" + let contentToSave = "" try { const prompt = await systemPromptForNonRagOption() @@ -441,132 +409,94 @@ export const useMessageOption = () => { const chunks = await ollama.stream( [...applicationChatHistory, humanMessage], { - signal: abortControllerRef.current.signal + signal: signal } ) let count = 0 for await (const chunk of chunks) { + contentToSave += chunk.content + fullText += chunk.content if (count === 0) { setIsProcessing(true) - newMessage[appendingIndex].message = chunk.content + "▋" - setMessages(newMessage) - } else { - newMessage[appendingIndex].message = - newMessage[appendingIndex].message.slice(0, -1) + - chunk.content + - "▋" - setMessages(newMessage) } - + setMessages((prev) => { + return prev.map((message) => { + if (message.id === generateMessageId) { + return { + ...message, + message: fullText.slice(0, -1) + "▋" + } + } + return message + }) + }) count++ } - newMessage[appendingIndex].message = newMessage[ - appendingIndex - ].message.slice(0, -1) - - if (!isRegenerate) { - setHistory([ - ...history, - { - role: "user", - content: message, - image - }, - { - role: "assistant", - content: newMessage[appendingIndex].message + setMessages((prev) => { + return prev.map((message) => { + if (message.id === generateMessageId) { + return { + ...message, + message: fullText.slice(0, -1) + } } - ]) - } else { - setHistory([ - ...history, - { - role: "assistant", - content: newMessage[appendingIndex].message - } - ]) - } + return message + }) + }) - if (historyId) { - if (!isRegenerate) { - await saveMessage(historyId, selectedModel, "user", message, [image]) - } - await saveMessage( - historyId, - selectedModel, - "assistant", - newMessage[appendingIndex].message, - [] - ) - } else { - const newHistoryId = await saveHistory(message) - await saveMessage(newHistoryId.id, selectedModel, "user", message, [ + setHistory([ + ...history, + { + role: "user", + content: message, image - ]) - await saveMessage( - newHistoryId.id, - selectedModel, - "assistant", - newMessage[appendingIndex].message, - [] - ) - setHistoryId(newHistoryId.id) - } + }, + { + role: "assistant", + content: fullText + } + ]) + + await saveMessageOnSuccess({ + historyId, + setHistoryId, + isRegenerate, + selectedModel: selectedModel, + message, + image, + fullText, + source: [] + }) setIsProcessing(false) setStreaming(false) + setIsProcessing(false) + setStreaming(false) } catch (e) { - if (e?.name === "AbortError") { - newMessage[appendingIndex].message = newMessage[ - appendingIndex - ].message.slice(0, -1) + const errorSave = await saveMessageOnError({ + e, + botMessage: fullText, + history, + historyId, + image, + selectedModel, + setHistory, + setHistoryId, + userMessage: message, + isRegenerating: isRegenerate + }) - setHistory([ - ...history, - { - role: "user", - content: message, - image - }, - { - role: "assistant", - content: newMessage[appendingIndex].message - } - ]) - - if (historyId) { - await saveMessage(historyId, selectedModel, "user", message, [image]) - await saveMessage( - historyId, - selectedModel, - "assistant", - newMessage[appendingIndex].message, - [] - ) - } else { - const newHistoryId = await saveHistory(message) - await saveMessage(newHistoryId.id, selectedModel, "user", message, [ - image - ]) - await saveMessage( - newHistoryId.id, - selectedModel, - "assistant", - newMessage[appendingIndex].message, - [] - ) - setHistoryId(newHistoryId.id) - } - } else { + if (!errorSave) { notification.error({ message: t("error"), description: e?.message || t("somethingWentWrong") }) } - setIsProcessing(false) setStreaming(false) + } finally { + setAbortController(null) } } @@ -575,22 +505,34 @@ export const useMessageOption = () => { image, isRegenerate = false, messages: chatHistory, - memory + memory, + controller }: { message: string image: string isRegenerate?: boolean messages?: Message[] memory?: ChatHistory + controller?: AbortController }) => { setStreaming(true) + let signal: AbortSignal + if (!controller) { + const newController = new AbortController() + signal = newController.signal + setAbortController(newController) + } else { + setAbortController(controller) + signal = controller.signal + } if (webSearch) { await searchChatMode( message, image, isRegenerate, chatHistory || messages, - memory || history + memory || history, + signal ) } else { await normalChatMode( @@ -598,7 +540,8 @@ export const useMessageOption = () => { image, isRegenerate, chatHistory || messages, - memory || history + memory || history, + signal ) } } @@ -611,28 +554,29 @@ export const useMessageOption = () => { } if (history.length > 0) { const lastMessage = history[history.length - 2] - let newHistory = history + let newHistory = history.slice(0, -2) let mewMessages = messages - newHistory.pop() mewMessages.pop() setHistory(newHistory) setMessages(mewMessages) await removeMessageUsingHistoryId(historyId) if (lastMessage.role === "user") { + const newController = new AbortController() await onSubmit({ message: lastMessage.content, image: lastMessage.image || "", isRegenerate: true, - memory: newHistory + memory: newHistory, + controller: newController }) } } } const stopStreamingRequest = () => { - if (abortControllerRef.current) { - abortControllerRef.current.abort() - abortControllerRef.current = null + if (abortController) { + abortController.abort() + setAbortController(null) } } @@ -653,7 +597,6 @@ export const useMessageOption = () => { message: string, isHuman: boolean ) => { - // update message and history by index let newMessages = messages let newHistory = history @@ -665,20 +608,21 @@ export const useMessageOption = () => { } const currentHumanMessage = newMessages[index] - newMessages[index].message = message - newHistory[index].content = message + const previousMessages = newMessages.slice(0, index + 1) setMessages(previousMessages) - const previousHistory = newHistory.slice(0, index + 1) + const previousHistory = newHistory.slice(0, index) setHistory(previousHistory) await updateMessageByIndex(historyId, index, message) await deleteChatForEdit(historyId, index) + const abortController = new AbortController() await onSubmit({ message: message, image: currentHumanMessage.images[0] || "", isRegenerate: true, messages: previousMessages, - memory: previousHistory + memory: previousHistory, + controller: abortController }) } else { newMessages[index].message = message diff --git a/src/i18n/index.ts b/src/i18n/index.ts index e85b63b..7c8820c 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -3,15 +3,20 @@ import { initReactI18next } from "react-i18next"; import { en } from "./lang/en"; import { ml } from "./lang/ml"; import { zh } from "./lang/zh"; +import { ja } from "./lang/ja"; +import LanguageDetector from 'i18next-browser-languagedetector'; i18n + .use(LanguageDetector) .use(initReactI18next) .init({ resources: { en: en, ml: ml, "zh-CN": zh, - zh: zh + zh: zh, + ja: ja, + "ja-JP": ja }, fallbackLng: "en", lng: localStorage.getItem("i18nextLng") || "en", diff --git a/src/i18n/lang/ja.ts b/src/i18n/lang/ja.ts new file mode 100644 index 0000000..2026585 --- /dev/null +++ b/src/i18n/lang/ja.ts @@ -0,0 +1,14 @@ +import option from "@/assets/locale/ja-JP/option.json"; +import playground from "@/assets/locale/ja-JP/playground.json"; +import common from "@/assets/locale/ja-JP/common.json"; +import sidepanel from "@/assets/locale/ja-JP/sidepanel.json"; +import settings from "@/assets/locale/ja-JP/settings.json"; + + +export const ja = { + option, + playground, + common, + sidepanel, + settings +} \ No newline at end of file diff --git a/src/i18n/support-language.ts b/src/i18n/support-language.ts index 04c1867..37a52ce 100644 --- a/src/i18n/support-language.ts +++ b/src/i18n/support-language.ts @@ -11,5 +11,9 @@ export const supportLanguage = [ { label: "简体中文", value: "zh-CN" + }, + { + label: "日本語", + value: "ja-JP" } ] \ No newline at end of file diff --git a/src/libs/db.ts b/src/libs/db.ts index 095c865..bab011a 100644 --- a/src/libs/db.ts +++ b/src/libs/db.ts @@ -6,6 +6,7 @@ import { type HistoryInfo = { id: string title: string + is_rag: boolean createdAt: number } @@ -31,7 +32,6 @@ type Message = { createdAt: number } - type Webshare = { id: string title: string @@ -41,7 +41,6 @@ type Webshare = { createdAt: number } - type Prompt = { id: string title: string @@ -125,7 +124,6 @@ export class PageAssitDatabase { await this.db.remove(history_id) } - async getAllPrompts(): Promise { return new Promise((resolve, reject) => { this.db.get("prompts", (result) => { @@ -146,7 +144,12 @@ export class PageAssitDatabase { this.db.set({ prompts: newPrompts }) } - async updatePrompt(id: string, title: string, content: string, is_system: boolean) { + async updatePrompt( + id: string, + title: string, + content: string, + is_system: boolean + ) { const prompts = await this.getAllPrompts() const newPrompts = prompts.map((prompt) => { if (prompt.id === id) { @@ -164,7 +167,6 @@ export class PageAssitDatabase { return prompts.find((prompt) => prompt.id === id) } - async getWebshare(id: string) { return new Promise((resolve, reject) => { this.db.get(id, (result) => { @@ -173,7 +175,6 @@ export class PageAssitDatabase { }) } - async getAllWebshares(): Promise { return new Promise((resolve, reject) => { this.db.get("webshares", (result) => { @@ -207,18 +208,17 @@ export class PageAssitDatabase { } } - -const generateID = () => { +export const generateID = () => { return "pa_xxxx-xxxx-xxx-xxxx".replace(/[x]/g, () => { const r = Math.floor(Math.random() * 16) return r.toString(16) }) } -export const saveHistory = async (title: string) => { +export const saveHistory = async (title: string, is_rag?: boolean) => { const id = generateID() const createdAt = Date.now() - const history = { id, title, createdAt } + const history = { id, title, createdAt, is_rag } const db = new PageAssitDatabase() await db.addChatHistory(history) return history @@ -230,11 +230,24 @@ export const saveMessage = async ( role: string, content: string, images: string[], - source?: any[] + source?: any[], + time?: number ) => { const id = generateID() - const createdAt = Date.now() - const message = { id, history_id, name, role, content, images, createdAt, sources: source } + let createdAt = Date.now() + if (time) { + createdAt += time + } + const message = { + id, + history_id, + name, + role, + content, + images, + createdAt, + sources: source + } const db = new PageAssitDatabase() await db.addMessage(message) return message @@ -292,19 +305,20 @@ export const removeMessageUsingHistoryId = async (history_id: string) => { await db.db.set({ [history_id]: chatHistory }) } - export const getAllPrompts = async () => { const db = new PageAssitDatabase() return await db.getAllPrompts() } - -export const updateMessageByIndex = async (history_id: string, index: number, message: string) => { +export const updateMessageByIndex = async ( + history_id: string, + index: number, + message: string +) => { const db = new PageAssitDatabase() const chatHistory = (await db.getChatHistory(history_id)).reverse() chatHistory[index].content = message await db.db.set({ [history_id]: chatHistory.reverse() }) - } export const deleteChatForEdit = async (history_id: string, index: number) => { @@ -315,7 +329,15 @@ export const deleteChatForEdit = async (history_id: string, index: number) => { await db.db.set({ [history_id]: previousHistory.reverse() }) } -export const savePrompt = async ({ content, title, is_system = false }: { title: string, content: string, is_system: boolean }) => { +export const savePrompt = async ({ + content, + title, + is_system = false +}: { + title: string + content: string + is_system: boolean +}) => { const db = new PageAssitDatabase() const id = generateID() const createdAt = Date.now() @@ -324,21 +346,28 @@ export const savePrompt = async ({ content, title, is_system = false }: { title: return prompt } - export const deletePromptById = async (id: string) => { const db = new PageAssitDatabase() await db.deletePrompt(id) return id } - -export const updatePrompt = async ({ content, id, title, is_system }: { id: string, title: string, content: string, is_system: boolean }) => { +export const updatePrompt = async ({ + content, + id, + title, + is_system +}: { + id: string + title: string + content: string + is_system: boolean +}) => { const db = new PageAssitDatabase() await db.updatePrompt(id, title, content, is_system) return id } - export const getPromptById = async (id: string) => { if (!id || id.trim() === "") return null const db = new PageAssitDatabase() @@ -354,10 +383,19 @@ export const deleteWebshare = async (id: string) => { const db = new PageAssitDatabase() await db.deleteWebshare(id) return id - } -export const saveWebshare = async ({ title, url, api_url, share_id }: { title: string, url: string, api_url: string, share_id: string }) => { +export const saveWebshare = async ({ + title, + url, + api_url, + share_id +}: { + title: string + url: string + api_url: string + share_id: string +}) => { const db = new PageAssitDatabase() const id = generateID() const createdAt = Date.now() @@ -368,7 +406,7 @@ export const saveWebshare = async ({ title, url, api_url, share_id }: { title: s export const getUserId = async () => { const db = new PageAssitDatabase() - const id = await db.getUserID() as string + const id = (await db.getUserID()) as string if (!id || id?.trim() === "") { const user_id = "user_xxxx-xxxx-xxx-xxxx-xxxx".replace(/[x]/g, () => { const r = Math.floor(Math.random() * 16) @@ -378,4 +416,4 @@ export const getUserId = async () => { return user_id } return id -} \ No newline at end of file +} diff --git a/src/public/_locales/ja/messages.json b/src/public/_locales/ja/messages.json new file mode 100644 index 0000000..569ec1a --- /dev/null +++ b/src/public/_locales/ja/messages.json @@ -0,0 +1,11 @@ +{ + "extName": { + "message": "Page Assist - ローカルAIモデル用のWeb UI" + }, + "extDescription": { + "message": "ローカルで実行中のAIモデルを使って、Webブラウジングをアシストします。" + }, + "openSidePanelToChat": { + "message": "サイドパネルを開いてチャット" + } + } \ No newline at end of file diff --git a/src/public/_locales/zh-CN/messages.json b/src/public/_locales/zh_CN/messages.json similarity index 99% rename from src/public/_locales/zh-CN/messages.json rename to src/public/_locales/zh_CN/messages.json index f777bfb..9b4fefe 100644 --- a/src/public/_locales/zh-CN/messages.json +++ b/src/public/_locales/zh_CN/messages.json @@ -8,4 +8,4 @@ "openSidePanelToChat": { "message": "打开侧边栏进行聊天" } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 87c6c4e..c474548 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -8,6 +8,8 @@ import { OptionPrompt } from "./option-settings-prompt" import { OptionOllamaSettings } from "./options-settings-ollama" import { OptionSettings } from "./option-settings" import { OptionShare } from "./option-settings-share" +import { OptionKnowledgeBase } from "./option-settings-knowledge" +import { OptionAbout } from "./option-settings-about" export const OptionRouting = () => { const { mode } = useDarkMode() @@ -21,6 +23,8 @@ export const OptionRouting = () => { } /> } /> } /> + } /> + } />
) diff --git a/src/routes/option-settings-about.tsx b/src/routes/option-settings-about.tsx new file mode 100644 index 0000000..982f1ea --- /dev/null +++ b/src/routes/option-settings-about.tsx @@ -0,0 +1,13 @@ +import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout" +import OptionLayout from "~/components/Layouts/Layout" +import { AboutApp } from "@/components/Option/Settings/about" + +export const OptionAbout = () => { + return ( + + + + + + ) +} diff --git a/src/routes/option-settings-knowledge.tsx b/src/routes/option-settings-knowledge.tsx new file mode 100644 index 0000000..0d8c573 --- /dev/null +++ b/src/routes/option-settings-knowledge.tsx @@ -0,0 +1,12 @@ +import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout" +import OptionLayout from "~/components/Layouts/Layout" + +export const OptionKnowledgeBase = () => { + return ( + + + hey + + + ) +} diff --git a/src/services/ollama.ts b/src/services/ollama.ts index 9a02c48..e8e65b1 100644 --- a/src/services/ollama.ts +++ b/src/services/ollama.ts @@ -296,19 +296,6 @@ export const setWebPrompts = async (prompt: string, followUpPrompt: string) => { 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 () => { const pageShareUrl = await storage.get("pageShareUrl") diff --git a/src/services/search.ts b/src/services/search.ts new file mode 100644 index 0000000..f3851b9 --- /dev/null +++ b/src/services/search.ts @@ -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) + ]) +} diff --git a/src/store/option.tsx b/src/store/option.tsx index 51e41c1..0de6c80 100644 --- a/src/store/option.tsx +++ b/src/store/option.tsx @@ -16,6 +16,7 @@ export type Message = { sources: any[] images?: string[] search?: WebSearch + id?: string } export type ChatHistory = { diff --git a/src/types/message.ts b/src/types/message.ts new file mode 100644 index 0000000..66e954e --- /dev/null +++ b/src/types/message.ts @@ -0,0 +1,18 @@ +type WebSearch = { + search_engine: string + search_url: string + search_query: string + search_results: { + title: string + link: string + }[] + } + export type Message = { + isBot: boolean + name: string + message: string + sources: any[] + images?: string[] + search?: WebSearch + id?: string + } \ No newline at end of file diff --git a/src/utils/search-provider.ts b/src/utils/search-provider.ts new file mode 100644 index 0000000..f23fb8f --- /dev/null +++ b/src/utils/search-provider.ts @@ -0,0 +1,10 @@ +export const SUPPORTED_SERACH_PROVIDERS = [ + { + label: "Google", + value: "google" + }, + { + label: "DuckDuckGo", + value: "duckduckgo" + } +] \ No newline at end of file diff --git a/src/web/local-duckduckgo.ts b/src/web/local-duckduckgo.ts new file mode 100644 index 0000000..51045d7 --- /dev/null +++ b/src/web/local-duckduckgo.ts @@ -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>[] = [] + 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 +} diff --git a/src/web/local-google.ts b/src/web/local-google.ts index 0f6f316..98fa2a8 100644 --- a/src/web/local-google.ts +++ b/src/web/local-google.ts @@ -1,3 +1,7 @@ +import { + getIsSimpleInternetSearch, + totalSearchResults +} from "@/services/search" import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama" import type { Document } from "@langchain/core/documents" import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" @@ -5,16 +9,13 @@ import { MemoryVectorStore } from "langchain/vectorstores/memory" import { cleanUrl } from "~/libs/clean-url" import { chromeRunTime } from "~/libs/runtime" 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) => { await chromeRunTime( @@ -40,23 +41,18 @@ export const localGoogleSearch = async (query: string) => { (result) => { const title = result.querySelector("h3")?.textContent 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 } } ) - const filteredSearchResults = searchResults - .filter( - (result) => - !result.link || - !BLOCKED_HOSTS.some((host) => result.link.includes(host)) - ) - .filter((result) => result.title && result.link) - return filteredSearchResults + return searchResults } - -export const webSearch = async (query: string) => { +export const webGoogleSearch = async (query: string) => { const results = await localGoogleSearch(query) + const TOTAL_SEARCH_RESULTS = await totalSearchResults() const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS) const isSimpleMode = await getIsSimpleInternetSearch() @@ -71,7 +67,7 @@ export const webSearch = async (query: string) => { }) } - const docs: Document>[] = []; + const docs: Document>[] = [] for (const result of searchResults) { const loader = new PageAssistHtmlLoader({ html: "", @@ -89,14 +85,14 @@ export const webSearch = async (query: string) => { const embeddingModle = await defaultEmbeddingModelForRag() const ollamaEmbedding = new OllamaEmbeddings({ model: embeddingModle || "", - baseUrl: cleanUrl(ollamaUrl), + baseUrl: cleanUrl(ollamaUrl) }) - const chunkSize = await defaultEmbeddingChunkSize(); - const chunkOverlap = await defaultEmbeddingChunkOverlap(); + const chunkSize = await defaultEmbeddingChunkSize() + const chunkOverlap = await defaultEmbeddingChunkOverlap() const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize, - chunkOverlap, + chunkOverlap }) const chunks = await textSplitter.splitDocuments(docs) @@ -105,7 +101,6 @@ export const webSearch = async (query: string) => { await store.addDocuments(chunks) - const resultsWithEmbeddings = await store.similaritySearch(query, 3) const searchResult = resultsWithEmbeddings.map((result) => { @@ -116,4 +111,4 @@ export const webSearch = async (query: string) => { }) return searchResult -} \ No newline at end of file +} diff --git a/src/web/web.ts b/src/web/web.ts index e1eccd3..83675e7 100644 --- a/src/web/web.ts +++ b/src/web/web.ts @@ -1,42 +1,61 @@ 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) => { - try { - const hostname = new URL(url).hostname - return hostname - } catch (e) { - return "" - } + try { + const hostname = new URL(url).hostname + return hostname + } catch (e) { + return "" + } +} + +const searchWeb = (provider: string, query: string) => { + switch (provider) { + case "duckduckgo": + return webDuckDuckGoSearch(query) + default: + return webGoogleSearch(query) + } } export const getSystemPromptForWeb = async (query: string) => { - try { - const search = await webSearch(query) + try { + const searchProvider = await getSearchProvider() + const search = await searchWeb(searchProvider, query) - const search_results = search.map((result, idx) => `${result.content}`).join("\n") + const search_results = search + .map( + (result, idx) => + `${result.content}` + ) + .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 { + prompt, + source: search.map((result) => { return { - prompt, - source: search.map((result) => { - return { - url: result.url, - name: getHostName(result.url), - type: "url", - } - }) - } - } catch (e) { - console.error(e) - return { - prompt: "", - source: [], + url: result.url, + name: getHostName(result.url), + type: "url" } + }) } -} \ No newline at end of file + } catch (e) { + console.error(e) + return { + prompt: "", + source: [] + } + } +} diff --git a/yarn.lock b/yarn.lock index 521c779..0ff8b4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1884,6 +1884,31 @@ charenc@0.0.2: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" 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: version "3.6.0" 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" selderee "^0.11.0" -htmlparser2@^8.0.2: +htmlparser2@^8.0.1, htmlparser2@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== @@ -4998,6 +5023,14 @@ parse5-htmlparser2-tree-adapter@^6.0.0: dependencies: 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: version "5.1.1" 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" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== -parse5@^7.1.1: +parse5@^7.0.0, parse5@^7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== @@ -6974,6 +7007,7 @@ winreg@0.0.12: integrity sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ== "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + name wrap-ansi-cjs version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==