Merge pull request #251 from n4ze3m/next

v1.3.5
This commit is contained in:
Muhammed Nazeem 2024-11-23 17:36:33 +05:30 committed by GitHub
commit 7e796e7c58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
59 changed files with 1701 additions and 145 deletions

View File

@ -25,5 +25,6 @@
"stopStreaming": "Stop Streaming",
"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?"
}

View File

@ -32,6 +32,9 @@
},
"generateTitle": {
"label": "Generer titel med AI"
},
"ollamaStatus": {
"label": "Aktivér eller deaktivér Ollama forbindelsesstatus kontrol"
}
},
"sidepanelRag": {

View File

@ -25,5 +25,6 @@
"stopStreaming": "Streaming stoppen",
"knowledge": "Wissen"
},
"sendWhenEnter": "Senden bei Drücken der Eingabetaste"
"sendWhenEnter": "Senden bei Drücken der Eingabetaste",
"welcome": "Hallo! Wie kann ich Ihnen heute helfen?"
}

View File

@ -32,6 +32,9 @@
},
"generateTitle": {
"label": "Titel mit KI generieren"
},
"ollamaStatus": {
"label": "Ollama-Verbindungsstatus-Überprüfung aktivieren oder deaktivieren"
}
},
"sidepanelRag": {

View File

@ -23,7 +23,9 @@
"speechToText": "Speech to Text",
"uploadImage": "Upload Image",
"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?"
}

View File

@ -32,6 +32,9 @@
},
"generateTitle": {
"label": "Generate Title using AI"
},
"ollamaStatus" :{
"label":"Enable or disable Ollama connection status check"
}
},
"sidepanelRag": {

View File

@ -25,5 +25,6 @@
"stopStreaming": "Parar Transmisión",
"knowledge": "Conocimiento"
},
"sendWhenEnter": "Enviar cuando presione Enter"
"sendWhenEnter": "Enviar cuando presione Enter",
"welcome": "¡Hola! ¿Cómo puedo ayudarte hoy?"
}

View File

@ -32,6 +32,9 @@
},
"generateTitle": {
"label": "Generar título usando IA"
},
"ollamaStatus": {
"label": "Habilitar o deshabilitar la verificación del estado de conexión de Ollama"
}
},
"sidepanelRag": {

View File

@ -25,5 +25,6 @@
"stopStreaming": "توقف Streaming",
"knowledge": "دانش"
},
"sendWhenEnter": "با فشار دادن Enter ارسال شود"
"sendWhenEnter": "با فشار دادن Enter ارسال شود",
"welcome": "سلام! امروز چطور می‌توانم به شما کمک کنم؟"
}

View File

@ -32,6 +32,9 @@
},
"generateTitle": {
"label": "تولید عنوان با استفاده از هوش مصنوعی"
},
"ollamaStatus": {
"label": "فعال یا غیرفعال کردن بررسی وضعیت اتصال Ollama"
}
},
"sidepanelRag": {

View File

@ -25,5 +25,6 @@
"stopStreaming": "Arrêtez la diffusion",
"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 ?"
}

View File

@ -32,6 +32,9 @@
},
"generateTitle": {
"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": {

View File

@ -25,5 +25,6 @@
"stopStreaming": "Ferma lo Streaming",
"knowledge": "Conoscenza"
},
"sendWhenEnter": "Invia subito dopo Enter"
"sendWhenEnter": "Invia subito dopo Enter",
"welcome": "Ciao! Come posso aiutarti oggi?"
}

View File

@ -32,6 +32,9 @@
},
"generateTitle": {
"label": "Genera titolo utilizzando l'IA"
},
"ollamaStatus": {
"label": "Abilita o disabilita il controllo dello stato della connessione Ollama"
}
},
"sidepanelRag": {

View File

@ -25,5 +25,6 @@
"stopStreaming": "ストリーミングを停止",
"knowledge": "知識"
},
"sendWhenEnter": "Enterキーを押すと送信"
"sendWhenEnter": "Enterキーを押すと送信",
"welcome": "こんにちは!本日はどのようなお手伝いができますか?"
}

View File

@ -35,6 +35,9 @@
},
"generateTitle": {
"label": "AIを使用してタイトルを生成"
},
"ollamaStatus": {
"label": "Ollamaの接続状態チェックを有効または無効にする"
}
},
"sidepanelRag": {

View File

@ -25,5 +25,6 @@
"stopStreaming": "스트리밍 중지",
"knowledge": "지식"
},
"sendWhenEnter": "Enter 키를 누르면 전송"
"sendWhenEnter": "Enter 키를 누르면 전송",
"welcome": "안녕하세요! 오늘 어떻게 도와드릴까요?"
}

View File

@ -35,6 +35,9 @@
},
"generateTitle": {
"label": "AI로 제목 생성"
},
"ollamaStatus": {
"label": "Ollama 연결 상태 확인 활성화 또는 비활성화"
}
},
"sidepanelRag": {
@ -342,4 +345,3 @@
"title": "Chrome AI 설정"
}
}

View File

@ -25,5 +25,6 @@
"stopStreaming": "സ്ട്രീമിംഗ് നിർത്തുക",
"knowledge": "അറിവ്"
},
"sendWhenEnter": "എന്റര്‍ അമര്‍ത്തുമ്പോള്‍ അയയ്ക്കുക"
"sendWhenEnter": "എന്റര്‍ അമര്‍ത്തുമ്പോള്‍ അയയ്ക്കുക",
"welcome": "നമസ്കാരം! ഇന്ന് എനിക്ക് നിങ്ങളെ എങ്ങനെ സഹായിക്കാൻ കഴിയും?"
}

View File

@ -35,6 +35,9 @@
},
"generateTitle": {
"label": "എഐ ഉപയോഗിച്ച് ശീർഷകം സൃഷ്ടിക്കുക"
},
"ollamaStatus": {
"label": "ഒല്ലാമ കണക്ഷൻ സ്റ്റാറ്റസ് പരിശോധന പ്രവർത്തനക്ഷമമാക്കുകയോ പ്രവർത്തനരഹിതമാക്കുകയോ ചെയ്യുക"
}
},
"sidepanelRag": {

View File

@ -25,5 +25,6 @@
"stopStreaming": "Stopp Streaming",
"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?"
}

View File

@ -32,6 +32,9 @@
},
"generateTitle": {
"label": "Generer tittel med AI"
},
"ollamaStatus": {
"label": "Aktiver eller deaktiver Ollama tilkoblingsstatussjekk"
}
},
"sidepanelRag": {

View File

@ -25,5 +25,6 @@
"stopStreaming": "Parar Streaming",
"knowledge": "Conhecimento"
},
"sendWhenEnter": "Enviar ao pressionar Enter"
"sendWhenEnter": "Enviar ao pressionar Enter",
"welcome": "Olá! Como posso ajudar você hoje?"
}

View File

@ -32,6 +32,9 @@
},
"generateTitle": {
"label": "Gerar título usando IA"
},
"ollamaStatus": {
"label": "Ativar ou desativar verificação de status da conexão Ollama"
}
},
"sidepanelRag": {

View File

@ -25,5 +25,6 @@
"stopStreaming": "Остановить поток",
"knowledge": "Знание"
},
"sendWhenEnter": "Отправить при нажатии клавиши Enter"
"sendWhenEnter": "Отправить при нажатии клавиши Enter",
"welcome": "Здравствуйте! Как я могу помочь вам сегодня?"
}

View File

@ -32,6 +32,9 @@
},
"generateTitle": {
"label": "Сгенерировать заголовок с помощью ИИ"
},
"ollamaStatus": {
"label": "Включить или отключить проверку состояния подключения Ollama"
}
},
"sidepanelRag": {

View File

@ -25,5 +25,6 @@
"stopStreaming": "Stoppa strömning",
"knowledge": "Kunskap"
},
"sendWhenEnter": "Skicka när Enter trycks"
"sendWhenEnter": "Skicka när Enter trycks",
"welcome": "Hej! Hur kan jag hjälpa dig idag?"
}

View File

@ -32,6 +32,9 @@
},
"generateTitle": {
"label": "Generera titel med AI"
},
"ollamaStatus": {
"label": "Aktivera eller inaktivera Ollama anslutningsstatuskontroll"
}
},
"sidepanelRag": {

View 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 та перезапустіть переглядач."
}

View 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": "Інформація про генерацію"
}

View 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 спочатку"
}

View 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 та інших повʼязаних завдань семантичного пошуку."
}
}

View File

@ -0,0 +1,13 @@
{
"newChat": "Новий чат",
"selectAPrompt": "Виберіть запит",
"githubRepository": "Репозиторій GitHub",
"settings": "Налаштування",
"sidebarTitle": "Історія чату",
"error": "Збій",
"somethingWentWrong": "Щось пішло не так",
"validationSelectModel": "Будь ласка, виберіть модель для продовження",
"deleteHistoryConfirmation": "Ви впевнені, що хочете видалити цю історію?",
"editHistoryTitle": "Введіть нову назву",
"temporaryChat": "Тимчасовий чат"
}

View 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": "Вітаю! Як я можу допомогти вам сьогодні?"
}

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

View File

@ -0,0 +1,7 @@
{
"tooltip": {
"embed": "Може знадобитися кілька хвилин для вкладення сторінки у базу. Будь ласка, зачекайте...",
"clear": "Стерти історію чату",
"history": "Історія чату"
}
}

View File

@ -25,5 +25,6 @@
"stopStreaming": "停止流媒体",
"knowledge": "知识"
},
"sendWhenEnter": "按Enter发送"
"sendWhenEnter": "按Enter发送",
"welcome": "你好!今天我能帮你什么?"
}

View File

@ -35,6 +35,9 @@
},
"generateTitle": {
"label": "使用人工智能生成标题"
},
"ollamaStatus": {
"label": "启用或禁用Ollama连接状态检查"
}
},
"sidepanelRag": {

View File

@ -13,7 +13,7 @@ type Props = {
}
export const MessageSource: React.FC<Props> = ({ source, onSourceClick }) => {
if (source?.mode === "rag") {
if (source?.mode === "rag" || source?.mode === "chat") {
return (
<button
onClick={() => {

View File

@ -22,6 +22,7 @@ import { getAllPrompts } from "@/db"
import { ShareBtn } from "~/components/Common/ShareBtn"
import { ProviderIcons } from "../Common/ProviderIcon"
import { NewChat } from "./NewChat"
import { PageAssistSelect } from "../Select"
type Props = {
setSidebarOpen: (open: boolean) => void
setOpenModelSettings: (open: boolean) => void
@ -49,14 +50,10 @@ export const Header: React.FC<Props> = ({
historyId,
temporaryChat
} = useMessageOption()
const {
data: models,
isLoading: isModelsLoading,
} = useQuery({
const { data: models, isLoading: isModelsLoading, refetch } = useQuery({
queryKey: ["fetchModel"],
queryFn: () => fetchChatModels({ returnEmpty: true }),
refetchInterval: 15_000,
refetchIntervalInBackground: true,
refetchIntervalInBackground: false,
placeholderData: (prev) => prev
})
@ -87,9 +84,10 @@ export const Header: React.FC<Props> = ({
}
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 ${
temporaryChat && "!bg-gray-200 dark:!bg-black"
}`}>
<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"
}`}>
<div className="flex gap-2 items-center">
{pathname !== "/" && (
<div>
@ -107,41 +105,39 @@ export const Header: React.FC<Props> = ({
<PanelLeftIcon className="w-6 h-6" />
</button>
</div>
<NewChat
clearChat={clearChat}
/>
<NewChat clearChat={clearChat} />
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
{"/"}
</span>
<div className="hidden lg:block">
<Select
<PageAssistSelect
className="w-80"
placeholder={t("common:selectAModel")}
loadingText={t("common:selectAModel")}
value={selectedModel}
onChange={(e) => {
setSelectedModel(e)
localStorage.setItem("selectedModel", e)
setSelectedModel(e.value)
localStorage.setItem("selectedModel", e.value)
}}
size="large"
loading={isModelsLoading}
filterOption={(input, option) =>
option.label.key.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
showSearch
placeholder={t("common:selectAModel")}
className="w-72"
isLoading={isModelsLoading}
options={models?.map((model) => ({
label: (
<span
key={model.model}
className="flex flex-row gap-3 items-center truncate">
className="flex flex-row gap-3 items-center ">
<ProviderIcons
provider={model?.provider}
className="w-5 h-5"
/>
<span className="truncate">{model.name}</span>
<span className="line-clamp-2">{model.name}</span>
</span>
),
value: model.model
}))}
onRefresh={() => {
refetch()
}}
/>
</div>
<div className="lg:hidden">

View File

@ -43,12 +43,12 @@ export const NewChat: React.FC<Props> = ({ clearChat }) => {
<div className="flex items-center justify-between">
<button
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" />
<span className="truncate ml-3">{t("newChat")}</span>
<span className="truncate ms-3">{t("newChat")}</span>
</button>
<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" />
</button>
</Dropdown>

View File

@ -1,4 +1,5 @@
import { cleanUrl } from "@/libs/clean-url"
import { useStorage } from "@plasmohq/storage/hook"
import { useQuery } from "@tanstack/react-query"
import { RotateCcw } from "lucide-react"
import { useEffect, useState } from "react"
@ -12,6 +13,9 @@ import {
export const PlaygroundEmpty = () => {
const [ollamaURL, setOllamaURL] = useState<string>("")
const { t } = useTranslation(["playground", "common"])
const [checkOllamaStatus] = useStorage("checkOllamaStatus", true)
const {
data: ollamaInfo,
status: ollamaStatus,
@ -23,19 +27,32 @@ export const PlaygroundEmpty = () => {
const ollamaURL = await getOllamaURL()
const isOk = await isOllamaRunning()
if (ollamaURL) {
saveOllamaURL(ollamaURL)
}
return {
isOk,
ollamaURL
}
}
},
enabled: checkOllamaStatus
})
useEffect(() => {
if (ollamaInfo?.ollamaURL) {
setOllamaURL(ollamaInfo.ollamaURL)
}
}, [ollamaInfo])
if (!checkOllamaStatus) {
return (
<div className="mx-auto sm:max-w-xl px-4 mt-10">
<div className="rounded-lg justify-center items-center flex flex-col border p-8 bg-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>
)
}
return (
<div className="mx-auto sm:max-w-xl px-4 mt-10">
<div className="rounded-lg justify-center items-center flex flex-col border p-8 bg-gray-50 dark:bg-[#262626] dark:border-gray-600">

View File

@ -16,13 +16,12 @@ import {
import { useStorage } from "@plasmohq/storage/hook"
export const GeneralSettings = () => {
const { clearChat } =
useMessageOption()
const { clearChat } = useMessageOption()
const [ speechToTextLanguage, setSpeechToTextLanguage ] = useStorage(
"speechToTextLanguage",
"en-US"
)
const [speechToTextLanguage, setSpeechToTextLanguage] = useStorage(
"speechToTextLanguage",
"en-US"
)
const [copilotResumeLastChat, setCopilotResumeLastChat] = useStorage(
"copilotResumeLastChat",
false
@ -41,6 +40,11 @@ export const GeneralSettings = () => {
const [sendNotificationAfterIndexing, setSendNotificationAfterIndexing] =
useStorage("sendNotificationAfterIndexing", false)
const [checkOllamaStatus, setCheckOllamaStatus] = useStorage(
"checkOllamaStatus",
true
)
const queryClient = useQueryClient()
const { mode, toggleDarkMode } = useDarkMode()
@ -160,6 +164,19 @@ export const GeneralSettings = () => {
/>
</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">
<span className="text-gray-700 dark:text-neutral-50 ">
{t("generalSettings.settings.darkMode.label")}

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

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

View File

@ -1,4 +1,5 @@
import { cleanUrl } from "@/libs/clean-url"
import { useStorage } from "@plasmohq/storage/hook"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { Select } from "antd"
import { RotateCcw } from "lucide-react"
@ -17,13 +18,15 @@ export const EmptySidePanel = () => {
const [ollamaURL, setOllamaURL] = useState<string>("")
const { t } = useTranslation(["playground", "common"])
const queryClient = useQueryClient()
const [checkOllamaStatus] = useStorage("checkOllamaStatus", true)
const {
data: ollamaInfo,
status: ollamaStatus,
refetch,
isRefetching
} = useQuery({
queryKey: ["ollamaStatus"],
queryKey: ["ollamaStatus", checkOllamaStatus],
queryFn: async () => {
const ollamaURL = await getOllamaURL()
const isOk = await isOllamaRunning()
@ -32,7 +35,7 @@ export const EmptySidePanel = () => {
queryKey: ["getAllModelsForSelect"]
})
return {
isOk,
isOk: checkOllamaStatus ? isOk : true,
models,
ollamaURL
}
@ -59,7 +62,7 @@ export const EmptySidePanel = () => {
</p>
</div>
)}
{!isRefetching && ollamaStatus === "success" ? (
{!isRefetching && ollamaStatus === "success" && checkOllamaStatus ? (
ollamaInfo.isOk ? (
<div className="inline-flex items-center space-x-2">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>

View File

@ -7,7 +7,14 @@ import { toBase64 } from "~/libs/to-base64"
import { Checkbox, Dropdown, Image, Switch, Tooltip } from "antd"
import { useWebUI } from "~/store/webui"
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 { ModelSelect } from "@/components/Common/ModelSelect"
import { useSpeechRecognition } from "@/hooks/useSpeechRecognition"
@ -36,7 +43,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
resetTranscript,
start: startListening,
stop: stopSpeechRecognition,
supported: browserSupportsSpeechRecognition,
supported: browserSupportsSpeechRecognition
} = useSpeechRecognition()
const stopListening = async () => {
@ -237,7 +244,10 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
}
}
await stopListening()
if (value.message.trim().length === 0 && value.image.length === 0) {
if (
value.message.trim().length === 0 &&
value.image.length === 0
) {
return
}
form.reset()
@ -281,20 +291,22 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
{...form.getInputProps("message")}
/>
<div className="flex mt-4 justify-end gap-3">
<Tooltip title={t("tooltip.searchInternet")}>
<button
type="button"
onClick={() => setWebSearch(!webSearch)}
className={`inline-flex items-center gap-2 ${
chatMode === "rag" ? "hidden" : "block"
}`}>
{webSearch ? (
<PiGlobe className="h-5 w-5 dark:text-gray-300" />
) : (
<PiGlobeX className="h-5 w-5 text-gray-600 dark:text-gray-400" />
)}
</button>
</Tooltip>
{chatMode !== "vision" && (
<Tooltip title={t("tooltip.searchInternet")}>
<button
type="button"
onClick={() => setWebSearch(!webSearch)}
className={`inline-flex items-center gap-2 ${
chatMode === "rag" ? "hidden" : "block"
}`}>
{webSearch ? (
<PiGlobe className="h-5 w-5 dark:text-gray-300" />
) : (
<PiGlobeX className="h-5 w-5 text-gray-600 dark:text-gray-400" />
)}
</button>
</Tooltip>
)}
<ModelSelect />
{browserSupportsSpeechRecognition && (
<Tooltip title={t("tooltip.speechToText")}>
@ -323,13 +335,35 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
</button>
</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")}>
<button
type="button"
onClick={() => {
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"
}`}>
<ImageIcon className="h-5 w-5" />

View File

@ -34,6 +34,9 @@ import { pageAssistModel } from "@/models"
import { getPrompt } from "@/services/application"
import { humanMessageFormatter } from "@/utils/human-message"
import { pageAssistEmbeddingModel } from "@/models/embedding"
import { PageAssistVectorStore } from "@/libs/PageAssistVectorStore"
import { PAMemoryVectorStore } from "@/libs/PAMemoryVectorStore"
import { getScreenshotFromCurrentTab } from "@/libs/get-screenshot"
export const useMessage = () => {
const {
@ -90,7 +93,7 @@ export const useMessage = () => {
)
const [keepTrackOfEmbedding, setKeepTrackOfEmbedding] = React.useState<{
[key: string]: MemoryVectorStore
[key: string]: PAMemoryVectorStore
}>({})
const clearChat = () => {
@ -134,8 +137,9 @@ export const useMessage = () => {
seed: currentChatModelSettings?.seed,
numGpu:
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
numPredict: currentChatModelSettings?.numPredict ?? userDefaultModelSettings?.numPredict,
numPredict:
currentChatModelSettings?.numPredict ??
userDefaultModelSettings?.numPredict
})
let newMessage: Message[] = []
@ -177,7 +181,7 @@ export const useMessage = () => {
let embedURL: string, embedHTML: string, embedType: string
let embedPDF: { content: string; page: number }[] = []
let isAlreadyExistEmbedding: MemoryVectorStore
let isAlreadyExistEmbedding: PAMemoryVectorStore
const {
content: html,
url: websiteUrl,
@ -212,7 +216,7 @@ export const useMessage = () => {
currentChatModelSettings?.keepAlive ??
userDefaultModelSettings?.keepAlive
})
let vectorstore: MemoryVectorStore
let vectorstore: PAMemoryVectorStore
try {
if (isAlreadyExistEmbedding) {
@ -263,9 +267,11 @@ export const useMessage = () => {
userDefaultModelSettings?.numCtx,
seed: currentChatModelSettings?.seed,
numGpu:
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
numPredict: currentChatModelSettings?.numPredict ?? userDefaultModelSettings?.numPredict,
currentChatModelSettings?.numGpu ??
userDefaultModelSettings?.numGpu,
numPredict:
currentChatModelSettings?.numPredict ??
userDefaultModelSettings?.numPredict
})
const response = await questionOllama.invoke(promptForQuestion)
query = response.content.toString()
@ -340,9 +346,7 @@ export const useMessage = () => {
signal: signal,
callbacks: [
{
handleLLMEnd(
output: any,
): any {
handleLLMEnd(output: any): any {
try {
generationInfo = output?.generations?.[0][0]?.generationInfo
} 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 (
message: string,
image: string,
@ -480,8 +714,9 @@ export const useMessage = () => {
seed: currentChatModelSettings?.seed,
numGpu:
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
numPredict: currentChatModelSettings?.numPredict ?? userDefaultModelSettings?.numPredict,
numPredict:
currentChatModelSettings?.numPredict ??
userDefaultModelSettings?.numPredict
})
let newMessage: Message[] = []
@ -575,9 +810,7 @@ export const useMessage = () => {
signal: signal,
callbacks: [
{
handleLLMEnd(
output: any,
): any {
handleLLMEnd(output: any): any {
try {
generationInfo = output?.generations?.[0][0]?.generationInfo
} catch (e) {
@ -709,8 +942,9 @@ export const useMessage = () => {
seed: currentChatModelSettings?.seed,
numGpu:
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
numPredict: currentChatModelSettings?.numPredict ?? userDefaultModelSettings?.numPredict,
numPredict:
currentChatModelSettings?.numPredict ??
userDefaultModelSettings?.numPredict
})
let newMessage: Message[] = []
@ -785,9 +1019,11 @@ export const useMessage = () => {
userDefaultModelSettings?.numCtx,
seed: currentChatModelSettings?.seed,
numGpu:
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
numPredict: currentChatModelSettings?.numPredict ?? userDefaultModelSettings?.numPredict,
currentChatModelSettings?.numGpu ??
userDefaultModelSettings?.numGpu,
numPredict:
currentChatModelSettings?.numPredict ??
userDefaultModelSettings?.numPredict
})
const response = await questionOllama.invoke(promptForQuestion)
query = response.content.toString()
@ -840,9 +1076,7 @@ export const useMessage = () => {
signal: signal,
callbacks: [
{
handleLLMEnd(
output: any,
): any {
handleLLMEnd(output: any): any {
try {
generationInfo = output?.generations?.[0][0]?.generationInfo
} catch (e) {
@ -975,8 +1209,9 @@ export const useMessage = () => {
seed: currentChatModelSettings?.seed,
numGpu:
currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,
numPredict: currentChatModelSettings?.numPredict ?? userDefaultModelSettings?.numPredict,
numPredict:
currentChatModelSettings?.numPredict ??
userDefaultModelSettings?.numPredict
})
let newMessage: Message[] = []
@ -1050,9 +1285,7 @@ export const useMessage = () => {
signal: signal,
callbacks: [
{
handleLLMEnd(
output: any,
): any {
handleLLMEnd(output: any): any {
try {
generationInfo = output?.generations?.[0][0]?.generationInfo
} catch (e) {
@ -1214,6 +1447,15 @@ export const useMessage = () => {
signal
)
}
} else if (chatMode === "vision") {
await visionChatMode(
message,
image,
isRegenerate,
chatHistory || messages,
memory || history,
signal
)
} else {
const newEmbeddingController = new AbortController()
let embeddingSignal = newEmbeddingController.signal

View File

@ -3,6 +3,7 @@ import { initReactI18next } from "react-i18next";
import { en } from "./lang/en";
import { pt } from "./lang/pt";
import { fr } from "./lang/fr";
import { uk } from "./lang/uk";
import { ru } from "./lang/ru";
import { ml } from "./lang/ml";
import { zh } from "./lang/zh";
@ -28,6 +29,8 @@ i18n
ml: ml,
"pt-BR": pt,
"zh-CN": zh,
uk: uk,
"uk-UA": uk,
ru: ru,
"ru-RU": ru,
zh: zh,

19
src/i18n/lang/uk.ts Normal file
View 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
}

View File

@ -16,6 +16,10 @@ export const supportLanguage = [
label: "Italiano",
value: "it"
},
{
label: "Ukrainian",
value: "uk"
},
{
label: "Russian",
value: "ru"

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

View File

@ -3,8 +3,6 @@ import { VectorStore } from "@langchain/core/vectorstores"
import type { EmbeddingsInterface } from "@langchain/core/embeddings"
import { Document } from "@langchain/core/documents"
import { getVector, insertVector } from "@/db/vector"
import { cp } from "fs"
/**
* Interface representing a vector in memory. It includes the content
* (text), the corresponding embedding (vector), and any associated

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

View File

@ -102,6 +102,7 @@ export const getAllModels = async ({
returnEmpty?: boolean
}) => {
try {
const baseUrl = await getOllamaURL()
const response = await fetcher(`${cleanUrl(baseUrl)}/api/tags`)
if (!response.ok) {
@ -178,30 +179,9 @@ export const fetchChatModels = async ({
returnEmpty?: boolean
}) => {
try {
const baseUrl = await getOllamaURL()
const response = await fetcher(`${cleanUrl(baseUrl)}/api/tags`)
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 models = await getAllModels({ returnEmpty })
const chatModels = models
?.filter((model) => {
return (
@ -408,3 +388,13 @@ export const getPageShareUrl = async () => {
export const setPageShareUrl = async (pageShareUrl: string) => {
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
}

View File

@ -32,8 +32,8 @@ type State = {
setIsProcessing: (isProcessing: boolean) => void
selectedModel: string | null
setSelectedModel: (selectedModel: string) => void
chatMode: "normal" | "rag"
setChatMode: (chatMode: "normal" | "rag") => void
chatMode: "normal" | "rag" | "vision"
setChatMode: (chatMode: "normal" | "rag" | "vision") => void
isEmbedding: boolean
setIsEmbedding: (isEmbedding: boolean) => void
speechToTextLanguage: string

View File

@ -1,12 +1,12 @@
import { PageAssistHtmlLoader } from "~/loader/html"
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
import { MemoryVectorStore } from "langchain/vectorstores/memory"
import {
defaultEmbeddingChunkOverlap,
defaultEmbeddingChunkSize
} from "@/services/ollama"
import { PageAssistPDFLoader } from "@/loader/pdf"
import { PAMemoryVectorStore } from "@/libs/PAMemoryVectorStore"
export const getLoader = ({
html,
@ -46,10 +46,10 @@ export const memoryEmbedding = async ({
html: string
type: string
pdf: { content: string; page: number }[]
keepTrackOfEmbedding: Record<string, MemoryVectorStore>
keepTrackOfEmbedding: Record<string, PAMemoryVectorStore>
ollamaEmbedding: any
setIsEmbedding: (value: boolean) => void
setKeepTrackOfEmbedding: (value: Record<string, MemoryVectorStore>) => void
setKeepTrackOfEmbedding: (value: Record<string, PAMemoryVectorStore>) => void
}) => {
setIsEmbedding(true)
const loader = getLoader({ html, pdf, type, url })
@ -63,7 +63,7 @@ export const memoryEmbedding = async ({
const chunks = await textSplitter.splitDocuments(docs)
const store = new MemoryVectorStore(ollamaEmbedding)
const store = new PAMemoryVectorStore(ollamaEmbedding)
await store.addDocuments(chunks)
setKeepTrackOfEmbedding({

View File

@ -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 * as ml_distance from "ml-distance"
@ -9,7 +9,7 @@ export const rerankDocs = async ({
}: {
query: string
docs: Document[]
embedding: Embeddings
embedding: EmbeddingsInterface
}) => {
if (docs.length === 0) {
return docs
@ -34,6 +34,7 @@ export const rerankDocs = async ({
}
})
console.log("similarity", similarity)
const sortedDocs = similarity
.sort((a, b) => b.similarity - a.similarity)
.filter((sim) => sim.similarity > 0.5)

View File

@ -50,7 +50,7 @@ export default defineConfig({
outDir: "build",
manifest: {
version: "1.3.4",
version: "1.3.5",
name:
process.env.TARGET === "firefox"
? "Page Assist - A Web UI for Local AI Models"