commit
7e796e7c58
@ -25,5 +25,6 @@
|
|||||||
"stopStreaming": "Stop Streaming",
|
"stopStreaming": "Stop Streaming",
|
||||||
"knowledge": "Viden"
|
"knowledge": "Viden"
|
||||||
},
|
},
|
||||||
"sendWhenEnter": "Søg, når Indtast trykkes"
|
"sendWhenEnter": "Søg, når Indtast trykkes",
|
||||||
|
"welcome": "Hej! Hvordan kan jeg hjælpe dig i dag?"
|
||||||
}
|
}
|
@ -32,6 +32,9 @@
|
|||||||
},
|
},
|
||||||
"generateTitle": {
|
"generateTitle": {
|
||||||
"label": "Generer titel med AI"
|
"label": "Generer titel med AI"
|
||||||
|
},
|
||||||
|
"ollamaStatus": {
|
||||||
|
"label": "Aktivér eller deaktivér Ollama forbindelsesstatus kontrol"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidepanelRag": {
|
"sidepanelRag": {
|
||||||
|
@ -25,5 +25,6 @@
|
|||||||
"stopStreaming": "Streaming stoppen",
|
"stopStreaming": "Streaming stoppen",
|
||||||
"knowledge": "Wissen"
|
"knowledge": "Wissen"
|
||||||
},
|
},
|
||||||
"sendWhenEnter": "Senden bei Drücken der Eingabetaste"
|
"sendWhenEnter": "Senden bei Drücken der Eingabetaste",
|
||||||
|
"welcome": "Hallo! Wie kann ich Ihnen heute helfen?"
|
||||||
}
|
}
|
@ -32,6 +32,9 @@
|
|||||||
},
|
},
|
||||||
"generateTitle": {
|
"generateTitle": {
|
||||||
"label": "Titel mit KI generieren"
|
"label": "Titel mit KI generieren"
|
||||||
|
},
|
||||||
|
"ollamaStatus": {
|
||||||
|
"label": "Ollama-Verbindungsstatus-Überprüfung aktivieren oder deaktivieren"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidepanelRag": {
|
"sidepanelRag": {
|
||||||
|
@ -23,7 +23,9 @@
|
|||||||
"speechToText": "Speech to Text",
|
"speechToText": "Speech to Text",
|
||||||
"uploadImage": "Upload Image",
|
"uploadImage": "Upload Image",
|
||||||
"stopStreaming": "Stop Streaming",
|
"stopStreaming": "Stop Streaming",
|
||||||
"knowledge": "Knowledge"
|
"knowledge": "Knowledge",
|
||||||
|
"vision": "[Experimental] Vision Chat"
|
||||||
},
|
},
|
||||||
"sendWhenEnter": "Send when Enter pressed"
|
"sendWhenEnter": "Send when Enter pressed",
|
||||||
|
"welcome": "Hello! How can I help you today?"
|
||||||
}
|
}
|
@ -32,6 +32,9 @@
|
|||||||
},
|
},
|
||||||
"generateTitle": {
|
"generateTitle": {
|
||||||
"label": "Generate Title using AI"
|
"label": "Generate Title using AI"
|
||||||
|
},
|
||||||
|
"ollamaStatus" :{
|
||||||
|
"label":"Enable or disable Ollama connection status check"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidepanelRag": {
|
"sidepanelRag": {
|
||||||
|
@ -25,5 +25,6 @@
|
|||||||
"stopStreaming": "Parar Transmisión",
|
"stopStreaming": "Parar Transmisión",
|
||||||
"knowledge": "Conocimiento"
|
"knowledge": "Conocimiento"
|
||||||
},
|
},
|
||||||
"sendWhenEnter": "Enviar cuando presione Enter"
|
"sendWhenEnter": "Enviar cuando presione Enter",
|
||||||
|
"welcome": "¡Hola! ¿Cómo puedo ayudarte hoy?"
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,9 @@
|
|||||||
},
|
},
|
||||||
"generateTitle": {
|
"generateTitle": {
|
||||||
"label": "Generar título usando IA"
|
"label": "Generar título usando IA"
|
||||||
|
},
|
||||||
|
"ollamaStatus": {
|
||||||
|
"label": "Habilitar o deshabilitar la verificación del estado de conexión de Ollama"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidepanelRag": {
|
"sidepanelRag": {
|
||||||
|
@ -25,5 +25,6 @@
|
|||||||
"stopStreaming": "توقف Streaming",
|
"stopStreaming": "توقف Streaming",
|
||||||
"knowledge": "دانش"
|
"knowledge": "دانش"
|
||||||
},
|
},
|
||||||
"sendWhenEnter": "با فشار دادن Enter ارسال شود"
|
"sendWhenEnter": "با فشار دادن Enter ارسال شود",
|
||||||
|
"welcome": "سلام! امروز چطور میتوانم به شما کمک کنم؟"
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,9 @@
|
|||||||
},
|
},
|
||||||
"generateTitle": {
|
"generateTitle": {
|
||||||
"label": "تولید عنوان با استفاده از هوش مصنوعی"
|
"label": "تولید عنوان با استفاده از هوش مصنوعی"
|
||||||
|
},
|
||||||
|
"ollamaStatus": {
|
||||||
|
"label": "فعال یا غیرفعال کردن بررسی وضعیت اتصال Ollama"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidepanelRag": {
|
"sidepanelRag": {
|
||||||
|
@ -25,5 +25,6 @@
|
|||||||
"stopStreaming": "Arrêtez la diffusion",
|
"stopStreaming": "Arrêtez la diffusion",
|
||||||
"knowledge": "Connaissance"
|
"knowledge": "Connaissance"
|
||||||
},
|
},
|
||||||
"sendWhenEnter": "Envoyer en appuyant sur Entrée"
|
"sendWhenEnter": "Envoyer en appuyant sur Entrée",
|
||||||
|
"welcome": "Bonjour ! Comment puis-je vous aider aujourd'hui ?"
|
||||||
}
|
}
|
@ -32,6 +32,9 @@
|
|||||||
},
|
},
|
||||||
"generateTitle": {
|
"generateTitle": {
|
||||||
"label": "Générer le titre en utilisant l'IA"
|
"label": "Générer le titre en utilisant l'IA"
|
||||||
|
},
|
||||||
|
"ollamaStatus": {
|
||||||
|
"label": "Activer ou désactiver la vérification de l'état de la connexion Ollama"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidepanelRag": {
|
"sidepanelRag": {
|
||||||
|
@ -25,5 +25,6 @@
|
|||||||
"stopStreaming": "Ferma lo Streaming",
|
"stopStreaming": "Ferma lo Streaming",
|
||||||
"knowledge": "Conoscenza"
|
"knowledge": "Conoscenza"
|
||||||
},
|
},
|
||||||
"sendWhenEnter": "Invia subito dopo Enter"
|
"sendWhenEnter": "Invia subito dopo Enter",
|
||||||
|
"welcome": "Ciao! Come posso aiutarti oggi?"
|
||||||
}
|
}
|
@ -32,6 +32,9 @@
|
|||||||
},
|
},
|
||||||
"generateTitle": {
|
"generateTitle": {
|
||||||
"label": "Genera titolo utilizzando l'IA"
|
"label": "Genera titolo utilizzando l'IA"
|
||||||
|
},
|
||||||
|
"ollamaStatus": {
|
||||||
|
"label": "Abilita o disabilita il controllo dello stato della connessione Ollama"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidepanelRag": {
|
"sidepanelRag": {
|
||||||
|
@ -25,5 +25,6 @@
|
|||||||
"stopStreaming": "ストリーミングを停止",
|
"stopStreaming": "ストリーミングを停止",
|
||||||
"knowledge": "知識"
|
"knowledge": "知識"
|
||||||
},
|
},
|
||||||
"sendWhenEnter": "Enterキーを押すと送信"
|
"sendWhenEnter": "Enterキーを押すと送信",
|
||||||
|
"welcome": "こんにちは!本日はどのようなお手伝いができますか?"
|
||||||
}
|
}
|
@ -35,6 +35,9 @@
|
|||||||
},
|
},
|
||||||
"generateTitle": {
|
"generateTitle": {
|
||||||
"label": "AIを使用してタイトルを生成"
|
"label": "AIを使用してタイトルを生成"
|
||||||
|
},
|
||||||
|
"ollamaStatus": {
|
||||||
|
"label": "Ollamaの接続状態チェックを有効または無効にする"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidepanelRag": {
|
"sidepanelRag": {
|
||||||
|
@ -25,5 +25,6 @@
|
|||||||
"stopStreaming": "스트리밍 중지",
|
"stopStreaming": "스트리밍 중지",
|
||||||
"knowledge": "지식"
|
"knowledge": "지식"
|
||||||
},
|
},
|
||||||
"sendWhenEnter": "Enter 키를 누르면 전송"
|
"sendWhenEnter": "Enter 키를 누르면 전송",
|
||||||
|
"welcome": "안녕하세요! 오늘 어떻게 도와드릴까요?"
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,9 @@
|
|||||||
},
|
},
|
||||||
"generateTitle": {
|
"generateTitle": {
|
||||||
"label": "AI로 제목 생성"
|
"label": "AI로 제목 생성"
|
||||||
|
},
|
||||||
|
"ollamaStatus": {
|
||||||
|
"label": "Ollama 연결 상태 확인 활성화 또는 비활성화"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidepanelRag": {
|
"sidepanelRag": {
|
||||||
@ -342,4 +345,3 @@
|
|||||||
"title": "Chrome AI 설정"
|
"title": "Chrome AI 설정"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,5 +25,6 @@
|
|||||||
"stopStreaming": "സ്ട്രീമിംഗ് നിർത്തുക",
|
"stopStreaming": "സ്ട്രീമിംഗ് നിർത്തുക",
|
||||||
"knowledge": "അറിവ്"
|
"knowledge": "അറിവ്"
|
||||||
},
|
},
|
||||||
"sendWhenEnter": "എന്റര് അമര്ത്തുമ്പോള് അയയ്ക്കുക"
|
"sendWhenEnter": "എന്റര് അമര്ത്തുമ്പോള് അയയ്ക്കുക",
|
||||||
|
"welcome": "നമസ്കാരം! ഇന്ന് എനിക്ക് നിങ്ങളെ എങ്ങനെ സഹായിക്കാൻ കഴിയും?"
|
||||||
}
|
}
|
@ -35,6 +35,9 @@
|
|||||||
},
|
},
|
||||||
"generateTitle": {
|
"generateTitle": {
|
||||||
"label": "എഐ ഉപയോഗിച്ച് ശീർഷകം സൃഷ്ടിക്കുക"
|
"label": "എഐ ഉപയോഗിച്ച് ശീർഷകം സൃഷ്ടിക്കുക"
|
||||||
|
},
|
||||||
|
"ollamaStatus": {
|
||||||
|
"label": "ഒല്ലാമ കണക്ഷൻ സ്റ്റാറ്റസ് പരിശോധന പ്രവർത്തനക്ഷമമാക്കുകയോ പ്രവർത്തനരഹിതമാക്കുകയോ ചെയ്യുക"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidepanelRag": {
|
"sidepanelRag": {
|
||||||
|
@ -25,5 +25,6 @@
|
|||||||
"stopStreaming": "Stopp Streaming",
|
"stopStreaming": "Stopp Streaming",
|
||||||
"knowledge": "Kunnskap"
|
"knowledge": "Kunnskap"
|
||||||
},
|
},
|
||||||
"sendWhenEnter": "Søk når Enter trykkes"
|
"sendWhenEnter": "Søk når Enter trykkes",
|
||||||
|
"welcome": "Hei! Hvordan kan jeg hjelpe deg i dag?"
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,9 @@
|
|||||||
},
|
},
|
||||||
"generateTitle": {
|
"generateTitle": {
|
||||||
"label": "Generer tittel med AI"
|
"label": "Generer tittel med AI"
|
||||||
|
},
|
||||||
|
"ollamaStatus": {
|
||||||
|
"label": "Aktiver eller deaktiver Ollama tilkoblingsstatussjekk"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidepanelRag": {
|
"sidepanelRag": {
|
||||||
|
@ -25,5 +25,6 @@
|
|||||||
"stopStreaming": "Parar Streaming",
|
"stopStreaming": "Parar Streaming",
|
||||||
"knowledge": "Conhecimento"
|
"knowledge": "Conhecimento"
|
||||||
},
|
},
|
||||||
"sendWhenEnter": "Enviar ao pressionar Enter"
|
"sendWhenEnter": "Enviar ao pressionar Enter",
|
||||||
|
"welcome": "Olá! Como posso ajudar você hoje?"
|
||||||
}
|
}
|
@ -32,6 +32,9 @@
|
|||||||
},
|
},
|
||||||
"generateTitle": {
|
"generateTitle": {
|
||||||
"label": "Gerar título usando IA"
|
"label": "Gerar título usando IA"
|
||||||
|
},
|
||||||
|
"ollamaStatus": {
|
||||||
|
"label": "Ativar ou desativar verificação de status da conexão Ollama"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidepanelRag": {
|
"sidepanelRag": {
|
||||||
|
@ -25,5 +25,6 @@
|
|||||||
"stopStreaming": "Остановить поток",
|
"stopStreaming": "Остановить поток",
|
||||||
"knowledge": "Знание"
|
"knowledge": "Знание"
|
||||||
},
|
},
|
||||||
"sendWhenEnter": "Отправить при нажатии клавиши Enter"
|
"sendWhenEnter": "Отправить при нажатии клавиши Enter",
|
||||||
|
"welcome": "Здравствуйте! Как я могу помочь вам сегодня?"
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,9 @@
|
|||||||
},
|
},
|
||||||
"generateTitle": {
|
"generateTitle": {
|
||||||
"label": "Сгенерировать заголовок с помощью ИИ"
|
"label": "Сгенерировать заголовок с помощью ИИ"
|
||||||
|
},
|
||||||
|
"ollamaStatus": {
|
||||||
|
"label": "Включить или отключить проверку состояния подключения Ollama"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidepanelRag": {
|
"sidepanelRag": {
|
||||||
|
@ -25,5 +25,6 @@
|
|||||||
"stopStreaming": "Stoppa strömning",
|
"stopStreaming": "Stoppa strömning",
|
||||||
"knowledge": "Kunskap"
|
"knowledge": "Kunskap"
|
||||||
},
|
},
|
||||||
"sendWhenEnter": "Skicka när Enter trycks"
|
"sendWhenEnter": "Skicka när Enter trycks",
|
||||||
|
"welcome": "Hej! Hur kan jag hjälpa dig idag?"
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,9 @@
|
|||||||
},
|
},
|
||||||
"generateTitle": {
|
"generateTitle": {
|
||||||
"label": "Generera titel med AI"
|
"label": "Generera titel med AI"
|
||||||
|
},
|
||||||
|
"ollamaStatus": {
|
||||||
|
"label": "Aktivera eller inaktivera Ollama anslutningsstatuskontroll"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidepanelRag": {
|
"sidepanelRag": {
|
||||||
|
13
src/assets/locale/uk/chrome.json
Normal file
13
src/assets/locale/uk/chrome.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"heading": "Налаштування Chrome AI",
|
||||||
|
"status": {
|
||||||
|
"label": "Ввімкнути або вимкнути підтримку Chrome AI на Page Assist"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"browser_not_supported": "Ця версія Chrome не підтримується моделлю Gemini Nano. Будь ласка, оновіть до версії 127 або новішої",
|
||||||
|
"ai_not_supported": "Налаштування chrome://flags/#prompt-api-for-gemini-nano не увімкнено. Будь ласка, увімкніть його.",
|
||||||
|
"ai_not_ready": "Модель Gemini Nano ще не готова; вам потрібно перевірити налаштування Chrome знову",
|
||||||
|
"internal_error": "Стався внутрішній збій. Будь ласка, спробуйте пізніше."
|
||||||
|
},
|
||||||
|
"errorDescription": "Щоб використовувати Chrome AI, вам потрібна версія переглядача новіша за 127-му, яка наразі доступна в каналах Dev і Canary. Після завантаження підтримуваної версії виконайте наступні кроки:\n\n1. Перейдіть до `chrome://flags/#prompt-api-for-gemini-nano` та виберіть \"Ввімкнути\".\n2. Перейдіть до `chrome://flags/#optimization-guide-on-device-model` та виберіть \"EnabledBypassPrefRequirement\".\n3. Перейдіть до `chrome://components`, знайдіть \"Optimization Guide On Device Model\" і натисніть \"Перевірити оновлення\". Це завантажить модель. Якщо ви не бачите налаштувань, повторіть кроки 1 і 2 та перезапустіть переглядач."
|
||||||
|
}
|
125
src/assets/locale/uk/common.json
Normal file
125
src/assets/locale/uk/common.json
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"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": "Редагувати",
|
||||||
|
"delete": "Видалити",
|
||||||
|
"saveAndSubmit": "Зберегти та надіслати",
|
||||||
|
"editMessage": {
|
||||||
|
"placeholder": "Введіть повідомлення..."
|
||||||
|
},
|
||||||
|
"submit": "Надіслати",
|
||||||
|
"noData": "Немає даних",
|
||||||
|
"noHistory": "Немає історії чату",
|
||||||
|
"chatWithCurrentPage": "Чат з поточної сторінки",
|
||||||
|
"beta": "Бета",
|
||||||
|
"tts": "Читання вголос",
|
||||||
|
"currentChatModelSettings": "Налаштування поточної моделі чату",
|
||||||
|
"modelSettings": {
|
||||||
|
"label": "Налаштування моделі",
|
||||||
|
"description": "Встановіть глобальні параметри моделі для всіх чатів",
|
||||||
|
"form": {
|
||||||
|
"keepAlive": {
|
||||||
|
"label": "Тривалість збереження в памʼяті",
|
||||||
|
"help": "вказує, як довго модель залишатиметься завантаженою у памʼять після запиту (за замовчуванням: 5 хв)",
|
||||||
|
"placeholder": "Введіть тривалість збереження у памʼяті (наприклад, 5m, 10m, 1h)"
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"label": "Температура",
|
||||||
|
"placeholder": "Введіть значення температури (наприклад, 0.7, 1.0)"
|
||||||
|
},
|
||||||
|
"numCtx": {
|
||||||
|
"label": "Кількість контекстів",
|
||||||
|
"placeholder": "Введіть кількість контекстів (типово: 2048)"
|
||||||
|
},
|
||||||
|
"numPredict": {
|
||||||
|
"label": "Максимальна кількість токенів",
|
||||||
|
"placeholder": "Введіть максимальну кількість токенів (наприклад, 2048, 4096)"
|
||||||
|
},
|
||||||
|
"seed": {
|
||||||
|
"label": "Зерно",
|
||||||
|
"placeholder": "Введіть значення зерна (наприклад, 1234)",
|
||||||
|
"help": "Повторюваність виводу моделі"
|
||||||
|
},
|
||||||
|
"topK": {
|
||||||
|
"label": "Top K",
|
||||||
|
"placeholder": "Введіть значення для Верхніх K (наприклад, 40, 100)"
|
||||||
|
},
|
||||||
|
"topP": {
|
||||||
|
"label": "Top P",
|
||||||
|
"placeholder": "Введіть значення для Верхнього P (наприклад, 0.9, 0.95)"
|
||||||
|
},
|
||||||
|
"numGpu": {
|
||||||
|
"label": "Кількість GPU",
|
||||||
|
"placeholder": "Введіть кількість шарів для відправки на GPU"
|
||||||
|
},
|
||||||
|
"systemPrompt": {
|
||||||
|
"label": "Тимчасовий системний запит",
|
||||||
|
"placeholder": "Введіть системний запит",
|
||||||
|
"help": "Швидкий спосіб встановити системний запит для поточного чату, який замінить вибраний системний запит, якщо він існує."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"advanced": "Додаткові налаштування моделі"
|
||||||
|
},
|
||||||
|
"copilot": {
|
||||||
|
"summary": "Підсумувати",
|
||||||
|
"explain": "Пояснити",
|
||||||
|
"rephrase": "Перефразувати",
|
||||||
|
"translate": "Перекласти",
|
||||||
|
"custom": "Власне"
|
||||||
|
},
|
||||||
|
"citations": "Цитати",
|
||||||
|
"segmented": {
|
||||||
|
"ollama": "Моделі Ollama",
|
||||||
|
"custom": "Власні моделі"
|
||||||
|
},
|
||||||
|
"downloadCode": "Завантажити код",
|
||||||
|
"date": {
|
||||||
|
"pinned": "Прикріплено",
|
||||||
|
"today": "Сьогодні",
|
||||||
|
"yesterday": "Вчора",
|
||||||
|
"last7Days": "Останні 7 днів",
|
||||||
|
"older": "Старіше"
|
||||||
|
},
|
||||||
|
"pin": "Прикріпити",
|
||||||
|
"unpin": "Відкріпити",
|
||||||
|
"generationInfo": "Інформація про генерацію"
|
||||||
|
}
|
40
src/assets/locale/uk/knowledge.json
Normal file
40
src/assets/locale/uk/knowledge.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"addBtn": "Додати нове знання",
|
||||||
|
"columns": {
|
||||||
|
"title": "Назва",
|
||||||
|
"status": "Статус",
|
||||||
|
"embeddings": "Модель вкладень",
|
||||||
|
"createdAt": "Створено в",
|
||||||
|
"action": "Дії"
|
||||||
|
},
|
||||||
|
"expandedColumns": {
|
||||||
|
"name": "Імʼя"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"delete": "Ви впевнені, що хочете видалити це знання?"
|
||||||
|
},
|
||||||
|
"deleteSuccess": "Знання успішно видалено",
|
||||||
|
"status": {
|
||||||
|
"pending": "Очікування",
|
||||||
|
"finished": "Завершено",
|
||||||
|
"processing": "Обробка",
|
||||||
|
"failed": "Не вдалося"
|
||||||
|
},
|
||||||
|
"addKnowledge": "Додати знання",
|
||||||
|
"form": {
|
||||||
|
"title": {
|
||||||
|
"label": "Назва знань",
|
||||||
|
"placeholder": "Введіть назву знань",
|
||||||
|
"required": "Назва знань обовʼязкова"
|
||||||
|
},
|
||||||
|
"uploadFile": {
|
||||||
|
"label": "Завантажити файл",
|
||||||
|
"uploadText": "Перетягніть файл сюди або натисніть для завантаження",
|
||||||
|
"uploadHint": "Підтримуються типи файлів: .pdf, .csv, .txt, .md, .docx",
|
||||||
|
"required": "Файл обовʼязковий"
|
||||||
|
},
|
||||||
|
"submit": "Надіслати",
|
||||||
|
"success": "Знання успішно додано"
|
||||||
|
},
|
||||||
|
"noEmbeddingModel": "Будь ласка, додайте модель вкладень з сторінки налаштувань RAG спочатку"
|
||||||
|
}
|
90
src/assets/locale/uk/openai.json
Normal file
90
src/assets/locale/uk/openai.json
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"settings": "Сумісний з OpenAI API",
|
||||||
|
"heading": "Сумісний з OpenAI API",
|
||||||
|
"subheading": "Тут керування та налаштування ваших постачальників OpenAI API.",
|
||||||
|
"addBtn": "Додати постачальника",
|
||||||
|
"table": {
|
||||||
|
"name": "Назва постачальника",
|
||||||
|
"baseUrl": "Базовий URL",
|
||||||
|
"actions": "Дії"
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"titleAdd": "Додати нового постачальника",
|
||||||
|
"name": {
|
||||||
|
"label": "Назва постачальника",
|
||||||
|
"required": "Назва постачальника обовʼязкова.",
|
||||||
|
"placeholder": "Введіть назву постачальника"
|
||||||
|
},
|
||||||
|
"baseUrl": {
|
||||||
|
"label": "Базовий URL",
|
||||||
|
"help": "Базовий URL постачальника OpenAI API. Наприклад, (http://localhost:1234/v1)",
|
||||||
|
"required": "Базовий URL обовʼязковий.",
|
||||||
|
"placeholder": "Введіть базовий URL"
|
||||||
|
},
|
||||||
|
"apiKey": {
|
||||||
|
"label": "Ключ API",
|
||||||
|
"required": "Ключ API обовʼязковий.",
|
||||||
|
"placeholder": "Введіть ключ API"
|
||||||
|
},
|
||||||
|
"submit": "Зберегти",
|
||||||
|
"update": "Оновити",
|
||||||
|
"deleteConfirm": "Ви впевнені, що хочете видалити цього постачальника?",
|
||||||
|
"model": {
|
||||||
|
"title": "Список моделей",
|
||||||
|
"subheading": "Виберіть чат-моделі, які ви хочете використовувати з цим постачальником.",
|
||||||
|
"success": "Моделі успішно додано."
|
||||||
|
},
|
||||||
|
"tipLMStudio": "Page Assist автоматично отримає моделі, завантажені в LM Studio. Вам не потрібно додавати їх вручну."
|
||||||
|
},
|
||||||
|
"addSuccess": "Постачальника додано успішно.",
|
||||||
|
"deleteSuccess": "Постачальника видалено успішно.",
|
||||||
|
"updateSuccess": "Постачальника оновлено успішно.",
|
||||||
|
"delete": "Видалити",
|
||||||
|
"edit": "Редагувати",
|
||||||
|
"newModel": "Додати моделі до постачальника",
|
||||||
|
"noNewModel": "Для LMStudio, Ollama, Llamafile ми отримуємо моделі динамічно. Ручного додавання не потрібно.",
|
||||||
|
"searchModel": "Пошук моделі",
|
||||||
|
"selectAll": "Вибрати все",
|
||||||
|
"save": "Зберегти",
|
||||||
|
"saving": "Збереження...",
|
||||||
|
"manageModels": {
|
||||||
|
"columns": {
|
||||||
|
"name": "Назва моделі",
|
||||||
|
"model_type": "Тип моделі",
|
||||||
|
"model_id": "Ідентифікатор моделі",
|
||||||
|
"provider": "Назва постачальника",
|
||||||
|
"actions": "Дії"
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"delete": "Видалити"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"delete": "Ви впевнені, що хочете видалити цю модель?"
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"title": "Додати власну модель",
|
||||||
|
"form": {
|
||||||
|
"name": {
|
||||||
|
"label": "Ідентифікатор моделі",
|
||||||
|
"placeholder": "llama3.2",
|
||||||
|
"required": "Ідентифікатор моделі обовʼязковий."
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"label": "Постачальник",
|
||||||
|
"placeholder": "Виберіть постачальника",
|
||||||
|
"required": "Постачальник обовʼязковий."
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"label": "Тип моделі"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"noModelFound": "Не знайдено моделей. Переконайтеся, що ви додали правильного постачальника з базовим URL та ключем API.",
|
||||||
|
"radio": {
|
||||||
|
"chat": "Модель чату",
|
||||||
|
"embedding": "Модель вкладень",
|
||||||
|
"chatInfo": "використовується для завершення чату та генерації розмови",
|
||||||
|
"embeddingInfo": "використовується для RAG та інших повʼязаних завдань семантичного пошуку."
|
||||||
|
}
|
||||||
|
}
|
13
src/assets/locale/uk/option.json
Normal file
13
src/assets/locale/uk/option.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"newChat": "Новий чат",
|
||||||
|
"selectAPrompt": "Виберіть запит",
|
||||||
|
"githubRepository": "Репозиторій GitHub",
|
||||||
|
"settings": "Налаштування",
|
||||||
|
"sidebarTitle": "Історія чату",
|
||||||
|
"error": "Збій",
|
||||||
|
"somethingWentWrong": "Щось пішло не так",
|
||||||
|
"validationSelectModel": "Будь ласка, виберіть модель для продовження",
|
||||||
|
"deleteHistoryConfirmation": "Ви впевнені, що хочете видалити цю історію?",
|
||||||
|
"editHistoryTitle": "Введіть нову назву",
|
||||||
|
"temporaryChat": "Тимчасовий чат"
|
||||||
|
}
|
30
src/assets/locale/uk/playground.json
Normal file
30
src/assets/locale/uk/playground.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"ollamaState": {
|
||||||
|
"searching": "Пошук вашого Ollama 🦙",
|
||||||
|
"running": "Ollama працює 🦙",
|
||||||
|
"notRunning": "Неможливо підключитися до Ollama 🦙",
|
||||||
|
"connectionError": "Здається, у вас виникла проблема з підключенням. Будь ласка, зверніться до цієї документації<anchor> для усунення несправностей.</anchor>"
|
||||||
|
},
|
||||||
|
"formError": {
|
||||||
|
"noModel": "Виберіть модель",
|
||||||
|
"noEmbeddingModel": "Налаштуйте модель вкладень на сторінці Налаштування > RAG"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"textarea": {
|
||||||
|
"placeholder": "Введіть повідомлення..."
|
||||||
|
},
|
||||||
|
"webSearch": {
|
||||||
|
"on": "Увімкнено",
|
||||||
|
"off": "Вимкнено"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"searchInternet": "Пошук в Інтернеті",
|
||||||
|
"speechToText": "Голос у текст",
|
||||||
|
"uploadImage": "Завантажити зображення",
|
||||||
|
"stopStreaming": "Зупинити трансляцію",
|
||||||
|
"knowledge": "Знання"
|
||||||
|
},
|
||||||
|
"sendWhenEnter": "Надсилати при натисканні Enter",
|
||||||
|
"welcome": "Вітаю! Як я можу допомогти вам сьогодні?"
|
||||||
|
}
|
344
src/assets/locale/uk/settings.json
Normal file
344
src/assets/locale/uk/settings.json
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
{
|
||||||
|
"generalSettings": {
|
||||||
|
"title": "Загальні налаштування",
|
||||||
|
"settings": {
|
||||||
|
"heading": "Налаштування веб-інтерфейсу",
|
||||||
|
"speechRecognitionLang": {
|
||||||
|
"label": "Мова для розпізнавання голосу",
|
||||||
|
"placeholder": "Виберіть мову"
|
||||||
|
},
|
||||||
|
"language": {
|
||||||
|
"label": "Мова інтерфейсу",
|
||||||
|
"placeholder": "Виберіть мову"
|
||||||
|
},
|
||||||
|
"darkMode": {
|
||||||
|
"label": "Змінити тему",
|
||||||
|
"options": {
|
||||||
|
"light": "Світла",
|
||||||
|
"dark": "Темна"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"copilotResumeLastChat": {
|
||||||
|
"label": "Поновити останню розмову при відкритті бічної панелі (Copilot)"
|
||||||
|
},
|
||||||
|
"hideCurrentChatModelSettings": {
|
||||||
|
"label": "Приховати налаштування поточної моделі чату"
|
||||||
|
},
|
||||||
|
"restoreLastChatModel": {
|
||||||
|
"label": "Відновити останню використану модель для попередніх чатів"
|
||||||
|
},
|
||||||
|
"sendNotificationAfterIndexing": {
|
||||||
|
"label": "Надсилати сповіщення після завершення обробки бази знань"
|
||||||
|
},
|
||||||
|
"generateTitle": {
|
||||||
|
"label": "Створювати заголовок за допомогою AI"
|
||||||
|
},
|
||||||
|
"ollamaStatus": {
|
||||||
|
"label": "Увімкнути або вимкнути перевірку стану з'єднання Ollama"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidepanelRag": {
|
||||||
|
"heading": "Чат з Copilot з налаштуваннями веб-сайту",
|
||||||
|
"ragEnabled": {
|
||||||
|
"label": "Чат з веб-сайтом за допомогою векторних вкладень"
|
||||||
|
},
|
||||||
|
"maxWebsiteContext": {
|
||||||
|
"label": "Розмір вмісту нормального режиму",
|
||||||
|
"placeholder": "Розмір вмісту (типово 4028)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webSearch": {
|
||||||
|
"heading": "Управління веб-пошуком",
|
||||||
|
"searchMode": {
|
||||||
|
"label": "Виконувати простий пошук в Інтернеті"
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"label": "Пошукова система",
|
||||||
|
"placeholder": "Виберіть пошукову систему"
|
||||||
|
},
|
||||||
|
"totalSearchResults": {
|
||||||
|
"label": "Загальна кількість результатів пошуку",
|
||||||
|
"placeholder": "Введіть загальну кількість результатів пошуку"
|
||||||
|
},
|
||||||
|
"visitSpecificWebsite": {
|
||||||
|
"label": "Відвідати веб-сайт, згаданий у повідомленні"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"system": {
|
||||||
|
"heading": "Системні налаштування",
|
||||||
|
"deleteChatHistory": {
|
||||||
|
"label": "Видалити історію чату",
|
||||||
|
"button": "Видалити",
|
||||||
|
"confirm": "Ви впевнені, що хочете видалити історію чату? Ця дія не може бути відвернута."
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"label": "Експорт історії чату, бази знань та запитів",
|
||||||
|
"button": "Експортувати дані",
|
||||||
|
"success": "Експорт успішний"
|
||||||
|
},
|
||||||
|
"import": {
|
||||||
|
"label": "Імпорт історії чату, бази знань, та запитів",
|
||||||
|
"button": "Імпортувати дані",
|
||||||
|
"success": "Імпорт успішний",
|
||||||
|
"error": "Помилка імпорту"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tts": {
|
||||||
|
"heading": "Налаштування Текст-у-Голос",
|
||||||
|
"ttsEnabled": {
|
||||||
|
"label": "Увімкнути Текст-у-Голос"
|
||||||
|
},
|
||||||
|
"ttsProvider": {
|
||||||
|
"label": "Поставник Текст-у-Голос",
|
||||||
|
"placeholder": "Виберіть постачальника"
|
||||||
|
},
|
||||||
|
"ttsVoice": {
|
||||||
|
"label": "Голос Текст-у-Голос",
|
||||||
|
"placeholder": "Виберіть голос"
|
||||||
|
},
|
||||||
|
"ssmlEnabled": {
|
||||||
|
"label": "Ввімкнути SSML (Мова Розмітки для Синтезу Голосу)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": "Питання-запит",
|
||||||
|
"segmented": {
|
||||||
|
"custom": "Власні запити",
|
||||||
|
"copilot": "Запити Copilot"
|
||||||
|
},
|
||||||
|
"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} як змінну у вашому запиті.",
|
||||||
|
"missingTextPlaceholder": "Змінна {text} відсутня в запиті. Додайте її, будь ласка."
|
||||||
|
},
|
||||||
|
"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": "Ви впевнені, що хочете видалити цей спільний доступ? Його не вдасться відновити."
|
||||||
|
},
|
||||||
|
"label": "Управління спільним доступом до сторінки",
|
||||||
|
"description": "Включіть або вимкніть функцію спільного доступу до сторінки"
|
||||||
|
},
|
||||||
|
"notification": {
|
||||||
|
"pageShareSuccess": "URL спільного доступу до сторінки оновлено успішно",
|
||||||
|
"someError": "Щось пішло не так. Будь ласка, спробуйте пізніше",
|
||||||
|
"webShareDeleteSuccess": "Спільний доступ видалено успішно"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ollamaSettings": {
|
||||||
|
"title": "Налаштування Ollama",
|
||||||
|
"heading": "Налаштуйте Ollama",
|
||||||
|
"settings": {
|
||||||
|
"ollamaUrl": {
|
||||||
|
"label": "URL до Ollama",
|
||||||
|
"placeholder": "Введіть URL до Ollama"
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"label": "Розширене налаштування URL Ollama",
|
||||||
|
"urlRewriteEnabled": {
|
||||||
|
"label": "Увімкнути або вимкнути налаштування власного URL походження запиту"
|
||||||
|
},
|
||||||
|
"rewriteUrl": {
|
||||||
|
"label": "Власний URL походження запиту",
|
||||||
|
"placeholder": "Введіть власний URL походження"
|
||||||
|
},
|
||||||
|
"headers": {
|
||||||
|
"label": "Власні заголовки",
|
||||||
|
"add": "Додати заголовок",
|
||||||
|
"key": {
|
||||||
|
"label": "Ключ заголовка",
|
||||||
|
"placeholder": "Authorization"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"label": "Значення заголовка",
|
||||||
|
"placeholder": "Bearer token"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"help": "Якщо у вас виникають проблеми з підключенням до Ollama на Page Assist, ви можете налаштувати власний URL походження запиту. Щоб дізнатися більше про це налаштування, <anchor>натисніть тут</anchor>."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"manageSearch": {
|
||||||
|
"title": "Управління пошуком в Інтернеті",
|
||||||
|
"heading": "Налаштуйте пошук в Інтернеті"
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"title": "Про додаток",
|
||||||
|
"heading": "Інформація про цей додаток",
|
||||||
|
"chromeVersion": "Версія Page Assist",
|
||||||
|
"ollamaVersion": "Версія Ollama",
|
||||||
|
"support": "Ви можете підтримати проект Page Assist, зробивши пожертву або ставши спонсором через наступні платформи:",
|
||||||
|
"koFi": "Підтримка на Ko-fi",
|
||||||
|
"githubSponsor": "Стати спонсором на GitHub",
|
||||||
|
"githubRepo": "Репозиторій GitHub"
|
||||||
|
},
|
||||||
|
"manageKnowledge": {
|
||||||
|
"title": "Управління знаннями",
|
||||||
|
"heading": "Налаштування бази знань"
|
||||||
|
},
|
||||||
|
"rag": {
|
||||||
|
"title": "Налаштування RAG",
|
||||||
|
"ragSettings": {
|
||||||
|
"label": "Налаштування RAG",
|
||||||
|
"model": {
|
||||||
|
"label": "Модель вбудованих даних",
|
||||||
|
"required": "Будь ласка, виберіть модель",
|
||||||
|
"help": "Рекомендується використовувати моделі вбудованих даних, такі як `nomic-embed-text`.",
|
||||||
|
"placeholder": "Вибрати модель"
|
||||||
|
},
|
||||||
|
"chunkSize": {
|
||||||
|
"label": "Розмір шматка",
|
||||||
|
"placeholder": "Ввести розмір шматка",
|
||||||
|
"required": "Будь ласка, введіть розмір шматка"
|
||||||
|
},
|
||||||
|
"chunkOverlap": {
|
||||||
|
"label": "Перекриття шматків",
|
||||||
|
"placeholder": "Ввести перекриття шматків",
|
||||||
|
"required": "Будь ласка, введіть перекриття шматків"
|
||||||
|
},
|
||||||
|
"totalFilePerKB": {
|
||||||
|
"label": "Типовий ліміт кількості завантажень файлів до бази знань",
|
||||||
|
"placeholder": "Введіть типовий ліміт (напр. 10)",
|
||||||
|
"required": "Будь ласка, введіть типовий ліміт кількості файлів для завантаження"
|
||||||
|
},
|
||||||
|
"noOfRetrievedDocs": {
|
||||||
|
"label": "Кількість отриманих документів",
|
||||||
|
"placeholder": "Ввести кількість отриманих документів",
|
||||||
|
"required": "Будь ласка, введіть кількість документів"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"label": "Налаштування запиту з RAG",
|
||||||
|
"option1": "Нормальний",
|
||||||
|
"option2": "З Веб-пошуком",
|
||||||
|
"alert": "Налаштування системного запиту тут застаріло. Використовуйте розділ «Управління запитами», щоб додавати або редагувати запити. Цей розділ буде видалено в майбутньому оновленні",
|
||||||
|
"systemPrompt": "Системний запит",
|
||||||
|
"systemPromptPlaceholder": "Ввести системний запит",
|
||||||
|
"webSearchPrompt": "Запит веб-пошуку",
|
||||||
|
"webSearchPromptHelp": "Не видаляйте `{search_results}` із запиту.",
|
||||||
|
"webSearchPromptError": "Будь ласка, введіть запит для веб-пошуку",
|
||||||
|
"webSearchPromptPlaceholder": "Ввести запит для веб-пошуку",
|
||||||
|
"webSearchFollowUpPrompt": "Запит для подальшого пошуку в мережі",
|
||||||
|
"webSearchFollowUpPromptHelp": "Не видаляйте `{chat_history}` і `{question}` із запиту.",
|
||||||
|
"webSearchFollowUpPromptError": "Будь ласка, введіть запит для подальшого пошуку в мережі!",
|
||||||
|
"webSearchFollowUpPromptPlaceholder": "Ваш запит для подальшого пошуку в мережі"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chromeAiSettings": {
|
||||||
|
"title": "Налаштування Chrome AI"
|
||||||
|
}
|
||||||
|
}
|
7
src/assets/locale/uk/sidepanel.json
Normal file
7
src/assets/locale/uk/sidepanel.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"tooltip": {
|
||||||
|
"embed": "Може знадобитися кілька хвилин для вкладення сторінки у базу. Будь ласка, зачекайте...",
|
||||||
|
"clear": "Стерти історію чату",
|
||||||
|
"history": "Історія чату"
|
||||||
|
}
|
||||||
|
}
|
@ -25,5 +25,6 @@
|
|||||||
"stopStreaming": "停止流媒体",
|
"stopStreaming": "停止流媒体",
|
||||||
"knowledge": "知识"
|
"knowledge": "知识"
|
||||||
},
|
},
|
||||||
"sendWhenEnter": "按Enter发送"
|
"sendWhenEnter": "按Enter发送",
|
||||||
|
"welcome": "你好!今天我能帮你什么?"
|
||||||
}
|
}
|
@ -35,6 +35,9 @@
|
|||||||
},
|
},
|
||||||
"generateTitle": {
|
"generateTitle": {
|
||||||
"label": "使用人工智能生成标题"
|
"label": "使用人工智能生成标题"
|
||||||
|
},
|
||||||
|
"ollamaStatus": {
|
||||||
|
"label": "启用或禁用Ollama连接状态检查"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sidepanelRag": {
|
"sidepanelRag": {
|
||||||
|
@ -13,7 +13,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const MessageSource: React.FC<Props> = ({ source, onSourceClick }) => {
|
export const MessageSource: React.FC<Props> = ({ source, onSourceClick }) => {
|
||||||
if (source?.mode === "rag") {
|
if (source?.mode === "rag" || source?.mode === "chat") {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -22,6 +22,7 @@ import { getAllPrompts } from "@/db"
|
|||||||
import { ShareBtn } from "~/components/Common/ShareBtn"
|
import { ShareBtn } from "~/components/Common/ShareBtn"
|
||||||
import { ProviderIcons } from "../Common/ProviderIcon"
|
import { ProviderIcons } from "../Common/ProviderIcon"
|
||||||
import { NewChat } from "./NewChat"
|
import { NewChat } from "./NewChat"
|
||||||
|
import { PageAssistSelect } from "../Select"
|
||||||
type Props = {
|
type Props = {
|
||||||
setSidebarOpen: (open: boolean) => void
|
setSidebarOpen: (open: boolean) => void
|
||||||
setOpenModelSettings: (open: boolean) => void
|
setOpenModelSettings: (open: boolean) => void
|
||||||
@ -49,14 +50,10 @@ export const Header: React.FC<Props> = ({
|
|||||||
historyId,
|
historyId,
|
||||||
temporaryChat
|
temporaryChat
|
||||||
} = useMessageOption()
|
} = useMessageOption()
|
||||||
const {
|
const { data: models, isLoading: isModelsLoading, refetch } = useQuery({
|
||||||
data: models,
|
|
||||||
isLoading: isModelsLoading,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["fetchModel"],
|
queryKey: ["fetchModel"],
|
||||||
queryFn: () => fetchChatModels({ returnEmpty: true }),
|
queryFn: () => fetchChatModels({ returnEmpty: true }),
|
||||||
refetchInterval: 15_000,
|
refetchIntervalInBackground: false,
|
||||||
refetchIntervalInBackground: true,
|
|
||||||
placeholderData: (prev) => prev
|
placeholderData: (prev) => prev
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -87,7 +84,8 @@ export const Header: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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={`sticky top-0 z-[999] flex h-16 p-3 bg-gray-50 border-b dark:bg-[#171717] dark:border-gray-600 ${
|
||||||
temporaryChat && "!bg-gray-200 dark:!bg-black"
|
temporaryChat && "!bg-gray-200 dark:!bg-black"
|
||||||
}`}>
|
}`}>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
@ -107,41 +105,39 @@ export const Header: React.FC<Props> = ({
|
|||||||
<PanelLeftIcon className="w-6 h-6" />
|
<PanelLeftIcon className="w-6 h-6" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<NewChat
|
<NewChat clearChat={clearChat} />
|
||||||
clearChat={clearChat}
|
|
||||||
/>
|
|
||||||
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
|
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
|
||||||
{"/"}
|
{"/"}
|
||||||
</span>
|
</span>
|
||||||
<div className="hidden lg:block">
|
<div className="hidden lg:block">
|
||||||
<Select
|
<PageAssistSelect
|
||||||
|
className="w-80"
|
||||||
|
placeholder={t("common:selectAModel")}
|
||||||
|
loadingText={t("common:selectAModel")}
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSelectedModel(e)
|
setSelectedModel(e.value)
|
||||||
localStorage.setItem("selectedModel", e)
|
localStorage.setItem("selectedModel", e.value)
|
||||||
}}
|
}}
|
||||||
size="large"
|
isLoading={isModelsLoading}
|
||||||
loading={isModelsLoading}
|
|
||||||
filterOption={(input, option) =>
|
|
||||||
option.label.key.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
|
||||||
}
|
|
||||||
showSearch
|
|
||||||
placeholder={t("common:selectAModel")}
|
|
||||||
className="w-72"
|
|
||||||
options={models?.map((model) => ({
|
options={models?.map((model) => ({
|
||||||
label: (
|
label: (
|
||||||
<span
|
<span
|
||||||
key={model.model}
|
key={model.model}
|
||||||
className="flex flex-row gap-3 items-center truncate">
|
className="flex flex-row gap-3 items-center ">
|
||||||
<ProviderIcons
|
<ProviderIcons
|
||||||
provider={model?.provider}
|
provider={model?.provider}
|
||||||
className="w-5 h-5"
|
className="w-5 h-5"
|
||||||
/>
|
/>
|
||||||
<span className="truncate">{model.name}</span>
|
<span className="line-clamp-2">{model.name}</span>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
value: model.model
|
value: model.model
|
||||||
}))}
|
}))}
|
||||||
|
|
||||||
|
onRefresh={() => {
|
||||||
|
refetch()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:hidden">
|
<div className="lg:hidden">
|
||||||
|
@ -43,12 +43,12 @@ export const NewChat: React.FC<Props> = ({ clearChat }) => {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<button
|
<button
|
||||||
onClick={clearChat}
|
onClick={clearChat}
|
||||||
className="inline-flex dark:bg-transparent bg-white items-center rounded-r-none rounded-lg border dark:border-gray-700 bg-transparent px-3 py-2.5 pr-6 text-xs lg: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">
|
className="inline-flex dark:bg-transparent bg-white items-center rounded-s-lg rounded-e-none border dark:border-gray-700 bg-transparent px-3 py-2.5 pe-6 text-xs lg: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-5 w-5" />
|
<SquarePen className="h-5 w-5" />
|
||||||
<span className="truncate ml-3">{t("newChat")}</span>
|
<span className="truncate ms-3">{t("newChat")}</span>
|
||||||
</button>
|
</button>
|
||||||
<Dropdown menu={{ items }} trigger={["click"]}>
|
<Dropdown menu={{ items }} trigger={["click"]}>
|
||||||
<button className="inline-flex dark:bg-transparent bg-white items-center rounded-lg border-l-0 rounded-l-none border dark:border-gray-700 bg-transparent px-3 py-2.5 text-xs lg: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">
|
<button className="inline-flex dark:bg-transparent bg-white items-center rounded-lg border-s-0 rounded-s-none border dark:border-gray-700 bg-transparent px-3 py-2.5 text-xs lg: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">
|
||||||
<MoreHorizontal className="h-5 w-5 text-gray-600 dark:text-gray-400" />
|
<MoreHorizontal className="h-5 w-5 text-gray-600 dark:text-gray-400" />
|
||||||
</button>
|
</button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { cleanUrl } from "@/libs/clean-url"
|
import { cleanUrl } from "@/libs/clean-url"
|
||||||
|
import { useStorage } from "@plasmohq/storage/hook"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { RotateCcw } from "lucide-react"
|
import { RotateCcw } from "lucide-react"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
@ -12,6 +13,9 @@ import {
|
|||||||
export const PlaygroundEmpty = () => {
|
export const PlaygroundEmpty = () => {
|
||||||
const [ollamaURL, setOllamaURL] = useState<string>("")
|
const [ollamaURL, setOllamaURL] = useState<string>("")
|
||||||
const { t } = useTranslation(["playground", "common"])
|
const { t } = useTranslation(["playground", "common"])
|
||||||
|
|
||||||
|
const [checkOllamaStatus] = useStorage("checkOllamaStatus", true)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: ollamaInfo,
|
data: ollamaInfo,
|
||||||
status: ollamaStatus,
|
status: ollamaStatus,
|
||||||
@ -23,19 +27,32 @@ export const PlaygroundEmpty = () => {
|
|||||||
const ollamaURL = await getOllamaURL()
|
const ollamaURL = await getOllamaURL()
|
||||||
const isOk = await isOllamaRunning()
|
const isOk = await isOllamaRunning()
|
||||||
|
|
||||||
|
if (ollamaURL) {
|
||||||
|
saveOllamaURL(ollamaURL)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isOk,
|
isOk,
|
||||||
ollamaURL
|
ollamaURL
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
enabled: checkOllamaStatus
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
if (!checkOllamaStatus) {
|
||||||
if (ollamaInfo?.ollamaURL) {
|
return (
|
||||||
setOllamaURL(ollamaInfo.ollamaURL)
|
<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-gray-50 dark:bg-[#262626] dark:border-gray-600">
|
||||||
|
<h1 className="text-sm font-medium text-center text-gray-500 dark:text-gray-400 flex gap-3 items-center justify-center">
|
||||||
|
<span >👋</span>
|
||||||
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
{t("welcome")}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}, [ollamaInfo])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto sm:max-w-xl px-4 mt-10">
|
<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-gray-50 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">
|
||||||
|
@ -16,10 +16,9 @@ import {
|
|||||||
import { useStorage } from "@plasmohq/storage/hook"
|
import { useStorage } from "@plasmohq/storage/hook"
|
||||||
|
|
||||||
export const GeneralSettings = () => {
|
export const GeneralSettings = () => {
|
||||||
const { clearChat } =
|
const { clearChat } = useMessageOption()
|
||||||
useMessageOption()
|
|
||||||
|
|
||||||
const [ speechToTextLanguage, setSpeechToTextLanguage ] = useStorage(
|
const [speechToTextLanguage, setSpeechToTextLanguage] = useStorage(
|
||||||
"speechToTextLanguage",
|
"speechToTextLanguage",
|
||||||
"en-US"
|
"en-US"
|
||||||
)
|
)
|
||||||
@ -41,6 +40,11 @@ export const GeneralSettings = () => {
|
|||||||
const [sendNotificationAfterIndexing, setSendNotificationAfterIndexing] =
|
const [sendNotificationAfterIndexing, setSendNotificationAfterIndexing] =
|
||||||
useStorage("sendNotificationAfterIndexing", false)
|
useStorage("sendNotificationAfterIndexing", false)
|
||||||
|
|
||||||
|
const [checkOllamaStatus, setCheckOllamaStatus] = useStorage(
|
||||||
|
"checkOllamaStatus",
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
const { mode, toggleDarkMode } = useDarkMode()
|
const { mode, toggleDarkMode } = useDarkMode()
|
||||||
@ -160,6 +164,19 @@ export const GeneralSettings = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row justify-between">
|
||||||
|
<div className="inline-flex items-center gap-2">
|
||||||
|
<span className="text-gray-700 dark:text-neutral-50">
|
||||||
|
{t("generalSettings.settings.ollamaStatus.label")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
checked={checkOllamaStatus}
|
||||||
|
onChange={(checked) => setCheckOllamaStatus(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row justify-between">
|
||||||
<span className="text-gray-700 dark:text-neutral-50 ">
|
<span className="text-gray-700 dark:text-neutral-50 ">
|
||||||
{t("generalSettings.settings.darkMode.label")}
|
{t("generalSettings.settings.darkMode.label")}
|
||||||
|
27
src/components/Select/LoadingIndicator.tsx
Normal file
27
src/components/Select/LoadingIndicator.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export const LoadingIndicator: React.FC<{ className?: string }> = ({
|
||||||
|
className = ""
|
||||||
|
}) => (
|
||||||
|
<div className={`animate-spin ${className}`}>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)
|
333
src/components/Select/index.tsx
Normal file
333
src/components/Select/index.tsx
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
import React, { useState, useRef, useEffect, useMemo } from "react"
|
||||||
|
import { Search, RotateCw, ChevronDown } from "lucide-react"
|
||||||
|
import { LoadingIndicator } from "./LoadingIndicator"
|
||||||
|
import { Empty } from "antd"
|
||||||
|
|
||||||
|
export interface SelectOption {
|
||||||
|
label: string | JSX.Element
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectProps {
|
||||||
|
options: SelectOption[]
|
||||||
|
value?: string
|
||||||
|
onChange: (option: SelectOption) => void
|
||||||
|
placeholder?: string
|
||||||
|
onRefresh?: () => void
|
||||||
|
className?: string
|
||||||
|
dropdownClassName?: string
|
||||||
|
optionClassName?: string
|
||||||
|
searchClassName?: string
|
||||||
|
disabled?: boolean
|
||||||
|
isLoading?: boolean
|
||||||
|
loadingText?: string
|
||||||
|
filterOption?: (input: string, option: SelectOption) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PageAssistSelect: React.FC<SelectProps> = ({
|
||||||
|
options = [],
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = "Select an option",
|
||||||
|
onRefresh,
|
||||||
|
className = "",
|
||||||
|
dropdownClassName = "",
|
||||||
|
optionClassName = "",
|
||||||
|
searchClassName = "",
|
||||||
|
disabled = false,
|
||||||
|
isLoading = false,
|
||||||
|
loadingText = "Loading...",
|
||||||
|
filterOption
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const [searchTerm, setSearchTerm] = useState("")
|
||||||
|
const [filteredOptions, setFilteredOptions] = useState<SelectOption[]>([])
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const optionsContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [activeIndex, setActiveIndex] = useState(-1)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
containerRef.current &&
|
||||||
|
!containerRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("mousedown", handleClickOutside)
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
if (isOpen && optionsContainerRef.current && value) {
|
||||||
|
const selectedOptionElement = optionsContainerRef.current.querySelector(
|
||||||
|
`[data-value="${value}"]`
|
||||||
|
)
|
||||||
|
if (selectedOptionElement) {
|
||||||
|
selectedOptionElement.scrollIntoView({ block: "nearest" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error scrolling to selected option:", error)
|
||||||
|
}
|
||||||
|
}, [isOpen, value])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!options) return
|
||||||
|
|
||||||
|
const filtered = options.filter((option) => {
|
||||||
|
if (!searchTerm) return true
|
||||||
|
|
||||||
|
if (filterOption) {
|
||||||
|
return filterOption(searchTerm, option)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof option.label === "string") {
|
||||||
|
return option.label.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (React.isValidElement(option.label)) {
|
||||||
|
const textContent = extractTextFromJSX(option.label)
|
||||||
|
return textContent.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
setFilteredOptions(filtered)
|
||||||
|
setActiveIndex(-1)
|
||||||
|
}, [searchTerm, options, filterOption])
|
||||||
|
|
||||||
|
const extractTextFromJSX = (element: React.ReactElement): string => {
|
||||||
|
if (typeof element.props.children === "string") {
|
||||||
|
return element.props.children
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(element.props.children)) {
|
||||||
|
return element.props.children
|
||||||
|
.map((child) => {
|
||||||
|
if (typeof child === "string") return child
|
||||||
|
if (React.isValidElement(child)) return extractTextFromJSX(child)
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (React.isValidElement(element.props.children)) {
|
||||||
|
return extractTextFromJSX(element.props.children)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRefresh = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onRefresh?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (disabled || isLoading) return
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case "Enter":
|
||||||
|
if (isOpen && activeIndex >= 0) {
|
||||||
|
e.preventDefault()
|
||||||
|
const selectedOption = filteredOptions[activeIndex]
|
||||||
|
if (selectedOption) {
|
||||||
|
onChange(selectedOption)
|
||||||
|
setIsOpen(false)
|
||||||
|
setSearchTerm("")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsOpen(!isOpen)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case " ":
|
||||||
|
if (!isOpen) {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsOpen(true)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "Escape":
|
||||||
|
setIsOpen(false)
|
||||||
|
break
|
||||||
|
case "ArrowDown":
|
||||||
|
e.preventDefault()
|
||||||
|
if (!isOpen) {
|
||||||
|
setIsOpen(true)
|
||||||
|
} else {
|
||||||
|
setActiveIndex((prev) =>
|
||||||
|
prev < filteredOptions.length - 1 ? prev + 1 : prev
|
||||||
|
)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case "ArrowUp":
|
||||||
|
e.preventDefault()
|
||||||
|
setActiveIndex((prev) => (prev > 0 ? prev - 1 : prev))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSelectClass = `
|
||||||
|
flex items-center justify-between p-2.5 rounded-lg border
|
||||||
|
${disabled || isLoading ? "cursor-not-allowed opacity-50" : "cursor-pointer"}
|
||||||
|
${isOpen ? "ring-2 ring-blue-500" : ""}
|
||||||
|
bg-transparent border-gray-200 text-gray-900
|
||||||
|
transition-all duration-200
|
||||||
|
dark:text-white
|
||||||
|
dark:border-[#353534]
|
||||||
|
`
|
||||||
|
|
||||||
|
const defaultDropdownClass = `
|
||||||
|
absolute z-50 w-full mt-1 bg-white dark:bg-[#1e1e1f] dark:text-white rounded-lg shadow-lg
|
||||||
|
border border-gray-200 dark:border-[#353534]
|
||||||
|
`
|
||||||
|
|
||||||
|
const defaultSearchClass = `
|
||||||
|
w-full pl-8 pr-8 py-1.5 rounded-md
|
||||||
|
bg-gray-50 border border-gray-200
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-gray-100
|
||||||
|
text-gray-900
|
||||||
|
dark:bg-[#1e1e1f] dark:text-white
|
||||||
|
dark:border-[#353534]
|
||||||
|
dark:focus:ring-gray-700
|
||||||
|
dark:focus:border-gray-700
|
||||||
|
dark:placeholder-gray-400
|
||||||
|
dark:bg-opacity-90
|
||||||
|
dark:hover:bg-opacity-100
|
||||||
|
dark:focus:bg-opacity-100
|
||||||
|
dark:hover:border-gray-700
|
||||||
|
dark:hover:bg-[#2a2a2b]
|
||||||
|
dark:focus:bg-[#2a2a2b]
|
||||||
|
`
|
||||||
|
|
||||||
|
const defaultOptionClass = `
|
||||||
|
p-2 cursor-pointer transition-colors duration-150
|
||||||
|
`
|
||||||
|
|
||||||
|
const selectedOption = useMemo(() => {
|
||||||
|
if (!value || !options) return null
|
||||||
|
return options?.find((opt) => opt.value === value)
|
||||||
|
}, [value, options])
|
||||||
|
|
||||||
|
if (!options) {
|
||||||
|
return (
|
||||||
|
<div className={`relative w-full ${className}`}>
|
||||||
|
<div className={`${defaultSelectClass} ${className}`}>
|
||||||
|
<LoadingIndicator />
|
||||||
|
<span>{loadingText}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={containerRef} className={`relative w-full ${className}`}>
|
||||||
|
<div
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-controls="select-dropdown"
|
||||||
|
aria-label={placeholder}
|
||||||
|
tabIndex={disabled ? -1 : 0}
|
||||||
|
onClick={() => !disabled && !isLoading && setIsOpen(!isOpen)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className={`${defaultSelectClass} ${className}`}>
|
||||||
|
<span className="!truncate flex items-center gap-2 select-none">
|
||||||
|
{isLoading && <LoadingIndicator />}
|
||||||
|
{isLoading ? (
|
||||||
|
loadingText
|
||||||
|
) : selectedOption ? (
|
||||||
|
selectedOption.label
|
||||||
|
) : (
|
||||||
|
<span className="dark:text-gray-500 font-semibold text-[14px]">
|
||||||
|
{placeholder}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ChevronDown
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`w-4 h-4 transition-transform duration-200 ${
|
||||||
|
isOpen ? "transform rotate-180" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
id="select-dropdown"
|
||||||
|
role="listbox"
|
||||||
|
className={`${defaultDropdownClass} ${dropdownClassName}`}>
|
||||||
|
<div className="p-2 border-b border-gray-200 dark:border-[#353534]">
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
placeholder="Search..."
|
||||||
|
className={`${defaultSearchClass} ${searchClassName}`}
|
||||||
|
disabled={isLoading}
|
||||||
|
aria-label="Search options"
|
||||||
|
/>
|
||||||
|
<Search
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute left-2 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400"
|
||||||
|
/>
|
||||||
|
{onRefresh && (
|
||||||
|
<button
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={isLoading}
|
||||||
|
aria-label="Refresh options"
|
||||||
|
className={`absolute right-2 top-1/2 transform -translate-y-1/2
|
||||||
|
hover:text-blue-500 dark:hover:text-blue-400 transition-colors duration-200
|
||||||
|
${isLoading ? "cursor-not-allowed opacity-50" : ""}`}>
|
||||||
|
<RotateCw
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`w-4 h-4 dark:text-gray-400 ${isLoading ? "animate-spin" : ""}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}{" "}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={optionsContainerRef}
|
||||||
|
className="max-h-60 overflow-y-auto custom-scrollbar">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="p-4 text-center text-gray-500 flex items-center justify-center gap-2">
|
||||||
|
<LoadingIndicator />
|
||||||
|
<span>{loadingText}</span>
|
||||||
|
</div>
|
||||||
|
) : filteredOptions.length === 0 ? (
|
||||||
|
<div className="p-6">
|
||||||
|
<Empty />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredOptions.map((option, index) => (
|
||||||
|
<div
|
||||||
|
key={option.value}
|
||||||
|
role="option"
|
||||||
|
aria-selected={value === option.value}
|
||||||
|
data-value={option.value}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(option)
|
||||||
|
setIsOpen(false)
|
||||||
|
setSearchTerm("")
|
||||||
|
}}
|
||||||
|
className={`
|
||||||
|
${defaultOptionClass}
|
||||||
|
${value === option.value ? "bg-blue-50 dark:bg-[#262627]" : "hover:bg-gray-100 dark:hover:bg-[#272728]"}
|
||||||
|
${activeIndex === index ? "bg-gray-100 dark:bg-[#272728]" : ""}
|
||||||
|
${optionClassName}`}>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { cleanUrl } from "@/libs/clean-url"
|
import { cleanUrl } from "@/libs/clean-url"
|
||||||
|
import { useStorage } from "@plasmohq/storage/hook"
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
import { Select } from "antd"
|
import { Select } from "antd"
|
||||||
import { RotateCcw } from "lucide-react"
|
import { RotateCcw } from "lucide-react"
|
||||||
@ -17,13 +18,15 @@ export const EmptySidePanel = () => {
|
|||||||
const [ollamaURL, setOllamaURL] = useState<string>("")
|
const [ollamaURL, setOllamaURL] = useState<string>("")
|
||||||
const { t } = useTranslation(["playground", "common"])
|
const { t } = useTranslation(["playground", "common"])
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const [checkOllamaStatus] = useStorage("checkOllamaStatus", true)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: ollamaInfo,
|
data: ollamaInfo,
|
||||||
status: ollamaStatus,
|
status: ollamaStatus,
|
||||||
refetch,
|
refetch,
|
||||||
isRefetching
|
isRefetching
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["ollamaStatus"],
|
queryKey: ["ollamaStatus", checkOllamaStatus],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const ollamaURL = await getOllamaURL()
|
const ollamaURL = await getOllamaURL()
|
||||||
const isOk = await isOllamaRunning()
|
const isOk = await isOllamaRunning()
|
||||||
@ -32,7 +35,7 @@ export const EmptySidePanel = () => {
|
|||||||
queryKey: ["getAllModelsForSelect"]
|
queryKey: ["getAllModelsForSelect"]
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
isOk,
|
isOk: checkOllamaStatus ? isOk : true,
|
||||||
models,
|
models,
|
||||||
ollamaURL
|
ollamaURL
|
||||||
}
|
}
|
||||||
@ -59,7 +62,7 @@ export const EmptySidePanel = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isRefetching && ollamaStatus === "success" ? (
|
{!isRefetching && ollamaStatus === "success" && checkOllamaStatus ? (
|
||||||
ollamaInfo.isOk ? (
|
ollamaInfo.isOk ? (
|
||||||
<div className="inline-flex items-center space-x-2">
|
<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"></div>
|
||||||
|
@ -7,7 +7,14 @@ import { toBase64 } from "~/libs/to-base64"
|
|||||||
import { Checkbox, Dropdown, Image, Switch, Tooltip } from "antd"
|
import { Checkbox, Dropdown, Image, Switch, Tooltip } from "antd"
|
||||||
import { useWebUI } from "~/store/webui"
|
import { useWebUI } from "~/store/webui"
|
||||||
import { defaultEmbeddingModelForRag } from "~/services/ollama"
|
import { defaultEmbeddingModelForRag } from "~/services/ollama"
|
||||||
import { ImageIcon, MicIcon, StopCircleIcon, X } from "lucide-react"
|
import {
|
||||||
|
ImageIcon,
|
||||||
|
MicIcon,
|
||||||
|
StopCircleIcon,
|
||||||
|
X,
|
||||||
|
EyeIcon,
|
||||||
|
EyeOffIcon
|
||||||
|
} from "lucide-react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { ModelSelect } from "@/components/Common/ModelSelect"
|
import { ModelSelect } from "@/components/Common/ModelSelect"
|
||||||
import { useSpeechRecognition } from "@/hooks/useSpeechRecognition"
|
import { useSpeechRecognition } from "@/hooks/useSpeechRecognition"
|
||||||
@ -36,7 +43,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
|
|||||||
resetTranscript,
|
resetTranscript,
|
||||||
start: startListening,
|
start: startListening,
|
||||||
stop: stopSpeechRecognition,
|
stop: stopSpeechRecognition,
|
||||||
supported: browserSupportsSpeechRecognition,
|
supported: browserSupportsSpeechRecognition
|
||||||
} = useSpeechRecognition()
|
} = useSpeechRecognition()
|
||||||
|
|
||||||
const stopListening = async () => {
|
const stopListening = async () => {
|
||||||
@ -237,7 +244,10 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await stopListening()
|
await stopListening()
|
||||||
if (value.message.trim().length === 0 && value.image.length === 0) {
|
if (
|
||||||
|
value.message.trim().length === 0 &&
|
||||||
|
value.image.length === 0
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
form.reset()
|
form.reset()
|
||||||
@ -281,6 +291,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
|
|||||||
{...form.getInputProps("message")}
|
{...form.getInputProps("message")}
|
||||||
/>
|
/>
|
||||||
<div className="flex mt-4 justify-end gap-3">
|
<div className="flex mt-4 justify-end gap-3">
|
||||||
|
{chatMode !== "vision" && (
|
||||||
<Tooltip title={t("tooltip.searchInternet")}>
|
<Tooltip title={t("tooltip.searchInternet")}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -295,6 +306,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
)}
|
||||||
<ModelSelect />
|
<ModelSelect />
|
||||||
{browserSupportsSpeechRecognition && (
|
{browserSupportsSpeechRecognition && (
|
||||||
<Tooltip title={t("tooltip.speechToText")}>
|
<Tooltip title={t("tooltip.speechToText")}>
|
||||||
@ -323,13 +335,35 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
<Tooltip title={t("tooltip.vision")}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (chatMode === "vision") {
|
||||||
|
setChatMode("normal")
|
||||||
|
} else {
|
||||||
|
setChatMode("vision")
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={chatMode === "rag"}
|
||||||
|
className={`flex items-center justify-center dark:text-gray-300 ${
|
||||||
|
chatMode === "rag" ? "hidden" : "block"
|
||||||
|
} disabled:opacity-50`}>
|
||||||
|
{chatMode === "vision" ? (
|
||||||
|
<EyeIcon className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<EyeOffIcon className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
<Tooltip title={t("tooltip.uploadImage")}>
|
<Tooltip title={t("tooltip.uploadImage")}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
inputRef.current?.click()
|
inputRef.current?.click()
|
||||||
}}
|
}}
|
||||||
className={`flex items-center justify-center dark:text-gray-300 ${
|
disabled={chatMode === "vision"}
|
||||||
|
className={`flex items-center justify-center disabled:opacity-50 dark:text-gray-300 ${
|
||||||
chatMode === "rag" ? "hidden" : "block"
|
chatMode === "rag" ? "hidden" : "block"
|
||||||
}`}>
|
}`}>
|
||||||
<ImageIcon className="h-5 w-5" />
|
<ImageIcon className="h-5 w-5" />
|
||||||
|
@ -34,6 +34,9 @@ import { pageAssistModel } from "@/models"
|
|||||||
import { getPrompt } from "@/services/application"
|
import { getPrompt } from "@/services/application"
|
||||||
import { humanMessageFormatter } from "@/utils/human-message"
|
import { humanMessageFormatter } from "@/utils/human-message"
|
||||||
import { pageAssistEmbeddingModel } from "@/models/embedding"
|
import { pageAssistEmbeddingModel } from "@/models/embedding"
|
||||||
|
import { PageAssistVectorStore } from "@/libs/PageAssistVectorStore"
|
||||||
|
import { PAMemoryVectorStore } from "@/libs/PAMemoryVectorStore"
|
||||||
|
import { getScreenshotFromCurrentTab } from "@/libs/get-screenshot"
|
||||||
|
|
||||||
export const useMessage = () => {
|
export const useMessage = () => {
|
||||||
const {
|
const {
|
||||||
@ -90,7 +93,7 @@ export const useMessage = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const [keepTrackOfEmbedding, setKeepTrackOfEmbedding] = React.useState<{
|
const [keepTrackOfEmbedding, setKeepTrackOfEmbedding] = React.useState<{
|
||||||
[key: string]: MemoryVectorStore
|
[key: string]: PAMemoryVectorStore
|
||||||
}>({})
|
}>({})
|
||||||
|
|
||||||
const clearChat = () => {
|
const clearChat = () => {
|
||||||
@ -134,8 +137,9 @@ export const useMessage = () => {
|
|||||||
seed: currentChatModelSettings?.seed,
|
seed: currentChatModelSettings?.seed,
|
||||||
numGpu:
|
numGpu:
|
||||||
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
|
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
|
||||||
numPredict: currentChatModelSettings?.numPredict ?? userDefaultModelSettings?.numPredict,
|
numPredict:
|
||||||
|
currentChatModelSettings?.numPredict ??
|
||||||
|
userDefaultModelSettings?.numPredict
|
||||||
})
|
})
|
||||||
|
|
||||||
let newMessage: Message[] = []
|
let newMessage: Message[] = []
|
||||||
@ -177,7 +181,7 @@ export const useMessage = () => {
|
|||||||
let embedURL: string, embedHTML: string, embedType: string
|
let embedURL: string, embedHTML: string, embedType: string
|
||||||
let embedPDF: { content: string; page: number }[] = []
|
let embedPDF: { content: string; page: number }[] = []
|
||||||
|
|
||||||
let isAlreadyExistEmbedding: MemoryVectorStore
|
let isAlreadyExistEmbedding: PAMemoryVectorStore
|
||||||
const {
|
const {
|
||||||
content: html,
|
content: html,
|
||||||
url: websiteUrl,
|
url: websiteUrl,
|
||||||
@ -212,7 +216,7 @@ export const useMessage = () => {
|
|||||||
currentChatModelSettings?.keepAlive ??
|
currentChatModelSettings?.keepAlive ??
|
||||||
userDefaultModelSettings?.keepAlive
|
userDefaultModelSettings?.keepAlive
|
||||||
})
|
})
|
||||||
let vectorstore: MemoryVectorStore
|
let vectorstore: PAMemoryVectorStore
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isAlreadyExistEmbedding) {
|
if (isAlreadyExistEmbedding) {
|
||||||
@ -263,9 +267,11 @@ export const useMessage = () => {
|
|||||||
userDefaultModelSettings?.numCtx,
|
userDefaultModelSettings?.numCtx,
|
||||||
seed: currentChatModelSettings?.seed,
|
seed: currentChatModelSettings?.seed,
|
||||||
numGpu:
|
numGpu:
|
||||||
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
|
currentChatModelSettings?.numGpu ??
|
||||||
numPredict: currentChatModelSettings?.numPredict ?? userDefaultModelSettings?.numPredict,
|
userDefaultModelSettings?.numGpu,
|
||||||
|
numPredict:
|
||||||
|
currentChatModelSettings?.numPredict ??
|
||||||
|
userDefaultModelSettings?.numPredict
|
||||||
})
|
})
|
||||||
const response = await questionOllama.invoke(promptForQuestion)
|
const response = await questionOllama.invoke(promptForQuestion)
|
||||||
query = response.content.toString()
|
query = response.content.toString()
|
||||||
@ -340,9 +346,7 @@ export const useMessage = () => {
|
|||||||
signal: signal,
|
signal: signal,
|
||||||
callbacks: [
|
callbacks: [
|
||||||
{
|
{
|
||||||
handleLLMEnd(
|
handleLLMEnd(output: any): any {
|
||||||
output: any,
|
|
||||||
): any {
|
|
||||||
try {
|
try {
|
||||||
generationInfo = output?.generations?.[0][0]?.generationInfo
|
generationInfo = output?.generations?.[0][0]?.generationInfo
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -448,6 +452,236 @@ export const useMessage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const visionChatMode = async (
|
||||||
|
message: string,
|
||||||
|
image: string,
|
||||||
|
isRegenerate: boolean,
|
||||||
|
messages: Message[],
|
||||||
|
history: ChatHistory,
|
||||||
|
signal: AbortSignal
|
||||||
|
) => {
|
||||||
|
setStreaming(true)
|
||||||
|
const url = await getOllamaURL()
|
||||||
|
const userDefaultModelSettings = await getAllDefaultModelSettings()
|
||||||
|
|
||||||
|
const ollama = await pageAssistModel({
|
||||||
|
model: selectedModel!,
|
||||||
|
baseUrl: cleanUrl(url),
|
||||||
|
keepAlive:
|
||||||
|
currentChatModelSettings?.keepAlive ??
|
||||||
|
userDefaultModelSettings?.keepAlive,
|
||||||
|
temperature:
|
||||||
|
currentChatModelSettings?.temperature ??
|
||||||
|
userDefaultModelSettings?.temperature,
|
||||||
|
topK: currentChatModelSettings?.topK ?? userDefaultModelSettings?.topK,
|
||||||
|
topP: currentChatModelSettings?.topP ?? userDefaultModelSettings?.topP,
|
||||||
|
numCtx:
|
||||||
|
currentChatModelSettings?.numCtx ?? userDefaultModelSettings?.numCtx,
|
||||||
|
seed: currentChatModelSettings?.seed,
|
||||||
|
numGpu:
|
||||||
|
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
|
||||||
|
numPredict:
|
||||||
|
currentChatModelSettings?.numPredict ??
|
||||||
|
userDefaultModelSettings?.numPredict
|
||||||
|
})
|
||||||
|
|
||||||
|
let newMessage: Message[] = []
|
||||||
|
let generateMessageId = generateID()
|
||||||
|
|
||||||
|
if (!isRegenerate) {
|
||||||
|
newMessage = [
|
||||||
|
...messages,
|
||||||
|
{
|
||||||
|
isBot: false,
|
||||||
|
name: "You",
|
||||||
|
message,
|
||||||
|
sources: [],
|
||||||
|
images: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isBot: true,
|
||||||
|
name: selectedModel,
|
||||||
|
message: "▋",
|
||||||
|
sources: [],
|
||||||
|
id: generateMessageId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
newMessage = [
|
||||||
|
...messages,
|
||||||
|
{
|
||||||
|
isBot: true,
|
||||||
|
name: selectedModel,
|
||||||
|
message: "▋",
|
||||||
|
sources: [],
|
||||||
|
id: generateMessageId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
setMessages(newMessage)
|
||||||
|
let fullText = ""
|
||||||
|
let contentToSave = ""
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prompt = await systemPromptForNonRag()
|
||||||
|
const selectedPrompt = await getPromptById(selectedSystemPrompt)
|
||||||
|
|
||||||
|
const applicationChatHistory = generateHistory(history, selectedModel)
|
||||||
|
|
||||||
|
const data = await getScreenshotFromCurrentTab()
|
||||||
|
console.log(
|
||||||
|
data?.success
|
||||||
|
? `[PageAssist] Screenshot is taken`
|
||||||
|
: `[PageAssist] Screenshot is not taken`
|
||||||
|
)
|
||||||
|
const visionImage = data?.screenshot || ""
|
||||||
|
|
||||||
|
if (visionImage === "") {
|
||||||
|
throw new Error(
|
||||||
|
"Please close and reopen the side panel. This is a bug that will be fixed soon."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prompt && !selectedPrompt) {
|
||||||
|
applicationChatHistory.unshift(
|
||||||
|
new SystemMessage({
|
||||||
|
content: prompt
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (selectedPrompt) {
|
||||||
|
applicationChatHistory.unshift(
|
||||||
|
new SystemMessage({
|
||||||
|
content: selectedPrompt.content
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let humanMessage = humanMessageFormatter({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: message,
|
||||||
|
type: "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image_url: visionImage,
|
||||||
|
type: "image_url"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
model: selectedModel
|
||||||
|
})
|
||||||
|
|
||||||
|
let generationInfo: any | undefined = undefined
|
||||||
|
|
||||||
|
const chunks = await ollama.stream(
|
||||||
|
[...applicationChatHistory, humanMessage],
|
||||||
|
{
|
||||||
|
signal: signal,
|
||||||
|
callbacks: [
|
||||||
|
{
|
||||||
|
handleLLMEnd(output: any): any {
|
||||||
|
try {
|
||||||
|
generationInfo = output?.generations?.[0][0]?.generationInfo
|
||||||
|
} catch (e) {
|
||||||
|
console.log("handleLLMEnd error", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
let count = 0
|
||||||
|
for await (const chunk of chunks) {
|
||||||
|
contentToSave += chunk?.content
|
||||||
|
fullText += chunk?.content
|
||||||
|
if (count === 0) {
|
||||||
|
setIsProcessing(true)
|
||||||
|
}
|
||||||
|
setMessages((prev) => {
|
||||||
|
return prev.map((message) => {
|
||||||
|
if (message.id === generateMessageId) {
|
||||||
|
return {
|
||||||
|
...message,
|
||||||
|
message: fullText + "▋"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
})
|
||||||
|
})
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
setMessages((prev) => {
|
||||||
|
return prev.map((message) => {
|
||||||
|
if (message.id === generateMessageId) {
|
||||||
|
return {
|
||||||
|
...message,
|
||||||
|
message: fullText,
|
||||||
|
generationInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
setHistory([
|
||||||
|
...history,
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: message
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: fullText
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
await saveMessageOnSuccess({
|
||||||
|
historyId,
|
||||||
|
setHistoryId,
|
||||||
|
isRegenerate,
|
||||||
|
selectedModel: selectedModel,
|
||||||
|
message,
|
||||||
|
image,
|
||||||
|
fullText,
|
||||||
|
source: [],
|
||||||
|
message_source: "copilot",
|
||||||
|
generationInfo
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsProcessing(false)
|
||||||
|
setStreaming(false)
|
||||||
|
} catch (e) {
|
||||||
|
const errorSave = await saveMessageOnError({
|
||||||
|
e,
|
||||||
|
botMessage: fullText,
|
||||||
|
history,
|
||||||
|
historyId,
|
||||||
|
image,
|
||||||
|
selectedModel,
|
||||||
|
setHistory,
|
||||||
|
setHistoryId,
|
||||||
|
userMessage: message,
|
||||||
|
isRegenerating: isRegenerate,
|
||||||
|
message_source: "copilot"
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!errorSave) {
|
||||||
|
notification.error({
|
||||||
|
message: t("error"),
|
||||||
|
description: e?.message || t("somethingWentWrong")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setIsProcessing(false)
|
||||||
|
setStreaming(false)
|
||||||
|
setIsProcessing(false)
|
||||||
|
setStreaming(false)
|
||||||
|
setIsEmbedding(false)
|
||||||
|
} finally {
|
||||||
|
setAbortController(null)
|
||||||
|
setEmbeddingController(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const normalChatMode = async (
|
const normalChatMode = async (
|
||||||
message: string,
|
message: string,
|
||||||
image: string,
|
image: string,
|
||||||
@ -480,8 +714,9 @@ export const useMessage = () => {
|
|||||||
seed: currentChatModelSettings?.seed,
|
seed: currentChatModelSettings?.seed,
|
||||||
numGpu:
|
numGpu:
|
||||||
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
|
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
|
||||||
numPredict: currentChatModelSettings?.numPredict ?? userDefaultModelSettings?.numPredict,
|
numPredict:
|
||||||
|
currentChatModelSettings?.numPredict ??
|
||||||
|
userDefaultModelSettings?.numPredict
|
||||||
})
|
})
|
||||||
|
|
||||||
let newMessage: Message[] = []
|
let newMessage: Message[] = []
|
||||||
@ -575,9 +810,7 @@ export const useMessage = () => {
|
|||||||
signal: signal,
|
signal: signal,
|
||||||
callbacks: [
|
callbacks: [
|
||||||
{
|
{
|
||||||
handleLLMEnd(
|
handleLLMEnd(output: any): any {
|
||||||
output: any,
|
|
||||||
): any {
|
|
||||||
try {
|
try {
|
||||||
generationInfo = output?.generations?.[0][0]?.generationInfo
|
generationInfo = output?.generations?.[0][0]?.generationInfo
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -709,8 +942,9 @@ export const useMessage = () => {
|
|||||||
seed: currentChatModelSettings?.seed,
|
seed: currentChatModelSettings?.seed,
|
||||||
numGpu:
|
numGpu:
|
||||||
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
|
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
|
||||||
numPredict: currentChatModelSettings?.numPredict ?? userDefaultModelSettings?.numPredict,
|
numPredict:
|
||||||
|
currentChatModelSettings?.numPredict ??
|
||||||
|
userDefaultModelSettings?.numPredict
|
||||||
})
|
})
|
||||||
|
|
||||||
let newMessage: Message[] = []
|
let newMessage: Message[] = []
|
||||||
@ -785,9 +1019,11 @@ export const useMessage = () => {
|
|||||||
userDefaultModelSettings?.numCtx,
|
userDefaultModelSettings?.numCtx,
|
||||||
seed: currentChatModelSettings?.seed,
|
seed: currentChatModelSettings?.seed,
|
||||||
numGpu:
|
numGpu:
|
||||||
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
|
currentChatModelSettings?.numGpu ??
|
||||||
numPredict: currentChatModelSettings?.numPredict ?? userDefaultModelSettings?.numPredict,
|
userDefaultModelSettings?.numGpu,
|
||||||
|
numPredict:
|
||||||
|
currentChatModelSettings?.numPredict ??
|
||||||
|
userDefaultModelSettings?.numPredict
|
||||||
})
|
})
|
||||||
const response = await questionOllama.invoke(promptForQuestion)
|
const response = await questionOllama.invoke(promptForQuestion)
|
||||||
query = response.content.toString()
|
query = response.content.toString()
|
||||||
@ -840,9 +1076,7 @@ export const useMessage = () => {
|
|||||||
signal: signal,
|
signal: signal,
|
||||||
callbacks: [
|
callbacks: [
|
||||||
{
|
{
|
||||||
handleLLMEnd(
|
handleLLMEnd(output: any): any {
|
||||||
output: any,
|
|
||||||
): any {
|
|
||||||
try {
|
try {
|
||||||
generationInfo = output?.generations?.[0][0]?.generationInfo
|
generationInfo = output?.generations?.[0][0]?.generationInfo
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -975,8 +1209,9 @@ export const useMessage = () => {
|
|||||||
seed: currentChatModelSettings?.seed,
|
seed: currentChatModelSettings?.seed,
|
||||||
numGpu:
|
numGpu:
|
||||||
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
|
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
|
||||||
numPredict: currentChatModelSettings?.numPredict ?? userDefaultModelSettings?.numPredict,
|
numPredict:
|
||||||
|
currentChatModelSettings?.numPredict ??
|
||||||
|
userDefaultModelSettings?.numPredict
|
||||||
})
|
})
|
||||||
|
|
||||||
let newMessage: Message[] = []
|
let newMessage: Message[] = []
|
||||||
@ -1050,9 +1285,7 @@ export const useMessage = () => {
|
|||||||
signal: signal,
|
signal: signal,
|
||||||
callbacks: [
|
callbacks: [
|
||||||
{
|
{
|
||||||
handleLLMEnd(
|
handleLLMEnd(output: any): any {
|
||||||
output: any,
|
|
||||||
): any {
|
|
||||||
try {
|
try {
|
||||||
generationInfo = output?.generations?.[0][0]?.generationInfo
|
generationInfo = output?.generations?.[0][0]?.generationInfo
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -1214,6 +1447,15 @@ export const useMessage = () => {
|
|||||||
signal
|
signal
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else if (chatMode === "vision") {
|
||||||
|
await visionChatMode(
|
||||||
|
message,
|
||||||
|
image,
|
||||||
|
isRegenerate,
|
||||||
|
chatHistory || messages,
|
||||||
|
memory || history,
|
||||||
|
signal
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
const newEmbeddingController = new AbortController()
|
const newEmbeddingController = new AbortController()
|
||||||
let embeddingSignal = newEmbeddingController.signal
|
let embeddingSignal = newEmbeddingController.signal
|
||||||
|
@ -3,6 +3,7 @@ import { initReactI18next } from "react-i18next";
|
|||||||
import { en } from "./lang/en";
|
import { en } from "./lang/en";
|
||||||
import { pt } from "./lang/pt";
|
import { pt } from "./lang/pt";
|
||||||
import { fr } from "./lang/fr";
|
import { fr } from "./lang/fr";
|
||||||
|
import { uk } from "./lang/uk";
|
||||||
import { ru } from "./lang/ru";
|
import { ru } from "./lang/ru";
|
||||||
import { ml } from "./lang/ml";
|
import { ml } from "./lang/ml";
|
||||||
import { zh } from "./lang/zh";
|
import { zh } from "./lang/zh";
|
||||||
@ -28,6 +29,8 @@ i18n
|
|||||||
ml: ml,
|
ml: ml,
|
||||||
"pt-BR": pt,
|
"pt-BR": pt,
|
||||||
"zh-CN": zh,
|
"zh-CN": zh,
|
||||||
|
uk: uk,
|
||||||
|
"uk-UA": uk,
|
||||||
ru: ru,
|
ru: ru,
|
||||||
"ru-RU": ru,
|
"ru-RU": ru,
|
||||||
zh: zh,
|
zh: zh,
|
||||||
|
19
src/i18n/lang/uk.ts
Normal file
19
src/i18n/lang/uk.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import option from "@/assets/locale/uk/option.json";
|
||||||
|
import playground from "@/assets/locale/uk/playground.json";
|
||||||
|
import common from "@/assets/locale/uk/common.json";
|
||||||
|
import sidepanel from "@/assets/locale/uk/sidepanel.json";
|
||||||
|
import settings from "@/assets/locale/uk/settings.json";
|
||||||
|
import knowledge from "@/assets/locale/uk/knowledge.json";
|
||||||
|
import chrome from "@/assets/locale/uk/chrome.json";
|
||||||
|
import openai from "@/assets/locale/uk/openai.json";
|
||||||
|
|
||||||
|
export const uk = {
|
||||||
|
option,
|
||||||
|
playground,
|
||||||
|
common,
|
||||||
|
sidepanel,
|
||||||
|
settings,
|
||||||
|
knowledge,
|
||||||
|
chrome,
|
||||||
|
openai
|
||||||
|
}
|
@ -16,6 +16,10 @@ export const supportLanguage = [
|
|||||||
label: "Italiano",
|
label: "Italiano",
|
||||||
value: "it"
|
value: "it"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Ukrainian",
|
||||||
|
value: "uk"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Russian",
|
label: "Russian",
|
||||||
value: "ru"
|
value: "ru"
|
||||||
|
104
src/libs/PAMemoryVectorStore.ts
Normal file
104
src/libs/PAMemoryVectorStore.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
|
||||||
|
import { similarity as ml_distance_similarity } from "ml-distance"
|
||||||
|
import { VectorStore } from "@langchain/core/vectorstores"
|
||||||
|
import type { EmbeddingsInterface } from "@langchain/core/embeddings"
|
||||||
|
import { Document, DocumentInterface } from "@langchain/core/documents"
|
||||||
|
import { rerankDocs } from "../utils/rerank"
|
||||||
|
|
||||||
|
interface MemoryVector {
|
||||||
|
content: string
|
||||||
|
embedding: number[]
|
||||||
|
metadata: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MemoryVectorStoreArgs {
|
||||||
|
similarity?: typeof ml_distance_similarity.cosine
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PAMemoryVectorStore extends VectorStore {
|
||||||
|
|
||||||
|
|
||||||
|
declare FilterType: (doc: Document) => boolean
|
||||||
|
|
||||||
|
private memoryVectors: MemoryVector[] = []
|
||||||
|
private similarity: typeof ml_distance_similarity.cosine
|
||||||
|
|
||||||
|
constructor(embeddings: EmbeddingsInterface, args?: MemoryVectorStoreArgs) {
|
||||||
|
super(embeddings, args)
|
||||||
|
this.similarity = args?.similarity ?? ml_distance_similarity.cosine
|
||||||
|
}
|
||||||
|
|
||||||
|
_vectorstoreType(): string {
|
||||||
|
return "memory"
|
||||||
|
}
|
||||||
|
|
||||||
|
async addVectors(vectors: number[][], documents: DocumentInterface[], options?: { [x: string]: any }): Promise<void> {
|
||||||
|
const memoryVectors = documents.map((doc, index) => ({
|
||||||
|
content: doc.pageContent,
|
||||||
|
embedding: vectors[index],
|
||||||
|
metadata: doc.metadata
|
||||||
|
}))
|
||||||
|
|
||||||
|
this.memoryVectors.push(...memoryVectors)
|
||||||
|
}
|
||||||
|
similaritySearchVectorWithScore(query: number[], k: number, filter?: this["FilterType"]): Promise<[DocumentInterface, number][]> {
|
||||||
|
throw new Error("Method not implemented.")
|
||||||
|
}
|
||||||
|
|
||||||
|
async addDocuments(documents: Document[]): Promise<void> {
|
||||||
|
const texts = documents.map((doc) => doc.pageContent)
|
||||||
|
const embeddings = await this.embeddings.embedDocuments(texts)
|
||||||
|
await this.addVectors(embeddings, documents)
|
||||||
|
}
|
||||||
|
|
||||||
|
async similaritySearch(query: string, k = 4): Promise<Document[]> {
|
||||||
|
const queryEmbedding = await this.embeddings.embedQuery(query)
|
||||||
|
|
||||||
|
const similarities = this.memoryVectors.map((vector) => ({
|
||||||
|
similarity: this.similarity(queryEmbedding, vector.embedding),
|
||||||
|
document: vector
|
||||||
|
}))
|
||||||
|
|
||||||
|
similarities.sort((a, b) => b.similarity - a.similarity)
|
||||||
|
const topK = similarities.slice(0, k)
|
||||||
|
|
||||||
|
const docs = topK.map(({ document }) =>
|
||||||
|
new Document({
|
||||||
|
pageContent: document.content,
|
||||||
|
metadata: document.metadata
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return docs
|
||||||
|
}
|
||||||
|
|
||||||
|
async similaritySearchWithScore(query: string, k = 4): Promise<[Document, number][]> {
|
||||||
|
const queryEmbedding = await this.embeddings.embedQuery(query)
|
||||||
|
|
||||||
|
const similarities = this.memoryVectors.map((vector) => ({
|
||||||
|
similarity: this.similarity(queryEmbedding, vector.embedding),
|
||||||
|
document: vector
|
||||||
|
}))
|
||||||
|
|
||||||
|
similarities.sort((a, b) => b.similarity - a.similarity)
|
||||||
|
const topK = similarities.slice(0, k)
|
||||||
|
|
||||||
|
return topK.map(({ document, similarity }) => [
|
||||||
|
new Document({
|
||||||
|
pageContent: document.content,
|
||||||
|
metadata: document.metadata
|
||||||
|
}),
|
||||||
|
similarity
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fromDocuments(
|
||||||
|
docs: Document[],
|
||||||
|
embeddings: EmbeddingsInterface,
|
||||||
|
args?: MemoryVectorStoreArgs
|
||||||
|
): Promise<PAMemoryVectorStore> {
|
||||||
|
const store = new PAMemoryVectorStore(embeddings, args)
|
||||||
|
await store.addDocuments(docs)
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,6 @@ import { VectorStore } from "@langchain/core/vectorstores"
|
|||||||
import type { EmbeddingsInterface } from "@langchain/core/embeddings"
|
import type { EmbeddingsInterface } from "@langchain/core/embeddings"
|
||||||
import { Document } from "@langchain/core/documents"
|
import { Document } from "@langchain/core/documents"
|
||||||
import { getVector, insertVector } from "@/db/vector"
|
import { getVector, insertVector } from "@/db/vector"
|
||||||
import { cp } from "fs"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface representing a vector in memory. It includes the content
|
* Interface representing a vector in memory. It includes the content
|
||||||
* (text), the corresponding embedding (vector), and any associated
|
* (text), the corresponding embedding (vector), and any associated
|
||||||
|
46
src/libs/get-screenshot.ts
Normal file
46
src/libs/get-screenshot.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
const captureVisibleTab = () => {
|
||||||
|
const result = new Promise<string>((resolve) => {
|
||||||
|
if (import.meta.env.BROWSER === "chrome") {
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
|
||||||
|
const tab = tabs[0]
|
||||||
|
chrome.tabs.captureVisibleTab(null, { format: "png" }, (dataUrl) => {
|
||||||
|
resolve(dataUrl)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
browser.tabs
|
||||||
|
.query({ active: true, currentWindow: true })
|
||||||
|
.then(async (tabs) => {
|
||||||
|
const dataUrl = (await Promise.race([
|
||||||
|
browser.tabs.captureVisibleTab(null, { format: "png" }),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(
|
||||||
|
() => reject(new Error("Screenshot capture timed out")),
|
||||||
|
10000
|
||||||
|
)
|
||||||
|
)
|
||||||
|
])) as string
|
||||||
|
resolve(dataUrl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getScreenshotFromCurrentTab = async () => {
|
||||||
|
try {
|
||||||
|
const screenshotDataUrl = await captureVisibleTab()
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
screenshot: screenshotDataUrl,
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
screenshot: null,
|
||||||
|
error:
|
||||||
|
error instanceof Error ? error.message : "Failed to capture screenshot"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -102,6 +102,7 @@ export const getAllModels = async ({
|
|||||||
returnEmpty?: boolean
|
returnEmpty?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const baseUrl = await getOllamaURL()
|
const baseUrl = await getOllamaURL()
|
||||||
const response = await fetcher(`${cleanUrl(baseUrl)}/api/tags`)
|
const response = await fetcher(`${cleanUrl(baseUrl)}/api/tags`)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -178,30 +179,9 @@ export const fetchChatModels = async ({
|
|||||||
returnEmpty?: boolean
|
returnEmpty?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const baseUrl = await getOllamaURL()
|
|
||||||
const response = await fetcher(`${cleanUrl(baseUrl)}/api/tags`)
|
const models = await getAllModels({ returnEmpty })
|
||||||
if (!response.ok) {
|
|
||||||
if (returnEmpty) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
throw new Error(response.statusText)
|
|
||||||
}
|
|
||||||
const json = await response.json()
|
|
||||||
const models = json.models as {
|
|
||||||
name: string
|
|
||||||
model: string
|
|
||||||
modified_at: string
|
|
||||||
size: number
|
|
||||||
digest: string
|
|
||||||
details?: {
|
|
||||||
parent_model?: string
|
|
||||||
format: string
|
|
||||||
family: string
|
|
||||||
families: string[]
|
|
||||||
parameter_size: string
|
|
||||||
quantization_level: string
|
|
||||||
}
|
|
||||||
}[]
|
|
||||||
const chatModels = models
|
const chatModels = models
|
||||||
?.filter((model) => {
|
?.filter((model) => {
|
||||||
return (
|
return (
|
||||||
@ -408,3 +388,13 @@ export const getPageShareUrl = async () => {
|
|||||||
export const setPageShareUrl = async (pageShareUrl: string) => {
|
export const setPageShareUrl = async (pageShareUrl: string) => {
|
||||||
await storage.set("pageShareUrl", pageShareUrl)
|
await storage.set("pageShareUrl", pageShareUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const isOllamaEnabled = async () => {
|
||||||
|
const ollamaStatus = await storage.get<boolean>("checkOllamaStatus")
|
||||||
|
// if data is empty or null then return true
|
||||||
|
if (typeof ollamaStatus === "undefined" || ollamaStatus === null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return ollamaStatus
|
||||||
|
}
|
@ -32,8 +32,8 @@ type State = {
|
|||||||
setIsProcessing: (isProcessing: boolean) => void
|
setIsProcessing: (isProcessing: boolean) => void
|
||||||
selectedModel: string | null
|
selectedModel: string | null
|
||||||
setSelectedModel: (selectedModel: string) => void
|
setSelectedModel: (selectedModel: string) => void
|
||||||
chatMode: "normal" | "rag"
|
chatMode: "normal" | "rag" | "vision"
|
||||||
setChatMode: (chatMode: "normal" | "rag") => void
|
setChatMode: (chatMode: "normal" | "rag" | "vision") => void
|
||||||
isEmbedding: boolean
|
isEmbedding: boolean
|
||||||
setIsEmbedding: (isEmbedding: boolean) => void
|
setIsEmbedding: (isEmbedding: boolean) => void
|
||||||
speechToTextLanguage: string
|
speechToTextLanguage: string
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { PageAssistHtmlLoader } from "~/loader/html"
|
import { PageAssistHtmlLoader } from "~/loader/html"
|
||||||
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
|
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
|
||||||
import { MemoryVectorStore } from "langchain/vectorstores/memory"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
defaultEmbeddingChunkOverlap,
|
defaultEmbeddingChunkOverlap,
|
||||||
defaultEmbeddingChunkSize
|
defaultEmbeddingChunkSize
|
||||||
} from "@/services/ollama"
|
} from "@/services/ollama"
|
||||||
import { PageAssistPDFLoader } from "@/loader/pdf"
|
import { PageAssistPDFLoader } from "@/loader/pdf"
|
||||||
|
import { PAMemoryVectorStore } from "@/libs/PAMemoryVectorStore"
|
||||||
|
|
||||||
export const getLoader = ({
|
export const getLoader = ({
|
||||||
html,
|
html,
|
||||||
@ -46,10 +46,10 @@ export const memoryEmbedding = async ({
|
|||||||
html: string
|
html: string
|
||||||
type: string
|
type: string
|
||||||
pdf: { content: string; page: number }[]
|
pdf: { content: string; page: number }[]
|
||||||
keepTrackOfEmbedding: Record<string, MemoryVectorStore>
|
keepTrackOfEmbedding: Record<string, PAMemoryVectorStore>
|
||||||
ollamaEmbedding: any
|
ollamaEmbedding: any
|
||||||
setIsEmbedding: (value: boolean) => void
|
setIsEmbedding: (value: boolean) => void
|
||||||
setKeepTrackOfEmbedding: (value: Record<string, MemoryVectorStore>) => void
|
setKeepTrackOfEmbedding: (value: Record<string, PAMemoryVectorStore>) => void
|
||||||
}) => {
|
}) => {
|
||||||
setIsEmbedding(true)
|
setIsEmbedding(true)
|
||||||
const loader = getLoader({ html, pdf, type, url })
|
const loader = getLoader({ html, pdf, type, url })
|
||||||
@ -63,7 +63,7 @@ export const memoryEmbedding = async ({
|
|||||||
|
|
||||||
const chunks = await textSplitter.splitDocuments(docs)
|
const chunks = await textSplitter.splitDocuments(docs)
|
||||||
|
|
||||||
const store = new MemoryVectorStore(ollamaEmbedding)
|
const store = new PAMemoryVectorStore(ollamaEmbedding)
|
||||||
|
|
||||||
await store.addDocuments(chunks)
|
await store.addDocuments(chunks)
|
||||||
setKeepTrackOfEmbedding({
|
setKeepTrackOfEmbedding({
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { Embeddings } from "@langchain/core/embeddings"
|
import type { EmbeddingsInterface } from "@langchain/core/embeddings"
|
||||||
import type { Document } from "@langchain/core/documents"
|
import type { Document } from "@langchain/core/documents"
|
||||||
import * as ml_distance from "ml-distance"
|
import * as ml_distance from "ml-distance"
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ export const rerankDocs = async ({
|
|||||||
}: {
|
}: {
|
||||||
query: string
|
query: string
|
||||||
docs: Document[]
|
docs: Document[]
|
||||||
embedding: Embeddings
|
embedding: EmbeddingsInterface
|
||||||
}) => {
|
}) => {
|
||||||
if (docs.length === 0) {
|
if (docs.length === 0) {
|
||||||
return docs
|
return docs
|
||||||
@ -34,6 +34,7 @@ export const rerankDocs = async ({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log("similarity", similarity)
|
||||||
const sortedDocs = similarity
|
const sortedDocs = similarity
|
||||||
.sort((a, b) => b.similarity - a.similarity)
|
.sort((a, b) => b.similarity - a.similarity)
|
||||||
.filter((sim) => sim.similarity > 0.5)
|
.filter((sim) => sim.similarity > 0.5)
|
||||||
|
@ -50,7 +50,7 @@ export default defineConfig({
|
|||||||
outDir: "build",
|
outDir: "build",
|
||||||
|
|
||||||
manifest: {
|
manifest: {
|
||||||
version: "1.3.4",
|
version: "1.3.5",
|
||||||
name:
|
name:
|
||||||
process.env.TARGET === "firefox"
|
process.env.TARGET === "firefox"
|
||||||
? "Page Assist - A Web UI for Local AI Models"
|
? "Page Assist - A Web UI for Local AI Models"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user