Merge pull request #32 from n4ze3m/next

v1.1.1
This commit is contained in:
Muhammed Nazeem 2024-03-31 21:21:02 +05:30 committed by GitHub
commit 9a7b4f43d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 1618 additions and 441 deletions

View File

@ -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",

View File

@ -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"
}
}

View File

@ -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": "現在のページでチャット"
}

View File

@ -0,0 +1,12 @@
{
"newChat": "新しいチャット",
"selectAPrompt": "プロンプトを選択",
"githubRepository": "GitHubリポジトリ",
"settings": "設定",
"sidebarTitle": "チャット履歴",
"error": "エラー",
"somethingWentWrong": "何かが間違っています",
"validationSelectModel": "続行するにはモデルを選択してください",
"deleteHistoryConfirmation": "この履歴を削除しますか?",
"editHistoryTitle": "新しいタイトルを入力"
}

View File

@ -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キーを押すと送信"
}

View File

@ -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をここに入力することができます。<anchor>詳細</anchor>"
}
},
"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リポジトリ"
}
}

View File

@ -0,0 +1,5 @@
{
"tooltip": {
"embed": "ページを埋め込むのに数分かかる場合があります。しばらくお待ちください..."
}
}

View File

@ -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": "ഗിറ്റ്ഹബ്ബ് റെപ്പോസിറ്ററി"
}
}

View File

@ -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仓库"
}
}

View File

@ -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<Message[]>([])
const [controller, setController] = React.useState<AbortController | null>(
null
)
return (
<PageAssistContext.Provider
value={{
messages,
setMessages,
controller,
setController
}}>
{children}
</PageAssistContext.Provider>
)
}

View File

@ -0,0 +1,20 @@
import React from "react"
export const OllamaIcon = React.forwardRef<
SVGSVGElement,
React.SVGProps<SVGSVGElement>
>((props, ref) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
fillRule="evenodd"
viewBox="0 0 646 854"
ref={ref}
{...props}>
<path d="M140.629.24c-7.969 1.287-17.532 5.456-24.275 10.605-20.413 15.51-36.229 48.428-42.91 89.438-2.514 15.509-4.23 37.026-4.23 53.455 0 19.371 2.268 44.136 5.517 61.239.736 3.801 1.103 7.173.797 7.418-.245.245-3.25 2.697-6.62 5.394-11.525 9.195-24.705 23.356-33.778 36.291-17.41 24.704-28.688 52.78-33.409 83.185-1.839 12.015-2.33 36.29-.858 48.305 3.25 27.708 11.586 51.125 25.87 72.581l4.658 6.927-1.349 2.268c-9.563 16.061-17.716 39.294-21.516 61.607-3.004 17.655-3.372 22.375-3.372 46.037 0 23.847.307 28.567 3.127 45.057 3.371 19.739 10.237 40.642 17.9 54.558 2.513 4.536 8.643 13.976 9.378 14.467.246.122-.49 2.39-1.655 5.026-8.827 19.31-16.367 44.995-19.493 66.635-2.207 14.834-2.514 19.616-2.514 35.248 0 19.922 1.104 29.608 5.272 45.485l.613 2.329h52.535l-1.716-3.249c-10.605-19.616-11.586-56.029-2.452-92.38 4.168-16.797 8.888-29.118 17.716-46.099l5.272-10.298v-6.314c0-5.885-.123-6.559-2.023-10.421-1.472-2.943-3.433-5.456-6.927-8.889-5.947-5.762-10.238-11.831-13.67-19.31-15.08-32.735-18.023-81.346-7.418-122.786 4.414-17.287 11.709-32.673 19.371-41.071 5.21-5.763 7.908-12.199 7.908-18.881 0-6.927-2.452-12.628-7.97-18.574-15.815-16.919-25.562-37.517-29.056-61.485-4.965-34.145 4.046-71.355 24.52-100.84 20.046-28.935 48.183-47.509 79.631-52.474 7.049-1.165 20.229-.981 27.585.368 8.031 1.41 13.057.98 18.207-1.472 6.375-3.003 9.563-6.743 13.302-15.325 3.31-7.662 5.885-11.831 12.812-20.474 8.337-10.36 16.367-17.41 29.24-25.931 14.713-9.624 31.448-16.612 48.122-19.984 6.068-1.226 8.888-1.41 20.229-1.41s14.161.184 20.229 1.41c24.459 4.966 48.735 17.594 68.106 35.493 4.168 3.862 14.16 16.245 17.348 21.395 1.226 2.022 3.372 6.314 4.72 9.501 3.739 8.582 6.927 12.322 13.302 15.325 4.966 2.391 10.176 2.882 17.9 1.594 12.199-2.084 21.578-1.9 33.532.552 40.704 8.214 76.136 41.746 91.829 86.68 13.67 39.416 9.808 80.672-10.544 112.18-3.433 5.334-6.866 9.625-11.831 14.897-10.728 11.463-10.728 25.685-.061 37.455 17.532 19.187 28.505 66.389 25.194 108.012-2.206 27.463-9.256 52.045-18.942 65.96-1.716 2.452-5.271 6.62-7.969 9.195-3.494 3.433-5.455 5.946-6.927 8.889-1.9 3.862-2.023 4.536-2.023 10.421v6.314l5.272 10.298c8.828 16.981 13.548 29.302 17.716 46.099 9.012 35.861 8.215 71.538-2.084 91.829-.858 1.716-1.594 3.31-1.594 3.494 0 .184 11.709.306 26.053.306h25.992l.674-2.636c.368-1.409.981-3.555 1.287-4.781.675-2.697 2.023-10.666 3.127-18.329 1.042-7.724 1.042-36.168 0-44.75-3.923-31.141-10.483-55.845-21.21-79.201-1.165-2.636-1.901-4.904-1.656-5.026.307-.184 2.023-2.636 3.862-5.395 13.364-20.229 21.578-45.669 25.747-79.262 1.103-9.257 1.103-49.041 0-57.93-2.943-22.926-6.498-38.497-12.383-54.251-2.452-6.559-8.95-20.413-11.708-24.888l-1.349-2.268 4.659-6.927c14.283-21.456 22.62-44.873 25.869-72.581 1.471-12.015.981-36.29-.858-48.305-4.782-30.467-16-58.42-33.409-83.185-9.073-12.935-22.253-27.096-33.777-36.291-3.372-2.697-6.376-5.149-6.621-5.394-.306-.245.062-3.617.797-7.418 7.418-38.681 7.172-86.924-.613-124.625-6.743-32.857-19.003-58.971-34.819-74.051C523.209 4.286 510.336-.864 494.888.117c-35.432 2.085-63.998 42.85-75.278 107.093-1.839 10.36-3.432 22.498-3.432 25.808 0 1.287-.246 2.329-.552 2.329-.307 0-2.697-1.226-5.272-2.758-27.34-16.184-57.746-24.827-87.354-24.827-29.608 0-60.014 8.643-87.354 24.827-2.575 1.532-4.965 2.758-5.272 2.758-.306 0-.552-1.042-.552-2.329 0-3.433-1.655-15.938-3.432-25.808-10.238-57.684-33.716-95.875-64.918-105.499C157.181.424 144.982-.434 140.629.24zm10.422 49.899c8.827 6.988 18.635 26.972 24.275 49.347 1.042 4.046 2.145 8.705 2.452 10.421.245 1.656.919 5.395 1.471 8.276 2.391 12.996 3.494 27.034 3.617 44.137l.061 16.858-4.23 6.252-4.229 6.314h-9.87c-11.524 0-22.988 1.472-33.961 4.414-3.923.981-7.724 1.962-8.459 2.146-1.165.245-1.349-.123-2.023-5.15-3.617-27.279-3.433-57.5.552-82.634 4.413-28.014 14.712-53.393 24.765-60.871 2.391-1.778 2.82-1.717 5.579.49zm349.538-.43c6.069 4.476 12.751 16.368 17.716 31.57 9.992 30.406 12.812 72.152 7.54 111.875-.674 5.027-.858 5.395-2.023 5.15-.735-.184-4.536-1.165-8.459-2.146-10.973-2.942-22.437-4.414-33.961-4.414h-9.87l-4.229-6.314-4.23-6.252.061-16.858c.123-23.785 2.33-42.359 7.601-63.018 5.579-22.19 15.448-42.175 24.214-49.163 2.759-2.207 3.188-2.268 5.64-.43z"></path>
<path d="M313.498 358.237c-13.303 1.288-16.919 1.778-23.295 3.066-10.36 2.145-24.214 6.927-33.838 11.647-33.47 16.367-56.519 43.646-63.569 75.216-1.41 6.253-1.594 8.337-1.594 18.881 0 10.421.184 12.689 1.533 18.635 9.379 41.256 47.385 71.723 96.549 77.301 10.666 1.165 56.765 1.165 67.431 0 39.478-4.475 73.439-25.869 88.703-55.907 4.045-8.03 6.007-13.241 7.846-21.394 1.349-5.946 1.533-8.214 1.533-18.635 0-10.544-.184-12.628-1.594-18.881-10.238-45.853-54.742-81.959-109.3-88.825-7.111-.858-25.746-1.594-30.405-1.104zm22.926 33.348c18.207 1.962 36.536 8.46 51.248 18.268 7.908 5.272 19.065 16.306 23.846 23.54 5.885 8.949 9.256 18.083 10.789 29.179.674 5.088.307 8.95-1.533 17.164-2.881 12.26-11.831 25.072-23.907 34.022-5.64 4.107-17.348 10.054-24.52 12.383-13.609 4.352-22.498 5.149-54.252 4.904-20.719-.184-24.398-.368-30.344-1.471-20.29-3.801-36.351-11.893-47.998-24.214-9.441-9.931-13.732-19.003-16.061-33.654-1.042-6.805.919-18.084 4.904-27.586 4.843-11.586 17.348-25.991 29.731-34.267 14.344-9.563 33.225-16.367 50.573-18.206 6.682-.736 20.842-.736 27.524-.062z"></path>
<path d="M299.584 436.336c-4.659 2.513-7.908 8.888-6.927 13.608 1.103 5.088 5.578 10.238 12.566 14.468 3.74 2.268 3.985 2.574 4.169 4.842.122 1.349-.368 5.211-1.042 8.644-.736 3.371-1.288 6.927-1.288 7.908.062 2.636 2.514 6.927 5.088 9.011 2.269 1.839 2.698 1.9 9.073 2.084 5.824.184 7.05.061 9.379-1.042 6.008-2.943 7.54-8.337 5.333-18.697-1.839-8.643-1.471-9.992 3.127-12.628 4.842-2.82 9.992-7.785 11.524-11.157 2.943-6.436.245-13.731-6.253-17.103-1.593-.797-3.555-1.164-6.436-1.164-4.475 0-7.356 1.042-12.628 4.413l-3.004 1.901-1.9-1.165c-7.785-4.598-9.195-5.149-13.916-5.088-3.371 0-5.21.306-6.865 1.165zM150.744 365.165c-10.85 3.433-18.942 11.402-23.11 22.743-2.023 5.395-3.004 13.916-2.146 18.513 2.023 10.973 11.034 20.965 21.272 23.724 12.873 3.371 22.497 1.164 31.018-7.295 4.965-4.843 7.663-9.073 10.36-15.939 1.961-4.842 2.084-5.7 2.084-12.566l.061-7.356-2.574-5.272c-4.108-8.337-11.525-14.529-20.107-16.797-4.843-1.226-12.628-1.164-16.858.245zM478.153 364.982c-8.398 2.268-15.877 8.52-19.862 16.735l-2.574 5.272.061 7.356c0 6.866.123 7.724 2.084 12.566 2.698 6.866 5.395 11.096 10.36 15.939 8.521 8.459 18.145 10.666 31.019 7.295 7.417-1.962 14.834-8.215 18.39-15.51 3.065-6.191 3.8-10.666 2.82-17.716-2.268-16.122-11.709-27.83-25.747-31.937-4.107-1.226-12.076-1.226-16.551 0z"></path>
</svg>
)
})

View File

@ -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({
<div>
<div>
<div className="flex flex-col">
<div className="sticky top-0 z-[999] flex h-16 p-3 bg-white border-b dark:bg-[#171717] dark:border-gray-600">
<div className="sticky top-0 z-[999] flex h-16 p-3 bg-gray-50 border-b dark:bg-[#171717] dark:border-gray-600">
<div className="flex gap-2 items-center">
{pathname !== "/" && (
<div>
@ -94,7 +95,7 @@ export default function OptionLayout({
<div>
<button
onClick={clearChat}
className="inline-flex items-center rounded-lg border dark:border-gray-700 bg-transparent px-3 py-3 text-sm font-medium leading-4 text-gray-800 dark:text-white disabled:opacity-50 ">
className="inline-flex dark:bg-transparent bg-white items-center rounded-lg border dark:border-gray-700 bg-transparent px-3 py-3 text-sm font-medium leading-4 text-gray-800 dark:text-white disabled:opacity-50 ease-in-out transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-800 dark:hover:text-white">
<SquarePen className="h-4 w-4 mr-3" />
{t("newChat")}
</button>
@ -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: (
<span
key={model.model}
className="flex flex-row gap-3 items-center">
<OllamaIcon className="w-5 h-5" />
{model.name}
</span>
),
value: model.model
}))}
/>
@ -146,13 +153,13 @@ export default function OptionLayout({
label: (
<span
key={prompt.title}
className="flex flex-row justify-between items-center">
{prompt.title}
className="flex flex-row gap-3 items-center">
{prompt.is_system ? (
<ComputerIcon className="w-4 h-4" />
) : (
<ZapIcon className="w-4 h-4" />
)}
{prompt.title}
</span>
),
value: prompt.id
@ -166,8 +173,7 @@ export default function OptionLayout({
{pathname === "/" && messages.length > 0 && !streaming && (
<ShareBtn messages={messages} />
)}
<Tooltip title={t("githubRepository")}
>
<Tooltip title={t("githubRepository")}>
<a
href="https://github.com/n4ze3m/page-assist"
target="_blank"
@ -175,8 +181,7 @@ export default function OptionLayout({
<GithubIcon className="w-6 h-6" />
</a>
</Tooltip>
<Tooltip title={t("settings")}
>
<Tooltip title={t("settings")}>
<NavLink
to="/settings"
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">

View File

@ -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 }) => {
<LinkComponent
href="/settings/ollama"
name={t("ollamaSettings.title")}
icon={CircuitBoardIcon}
icon={OllamaIcon}
current={location.pathname}
/>
<LinkComponent
@ -73,6 +68,12 @@ export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
current={location.pathname}
icon={BrainCircuit}
/>
{/* <LinkComponent
href="/settings/knowledge"
name={t("manageKnowledge.title")}
icon={BlocksIcon}
current={location.pathname}
/> */}
<LinkComponent
href="/settings/prompt"
name={t("managePrompts.title")}
@ -85,6 +86,12 @@ export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
icon={Share}
current={location.pathname}
/>
<LinkComponent
href="/settings/about"
name={t("about.title")}
icon={InfoIcon}
current={location.pathname}
/>
</ul>
</nav>
</aside>

View File

@ -37,10 +37,10 @@ export const PlaygroundEmpty = () => {
return (
<div className="mx-auto sm:max-w-xl px-4 mt-10">
<div className="rounded-lg justify-center items-center flex flex-col border p-8 bg-white dark:bg-[#262626] dark:border-gray-600">
<div className="rounded-lg justify-center items-center flex flex-col border p-8 bg-gray-50 dark:bg-[#262626] dark:border-gray-600">
{(ollamaStatus === "pending" || isRefetching) && (
<div className="inline-flex items-center space-x-2">
<div className="w-3 h-3 bg-blue-500 rounded-full animate-bounce"></div>
<div className="w-3 h-3 bg-blue-500 rounded-full animate-pulse"></div>
<p className="dark:text-gray-400 text-gray-900">
{t("ollamaState.searching")}
</p>
@ -49,7 +49,7 @@ export const PlaygroundEmpty = () => {
{!isRefetching && ollamaStatus === "success" ? (
ollamaInfo.isOk ? (
<div className="inline-flex items-center space-x-2">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<div className="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
<p className="dark:text-gray-400 text-gray-900">
{t("ollamaState.running")}
</p>
@ -57,7 +57,7 @@ export const PlaygroundEmpty = () => {
) : (
<div className="flex flex-col space-y-2 justify-center items-center">
<div className="inline-flex space-x-2">
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
<div className="w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
<p className="dark:text-gray-400 text-gray-900">
{t("ollamaState.notRunning")}
</p>

View File

@ -153,7 +153,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
}
}
return (
<div className="px-3 pt-3 md:px-6 md:pt-6 md:bg-white dark:bg-[#262626] border rounded-t-xl dark:border-gray-600">
<div className="px-3 pt-3 md:px-6 md:pt-6 bg-gray-50 dark:bg-[#262626] border rounded-t-xl dark:border-gray-600">
<div
className={`h-full rounded-md shadow relative ${
form.values.image.length === 0 ? "hidden" : "block"
@ -176,7 +176,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
</div>
</div>
<div>
<div className="flex">
<div className="flex bg-white dark:bg-transparent">
<form
onSubmit={form.onSubmit(async (value) => {
if (!selectedModel || selectedModel.length === 0) {

View File

@ -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 (
<div className="flex flex-col space-y-3">
{status === "pending" && <Skeleton paragraph={{ rows: 4 }} active />}
{status === "success" && (
<div className="flex flex-col space-y-4">
<div>
<div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
{t("about.heading")}
</h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
</div>
</div>
<div>
<div className="flex flex-col space-y-6">
<div className="flex gap-6">
<span className="text-sm text-gray-500 dark:text-gray-400">
{t("about.chromeVersion")}
</span>
<span className="text-sm text-gray-900 dark:text-white">
{data.chromeVersion}
</span>
</div>
<div className="flex gap-6">
<span className="text-sm text-gray-500 dark:text-gray-400">
{t("about.ollamaVersion")}
</span>
<span className="text-sm text-gray-900 dark:text-white">
{data.ollama}
</span>
</div>
</div>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
{t("about.support")}
</p>
<div className="flex gap-2">
<a
href="https://ko-fi.com/n4ze3m"
target="_blank"
rel="noreferrer"
className="text-blue-500 dark:text-blue-400 border dark:border-gray-600 px-2.5 py-2 rounded-md">
{t("about.koFi")}
</a>
<a
href="https://github.com/sponsors/n4ze3m"
target="_blank"
rel="noreferrer"
className="text-blue-500 dark:text-blue-400 border dark:border-gray-600 px-2.5 py-2 rounded-md">
{t("about.githubSponsor")}
</a>
</div>
</div>
</div>
)}
</div>
)
}

View File

@ -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 (
<dl className="flex flex-col space-y-6 text-sm">
<div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
{t("generalSettings.heading")}
{t("generalSettings.settings.heading")}
</h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3"></div>
</div>
@ -38,7 +33,9 @@ export const SettingOther = () => {
</span>
<Select
placeholder={t("generalSettings.settings.speechRecognitionLang.placeholder")}
placeholder={t(
"generalSettings.settings.speechRecognitionLang.placeholder"
)}
allowClear
showSearch
options={SUPPORTED_LANGUAGES}
@ -86,33 +83,43 @@ export const SettingOther = () => {
) : (
<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>
</div>
<SearchModeSettings />
<div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-neutral-50 ">
{t("generalSettings.settings.deleteChatHistory.label")}
</span>
<div>
<div className="mb-5">
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
{t("generalSettings.system.heading")}
</h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3"></div>
</div>
<div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-neutral-50 ">
{t("generalSettings.system.deleteChatHistory.label")}
</span>
<button
onClick={async () => {
const confirm = window.confirm(
t("generalSettings.settings.deleteChatHistory.confirm")
)
<button
onClick={async () => {
const confirm = window.confirm(
t("generalSettings.system.deleteChatHistory.confirm")
)
if (confirm) {
const db = new PageAssitDatabase()
await db.deleteChatHistory()
queryClient.invalidateQueries({
queryKey: ["fetchChatHistory"]
})
clearChat()
}
}}
className="bg-red-500 dark:bg-red-600 text-white dark:text-gray-200 px-4 py-2 rounded-md">
{t("generalSettings.settings.deleteChatHistory.button")}
</button>
if (confirm) {
const db = new PageAssitDatabase()
await db.deleteChatHistory()
queryClient.invalidateQueries({
queryKey: ["fetchChatHistory"]
})
clearChat()
}
}}
className="bg-red-500 dark:bg-red-600 text-white dark:text-gray-200 px-4 py-2 rounded-md">
{t("generalSettings.system.deleteChatHistory.button")}
</button>
</div>
</div>
</dl>
)

View File

@ -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 { Skeleton, Switch } from "antd"
import { Select, Skeleton, Switch, InputNumber } from "antd"
import { useTranslation } from "react-i18next"
import {
getIsSimpleInternetSearch,
setIsSimpleInternetSearch
} from "~/services/ollama"
export const SearchModeSettings = () => {
const { t } = useTranslation("settings")
const { data, status } = useQuery({
queryKey: ["fetchIsSimpleInternetSearch"],
queryFn: () => getIsSimpleInternetSearch()
const queryClient = useQueryClient()
const form = useForm({
initialValues: {
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") {
return <Skeleton active />
}
return (
<div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-neutral-50 ">
{t("generalSettings.settings.searchMode.label")}
</span>
<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">
<span className="text-gray-500 dark:text-neutral-50 ">
{t("generalSettings.webSearch.provider.label")}
</span>
<Select
placeholder={t("generalSettings.webSearch.provider.placeholder")}
showSearch
style={{ width: "200px" }}
options={SUPPORTED_SERACH_PROVIDERS}
filterOption={(input, option) =>
option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||
option!.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
{...form.getInputProps("searchProvider")}
/>
</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>
<Switch
checked={data}
onChange={(checked) => {
setIsSimpleInternetSearch(checked)
queryClient.invalidateQueries({
queryKey: ["fetchIsSimpleInternetSearch"]
})
}}
/>
<div className="flex justify-end">
<SaveButton btnType="submit" />
</div>
</form>
</div>
)
}

26
src/context/index.tsx Normal file
View File

@ -0,0 +1,26 @@
import { Message } from "@/types/message"
import React, { Dispatch, SetStateAction, createContext } from "react"
interface PageAssistContext {
messages: Message[]
setMessages: Dispatch<SetStateAction<Message[]>>
controller: AbortController | null
setController: Dispatch<SetStateAction<AbortController>>
}
export const PageAssistContext = createContext<PageAssistContext>({
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
}

View File

@ -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!,
})
})
}

View File

@ -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")}
/>
)}
>
)}>
<StyleProvider hashPriority="high">
<QueryClientProvider client={queryClient}>
<OptionRouting />
<ToastContainer />
<PageAssistProvider>
<OptionRouting />
</PageAssistProvider>
</QueryClientProvider>
</StyleProvider>
</ConfigProvider>

View File

@ -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")}
/>
)}
>
)}>
<StyleProvider hashPriority="high">
<QueryClientProvider client={queryClient}>
<SidepanelRouting />
<ToastContainer />
<PageAssistProvider>
<SidepanelRouting />
</PageAssistProvider>
</QueryClientProvider>
</StyleProvider>
</ConfigProvider>

View File

@ -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)
}
}

View File

@ -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<HTMLTextAreaElement>(null)
const abortControllerRef = React.useRef<AbortController | null>(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

View File

@ -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",

14
src/i18n/lang/ja.ts Normal file
View File

@ -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
}

View File

@ -11,5 +11,9 @@ export const supportLanguage = [
{
label: "简体中文",
value: "zh-CN"
},
{
label: "日本語",
value: "ja-JP"
}
]

View File

@ -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<Prompts> {
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<Webshare[]> {
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
}
}

View File

@ -0,0 +1,11 @@
{
"extName": {
"message": "Page Assist - ローカルAIモデル用のWeb UI"
},
"extDescription": {
"message": "ローカルで実行中のAIモデルを使って、Webブラウジングをアシストします。"
},
"openSidePanelToChat": {
"message": "サイドパネルを開いてチャット"
}
}

View File

@ -8,4 +8,4 @@
"openSidePanelToChat": {
"message": "打开侧边栏进行聊天"
}
}
}

View File

@ -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 = () => {
<Route path="/settings/prompt" element={<OptionPrompt />} />
<Route path="/settings/ollama" element={<OptionOllamaSettings />} />
<Route path="/settings/share" element={<OptionShare />} />
<Route path="/settings/knowledge" element={<OptionKnowledgeBase />} />
<Route path="/settings/about" element={<OptionAbout />} />
</Routes>
</div>
)

View File

@ -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 (
<OptionLayout>
<SettingsLayout>
<AboutApp />
</SettingsLayout>
</OptionLayout>
)
}

View File

@ -0,0 +1,12 @@
import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
import OptionLayout from "~/components/Layouts/Layout"
export const OptionKnowledgeBase = () => {
return (
<OptionLayout>
<SettingsLayout>
hey
</SettingsLayout>
</OptionLayout>
)
}

View File

@ -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")

79
src/services/search.ts Normal file
View 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)
])
}

View File

@ -16,6 +16,7 @@ export type Message = {
sources: any[]
images?: string[]
search?: WebSearch
id?: string
}
export type ChatHistory = {

18
src/types/message.ts Normal file
View File

@ -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
}

View 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
View 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
}

View File

@ -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<Record<string, any>>[] = [];
const docs: Document<Record<string, any>>[] = []
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
}
}

View File

@ -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 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 {
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"
}
})
}
}
} catch (e) {
console.error(e)
return {
prompt: "",
source: []
}
}
}

View File

@ -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==