Compare commits
243 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 96a004d4d8 | |||
| 1fa16d73f0 | |||
| d2cd803a57 | |||
| e685953df7 | |||
| d6520e7fd8 | |||
| c54e3d5bf4 | |||
| a71b824728 | |||
| bc1a7ab567 | |||
| b565301a47 | |||
| 78272aed8d | |||
| 3f7913b36f | |||
| e1edf9227d | |||
| a8d709102f | |||
| 9032ab8857 | |||
| c9137edf5a | |||
| d11c373cf1 | |||
| 2755ef62d9 | |||
| 8debb71197 | |||
| 8b4867deb5 | |||
| a93645aee8 | |||
| 28ca6fb678 | |||
| da4586f27d | |||
| 591cd993c2 | |||
| aa1bb4fb6d | |||
| 5e7237b9cb | |||
| 84defafc14 | |||
| 7b1404f490 | |||
| 9b38788aa2 | |||
| c44fc82ecd | |||
| 8b6f422d45 | |||
| 2eb8c3456f | |||
| fa9835a7ad | |||
| 3db9d19ae3 | |||
| 762db11bf4 | |||
| f2b9a5f5bf | |||
| 277fc3feac | |||
| 9053bfdadf | |||
| 414ab53144 | |||
| 4a7d3a076c | |||
| 8198807fc9 | |||
| e556c02fe9 | |||
| f7de01cbc2 | |||
| 1bc96de620 | |||
| 23489105fc | |||
| 8c95a8be3a | |||
| d00c8b00df | |||
| 4bae43096b | |||
| d90e138773 | |||
| bb15eb016e | |||
| 97b39115a1 | |||
| cc53fd8379 | |||
| 5342063e24 | |||
| e274a0dc3e | |||
| 9c3920a489 | |||
| d1da3dde65 | |||
| 0ccd96c950 | |||
| 3499ec3f79 | |||
| 40ea18d54d | |||
| ffbc480bb4 | |||
| 9b839e5c9f | |||
| 575f23cde9 | |||
| 711317555c | |||
| 063e1ee46c | |||
| 0d0f8aac15 | |||
| 5cbb79fa6e | |||
| 277e7aead7 | |||
| d3558fdb33 | |||
| 1271d1dccc | |||
| 3be1cb98d3 | |||
| c2a7e7f510 | |||
| dbde628141 | |||
| 2412f31ed9 | |||
| 8df0c9e0dd | |||
| 6f9148ac4c | |||
| d7d8896e43 | |||
| d5a2a8a880 | |||
| cff8534f33 | |||
| f69bc57fed | |||
| 4ecc1c06d3 | |||
| f78e6a5fba | |||
| 411e6c2fe4 | |||
| 369816ace0 | |||
| 06e4b87c9e | |||
| c6b74a3cf9 | |||
| 27417dd771 | |||
| 16615010f7 | |||
| e516374d54 | |||
| 55dc6c1b3b | |||
| da47c2dfa3 | |||
| 694d1708a3 | |||
| f9aa03bc6d | |||
| bd3e5d7bfa | |||
| 5743df2280 | |||
| e3efe2c565 | |||
| 32c4748749 | |||
| f4a15f9590 | |||
| f92ef3211b | |||
| 508e8eb4b1 | |||
| dfc2fb65af | |||
| d7ce408d25 | |||
| 374f647048 | |||
| 00018085ed | |||
| d17cdc8068 | |||
| e5ad76615c | |||
| 54420f5300 | |||
| f076f81e0c | |||
| ea80285e9c | |||
| f43d255e8f | |||
| fc3a31e3d5 | |||
| 4e640daf83 | |||
| 404bb3fd67 | |||
| eacce66a82 | |||
| 4f19a92dc5 | |||
| 5196b65c2e | |||
| 340b716a90 | |||
| ff4cf16742 | |||
| 7eaf83cb7d | |||
| 4a73a01c24 | |||
| 1a9dee50ce | |||
| f97f6601f7 | |||
| 5ab5231c73 | |||
| 046dd02a6d | |||
| 72e1615fe1 | |||
| b512501d9d | |||
| 36cc3c24e6 | |||
| 62bfcf35e5 | |||
| 9bec2cea12 | |||
| f4a122a4bb | |||
| 34aea03c02 | |||
| e7fb86190a | |||
| 194564e253 | |||
| d4b10097d2 | |||
| 3069fa8074 | |||
| c2bbb37fc1 | |||
| e52568e7cb | |||
| bc4aeeeec8 | |||
| 25ee5f6cb0 | |||
| 882ad219ea | |||
| c108d863f6 | |||
| a8d80f936a | |||
| 1bb7fc7c92 | |||
| dbb83f9824 | |||
| e1889b0c5e | |||
| 88758af621 | |||
| 43f83673e2 | |||
| 950073c1de | |||
| 651c85fcc4 | |||
| ec7c5a3f07 | |||
| d077b3dcdb | |||
| 1d6bbdf917 | |||
| f248425931 | |||
| bdf7ed7f02 | |||
| 66200cfb7d | |||
| 9174e01d6a | |||
| f17df4f114 | |||
| 1948e78351 | |||
| 73a98ff938 | |||
| 92d056c982 | |||
| 2be9c25ba7 | |||
| 56fda46215 | |||
| 1d1b945483 | |||
| 9aac6e05a9 | |||
| 73e2912849 | |||
| 7cf80289c3 | |||
| 2fa594d5bd | |||
| 45f1a4157b | |||
| b3e4ecd2f4 | |||
| ab04e0dfd4 | |||
| ee625a0519 | |||
| fea563cf89 | |||
| fcfdfa3dea | |||
| 844923d0a4 | |||
| bbfa54a6b9 | |||
| e2429314f3 | |||
| 4925a6530b | |||
| 85484392b2 | |||
| 8d9712d66e | |||
| 0495f01acb | |||
| f28877f4db | |||
| b673ae7ab2 | |||
| 8c3a6eb262 | |||
| 25336f85f3 | |||
| 74ed74f1a4 | |||
| 94ebf02719 | |||
| 11bb1a6f7d | |||
| 61867c1545 | |||
| 4068a421bf | |||
| 8a94d8a226 | |||
| 5a24ce06f0 | |||
| e72170bac7 | |||
| dd93b0dd24 | |||
| 0ac04c9518 | |||
| d28925de05 | |||
| 2c4c62fb75 | |||
| de162a1f32 | |||
| aa7e5dbfce | |||
| 8d99bffbdc | |||
| 768941bded | |||
| e0772c6807 | |||
| cbd60168ea | |||
| c6c5c0ddb2 | |||
| 7c35447666 | |||
| 58b5e0bf3e | |||
| 0586d76b5d | |||
| eb3fcef6d7 | |||
| 59a730cb8b | |||
| 7fefbb316d | |||
| 14646e84ea | |||
| ba56edab19 | |||
| a233a8855b | |||
| c11fdad82d | |||
| 82f7b6c315 | |||
| 92d9b38110 | |||
| 36a66fcfc4 | |||
| ea27bc50f5 | |||
| 8a9c18ddd5 | |||
| 3729771f33 | |||
| ab58947a19 | |||
| f9351af6a3 | |||
| 8f03daeaf6 | |||
| de03bf834e | |||
| db3d48ee15 | |||
| 6022f6f5df | |||
| 2d1a890ed9 | |||
| 08443b3c55 | |||
| 7e761a69a7 | |||
| be08dbf00d | |||
| b4fca046a8 | |||
| 61287a16d5 | |||
| 16e72b2f67 | |||
| 80d173c1c3 | |||
| 4b459e372f | |||
| fddf693dad | |||
| a39ab4dd24 | |||
| 13db33e947 | |||
| 727a837e53 | |||
| 22d7ce2b7f | |||
| 1d4c358624 | |||
| a3ecf62564 | |||
| 6199296205 | |||
| 5ea88f2e4f | |||
| 8114fa93cf | |||
| 0cb8163321 |
@@ -11,7 +11,7 @@
|
||||
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
|
||||
- [ ] **Testing:** Have you written and run sufficient tests for validating the changes?
|
||||
- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
|
||||
- [ ] **Label:** To cleary categorize this pull request, assign a relevant label to the pull request title, using one of the following:
|
||||
- [ ] **Prefix:** To cleary categorize this pull request, prefix the pull request title, using one of the following:
|
||||
- **BREAKING CHANGE**: Significant changes that may affect compatibility
|
||||
- **build**: Changes that affect the build system or external dependencies
|
||||
- **ci**: Changes to our continuous integration processes or workflows
|
||||
|
||||
@@ -5,6 +5,80 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.3.0] - 2024-06-09
|
||||
|
||||
### Added
|
||||
|
||||
- **📚 Knowledge Support for Models**: Attach documents directly to models from the models workspace, enhancing the information available to each model.
|
||||
- **🎙️ Hands-Free Voice Call Feature**: Initiate voice calls without needing to use your hands, making interactions more seamless.
|
||||
- **📹 Video Call Feature**: Enable video calls with supported vision models like Llava and GPT-4o, adding a visual dimension to your communications.
|
||||
- **🎛️ Enhanced UI for Voice Recording**: Improved user interface for the voice recording feature, making it more intuitive and user-friendly.
|
||||
- **🌐 External STT Support**: Now support for external Speech-To-Text services, providing more flexibility in choosing your STT provider.
|
||||
- **⚙️ Unified Settings**: Consolidated settings including document settings under a new admin settings section for easier management.
|
||||
- **🌑 Dark Mode Splash Screen**: A new splash screen for dark mode, ensuring a consistent and visually appealing experience for dark mode users.
|
||||
- **📥 Upload Pipeline**: Directly upload pipelines from the admin settings > pipelines section, streamlining the pipeline management process.
|
||||
- **🌍 Improved Language Support**: Enhanced support for Chinese and Ukrainian languages, better catering to a global user base.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🛠️ Playground Issue**: Fixed the playground not functioning properly, ensuring a smoother user experience.
|
||||
- **🔥 Temperature Parameter Issue**: Corrected the issue where the temperature value '0' was not being passed correctly.
|
||||
- **📝 Prompt Input Clearing**: Resolved prompt input textarea not being cleared right away, ensuring a clean slate for new inputs.
|
||||
- **✨ Various UI Styling Issues**: Fixed numerous user interface styling problems for a more cohesive look.
|
||||
- **👥 Active Users Display**: Fixed active users showing active sessions instead of actual users, now reflecting accurate user activity.
|
||||
- **🌐 Community Platform Compatibility**: The Community Platform is back online and fully compatible with Open WebUI.
|
||||
|
||||
### Changed
|
||||
|
||||
- **📝 RAG Implementation**: Updated the RAG (Retrieval-Augmented Generation) implementation to use a system prompt for context, instead of overriding the user's prompt.
|
||||
- **🔄 Settings Relocation**: Moved Models, Connections, Audio, and Images settings to the admin settings for better organization.
|
||||
- **✍️ Improved Title Generation**: Enhanced the default prompt for title generation, yielding better results.
|
||||
- **🔧 Backend Task Management**: Tasks like title generation and search query generation are now managed on the backend side and controlled only by the admin.
|
||||
- **🔍 Editable Search Query Prompt**: You can now edit the search query generation prompt, offering more control over how queries are generated.
|
||||
- **📏 Prompt Length Threshold**: Set the prompt length threshold for search query generation from the admin settings, giving more customization options.
|
||||
- **📣 Settings Consolidation**: Merged the Banners admin setting with the Interface admin setting for a more streamlined settings area.
|
||||
|
||||
## [0.2.5] - 2024-06-05
|
||||
|
||||
### Added
|
||||
|
||||
- **👥 Active Users Indicator**: Now you can see how many people are currently active and what they are running. This helps you gauge when performance might slow down due to a high number of users.
|
||||
- **🗂️ Create Ollama Modelfile**: The option to create a modelfile for Ollama has been reintroduced in the Settings > Models section, making it easier to manage your models.
|
||||
- **⚙️ Default Model Setting**: Added an option to set the default model from Settings > Interface. This feature is now easily accessible, especially convenient for mobile users as it was previously hidden.
|
||||
- **🌐 Enhanced Translations**: We've improved the Chinese translations and added support for Turkmen and Norwegian languages to make the interface more accessible globally.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **📱 Mobile View Improvements**: The UI now uses dvh (dynamic viewport height) instead of vh (viewport height), providing a better and more responsive experience for mobile users.
|
||||
|
||||
## [0.2.4] - 2024-06-03
|
||||
|
||||
### Added
|
||||
|
||||
- **👤 Improved Account Pending Page**: The account pending page now displays admin details by default to avoid confusion. You can disable this feature in the admin settings if needed.
|
||||
- **🌐 HTTP Proxy Support**: We have enabled the use of the 'http_proxy' environment variable in OpenAI and Ollama API calls, making it easier to configure network settings.
|
||||
- **❓ Quick Access to Documentation**: You can now easily access Open WebUI documents via a question mark button located at the bottom right corner of the screen (available on larger screens like PCs).
|
||||
- **🌍 Enhanced Translation**: Improvements have been made to translations.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔍 SearxNG Web Search**: Fixed the issue where the SearxNG web search functionality was not working properly.
|
||||
|
||||
## [0.2.3] - 2024-06-03
|
||||
|
||||
### Added
|
||||
|
||||
- **📁 Export Chat as JSON**: You can now export individual chats as JSON files from the navbar menu by navigating to 'Download > Export Chat'. This makes sharing specific conversations easier.
|
||||
- **✏️ Edit Titles with Double Click**: Double-click on titles to rename them quickly and efficiently.
|
||||
- **🧩 Batch Multiple Embeddings**: Introduced 'RAG_EMBEDDING_OPENAI_BATCH_SIZE' to process multiple embeddings in a batch, enhancing performance for large datasets.
|
||||
- **🌍 Improved Translations**: Enhanced the translation quality across various languages for a better user experience.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🛠️ Modelfile Migration Script**: Fixed an issue where the modelfile migration script would fail if an invalid modelfile was encountered.
|
||||
- **💬 Zhuyin Input Method on Mac**: Resolved an issue where using the Zhuyin input method in the Web UI on a Mac caused text to send immediately upon pressing the enter key, leading to incorrect input.
|
||||
- **🔊 Local TTS Voice Selection**: Fixed the issue where the selected local Text-to-Speech (TTS) voice was not being displayed in settings.
|
||||
|
||||
## [0.2.2] - 2024-06-02
|
||||
|
||||
### Added
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
[](https://discord.gg/5rJgQTnV4s)
|
||||
[](https://github.com/sponsors/tjbck)
|
||||
|
||||
Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
|
||||
Open WebUI is an [extensible](https://github.com/open-webui/pipelines), feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
|
||||
|
||||

|
||||
|
||||
@@ -21,7 +21,7 @@ Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI d
|
||||
|
||||
- 🤝 **Ollama/OpenAI API Integration**: Effortlessly integrate OpenAI-compatible APIs for versatile conversations alongside Ollama models. Customize the OpenAI API URL to link with **LMStudio, GroqCloud, Mistral, OpenRouter, and more**.
|
||||
|
||||
- 🧩 **Pipelines, Open WebUI Plugin Support**: Seamlessly integrate custom logic and Python libraries into Open WebUI using [Pipelines Plugin Framework](https://github.com/open-webui/pipelines). Launch your Pipelines instance, set the OpenAI URL to the Pipelines URL, and explore endless possibilities. [Examples](https://github.com/open-webui/pipelines/examples) include **Function Calling**, User **Rate Limiting** to control access, **Usage Monitoring** with tools like Langfuse, **Live Translation with LibreTranslate** for multilingual support, **Toxic Message Filtering** and much more.
|
||||
- 🧩 **Pipelines, Open WebUI Plugin Support**: Seamlessly integrate custom logic and Python libraries into Open WebUI using [Pipelines Plugin Framework](https://github.com/open-webui/pipelines). Launch your Pipelines instance, set the OpenAI URL to the Pipelines URL, and explore endless possibilities. [Examples](https://github.com/open-webui/pipelines/tree/main/examples) include **Function Calling**, User **Rate Limiting** to control access, **Usage Monitoring** with tools like Langfuse, **Live Translation with LibreTranslate** for multilingual support, **Toxic Message Filtering** and much more.
|
||||
|
||||
- 📱 **Responsive Design**: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices.
|
||||
|
||||
@@ -146,10 +146,19 @@ docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/wa
|
||||
|
||||
In the last part of the command, replace `open-webui` with your container name if it is different.
|
||||
|
||||
### Moving from Ollama WebUI to Open WebUI
|
||||
|
||||
Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/migration/).
|
||||
|
||||
### Using the Dev Branch 🌙
|
||||
|
||||
> [!WARNING]
|
||||
> The `:dev` branch contains the latest unstable features and changes. Use it at your own risk as it may have bugs or incomplete features.
|
||||
|
||||
If you want to try out the latest bleeding-edge features and are okay with occasional instability, you can use the `:dev` tag like this:
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:dev
|
||||
```
|
||||
|
||||
## What's Next? 🌟
|
||||
|
||||
Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/).
|
||||
|
||||
+207
-59
@@ -17,13 +17,12 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from faster_whisper import WhisperModel
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
import uuid
|
||||
import requests
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
|
||||
from constants import ERROR_MESSAGES
|
||||
from utils.utils import (
|
||||
decode_token,
|
||||
@@ -41,10 +40,15 @@ from config import (
|
||||
WHISPER_MODEL_DIR,
|
||||
WHISPER_MODEL_AUTO_UPDATE,
|
||||
DEVICE_TYPE,
|
||||
AUDIO_OPENAI_API_BASE_URL,
|
||||
AUDIO_OPENAI_API_KEY,
|
||||
AUDIO_OPENAI_API_MODEL,
|
||||
AUDIO_OPENAI_API_VOICE,
|
||||
AUDIO_STT_OPENAI_API_BASE_URL,
|
||||
AUDIO_STT_OPENAI_API_KEY,
|
||||
AUDIO_TTS_OPENAI_API_BASE_URL,
|
||||
AUDIO_TTS_OPENAI_API_KEY,
|
||||
AUDIO_STT_ENGINE,
|
||||
AUDIO_STT_MODEL,
|
||||
AUDIO_TTS_ENGINE,
|
||||
AUDIO_TTS_MODEL,
|
||||
AUDIO_TTS_VOICE,
|
||||
AppConfig,
|
||||
)
|
||||
|
||||
@@ -61,10 +65,17 @@ app.add_middleware(
|
||||
)
|
||||
|
||||
app.state.config = AppConfig()
|
||||
app.state.config.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
|
||||
app.state.config.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
|
||||
app.state.config.OPENAI_API_MODEL = AUDIO_OPENAI_API_MODEL
|
||||
app.state.config.OPENAI_API_VOICE = AUDIO_OPENAI_API_VOICE
|
||||
|
||||
app.state.config.STT_OPENAI_API_BASE_URL = AUDIO_STT_OPENAI_API_BASE_URL
|
||||
app.state.config.STT_OPENAI_API_KEY = AUDIO_STT_OPENAI_API_KEY
|
||||
app.state.config.STT_ENGINE = AUDIO_STT_ENGINE
|
||||
app.state.config.STT_MODEL = AUDIO_STT_MODEL
|
||||
|
||||
app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
|
||||
app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
|
||||
app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
|
||||
app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
|
||||
app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
|
||||
|
||||
# setting device type for whisper model
|
||||
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
|
||||
@@ -74,41 +85,101 @@ SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
|
||||
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
class OpenAIConfigUpdateForm(BaseModel):
|
||||
url: str
|
||||
key: str
|
||||
model: str
|
||||
speaker: str
|
||||
class TTSConfigForm(BaseModel):
|
||||
OPENAI_API_BASE_URL: str
|
||||
OPENAI_API_KEY: str
|
||||
ENGINE: str
|
||||
MODEL: str
|
||||
VOICE: str
|
||||
|
||||
|
||||
class STTConfigForm(BaseModel):
|
||||
OPENAI_API_BASE_URL: str
|
||||
OPENAI_API_KEY: str
|
||||
ENGINE: str
|
||||
MODEL: str
|
||||
|
||||
|
||||
class AudioConfigUpdateForm(BaseModel):
|
||||
tts: TTSConfigForm
|
||||
stt: STTConfigForm
|
||||
|
||||
|
||||
from pydub import AudioSegment
|
||||
from pydub.utils import mediainfo
|
||||
|
||||
|
||||
def is_mp4_audio(file_path):
|
||||
"""Check if the given file is an MP4 audio file."""
|
||||
if not os.path.isfile(file_path):
|
||||
print(f"File not found: {file_path}")
|
||||
return False
|
||||
|
||||
info = mediainfo(file_path)
|
||||
if (
|
||||
info.get("codec_name") == "aac"
|
||||
and info.get("codec_type") == "audio"
|
||||
and info.get("codec_tag_string") == "mp4a"
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def convert_mp4_to_wav(file_path, output_path):
|
||||
"""Convert MP4 audio file to WAV format."""
|
||||
audio = AudioSegment.from_file(file_path, format="mp4")
|
||||
audio.export(output_path, format="wav")
|
||||
print(f"Converted {file_path} to {output_path}")
|
||||
|
||||
|
||||
@app.get("/config")
|
||||
async def get_openai_config(user=Depends(get_admin_user)):
|
||||
async def get_audio_config(user=Depends(get_admin_user)):
|
||||
return {
|
||||
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
||||
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
|
||||
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
|
||||
"tts": {
|
||||
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
|
||||
"ENGINE": app.state.config.TTS_ENGINE,
|
||||
"MODEL": app.state.config.TTS_MODEL,
|
||||
"VOICE": app.state.config.TTS_VOICE,
|
||||
},
|
||||
"stt": {
|
||||
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY,
|
||||
"ENGINE": app.state.config.STT_ENGINE,
|
||||
"MODEL": app.state.config.STT_MODEL,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@app.post("/config/update")
|
||||
async def update_openai_config(
|
||||
form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
|
||||
async def update_audio_config(
|
||||
form_data: AudioConfigUpdateForm, user=Depends(get_admin_user)
|
||||
):
|
||||
if form_data.key == "":
|
||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
||||
app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL
|
||||
app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY
|
||||
app.state.config.TTS_ENGINE = form_data.tts.ENGINE
|
||||
app.state.config.TTS_MODEL = form_data.tts.MODEL
|
||||
app.state.config.TTS_VOICE = form_data.tts.VOICE
|
||||
|
||||
app.state.config.OPENAI_API_BASE_URL = form_data.url
|
||||
app.state.config.OPENAI_API_KEY = form_data.key
|
||||
app.state.config.OPENAI_API_MODEL = form_data.model
|
||||
app.state.config.OPENAI_API_VOICE = form_data.speaker
|
||||
app.state.config.STT_OPENAI_API_BASE_URL = form_data.stt.OPENAI_API_BASE_URL
|
||||
app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY
|
||||
app.state.config.STT_ENGINE = form_data.stt.ENGINE
|
||||
app.state.config.STT_MODEL = form_data.stt.MODEL
|
||||
|
||||
return {
|
||||
"status": True,
|
||||
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
||||
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
|
||||
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
|
||||
"tts": {
|
||||
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
|
||||
"ENGINE": app.state.config.TTS_ENGINE,
|
||||
"MODEL": app.state.config.TTS_MODEL,
|
||||
"VOICE": app.state.config.TTS_VOICE,
|
||||
},
|
||||
"stt": {
|
||||
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY,
|
||||
"ENGINE": app.state.config.STT_ENGINE,
|
||||
"MODEL": app.state.config.STT_MODEL,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -125,13 +196,21 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
return FileResponse(file_path)
|
||||
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
|
||||
headers["Authorization"] = f"Bearer {app.state.config.TTS_OPENAI_API_KEY}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
try:
|
||||
body = body.decode("utf-8")
|
||||
body = json.loads(body)
|
||||
body["model"] = app.state.config.TTS_MODEL
|
||||
body = json.dumps(body).encode("utf-8")
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
r = None
|
||||
try:
|
||||
r = requests.post(
|
||||
url=f"{app.state.config.OPENAI_API_BASE_URL}/audio/speech",
|
||||
url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
|
||||
data=body,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
@@ -181,41 +260,110 @@ def transcribe(
|
||||
)
|
||||
|
||||
try:
|
||||
filename = file.filename
|
||||
file_path = f"{UPLOAD_DIR}/{filename}"
|
||||
ext = file.filename.split(".")[-1]
|
||||
|
||||
id = uuid.uuid4()
|
||||
filename = f"{id}.{ext}"
|
||||
|
||||
file_dir = f"{CACHE_DIR}/audio/transcriptions"
|
||||
os.makedirs(file_dir, exist_ok=True)
|
||||
file_path = f"{file_dir}/{filename}"
|
||||
|
||||
print(filename)
|
||||
|
||||
contents = file.file.read()
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(contents)
|
||||
f.close()
|
||||
|
||||
whisper_kwargs = {
|
||||
"model_size_or_path": WHISPER_MODEL,
|
||||
"device": whisper_device_type,
|
||||
"compute_type": "int8",
|
||||
"download_root": WHISPER_MODEL_DIR,
|
||||
"local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
|
||||
}
|
||||
if app.state.config.STT_ENGINE == "":
|
||||
whisper_kwargs = {
|
||||
"model_size_or_path": WHISPER_MODEL,
|
||||
"device": whisper_device_type,
|
||||
"compute_type": "int8",
|
||||
"download_root": WHISPER_MODEL_DIR,
|
||||
"local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
|
||||
}
|
||||
|
||||
log.debug(f"whisper_kwargs: {whisper_kwargs}")
|
||||
log.debug(f"whisper_kwargs: {whisper_kwargs}")
|
||||
|
||||
try:
|
||||
model = WhisperModel(**whisper_kwargs)
|
||||
except:
|
||||
log.warning(
|
||||
"WhisperModel initialization failed, attempting download with local_files_only=False"
|
||||
try:
|
||||
model = WhisperModel(**whisper_kwargs)
|
||||
except:
|
||||
log.warning(
|
||||
"WhisperModel initialization failed, attempting download with local_files_only=False"
|
||||
)
|
||||
whisper_kwargs["local_files_only"] = False
|
||||
model = WhisperModel(**whisper_kwargs)
|
||||
|
||||
segments, info = model.transcribe(file_path, beam_size=5)
|
||||
log.info(
|
||||
"Detected language '%s' with probability %f"
|
||||
% (info.language, info.language_probability)
|
||||
)
|
||||
whisper_kwargs["local_files_only"] = False
|
||||
model = WhisperModel(**whisper_kwargs)
|
||||
|
||||
segments, info = model.transcribe(file_path, beam_size=5)
|
||||
log.info(
|
||||
"Detected language '%s' with probability %f"
|
||||
% (info.language, info.language_probability)
|
||||
)
|
||||
transcript = "".join([segment.text for segment in list(segments)])
|
||||
|
||||
transcript = "".join([segment.text for segment in list(segments)])
|
||||
data = {"text": transcript.strip()}
|
||||
|
||||
return {"text": transcript.strip()}
|
||||
# save the transcript to a json file
|
||||
transcript_file = f"{file_dir}/{id}.json"
|
||||
with open(transcript_file, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
print(data)
|
||||
|
||||
return data
|
||||
|
||||
elif app.state.config.STT_ENGINE == "openai":
|
||||
if is_mp4_audio(file_path):
|
||||
print("is_mp4_audio")
|
||||
os.rename(file_path, file_path.replace(".wav", ".mp4"))
|
||||
# Convert MP4 audio file to WAV format
|
||||
convert_mp4_to_wav(file_path.replace(".wav", ".mp4"), file_path)
|
||||
|
||||
headers = {"Authorization": f"Bearer {app.state.config.STT_OPENAI_API_KEY}"}
|
||||
|
||||
files = {"file": (filename, open(file_path, "rb"))}
|
||||
data = {"model": "whisper-1"}
|
||||
|
||||
print(files, data)
|
||||
|
||||
r = None
|
||||
try:
|
||||
r = requests.post(
|
||||
url=f"{app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions",
|
||||
headers=headers,
|
||||
files=files,
|
||||
data=data,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
data = r.json()
|
||||
|
||||
# save the transcript to a json file
|
||||
transcript_file = f"{file_dir}/{id}.json"
|
||||
with open(transcript_file, "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
print(data)
|
||||
return data
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message']}"
|
||||
except:
|
||||
error_detail = f"External: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r != None else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
+56
-52
@@ -41,8 +41,6 @@ from utils.utils import (
|
||||
get_admin_user,
|
||||
)
|
||||
|
||||
from utils.models import get_model_id_from_custom_model_id
|
||||
|
||||
|
||||
from config import (
|
||||
SRC_LOG_LEVELS,
|
||||
@@ -134,7 +132,7 @@ async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin
|
||||
async def fetch_url(url):
|
||||
timeout = aiohttp.ClientTimeout(total=5)
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||
async with session.get(url) as response:
|
||||
return await response.json()
|
||||
except Exception as e:
|
||||
@@ -156,7 +154,7 @@ async def cleanup_response(
|
||||
async def post_streaming_url(url: str, payload: str):
|
||||
r = None
|
||||
try:
|
||||
session = aiohttp.ClientSession()
|
||||
session = aiohttp.ClientSession(trust_env=True)
|
||||
r = await session.post(url, data=payload)
|
||||
r.raise_for_status()
|
||||
|
||||
@@ -274,54 +272,57 @@ async def get_ollama_tags(
|
||||
@app.get("/api/version")
|
||||
@app.get("/api/version/{url_idx}")
|
||||
async def get_ollama_versions(url_idx: Optional[int] = None):
|
||||
if app.state.config.ENABLE_OLLAMA_API:
|
||||
if url_idx == None:
|
||||
|
||||
if url_idx == None:
|
||||
# returns lowest version
|
||||
tasks = [
|
||||
fetch_url(f"{url}/api/version")
|
||||
for url in app.state.config.OLLAMA_BASE_URLS
|
||||
]
|
||||
responses = await asyncio.gather(*tasks)
|
||||
responses = list(filter(lambda x: x is not None, responses))
|
||||
|
||||
# returns lowest version
|
||||
tasks = [
|
||||
fetch_url(f"{url}/api/version") for url in app.state.config.OLLAMA_BASE_URLS
|
||||
]
|
||||
responses = await asyncio.gather(*tasks)
|
||||
responses = list(filter(lambda x: x is not None, responses))
|
||||
if len(responses) > 0:
|
||||
lowest_version = min(
|
||||
responses,
|
||||
key=lambda x: tuple(
|
||||
map(int, re.sub(r"^v|-.*", "", x["version"]).split("."))
|
||||
),
|
||||
)
|
||||
|
||||
if len(responses) > 0:
|
||||
lowest_version = min(
|
||||
responses,
|
||||
key=lambda x: tuple(
|
||||
map(int, re.sub(r"^v|-.*", "", x["version"]).split("."))
|
||||
),
|
||||
)
|
||||
|
||||
return {"version": lowest_version["version"]}
|
||||
return {"version": lowest_version["version"]}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND,
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND,
|
||||
)
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
|
||||
r = None
|
||||
try:
|
||||
r = requests.request(method="GET", url=f"{url}/api/version")
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
else:
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
|
||||
r = None
|
||||
try:
|
||||
r = requests.request(method="GET", url=f"{url}/api/version")
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
return {"version": False}
|
||||
|
||||
|
||||
class ModelNameForm(BaseModel):
|
||||
@@ -725,7 +726,6 @@ async def generate_chat_completion(
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
|
||||
if model_info:
|
||||
print(model_info)
|
||||
if model_info.base_model_id:
|
||||
payload["model"] = model_info.base_model_id
|
||||
|
||||
@@ -761,7 +761,7 @@ async def generate_chat_completion(
|
||||
"frequency_penalty", None
|
||||
)
|
||||
|
||||
if model_info.params.get("temperature", None):
|
||||
if model_info.params.get("temperature", None) is not None:
|
||||
payload["options"]["temperature"] = model_info.params.get(
|
||||
"temperature", None
|
||||
)
|
||||
@@ -846,9 +846,14 @@ async def generate_chat_completion(
|
||||
|
||||
|
||||
# TODO: we should update this part once Ollama supports other types
|
||||
class OpenAIChatMessageContent(BaseModel):
|
||||
type: str
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class OpenAIChatMessage(BaseModel):
|
||||
role: str
|
||||
content: str
|
||||
content: Union[str, OpenAIChatMessageContent]
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
@@ -876,7 +881,6 @@ async def generate_openai_chat_completion(
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
|
||||
if model_info:
|
||||
print(model_info)
|
||||
if model_info.base_model_id:
|
||||
payload["model"] = model_info.base_model_id
|
||||
|
||||
@@ -1045,7 +1049,7 @@ async def download_file_stream(
|
||||
|
||||
timeout = aiohttp.ClientTimeout(total=600) # Set the timeout
|
||||
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||
async with session.get(file_url, headers=headers) as response:
|
||||
total_size = int(response.headers.get("content-length", 0)) + current_size
|
||||
|
||||
|
||||
+138
-91
@@ -186,7 +186,7 @@ async def fetch_url(url, key):
|
||||
timeout = aiohttp.ClientTimeout(total=5)
|
||||
try:
|
||||
headers = {"Authorization": f"Bearer {key}"}
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||
async with session.get(url, headers=headers) as response:
|
||||
return await response.json()
|
||||
except Exception as e:
|
||||
@@ -345,113 +345,97 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_use
|
||||
)
|
||||
|
||||
|
||||
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||
@app.post("/chat/completions")
|
||||
@app.post("/chat/completions/{url_idx}")
|
||||
async def generate_chat_completion(
|
||||
form_data: dict,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
idx = 0
|
||||
payload = {**form_data}
|
||||
|
||||
body = await request.body()
|
||||
# TODO: Remove below after gpt-4-vision fix from Open AI
|
||||
# Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision)
|
||||
model_id = form_data.get("model")
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
|
||||
payload = None
|
||||
if model_info:
|
||||
if model_info.base_model_id:
|
||||
payload["model"] = model_info.base_model_id
|
||||
|
||||
try:
|
||||
if "chat/completions" in path:
|
||||
body = body.decode("utf-8")
|
||||
body = json.loads(body)
|
||||
model_info.params = model_info.params.model_dump()
|
||||
|
||||
payload = {**body}
|
||||
if model_info.params:
|
||||
if model_info.params.get("temperature", None) is not None:
|
||||
payload["temperature"] = float(model_info.params.get("temperature"))
|
||||
|
||||
model_id = body.get("model")
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
if model_info.params.get("top_p", None):
|
||||
payload["top_p"] = int(model_info.params.get("top_p", None))
|
||||
|
||||
if model_info:
|
||||
print(model_info)
|
||||
if model_info.base_model_id:
|
||||
payload["model"] = model_info.base_model_id
|
||||
if model_info.params.get("max_tokens", None):
|
||||
payload["max_tokens"] = int(model_info.params.get("max_tokens", None))
|
||||
|
||||
model_info.params = model_info.params.model_dump()
|
||||
if model_info.params.get("frequency_penalty", None):
|
||||
payload["frequency_penalty"] = int(
|
||||
model_info.params.get("frequency_penalty", None)
|
||||
)
|
||||
|
||||
if model_info.params:
|
||||
if model_info.params.get("temperature", None):
|
||||
payload["temperature"] = int(
|
||||
model_info.params.get("temperature")
|
||||
if model_info.params.get("seed", None):
|
||||
payload["seed"] = model_info.params.get("seed", None)
|
||||
|
||||
if model_info.params.get("stop", None):
|
||||
payload["stop"] = (
|
||||
[
|
||||
bytes(stop, "utf-8").decode("unicode_escape")
|
||||
for stop in model_info.params["stop"]
|
||||
]
|
||||
if model_info.params.get("stop", None)
|
||||
else None
|
||||
)
|
||||
|
||||
if model_info.params.get("system", None):
|
||||
# Check if the payload already has a system message
|
||||
# If not, add a system message to the payload
|
||||
if payload.get("messages"):
|
||||
for message in payload["messages"]:
|
||||
if message.get("role") == "system":
|
||||
message["content"] = (
|
||||
model_info.params.get("system", None) + message["content"]
|
||||
)
|
||||
break
|
||||
else:
|
||||
payload["messages"].insert(
|
||||
0,
|
||||
{
|
||||
"role": "system",
|
||||
"content": model_info.params.get("system", None),
|
||||
},
|
||||
)
|
||||
|
||||
if model_info.params.get("top_p", None):
|
||||
payload["top_p"] = int(model_info.params.get("top_p", None))
|
||||
else:
|
||||
pass
|
||||
|
||||
if model_info.params.get("max_tokens", None):
|
||||
payload["max_tokens"] = int(
|
||||
model_info.params.get("max_tokens", None)
|
||||
)
|
||||
model = app.state.MODELS[payload.get("model")]
|
||||
idx = model["urlIdx"]
|
||||
|
||||
if model_info.params.get("frequency_penalty", None):
|
||||
payload["frequency_penalty"] = int(
|
||||
model_info.params.get("frequency_penalty", None)
|
||||
)
|
||||
if "pipeline" in model and model.get("pipeline"):
|
||||
payload["user"] = {"name": user.name, "id": user.id}
|
||||
|
||||
if model_info.params.get("seed", None):
|
||||
payload["seed"] = model_info.params.get("seed", None)
|
||||
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
|
||||
# This is a workaround until OpenAI fixes the issue with this model
|
||||
if payload.get("model") == "gpt-4-vision-preview":
|
||||
if "max_tokens" not in payload:
|
||||
payload["max_tokens"] = 4000
|
||||
log.debug("Modified payload:", payload)
|
||||
|
||||
if model_info.params.get("stop", None):
|
||||
payload["stop"] = (
|
||||
[
|
||||
bytes(stop, "utf-8").decode("unicode_escape")
|
||||
for stop in model_info.params["stop"]
|
||||
]
|
||||
if model_info.params.get("stop", None)
|
||||
else None
|
||||
)
|
||||
|
||||
if model_info.params.get("system", None):
|
||||
# Check if the payload already has a system message
|
||||
# If not, add a system message to the payload
|
||||
if payload.get("messages"):
|
||||
for message in payload["messages"]:
|
||||
if message.get("role") == "system":
|
||||
message["content"] = (
|
||||
model_info.params.get("system", None)
|
||||
+ message["content"]
|
||||
)
|
||||
break
|
||||
else:
|
||||
payload["messages"].insert(
|
||||
0,
|
||||
{
|
||||
"role": "system",
|
||||
"content": model_info.params.get("system", None),
|
||||
},
|
||||
)
|
||||
else:
|
||||
pass
|
||||
|
||||
model = app.state.MODELS[payload.get("model")]
|
||||
|
||||
idx = model["urlIdx"]
|
||||
|
||||
if "pipeline" in model and model.get("pipeline"):
|
||||
payload["user"] = {"name": user.name, "id": user.id}
|
||||
|
||||
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
|
||||
# This is a workaround until OpenAI fixes the issue with this model
|
||||
if payload.get("model") == "gpt-4-vision-preview":
|
||||
if "max_tokens" not in payload:
|
||||
payload["max_tokens"] = 4000
|
||||
log.debug("Modified payload:", payload)
|
||||
|
||||
# Convert the modified body back to JSON
|
||||
payload = json.dumps(payload)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
log.error("Error loading request body into a dictionary:", e)
|
||||
# Convert the modified body back to JSON
|
||||
payload = json.dumps(payload)
|
||||
|
||||
print(payload)
|
||||
|
||||
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||
key = app.state.config.OPENAI_API_KEYS[idx]
|
||||
|
||||
target_url = f"{url}/{path}"
|
||||
print(payload)
|
||||
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {key}"
|
||||
@@ -462,11 +446,74 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||
streaming = False
|
||||
|
||||
try:
|
||||
session = aiohttp.ClientSession()
|
||||
session = aiohttp.ClientSession(trust_env=True)
|
||||
r = await session.request(
|
||||
method=request.method,
|
||||
url=target_url,
|
||||
data=payload if payload else body,
|
||||
method="POST",
|
||||
url=f"{url}/chat/completions",
|
||||
data=payload,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
# Check if response is SSE
|
||||
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
||||
streaming = True
|
||||
return StreamingResponse(
|
||||
r.content,
|
||||
status_code=r.status,
|
||||
headers=dict(r.headers),
|
||||
background=BackgroundTask(
|
||||
cleanup_response, response=r, session=session
|
||||
),
|
||||
)
|
||||
else:
|
||||
response_data = await r.json()
|
||||
return response_data
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
if r is not None:
|
||||
try:
|
||||
res = await r.json()
|
||||
print(res)
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||
except:
|
||||
error_detail = f"External: {e}"
|
||||
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
|
||||
finally:
|
||||
if not streaming and session:
|
||||
if r:
|
||||
r.close()
|
||||
await session.close()
|
||||
|
||||
|
||||
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||
idx = 0
|
||||
|
||||
body = await request.body()
|
||||
|
||||
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||
key = app.state.config.OPENAI_API_KEYS[idx]
|
||||
|
||||
target_url = f"{url}/{path}"
|
||||
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {key}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
r = None
|
||||
session = None
|
||||
streaming = False
|
||||
|
||||
try:
|
||||
session = aiohttp.ClientSession(trust_env=True)
|
||||
r = await session.request(
|
||||
method=request.method,
|
||||
url=target_url,
|
||||
data=body,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from fastapi import (
|
||||
)
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import os, shutil, logging, re
|
||||
from datetime import datetime
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Union, Sequence
|
||||
@@ -30,6 +31,7 @@ from langchain_community.document_loaders import (
|
||||
UnstructuredExcelLoader,
|
||||
UnstructuredPowerPointLoader,
|
||||
YoutubeLoader,
|
||||
OutlookMessageLoader,
|
||||
)
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
|
||||
@@ -78,6 +80,7 @@ from utils.misc import (
|
||||
from utils.utils import get_current_user, get_admin_user
|
||||
|
||||
from config import (
|
||||
AppConfig,
|
||||
ENV,
|
||||
SRC_LOG_LEVELS,
|
||||
UPLOAD_DIR,
|
||||
@@ -114,7 +117,7 @@ from config import (
|
||||
SERPER_API_KEY,
|
||||
RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||
AppConfig,
|
||||
RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
)
|
||||
|
||||
from constants import ERROR_MESSAGES
|
||||
@@ -139,6 +142,7 @@ app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
|
||||
|
||||
app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
|
||||
app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
||||
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = RAG_EMBEDDING_OPENAI_BATCH_SIZE
|
||||
app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
|
||||
app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
|
||||
|
||||
@@ -212,6 +216,7 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
||||
app.state.sentence_transformer_ef,
|
||||
app.state.config.OPENAI_API_KEY,
|
||||
app.state.config.OPENAI_API_BASE_URL,
|
||||
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
)
|
||||
|
||||
origins = ["*"]
|
||||
@@ -248,6 +253,7 @@ async def get_status():
|
||||
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
|
||||
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
|
||||
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
|
||||
"openai_batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
}
|
||||
|
||||
|
||||
@@ -260,6 +266,7 @@ async def get_embedding_config(user=Depends(get_admin_user)):
|
||||
"openai_config": {
|
||||
"url": app.state.config.OPENAI_API_BASE_URL,
|
||||
"key": app.state.config.OPENAI_API_KEY,
|
||||
"batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -275,6 +282,7 @@ async def get_reraanking_config(user=Depends(get_admin_user)):
|
||||
class OpenAIConfigForm(BaseModel):
|
||||
url: str
|
||||
key: str
|
||||
batch_size: Optional[int] = None
|
||||
|
||||
|
||||
class EmbeddingModelUpdateForm(BaseModel):
|
||||
@@ -295,9 +303,14 @@ async def update_embedding_config(
|
||||
app.state.config.RAG_EMBEDDING_MODEL = form_data.embedding_model
|
||||
|
||||
if app.state.config.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
|
||||
if form_data.openai_config != None:
|
||||
if form_data.openai_config is not None:
|
||||
app.state.config.OPENAI_API_BASE_URL = form_data.openai_config.url
|
||||
app.state.config.OPENAI_API_KEY = form_data.openai_config.key
|
||||
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = (
|
||||
form_data.openai_config.batch_size
|
||||
if form_data.openai_config.batch_size
|
||||
else 1
|
||||
)
|
||||
|
||||
update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL)
|
||||
|
||||
@@ -307,6 +320,7 @@ async def update_embedding_config(
|
||||
app.state.sentence_transformer_ef,
|
||||
app.state.config.OPENAI_API_KEY,
|
||||
app.state.config.OPENAI_API_BASE_URL,
|
||||
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -316,6 +330,7 @@ async def update_embedding_config(
|
||||
"openai_config": {
|
||||
"url": app.state.config.OPENAI_API_BASE_URL,
|
||||
"key": app.state.config.OPENAI_API_KEY,
|
||||
"batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
@@ -866,6 +881,13 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
|
||||
texts = [doc.page_content for doc in docs]
|
||||
metadatas = [doc.metadata for doc in docs]
|
||||
|
||||
# ChromaDB does not like datetime formats
|
||||
# for meta-data so convert them to string.
|
||||
for metadata in metadatas:
|
||||
for key, value in metadata.items():
|
||||
if isinstance(value, datetime):
|
||||
metadata[key] = str(value)
|
||||
|
||||
try:
|
||||
if overwrite:
|
||||
for collection in CHROMA_CLIENT.list_collections():
|
||||
@@ -881,6 +903,7 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
|
||||
app.state.sentence_transformer_ef,
|
||||
app.state.config.OPENAI_API_KEY,
|
||||
app.state.config.OPENAI_API_BASE_URL,
|
||||
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
)
|
||||
|
||||
embedding_texts = list(map(lambda x: x.replace("\n", " "), texts))
|
||||
@@ -951,6 +974,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
||||
"swift",
|
||||
"vue",
|
||||
"svelte",
|
||||
"msg",
|
||||
]
|
||||
|
||||
if file_ext == "pdf":
|
||||
@@ -985,6 +1009,8 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
] or file_ext in ["ppt", "pptx"]:
|
||||
loader = UnstructuredPowerPointLoader(file_path)
|
||||
elif file_ext == "msg":
|
||||
loader = OutlookMessageLoader(file_path)
|
||||
elif file_ext in known_source_ext or (
|
||||
file_content_type and file_content_type.find("text/") >= 0
|
||||
):
|
||||
@@ -1150,6 +1176,30 @@ def reset_vector_db(user=Depends(get_admin_user)):
|
||||
CHROMA_CLIENT.reset()
|
||||
|
||||
|
||||
@app.get("/reset/uploads")
|
||||
def reset_upload_dir(user=Depends(get_admin_user)) -> bool:
|
||||
folder = f"{UPLOAD_DIR}"
|
||||
try:
|
||||
# Check if the directory exists
|
||||
if os.path.exists(folder):
|
||||
# Iterate over all the files and directories in the specified directory
|
||||
for filename in os.listdir(folder):
|
||||
file_path = os.path.join(folder, filename)
|
||||
try:
|
||||
if os.path.isfile(file_path) or os.path.islink(file_path):
|
||||
os.unlink(file_path) # Remove the file or link
|
||||
elif os.path.isdir(file_path):
|
||||
shutil.rmtree(file_path) # Remove the directory
|
||||
except Exception as e:
|
||||
print(f"Failed to delete {file_path}. Reason: {e}")
|
||||
else:
|
||||
print(f"The directory {folder} does not exist")
|
||||
except Exception as e:
|
||||
print(f"Failed to process the directory {folder}. Reason: {e}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@app.get("/reset")
|
||||
def reset(user=Depends(get_admin_user)) -> bool:
|
||||
folder = f"{UPLOAD_DIR}"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from typing import List
|
||||
|
||||
from apps.rag.search.main import SearchResult
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
@@ -9,20 +10,56 @@ log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
def search_searxng(query_url: str, query: str, count: int) -> list[SearchResult]:
|
||||
"""Search a SearXNG instance for a query and return the results as a list of SearchResult objects.
|
||||
def search_searxng(
|
||||
query_url: str, query: str, count: int, **kwargs
|
||||
) -> List[SearchResult]:
|
||||
"""
|
||||
Search a SearXNG instance for a given query and return the results as a list of SearchResult objects.
|
||||
|
||||
The function allows passing additional parameters such as language or time_range to tailor the search result.
|
||||
|
||||
Args:
|
||||
query_url (str): The URL of the SearXNG instance to search. Must contain "<query>" as a placeholder
|
||||
query (str): The query to search for
|
||||
"""
|
||||
url = query_url.replace("<query>", query)
|
||||
if "&format=json" not in url:
|
||||
url += "&format=json"
|
||||
log.debug(f"searching {url}")
|
||||
query_url (str): The base URL of the SearXNG server.
|
||||
query (str): The search term or question to find in the SearXNG database.
|
||||
count (int): The maximum number of results to retrieve from the search.
|
||||
|
||||
r = requests.get(
|
||||
url,
|
||||
Keyword Args:
|
||||
language (str): Language filter for the search results; e.g., "en-US". Defaults to an empty string.
|
||||
time_range (str): Time range for filtering results by date; e.g., "2023-04-05..today" or "all-time". Defaults to ''.
|
||||
categories: (Optional[List[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided.
|
||||
|
||||
Returns:
|
||||
List[SearchResult]: A list of SearchResults sorted by relevance score in descending order.
|
||||
|
||||
Raise:
|
||||
requests.exceptions.RequestException: If a request error occurs during the search process.
|
||||
"""
|
||||
|
||||
# Default values for optional parameters are provided as empty strings or None when not specified.
|
||||
language = kwargs.get("language", "en-US")
|
||||
time_range = kwargs.get("time_range", "")
|
||||
categories = "".join(kwargs.get("categories", []))
|
||||
|
||||
params = {
|
||||
"q": query,
|
||||
"format": "json",
|
||||
"pageno": 1,
|
||||
"language": language,
|
||||
"time_range": time_range,
|
||||
"categories": categories,
|
||||
"theme": "simple",
|
||||
"image_proxy": 0,
|
||||
}
|
||||
|
||||
# Legacy query format
|
||||
if "<query>" in query_url:
|
||||
# Strip all query parameters from the URL
|
||||
query_url = query_url.split("?")[0]
|
||||
|
||||
log.debug(f"searching {query_url}")
|
||||
|
||||
response = requests.get(
|
||||
query_url,
|
||||
headers={
|
||||
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
|
||||
"Accept": "text/html",
|
||||
@@ -30,10 +67,12 @@ def search_searxng(query_url: str, query: str, count: int) -> list[SearchResult]
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
"Connection": "keep-alive",
|
||||
},
|
||||
params=params,
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
json_response = r.json()
|
||||
response.raise_for_status() # Raise an exception for HTTP errors.
|
||||
|
||||
json_response = response.json()
|
||||
results = json_response.get("results", [])
|
||||
sorted_results = sorted(results, key=lambda x: x.get("score", 0), reverse=True)
|
||||
return [
|
||||
|
||||
+30
-60
@@ -2,7 +2,7 @@ import os
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from typing import List
|
||||
from typing import List, Union
|
||||
|
||||
from apps.ollama.main import (
|
||||
generate_ollama_embeddings,
|
||||
@@ -20,18 +20,8 @@ from langchain.retrievers import (
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
from config import (
|
||||
SRC_LOG_LEVELS,
|
||||
CHROMA_CLIENT,
|
||||
SEARXNG_QUERY_URL,
|
||||
GOOGLE_PSE_API_KEY,
|
||||
GOOGLE_PSE_ENGINE_ID,
|
||||
BRAVE_SEARCH_API_KEY,
|
||||
SERPSTACK_API_KEY,
|
||||
SERPSTACK_HTTPS,
|
||||
SERPER_API_KEY,
|
||||
)
|
||||
from utils.misc import get_last_user_message, add_or_update_system_message
|
||||
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
@@ -209,6 +199,7 @@ def get_embedding_function(
|
||||
embedding_function,
|
||||
openai_key,
|
||||
openai_url,
|
||||
batch_size,
|
||||
):
|
||||
if embedding_engine == "":
|
||||
return lambda query: embedding_function.encode(query).tolist()
|
||||
@@ -232,7 +223,13 @@ def get_embedding_function(
|
||||
|
||||
def generate_multiple(query, f):
|
||||
if isinstance(query, list):
|
||||
return [f(q) for q in query]
|
||||
if embedding_engine == "openai":
|
||||
embeddings = []
|
||||
for i in range(0, len(query), batch_size):
|
||||
embeddings.extend(f(query[i : i + batch_size]))
|
||||
return embeddings
|
||||
else:
|
||||
return [f(q) for q in query]
|
||||
else:
|
||||
return f(query)
|
||||
|
||||
@@ -250,31 +247,7 @@ def rag_messages(
|
||||
hybrid_search,
|
||||
):
|
||||
log.debug(f"docs: {docs} {messages} {embedding_function} {reranking_function}")
|
||||
|
||||
last_user_message_idx = None
|
||||
for i in range(len(messages) - 1, -1, -1):
|
||||
if messages[i]["role"] == "user":
|
||||
last_user_message_idx = i
|
||||
break
|
||||
|
||||
user_message = messages[last_user_message_idx]
|
||||
|
||||
if isinstance(user_message["content"], list):
|
||||
# Handle list content input
|
||||
content_type = "list"
|
||||
query = ""
|
||||
for content_item in user_message["content"]:
|
||||
if content_item["type"] == "text":
|
||||
query = content_item["text"]
|
||||
break
|
||||
elif isinstance(user_message["content"], str):
|
||||
# Handle text content input
|
||||
content_type = "text"
|
||||
query = user_message["content"]
|
||||
else:
|
||||
# Fallback in case the input does not match expected types
|
||||
content_type = None
|
||||
query = ""
|
||||
query = get_last_user_message(messages)
|
||||
|
||||
extracted_collections = []
|
||||
relevant_contexts = []
|
||||
@@ -352,24 +325,7 @@ def rag_messages(
|
||||
)
|
||||
|
||||
log.debug(f"ra_content: {ra_content}")
|
||||
|
||||
if content_type == "list":
|
||||
new_content = []
|
||||
for content_item in user_message["content"]:
|
||||
if content_item["type"] == "text":
|
||||
# Update the text item's content with ra_content
|
||||
new_content.append({"type": "text", "text": ra_content})
|
||||
else:
|
||||
# Keep other types of content as they are
|
||||
new_content.append(content_item)
|
||||
new_user_message = {**user_message, "content": new_content}
|
||||
else:
|
||||
new_user_message = {
|
||||
**user_message,
|
||||
"content": ra_content,
|
||||
}
|
||||
|
||||
messages[last_user_message_idx] = new_user_message
|
||||
messages = add_or_update_system_message(ra_content, messages)
|
||||
|
||||
return messages, citations
|
||||
|
||||
@@ -413,8 +369,22 @@ def get_model_path(model: str, update_model: bool = False):
|
||||
|
||||
|
||||
def generate_openai_embeddings(
|
||||
model: str, text: str, key: str, url: str = "https://api.openai.com/v1"
|
||||
model: str,
|
||||
text: Union[str, list[str]],
|
||||
key: str,
|
||||
url: str = "https://api.openai.com/v1",
|
||||
):
|
||||
if isinstance(text, list):
|
||||
embeddings = generate_openai_batch_embeddings(model, text, key, url)
|
||||
else:
|
||||
embeddings = generate_openai_batch_embeddings(model, [text], key, url)
|
||||
|
||||
return embeddings[0] if isinstance(text, str) else embeddings
|
||||
|
||||
|
||||
def generate_openai_batch_embeddings(
|
||||
model: str, texts: list[str], key: str, url: str = "https://api.openai.com/v1"
|
||||
) -> Optional[list[list[float]]]:
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{url}/embeddings",
|
||||
@@ -422,12 +392,12 @@ def generate_openai_embeddings(
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {key}",
|
||||
},
|
||||
json={"input": text, "model": model},
|
||||
json={"input": texts, "model": model},
|
||||
)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
if "data" in data:
|
||||
return data["data"][0]["embedding"]
|
||||
return [elem["embedding"] for elem in data["data"]]
|
||||
else:
|
||||
raise "Something went wrong :/"
|
||||
except Exception as e:
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import socketio
|
||||
import asyncio
|
||||
|
||||
|
||||
from apps.webui.models.users import Users
|
||||
from utils.utils import decode_token
|
||||
|
||||
sio = socketio.AsyncServer(cors_allowed_origins=[], async_mode="asgi")
|
||||
app = socketio.ASGIApp(sio, socketio_path="/ws/socket.io")
|
||||
|
||||
# Dictionary to maintain the user pool
|
||||
|
||||
SESSION_POOL = {}
|
||||
USER_POOL = {}
|
||||
USAGE_POOL = {}
|
||||
# Timeout duration in seconds
|
||||
TIMEOUT_DURATION = 3
|
||||
|
||||
|
||||
@sio.event
|
||||
async def connect(sid, environ, auth):
|
||||
print("connect ", sid)
|
||||
|
||||
user = None
|
||||
if auth and "token" in auth:
|
||||
data = decode_token(auth["token"])
|
||||
|
||||
if data is not None and "id" in data:
|
||||
user = Users.get_user_by_id(data["id"])
|
||||
|
||||
if user:
|
||||
SESSION_POOL[sid] = user.id
|
||||
if user.id in USER_POOL:
|
||||
USER_POOL[user.id].append(sid)
|
||||
else:
|
||||
USER_POOL[user.id] = [sid]
|
||||
|
||||
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
|
||||
print(len(set(USER_POOL)))
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
await sio.emit("usage", {"models": get_models_in_use()})
|
||||
|
||||
|
||||
@sio.on("user-join")
|
||||
async def user_join(sid, data):
|
||||
print("user-join", sid, data)
|
||||
|
||||
auth = data["auth"] if "auth" in data else None
|
||||
|
||||
if auth and "token" in auth:
|
||||
data = decode_token(auth["token"])
|
||||
|
||||
if data is not None and "id" in data:
|
||||
user = Users.get_user_by_id(data["id"])
|
||||
|
||||
if user:
|
||||
|
||||
SESSION_POOL[sid] = user.id
|
||||
if user.id in USER_POOL:
|
||||
USER_POOL[user.id].append(sid)
|
||||
else:
|
||||
USER_POOL[user.id] = [sid]
|
||||
|
||||
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
|
||||
print(len(set(USER_POOL)))
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
|
||||
|
||||
@sio.on("user-count")
|
||||
async def user_count(sid):
|
||||
print("user-count", sid)
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
|
||||
|
||||
def get_models_in_use():
|
||||
# Aggregate all models in use
|
||||
models_in_use = []
|
||||
for model_id, data in USAGE_POOL.items():
|
||||
models_in_use.append(model_id)
|
||||
print(f"Models in use: {models_in_use}")
|
||||
|
||||
return models_in_use
|
||||
|
||||
|
||||
@sio.on("usage")
|
||||
async def usage(sid, data):
|
||||
print(f'Received "usage" event from {sid}: {data}')
|
||||
|
||||
model_id = data["model"]
|
||||
|
||||
# Cancel previous callback if there is one
|
||||
if model_id in USAGE_POOL:
|
||||
USAGE_POOL[model_id]["callback"].cancel()
|
||||
|
||||
# Store the new usage data and task
|
||||
|
||||
if model_id in USAGE_POOL:
|
||||
USAGE_POOL[model_id]["sids"].append(sid)
|
||||
USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"]))
|
||||
|
||||
else:
|
||||
USAGE_POOL[model_id] = {"sids": [sid]}
|
||||
|
||||
# Schedule a task to remove the usage data after TIMEOUT_DURATION
|
||||
USAGE_POOL[model_id]["callback"] = asyncio.create_task(
|
||||
remove_after_timeout(sid, model_id)
|
||||
)
|
||||
|
||||
# Broadcast the usage data to all clients
|
||||
await sio.emit("usage", {"models": get_models_in_use()})
|
||||
|
||||
|
||||
async def remove_after_timeout(sid, model_id):
|
||||
try:
|
||||
print("remove_after_timeout", sid, model_id)
|
||||
await asyncio.sleep(TIMEOUT_DURATION)
|
||||
if model_id in USAGE_POOL:
|
||||
print(USAGE_POOL[model_id]["sids"])
|
||||
USAGE_POOL[model_id]["sids"].remove(sid)
|
||||
USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"]))
|
||||
|
||||
if len(USAGE_POOL[model_id]["sids"]) == 0:
|
||||
del USAGE_POOL[model_id]
|
||||
|
||||
print(f"Removed usage data for {model_id} due to timeout")
|
||||
# Broadcast the usage data to all clients
|
||||
await sio.emit("usage", {"models": get_models_in_use()})
|
||||
except asyncio.CancelledError:
|
||||
# Task was cancelled due to new 'usage' event
|
||||
pass
|
||||
|
||||
|
||||
@sio.event
|
||||
async def disconnect(sid):
|
||||
if sid in SESSION_POOL:
|
||||
user_id = SESSION_POOL[sid]
|
||||
del SESSION_POOL[sid]
|
||||
|
||||
USER_POOL[user_id].remove(sid)
|
||||
|
||||
if len(USER_POOL[user_id]) == 0:
|
||||
del USER_POOL[user_id]
|
||||
|
||||
print(f"user {user_id} disconnected with session ID {sid}")
|
||||
print(USER_POOL)
|
||||
|
||||
await sio.emit("user-count", {"count": len(USER_POOL)})
|
||||
else:
|
||||
print(f"Unknown session ID {sid} disconnected")
|
||||
@@ -14,6 +14,8 @@ from apps.webui.routers import (
|
||||
)
|
||||
from config import (
|
||||
WEBUI_BUILD_HASH,
|
||||
SHOW_ADMIN_DETAILS,
|
||||
ADMIN_EMAIL,
|
||||
WEBUI_AUTH,
|
||||
DEFAULT_MODELS,
|
||||
DEFAULT_PROMPT_SUGGESTIONS,
|
||||
@@ -37,6 +39,11 @@ app.state.config = AppConfig()
|
||||
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
|
||||
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
||||
|
||||
|
||||
app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS
|
||||
app.state.config.ADMIN_EMAIL = ADMIN_EMAIL
|
||||
|
||||
|
||||
app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
|
||||
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
|
||||
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
|
||||
|
||||
@@ -298,6 +298,15 @@ class ChatTable:
|
||||
# .limit(limit).offset(skip)
|
||||
]
|
||||
|
||||
def get_archived_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
|
||||
return [
|
||||
ChatModel(**model_to_dict(chat))
|
||||
for chat in Chat.select()
|
||||
.where(Chat.archived == True)
|
||||
.where(Chat.user_id == user_id)
|
||||
.order_by(Chat.updated_at.desc())
|
||||
]
|
||||
|
||||
def delete_chat_by_id(self, id: str) -> bool:
|
||||
try:
|
||||
query = Chat.delete().where((Chat.id == id))
|
||||
|
||||
@@ -269,73 +269,88 @@ async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
|
||||
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
||||
|
||||
|
||||
############################
|
||||
# GetAdminDetails
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/admin/details")
|
||||
async def get_admin_details(request: Request, user=Depends(get_current_user)):
|
||||
if request.app.state.config.SHOW_ADMIN_DETAILS:
|
||||
admin_email = request.app.state.config.ADMIN_EMAIL
|
||||
admin_name = None
|
||||
|
||||
print(admin_email, admin_name)
|
||||
|
||||
if admin_email:
|
||||
admin = Users.get_user_by_email(admin_email)
|
||||
if admin:
|
||||
admin_name = admin.name
|
||||
else:
|
||||
admin = Users.get_first_user()
|
||||
if admin:
|
||||
admin_email = admin.email
|
||||
admin_name = admin.name
|
||||
|
||||
return {
|
||||
"name": admin_name,
|
||||
"email": admin_email,
|
||||
}
|
||||
else:
|
||||
raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED)
|
||||
|
||||
|
||||
############################
|
||||
# ToggleSignUp
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/signup/enabled", response_model=bool)
|
||||
async def get_sign_up_status(request: Request, user=Depends(get_admin_user)):
|
||||
return request.app.state.config.ENABLE_SIGNUP
|
||||
@router.get("/admin/config")
|
||||
async def get_admin_config(request: Request, user=Depends(get_admin_user)):
|
||||
return {
|
||||
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
|
||||
"ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
|
||||
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
|
||||
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
|
||||
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/signup/enabled/toggle", response_model=bool)
|
||||
async def toggle_sign_up(request: Request, user=Depends(get_admin_user)):
|
||||
request.app.state.config.ENABLE_SIGNUP = not request.app.state.config.ENABLE_SIGNUP
|
||||
return request.app.state.config.ENABLE_SIGNUP
|
||||
class AdminConfig(BaseModel):
|
||||
SHOW_ADMIN_DETAILS: bool
|
||||
ENABLE_SIGNUP: bool
|
||||
DEFAULT_USER_ROLE: str
|
||||
JWT_EXPIRES_IN: str
|
||||
ENABLE_COMMUNITY_SHARING: bool
|
||||
|
||||
|
||||
############################
|
||||
# Default User Role
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/signup/user/role")
|
||||
async def get_default_user_role(request: Request, user=Depends(get_admin_user)):
|
||||
return request.app.state.config.DEFAULT_USER_ROLE
|
||||
|
||||
|
||||
class UpdateRoleForm(BaseModel):
|
||||
role: str
|
||||
|
||||
|
||||
@router.post("/signup/user/role")
|
||||
async def update_default_user_role(
|
||||
request: Request, form_data: UpdateRoleForm, user=Depends(get_admin_user)
|
||||
@router.post("/admin/config")
|
||||
async def update_admin_config(
|
||||
request: Request, form_data: AdminConfig, user=Depends(get_admin_user)
|
||||
):
|
||||
if form_data.role in ["pending", "user", "admin"]:
|
||||
request.app.state.config.DEFAULT_USER_ROLE = form_data.role
|
||||
return request.app.state.config.DEFAULT_USER_ROLE
|
||||
request.app.state.config.SHOW_ADMIN_DETAILS = form_data.SHOW_ADMIN_DETAILS
|
||||
request.app.state.config.ENABLE_SIGNUP = form_data.ENABLE_SIGNUP
|
||||
|
||||
if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]:
|
||||
request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE
|
||||
|
||||
############################
|
||||
# JWT Expiration
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/token/expires")
|
||||
async def get_token_expires_duration(request: Request, user=Depends(get_admin_user)):
|
||||
return request.app.state.config.JWT_EXPIRES_IN
|
||||
|
||||
|
||||
class UpdateJWTExpiresDurationForm(BaseModel):
|
||||
duration: str
|
||||
|
||||
|
||||
@router.post("/token/expires/update")
|
||||
async def update_token_expires_duration(
|
||||
request: Request,
|
||||
form_data: UpdateJWTExpiresDurationForm,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
pattern = r"^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$"
|
||||
|
||||
# Check if the input string matches the pattern
|
||||
if re.match(pattern, form_data.duration):
|
||||
request.app.state.config.JWT_EXPIRES_IN = form_data.duration
|
||||
return request.app.state.config.JWT_EXPIRES_IN
|
||||
else:
|
||||
return request.app.state.config.JWT_EXPIRES_IN
|
||||
if re.match(pattern, form_data.JWT_EXPIRES_IN):
|
||||
request.app.state.config.JWT_EXPIRES_IN = form_data.JWT_EXPIRES_IN
|
||||
|
||||
request.app.state.config.ENABLE_COMMUNITY_SHARING = (
|
||||
form_data.ENABLE_COMMUNITY_SHARING
|
||||
)
|
||||
|
||||
return {
|
||||
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
|
||||
"ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP,
|
||||
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
|
||||
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
|
||||
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||
}
|
||||
|
||||
|
||||
############################
|
||||
|
||||
@@ -113,6 +113,19 @@ async def get_user_chats(user=Depends(get_current_user)):
|
||||
]
|
||||
|
||||
|
||||
############################
|
||||
# GetArchivedChats
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/all/archived", response_model=List[ChatResponse])
|
||||
async def get_user_chats(user=Depends(get_current_user)):
|
||||
return [
|
||||
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
for chat in Chats.get_archived_chats_by_user_id(user.id)
|
||||
]
|
||||
|
||||
|
||||
############################
|
||||
# GetAllChatsInDB
|
||||
############################
|
||||
|
||||
@@ -19,7 +19,12 @@ from apps.webui.models.users import (
|
||||
from apps.webui.models.auths import Auths
|
||||
from apps.webui.models.chats import Chats
|
||||
|
||||
from utils.utils import get_verified_user, get_password_hash, get_admin_user
|
||||
from utils.utils import (
|
||||
get_verified_user,
|
||||
get_password_hash,
|
||||
get_current_user,
|
||||
get_admin_user,
|
||||
)
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
from config import SRC_LOG_LEVELS
|
||||
|
||||
+134
-17
@@ -306,7 +306,10 @@ STATIC_DIR = Path(os.getenv("STATIC_DIR", BACKEND_DIR / "static")).resolve()
|
||||
|
||||
frontend_favicon = FRONTEND_BUILD_DIR / "favicon.png"
|
||||
if frontend_favicon.exists():
|
||||
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
|
||||
try:
|
||||
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
|
||||
except PermissionError:
|
||||
logging.error(f"No write permission to {STATIC_DIR / 'favicon.png'}")
|
||||
else:
|
||||
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
|
||||
|
||||
@@ -601,6 +604,80 @@ WEBUI_BANNERS = PersistentConfig(
|
||||
[BannerModel(**banner) for banner in json.loads("[]")],
|
||||
)
|
||||
|
||||
|
||||
SHOW_ADMIN_DETAILS = PersistentConfig(
|
||||
"SHOW_ADMIN_DETAILS",
|
||||
"auth.admin.show",
|
||||
os.environ.get("SHOW_ADMIN_DETAILS", "true").lower() == "true",
|
||||
)
|
||||
|
||||
ADMIN_EMAIL = PersistentConfig(
|
||||
"ADMIN_EMAIL",
|
||||
"auth.admin.email",
|
||||
os.environ.get("ADMIN_EMAIL", None),
|
||||
)
|
||||
|
||||
|
||||
####################################
|
||||
# TASKS
|
||||
####################################
|
||||
|
||||
|
||||
TASK_MODEL = PersistentConfig(
|
||||
"TASK_MODEL",
|
||||
"task.model.default",
|
||||
os.environ.get("TASK_MODEL", ""),
|
||||
)
|
||||
|
||||
TASK_MODEL_EXTERNAL = PersistentConfig(
|
||||
"TASK_MODEL_EXTERNAL",
|
||||
"task.model.external",
|
||||
os.environ.get("TASK_MODEL_EXTERNAL", ""),
|
||||
)
|
||||
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||
"TITLE_GENERATION_PROMPT_TEMPLATE",
|
||||
"task.title.prompt_template",
|
||||
os.environ.get(
|
||||
"TITLE_GENERATION_PROMPT_TEMPLATE",
|
||||
"""Here is the query:
|
||||
{{prompt:middletruncate:8000}}
|
||||
|
||||
Create a concise, 3-5 word phrase with an emoji as a title for the previous query. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
|
||||
|
||||
Examples of titles:
|
||||
📉 Stock Market Trends
|
||||
🍪 Perfect Chocolate Chip Recipe
|
||||
Evolution of Music Streaming
|
||||
Remote Work Productivity Tips
|
||||
Artificial Intelligence in Healthcare
|
||||
🎮 Video Game Development Insights""",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE",
|
||||
"task.search.prompt_template",
|
||||
os.environ.get(
|
||||
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE",
|
||||
"""You are tasked with generating web search queries. Give me an appropriate query to answer my question for google search. Answer with only the query. Today is {{CURRENT_DATE}}.
|
||||
|
||||
Question:
|
||||
{{prompt:end:4000}}""",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = PersistentConfig(
|
||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
|
||||
"task.search.prompt_length_threshold",
|
||||
os.environ.get(
|
||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
|
||||
100,
|
||||
),
|
||||
)
|
||||
|
||||
####################################
|
||||
# WEBUI_SECRET_KEY
|
||||
####################################
|
||||
@@ -683,6 +760,12 @@ RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
|
||||
os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
|
||||
)
|
||||
|
||||
RAG_EMBEDDING_OPENAI_BATCH_SIZE = PersistentConfig(
|
||||
"RAG_EMBEDDING_OPENAI_BATCH_SIZE",
|
||||
"rag.embedding_openai_batch_size",
|
||||
os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", 1),
|
||||
)
|
||||
|
||||
RAG_RERANKING_MODEL = PersistentConfig(
|
||||
"RAG_RERANKING_MODEL",
|
||||
"rag.reranking_model",
|
||||
@@ -913,25 +996,59 @@ IMAGE_GENERATION_MODEL = PersistentConfig(
|
||||
# Audio
|
||||
####################################
|
||||
|
||||
AUDIO_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
"AUDIO_OPENAI_API_BASE_URL",
|
||||
"audio.openai.api_base_url",
|
||||
os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
||||
AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
"AUDIO_STT_OPENAI_API_BASE_URL",
|
||||
"audio.stt.openai.api_base_url",
|
||||
os.getenv("AUDIO_STT_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
||||
)
|
||||
AUDIO_OPENAI_API_KEY = PersistentConfig(
|
||||
"AUDIO_OPENAI_API_KEY",
|
||||
"audio.openai.api_key",
|
||||
os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY),
|
||||
|
||||
AUDIO_STT_OPENAI_API_KEY = PersistentConfig(
|
||||
"AUDIO_STT_OPENAI_API_KEY",
|
||||
"audio.stt.openai.api_key",
|
||||
os.getenv("AUDIO_STT_OPENAI_API_KEY", OPENAI_API_KEY),
|
||||
)
|
||||
AUDIO_OPENAI_API_MODEL = PersistentConfig(
|
||||
"AUDIO_OPENAI_API_MODEL",
|
||||
"audio.openai.api_model",
|
||||
os.getenv("AUDIO_OPENAI_API_MODEL", "tts-1"),
|
||||
|
||||
AUDIO_STT_ENGINE = PersistentConfig(
|
||||
"AUDIO_STT_ENGINE",
|
||||
"audio.stt.engine",
|
||||
os.getenv("AUDIO_STT_ENGINE", ""),
|
||||
)
|
||||
AUDIO_OPENAI_API_VOICE = PersistentConfig(
|
||||
"AUDIO_OPENAI_API_VOICE",
|
||||
"audio.openai.api_voice",
|
||||
os.getenv("AUDIO_OPENAI_API_VOICE", "alloy"),
|
||||
|
||||
AUDIO_STT_MODEL = PersistentConfig(
|
||||
"AUDIO_STT_MODEL",
|
||||
"audio.stt.model",
|
||||
os.getenv("AUDIO_STT_MODEL", "whisper-1"),
|
||||
)
|
||||
|
||||
AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
"AUDIO_TTS_OPENAI_API_BASE_URL",
|
||||
"audio.tts.openai.api_base_url",
|
||||
os.getenv("AUDIO_TTS_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
||||
)
|
||||
AUDIO_TTS_OPENAI_API_KEY = PersistentConfig(
|
||||
"AUDIO_TTS_OPENAI_API_KEY",
|
||||
"audio.tts.openai.api_key",
|
||||
os.getenv("AUDIO_TTS_OPENAI_API_KEY", OPENAI_API_KEY),
|
||||
)
|
||||
|
||||
|
||||
AUDIO_TTS_ENGINE = PersistentConfig(
|
||||
"AUDIO_TTS_ENGINE",
|
||||
"audio.tts.engine",
|
||||
os.getenv("AUDIO_TTS_ENGINE", ""),
|
||||
)
|
||||
|
||||
|
||||
AUDIO_TTS_MODEL = PersistentConfig(
|
||||
"AUDIO_TTS_MODEL",
|
||||
"audio.tts.model",
|
||||
os.getenv("AUDIO_TTS_MODEL", "tts-1"),
|
||||
)
|
||||
|
||||
AUDIO_TTS_VOICE = PersistentConfig(
|
||||
"AUDIO_TTS_VOICE",
|
||||
"audio.tts.voice",
|
||||
os.getenv("AUDIO_TTS_VOICE", "alloy"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -84,3 +84,7 @@ class ERROR_MESSAGES(str, Enum):
|
||||
WEB_SEARCH_ERROR = (
|
||||
lambda err="": f"{err if err else 'Oops! Something went wrong while searching the web.'}"
|
||||
)
|
||||
|
||||
OLLAMA_API_DISABLED = (
|
||||
"The Ollama API is disabled. Please enable it to use this feature."
|
||||
)
|
||||
|
||||
+355
-103
@@ -9,8 +9,11 @@ import logging
|
||||
import aiohttp
|
||||
import requests
|
||||
import mimetypes
|
||||
import shutil
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
from fastapi import FastAPI, Request, Depends, status
|
||||
from fastapi import FastAPI, Request, Depends, status, UploadFile, File, Form
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import HTTPException
|
||||
@@ -20,15 +23,26 @@ from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.responses import StreamingResponse, Response
|
||||
|
||||
from apps.ollama.main import app as ollama_app, get_all_models as get_ollama_models
|
||||
from apps.openai.main import app as openai_app, get_all_models as get_openai_models
|
||||
|
||||
from apps.socket.main import app as socket_app
|
||||
from apps.ollama.main import (
|
||||
app as ollama_app,
|
||||
OpenAIChatCompletionForm,
|
||||
get_all_models as get_ollama_models,
|
||||
generate_openai_chat_completion as generate_ollama_chat_completion,
|
||||
)
|
||||
from apps.openai.main import (
|
||||
app as openai_app,
|
||||
get_all_models as get_openai_models,
|
||||
generate_chat_completion as generate_openai_chat_completion,
|
||||
)
|
||||
|
||||
from apps.audio.main import app as audio_app
|
||||
from apps.images.main import app as images_app
|
||||
from apps.rag.main import app as rag_app
|
||||
from apps.webui.main import app as webui_app
|
||||
|
||||
import asyncio
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
|
||||
@@ -39,6 +53,8 @@ from utils.utils import (
|
||||
get_current_user,
|
||||
get_http_authorization_cred,
|
||||
)
|
||||
from utils.task import title_generation_template, search_query_generation_template
|
||||
|
||||
from apps.rag.utils import rag_messages
|
||||
|
||||
from config import (
|
||||
@@ -60,8 +76,13 @@ from config import (
|
||||
SRC_LOG_LEVELS,
|
||||
WEBHOOK_URL,
|
||||
ENABLE_ADMIN_EXPORT,
|
||||
AppConfig,
|
||||
WEBUI_BUILD_HASH,
|
||||
TASK_MODEL,
|
||||
TASK_MODEL_EXTERNAL,
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
||||
AppConfig,
|
||||
)
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
@@ -115,10 +136,19 @@ app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
|
||||
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
||||
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
||||
|
||||
|
||||
app.state.config.WEBHOOK_URL = WEBHOOK_URL
|
||||
|
||||
|
||||
app.state.config.TASK_MODEL = TASK_MODEL
|
||||
app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL
|
||||
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
|
||||
app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||
)
|
||||
app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = (
|
||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD
|
||||
)
|
||||
|
||||
app.state.MODELS = {}
|
||||
|
||||
origins = ["*"]
|
||||
@@ -226,6 +256,78 @@ class RAGMiddleware(BaseHTTPMiddleware):
|
||||
app.add_middleware(RAGMiddleware)
|
||||
|
||||
|
||||
def filter_pipeline(payload, user):
|
||||
user = {"id": user.id, "name": user.name, "role": user.role}
|
||||
model_id = payload["model"]
|
||||
filters = [
|
||||
model
|
||||
for model in app.state.MODELS.values()
|
||||
if "pipeline" in model
|
||||
and "type" in model["pipeline"]
|
||||
and model["pipeline"]["type"] == "filter"
|
||||
and (
|
||||
model["pipeline"]["pipelines"] == ["*"]
|
||||
or any(
|
||||
model_id == target_model_id
|
||||
for target_model_id in model["pipeline"]["pipelines"]
|
||||
)
|
||||
)
|
||||
]
|
||||
sorted_filters = sorted(filters, key=lambda x: x["pipeline"]["priority"])
|
||||
|
||||
model = app.state.MODELS[model_id]
|
||||
|
||||
if "pipeline" in model:
|
||||
sorted_filters.append(model)
|
||||
|
||||
for filter in sorted_filters:
|
||||
r = None
|
||||
try:
|
||||
urlIdx = filter["urlIdx"]
|
||||
|
||||
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
|
||||
key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
|
||||
|
||||
if key != "":
|
||||
headers = {"Authorization": f"Bearer {key}"}
|
||||
r = requests.post(
|
||||
f"{url}/{filter['id']}/filter/inlet",
|
||||
headers=headers,
|
||||
json={
|
||||
"user": user,
|
||||
"body": payload,
|
||||
},
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
payload = r.json()
|
||||
except Exception as e:
|
||||
# Handle connection error here
|
||||
print(f"Connection error: {e}")
|
||||
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "detail" in res:
|
||||
return JSONResponse(
|
||||
status_code=r.status_code,
|
||||
content=res,
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
if "pipeline" not in app.state.MODELS[model_id]:
|
||||
if "chat_id" in payload:
|
||||
del payload["chat_id"]
|
||||
|
||||
if "title" in payload:
|
||||
del payload["title"]
|
||||
return payload
|
||||
|
||||
|
||||
class PipelineMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
if request.method == "POST" and (
|
||||
@@ -241,85 +343,10 @@ class PipelineMiddleware(BaseHTTPMiddleware):
|
||||
# Parse string to JSON
|
||||
data = json.loads(body_str) if body_str else {}
|
||||
|
||||
model_id = data["model"]
|
||||
filters = [
|
||||
model
|
||||
for model in app.state.MODELS.values()
|
||||
if "pipeline" in model
|
||||
and "type" in model["pipeline"]
|
||||
and model["pipeline"]["type"] == "filter"
|
||||
and (
|
||||
model["pipeline"]["pipelines"] == ["*"]
|
||||
or any(
|
||||
model_id == target_model_id
|
||||
for target_model_id in model["pipeline"]["pipelines"]
|
||||
)
|
||||
)
|
||||
]
|
||||
sorted_filters = sorted(filters, key=lambda x: x["pipeline"]["priority"])
|
||||
|
||||
user = None
|
||||
if len(sorted_filters) > 0:
|
||||
try:
|
||||
user = get_current_user(
|
||||
get_http_authorization_cred(
|
||||
request.headers.get("Authorization")
|
||||
)
|
||||
)
|
||||
user = {"id": user.id, "name": user.name, "role": user.role}
|
||||
except:
|
||||
pass
|
||||
|
||||
model = app.state.MODELS[model_id]
|
||||
|
||||
if "pipeline" in model:
|
||||
sorted_filters.append(model)
|
||||
|
||||
for filter in sorted_filters:
|
||||
r = None
|
||||
try:
|
||||
urlIdx = filter["urlIdx"]
|
||||
|
||||
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
|
||||
key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
|
||||
|
||||
if key != "":
|
||||
headers = {"Authorization": f"Bearer {key}"}
|
||||
r = requests.post(
|
||||
f"{url}/{filter['id']}/filter/inlet",
|
||||
headers=headers,
|
||||
json={
|
||||
"user": user,
|
||||
"body": data,
|
||||
},
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
except Exception as e:
|
||||
# Handle connection error here
|
||||
print(f"Connection error: {e}")
|
||||
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "detail" in res:
|
||||
return JSONResponse(
|
||||
status_code=r.status_code,
|
||||
content=res,
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
if "pipeline" not in app.state.MODELS[model_id]:
|
||||
if "chat_id" in data:
|
||||
del data["chat_id"]
|
||||
|
||||
if "title" in data:
|
||||
del data["title"]
|
||||
user = get_current_user(
|
||||
get_http_authorization_cred(request.headers.get("Authorization"))
|
||||
)
|
||||
data = filter_pipeline(data, user)
|
||||
|
||||
modified_body_bytes = json.dumps(data).encode("utf-8")
|
||||
# Replace the request body with the modified one
|
||||
@@ -376,6 +403,9 @@ async def update_embedding_function(request: Request, call_next):
|
||||
return response
|
||||
|
||||
|
||||
app.mount("/ws", socket_app)
|
||||
|
||||
|
||||
app.mount("/ollama", ollama_app)
|
||||
app.mount("/openai", openai_app)
|
||||
|
||||
@@ -477,6 +507,178 @@ async def get_models(user=Depends(get_verified_user)):
|
||||
return {"data": models}
|
||||
|
||||
|
||||
@app.get("/api/task/config")
|
||||
async def get_task_config(user=Depends(get_verified_user)):
|
||||
return {
|
||||
"TASK_MODEL": app.state.config.TASK_MODEL,
|
||||
"TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
|
||||
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD": app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
||||
}
|
||||
|
||||
|
||||
class TaskConfigForm(BaseModel):
|
||||
TASK_MODEL: Optional[str]
|
||||
TASK_MODEL_EXTERNAL: Optional[str]
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE: str
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: str
|
||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD: int
|
||||
|
||||
|
||||
@app.post("/api/task/config/update")
|
||||
async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_user)):
|
||||
app.state.config.TASK_MODEL = form_data.TASK_MODEL
|
||||
app.state.config.TASK_MODEL_EXTERNAL = form_data.TASK_MODEL_EXTERNAL
|
||||
app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = (
|
||||
form_data.TITLE_GENERATION_PROMPT_TEMPLATE
|
||||
)
|
||||
app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = (
|
||||
form_data.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||
)
|
||||
app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = (
|
||||
form_data.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD
|
||||
)
|
||||
|
||||
return {
|
||||
"TASK_MODEL": app.state.config.TASK_MODEL,
|
||||
"TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL,
|
||||
"TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE,
|
||||
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE,
|
||||
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD": app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/task/title/completions")
|
||||
async def generate_title(form_data: dict, user=Depends(get_verified_user)):
|
||||
print("generate_title")
|
||||
|
||||
model_id = form_data["model"]
|
||||
if model_id not in app.state.MODELS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Model not found",
|
||||
)
|
||||
|
||||
# Check if the user has a custom task model
|
||||
# If the user has a custom task model, use that model
|
||||
if app.state.MODELS[model_id]["owned_by"] == "ollama":
|
||||
if app.state.config.TASK_MODEL:
|
||||
task_model_id = app.state.config.TASK_MODEL
|
||||
if task_model_id in app.state.MODELS:
|
||||
model_id = task_model_id
|
||||
else:
|
||||
if app.state.config.TASK_MODEL_EXTERNAL:
|
||||
task_model_id = app.state.config.TASK_MODEL_EXTERNAL
|
||||
if task_model_id in app.state.MODELS:
|
||||
model_id = task_model_id
|
||||
|
||||
print(model_id)
|
||||
model = app.state.MODELS[model_id]
|
||||
|
||||
template = app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE
|
||||
|
||||
content = title_generation_template(
|
||||
template, form_data["prompt"], user.model_dump()
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": model_id,
|
||||
"messages": [{"role": "user", "content": content}],
|
||||
"stream": False,
|
||||
"max_tokens": 50,
|
||||
"chat_id": form_data.get("chat_id", None),
|
||||
"title": True,
|
||||
}
|
||||
|
||||
print(payload)
|
||||
payload = filter_pipeline(payload, user)
|
||||
|
||||
if model["owned_by"] == "ollama":
|
||||
return await generate_ollama_chat_completion(
|
||||
OpenAIChatCompletionForm(**payload), user=user
|
||||
)
|
||||
else:
|
||||
return await generate_openai_chat_completion(payload, user=user)
|
||||
|
||||
|
||||
@app.post("/api/task/query/completions")
|
||||
async def generate_search_query(form_data: dict, user=Depends(get_verified_user)):
|
||||
print("generate_search_query")
|
||||
|
||||
if len(form_data["prompt"]) < app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Skip search query generation for short prompts (< {app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD} characters)",
|
||||
)
|
||||
|
||||
model_id = form_data["model"]
|
||||
if model_id not in app.state.MODELS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Model not found",
|
||||
)
|
||||
|
||||
# Check if the user has a custom task model
|
||||
# If the user has a custom task model, use that model
|
||||
if app.state.MODELS[model_id]["owned_by"] == "ollama":
|
||||
if app.state.config.TASK_MODEL:
|
||||
task_model_id = app.state.config.TASK_MODEL
|
||||
if task_model_id in app.state.MODELS:
|
||||
model_id = task_model_id
|
||||
else:
|
||||
if app.state.config.TASK_MODEL_EXTERNAL:
|
||||
task_model_id = app.state.config.TASK_MODEL_EXTERNAL
|
||||
if task_model_id in app.state.MODELS:
|
||||
model_id = task_model_id
|
||||
|
||||
print(model_id)
|
||||
model = app.state.MODELS[model_id]
|
||||
|
||||
template = app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||
|
||||
content = search_query_generation_template(
|
||||
template, form_data["prompt"], user.model_dump()
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": model_id,
|
||||
"messages": [{"role": "user", "content": content}],
|
||||
"stream": False,
|
||||
"max_tokens": 30,
|
||||
}
|
||||
|
||||
print(payload)
|
||||
payload = filter_pipeline(payload, user)
|
||||
|
||||
if model["owned_by"] == "ollama":
|
||||
return await generate_ollama_chat_completion(
|
||||
OpenAIChatCompletionForm(**payload), user=user
|
||||
)
|
||||
else:
|
||||
return await generate_openai_chat_completion(payload, user=user)
|
||||
|
||||
|
||||
@app.post("/api/chat/completions")
|
||||
async def generate_chat_completions(form_data: dict, user=Depends(get_verified_user)):
|
||||
model_id = form_data["model"]
|
||||
if model_id not in app.state.MODELS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Model not found",
|
||||
)
|
||||
|
||||
model = app.state.MODELS[model_id]
|
||||
print(model)
|
||||
|
||||
if model["owned_by"] == "ollama":
|
||||
return await generate_ollama_chat_completion(
|
||||
OpenAIChatCompletionForm(**form_data), user=user
|
||||
)
|
||||
else:
|
||||
return await generate_openai_chat_completion(form_data, user=user)
|
||||
|
||||
|
||||
@app.post("/api/chat/completed")
|
||||
async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
|
||||
data = form_data
|
||||
@@ -569,6 +771,63 @@ async def get_pipelines_list(user=Depends(get_admin_user)):
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/pipelines/upload")
|
||||
async def upload_pipeline(
|
||||
urlIdx: int = Form(...), file: UploadFile = File(...), user=Depends(get_admin_user)
|
||||
):
|
||||
print("upload_pipeline", urlIdx, file.filename)
|
||||
# Check if the uploaded file is a python file
|
||||
if not file.filename.endswith(".py"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Only Python (.py) files are allowed.",
|
||||
)
|
||||
|
||||
upload_folder = f"{CACHE_DIR}/pipelines"
|
||||
os.makedirs(upload_folder, exist_ok=True)
|
||||
file_path = os.path.join(upload_folder, file.filename)
|
||||
|
||||
try:
|
||||
# Save the uploaded file
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
|
||||
key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
|
||||
|
||||
headers = {"Authorization": f"Bearer {key}"}
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
files = {"file": f}
|
||||
r = requests.post(f"{url}/pipelines/upload", headers=headers, files=files)
|
||||
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
|
||||
return {**data}
|
||||
except Exception as e:
|
||||
# Handle connection error here
|
||||
print(f"Connection error: {e}")
|
||||
|
||||
detail = "Pipeline not found"
|
||||
if r is not None:
|
||||
try:
|
||||
res = r.json()
|
||||
if "detail" in res:
|
||||
detail = res["detail"]
|
||||
except:
|
||||
pass
|
||||
|
||||
raise HTTPException(
|
||||
status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND),
|
||||
detail=detail,
|
||||
)
|
||||
finally:
|
||||
# Ensure the file is deleted after the upload is completed or on failure
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
|
||||
class AddPipelineForm(BaseModel):
|
||||
url: str
|
||||
urlIdx: int
|
||||
@@ -835,6 +1094,15 @@ async def get_app_config():
|
||||
"enable_community_sharing": webui_app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||
"enable_admin_export": ENABLE_ADMIN_EXPORT,
|
||||
},
|
||||
"audio": {
|
||||
"tts": {
|
||||
"engine": audio_app.state.config.TTS_ENGINE,
|
||||
"voice": audio_app.state.config.TTS_VOICE,
|
||||
},
|
||||
"stt": {
|
||||
"engine": audio_app.state.config.STT_ENGINE,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -879,23 +1147,7 @@ class UrlForm(BaseModel):
|
||||
async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
|
||||
app.state.config.WEBHOOK_URL = form_data.url
|
||||
webui_app.state.WEBHOOK_URL = app.state.config.WEBHOOK_URL
|
||||
|
||||
return {
|
||||
"url": app.state.config.WEBHOOK_URL,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/community_sharing", response_model=bool)
|
||||
async def get_community_sharing_status(request: Request, user=Depends(get_admin_user)):
|
||||
return webui_app.state.config.ENABLE_COMMUNITY_SHARING
|
||||
|
||||
|
||||
@app.get("/api/community_sharing/toggle", response_model=bool)
|
||||
async def toggle_community_sharing(request: Request, user=Depends(get_admin_user)):
|
||||
webui_app.state.config.ENABLE_COMMUNITY_SHARING = (
|
||||
not webui_app.state.config.ENABLE_COMMUNITY_SHARING
|
||||
)
|
||||
return webui_app.state.config.ENABLE_COMMUNITY_SHARING
|
||||
return {"url": app.state.config.WEBHOOK_URL}
|
||||
|
||||
|
||||
@app.get("/api/version")
|
||||
@@ -913,7 +1165,7 @@ async def get_app_changelog():
|
||||
@app.get("/api/version/updates")
|
||||
async def get_app_latest_release_version():
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with session.get(
|
||||
"https://api.github.com/repos/open-webui/open-webui/releases/latest"
|
||||
) as response:
|
||||
|
||||
@@ -56,4 +56,7 @@ PyJWT[crypto]==2.8.0
|
||||
black==24.4.2
|
||||
langfuse==2.33.0
|
||||
youtube-transcript-api==0.6.2
|
||||
pytube==15.0.0
|
||||
pytube==15.0.0
|
||||
|
||||
extract_msg
|
||||
pydub
|
||||
+2
-2
@@ -20,12 +20,12 @@ if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then
|
||||
WEBUI_SECRET_KEY=$(cat "$KEY_FILE")
|
||||
fi
|
||||
|
||||
if [ "$USE_OLLAMA_DOCKER" = "true" ]; then
|
||||
if [[ "${USE_OLLAMA_DOCKER,,}" == "true" ]]; then
|
||||
echo "USE_OLLAMA is set to true, starting ollama serve."
|
||||
ollama serve &
|
||||
fi
|
||||
|
||||
if [ "$USE_CUDA_DOCKER" = "true" ]; then
|
||||
if [[ "${USE_CUDA_DOCKER,,}" == "true" ]]; then
|
||||
echo "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries."
|
||||
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib"
|
||||
fi
|
||||
|
||||
+75
-6
@@ -3,7 +3,48 @@ import hashlib
|
||||
import json
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
def get_last_user_message(messages: List[dict]) -> str:
|
||||
for message in reversed(messages):
|
||||
if message["role"] == "user":
|
||||
if isinstance(message["content"], list):
|
||||
for item in message["content"]:
|
||||
if item["type"] == "text":
|
||||
return item["text"]
|
||||
return message["content"]
|
||||
return None
|
||||
|
||||
|
||||
def get_last_assistant_message(messages: List[dict]) -> str:
|
||||
for message in reversed(messages):
|
||||
if message["role"] == "assistant":
|
||||
if isinstance(message["content"], list):
|
||||
for item in message["content"]:
|
||||
if item["type"] == "text":
|
||||
return item["text"]
|
||||
return message["content"]
|
||||
return None
|
||||
|
||||
|
||||
def add_or_update_system_message(content: str, messages: List[dict]):
|
||||
"""
|
||||
Adds a new system message at the beginning of the messages list
|
||||
or updates the existing system message at the beginning.
|
||||
|
||||
:param msg: The message to be added or appended.
|
||||
:param messages: The list of message dictionaries.
|
||||
:return: The updated list of message dictionaries.
|
||||
"""
|
||||
|
||||
if messages and messages[0].get("role") == "system":
|
||||
messages[0]["content"] += f"{content}\n{messages[0]['content']}"
|
||||
else:
|
||||
# Insert at the beginning
|
||||
messages.insert(0, {"role": "system", "content": content})
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
def get_gravatar_url(email):
|
||||
@@ -123,11 +164,25 @@ def parse_ollama_modelfile(model_text):
|
||||
"repeat_penalty": float,
|
||||
"temperature": float,
|
||||
"seed": int,
|
||||
"stop": str,
|
||||
"tfs_z": float,
|
||||
"num_predict": int,
|
||||
"top_k": int,
|
||||
"top_p": float,
|
||||
"num_keep": int,
|
||||
"typical_p": float,
|
||||
"presence_penalty": float,
|
||||
"frequency_penalty": float,
|
||||
"penalize_newline": bool,
|
||||
"numa": bool,
|
||||
"num_batch": int,
|
||||
"num_gpu": int,
|
||||
"main_gpu": int,
|
||||
"low_vram": bool,
|
||||
"f16_kv": bool,
|
||||
"vocab_only": bool,
|
||||
"use_mmap": bool,
|
||||
"use_mlock": bool,
|
||||
"num_thread": int,
|
||||
}
|
||||
|
||||
data = {"base_model_id": None, "params": {}}
|
||||
@@ -156,10 +211,18 @@ def parse_ollama_modelfile(model_text):
|
||||
param_match = re.search(rf"PARAMETER {param} (.+)", model_text, re.IGNORECASE)
|
||||
if param_match:
|
||||
value = param_match.group(1)
|
||||
if param_type == int:
|
||||
value = int(value)
|
||||
elif param_type == float:
|
||||
value = float(value)
|
||||
|
||||
try:
|
||||
if param_type == int:
|
||||
value = int(value)
|
||||
elif param_type == float:
|
||||
value = float(value)
|
||||
elif param_type == bool:
|
||||
value = value.lower() == "true"
|
||||
except Exception as e:
|
||||
print(e)
|
||||
continue
|
||||
|
||||
data["params"][param] = value
|
||||
|
||||
# Parse adapter
|
||||
@@ -171,8 +234,14 @@ def parse_ollama_modelfile(model_text):
|
||||
system_desc_match = re.search(
|
||||
r'SYSTEM\s+"""(.+?)"""', model_text, re.DOTALL | re.IGNORECASE
|
||||
)
|
||||
system_desc_match_single = re.search(
|
||||
r"SYSTEM\s+([^\n]+)", model_text, re.IGNORECASE
|
||||
)
|
||||
|
||||
if system_desc_match:
|
||||
data["params"]["system"] = system_desc_match.group(1).strip()
|
||||
elif system_desc_match_single:
|
||||
data["params"]["system"] = system_desc_match_single.group(1).strip()
|
||||
|
||||
# Parse messages
|
||||
messages = []
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse
|
||||
|
||||
|
||||
def get_model_id_from_custom_model_id(id: str):
|
||||
model = Models.get_model_by_id(id)
|
||||
|
||||
if model:
|
||||
return model.id
|
||||
else:
|
||||
return id
|
||||
@@ -0,0 +1,112 @@
|
||||
import re
|
||||
import math
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def prompt_template(
|
||||
template: str, user_name: str = None, current_location: str = None
|
||||
) -> str:
|
||||
# Get the current date
|
||||
current_date = datetime.now()
|
||||
|
||||
# Format the date to YYYY-MM-DD
|
||||
formatted_date = current_date.strftime("%Y-%m-%d")
|
||||
|
||||
# Replace {{CURRENT_DATE}} in the template with the formatted date
|
||||
template = template.replace("{{CURRENT_DATE}}", formatted_date)
|
||||
|
||||
if user_name:
|
||||
# Replace {{USER_NAME}} in the template with the user's name
|
||||
template = template.replace("{{USER_NAME}}", user_name)
|
||||
|
||||
if current_location:
|
||||
# Replace {{CURRENT_LOCATION}} in the template with the current location
|
||||
template = template.replace("{{CURRENT_LOCATION}}", current_location)
|
||||
|
||||
return template
|
||||
|
||||
|
||||
def title_generation_template(
|
||||
template: str, prompt: str, user: Optional[dict] = None
|
||||
) -> str:
|
||||
def replacement_function(match):
|
||||
full_match = match.group(0)
|
||||
start_length = match.group(1)
|
||||
end_length = match.group(2)
|
||||
middle_length = match.group(3)
|
||||
|
||||
if full_match == "{{prompt}}":
|
||||
return prompt
|
||||
elif start_length is not None:
|
||||
return prompt[: int(start_length)]
|
||||
elif end_length is not None:
|
||||
return prompt[-int(end_length) :]
|
||||
elif middle_length is not None:
|
||||
middle_length = int(middle_length)
|
||||
if len(prompt) <= middle_length:
|
||||
return prompt
|
||||
start = prompt[: math.ceil(middle_length / 2)]
|
||||
end = prompt[-math.floor(middle_length / 2) :]
|
||||
return f"{start}...{end}"
|
||||
return ""
|
||||
|
||||
template = re.sub(
|
||||
r"{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}",
|
||||
replacement_function,
|
||||
template,
|
||||
)
|
||||
|
||||
template = prompt_template(
|
||||
template,
|
||||
**(
|
||||
{"user_name": user.get("name"), "current_location": user.get("location")}
|
||||
if user
|
||||
else {}
|
||||
),
|
||||
)
|
||||
|
||||
return template
|
||||
|
||||
|
||||
def search_query_generation_template(
|
||||
template: str, prompt: str, user: Optional[dict] = None
|
||||
) -> str:
|
||||
|
||||
def replacement_function(match):
|
||||
full_match = match.group(0)
|
||||
start_length = match.group(1)
|
||||
end_length = match.group(2)
|
||||
middle_length = match.group(3)
|
||||
|
||||
if full_match == "{{prompt}}":
|
||||
return prompt
|
||||
elif start_length is not None:
|
||||
return prompt[: int(start_length)]
|
||||
elif end_length is not None:
|
||||
return prompt[-int(end_length) :]
|
||||
elif middle_length is not None:
|
||||
middle_length = int(middle_length)
|
||||
if len(prompt) <= middle_length:
|
||||
return prompt
|
||||
start = prompt[: math.ceil(middle_length / 2)]
|
||||
end = prompt[-math.floor(middle_length / 2) :]
|
||||
return f"{start}...{end}"
|
||||
return ""
|
||||
|
||||
template = re.sub(
|
||||
r"{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}",
|
||||
replacement_function,
|
||||
template,
|
||||
)
|
||||
|
||||
template = prompt_template(
|
||||
template,
|
||||
**(
|
||||
{"user_name": user.get("name"), "current_location": user.get("location")}
|
||||
if user
|
||||
else {}
|
||||
),
|
||||
)
|
||||
return template
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.0 MiB After Width: | Height: | Size: 4.1 MiB |
@@ -41,10 +41,11 @@ Looking to contribute? Great! Here's how you can help:
|
||||
|
||||
We welcome pull requests. Before submitting one, please:
|
||||
|
||||
1. Discuss your idea or issue in the [issues section](https://github.com/open-webui/open-webui/issues).
|
||||
1. Open a discussion regarding your ideas [here](https://github.com/open-webui/open-webui/discussions/new/choose).
|
||||
2. Follow the project's coding standards and include tests for new features.
|
||||
3. Update documentation as necessary.
|
||||
4. Write clear, descriptive commit messages.
|
||||
5. It's essential to complete your pull request in a timely manner. We move fast, and having PRs hang around too long is not feasible. If you can't get it done within a reasonable time frame, we may have to close it to keep the project moving forward.
|
||||
|
||||
### 📚 Documentation & Tutorials
|
||||
|
||||
|
||||
Generated
+82
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.2.2",
|
||||
"version": "0.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "open-webui",
|
||||
"version": "0.2.2",
|
||||
"version": "0.3.0",
|
||||
"dependencies": {
|
||||
"@pyscript/core": "^0.4.32",
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
@@ -25,6 +25,7 @@
|
||||
"marked": "^9.1.0",
|
||||
"mermaid": "^10.9.1",
|
||||
"pyodide": "^0.26.0-alpha.4",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"sortablejs": "^1.15.2",
|
||||
"svelte-sonner": "^0.3.19",
|
||||
"tippy.js": "^6.3.7",
|
||||
@@ -1214,6 +1215,11 @@
|
||||
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
||||
},
|
||||
"node_modules/@sveltejs/adapter-auto": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-2.1.1.tgz",
|
||||
@@ -3800,6 +3806,46 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
|
||||
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.11.0",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client/node_modules/ws": {
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz",
|
||||
"integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enquirer": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
|
||||
@@ -7949,6 +7995,32 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.7.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
|
||||
"integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sorcery": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz",
|
||||
@@ -10142,6 +10214,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.2.2",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||
@@ -65,6 +65,7 @@
|
||||
"marked": "^9.1.0",
|
||||
"mermaid": "^10.9.1",
|
||||
"pyodide": "^0.26.0-alpha.4",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"sortablejs": "^1.15.2",
|
||||
"svelte-sonner": "^0.3.19",
|
||||
"tippy.js": "^6.3.7",
|
||||
|
||||
+18
-9
@@ -59,15 +59,7 @@
|
||||
|
||||
<div
|
||||
id="splash-screen"
|
||||
style="
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
background: #fff;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
"
|
||||
style="position: fixed; z-index: 100; top: 0; left: 0; width: 100%; height: 100%"
|
||||
>
|
||||
<style type="text/css" nonce="">
|
||||
html {
|
||||
@@ -93,3 +85,20 @@
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style type="text/css" nonce="">
|
||||
html {
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
|
||||
#splash-screen {
|
||||
background: #fff;
|
||||
}
|
||||
html.dark #splash-screen {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
html.dark #splash-screen img {
|
||||
filter: invert(1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -98,7 +98,7 @@ export const synthesizeOpenAISpeech = async (
|
||||
token: string = '',
|
||||
speaker: string = 'alloy',
|
||||
text: string = '',
|
||||
model: string = 'tts-1'
|
||||
model?: string
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
@@ -109,9 +109,9 @@ export const synthesizeOpenAISpeech = async (
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
input: text,
|
||||
voice: speaker
|
||||
voice: speaker,
|
||||
...(model && { model })
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
|
||||
@@ -1,5 +1,87 @@
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const getAdminDetails = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/details`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getAdminConfig = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/config`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateAdminConfig = async (token: string, body: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/config`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getSessionUser = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -162,6 +162,37 @@ export const getAllChats = async (token: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getAllArchivedChats = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/all/archived`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err;
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getAllUserChats = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -104,6 +104,147 @@ export const chatCompleted = async (token: string, body: ChatCompletedForm) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getTaskConfig = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/task/config`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
error = err;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateTaskConfig = async (token: string, config: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/task/config/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify(config)
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = err;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const generateTitle = async (
|
||||
token: string = '',
|
||||
model: string,
|
||||
prompt: string,
|
||||
chat_id?: string
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/task/title/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
...(chat_id && { chat_id: chat_id })
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? 'New Chat';
|
||||
};
|
||||
|
||||
export const generateSearchQuery = async (
|
||||
token: string = '',
|
||||
model: string,
|
||||
messages: object[],
|
||||
prompt: string
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/task/query/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
messages: messages,
|
||||
prompt: prompt
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? prompt;
|
||||
};
|
||||
|
||||
export const getPipelinesList = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
@@ -133,6 +274,43 @@ export const getPipelinesList = async (token: string = '') => {
|
||||
return pipelines;
|
||||
};
|
||||
|
||||
export const uploadPipeline = async (token: string, file: File, urlIdx: string) => {
|
||||
let error = null;
|
||||
|
||||
// Create a new FormData object to handle the file upload
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('urlIdx', urlIdx);
|
||||
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/pipelines/upload`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
// 'Content-Type': 'multipart/form-data' is not needed as Fetch API will set it automatically
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = err;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const downloadPipeline = async (token: string, url: string, urlIdx: string) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -369,21 +369,29 @@ export const generateChatCompletion = async (token: string = '', body: object) =
|
||||
return [res, controller];
|
||||
};
|
||||
|
||||
export const createModel = async (token: string, tagName: string, content: string) => {
|
||||
export const createModel = async (
|
||||
token: string,
|
||||
tagName: string,
|
||||
content: string,
|
||||
urlIdx: string | null = null
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: tagName,
|
||||
modelfile: content
|
||||
})
|
||||
}).catch((err) => {
|
||||
const res = await fetch(
|
||||
`${OLLAMA_API_BASE_URL}/api/create${urlIdx !== null ? `/${urlIdx}` : ''}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: tagName,
|
||||
modelfile: content
|
||||
})
|
||||
}
|
||||
).catch((err) => {
|
||||
error = err;
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -359,6 +359,32 @@ export const scanDocs = async (token: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const resetUploadDir = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${RAG_API_BASE_URL}/reset/uploads`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const resetVectorDB = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
@@ -415,6 +441,7 @@ export const getEmbeddingConfig = async (token: string) => {
|
||||
type OpenAIConfigForm = {
|
||||
key: string;
|
||||
url: string;
|
||||
batch_size: number;
|
||||
};
|
||||
|
||||
type EmbeddingModelUpdateForm = {
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
<script>
|
||||
import { getContext, tick } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import Database from './Settings/Database.svelte';
|
||||
|
||||
import General from './Settings/General.svelte';
|
||||
import Users from './Settings/Users.svelte';
|
||||
|
||||
import Pipelines from './Settings/Pipelines.svelte';
|
||||
import Audio from './Settings/Audio.svelte';
|
||||
import Images from './Settings/Images.svelte';
|
||||
import Interface from './Settings/Interface.svelte';
|
||||
import Models from './Settings/Models.svelte';
|
||||
import Connections from './Settings/Connections.svelte';
|
||||
import Documents from './Settings/Documents.svelte';
|
||||
import WebSearch from './Settings/WebSearch.svelte';
|
||||
import { config } from '$lib/stores';
|
||||
import { getBackendConfig } from '$lib/apis';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let selectedTab = 'general';
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col lg:flex-row w-full h-full py-2 lg:space-x-4">
|
||||
<div
|
||||
class="tabs flex flex-row overflow-x-auto space-x-1 max-w-full lg:space-x-0 lg:space-y-1 lg:flex-col lg:flex-none lg:w-44 dark:text-gray-200 text-xs text-left scrollbar-none"
|
||||
>
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab ===
|
||||
'general'
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'general';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('General')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'users'
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'users';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M8 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM3.156 11.763c.16-.629.44-1.21.813-1.72a2.5 2.5 0 0 0-2.725 1.377c-.136.287.102.58.418.58h1.449c.01-.077.025-.156.045-.237ZM12.847 11.763c.02.08.036.16.046.237h1.446c.316 0 .554-.293.417-.579a2.5 2.5 0 0 0-2.722-1.378c.374.51.653 1.09.813 1.72ZM14 7.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM3.5 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM5 13c-.552 0-1.013-.455-.876-.99a4.002 4.002 0 0 1 7.753 0c.136.535-.324.99-.877.99H5Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Users')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'connections'
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'connections';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Connections')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'models'
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'models';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 1c3.866 0 7 1.79 7 4s-3.134 4-7 4-7-1.79-7-4 3.134-4 7-4zm5.694 8.13c.464-.264.91-.583 1.306-.952V10c0 2.21-3.134 4-7 4s-7-1.79-7-4V8.178c.396.37.842.688 1.306.953C5.838 10.006 7.854 10.5 10 10.5s4.162-.494 5.694-1.37zM3 13.179V15c0 2.21 3.134 4 7 4s7-1.79 7-4v-1.822c-.396.37-.842.688-1.306.953-1.532.875-3.548 1.369-5.694 1.369s-4.162-.494-5.694-1.37A7.009 7.009 0 013 13.179z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Models')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'documents'
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'documents';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M11.625 16.5a1.875 1.875 0 1 0 0-3.75 1.875 1.875 0 0 0 0 3.75Z" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875Zm6 16.5c.66 0 1.277-.19 1.797-.518l1.048 1.048a.75.75 0 0 0 1.06-1.06l-1.047-1.048A3.375 3.375 0 1 0 11.625 18Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Documents')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'web'
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'web';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M21.721 12.752a9.711 9.711 0 0 0-.945-5.003 12.754 12.754 0 0 1-4.339 2.708 18.991 18.991 0 0 1-.214 4.772 17.165 17.165 0 0 0 5.498-2.477ZM14.634 15.55a17.324 17.324 0 0 0 .332-4.647c-.952.227-1.945.347-2.966.347-1.021 0-2.014-.12-2.966-.347a17.515 17.515 0 0 0 .332 4.647 17.385 17.385 0 0 0 5.268 0ZM9.772 17.119a18.963 18.963 0 0 0 4.456 0A17.182 17.182 0 0 1 12 21.724a17.18 17.18 0 0 1-2.228-4.605ZM7.777 15.23a18.87 18.87 0 0 1-.214-4.774 12.753 12.753 0 0 1-4.34-2.708 9.711 9.711 0 0 0-.944 5.004 17.165 17.165 0 0 0 5.498 2.477ZM21.356 14.752a9.765 9.765 0 0 1-7.478 6.817 18.64 18.64 0 0 0 1.988-4.718 18.627 18.627 0 0 0 5.49-2.098ZM2.644 14.752c1.682.971 3.53 1.688 5.49 2.099a18.64 18.64 0 0 0 1.988 4.718 9.765 9.765 0 0 1-7.478-6.816ZM13.878 2.43a9.755 9.755 0 0 1 6.116 3.986 11.267 11.267 0 0 1-3.746 2.504 18.63 18.63 0 0 0-2.37-6.49ZM12 2.276a17.152 17.152 0 0 1 2.805 7.121c-.897.23-1.837.353-2.805.353-.968 0-1.908-.122-2.805-.353A17.151 17.151 0 0 1 12 2.276ZM10.122 2.43a18.629 18.629 0 0 0-2.37 6.49 11.266 11.266 0 0 1-3.746-2.504 9.754 9.754 0 0 1 6.116-3.985Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Web Search')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'interface'
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'interface';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 4.25A2.25 2.25 0 0 1 4.25 2h7.5A2.25 2.25 0 0 1 14 4.25v5.5A2.25 2.25 0 0 1 11.75 12h-1.312c.1.128.21.248.328.36a.75.75 0 0 1 .234.545v.345a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-.345a.75.75 0 0 1 .234-.545c.118-.111.228-.232.328-.36H4.25A2.25 2.25 0 0 1 2 9.75v-5.5Zm2.25-.75a.75.75 0 0 0-.75.75v4.5c0 .414.336.75.75.75h7.5a.75.75 0 0 0 .75-.75v-4.5a.75.75 0 0 0-.75-.75h-7.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Interface')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'audio'
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'audio';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M7.557 2.066A.75.75 0 0 1 8 2.75v10.5a.75.75 0 0 1-1.248.56L3.59 11H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.59l3.162-2.81a.75.75 0 0 1 .805-.124ZM12.95 3.05a.75.75 0 1 0-1.06 1.06 5.5 5.5 0 0 1 0 7.78.75.75 0 1 0 1.06 1.06 7 7 0 0 0 0-9.9Z"
|
||||
/>
|
||||
<path
|
||||
d="M10.828 5.172a.75.75 0 1 0-1.06 1.06 2.5 2.5 0 0 1 0 3.536.75.75 0 1 0 1.06 1.06 4 4 0 0 0 0-5.656Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Audio')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'images'
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'images';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Images')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'pipelines'
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'pipelines';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
d="M11.644 1.59a.75.75 0 0 1 .712 0l9.75 5.25a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.712 0l-9.75-5.25a.75.75 0 0 1 0-1.32l9.75-5.25Z"
|
||||
/>
|
||||
<path
|
||||
d="m3.265 10.602 7.668 4.129a2.25 2.25 0 0 0 2.134 0l7.668-4.13 1.37.739a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.71 0l-9.75-5.25a.75.75 0 0 1 0-1.32l1.37-.738Z"
|
||||
/>
|
||||
<path
|
||||
d="m10.933 19.231-7.668-4.13-1.37.739a.75.75 0 0 0 0 1.32l9.75 5.25c.221.12.489.12.71 0l9.75-5.25a.75.75 0 0 0 0-1.32l-1.37-.738-7.668 4.13a2.25 2.25 0 0 1-2.134-.001Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Pipelines')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'db'
|
||||
? 'bg-gray-200 dark:bg-gray-800'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-850'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'db';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M8 7c3.314 0 6-1.343 6-3s-2.686-3-6-3-6 1.343-6 3 2.686 3 6 3Z" />
|
||||
<path
|
||||
d="M8 8.5c1.84 0 3.579-.37 4.914-1.037A6.33 6.33 0 0 0 14 6.78V8c0 1.657-2.686 3-6 3S2 9.657 2 8V6.78c.346.273.72.5 1.087.683C4.42 8.131 6.16 8.5 8 8.5Z"
|
||||
/>
|
||||
<path
|
||||
d="M8 12.5c1.84 0 3.579-.37 4.914-1.037.366-.183.74-.41 1.086-.684V12c0 1.657-2.686 3-6 3s-6-1.343-6-3v-1.22c.346.273.72.5 1.087.683C4.42 12.131 6.16 12.5 8 12.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Database')}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 mt-3 lg:mt-0 overflow-y-scroll">
|
||||
{#if selectedTab === 'general'}
|
||||
<General
|
||||
saveHandler={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'users'}
|
||||
<Users
|
||||
saveHandler={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'connections'}
|
||||
<Connections
|
||||
on:save={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'models'}
|
||||
<Models />
|
||||
{:else if selectedTab === 'documents'}
|
||||
<Documents
|
||||
saveHandler={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'web'}
|
||||
<WebSearch
|
||||
saveHandler={async () => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
|
||||
await tick();
|
||||
await config.set(await getBackendConfig());
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'interface'}
|
||||
<Interface
|
||||
on:save={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'audio'}
|
||||
<Audio
|
||||
saveHandler={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'images'}
|
||||
<Images
|
||||
on:save={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'db'}
|
||||
<Database
|
||||
saveHandler={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'pipelines'}
|
||||
<Pipelines
|
||||
saveHandler={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,302 @@
|
||||
<script lang="ts">
|
||||
import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio';
|
||||
import { user, settings, config } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import { getBackendConfig } from '$lib/apis';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let saveHandler: Function;
|
||||
|
||||
// Audio
|
||||
|
||||
let TTS_OPENAI_API_BASE_URL = '';
|
||||
let TTS_OPENAI_API_KEY = '';
|
||||
let TTS_ENGINE = '';
|
||||
let TTS_MODEL = '';
|
||||
let TTS_VOICE = '';
|
||||
|
||||
let STT_OPENAI_API_BASE_URL = '';
|
||||
let STT_OPENAI_API_KEY = '';
|
||||
let STT_ENGINE = '';
|
||||
let STT_MODEL = '';
|
||||
|
||||
let voices = [];
|
||||
let models = [];
|
||||
let nonLocalVoices = false;
|
||||
|
||||
const getOpenAIVoices = () => {
|
||||
voices = [
|
||||
{ name: 'alloy' },
|
||||
{ name: 'echo' },
|
||||
{ name: 'fable' },
|
||||
{ name: 'onyx' },
|
||||
{ name: 'nova' },
|
||||
{ name: 'shimmer' }
|
||||
];
|
||||
};
|
||||
|
||||
const getOpenAIModels = () => {
|
||||
models = [{ name: 'tts-1' }, { name: 'tts-1-hd' }];
|
||||
};
|
||||
|
||||
const getWebAPIVoices = () => {
|
||||
const getVoicesLoop = setInterval(async () => {
|
||||
voices = await speechSynthesis.getVoices();
|
||||
|
||||
// do your loop
|
||||
if (voices.length > 0) {
|
||||
clearInterval(getVoicesLoop);
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const updateConfigHandler = async () => {
|
||||
const res = await updateAudioConfig(localStorage.token, {
|
||||
tts: {
|
||||
OPENAI_API_BASE_URL: TTS_OPENAI_API_BASE_URL,
|
||||
OPENAI_API_KEY: TTS_OPENAI_API_KEY,
|
||||
ENGINE: TTS_ENGINE,
|
||||
MODEL: TTS_MODEL,
|
||||
VOICE: TTS_VOICE
|
||||
},
|
||||
stt: {
|
||||
OPENAI_API_BASE_URL: STT_OPENAI_API_BASE_URL,
|
||||
OPENAI_API_KEY: STT_OPENAI_API_KEY,
|
||||
ENGINE: STT_ENGINE,
|
||||
MODEL: STT_MODEL
|
||||
}
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success('Audio settings updated successfully');
|
||||
|
||||
config.set(await getBackendConfig());
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
const res = await getAudioConfig(localStorage.token);
|
||||
|
||||
if (res) {
|
||||
console.log(res);
|
||||
TTS_OPENAI_API_BASE_URL = res.tts.OPENAI_API_BASE_URL;
|
||||
TTS_OPENAI_API_KEY = res.tts.OPENAI_API_KEY;
|
||||
|
||||
TTS_ENGINE = res.tts.ENGINE;
|
||||
TTS_MODEL = res.tts.MODEL;
|
||||
TTS_VOICE = res.tts.VOICE;
|
||||
|
||||
STT_OPENAI_API_BASE_URL = res.stt.OPENAI_API_BASE_URL;
|
||||
STT_OPENAI_API_KEY = res.stt.OPENAI_API_KEY;
|
||||
|
||||
STT_ENGINE = res.stt.ENGINE;
|
||||
STT_MODEL = res.stt.MODEL;
|
||||
}
|
||||
|
||||
if (TTS_ENGINE === 'openai') {
|
||||
getOpenAIVoices();
|
||||
getOpenAIModels();
|
||||
} else {
|
||||
getWebAPIVoices();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={async () => {
|
||||
await updateConfigHandler();
|
||||
dispatch('save');
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={STT_ENGINE}
|
||||
placeholder="Select an engine"
|
||||
>
|
||||
<option value="">{$i18n.t('Whisper (Local)')}</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="web">{$i18n.t('Web API')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if STT_ENGINE === 'openai'}
|
||||
<div>
|
||||
<div class="mt-1 flex gap-2 mb-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={STT_OPENAI_API_BASE_URL}
|
||||
required
|
||||
/>
|
||||
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={STT_OPENAI_API_KEY}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="model-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={STT_MODEL}
|
||||
placeholder="Select a model"
|
||||
/>
|
||||
|
||||
<datalist id="model-list">
|
||||
<option value="whisper-1" />
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-800" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={TTS_ENGINE}
|
||||
placeholder="Select a mode"
|
||||
on:change={(e) => {
|
||||
if (e.target.value === 'openai') {
|
||||
getOpenAIVoices();
|
||||
TTS_VOICE = 'alloy';
|
||||
TTS_MODEL = 'tts-1';
|
||||
} else {
|
||||
getWebAPIVoices();
|
||||
TTS_VOICE = '';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">{$i18n.t('Web API')}</option>
|
||||
<option value="openai">{$i18n.t('Open AI')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if TTS_ENGINE === 'openai'}
|
||||
<div>
|
||||
<div class="mt-1 flex gap-2 mb-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={TTS_OPENAI_API_BASE_URL}
|
||||
required
|
||||
/>
|
||||
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={TTS_OPENAI_API_KEY}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
|
||||
{#if TTS_ENGINE === ''}
|
||||
<div>
|
||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={TTS_VOICE}
|
||||
>
|
||||
<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
|
||||
{#each voices as voice}
|
||||
<option
|
||||
value={voice.voiceURI}
|
||||
class="bg-gray-100 dark:bg-gray-700"
|
||||
selected={TTS_VOICE === voice.voiceURI}>{voice.name}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if TTS_ENGINE === 'openai'}
|
||||
<div class=" flex gap-2">
|
||||
<div class="w-full">
|
||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="voice-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={TTS_VOICE}
|
||||
placeholder="Select a voice"
|
||||
/>
|
||||
|
||||
<datalist id="voice-list">
|
||||
{#each voices as voice}
|
||||
<option value={voice.name} />
|
||||
{/each}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Model')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="model-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={TTS_MODEL}
|
||||
placeholder="Select a model"
|
||||
/>
|
||||
|
||||
<datalist id="model-list">
|
||||
{#each models as model}
|
||||
<option value={model.name} />
|
||||
{/each}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,137 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import { banners as _banners } from '$lib/stores';
|
||||
import type { Banner } from '$lib/types';
|
||||
|
||||
import { getBanners, setBanners } from '$lib/apis/configs';
|
||||
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
const i18n: Writable<i18nType> = getContext('i18n');
|
||||
|
||||
export let saveHandler: Function;
|
||||
|
||||
let banners: Banner[] = [];
|
||||
|
||||
onMount(async () => {
|
||||
banners = await getBanners(localStorage.token);
|
||||
});
|
||||
|
||||
const updateBanners = async () => {
|
||||
_banners.set(await setBanners(localStorage.token, banners));
|
||||
};
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={async () => {
|
||||
updateBanners();
|
||||
saveHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80 h-full">
|
||||
<div class=" space-y-3 pr-1.5">
|
||||
<div class="flex w-full justify-between mb-2">
|
||||
<div class=" self-center text-sm font-semibold">
|
||||
{$i18n.t('Banners')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (banners.length === 0 || banners.at(-1).content !== '') {
|
||||
banners = [
|
||||
...banners,
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: '',
|
||||
title: '',
|
||||
content: '',
|
||||
dismissible: true,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
];
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-1">
|
||||
{#each banners as banner, bannerIdx}
|
||||
<div class=" flex justify-between">
|
||||
<div class="flex flex-row flex-1 border rounded-xl dark:border-gray-800">
|
||||
<select
|
||||
class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-none"
|
||||
bind:value={banner.type}
|
||||
>
|
||||
{#if banner.type == ''}
|
||||
<option value="" selected disabled class="text-gray-900">{$i18n.t('Type')}</option
|
||||
>
|
||||
{/if}
|
||||
<option value="info" class="text-gray-900">{$i18n.t('Info')}</option>
|
||||
<option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option>
|
||||
<option value="error" class="text-gray-900">{$i18n.t('Error')}</option>
|
||||
<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
class="pr-5 py-1.5 text-xs w-full bg-transparent outline-none"
|
||||
placeholder={$i18n.t('Content')}
|
||||
bind:value={banner.content}
|
||||
/>
|
||||
|
||||
<div class="relative top-1.5 -left-2">
|
||||
<Tooltip content="Dismissible" className="flex h-fit items-center">
|
||||
<Switch bind:state={banner.dismissible} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="px-2"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
banners.splice(bannerIdx, 1);
|
||||
banners = banners;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
+7
-3
@@ -23,10 +23,14 @@
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import { getModels as _getModels } from '$lib/apis';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let getModels: Function;
|
||||
const getModels = async () => {
|
||||
const models = await _getModels(localStorage.token);
|
||||
return models;
|
||||
};
|
||||
|
||||
// External
|
||||
let OLLAMA_BASE_URLS = [''];
|
||||
@@ -158,7 +162,7 @@
|
||||
dispatch('save');
|
||||
}}
|
||||
>
|
||||
<div class="space-y-3 pr-1.5 overflow-y-scroll h-[24rem] max-h-[25rem]">
|
||||
<div class="space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null}
|
||||
<div class=" space-y-3">
|
||||
<div class="mt-2 space-y-2 pr-1.5">
|
||||
@@ -300,7 +304,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div class="pr-1.5 space-y-2">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
@@ -30,7 +30,7 @@
|
||||
saveHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div>
|
||||
|
||||
|
||||
+346
-76
@@ -8,7 +8,10 @@
|
||||
getEmbeddingConfig,
|
||||
updateEmbeddingConfig,
|
||||
getRerankingConfig,
|
||||
updateRerankingConfig
|
||||
updateRerankingConfig,
|
||||
resetUploadDir,
|
||||
getRAGConfig,
|
||||
updateRAGConfig
|
||||
} from '$lib/apis/rag';
|
||||
|
||||
import { documents, models } from '$lib/stores';
|
||||
@@ -24,13 +27,19 @@
|
||||
let updateRerankingModelLoading = false;
|
||||
|
||||
let showResetConfirm = false;
|
||||
let showResetUploadDirConfirm = false;
|
||||
|
||||
let embeddingEngine = '';
|
||||
let embeddingModel = '';
|
||||
let rerankingModel = '';
|
||||
|
||||
let chunkSize = 0;
|
||||
let chunkOverlap = 0;
|
||||
let pdfExtractImages = true;
|
||||
|
||||
let OpenAIKey = '';
|
||||
let OpenAIUrl = '';
|
||||
let OpenAIBatchSize = 1;
|
||||
|
||||
let querySettings = {
|
||||
template: '',
|
||||
@@ -92,7 +101,8 @@
|
||||
? {
|
||||
openai_config: {
|
||||
key: OpenAIKey,
|
||||
url: OpenAIUrl
|
||||
url: OpenAIUrl,
|
||||
batch_size: OpenAIBatchSize
|
||||
}
|
||||
}
|
||||
: {})
|
||||
@@ -148,6 +158,14 @@
|
||||
if (querySettings.hybrid) {
|
||||
rerankingModelUpdateHandler();
|
||||
}
|
||||
|
||||
const res = await updateRAGConfig(localStorage.token, {
|
||||
pdf_extract_images: pdfExtractImages,
|
||||
chunk: {
|
||||
chunk_overlap: chunkOverlap,
|
||||
chunk_size: chunkSize
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const setEmbeddingConfig = async () => {
|
||||
@@ -159,6 +177,7 @@
|
||||
|
||||
OpenAIKey = embeddingConfig.openai_config.key;
|
||||
OpenAIUrl = embeddingConfig.openai_config.url;
|
||||
OpenAIBatchSize = embeddingConfig.openai_config.batch_size ?? 1;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -180,6 +199,15 @@
|
||||
await setRerankingConfig();
|
||||
|
||||
querySettings = await getQuerySettings(localStorage.token);
|
||||
|
||||
const res = await getRAGConfig(localStorage.token);
|
||||
|
||||
if (res) {
|
||||
pdfExtractImages = res.pdf_extract_images;
|
||||
|
||||
chunkSize = res.chunk.chunk_size;
|
||||
chunkOverlap = res.chunk.chunk_overlap;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -190,7 +218,7 @@
|
||||
saveHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-2.5 pr-1.5 overflow-y-scroll max-h-[28rem]">
|
||||
<div class=" space-y-2.5 overflow-y-scroll scrollbar-hidden h-full">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class=" mb-0.5 text-sm font-medium">{$i18n.t('General Settings')}</div>
|
||||
|
||||
@@ -282,6 +310,30 @@
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="flex mt-0.5 space-x-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Embedding Batch Size')}</div>
|
||||
<div class=" flex-1">
|
||||
<input
|
||||
id="steps-range"
|
||||
type="range"
|
||||
min="1"
|
||||
max="2048"
|
||||
step="1"
|
||||
bind:value={OpenAIBatchSize}
|
||||
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
/>
|
||||
</div>
|
||||
<div class="">
|
||||
<input
|
||||
bind:value={OpenAIBatchSize}
|
||||
type="number"
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="-2"
|
||||
max="16000"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=" flex w-full justify-between">
|
||||
@@ -303,7 +355,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700 my-1" />
|
||||
<hr class=" dark:border-gray-850 my-1" />
|
||||
|
||||
<div class="space-y-2" />
|
||||
<div>
|
||||
@@ -321,10 +373,8 @@
|
||||
{#if !embeddingModel}
|
||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||
{/if}
|
||||
{#each $models.filter((m) => m.id && !m.external) as model}
|
||||
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
|
||||
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
|
||||
>
|
||||
{#each $models.filter((m) => m.id && m.ollama && !(m?.preset ?? false)) as model}
|
||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
@@ -469,99 +519,319 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
{#if showResetConfirm}
|
||||
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z"
|
||||
clip-rule="evenodd"
|
||||
<div class=" ">
|
||||
<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
|
||||
|
||||
<div class=" flex">
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Top K')}</div>
|
||||
|
||||
<div class="self-center p-3">
|
||||
<input
|
||||
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="number"
|
||||
placeholder={$i18n.t('Enter Top K')}
|
||||
bind:value={querySettings.k}
|
||||
autocomplete="off"
|
||||
min="0"
|
||||
/>
|
||||
</svg>
|
||||
<span>{$i18n.t('Are you sure?')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1.5 items-center">
|
||||
{#if querySettings.hybrid === true}
|
||||
<div class="flex w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit">
|
||||
{$i18n.t('Minimum Score')}
|
||||
</div>
|
||||
|
||||
<div class="self-center p-3">
|
||||
<input
|
||||
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder={$i18n.t('Enter Score')}
|
||||
bind:value={querySettings.r}
|
||||
autocomplete="off"
|
||||
min="0.0"
|
||||
title={$i18n.t('The score should be a value between 0.0 (0%) and 1.0 (100%).')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if querySettings.hybrid === true}
|
||||
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t(
|
||||
'Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.'
|
||||
)}
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850 my-3" />
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div>
|
||||
<textarea
|
||||
bind:value={querySettings.template}
|
||||
class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div class=" ">
|
||||
<div class=" text-sm font-medium">{$i18n.t('Chunk Params')}</div>
|
||||
|
||||
<div class=" my-2 flex gap-1.5">
|
||||
<div class=" w-full justify-between">
|
||||
<div class="self-center text-xs font-medium min-w-fit mb-1">{$i18n.t('Chunk Size')}</div>
|
||||
<div class="self-center">
|
||||
<input
|
||||
class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="number"
|
||||
placeholder={$i18n.t('Enter Chunk Size')}
|
||||
bind:value={chunkSize}
|
||||
autocomplete="off"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium min-w-fit mb-1">
|
||||
{$i18n.t('Chunk Overlap')}
|
||||
</div>
|
||||
|
||||
<div class="self-center">
|
||||
<input
|
||||
class="w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="number"
|
||||
placeholder={$i18n.t('Enter Chunk Overlap')}
|
||||
bind:value={chunkOverlap}
|
||||
autocomplete="off"
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div>
|
||||
|
||||
<button
|
||||
class="hover:text-white transition"
|
||||
class=" text-xs font-medium text-gray-500"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
const res = resetVectorDB(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Success'));
|
||||
}
|
||||
|
||||
showResetConfirm = false;
|
||||
}}
|
||||
pdfExtractImages = !pdfExtractImages;
|
||||
}}>{pdfExtractImages ? $i18n.t('On') : $i18n.t('Off')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div>
|
||||
{#if showResetUploadDirConfirm}
|
||||
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM9.75 14.25a.75.75 0 0 0 0 1.5H15a.75.75 0 0 0 0-1.5H9.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{$i18n.t('Are you sure?')}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1.5 items-center">
|
||||
<button
|
||||
class="hover:text-white transition"
|
||||
on:click={() => {
|
||||
const res = resetUploadDir(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Success'));
|
||||
}
|
||||
|
||||
showResetUploadDirConfirm = false;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="hover:text-white transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showResetUploadDirConfirm = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
showResetUploadDirConfirm = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM9.75 14.25a.75.75 0 0 0 0 1.5H15a.75.75 0 0 0 0-1.5H9.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Upload Directory')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if showResetConfirm}
|
||||
<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition">
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="hover:text-white transition"
|
||||
on:click={() => {
|
||||
showResetConfirm = false;
|
||||
}}
|
||||
>
|
||||
<span>{$i18n.t('Are you sure?')}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1.5 items-center">
|
||||
<button
|
||||
class="hover:text-white transition"
|
||||
on:click={() => {
|
||||
const res = resetVectorDB(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Success'));
|
||||
}
|
||||
|
||||
showResetConfirm = false;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="hover:text-white transition"
|
||||
on:click={() => {
|
||||
showResetConfirm = false;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
showResetConfirm = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
fill-rule="evenodd"
|
||||
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
showResetConfirm = true;
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
@@ -6,61 +6,44 @@
|
||||
updateWebhookUrl
|
||||
} from '$lib/apis';
|
||||
import {
|
||||
getAdminConfig,
|
||||
getDefaultUserRole,
|
||||
getJWTExpiresDuration,
|
||||
getSignUpEnabledStatus,
|
||||
toggleSignUpEnabledStatus,
|
||||
updateAdminConfig,
|
||||
updateDefaultUserRole,
|
||||
updateJWTExpiresDuration
|
||||
} from '$lib/apis/auths';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let saveHandler: Function;
|
||||
let signUpEnabled = true;
|
||||
let defaultUserRole = 'pending';
|
||||
let JWTExpiresIn = '';
|
||||
|
||||
let adminConfig = null;
|
||||
let webhookUrl = '';
|
||||
let communitySharingEnabled = true;
|
||||
|
||||
const toggleSignUpEnabled = async () => {
|
||||
signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token);
|
||||
};
|
||||
|
||||
const updateDefaultUserRoleHandler = async (role) => {
|
||||
defaultUserRole = await updateDefaultUserRole(localStorage.token, role);
|
||||
};
|
||||
|
||||
const updateJWTExpiresDurationHandler = async (duration) => {
|
||||
JWTExpiresIn = await updateJWTExpiresDuration(localStorage.token, duration);
|
||||
};
|
||||
|
||||
const updateWebhookUrlHandler = async () => {
|
||||
const updateHandler = async () => {
|
||||
webhookUrl = await updateWebhookUrl(localStorage.token, webhookUrl);
|
||||
};
|
||||
const res = await updateAdminConfig(localStorage.token, adminConfig);
|
||||
|
||||
const toggleCommunitySharingEnabled = async () => {
|
||||
communitySharingEnabled = await toggleCommunitySharingEnabledStatus(localStorage.token);
|
||||
if (res) {
|
||||
toast.success(i18n.t('Settings updated successfully'));
|
||||
} else {
|
||||
toast.error(i18n.t('Failed to update settings'));
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
signUpEnabled = await getSignUpEnabledStatus(localStorage.token);
|
||||
})(),
|
||||
(async () => {
|
||||
defaultUserRole = await getDefaultUserRole(localStorage.token);
|
||||
})(),
|
||||
(async () => {
|
||||
JWTExpiresIn = await getJWTExpiresDuration(localStorage.token);
|
||||
adminConfig = await getAdminConfig(localStorage.token);
|
||||
})(),
|
||||
|
||||
(async () => {
|
||||
webhookUrl = await getWebhookUrl(localStorage.token);
|
||||
})(),
|
||||
(async () => {
|
||||
communitySharingEnabled = await getCommunitySharingEnabledStatus(localStorage.token);
|
||||
})()
|
||||
]);
|
||||
});
|
||||
@@ -69,156 +52,94 @@
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
updateJWTExpiresDurationHandler(JWTExpiresIn);
|
||||
updateWebhookUrlHandler();
|
||||
updateHandler();
|
||||
saveHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div>
|
||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||
{#if adminConfig !== null}
|
||||
<div>
|
||||
<div class=" mb-3 text-sm font-medium">{$i18n.t('General Settings')}</div>
|
||||
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>
|
||||
<div class=" flex w-full justify-between pr-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleSignUpEnabled();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if signUpEnabled}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
<Switch bind:state={adminConfig.ENABLE_SIGNUP} />
|
||||
</div>
|
||||
|
||||
<div class=" my-3 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={adminConfig.DEFAULT_USER_ROLE}
|
||||
placeholder="Select a role"
|
||||
>
|
||||
<path
|
||||
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="ml-2 self-center">{$i18n.t('Enabled')}</span>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
<option value="pending">{$i18n.t('pending')}</option>
|
||||
<option value="user">{$i18n.t('user')}</option>
|
||||
<option value="admin">{$i18n.t('admin')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="my-3 flex w-full items-center justify-between pr-2">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Show Admin Details in Account Pending Overlay')}
|
||||
</div>
|
||||
|
||||
<Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} />
|
||||
</div>
|
||||
|
||||
<div class="my-3 flex w-full items-center justify-between pr-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable Community Sharing')}</div>
|
||||
|
||||
<Switch bind:state={adminConfig.ENABLE_COMMUNITY_SHARING} />
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
|
||||
<div class=" w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2 space-x-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={`e.g.) "30m","1h", "10d". `}
|
||||
bind:value={adminConfig.JWT_EXPIRES_IN}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Valid time units:')}
|
||||
<span class=" text-gray-300 font-medium"
|
||||
>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="ml-2 self-center">{$i18n.t('Disabled')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={defaultUserRole}
|
||||
placeholder="Select a theme"
|
||||
on:change={(e) => {
|
||||
updateDefaultUserRoleHandler(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="pending">{$i18n.t('pending')}</option>
|
||||
<option value="user">{$i18n.t('user')}</option>
|
||||
<option value="admin">{$i18n.t('admin')}</option>
|
||||
</select>
|
||||
<div class=" w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Webhook URL')}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2 space-x-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={`https://example.com/webhook`}
|
||||
bind:value={webhookUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable Community Sharing')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleCommunitySharingEnabled();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if communitySharingEnabled}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="ml-2 self-center">{$i18n.t('Enabled')}</span>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span class="ml-2 self-center">{$i18n.t('Disabled')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700 my-3" />
|
||||
|
||||
<div class=" w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Webhook URL')}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2 space-x-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={`https://example.com/webhook`}
|
||||
bind:value={webhookUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700 my-3" />
|
||||
|
||||
<div class=" w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2 space-x-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={`e.g.) "30m","1h", "10d". `}
|
||||
bind:value={JWTExpiresIn}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Valid time units:')}
|
||||
<span class=" text-gray-300 font-medium"
|
||||
>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
|
||||
+3
-5
@@ -23,8 +23,6 @@
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let saveSettings: Function;
|
||||
|
||||
let loading = false;
|
||||
|
||||
let imageGenerationEngine = '';
|
||||
@@ -171,7 +169,7 @@
|
||||
loading = false;
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[24rem]">
|
||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden">
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
|
||||
|
||||
@@ -228,7 +226,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class=" dark:border-gray-700" />
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
{#if imageGenerationEngine === ''}
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div>
|
||||
@@ -326,7 +324,7 @@
|
||||
{/if}
|
||||
|
||||
{#if enableImageGeneration}
|
||||
<hr class=" dark:border-gray-700" />
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
|
||||
@@ -0,0 +1,339 @@
|
||||
<script lang="ts">
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { getBackendConfig, getTaskConfig, updateTaskConfig } from '$lib/apis';
|
||||
import { setDefaultPromptSuggestions } from '$lib/apis/configs';
|
||||
import { config, models, settings, user } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
|
||||
import { banners as _banners } from '$lib/stores';
|
||||
import type { Banner } from '$lib/types';
|
||||
|
||||
import { getBanners, setBanners } from '$lib/apis/configs';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let taskConfig = {
|
||||
TASK_MODEL: '',
|
||||
TASK_MODEL_EXTERNAL: '',
|
||||
TITLE_GENERATION_PROMPT_TEMPLATE: '',
|
||||
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: '',
|
||||
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD: 0
|
||||
};
|
||||
|
||||
let promptSuggestions = [];
|
||||
let banners: Banner[] = [];
|
||||
|
||||
const updateInterfaceHandler = async () => {
|
||||
taskConfig = await updateTaskConfig(localStorage.token, taskConfig);
|
||||
|
||||
promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
|
||||
await updateBanners();
|
||||
|
||||
await config.set(await getBackendConfig());
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
taskConfig = await getTaskConfig(localStorage.token);
|
||||
|
||||
promptSuggestions = $config?.default_prompt_suggestions;
|
||||
|
||||
banners = await getBanners(localStorage.token);
|
||||
});
|
||||
|
||||
const updateBanners = async () => {
|
||||
_banners.set(await setBanners(localStorage.token, banners));
|
||||
};
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
updateInterfaceHandler();
|
||||
dispatch('save');
|
||||
}}
|
||||
>
|
||||
<div class=" overflow-y-scroll scrollbar-hidden h-full pr-1.5">
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium flex">
|
||||
<div class=" mr-1">{$i18n.t('Set Task Model')}</div>
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'A task model is used when performing tasks such as generating titles for chats and web search queries'
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
|
||||
/>
|
||||
</svg>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="flex-1">
|
||||
<div class=" text-xs mb-1">{$i18n.t('Local Models')}</div>
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={taskConfig.TASK_MODEL}
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
>
|
||||
<option value="" selected>{$i18n.t('Current Model')}</option>
|
||||
{#each $models.filter((m) => m.owned_by === 'ollama') as model}
|
||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
|
||||
{model.name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<div class=" text-xs mb-1">{$i18n.t('External Models')}</div>
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={taskConfig.TASK_MODEL_EXTERNAL}
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
>
|
||||
<option value="" selected>{$i18n.t('Current Model')}</option>
|
||||
{#each $models as model}
|
||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
|
||||
{model.name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
|
||||
<textarea
|
||||
bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE}
|
||||
class="w-full rounded-lg py-3 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Search Query Generation Prompt')}</div>
|
||||
<textarea
|
||||
bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE}
|
||||
class="w-full rounded-lg py-3 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class=" mb-2.5 text-sm font-medium">
|
||||
{$i18n.t('Search Query Generation Prompt Length Threshold')}
|
||||
</div>
|
||||
<input
|
||||
bind:value={taskConfig.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD}
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850 my-3" />
|
||||
|
||||
<div class=" space-y-3 {banners.length > 0 ? ' mb-3' : ''}">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-sm font-semibold">
|
||||
{$i18n.t('Banners')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (banners.length === 0 || banners.at(-1).content !== '') {
|
||||
banners = [
|
||||
...banners,
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: '',
|
||||
title: '',
|
||||
content: '',
|
||||
dismissible: true,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
];
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-1">
|
||||
{#each banners as banner, bannerIdx}
|
||||
<div class=" flex justify-between">
|
||||
<div class="flex flex-row flex-1 border rounded-xl dark:border-gray-800">
|
||||
<select
|
||||
class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-none"
|
||||
bind:value={banner.type}
|
||||
required
|
||||
>
|
||||
{#if banner.type == ''}
|
||||
<option value="" selected disabled class="text-gray-900">{$i18n.t('Type')}</option
|
||||
>
|
||||
{/if}
|
||||
<option value="info" class="text-gray-900">{$i18n.t('Info')}</option>
|
||||
<option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option>
|
||||
<option value="error" class="text-gray-900">{$i18n.t('Error')}</option>
|
||||
<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
class="pr-5 py-1.5 text-xs w-full bg-transparent outline-none"
|
||||
placeholder={$i18n.t('Content')}
|
||||
bind:value={banner.content}
|
||||
/>
|
||||
|
||||
<div class="relative top-1.5 -left-2">
|
||||
<Tooltip content={$i18n.t('Dismissible')} className="flex h-fit items-center">
|
||||
<Switch bind:state={banner.dismissible} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="px-2"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
banners.splice(bannerIdx, 1);
|
||||
banners = banners;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $user.role === 'admin'}
|
||||
<div class=" space-y-3">
|
||||
<div class="flex w-full justify-between mb-2">
|
||||
<div class=" self-center text-sm font-semibold">
|
||||
{$i18n.t('Default Prompt Suggestions')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
|
||||
promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid lg:grid-cols-2 flex-col gap-1.5">
|
||||
{#each promptSuggestions as prompt, promptIdx}
|
||||
<div class=" flex dark:bg-gray-850 rounded-xl py-1.5">
|
||||
<div class="flex flex-col flex-1 pl-1">
|
||||
<div class="flex border-b dark:border-gray-800 w-full">
|
||||
<input
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-800"
|
||||
placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
|
||||
bind:value={prompt.title[0]}
|
||||
/>
|
||||
|
||||
<input
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-800"
|
||||
placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
|
||||
bind:value={prompt.title[1]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-800"
|
||||
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
|
||||
bind:value={prompt.content}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="px-3"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
promptSuggestions.splice(promptIdx, 1);
|
||||
promptSuggestions = promptSuggestions;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if promptSuggestions.length > 0}
|
||||
<div class="text-xs text-left w-full mt-2">
|
||||
{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,8 @@
|
||||
getModels,
|
||||
getPipelinesList,
|
||||
downloadPipeline,
|
||||
deletePipeline
|
||||
deletePipeline,
|
||||
uploadPipeline
|
||||
} from '$lib/apis';
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
@@ -24,6 +25,9 @@
|
||||
export let saveHandler: Function;
|
||||
|
||||
let downloading = false;
|
||||
let uploading = false;
|
||||
|
||||
let pipelineFiles;
|
||||
|
||||
let PIPELINES_LIST = null;
|
||||
let selectedPipelinesUrlIdx = '';
|
||||
@@ -126,6 +130,41 @@
|
||||
downloading = false;
|
||||
};
|
||||
|
||||
const uploadPipelineHandler = async () => {
|
||||
uploading = true;
|
||||
|
||||
if (pipelineFiles && pipelineFiles.length !== 0) {
|
||||
const file = pipelineFiles[0];
|
||||
|
||||
console.log(file);
|
||||
|
||||
const res = await uploadPipeline(localStorage.token, file, selectedPipelinesUrlIdx).catch(
|
||||
(error) => {
|
||||
console.log(error);
|
||||
toast.error('Something went wrong :/');
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
toast.success('Pipeline downloaded successfully');
|
||||
setPipelines();
|
||||
models.set(await getModels(localStorage.token));
|
||||
}
|
||||
} else {
|
||||
toast.error('No file selected');
|
||||
}
|
||||
|
||||
pipelineFiles = null;
|
||||
const pipelineUploadInputElement = document.getElementById('pipeline-upload-input');
|
||||
|
||||
if (pipelineUploadInputElement) {
|
||||
pipelineUploadInputElement.value = null;
|
||||
}
|
||||
|
||||
uploading = false;
|
||||
};
|
||||
|
||||
const deletePipelineHandler = async () => {
|
||||
const res = await deletePipeline(
|
||||
localStorage.token,
|
||||
@@ -161,7 +200,7 @@
|
||||
updateHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" pr-1.5 overflow-y-scroll max-h-80 h-full">
|
||||
<div class="overflow-y-scroll scrollbar-hidden h-full">
|
||||
{#if PIPELINES_LIST !== null}
|
||||
<div class="flex w-full justify-between mb-2">
|
||||
<div class=" self-center text-sm font-semibold">
|
||||
@@ -196,6 +235,91 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" my-2">
|
||||
<div class=" mb-2 text-sm font-medium">
|
||||
{$i18n.t('Upload Pipeline')}
|
||||
</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
id="pipelines-upload-input"
|
||||
bind:files={pipelineFiles}
|
||||
type="file"
|
||||
accept=".py"
|
||||
hidden
|
||||
/>
|
||||
|
||||
<button
|
||||
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
document.getElementById('pipelines-upload-input')?.click();
|
||||
}}
|
||||
>
|
||||
{#if pipelineFiles}
|
||||
{pipelineFiles.length > 0 ? `${pipelineFiles.length}` : ''} pipeline(s) selected.
|
||||
{:else}
|
||||
{$i18n.t('Click here to select a py file.')}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||
on:click={() => {
|
||||
uploadPipelineHandler();
|
||||
}}
|
||||
disabled={uploading}
|
||||
type="button"
|
||||
>
|
||||
{#if uploading}
|
||||
<div class="self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/>
|
||||
<path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" my-2">
|
||||
<div class=" mb-2 text-sm font-medium">
|
||||
{$i18n.t('Install from Github URL')}
|
||||
@@ -384,6 +508,8 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div>Pipelines Not Detected</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex justify-center h-full">
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
await config.set(await getBackendConfig());
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||
<div class=" space-y-3 overflow-y-scroll max-h-full">
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('User Permissions')}</div>
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700 my-2" />
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="mt-2 space-y-3">
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
<script lang="ts">
|
||||
import { getRAGConfig, updateRAGConfig } from '$lib/apis/rag';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
|
||||
import { documents, models } from '$lib/stores';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let saveHandler: Function;
|
||||
|
||||
let webConfig = null;
|
||||
let webSearchEngines = ['searxng', 'google_pse', 'brave', 'serpstack', 'serper'];
|
||||
|
||||
let youtubeLanguage = 'en';
|
||||
let youtubeTranslation = null;
|
||||
|
||||
const submitHandler = async () => {
|
||||
const res = await updateRAGConfig(localStorage.token, {
|
||||
web: webConfig,
|
||||
youtube: {
|
||||
language: youtubeLanguage.split(',').map((lang) => lang.trim()),
|
||||
translation: youtubeTranslation
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
const res = await getRAGConfig(localStorage.token);
|
||||
|
||||
if (res) {
|
||||
webConfig = res.web;
|
||||
|
||||
youtubeLanguage = res.youtube.language.join(',');
|
||||
youtubeTranslation = res.youtube.translation;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={async () => {
|
||||
await submitHandler();
|
||||
saveHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||
{#if webConfig}
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">
|
||||
{$i18n.t('Web Search')}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Enable Web Search')}
|
||||
</div>
|
||||
|
||||
<Switch bind:state={webConfig.search.enabled} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Web Search Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={webConfig.search.engine}
|
||||
placeholder={$i18n.t('Select a engine')}
|
||||
required
|
||||
>
|
||||
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
|
||||
{#each webSearchEngines as engine}
|
||||
<option value={engine}>{engine}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if webConfig.search.engine !== ''}
|
||||
<div class="mt-1.5">
|
||||
{#if webConfig.search.engine === 'searxng'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Searxng Query URL')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Searxng Query URL')}
|
||||
bind:value={webConfig.search.searxng_query_url}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'google_pse'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Google PSE API Key')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Google PSE API Key')}
|
||||
bind:value={webConfig.search.google_pse_api_key}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1.5">
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Google PSE Engine Id')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Google PSE Engine Id')}
|
||||
bind:value={webConfig.search.google_pse_engine_id}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'brave'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Brave Search API Key')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Brave Search API Key')}
|
||||
bind:value={webConfig.search.brave_search_api_key}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'serpstack'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Serpstack API Key')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Serpstack API Key')}
|
||||
bind:value={webConfig.search.serpstack_api_key}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.search.engine === 'serper'}
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Serper API Key')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter Serper API Key')}
|
||||
bind:value={webConfig.search.serper_api_key}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if webConfig.search.enabled}
|
||||
<div class="mt-2 flex gap-2 mb-1">
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Search Result Count')}
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Search Result Count')}
|
||||
bind:value={webConfig.search.result_count}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Concurrent Requests')}
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Concurrent Requests')}
|
||||
bind:value={webConfig.search.concurrent_requests}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">
|
||||
{$i18n.t('Web Loader Settings')}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Bypass SSL verification for Websites')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
webConfig.ssl_verification = !webConfig.ssl_verification;
|
||||
submitHandler();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if webConfig.ssl_verification === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" mt-2 mb-1 text-sm font-medium">
|
||||
{$i18n.t('Youtube Loader Settings')}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
|
||||
<div class=" flex-1 self-center">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
type="text"
|
||||
placeholder={$i18n.t('Enter language codes')}
|
||||
bind:value={youtubeLanguage}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -39,181 +39,5 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
|
||||
<div
|
||||
class="tabs flex flex-row overflow-x-auto space-x-1 md:space-x-0 md:space-y-1 md:flex-col flex-1 md:flex-none md:w-40 dark:text-gray-200 text-xs text-left mb-3 md:mb-0"
|
||||
>
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'general'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'general';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('General')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'users'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'users';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M8 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM3.156 11.763c.16-.629.44-1.21.813-1.72a2.5 2.5 0 0 0-2.725 1.377c-.136.287.102.58.418.58h1.449c.01-.077.025-.156.045-.237ZM12.847 11.763c.02.08.036.16.046.237h1.446c.316 0 .554-.293.417-.579a2.5 2.5 0 0 0-2.722-1.378c.374.51.653 1.09.813 1.72ZM14 7.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM3.5 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM5 13c-.552 0-1.013-.455-.876-.99a4.002 4.002 0 0 1 7.753 0c.136.535-.324.99-.877.99H5Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Users')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'db'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'db';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M8 7c3.314 0 6-1.343 6-3s-2.686-3-6-3-6 1.343-6 3 2.686 3 6 3Z" />
|
||||
<path
|
||||
d="M8 8.5c1.84 0 3.579-.37 4.914-1.037A6.33 6.33 0 0 0 14 6.78V8c0 1.657-2.686 3-6 3S2 9.657 2 8V6.78c.346.273.72.5 1.087.683C4.42 8.131 6.16 8.5 8 8.5Z"
|
||||
/>
|
||||
<path
|
||||
d="M8 12.5c1.84 0 3.579-.37 4.914-1.037.366-.183.74-.41 1.086-.684V12c0 1.657-2.686 3-6 3s-6-1.343-6-3v-1.22c.346.273.72.5 1.087.683C4.42 12.131 6.16 12.5 8 12.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Database')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'banners'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'banners';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
d="M5.85 3.5a.75.75 0 0 0-1.117-1 9.719 9.719 0 0 0-2.348 4.876.75.75 0 0 0 1.479.248A8.219 8.219 0 0 1 5.85 3.5ZM19.267 2.5a.75.75 0 1 0-1.118 1 8.22 8.22 0 0 1 1.987 4.124.75.75 0 0 0 1.48-.248A9.72 9.72 0 0 0 19.266 2.5Z"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12 2.25A6.75 6.75 0 0 0 5.25 9v.75a8.217 8.217 0 0 1-2.119 5.52.75.75 0 0 0 .298 1.206c1.544.57 3.16.99 4.831 1.243a3.75 3.75 0 1 0 7.48 0 24.583 24.583 0 0 0 4.83-1.244.75.75 0 0 0 .298-1.205 8.217 8.217 0 0 1-2.118-5.52V9A6.75 6.75 0 0 0 12 2.25ZM9.75 18c0-.034 0-.067.002-.1a25.05 25.05 0 0 0 4.496 0l.002.1a2.25 2.25 0 1 1-4.5 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Banners')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'pipelines'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'pipelines';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
d="M11.644 1.59a.75.75 0 0 1 .712 0l9.75 5.25a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.712 0l-9.75-5.25a.75.75 0 0 1 0-1.32l9.75-5.25Z"
|
||||
/>
|
||||
<path
|
||||
d="m3.265 10.602 7.668 4.129a2.25 2.25 0 0 0 2.134 0l7.668-4.13 1.37.739a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.71 0l-9.75-5.25a.75.75 0 0 1 0-1.32l1.37-.738Z"
|
||||
/>
|
||||
<path
|
||||
d="m10.933 19.231-7.668-4.13-1.37.739a.75.75 0 0 0 0 1.32l9.75 5.25c.221.12.489.12.71 0l9.75-5.25a.75.75 0 0 0 0-1.32l-1.37-.738-7.668 4.13a2.25 2.25 0 0 1-2.134-.001Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Pipelines')}</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 md:min-h-[380px]">
|
||||
{#if selectedTab === 'general'}
|
||||
<General
|
||||
saveHandler={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'users'}
|
||||
<Users
|
||||
saveHandler={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'db'}
|
||||
<Database
|
||||
saveHandler={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'banners'}
|
||||
<Banners
|
||||
saveHandler={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'pipelines'}
|
||||
<Pipelines
|
||||
saveHandler={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
+136
-128
@@ -7,6 +7,10 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
import { OLLAMA_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
import {
|
||||
chatId,
|
||||
chats,
|
||||
@@ -18,7 +22,9 @@
|
||||
tags as _tags,
|
||||
WEBUI_NAME,
|
||||
banners,
|
||||
user
|
||||
user,
|
||||
socket,
|
||||
showCallOverlay
|
||||
} from '$lib/stores';
|
||||
import {
|
||||
convertMessagesToHistory,
|
||||
@@ -38,24 +44,19 @@
|
||||
getTagsById,
|
||||
updateChatById
|
||||
} from '$lib/apis/chats';
|
||||
import {
|
||||
generateOpenAIChatCompletion,
|
||||
generateSearchQuery,
|
||||
generateTitle
|
||||
} from '$lib/apis/openai';
|
||||
import { generateOpenAIChatCompletion } from '$lib/apis/openai';
|
||||
import { runWebSearch } from '$lib/apis/rag';
|
||||
import { createOpenAITextStream } from '$lib/apis/streaming';
|
||||
import { queryMemory } from '$lib/apis/memories';
|
||||
import { getUserSettings } from '$lib/apis/users';
|
||||
import { chatCompleted, generateTitle, generateSearchQuery } from '$lib/apis';
|
||||
|
||||
import Banner from '../common/Banner.svelte';
|
||||
import MessageInput from '$lib/components/chat/MessageInput.svelte';
|
||||
import Messages from '$lib/components/chat/Messages.svelte';
|
||||
import Navbar from '$lib/components/layout/Navbar.svelte';
|
||||
import { OLLAMA_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { createOpenAITextStream } from '$lib/apis/streaming';
|
||||
import { queryMemory } from '$lib/apis/memories';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
import { runWebSearch } from '$lib/apis/rag';
|
||||
import Banner from '../common/Banner.svelte';
|
||||
import { getUserSettings } from '$lib/apis/users';
|
||||
import { chatCompleted } from '$lib/apis';
|
||||
import CallOverlay from './MessageInput/CallOverlay.svelte';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
const i18n: Writable<i18nType> = getContext('i18n');
|
||||
|
||||
@@ -247,13 +248,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
const chatCompletedHandler = async (model, messages) => {
|
||||
const chatCompletedHandler = async (modelId, messages) => {
|
||||
await mermaid.run({
|
||||
querySelector: '.mermaid'
|
||||
});
|
||||
|
||||
const res = await chatCompleted(localStorage.token, {
|
||||
model: model.id,
|
||||
model: modelId,
|
||||
messages: messages.map((m) => ({
|
||||
id: m.id,
|
||||
role: m.role,
|
||||
@@ -280,11 +281,22 @@
|
||||
}
|
||||
};
|
||||
|
||||
const getChatEventEmitter = async (modelId: string, chatId: string = '') => {
|
||||
return setInterval(() => {
|
||||
$socket?.emit('usage', {
|
||||
action: 'chat',
|
||||
model: modelId,
|
||||
chat_id: chatId
|
||||
});
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
//////////////////////////
|
||||
// Ollama functions
|
||||
// Chat functions
|
||||
//////////////////////////
|
||||
|
||||
const submitPrompt = async (userPrompt, _user = null) => {
|
||||
let _responses = [];
|
||||
console.log('submitPrompt', $chatId);
|
||||
|
||||
selectedModels = selectedModels.map((modelId) =>
|
||||
@@ -307,8 +319,15 @@
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Reset chat message textarea height
|
||||
document.getElementById('chat-textarea').style.height = '';
|
||||
// Reset chat input textarea
|
||||
const chatTextAreaElement = document.getElementById('chat-textarea');
|
||||
|
||||
if (chatTextAreaElement) {
|
||||
chatTextAreaElement.value = '';
|
||||
chatTextAreaElement.style.height = '';
|
||||
}
|
||||
|
||||
prompt = '';
|
||||
|
||||
// Create user message
|
||||
let userMessageId = uuidv4();
|
||||
@@ -360,17 +379,17 @@
|
||||
await tick();
|
||||
}
|
||||
|
||||
// Reset chat input textarea
|
||||
prompt = '';
|
||||
document.getElementById('chat-textarea').style.height = '';
|
||||
files = [];
|
||||
|
||||
// Send prompt
|
||||
await sendPrompt(userPrompt, userMessageId);
|
||||
_responses = await sendPrompt(userPrompt, userMessageId);
|
||||
}
|
||||
|
||||
return _responses;
|
||||
};
|
||||
|
||||
const sendPrompt = async (prompt, parentId, modelId = null) => {
|
||||
let _responses = [];
|
||||
const _chatId = JSON.parse(JSON.stringify($chatId));
|
||||
|
||||
await Promise.all(
|
||||
@@ -451,15 +470,24 @@
|
||||
}
|
||||
responseMessage.userContext = userContext;
|
||||
|
||||
const chatEventEmitter = await getChatEventEmitter(model.id, _chatId);
|
||||
|
||||
if (webSearchEnabled) {
|
||||
await getWebSearchResults(model.id, parentId, responseMessageId);
|
||||
}
|
||||
|
||||
let _response = null;
|
||||
|
||||
if (model?.owned_by === 'openai') {
|
||||
await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
|
||||
_response = await sendPromptOpenAI(model, prompt, responseMessageId, _chatId);
|
||||
} else if (model) {
|
||||
await sendPromptOllama(model, prompt, responseMessageId, _chatId);
|
||||
_response = await sendPromptOllama(model, prompt, responseMessageId, _chatId);
|
||||
}
|
||||
_responses.push(_response);
|
||||
|
||||
console.log('chatEventEmitter', chatEventEmitter);
|
||||
|
||||
if (chatEventEmitter) clearInterval(chatEventEmitter);
|
||||
} else {
|
||||
toast.error($i18n.t(`Model {{modelId}} not found`, { modelId }));
|
||||
}
|
||||
@@ -467,6 +495,8 @@
|
||||
);
|
||||
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
return _responses;
|
||||
};
|
||||
|
||||
const getWebSearchResults = async (model: string, parentId: string, responseId: string) => {
|
||||
@@ -480,20 +510,22 @@
|
||||
messages = messages;
|
||||
|
||||
const prompt = history.messages[parentId].content;
|
||||
let searchQuery = prompt;
|
||||
if (prompt.length > 100) {
|
||||
searchQuery = await generateChatSearchQuery(model, prompt);
|
||||
if (!searchQuery) {
|
||||
toast.warning($i18n.t('No search query generated'));
|
||||
responseMessage.status = {
|
||||
...responseMessage.status,
|
||||
done: true,
|
||||
error: true,
|
||||
description: 'No search query generated'
|
||||
};
|
||||
messages = messages;
|
||||
return;
|
||||
let searchQuery = await generateSearchQuery(localStorage.token, model, messages, prompt).catch(
|
||||
(error) => {
|
||||
console.log(error);
|
||||
return prompt;
|
||||
}
|
||||
);
|
||||
|
||||
if (!searchQuery) {
|
||||
toast.warning($i18n.t('No search query generated'));
|
||||
responseMessage.status = {
|
||||
...responseMessage.status,
|
||||
done: true,
|
||||
error: true,
|
||||
description: 'No search query generated'
|
||||
};
|
||||
messages = messages;
|
||||
}
|
||||
|
||||
responseMessage.status = {
|
||||
@@ -541,7 +573,8 @@
|
||||
};
|
||||
|
||||
const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
|
||||
model = model.id;
|
||||
let _response = null;
|
||||
|
||||
const responseMessage = history.messages[responseMessageId];
|
||||
|
||||
// Wait until history/message have been updated
|
||||
@@ -599,17 +632,29 @@
|
||||
}
|
||||
});
|
||||
|
||||
const docs = messages
|
||||
.filter((message) => message?.files ?? null)
|
||||
.map((message) =>
|
||||
message.files.filter((item) =>
|
||||
['doc', 'collection', 'web_search_results'].includes(item.type)
|
||||
let docs = [];
|
||||
|
||||
if (model.info.meta.knowledge) {
|
||||
docs = model.info.meta.knowledge;
|
||||
}
|
||||
|
||||
docs = [
|
||||
...docs,
|
||||
...messages
|
||||
.filter((message) => message?.files ?? null)
|
||||
.map((message) =>
|
||||
message.files.filter((item) =>
|
||||
['doc', 'collection', 'web_search_results'].includes(item.type)
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat(1);
|
||||
.flat(1)
|
||||
].filter(
|
||||
(item, index, array) =>
|
||||
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
|
||||
);
|
||||
|
||||
const [res, controller] = await generateChatCompletion(localStorage.token, {
|
||||
model: model,
|
||||
model: model.id,
|
||||
messages: messagesBody,
|
||||
options: {
|
||||
...($settings.params ?? {}),
|
||||
@@ -647,9 +692,10 @@
|
||||
controller.abort('User: Stop Response');
|
||||
} else {
|
||||
const messages = createMessagesList(responseMessageId);
|
||||
await chatCompletedHandler(model, messages);
|
||||
await chatCompletedHandler(model.id, messages);
|
||||
}
|
||||
|
||||
_response = responseMessage.content;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -707,7 +753,7 @@
|
||||
selectedModelfile.title.charAt(0).toUpperCase() +
|
||||
selectedModelfile.title.slice(1)
|
||||
}`
|
||||
: `${model}`,
|
||||
: `${model.id}`,
|
||||
{
|
||||
body: responseMessage.content,
|
||||
icon: selectedModelfile?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`
|
||||
@@ -719,7 +765,7 @@
|
||||
copyToClipboard(responseMessage.content);
|
||||
}
|
||||
|
||||
if ($settings.responseAutoPlayback) {
|
||||
if ($settings.responseAutoPlayback && !$showCallOverlay) {
|
||||
await tick();
|
||||
document.getElementById(`speak-button-${responseMessage.id}`)?.click();
|
||||
}
|
||||
@@ -786,21 +832,34 @@
|
||||
const _title = await generateChatTitle(userPrompt);
|
||||
await setChatTitle(_chatId, _title);
|
||||
}
|
||||
|
||||
return _response;
|
||||
};
|
||||
|
||||
const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => {
|
||||
let _response = null;
|
||||
const responseMessage = history.messages[responseMessageId];
|
||||
|
||||
const docs = messages
|
||||
.filter((message) => message?.files ?? null)
|
||||
.map((message) =>
|
||||
message.files.filter((item) =>
|
||||
['doc', 'collection', 'web_search_results'].includes(item.type)
|
||||
)
|
||||
)
|
||||
.flat(1);
|
||||
let docs = [];
|
||||
|
||||
console.log(docs);
|
||||
if (model.info.meta.knowledge) {
|
||||
docs = model.info.meta.knowledge;
|
||||
}
|
||||
|
||||
docs = [
|
||||
...docs,
|
||||
...messages
|
||||
.filter((message) => message?.files ?? null)
|
||||
.map((message) =>
|
||||
message.files.filter((item) =>
|
||||
['doc', 'collection', 'web_search_results'].includes(item.type)
|
||||
)
|
||||
)
|
||||
.flat(1)
|
||||
].filter(
|
||||
(item, index, array) =>
|
||||
array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index
|
||||
);
|
||||
|
||||
scrollToBottom();
|
||||
|
||||
@@ -902,9 +961,11 @@
|
||||
} else {
|
||||
const messages = createMessagesList(responseMessageId);
|
||||
|
||||
await chatCompletedHandler(model, messages);
|
||||
await chatCompletedHandler(model.id, messages);
|
||||
}
|
||||
|
||||
_response = responseMessage.content;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -930,7 +991,7 @@
|
||||
}
|
||||
|
||||
if ($settings.notificationEnabled && !document.hasFocus()) {
|
||||
const notification = new Notification(`OpenAI ${model}`, {
|
||||
const notification = new Notification(`${model.id}`, {
|
||||
body: responseMessage.content,
|
||||
icon: `${WEBUI_BASE_URL}/static/favicon.png`
|
||||
});
|
||||
@@ -940,8 +1001,9 @@
|
||||
copyToClipboard(responseMessage.content);
|
||||
}
|
||||
|
||||
if ($settings.responseAutoPlayback) {
|
||||
if ($settings.responseAutoPlayback && !$showCallOverlay) {
|
||||
await tick();
|
||||
|
||||
document.getElementById(`speak-button-${responseMessage.id}`)?.click();
|
||||
}
|
||||
|
||||
@@ -980,6 +1042,8 @@
|
||||
const _title = await generateChatTitle(userPrompt);
|
||||
await setChatTitle(_chatId, _title);
|
||||
}
|
||||
|
||||
return _response;
|
||||
};
|
||||
|
||||
const handleOpenAIError = async (error, res: Response | null, model, responseMessage) => {
|
||||
@@ -1075,28 +1139,15 @@
|
||||
|
||||
const generateChatTitle = async (userPrompt) => {
|
||||
if ($settings?.title?.auto ?? true) {
|
||||
const model = $models.find((model) => model.id === selectedModels[0]);
|
||||
|
||||
const titleModelId =
|
||||
model?.owned_by === 'openai' ?? false
|
||||
? $settings?.title?.modelExternal ?? selectedModels[0]
|
||||
: $settings?.title?.model ?? selectedModels[0];
|
||||
const titleModel = $models.find((model) => model.id === titleModelId);
|
||||
|
||||
console.log(titleModel);
|
||||
const title = await generateTitle(
|
||||
localStorage.token,
|
||||
$settings?.title?.prompt ??
|
||||
$i18n.t(
|
||||
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':"
|
||||
) + ' {{prompt}}',
|
||||
titleModelId,
|
||||
selectedModels[0],
|
||||
userPrompt,
|
||||
$chatId,
|
||||
titleModel?.owned_by === 'openai' ?? false
|
||||
? `${OPENAI_API_BASE_URL}`
|
||||
: `${OLLAMA_API_BASE_URL}/v1`
|
||||
);
|
||||
$chatId
|
||||
).catch((error) => {
|
||||
console.error(error);
|
||||
return 'New Chat';
|
||||
});
|
||||
|
||||
return title;
|
||||
} else {
|
||||
@@ -1104,29 +1155,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const generateChatSearchQuery = async (modelId: string, prompt: string) => {
|
||||
const model = $models.find((model) => model.id === modelId);
|
||||
const taskModelId =
|
||||
model?.owned_by === 'openai' ?? false
|
||||
? $settings?.title?.modelExternal ?? modelId
|
||||
: $settings?.title?.model ?? modelId;
|
||||
const taskModel = $models.find((model) => model.id === taskModelId);
|
||||
|
||||
const previousMessages = messages
|
||||
.filter((message) => message.role === 'user')
|
||||
.map((message) => message.content);
|
||||
|
||||
return await generateSearchQuery(
|
||||
localStorage.token,
|
||||
taskModelId,
|
||||
previousMessages,
|
||||
prompt,
|
||||
taskModel?.owned_by === 'openai' ?? false
|
||||
? `${OPENAI_API_BASE_URL}`
|
||||
: `${OLLAMA_API_BASE_URL}/v1`
|
||||
);
|
||||
};
|
||||
|
||||
const setChatTitle = async (_chatId, _title) => {
|
||||
if (_chatId === $chatId) {
|
||||
title = _title;
|
||||
@@ -1143,28 +1171,6 @@
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
||||
const addTag = async (tagName) => {
|
||||
const res = await addTagById(localStorage.token, $chatId, tagName);
|
||||
tags = await getTags();
|
||||
|
||||
chat = await updateChatById(localStorage.token, $chatId, {
|
||||
tags: tags
|
||||
});
|
||||
|
||||
_tags.set(await getAllChatTags(localStorage.token));
|
||||
};
|
||||
|
||||
const deleteTag = async (tagName) => {
|
||||
const res = await deleteTagById(localStorage.token, $chatId, tagName);
|
||||
tags = await getTags();
|
||||
|
||||
chat = await updateChatById(localStorage.token, $chatId, {
|
||||
tags: tags
|
||||
});
|
||||
|
||||
_tags.set(await getAllChatTags(localStorage.token));
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -1175,9 +1181,11 @@
|
||||
</title>
|
||||
</svelte:head>
|
||||
|
||||
<CallOverlay {submitPrompt} bind:files />
|
||||
|
||||
{#if !chatIdProp || (loaded && chatIdProp)}
|
||||
<div
|
||||
class="min-h-screen max-h-screen {$showSidebar
|
||||
class="h-screen max-h-[100dvh] {$showSidebar
|
||||
? 'md:max-w-[calc(100%-260px)]'
|
||||
: ''} w-full max-w-full flex flex-col"
|
||||
>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,693 @@
|
||||
<script lang="ts">
|
||||
import { config, settings, showCallOverlay } from '$lib/stores';
|
||||
import { onMount, tick, getContext } from 'svelte';
|
||||
|
||||
import { blobToFile, calculateSHA256, extractSentences, findWordIndices } from '$lib/utils';
|
||||
import { synthesizeOpenAISpeech, transcribeAudio } from '$lib/apis/audio';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import VideoInputMenu from './CallOverlay/VideoInputMenu.svelte';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let submitPrompt: Function;
|
||||
export let files;
|
||||
|
||||
let loading = false;
|
||||
let confirmed = false;
|
||||
|
||||
let camera = false;
|
||||
let cameraStream = null;
|
||||
|
||||
let assistantSpeaking = false;
|
||||
let assistantAudio = {};
|
||||
let assistantAudioIdx = null;
|
||||
|
||||
let rmsLevel = 0;
|
||||
let hasStartedSpeaking = false;
|
||||
|
||||
let currentUtterance = null;
|
||||
|
||||
let mediaRecorder;
|
||||
let audioChunks = [];
|
||||
|
||||
const MIN_DECIBELS = -45;
|
||||
const VISUALIZER_BUFFER_LENGTH = 300;
|
||||
|
||||
// Function to calculate the RMS level from time domain data
|
||||
const calculateRMS = (data: Uint8Array) => {
|
||||
let sumSquares = 0;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const normalizedValue = (data[i] - 128) / 128; // Normalize the data
|
||||
sumSquares += normalizedValue * normalizedValue;
|
||||
}
|
||||
return Math.sqrt(sumSquares / data.length);
|
||||
};
|
||||
|
||||
const normalizeRMS = (rms) => {
|
||||
rms = rms * 10;
|
||||
const exp = 1.5; // Adjust exponent value; values greater than 1 expand larger numbers more and compress smaller numbers more
|
||||
const scaledRMS = Math.pow(rms, exp);
|
||||
|
||||
// Scale between 0.01 (1%) and 1.0 (100%)
|
||||
return Math.min(1.0, Math.max(0.01, scaledRMS));
|
||||
};
|
||||
|
||||
const analyseAudio = (stream) => {
|
||||
const audioContext = new AudioContext();
|
||||
const audioStreamSource = audioContext.createMediaStreamSource(stream);
|
||||
|
||||
const analyser = audioContext.createAnalyser();
|
||||
analyser.minDecibels = MIN_DECIBELS;
|
||||
audioStreamSource.connect(analyser);
|
||||
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
|
||||
const domainData = new Uint8Array(bufferLength);
|
||||
const timeDomainData = new Uint8Array(analyser.fftSize);
|
||||
|
||||
let lastSoundTime = Date.now();
|
||||
hasStartedSpeaking = false;
|
||||
|
||||
const detectSound = () => {
|
||||
const processFrame = () => {
|
||||
if (!mediaRecorder || !$showCallOverlay) {
|
||||
if (mediaRecorder) {
|
||||
mediaRecorder.stop();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
analyser.getByteTimeDomainData(timeDomainData);
|
||||
analyser.getByteFrequencyData(domainData);
|
||||
|
||||
// Calculate RMS level from time domain data
|
||||
rmsLevel = calculateRMS(timeDomainData);
|
||||
|
||||
// Check if initial speech/noise has started
|
||||
const hasSound = domainData.some((value) => value > 0);
|
||||
if (hasSound) {
|
||||
stopAllAudio();
|
||||
hasStartedSpeaking = true;
|
||||
lastSoundTime = Date.now();
|
||||
}
|
||||
|
||||
// Start silence detection only after initial speech/noise has been detected
|
||||
if (hasStartedSpeaking) {
|
||||
if (Date.now() - lastSoundTime > 2000) {
|
||||
confirmed = true;
|
||||
|
||||
if (mediaRecorder) {
|
||||
mediaRecorder.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(processFrame);
|
||||
};
|
||||
|
||||
window.requestAnimationFrame(processFrame);
|
||||
};
|
||||
|
||||
detectSound();
|
||||
};
|
||||
|
||||
const stopAllAudio = () => {
|
||||
if (currentUtterance) {
|
||||
speechSynthesis.cancel();
|
||||
currentUtterance = null;
|
||||
}
|
||||
if (assistantAudio[assistantAudioIdx]) {
|
||||
assistantAudio[assistantAudioIdx].pause();
|
||||
assistantAudio[assistantAudioIdx].currentTime = 0;
|
||||
}
|
||||
|
||||
const audioElement = document.getElementById('audioElement');
|
||||
audioElement.pause();
|
||||
audioElement.currentTime = 0;
|
||||
|
||||
assistantSpeaking = false;
|
||||
};
|
||||
|
||||
const playAudio = (idx) => {
|
||||
if ($showCallOverlay) {
|
||||
return new Promise((res) => {
|
||||
assistantAudioIdx = idx;
|
||||
const audioElement = document.getElementById('audioElement');
|
||||
const audio = assistantAudio[idx];
|
||||
|
||||
audioElement.src = audio.src; // Assume `assistantAudio` has objects with a `src` property
|
||||
|
||||
audioElement.muted = true;
|
||||
|
||||
audioElement
|
||||
.play()
|
||||
.then(() => {
|
||||
audioElement.muted = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error);
|
||||
});
|
||||
|
||||
audioElement.onended = async (e) => {
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
|
||||
if (Object.keys(assistantAudio).length - 1 === idx) {
|
||||
assistantSpeaking = false;
|
||||
}
|
||||
|
||||
res(e);
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
const getOpenAISpeech = async (text) => {
|
||||
const res = await synthesizeOpenAISpeech(
|
||||
localStorage.token,
|
||||
$settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice,
|
||||
text
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
assistantSpeaking = false;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
const blob = await res.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const audio = new Audio(blobUrl);
|
||||
assistantAudio = audio;
|
||||
}
|
||||
};
|
||||
|
||||
const transcribeHandler = async (audioBlob) => {
|
||||
// Create a blob from the audio chunks
|
||||
|
||||
await tick();
|
||||
const file = blobToFile(audioBlob, 'recording.wav');
|
||||
|
||||
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
console.log(res.text);
|
||||
|
||||
if (res.text !== '') {
|
||||
const _responses = await submitPrompt(res.text);
|
||||
console.log(_responses);
|
||||
|
||||
if (_responses.at(0)) {
|
||||
const content = _responses[0];
|
||||
if (content) {
|
||||
assistantSpeakingHandler(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const assistantSpeakingHandler = async (content) => {
|
||||
assistantSpeaking = true;
|
||||
|
||||
if (($config.audio.tts.engine ?? '') == '') {
|
||||
let voices = [];
|
||||
const getVoicesLoop = setInterval(async () => {
|
||||
voices = await speechSynthesis.getVoices();
|
||||
if (voices.length > 0) {
|
||||
clearInterval(getVoicesLoop);
|
||||
|
||||
const voice =
|
||||
voices
|
||||
?.filter(
|
||||
(v) => v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
|
||||
)
|
||||
?.at(0) ?? undefined;
|
||||
|
||||
currentUtterance = new SpeechSynthesisUtterance(content);
|
||||
|
||||
if (voice) {
|
||||
currentUtterance.voice = voice;
|
||||
}
|
||||
|
||||
speechSynthesis.speak(currentUtterance);
|
||||
}
|
||||
}, 100);
|
||||
} else if ($config.audio.tts.engine === 'openai') {
|
||||
console.log('openai');
|
||||
|
||||
const sentences = extractSentences(content).reduce((mergedTexts, currentText) => {
|
||||
const lastIndex = mergedTexts.length - 1;
|
||||
if (lastIndex >= 0) {
|
||||
const previousText = mergedTexts[lastIndex];
|
||||
const wordCount = previousText.split(/\s+/).length;
|
||||
if (wordCount < 2) {
|
||||
mergedTexts[lastIndex] = previousText + ' ' + currentText;
|
||||
} else {
|
||||
mergedTexts.push(currentText);
|
||||
}
|
||||
} else {
|
||||
mergedTexts.push(currentText);
|
||||
}
|
||||
return mergedTexts;
|
||||
}, []);
|
||||
|
||||
console.log(sentences);
|
||||
|
||||
let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately
|
||||
|
||||
for (const [idx, sentence] of sentences.entries()) {
|
||||
const res = await synthesizeOpenAISpeech(
|
||||
localStorage.token,
|
||||
$settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice,
|
||||
sentence
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
|
||||
assistantSpeaking = false;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
const blob = await res.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const audio = new Audio(blobUrl);
|
||||
assistantAudio[idx] = audio;
|
||||
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const stopRecordingCallback = async () => {
|
||||
if ($showCallOverlay) {
|
||||
if (confirmed) {
|
||||
loading = true;
|
||||
|
||||
if (cameraStream) {
|
||||
const imageUrl = takeScreenshot();
|
||||
|
||||
files = [
|
||||
{
|
||||
type: 'image',
|
||||
url: imageUrl
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
||||
await transcribeHandler(audioBlob);
|
||||
|
||||
confirmed = false;
|
||||
loading = false;
|
||||
}
|
||||
audioChunks = [];
|
||||
mediaRecorder = false;
|
||||
|
||||
startRecording();
|
||||
} else {
|
||||
audioChunks = [];
|
||||
mediaRecorder = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startRecording = async () => {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
mediaRecorder = new MediaRecorder(stream);
|
||||
mediaRecorder.onstart = () => {
|
||||
console.log('Recording started');
|
||||
audioChunks = [];
|
||||
analyseAudio(stream);
|
||||
};
|
||||
mediaRecorder.ondataavailable = (event) => {
|
||||
if (hasStartedSpeaking) {
|
||||
audioChunks.push(event.data);
|
||||
}
|
||||
};
|
||||
mediaRecorder.onstop = async () => {
|
||||
console.log('Recording stopped');
|
||||
|
||||
await stopRecordingCallback();
|
||||
};
|
||||
mediaRecorder.start();
|
||||
};
|
||||
|
||||
let videoInputDevices = [];
|
||||
let selectedVideoInputDeviceId = null;
|
||||
|
||||
const getVideoInputDevices = async () => {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
videoInputDevices = devices.filter((device) => device.kind === 'videoinput');
|
||||
|
||||
if (!!navigator.mediaDevices.getDisplayMedia) {
|
||||
videoInputDevices = [
|
||||
...videoInputDevices,
|
||||
{
|
||||
deviceId: 'screen',
|
||||
label: 'Screen Share'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
console.log(videoInputDevices);
|
||||
if (selectedVideoInputDeviceId === null && videoInputDevices.length > 0) {
|
||||
selectedVideoInputDeviceId = videoInputDevices[0].deviceId;
|
||||
}
|
||||
};
|
||||
|
||||
const startCamera = async () => {
|
||||
await getVideoInputDevices();
|
||||
|
||||
if (cameraStream === null) {
|
||||
camera = true;
|
||||
await tick();
|
||||
try {
|
||||
await startVideoStream();
|
||||
} catch (err) {
|
||||
console.error('Error accessing webcam: ', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const startVideoStream = async () => {
|
||||
const video = document.getElementById('camera-feed');
|
||||
if (video) {
|
||||
if (selectedVideoInputDeviceId === 'screen') {
|
||||
cameraStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
video: {
|
||||
cursor: 'always'
|
||||
},
|
||||
audio: false
|
||||
});
|
||||
} else {
|
||||
cameraStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
deviceId: selectedVideoInputDeviceId ? { exact: selectedVideoInputDeviceId } : undefined
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (cameraStream) {
|
||||
await getVideoInputDevices();
|
||||
video.srcObject = cameraStream;
|
||||
await video.play();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const stopVideoStream = async () => {
|
||||
if (cameraStream) {
|
||||
const tracks = cameraStream.getTracks();
|
||||
tracks.forEach((track) => track.stop());
|
||||
}
|
||||
|
||||
cameraStream = null;
|
||||
};
|
||||
|
||||
const takeScreenshot = () => {
|
||||
const video = document.getElementById('camera-feed');
|
||||
const canvas = document.getElementById('camera-canvas');
|
||||
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
// Make the canvas match the video dimensions
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
|
||||
// Draw the image from the video onto the canvas
|
||||
context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
|
||||
|
||||
// Convert the canvas to a data base64 URL and console log it
|
||||
const dataURL = canvas.toDataURL('image/png');
|
||||
console.log(dataURL);
|
||||
|
||||
return dataURL;
|
||||
};
|
||||
|
||||
const stopCamera = async () => {
|
||||
await stopVideoStream();
|
||||
camera = false;
|
||||
};
|
||||
|
||||
$: if ($showCallOverlay) {
|
||||
startRecording();
|
||||
} else {
|
||||
stopCamera();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $showCallOverlay}
|
||||
<audio id="audioElement" src="" style="display: none;" />
|
||||
<div class=" absolute w-full h-screen max-h-[100dvh] flex z-[999] overflow-hidden">
|
||||
<div
|
||||
class="absolute w-full h-screen max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
|
||||
>
|
||||
<div class="max-w-lg w-full h-screen max-h-[100dvh] flex flex-col justify-between p-3 md:p-6">
|
||||
{#if camera}
|
||||
<div class="flex justify-center items-center w-full min-h-20">
|
||||
{#if loading}
|
||||
<svg
|
||||
class="size-12 text-gray-900 dark:text-gray-400"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_qM83 {
|
||||
animation: spinner_8HQG 1.05s infinite;
|
||||
}
|
||||
.spinner_oXPr {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.spinner_ZTLf {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
@keyframes spinner_8HQG {
|
||||
0%,
|
||||
57.14% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
|
||||
transform: translate(0);
|
||||
}
|
||||
28.57% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
</style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle
|
||||
class="spinner_qM83 spinner_oXPr"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="3"
|
||||
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
|
||||
>
|
||||
{:else}
|
||||
<div
|
||||
class=" {rmsLevel * 100 > 4
|
||||
? ' size-[4.5rem]'
|
||||
: rmsLevel * 100 > 2
|
||||
? ' size-16'
|
||||
: rmsLevel * 100 > 1
|
||||
? 'size-14'
|
||||
: 'size-12'} transition-all bg-black dark:bg-white rounded-full"
|
||||
/>
|
||||
{/if}
|
||||
<!-- navbar -->
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-center items-center flex-1 h-full w-full max-h-full">
|
||||
{#if !camera}
|
||||
{#if loading}
|
||||
<svg
|
||||
class="size-44 text-gray-900 dark:text-gray-400"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_qM83 {
|
||||
animation: spinner_8HQG 1.05s infinite;
|
||||
}
|
||||
.spinner_oXPr {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.spinner_ZTLf {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
@keyframes spinner_8HQG {
|
||||
0%,
|
||||
57.14% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
|
||||
transform: translate(0);
|
||||
}
|
||||
28.57% {
|
||||
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
100% {
|
||||
transform: translate(0);
|
||||
}
|
||||
}
|
||||
</style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle
|
||||
class="spinner_qM83 spinner_oXPr"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="3"
|
||||
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
|
||||
>
|
||||
{:else}
|
||||
<div
|
||||
class=" {rmsLevel * 100 > 4
|
||||
? ' size-52'
|
||||
: rmsLevel * 100 > 2
|
||||
? 'size-48'
|
||||
: rmsLevel * 100 > 1
|
||||
? 'size-[11.5rem]'
|
||||
: 'size-44'} transition-all bg-black dark:bg-white rounded-full"
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<div
|
||||
class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full"
|
||||
>
|
||||
<video
|
||||
id="camera-feed"
|
||||
autoplay
|
||||
class="rounded-2xl h-full min-w-full object-cover object-center"
|
||||
playsinline
|
||||
/>
|
||||
|
||||
<canvas id="camera-canvas" style="display:none;" />
|
||||
|
||||
<div class=" absolute top-4 md:top-8 left-4">
|
||||
<button
|
||||
type="button"
|
||||
class="p-1.5 text-white cursor-pointer backdrop-blur-xl bg-black/10 rounded-full"
|
||||
on:click={() => {
|
||||
stopCamera();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="size-6"
|
||||
>
|
||||
<path
|
||||
d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center pb-2 w-full">
|
||||
<div>
|
||||
{#if camera}
|
||||
<VideoInputMenu
|
||||
devices={videoInputDevices}
|
||||
on:change={async (e) => {
|
||||
console.log(e.detail);
|
||||
selectedVideoInputDeviceId = e.detail;
|
||||
await stopVideoStream();
|
||||
await startVideoStream();
|
||||
}}
|
||||
>
|
||||
<button class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900" type="button">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0V5.36l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389A5.5 5.5 0 0 1 13.89 6.11l.311.31h-2.432a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.219Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</VideoInputMenu>
|
||||
{:else}
|
||||
<Tooltip content={$i18n.t('Camera')}>
|
||||
<button
|
||||
class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
startCamera();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="button">
|
||||
<div class=" line-clamp-1 text-sm font-medium">
|
||||
{#if loading}
|
||||
{$i18n.t('Thinking...')}
|
||||
{:else}
|
||||
{$i18n.t('Listening...')}
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900"
|
||||
on:click={async () => {
|
||||
showCallOverlay.set(false);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
|
||||
export let onClose: Function = () => {};
|
||||
export let devices: any;
|
||||
|
||||
let show = false;
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
bind:show
|
||||
on:change={(e) => {
|
||||
if (e.detail === false) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<slot />
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[180px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-[9999] bg-white dark:bg-gray-900 dark:text-white shadow-sm"
|
||||
sideOffset={6}
|
||||
side="top"
|
||||
align="start"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
{#each devices as device}
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
dispatch('change', device.deviceId);
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class=" line-clamp-1">
|
||||
{device?.label ?? 'Camera'}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
</DropdownMenu.Content>
|
||||
</div>
|
||||
</Dropdown>
|
||||
@@ -101,7 +101,7 @@
|
||||
</script>
|
||||
|
||||
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||
<div class="flex w-full px-2">
|
||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||
<div class=" text-lg font-semibold mt-2">#</div>
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
|
||||
{#if prompt.charAt(0) === '@'}
|
||||
{#if filteredModels.length > 0}
|
||||
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||
<div class="flex w-full px-2">
|
||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||
<div class=" text-lg font-semibold mt-2">@</div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let files;
|
||||
export let prompt = '';
|
||||
let selectedCommandIdx = 0;
|
||||
let filteredPromptCommands = [];
|
||||
@@ -35,6 +36,32 @@
|
||||
return '{{CLIPBOARD}}';
|
||||
});
|
||||
|
||||
console.log(clipboardText);
|
||||
|
||||
const clipboardItems = await navigator.clipboard.read();
|
||||
|
||||
let imageUrl = null;
|
||||
for (const item of clipboardItems) {
|
||||
// Check for known image types
|
||||
for (const type of item.types) {
|
||||
if (type.startsWith('image/')) {
|
||||
const blob = await item.getType(type);
|
||||
imageUrl = URL.createObjectURL(blob);
|
||||
console.log(`Image URL (${type}): ${imageUrl}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
files = [
|
||||
...files,
|
||||
{
|
||||
type: 'image',
|
||||
url: imageUrl
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
text = command.content.replaceAll('{{CLIPBOARD}}', clipboardText);
|
||||
}
|
||||
|
||||
@@ -61,7 +88,7 @@
|
||||
</script>
|
||||
|
||||
{#if filteredPromptCommands.length > 0}
|
||||
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||
<div class="flex w-full px-2">
|
||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||
<div class=" text-lg font-semibold mt-2">/</div>
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { createEventDispatcher, tick, getContext } from 'svelte';
|
||||
import { config, settings } from '$lib/stores';
|
||||
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
|
||||
|
||||
import { transcribeAudio } from '$lib/apis/audio';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let recording = false;
|
||||
|
||||
let loading = false;
|
||||
let confirmed = false;
|
||||
|
||||
let durationSeconds = 0;
|
||||
let durationCounter = null;
|
||||
|
||||
let transcription = '';
|
||||
|
||||
const startDurationCounter = () => {
|
||||
durationCounter = setInterval(() => {
|
||||
durationSeconds++;
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const stopDurationCounter = () => {
|
||||
clearInterval(durationCounter);
|
||||
durationSeconds = 0;
|
||||
};
|
||||
|
||||
$: if (recording) {
|
||||
startRecording();
|
||||
} else {
|
||||
stopRecording();
|
||||
}
|
||||
|
||||
const formatSeconds = (seconds) => {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
const formattedSeconds = remainingSeconds < 10 ? `0${remainingSeconds}` : remainingSeconds;
|
||||
return `${minutes}:${formattedSeconds}`;
|
||||
};
|
||||
|
||||
let speechRecognition;
|
||||
|
||||
let mediaRecorder;
|
||||
let audioChunks = [];
|
||||
|
||||
const MIN_DECIBELS = -45;
|
||||
const VISUALIZER_BUFFER_LENGTH = 300;
|
||||
|
||||
let visualizerData = Array(VISUALIZER_BUFFER_LENGTH).fill(0);
|
||||
|
||||
// Function to calculate the RMS level from time domain data
|
||||
const calculateRMS = (data: Uint8Array) => {
|
||||
let sumSquares = 0;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const normalizedValue = (data[i] - 128) / 128; // Normalize the data
|
||||
sumSquares += normalizedValue * normalizedValue;
|
||||
}
|
||||
return Math.sqrt(sumSquares / data.length);
|
||||
};
|
||||
|
||||
const normalizeRMS = (rms) => {
|
||||
rms = rms * 10;
|
||||
const exp = 1.5; // Adjust exponent value; values greater than 1 expand larger numbers more and compress smaller numbers more
|
||||
const scaledRMS = Math.pow(rms, exp);
|
||||
|
||||
// Scale between 0.01 (1%) and 1.0 (100%)
|
||||
return Math.min(1.0, Math.max(0.01, scaledRMS));
|
||||
};
|
||||
|
||||
const analyseAudio = (stream) => {
|
||||
const audioContext = new AudioContext();
|
||||
const audioStreamSource = audioContext.createMediaStreamSource(stream);
|
||||
|
||||
const analyser = audioContext.createAnalyser();
|
||||
analyser.minDecibels = MIN_DECIBELS;
|
||||
audioStreamSource.connect(analyser);
|
||||
|
||||
const bufferLength = analyser.frequencyBinCount;
|
||||
|
||||
const domainData = new Uint8Array(bufferLength);
|
||||
const timeDomainData = new Uint8Array(analyser.fftSize);
|
||||
|
||||
let lastSoundTime = Date.now();
|
||||
|
||||
const detectSound = () => {
|
||||
const processFrame = () => {
|
||||
if (!recording || loading) return;
|
||||
|
||||
if (recording && !loading) {
|
||||
analyser.getByteTimeDomainData(timeDomainData);
|
||||
analyser.getByteFrequencyData(domainData);
|
||||
|
||||
// Calculate RMS level from time domain data
|
||||
const rmsLevel = calculateRMS(timeDomainData);
|
||||
// Push the calculated decibel level to visualizerData
|
||||
visualizerData.push(normalizeRMS(rmsLevel));
|
||||
|
||||
// Ensure visualizerData array stays within the buffer length
|
||||
if (visualizerData.length >= VISUALIZER_BUFFER_LENGTH) {
|
||||
visualizerData.shift();
|
||||
}
|
||||
|
||||
visualizerData = visualizerData;
|
||||
|
||||
// if (domainData.some((value) => value > 0)) {
|
||||
// lastSoundTime = Date.now();
|
||||
// }
|
||||
|
||||
// if (recording && Date.now() - lastSoundTime > 3000) {
|
||||
// if ($settings?.speechAutoSend ?? false) {
|
||||
// confirmRecording();
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(processFrame);
|
||||
};
|
||||
|
||||
window.requestAnimationFrame(processFrame);
|
||||
};
|
||||
|
||||
detectSound();
|
||||
};
|
||||
|
||||
const transcribeHandler = async (audioBlob) => {
|
||||
// Create a blob from the audio chunks
|
||||
|
||||
await tick();
|
||||
const file = blobToFile(audioBlob, 'recording.wav');
|
||||
|
||||
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
console.log(res.text);
|
||||
dispatch('confirm', res.text);
|
||||
}
|
||||
};
|
||||
|
||||
const saveRecording = (blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
document.body.appendChild(a);
|
||||
a.style = 'display: none';
|
||||
a.href = url;
|
||||
a.download = 'recording.wav';
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const startRecording = async () => {
|
||||
startDurationCounter();
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
mediaRecorder = new MediaRecorder(stream);
|
||||
mediaRecorder.onstart = () => {
|
||||
console.log('Recording started');
|
||||
audioChunks = [];
|
||||
analyseAudio(stream);
|
||||
};
|
||||
mediaRecorder.ondataavailable = (event) => audioChunks.push(event.data);
|
||||
mediaRecorder.onstop = async () => {
|
||||
console.log('Recording stopped');
|
||||
if (($settings?.audio?.stt?.engine ?? '') === 'web') {
|
||||
audioChunks = [];
|
||||
} else {
|
||||
if (confirmed) {
|
||||
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
||||
|
||||
await transcribeHandler(audioBlob);
|
||||
|
||||
confirmed = false;
|
||||
loading = false;
|
||||
}
|
||||
audioChunks = [];
|
||||
recording = false;
|
||||
}
|
||||
};
|
||||
mediaRecorder.start();
|
||||
if ($config.audio.stt.engine === 'web' || ($settings?.audio?.stt?.engine ?? '') === 'web') {
|
||||
if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) {
|
||||
// Create a SpeechRecognition object
|
||||
speechRecognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
|
||||
|
||||
// Set continuous to true for continuous recognition
|
||||
speechRecognition.continuous = true;
|
||||
|
||||
// Set the timeout for turning off the recognition after inactivity (in milliseconds)
|
||||
const inactivityTimeout = 2000; // 3 seconds
|
||||
|
||||
let timeoutId;
|
||||
// Start recognition
|
||||
speechRecognition.start();
|
||||
|
||||
// Event triggered when speech is recognized
|
||||
speechRecognition.onresult = async (event) => {
|
||||
// Clear the inactivity timeout
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Handle recognized speech
|
||||
console.log(event);
|
||||
const transcript = event.results[Object.keys(event.results).length - 1][0].transcript;
|
||||
|
||||
transcription = `${transcription}${transcript}`;
|
||||
|
||||
await tick();
|
||||
document.getElementById('chat-textarea')?.focus();
|
||||
|
||||
// Restart the inactivity timeout
|
||||
timeoutId = setTimeout(() => {
|
||||
console.log('Speech recognition turned off due to inactivity.');
|
||||
speechRecognition.stop();
|
||||
}, inactivityTimeout);
|
||||
};
|
||||
|
||||
// Event triggered when recognition is ended
|
||||
speechRecognition.onend = function () {
|
||||
// Restart recognition after it ends
|
||||
console.log('recognition ended');
|
||||
|
||||
confirmRecording();
|
||||
dispatch('confirm', transcription);
|
||||
|
||||
confirmed = false;
|
||||
loading = false;
|
||||
};
|
||||
|
||||
// Event triggered when an error occurs
|
||||
speechRecognition.onerror = function (event) {
|
||||
console.log(event);
|
||||
toast.error($i18n.t(`Speech recognition error: {{error}}`, { error: event.error }));
|
||||
dispatch('cancel');
|
||||
|
||||
stopRecording();
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const stopRecording = async () => {
|
||||
if (recording && mediaRecorder) {
|
||||
await mediaRecorder.stop();
|
||||
}
|
||||
stopDurationCounter();
|
||||
audioChunks = [];
|
||||
};
|
||||
|
||||
const confirmRecording = async () => {
|
||||
loading = true;
|
||||
confirmed = true;
|
||||
|
||||
if (recording && mediaRecorder) {
|
||||
await mediaRecorder.stop();
|
||||
}
|
||||
clearInterval(durationCounter);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="{loading
|
||||
? ' bg-gray-100/50 dark:bg-gray-850/50'
|
||||
: 'bg-indigo-300/10 dark:bg-indigo-500/10 '} rounded-full flex p-2.5"
|
||||
>
|
||||
<div class="flex items-center mr-1">
|
||||
<button
|
||||
type="button"
|
||||
class="p-1.5
|
||||
|
||||
{loading
|
||||
? ' bg-gray-200 dark:bg-gray-700/50'
|
||||
: 'bg-indigo-400/20 text-indigo-600 dark:text-indigo-300 '}
|
||||
|
||||
|
||||
rounded-full"
|
||||
on:click={async () => {
|
||||
dispatch('cancel');
|
||||
stopRecording();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="3"
|
||||
stroke="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-1 self-center items-center justify-between ml-2 mx-1 overflow-hidden h-6"
|
||||
dir="rtl"
|
||||
>
|
||||
<div class="flex-1 flex items-center gap-0.5 h-6">
|
||||
{#each visualizerData.slice().reverse() as rms}
|
||||
<div
|
||||
class="w-[2px]
|
||||
|
||||
{loading
|
||||
? ' bg-gray-500 dark:bg-gray-400 '
|
||||
: 'bg-indigo-500 dark:bg-indigo-400 '}
|
||||
|
||||
inline-block h-full"
|
||||
style="height: {Math.min(100, Math.max(14, rms * 100))}%;"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" mx-1.5 pr-1 flex justify-center items-center">
|
||||
<div
|
||||
class="text-sm
|
||||
|
||||
|
||||
{loading ? ' text-gray-500 dark:text-gray-400 ' : ' text-indigo-400 '}
|
||||
font-medium flex-1 mx-auto text-center"
|
||||
>
|
||||
{formatSeconds(durationSeconds)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mr-1">
|
||||
{#if loading}
|
||||
<div class=" text-gray-500 rounded-full cursor-not-allowed">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
><style>
|
||||
.spinner_OSmW {
|
||||
transform-origin: center;
|
||||
animation: spinner_T6mA 0.75s step-end infinite;
|
||||
}
|
||||
@keyframes spinner_T6mA {
|
||||
8.3% {
|
||||
transform: rotate(30deg);
|
||||
}
|
||||
16.6% {
|
||||
transform: rotate(60deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
33.3% {
|
||||
transform: rotate(120deg);
|
||||
}
|
||||
41.6% {
|
||||
transform: rotate(150deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
58.3% {
|
||||
transform: rotate(210deg);
|
||||
}
|
||||
66.6% {
|
||||
transform: rotate(240deg);
|
||||
}
|
||||
75% {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
83.3% {
|
||||
transform: rotate(300deg);
|
||||
}
|
||||
91.6% {
|
||||
transform: rotate(330deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><g class="spinner_OSmW"
|
||||
><rect x="11" y="1" width="2" height="5" opacity=".14" /><rect
|
||||
x="11"
|
||||
y="1"
|
||||
width="2"
|
||||
height="5"
|
||||
transform="rotate(30 12 12)"
|
||||
opacity=".29"
|
||||
/><rect
|
||||
x="11"
|
||||
y="1"
|
||||
width="2"
|
||||
height="5"
|
||||
transform="rotate(60 12 12)"
|
||||
opacity=".43"
|
||||
/><rect
|
||||
x="11"
|
||||
y="1"
|
||||
width="2"
|
||||
height="5"
|
||||
transform="rotate(90 12 12)"
|
||||
opacity=".57"
|
||||
/><rect
|
||||
x="11"
|
||||
y="1"
|
||||
width="2"
|
||||
height="5"
|
||||
transform="rotate(120 12 12)"
|
||||
opacity=".71"
|
||||
/><rect
|
||||
x="11"
|
||||
y="1"
|
||||
width="2"
|
||||
height="5"
|
||||
transform="rotate(150 12 12)"
|
||||
opacity=".86"
|
||||
/><rect x="11" y="1" width="2" height="5" transform="rotate(180 12 12)" /></g
|
||||
></svg
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="p-1.5 bg-indigo-500 text-white dark:bg-indigo-500 dark:text-blue-950 rounded-full"
|
||||
on:click={async () => {
|
||||
await confirmRecording();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.5"
|
||||
stroke="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.visualizer {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.visualizer-bar {
|
||||
width: 2px;
|
||||
background-color: #4a5aba; /* or whatever color you need */
|
||||
}
|
||||
</style>
|
||||
@@ -286,7 +286,7 @@
|
||||
{#each messages as message, messageIdx}
|
||||
<div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}">
|
||||
<div
|
||||
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
|
||||
class="flex flex-col justify-between px-5 mb-3 {$settings?.widescreenMode ?? null
|
||||
? 'max-w-full'
|
||||
: 'max-w-5xl'} mx-auto rounded-lg group"
|
||||
>
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
class=" snap-center min-w-80 w-full max-w-full m-1 border {history.messages[
|
||||
currentMessageId
|
||||
].model === model
|
||||
? 'border-gray-100 dark:border-gray-700 border-[1.5px]'
|
||||
? 'border-gray-100 dark:border-gray-850 border-[1.5px]'
|
||||
: 'border-gray-50 dark:border-gray-850 '} transition p-5 rounded-3xl"
|
||||
on:click={() => {
|
||||
currentMessageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
} else {
|
||||
speaking = true;
|
||||
|
||||
if ($settings?.audio?.TTSEngine === 'openai') {
|
||||
if ($config.audio.tts.engine === 'openai') {
|
||||
loadingSpeech = true;
|
||||
|
||||
const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => {
|
||||
@@ -244,9 +244,8 @@
|
||||
for (const [idx, sentence] of sentences.entries()) {
|
||||
const res = await synthesizeOpenAISpeech(
|
||||
localStorage.token,
|
||||
$settings?.audio?.speaker,
|
||||
sentence,
|
||||
$settings?.audio?.model
|
||||
$settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice,
|
||||
sentence
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
|
||||
@@ -273,17 +272,29 @@
|
||||
clearInterval(getVoicesLoop);
|
||||
|
||||
const voice =
|
||||
voices?.filter((v) => v.name === $settings?.audio?.speaker)?.at(0) ?? undefined;
|
||||
voices
|
||||
?.filter(
|
||||
(v) => v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice)
|
||||
)
|
||||
?.at(0) ?? undefined;
|
||||
|
||||
console.log(voice);
|
||||
|
||||
const speak = new SpeechSynthesisUtterance(message.content);
|
||||
|
||||
console.log(speak);
|
||||
|
||||
speak.onend = () => {
|
||||
speaking = null;
|
||||
if ($settings.conversationMode) {
|
||||
document.getElementById('voice-input-button')?.click();
|
||||
}
|
||||
};
|
||||
speak.voice = voice;
|
||||
|
||||
if (voice) {
|
||||
speak.voice = voice;
|
||||
}
|
||||
|
||||
speechSynthesis.speak(speak);
|
||||
}
|
||||
}, 100);
|
||||
@@ -341,6 +352,17 @@
|
||||
generatingImage = false;
|
||||
};
|
||||
|
||||
$: if (!edit) {
|
||||
(async () => {
|
||||
await tick();
|
||||
renderStyling();
|
||||
|
||||
await mermaid.run({
|
||||
querySelector: '.mermaid'
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await tick();
|
||||
renderStyling();
|
||||
@@ -746,7 +768,7 @@
|
||||
</Tooltip>
|
||||
|
||||
{#if $config?.features.enable_image_generation && !readOnly}
|
||||
<Tooltip content="Generate Image" placement="bottom">
|
||||
<Tooltip content={$i18n.t('Generate Image')} placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
|
||||
@@ -219,7 +219,7 @@
|
||||
<DropdownMenu.Content
|
||||
class=" z-40 {$mobile
|
||||
? `w-full`
|
||||
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none "
|
||||
: `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-850/50 outline-none "
|
||||
transition={flyAndScale}
|
||||
side={$mobile ? 'bottom' : 'bottom-start'}
|
||||
sideOffset={4}
|
||||
@@ -265,7 +265,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center min-w-fit">
|
||||
<div class="line-clamp-1">
|
||||
{item.label}
|
||||
</div>
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
</div>
|
||||
|
||||
{#if ollamaVersion}
|
||||
<hr class=" dark:border-gray-700" />
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div>
|
||||
@@ -104,7 +104,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div class="flex space-x-1">
|
||||
<a href="https://discord.gg/5rJgQTnV4s" target="_blank">
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
<UpdatePassword />
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700 my-4" />
|
||||
<hr class=" dark:border-gray-850 my-4" />
|
||||
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('API keys')}</div>
|
||||
|
||||
@@ -556,7 +556,7 @@
|
||||
type="number"
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="-1"
|
||||
step="10"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio';
|
||||
import { user, settings } from '$lib/stores';
|
||||
import { user, settings, config } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
@@ -10,24 +10,15 @@
|
||||
export let saveSettings: Function;
|
||||
|
||||
// Audio
|
||||
|
||||
let OpenAIUrl = '';
|
||||
let OpenAIKey = '';
|
||||
|
||||
let STTEngines = ['', 'openai'];
|
||||
let STTEngine = '';
|
||||
|
||||
let conversationMode = false;
|
||||
let speechAutoSend = false;
|
||||
let responseAutoPlayback = false;
|
||||
let nonLocalVoices = false;
|
||||
|
||||
let TTSEngines = ['', 'openai'];
|
||||
let TTSEngine = '';
|
||||
let STTEngine = '';
|
||||
|
||||
let voices = [];
|
||||
let speaker = '';
|
||||
let models = [];
|
||||
let model = '';
|
||||
let voice = '';
|
||||
|
||||
const getOpenAIVoices = () => {
|
||||
voices = [
|
||||
@@ -40,10 +31,6 @@
|
||||
];
|
||||
};
|
||||
|
||||
const getOpenAIVoicesModel = () => {
|
||||
models = [{ name: 'tts-1' }, { name: 'tts-1-hd' }];
|
||||
};
|
||||
|
||||
const getWebAPIVoices = () => {
|
||||
const getVoicesLoop = setInterval(async () => {
|
||||
voices = await speechSynthesis.getVoices();
|
||||
@@ -55,21 +42,6 @@
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const toggleConversationMode = async () => {
|
||||
conversationMode = !conversationMode;
|
||||
|
||||
if (conversationMode) {
|
||||
responseAutoPlayback = true;
|
||||
speechAutoSend = true;
|
||||
}
|
||||
|
||||
saveSettings({
|
||||
conversationMode: conversationMode,
|
||||
responseAutoPlayback: responseAutoPlayback,
|
||||
speechAutoSend: speechAutoSend
|
||||
});
|
||||
};
|
||||
|
||||
const toggleResponseAutoPlayback = async () => {
|
||||
responseAutoPlayback = !responseAutoPlayback;
|
||||
saveSettings({ responseAutoPlayback: responseAutoPlayback });
|
||||
@@ -80,66 +52,35 @@
|
||||
saveSettings({ speechAutoSend: speechAutoSend });
|
||||
};
|
||||
|
||||
const updateConfigHandler = async () => {
|
||||
if (TTSEngine === 'openai') {
|
||||
const res = await updateAudioConfig(localStorage.token, {
|
||||
url: OpenAIUrl,
|
||||
key: OpenAIKey,
|
||||
model: model,
|
||||
speaker: speaker
|
||||
});
|
||||
|
||||
if (res) {
|
||||
OpenAIUrl = res.OPENAI_API_BASE_URL;
|
||||
OpenAIKey = res.OPENAI_API_KEY;
|
||||
model = res.OPENAI_API_MODEL;
|
||||
speaker = res.OPENAI_API_VOICE;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
conversationMode = $settings.conversationMode ?? false;
|
||||
speechAutoSend = $settings.speechAutoSend ?? false;
|
||||
responseAutoPlayback = $settings.responseAutoPlayback ?? false;
|
||||
|
||||
STTEngine = $settings?.audio?.STTEngine ?? '';
|
||||
TTSEngine = $settings?.audio?.TTSEngine ?? '';
|
||||
speaker = $settings?.audio?.speaker ?? '';
|
||||
model = $settings?.audio?.model ?? '';
|
||||
STTEngine = $settings?.audio?.stt?.engine ?? '';
|
||||
voice = $settings?.audio?.tts?.voice ?? $config.audio.tts.voice ?? '';
|
||||
nonLocalVoices = $settings.audio?.tts?.nonLocalVoices ?? false;
|
||||
|
||||
if (TTSEngine === 'openai') {
|
||||
if ($config.audio.tts.engine === 'openai') {
|
||||
getOpenAIVoices();
|
||||
getOpenAIVoicesModel();
|
||||
} else {
|
||||
getWebAPIVoices();
|
||||
}
|
||||
|
||||
if ($user.role === 'admin') {
|
||||
const res = await getAudioConfig(localStorage.token);
|
||||
|
||||
if (res) {
|
||||
OpenAIUrl = res.OPENAI_API_BASE_URL;
|
||||
OpenAIKey = res.OPENAI_API_KEY;
|
||||
model = res.OPENAI_API_MODEL;
|
||||
speaker = res.OPENAI_API_VOICE;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={async () => {
|
||||
if ($user.role === 'admin') {
|
||||
await updateConfigHandler();
|
||||
}
|
||||
saveSettings({
|
||||
audio: {
|
||||
STTEngine: STTEngine !== '' ? STTEngine : undefined,
|
||||
TTSEngine: TTSEngine !== '' ? TTSEngine : undefined,
|
||||
speaker: speaker !== '' ? speaker : undefined,
|
||||
model: model !== '' ? model : undefined
|
||||
stt: {
|
||||
engine: STTEngine !== '' ? STTEngine : undefined
|
||||
},
|
||||
tts: {
|
||||
voice: voice !== '' ? voice : undefined,
|
||||
nonLocalVoices: $config.audio.tts.engine === '' ? nonLocalVoices : undefined
|
||||
}
|
||||
}
|
||||
});
|
||||
dispatch('save');
|
||||
@@ -149,53 +90,25 @@
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={STTEngine}
|
||||
placeholder="Select a mode"
|
||||
on:change={(e) => {
|
||||
if (e.target.value !== '') {
|
||||
navigator.mediaDevices.getUserMedia({ audio: true }).catch(function (err) {
|
||||
toast.error(
|
||||
$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
|
||||
error: err
|
||||
})
|
||||
);
|
||||
STTEngine = '';
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">{$i18n.t('Default (Web API)')}</option>
|
||||
<option value="whisper-local">{$i18n.t('Whisper (Local)')}</option>
|
||||
</select>
|
||||
{#if $config.audio.stt.engine !== 'web'}
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={STTEngine}
|
||||
placeholder="Select an engine"
|
||||
>
|
||||
<option value="">{$i18n.t('Default')}</option>
|
||||
<option value="web">{$i18n.t('Web API')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Conversation Mode')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleConversationMode();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if conversationMode === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Auto-send input after 3 sec.')}
|
||||
{$i18n.t('Instant Auto-Send After Voice Transcription')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -217,50 +130,6 @@
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={TTSEngine}
|
||||
placeholder="Select a mode"
|
||||
on:change={(e) => {
|
||||
if (e.target.value === 'openai') {
|
||||
getOpenAIVoices();
|
||||
speaker = 'alloy';
|
||||
model = 'tts-1';
|
||||
} else {
|
||||
getWebAPIVoices();
|
||||
speaker = '';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">{$i18n.t('Default (Web API)')}</option>
|
||||
<option value="openai">{$i18n.t('Open AI')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $user.role === 'admin'}
|
||||
{#if TTSEngine === 'openai'}
|
||||
<div class="mt-1 flex gap-2 mb-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={OpenAIUrl}
|
||||
required
|
||||
/>
|
||||
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={OpenAIKey}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
|
||||
|
||||
@@ -280,28 +149,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
{#if TTSEngine === ''}
|
||||
{#if $config.audio.tts.engine === ''}
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={speaker}
|
||||
placeholder="Select a voice"
|
||||
bind:value={voice}
|
||||
>
|
||||
<option value="" selected>{$i18n.t('Default')}</option>
|
||||
{#each voices.filter((v) => v.localService === true) as voice}
|
||||
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
|
||||
<option value="" selected={voice !== ''}>{$i18n.t('Default')}</option>
|
||||
{#each voices.filter((v) => nonLocalVoices || v.localService === true) as _voice}
|
||||
<option
|
||||
value={_voice.name}
|
||||
class="bg-gray-100 dark:bg-gray-700"
|
||||
selected={voice === _voice.name}>{_voice.name}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between my-1.5">
|
||||
<div class="text-xs">
|
||||
{$i18n.t('Allow non-local voices')}
|
||||
</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<Switch bind:state={nonLocalVoices} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if TTSEngine === 'openai'}
|
||||
{:else if $config.audio.tts.engine === 'openai'}
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
|
||||
<div class="flex w-full">
|
||||
@@ -309,7 +189,7 @@
|
||||
<input
|
||||
list="voice-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={speaker}
|
||||
bind:value={voice}
|
||||
placeholder="Select a voice"
|
||||
/>
|
||||
|
||||
@@ -321,25 +201,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Model')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="model-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={model}
|
||||
placeholder="Select a model"
|
||||
/>
|
||||
|
||||
<datalist id="model-list">
|
||||
{#each models as model}
|
||||
<option value={model.name} />
|
||||
{/each}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
@@ -218,7 +218,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div class="flex flex-col">
|
||||
{#if showArchiveConfirm}
|
||||
|
||||
@@ -203,7 +203,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700 my-3" />
|
||||
<hr class=" dark:border-gray-850 my-3" />
|
||||
|
||||
<div>
|
||||
<div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div>
|
||||
@@ -228,7 +228,7 @@
|
||||
|
||||
{#if showAdvanced}
|
||||
<AdvancedParams bind:params />
|
||||
<hr class=" dark:border-gray-700" />
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div class=" py-1 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
|
||||
@@ -14,14 +14,11 @@
|
||||
// Addons
|
||||
let titleAutoGenerate = true;
|
||||
let responseAutoCopy = false;
|
||||
let titleAutoGenerateModel = '';
|
||||
let titleAutoGenerateModelExternal = '';
|
||||
let fullScreenMode = false;
|
||||
let titleGenerationPrompt = '';
|
||||
let widescreenMode = false;
|
||||
let splitLargeChunks = false;
|
||||
|
||||
// Interface
|
||||
let promptSuggestions = [];
|
||||
let defaultModelId = '';
|
||||
let showUsername = false;
|
||||
let chatBubble = true;
|
||||
let chatDirection: 'LTR' | 'RTL' = 'LTR';
|
||||
@@ -31,9 +28,9 @@
|
||||
saveSettings({ splitLargeChunks: splitLargeChunks });
|
||||
};
|
||||
|
||||
const toggleFullScreenMode = async () => {
|
||||
fullScreenMode = !fullScreenMode;
|
||||
saveSettings({ fullScreenMode: fullScreenMode });
|
||||
const togglewidescreenMode = async () => {
|
||||
widescreenMode = !widescreenMode;
|
||||
saveSettings({ widescreenMode: widescreenMode });
|
||||
};
|
||||
|
||||
const toggleChatBubble = async () => {
|
||||
@@ -84,39 +81,21 @@
|
||||
};
|
||||
|
||||
const updateInterfaceHandler = async () => {
|
||||
if ($user.role === 'admin') {
|
||||
promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
|
||||
await config.set(await getBackendConfig());
|
||||
}
|
||||
|
||||
saveSettings({
|
||||
title: {
|
||||
...$settings.title,
|
||||
model: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
|
||||
modelExternal:
|
||||
titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined,
|
||||
prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
|
||||
}
|
||||
models: [defaultModelId]
|
||||
});
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if ($user.role === 'admin') {
|
||||
promptSuggestions = $config?.default_prompt_suggestions;
|
||||
}
|
||||
|
||||
titleAutoGenerate = $settings?.title?.auto ?? true;
|
||||
titleAutoGenerateModel = $settings?.title?.model ?? '';
|
||||
titleAutoGenerateModelExternal = $settings?.title?.modelExternal ?? '';
|
||||
titleGenerationPrompt =
|
||||
$settings?.title?.prompt ??
|
||||
`Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': {{prompt}}`;
|
||||
responseAutoCopy = $settings.responseAutoCopy ?? false;
|
||||
showUsername = $settings.showUsername ?? false;
|
||||
chatBubble = $settings.chatBubble ?? true;
|
||||
fullScreenMode = $settings.fullScreenMode ?? false;
|
||||
widescreenMode = $settings.widescreenMode ?? false;
|
||||
splitLargeChunks = $settings.splitLargeChunks ?? false;
|
||||
chatDirection = $settings.chatDirection ?? 'LTR';
|
||||
|
||||
defaultModelId = ($settings?.models ?? ['']).at(0);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -195,16 +174,16 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Full Screen Mode')}</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Widescreen Mode')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleFullScreenMode();
|
||||
togglewidescreenMode();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if fullScreenMode === true}
|
||||
{#if widescreenMode === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
@@ -278,161 +257,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium flex">
|
||||
<div class=" mr-1">{$i18n.t('Set Task Model')}</div>
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'A task model is used when performing tasks such as generating titles for chats and web search queries'
|
||||
)}
|
||||
<div class=" space-y-1 mb-3">
|
||||
<div class="mb-2">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<div class=" text-xs font-medium">{$i18n.t('Default Model')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 mr-2">
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={defaultModelId}
|
||||
placeholder="Select a model"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
|
||||
/>
|
||||
</svg>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="flex w-full gap-2 pr-2">
|
||||
<div class="flex-1">
|
||||
<div class=" text-xs mb-1">Local Models</div>
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={titleAutoGenerateModel}
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
>
|
||||
<option value="" selected>{$i18n.t('Current Model')}</option>
|
||||
{#each $models.filter((m) => m.owned_by === 'ollama') as model}
|
||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
|
||||
{model.name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<div class=" text-xs mb-1">External Models</div>
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={titleAutoGenerateModelExternal}
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
>
|
||||
<option value="" selected>{$i18n.t('Current Model')}</option>
|
||||
{#each $models as model}
|
||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">
|
||||
{model.name}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 mr-2">
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
|
||||
<textarea
|
||||
bind:value={titleGenerationPrompt}
|
||||
class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="3"
|
||||
/>
|
||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||
{#each $models.filter((model) => model.id) as model}
|
||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $user.role === 'admin'}
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div class=" space-y-3 pr-1.5">
|
||||
<div class="flex w-full justify-between mb-2">
|
||||
<div class=" self-center text-sm font-semibold">
|
||||
{$i18n.t('Default Prompt Suggestions')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') {
|
||||
promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }];
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-1">
|
||||
{#each promptSuggestions as prompt, promptIdx}
|
||||
<div class=" flex border dark:border-gray-600 rounded-lg">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex border-b dark:border-gray-600 w-full">
|
||||
<input
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
|
||||
placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
|
||||
bind:value={prompt.title[0]}
|
||||
/>
|
||||
|
||||
<input
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
|
||||
placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
|
||||
bind:value={prompt.title[1]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
|
||||
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
|
||||
bind:value={prompt.content}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="px-2"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
promptSuggestions.splice(promptIdx, 1);
|
||||
promptSuggestions = promptSuggestions;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if promptSuggestions.length > 0}
|
||||
<div class="text-xs text-left w-full mt-2">
|
||||
{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end text-sm font-medium">
|
||||
|
||||
@@ -43,6 +43,13 @@
|
||||
|
||||
let modelTransferring = false;
|
||||
let modelTag = '';
|
||||
|
||||
let createModelLoading = false;
|
||||
let createModelTag = '';
|
||||
let createModelContent = '';
|
||||
let createModelDigest = '';
|
||||
let createModelPullProgress = null;
|
||||
|
||||
let digest = '';
|
||||
let pullProgress = null;
|
||||
|
||||
@@ -434,6 +441,83 @@
|
||||
}
|
||||
};
|
||||
|
||||
const createModelHandler = async () => {
|
||||
createModelLoading = true;
|
||||
const res = await createModel(
|
||||
localStorage.token,
|
||||
createModelTag,
|
||||
createModelContent,
|
||||
selectedOllamaUrlIdx
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res && res.ok) {
|
||||
const reader = res.body
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
.pipeThrough(splitStream('\n'))
|
||||
.getReader();
|
||||
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
try {
|
||||
let lines = value.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line !== '') {
|
||||
console.log(line);
|
||||
let data = JSON.parse(line);
|
||||
console.log(data);
|
||||
|
||||
if (data.error) {
|
||||
throw data.error;
|
||||
}
|
||||
if (data.detail) {
|
||||
throw data.detail;
|
||||
}
|
||||
|
||||
if (data.status) {
|
||||
if (
|
||||
!data.digest &&
|
||||
!data.status.includes('writing') &&
|
||||
!data.status.includes('sha256')
|
||||
) {
|
||||
toast.success(data.status);
|
||||
} else {
|
||||
if (data.digest) {
|
||||
createModelDigest = data.digest;
|
||||
|
||||
if (data.completed) {
|
||||
createModelPullProgress =
|
||||
Math.round((data.completed / data.total) * 1000) / 10;
|
||||
} else {
|
||||
createModelPullProgress = 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
toast.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
models.set(await getModels());
|
||||
|
||||
createModelLoading = false;
|
||||
|
||||
createModelTag = '';
|
||||
createModelContent = '';
|
||||
createModelDigest = '';
|
||||
createModelPullProgress = null;
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
const ollamaConfig = await getOllamaConfig(localStorage.token);
|
||||
|
||||
@@ -457,13 +541,13 @@
|
||||
]);
|
||||
} else {
|
||||
ollamaEnabled = false;
|
||||
toast.error('Ollama API is disabled');
|
||||
toast.error($i18n.t('Ollama API is disabled'));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full justify-between text-sm">
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-[24rem]">
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[27rem]">
|
||||
{#if ollamaEnabled}
|
||||
{#if ollamaVersion !== null}
|
||||
<div class="space-y-2 pr-1.5">
|
||||
@@ -695,6 +779,77 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Create a model')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2 flex flex-col gap-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', {
|
||||
modelTag: 'my-modelfile'
|
||||
})}
|
||||
bind:value={createModelTag}
|
||||
disabled={createModelLoading}
|
||||
/>
|
||||
|
||||
<textarea
|
||||
bind:value={createModelContent}
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden"
|
||||
rows="6"
|
||||
placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`}
|
||||
disabled={createModelLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex self-start">
|
||||
<button
|
||||
class="px-2.5 py-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed"
|
||||
on:click={() => {
|
||||
createModelHandler();
|
||||
}}
|
||||
disabled={createModelLoading}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
|
||||
/>
|
||||
<path
|
||||
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if createModelDigest !== ''}
|
||||
<div class="flex flex-col mt-1">
|
||||
<div class="font-medium mb-1">{createModelTag}</div>
|
||||
<div class="">
|
||||
<div class="flex flex-row justify-between space-x-4 pr-2">
|
||||
<div class=" flex-1">
|
||||
<div
|
||||
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
|
||||
style="width: {Math.max(15, createModelPullProgress ?? 0)}%"
|
||||
>
|
||||
{createModelPullProgress ?? 0}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if createModelDigest}
|
||||
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
|
||||
{createModelDigest}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="pt-1">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<div class=" text-sm font-medium">{$i18n.t('Experimental')}</div>
|
||||
@@ -908,7 +1063,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else if ollamaEnabled === false}
|
||||
<div>Ollama API is disabled</div>
|
||||
<div>{$i18n.t('Ollama API is disabled')}</div>
|
||||
{:else}
|
||||
<div class="flex h-full justify-center">
|
||||
<div class="my-auto">
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<Tooltip
|
||||
content="This is an experimental feature, it may not function as expected and is subject to change at any time."
|
||||
content={$i18n.t(
|
||||
'This is an experimental feature, it may not function as expected and is subject to change at any time.'
|
||||
)}
|
||||
>
|
||||
<div class="text-sm font-medium">
|
||||
{$i18n.t('Memory')}
|
||||
@@ -57,8 +59,9 @@
|
||||
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<div>
|
||||
You can personalize your interactions with LLMs by adding memories through the 'Manage'
|
||||
button below, making them more helpful and tailored to you.
|
||||
{$i18n.t(
|
||||
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you."
|
||||
)}
|
||||
</div>
|
||||
|
||||
<!-- <div class="mt-3">
|
||||
@@ -79,7 +82,7 @@
|
||||
showManageModal = true;
|
||||
}}
|
||||
>
|
||||
Manage
|
||||
{$i18n.t('Manage')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
/>
|
||||
|
||||
<div class="text-xs text-gray-500">
|
||||
ⓘ Refer to yourself as "User" (e.g., "User is learning Spanish")
|
||||
ⓘ {$i18n.t('Refer to yourself as "User" (e.g., "User is learning Spanish")')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl"
|
||||
on:click={() => {
|
||||
showAddMemoryModal = true;
|
||||
}}>Add memory</button
|
||||
}}>{$i18n.t('Add Memory')}</button
|
||||
>
|
||||
<button
|
||||
class=" px-3.5 py-1.5 font-medium text-red-500 hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-red-300 dark:outline-red-800 rounded-3xl"
|
||||
@@ -150,7 +150,7 @@
|
||||
toast.success('Memory cleared successfully');
|
||||
memories = [];
|
||||
}
|
||||
}}>Clear memory</button
|
||||
}}>{$i18n.t('Clear memory')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,16 +8,14 @@
|
||||
import Modal from '../common/Modal.svelte';
|
||||
import Account from './Settings/Account.svelte';
|
||||
import About from './Settings/About.svelte';
|
||||
import Models from './Settings/Models.svelte';
|
||||
import General from './Settings/General.svelte';
|
||||
import Interface from './Settings/Interface.svelte';
|
||||
import Audio from './Settings/Audio.svelte';
|
||||
import Chats from './Settings/Chats.svelte';
|
||||
import Connections from './Settings/Connections.svelte';
|
||||
import Images from './Settings/Images.svelte';
|
||||
import User from '../icons/User.svelte';
|
||||
import Personalization from './Settings/Personalization.svelte';
|
||||
import { updateUserSettings } from '$lib/apis/users';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -90,55 +88,32 @@
|
||||
<div class=" self-center">{$i18n.t('General')}</div>
|
||||
</button>
|
||||
|
||||
{#if $user?.role === 'admin'}
|
||||
{#if $user.role === 'admin'}
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'connections'
|
||||
'admin'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'connections';
|
||||
on:click={async () => {
|
||||
await goto('/admin/settings');
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Connections')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'models'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'models';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 1c3.866 0 7 1.79 7 4s-3.134 4-7 4-7-1.79-7-4 3.134-4 7-4zm5.694 8.13c.464-.264.91-.583 1.306-.952V10c0 2.21-3.134 4-7 4s-7-1.79-7-4V8.178c.396.37.842.688 1.306.953C5.838 10.006 7.854 10.5 10 10.5s4.162-.494 5.694-1.37zM3 13.179V15c0 2.21 3.134 4 7 4s7-1.79 7-4v-1.822c-.396.37-.842.688-1.306.953-1.532.875-3.548 1.369-5.694 1.369s-4.162-.494-5.694-1.37A7.009 7.009 0 013 13.179z"
|
||||
d="M4.5 3.75a3 3 0 0 0-3 3v10.5a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V6.75a3 3 0 0 0-3-3h-15Zm4.125 3a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Zm-3.873 8.703a4.126 4.126 0 0 1 7.746 0 .75.75 0 0 1-.351.92 7.47 7.47 0 0 1-3.522.877 7.47 7.47 0 0 1-3.522-.877.75.75 0 0 1-.351-.92ZM15 8.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15ZM14.25 12a.75.75 0 0 1 .75-.75h3.75a.75.75 0 0 1 0 1.5H15a.75.75 0 0 1-.75-.75Zm.75 2.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Models')}</div>
|
||||
<div class=" self-center">{$i18n.t('Admin Settings')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -210,34 +185,6 @@
|
||||
<div class=" self-center">{$i18n.t('Audio')}</div>
|
||||
</button>
|
||||
|
||||
{#if $user.role === 'admin'}
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'images'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'images';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Images')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'chats'
|
||||
@@ -325,15 +272,6 @@
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'models'}
|
||||
<Models {getModels} />
|
||||
{:else if selectedTab === 'connections'}
|
||||
<Connections
|
||||
{getModels}
|
||||
on:save={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'interface'}
|
||||
<Interface
|
||||
{saveSettings}
|
||||
@@ -355,13 +293,6 @@
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'images'}
|
||||
<Images
|
||||
{saveSettings}
|
||||
on:save={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'chats'}
|
||||
<Chats {saveSettings} />
|
||||
{:else if selectedTab === 'account'}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
{#if !dismissed}
|
||||
{#if mounted}
|
||||
<div
|
||||
class=" top-0 left-0 right-0 p-2 mx-4 px-3 flex justify-center items-center relative rounded-xl border border-gray-50 dark:border-gray-850 text-gray-800 dark:text-gary-100 bg-white dark:bg-gray-900 backdrop-blur-xl z-40"
|
||||
class=" top-0 left-0 right-0 p-2 mx-4 px-3 flex justify-center items-center relative rounded-xl border border-gray-50 dark:border-gray-850 text-gray-800 dark:text-gary-100 bg-white dark:bg-gray-900 backdrop-blur-xl z-30"
|
||||
transition:fade={{ delay: 100, duration: 300 }}
|
||||
>
|
||||
<div class=" flex flex-col md:flex-row md:items-center flex-1 text-sm w-fit gap-1.5">
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" />
|
||||
</Select.Trigger>
|
||||
<Select.Content
|
||||
class="w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50 outline-none"
|
||||
class="w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-850/50 outline-none"
|
||||
transition={flyAndScale}
|
||||
sideOffset={4}
|
||||
>
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
touch: touch
|
||||
});
|
||||
}
|
||||
} else if (tooltipInstance && content === '') {
|
||||
if (tooltipInstance) {
|
||||
tooltipInstance.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
)}
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700 my-3" />
|
||||
<hr class=" dark:border-gray-850 my-3" />
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
|
||||
@@ -68,10 +68,10 @@
|
||||
<select
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={webConfig.search.engine}
|
||||
placeholder="Select a engine"
|
||||
placeholder={$i18n.t('Select a engine')}
|
||||
required
|
||||
>
|
||||
<option disabled selected value="">Select a engine</option>
|
||||
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
|
||||
{#each webSearchEngines as engine}
|
||||
<option value={engine}>{engine}</option>
|
||||
{/each}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
export let className = 'w-4 h-4';
|
||||
export let strokeWidth = '0';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12 5a7 7 0 0 0-7 7v1.17c.313-.11.65-.17 1-.17h2a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H6a3 3 0 0 1-3-3v-6a9 9 0 0 1 18 0v6a3 3 0 0 1-3 3h-2a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h2c.35 0 .687.06 1 .17V12a7 7 0 0 0-7-7Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let className = 'size-4';
|
||||
export let strokeWidth = '2';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7Zm5.01 1H5v2.01h2.01V8Zm3 0H8v2.01h2.01V8Zm3 0H11v2.01h2.01V8Zm3 0H14v2.01h2.01V8Zm3 0H17v2.01h2.01V8Zm-12 3H5v2.01h2.01V11Zm3 0H8v2.01h2.01V11Zm3 0H11v2.01h2.01V11Zm3 0H14v2.01h2.01V11Zm3 0H17v2.01h2.01V11Zm-12 3H5v2.01h2.01V14ZM8 14l-.001 2 8.011.01V14H8Zm11.01 0H17v2.01h2.01V14Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let className = 'w-4 h-4';
|
||||
export let strokeWidth = '2';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.712 4.33a9.027 9.027 0 0 1 1.652 1.306c.51.51.944 1.064 1.306 1.652M16.712 4.33l-3.448 4.138m3.448-4.138a9.014 9.014 0 0 0-9.424 0M19.67 7.288l-4.138 3.448m4.138-3.448a9.014 9.014 0 0 1 0 9.424m-4.138-5.976a3.736 3.736 0 0 0-.88-1.388 3.737 3.737 0 0 0-1.388-.88m2.268 2.268a3.765 3.765 0 0 1 0 2.528m-2.268-4.796a3.765 3.765 0 0 0-2.528 0m4.796 4.796c-.181.506-.475.982-.88 1.388a3.736 3.736 0 0 1-1.388.88m2.268-2.268 4.138 3.448m0 0a9.027 9.027 0 0 1-1.306 1.652c-.51.51-1.064.944-1.652 1.306m0 0-3.448-4.138m3.448 4.138a9.014 9.014 0 0 1-9.424 0m5.976-4.138a3.765 3.765 0 0 1-2.528 0m0 0a3.736 3.736 0 0 1-1.388-.88 3.737 3.737 0 0 1-.88-1.388m2.268 2.268L7.288 19.67m0 0a9.024 9.024 0 0 1-1.652-1.306 9.027 9.027 0 0 1-1.306-1.652m0 0 4.138-3.448M4.33 16.712a9.014 9.014 0 0 1 0-9.424m4.138 5.976a3.765 3.765 0 0 1 0-2.528m0 0c.181-.506.475-.982.88-1.388a3.736 3.736 0 0 1 1.388-.88m-2.268 2.268L4.33 7.288m6.406 1.18L7.288 4.33m0 0a9.024 9.024 0 0 0-1.652 1.306A9.025 9.025 0 0 0 4.33 7.288"
|
||||
/>
|
||||
</svg>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let className = 'w-4 h-4';
|
||||
export let strokeWidth = '2';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { onMount, tick, getContext } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import ShortcutsModal from '../chat/ShortcutsModal.svelte';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
import HelpMenu from './Help/HelpMenu.svelte';
|
||||
|
||||
let showShortcuts = false;
|
||||
</script>
|
||||
|
||||
<div class=" hidden lg:flex fixed bottom-0 right-0 px-2 py-2 z-10">
|
||||
<button
|
||||
id="show-shortcuts-button"
|
||||
class="hidden"
|
||||
on:click={() => {
|
||||
showShortcuts = !showShortcuts;
|
||||
}}
|
||||
/>
|
||||
|
||||
<HelpMenu
|
||||
showDocsHandler={() => {
|
||||
showShortcuts = !showShortcuts;
|
||||
}}
|
||||
showShortcutsHandler={() => {
|
||||
showShortcuts = !showShortcuts;
|
||||
}}
|
||||
>
|
||||
<Tooltip content={$i18n.t('Help')} placement="left">
|
||||
<button
|
||||
class="text-gray-600 dark:text-gray-300 bg-gray-300/20 size-5 flex items-center justify-center text-[0.7rem] rounded-full"
|
||||
>
|
||||
?
|
||||
</button>
|
||||
</Tooltip>
|
||||
</HelpMenu>
|
||||
</div>
|
||||
|
||||
<ShortcutsModal bind:show={showShortcuts} />
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
import { showSettings } from '$lib/stores';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import QuestionMarkCircle from '$lib/components/icons/QuestionMarkCircle.svelte';
|
||||
import Lifebuoy from '$lib/components/icons/Lifebuoy.svelte';
|
||||
import Keyboard from '$lib/components/icons/Keyboard.svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let showDocsHandler: Function;
|
||||
export let showShortcutsHandler: Function;
|
||||
|
||||
export let onClose: Function = () => {};
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
on:change={(e) => {
|
||||
if (e.detail === false) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<slot />
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[200px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
|
||||
sideOffset={4}
|
||||
side="top"
|
||||
align="end"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
id="chat-share-button"
|
||||
on:click={() => {
|
||||
window.open('https://docs.openwebui.com', '_blank');
|
||||
}}
|
||||
>
|
||||
<QuestionMarkCircle className="size-5" />
|
||||
<div class="flex items-center">{$i18n.t('Documentation')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
id="chat-share-button"
|
||||
on:click={() => {
|
||||
showShortcutsHandler();
|
||||
}}
|
||||
>
|
||||
<Keyboard className="size-5" />
|
||||
<div class="flex items-center">{$i18n.t('Keyboard shortcuts')}</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</div>
|
||||
</Dropdown>
|
||||
@@ -67,7 +67,7 @@
|
||||
<div class="self-start flex flex-none items-center text-gray-600 dark:text-gray-400">
|
||||
<!-- <div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> -->
|
||||
|
||||
{#if shareEnabled}
|
||||
{#if shareEnabled && chat && chat.id}
|
||||
<Menu
|
||||
{chat}
|
||||
{shareEnabled}
|
||||
|
||||
@@ -63,6 +63,13 @@
|
||||
// Revoke the URL to release memory
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const downloadJSONExport = async () => {
|
||||
let blob = new Blob([JSON.stringify([chat])], {
|
||||
type: 'application/json'
|
||||
});
|
||||
saveAs(blob, `chat-export-${Date.now()}.json`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
@@ -131,7 +138,6 @@
|
||||
</svg>
|
||||
<div class="flex items-center">{$i18n.t('Share')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<!-- <DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
|
||||
on:click={() => {
|
||||
@@ -164,6 +170,14 @@
|
||||
transition={flyAndScale}
|
||||
sideOffset={8}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
downloadJSONExport();
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</div>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import { getAdminDetails } from '$lib/apis/auths';
|
||||
import { onMount, tick, getContext } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let adminDetails = null;
|
||||
|
||||
onMount(async () => {
|
||||
adminDetails = await getAdminDetails(localStorage.token).catch((err) => {
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="fixed w-full h-full flex z-[999]">
|
||||
<div
|
||||
class="absolute w-full h-full backdrop-blur-lg bg-white/10 dark:bg-gray-900/50 flex justify-center"
|
||||
>
|
||||
<div class="m-auto pb-10 flex flex-col justify-center">
|
||||
<div class="max-w-md">
|
||||
<div class="text-center dark:text-white text-2xl font-medium z-50">
|
||||
{$i18n.t('Account Activation Pending')}<br />
|
||||
{$i18n.t('Contact Admin for WebUI Access')}
|
||||
</div>
|
||||
|
||||
<div class=" mt-4 text-center text-sm dark:text-gray-200 w-full">
|
||||
{$i18n.t('Your account status is currently pending activation.')}<br />
|
||||
{$i18n.t(
|
||||
'To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.'
|
||||
)}
|
||||
</div>
|
||||
|
||||
{#if adminDetails}
|
||||
<div class="mt-4 text-sm font-medium text-center">
|
||||
<div>{$i18n.t('Admin')}: {adminDetails.name} ({adminDetails.email})</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class=" mt-6 mx-auto relative group w-fit">
|
||||
<button
|
||||
class="relative z-20 flex px-5 py-2 rounded-full bg-white border border-gray-100 dark:border-none hover:bg-gray-100 text-gray-700 transition font-medium text-sm"
|
||||
on:click={async () => {
|
||||
location.href = '/';
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Check Again')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="text-xs text-center w-full mt-2 text-gray-400 underline"
|
||||
on:click={async () => {
|
||||
localStorage.removeItem('token');
|
||||
location.href = '/auth';
|
||||
}}>{$i18n.t('Sign Out')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -205,6 +205,10 @@
|
||||
await archiveChatById(localStorage.token, id);
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
};
|
||||
|
||||
const focusEdit = async (node: HTMLInputElement) => {
|
||||
node.focus();
|
||||
};
|
||||
</script>
|
||||
|
||||
<ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
|
||||
@@ -321,6 +325,10 @@
|
||||
on:click={() => {
|
||||
selectedChatId = null;
|
||||
chatId.set('');
|
||||
|
||||
if ($mobile) {
|
||||
showSidebar.set(false);
|
||||
}
|
||||
}}
|
||||
draggable="false"
|
||||
>
|
||||
@@ -489,7 +497,11 @@
|
||||
? 'bg-gray-100 dark:bg-gray-950'
|
||||
: 'group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
<input bind:value={chatTitle} class=" bg-transparent w-full outline-none mr-10" />
|
||||
<input
|
||||
use:focusEdit
|
||||
bind:value={chatTitle}
|
||||
class=" bg-transparent w-full outline-none mr-10"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<a
|
||||
@@ -507,6 +519,10 @@
|
||||
showSidebar.set(false);
|
||||
}
|
||||
}}
|
||||
on:dblclick={() => {
|
||||
chatTitle = chat.title;
|
||||
chatTitleEditId = chat.id;
|
||||
}}
|
||||
draggable="false"
|
||||
>
|
||||
<div class=" flex self-center flex-1 w-full">
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import { archiveChatById, deleteChatById, getArchivedChatList } from '$lib/apis/chats';
|
||||
import {
|
||||
archiveChatById,
|
||||
deleteChatById,
|
||||
getAllArchivedChats,
|
||||
getArchivedChatList
|
||||
} from '$lib/apis/chats';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
@@ -38,6 +43,7 @@
|
||||
};
|
||||
|
||||
const exportChatsHandler = async () => {
|
||||
const chats = await getAllArchivedChats(localStorage.token);
|
||||
let blob = new Blob([JSON.stringify(chats)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
import { createEventDispatcher, getContext } from 'svelte';
|
||||
import { createEventDispatcher, getContext, onMount } from 'svelte';
|
||||
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { goto } from '$app/navigation';
|
||||
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
||||
import { showSettings } from '$lib/stores';
|
||||
import { showSettings, activeUserCount, USAGE_POOL } from '$lib/stores';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -107,7 +108,7 @@
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<hr class=" dark:border-gray-800 my-2 p-0" />
|
||||
<hr class=" dark:border-gray-800 my-1.5 p-0" />
|
||||
|
||||
<button
|
||||
class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
|
||||
@@ -139,6 +140,36 @@
|
||||
<div class=" self-center font-medium">{$i18n.t('Sign Out')}</div>
|
||||
</button>
|
||||
|
||||
{#if $activeUserCount}
|
||||
<hr class=" dark:border-gray-800 my-1.5 p-0" />
|
||||
|
||||
<Tooltip
|
||||
content={$USAGE_POOL && $USAGE_POOL.length > 0
|
||||
? `${$i18n.t('Running')}: ${$USAGE_POOL.join(', ')} ✨`
|
||||
: ''}
|
||||
>
|
||||
<div class="flex rounded-md py-1.5 px-3 text-xs gap-2.5 items-center">
|
||||
<div class=" flex items-center">
|
||||
<span class="relative flex size-2">
|
||||
<span
|
||||
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
|
||||
/>
|
||||
<span class="relative inline-flex rounded-full size-2 bg-green-500" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class=" ">
|
||||
<span class=" font-medium">
|
||||
{$i18n.t('Active Users')}:
|
||||
</span>
|
||||
<span class=" font-semibold">
|
||||
{$activeUserCount}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<!-- <DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
|
||||
<div class="flex items-center">Profile</div>
|
||||
</DropdownMenu.Item> -->
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { WEBUI_NAME, documents } from '$lib/stores';
|
||||
import { WEBUI_NAME, documents, showSidebar } from '$lib/stores';
|
||||
import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents';
|
||||
|
||||
import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants';
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
import EditDocModal from '$lib/components/documents/EditDocModal.svelte';
|
||||
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
|
||||
import SettingsModal from '$lib/components/documents/SettingsModal.svelte';
|
||||
import AddDocModal from '$lib/components/documents/AddDocModal.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
@@ -158,12 +157,14 @@
|
||||
|
||||
{#if dragged}
|
||||
<div
|
||||
class="fixed w-full h-full flex z-50 touch-none pointer-events-none"
|
||||
class="fixed {$showSidebar
|
||||
? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]'
|
||||
: 'left-0'} w-full h-full flex z-50 touch-none pointer-events-none"
|
||||
id="dropzone"
|
||||
role="region"
|
||||
aria-label="Drag and Drop Container"
|
||||
>
|
||||
<div class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
|
||||
<div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
|
||||
<div class="m-auto pt-64 flex flex-col justify-center">
|
||||
<div class="max-w-md">
|
||||
<AddFilesPlaceholder>
|
||||
@@ -183,39 +184,9 @@
|
||||
|
||||
<AddDocModal bind:show={showAddDocModal} />
|
||||
|
||||
<SettingsModal bind:show={showSettingsModal} />
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class=" text-lg font-semibold self-center">{$i18n.t('Documents')}</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
class="flex items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showSettingsModal = !showSettingsModal;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div class=" text-xs">{$i18n.t('Document Settings')}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" text-gray-500 text-xs mt-1">
|
||||
ⓘ {$i18n.t("Use '#' in the prompt input to load and select your documents.")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -520,6 +491,10 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class=" text-gray-500 text-xs mt-1">
|
||||
ⓘ {$i18n.t("Use '#' in the prompt input to load and select your documents.")}
|
||||
</div>
|
||||
|
||||
<div class=" flex justify-end w-full mb-2">
|
||||
<div class="flex space-x-2">
|
||||
<input
|
||||
|
||||
@@ -71,16 +71,19 @@
|
||||
const url = 'https://openwebui.com';
|
||||
|
||||
const tab = await window.open(`${url}/models/create`, '_blank');
|
||||
window.addEventListener(
|
||||
'message',
|
||||
(event) => {
|
||||
if (event.origin !== url) return;
|
||||
if (event.data === 'loaded') {
|
||||
tab.postMessage(JSON.stringify(model), '*');
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// Define the event handler function
|
||||
const messageHandler = (event) => {
|
||||
if (event.origin !== url) return;
|
||||
if (event.data === 'loaded') {
|
||||
tab.postMessage(JSON.stringify(model), '*');
|
||||
|
||||
// Remove the event listener after handling the message
|
||||
window.removeEventListener('message', messageHandler);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageHandler, false);
|
||||
};
|
||||
|
||||
const hideModelHandler = async (model) => {
|
||||
@@ -384,6 +387,7 @@
|
||||
}
|
||||
|
||||
await models.set(await getModels(localStorage.token));
|
||||
_models = $models;
|
||||
};
|
||||
|
||||
reader.readAsText(importFiles[0]);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import Selector from './Knowledge/Selector.svelte';
|
||||
|
||||
export let knowledge = [];
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="flex w-full justify-between mb-1">
|
||||
<div class=" self-center text-sm font-semibold">{$i18n.t('Knowledge')}</div>
|
||||
</div>
|
||||
|
||||
<div class=" text-xs dark:text-gray-500">
|
||||
To add documents here, upload them to the "Documents" workspace first.
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
{#if knowledge.length > 0}
|
||||
<div class=" flex items-center gap-2 mt-2">
|
||||
{#each knowledge as file, fileIdx}
|
||||
<div class=" relative group">
|
||||
<div
|
||||
class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none"
|
||||
>
|
||||
<div class="p-2.5 bg-red-400 text-white rounded-lg">
|
||||
{#if (file?.type ?? 'doc') === 'doc'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
|
||||
/>
|
||||
</svg>
|
||||
{:else if file.type === 'collection'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-6"
|
||||
>
|
||||
<path
|
||||
d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z"
|
||||
/>
|
||||
<path
|
||||
d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-center -space-y-0.5">
|
||||
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
|
||||
{file?.title ?? `#${file.name}`}
|
||||
</div>
|
||||
|
||||
<div class=" text-gray-500 text-sm">{$i18n.t(file?.type ?? 'Document')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" absolute -top-1 -right-1">
|
||||
<button
|
||||
class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
knowledge.splice(fileIdx, 1);
|
||||
knowledge = knowledge;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-wrap text-sm font-medium gap-1.5 mt-2">
|
||||
<Selector bind:knowledge>
|
||||
<button
|
||||
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl"
|
||||
type="button">Select Documents</button
|
||||
>
|
||||
</Selector>
|
||||
</div>
|
||||
<!-- {knowledge} -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,138 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
|
||||
import { documents } from '$lib/stores';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let onClose: Function = () => {};
|
||||
|
||||
export let knowledge = [];
|
||||
|
||||
let items = [];
|
||||
|
||||
onMount(() => {
|
||||
let collections = [
|
||||
...($documents.length > 0
|
||||
? [
|
||||
{
|
||||
name: 'All Documents',
|
||||
type: 'collection',
|
||||
title: $i18n.t('All Documents'),
|
||||
collection_names: $documents.map((doc) => doc.collection_name)
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...$documents
|
||||
.reduce((a, e, i, arr) => {
|
||||
return [...new Set([...a, ...(e?.content?.tags ?? []).map((tag) => tag.name)])];
|
||||
}, [])
|
||||
.map((tag) => ({
|
||||
name: tag,
|
||||
type: 'collection',
|
||||
collection_names: $documents
|
||||
.filter((doc) => (doc?.content?.tags ?? []).map((tag) => tag.name).includes(tag))
|
||||
.map((doc) => doc.collection_name)
|
||||
}))
|
||||
];
|
||||
|
||||
items = [...collections, ...$documents];
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
on:change={(e) => {
|
||||
if (e.detail === false) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<slot />
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[300px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
|
||||
sideOffset={8}
|
||||
side="bottom"
|
||||
align="start"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
<div class="max-h-[10rem] overflow-y-scroll">
|
||||
{#if items.length === 0}
|
||||
<div class="text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
{$i18n.t('No documents found')}
|
||||
</div>
|
||||
{:else}
|
||||
{#each items as item}
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2.5 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
if (!knowledge.find((k) => k.name === item.name)) {
|
||||
knowledge = [
|
||||
...knowledge,
|
||||
{
|
||||
...item,
|
||||
type: item?.type ?? 'doc'
|
||||
}
|
||||
];
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex self-start">
|
||||
{#if (item?.type ?? 'doc') === 'doc'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
|
||||
/>
|
||||
</svg>
|
||||
{:else if item.type === 'collection'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z"
|
||||
/>
|
||||
<path
|
||||
d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
class=" w-fit text-xs font-black px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
{item?.type ?? 'Document'}
|
||||
</div>
|
||||
|
||||
<div class="line-clamp-1 font-medium pr-0.5">
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</DropdownMenu.Content>
|
||||
</div>
|
||||
</Dropdown>
|
||||
@@ -80,8 +80,6 @@
|
||||
if (stopResponseFlag) {
|
||||
controller.abort('User: Stop Response');
|
||||
}
|
||||
|
||||
currentRequestId = null;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -97,11 +95,7 @@
|
||||
let data = JSON.parse(line.replace(/^data: /, ''));
|
||||
console.log(data);
|
||||
|
||||
if ('request_id' in data) {
|
||||
currentRequestId = data.request_id;
|
||||
} else {
|
||||
text += data.choices[0].delta.content ?? '';
|
||||
}
|
||||
text += data.choices[0].delta.content ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,21 +172,17 @@
|
||||
let data = JSON.parse(line.replace(/^data: /, ''));
|
||||
console.log(data);
|
||||
|
||||
if ('request_id' in data) {
|
||||
currentRequestId = data.request_id;
|
||||
if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
|
||||
continue;
|
||||
} else {
|
||||
if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
|
||||
continue;
|
||||
} else {
|
||||
textareaElement.style.height = textareaElement.scrollHeight + 'px';
|
||||
textareaElement.style.height = textareaElement.scrollHeight + 'px';
|
||||
|
||||
responseMessage.content += data.choices[0].delta.content ?? '';
|
||||
messages = messages;
|
||||
responseMessage.content += data.choices[0].delta.content ?? '';
|
||||
messages = messages;
|
||||
|
||||
textareaElement.style.height = textareaElement.scrollHeight + 'px';
|
||||
textareaElement.style.height = textareaElement.scrollHeight + 'px';
|
||||
|
||||
await tick();
|
||||
}
|
||||
await tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ import { browser, dev } from '$app/environment';
|
||||
// import { version } from '../../package.json';
|
||||
|
||||
export const APP_NAME = 'Open WebUI';
|
||||
export const WEBUI_BASE_URL = browser ? (dev ? `http://${location.hostname}:8080` : ``) : ``;
|
||||
|
||||
export const WEBUI_HOSTNAME = browser ? (dev ? `${location.hostname}:8080` : ``) : '';
|
||||
export const WEBUI_BASE_URL = browser ? (dev ? `http://${WEBUI_HOSTNAME}` : ``) : ``;
|
||||
export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
|
||||
|
||||
export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama`;
|
||||
@@ -88,7 +89,8 @@ export const SUPPORTED_FILE_EXTENSIONS = [
|
||||
'xls',
|
||||
'xlsx',
|
||||
'pptx',
|
||||
'ppt'
|
||||
'ppt',
|
||||
'msg'
|
||||
];
|
||||
|
||||
// Source: https://kit.svelte.dev/docs/modules#$env-static-public
|
||||
|
||||
@@ -3,19 +3,21 @@
|
||||
"(Beta)": "(تجريبي)",
|
||||
"(e.g. `sh webui.sh --api`)": "( `sh webui.sh --api`مثال)",
|
||||
"(latest)": "(الأخير)",
|
||||
"{{ models }}": "",
|
||||
"{{ owner }}: You cannot delete a base model": "",
|
||||
"{{ models }}": "{{ نماذج }}",
|
||||
"{{ owner }}: You cannot delete a base model": "{{ المالك }}: لا يمكنك حذف نموذج أساسي",
|
||||
"{{modelName}} is thinking...": "{{modelName}} ...يفكر",
|
||||
"{{user}}'s Chats": "دردشات {{user}}",
|
||||
"{{webUIName}} Backend Required": "{{webUIName}} مطلوب",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "يتم استخدام نموذج المهمة عند تنفيذ مهام مثل إنشاء عناوين للدردشات واستعلامات بحث الويب",
|
||||
"a user": "مستخدم",
|
||||
"About": "عن",
|
||||
"Account": "الحساب",
|
||||
"Account Activation Pending": "",
|
||||
"Accurate information": "معلومات دقيقة",
|
||||
"Active Users": "",
|
||||
"Add": "أضف",
|
||||
"Add a model id": "",
|
||||
"Add a short description about what this model does": "",
|
||||
"Add a model id": "إضافة معرف نموذج",
|
||||
"Add a short description about what this model does": "أضف وصفا موجزا حول ما يفعله هذا النموذج",
|
||||
"Add a short title for this prompt": "أضف عنوانًا قصيرًا لبداء المحادثة",
|
||||
"Add a tag": "أضافة تاق",
|
||||
"Add custom prompt": "أضافة مطالبة مخصصه",
|
||||
@@ -28,15 +30,17 @@
|
||||
"Add User": "اضافة مستخدم",
|
||||
"Adjusting these settings will apply changes universally to all users.": "سيؤدي ضبط هذه الإعدادات إلى تطبيق التغييرات بشكل عام على كافة المستخدمين",
|
||||
"admin": "المشرف",
|
||||
"Admin": "",
|
||||
"Admin Panel": "لوحة التحكم",
|
||||
"Admin Settings": "اعدادات المشرف",
|
||||
"Advanced Parameters": "التعليمات المتقدمة",
|
||||
"Advanced Params": "",
|
||||
"Advanced Params": "المعلمات المتقدمة",
|
||||
"all": "الكل",
|
||||
"All Documents": "جميع الملفات",
|
||||
"All Users": "جميع المستخدمين",
|
||||
"Allow": "يسمح",
|
||||
"Allow Chat Deletion": "يستطيع حذف المحادثات",
|
||||
"Allow non-local voices": "",
|
||||
"alphanumeric characters and hyphens": "الأحرف الأبجدية الرقمية والواصلات",
|
||||
"Already have an account?": "هل تملك حساب ؟",
|
||||
"an assistant": "مساعد",
|
||||
@@ -48,7 +52,7 @@
|
||||
"API keys": "مفاتيح واجهة برمجة التطبيقات",
|
||||
"April": "أبريل",
|
||||
"Archive": "الأرشيف",
|
||||
"Archive All Chats": "",
|
||||
"Archive All Chats": "أرشفة جميع الدردشات",
|
||||
"Archived Chats": "الأرشيف المحادثات",
|
||||
"are allowed - Activate this command by typing": "مسموح - قم بتنشيط هذا الأمر عن طريق الكتابة",
|
||||
"Are you sure?": "هل أنت متأكد ؟",
|
||||
@@ -57,20 +61,22 @@
|
||||
"Audio": "صوتي",
|
||||
"August": "أغسطس",
|
||||
"Auto-playback response": "استجابة التشغيل التلقائي",
|
||||
"Auto-send input after 3 sec.": "إرسال تلقائي للإدخال بعد 3 ثواني.",
|
||||
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 الرابط الرئيسي",
|
||||
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 الرابط مطلوب",
|
||||
"available!": "متاح",
|
||||
"Back": "خلف",
|
||||
"Bad Response": "استجابة خطاء",
|
||||
"Banners": "",
|
||||
"Base Model (From)": "",
|
||||
"Banners": "لافتات",
|
||||
"Base Model (From)": "النموذج الأساسي (من)",
|
||||
"before": "قبل",
|
||||
"Being lazy": "كون كسول",
|
||||
"Brave Search API Key": "",
|
||||
"Brave Search API Key": "مفتاح واجهة برمجة تطبيقات البحث الشجاع",
|
||||
"Bypass SSL verification for Websites": "تجاوز التحقق من SSL للموقع",
|
||||
"Call": "",
|
||||
"Call feature is not supported when using Web STT engine": "",
|
||||
"Camera": "",
|
||||
"Cancel": "اللغاء",
|
||||
"Capabilities": "",
|
||||
"Capabilities": "قدرات",
|
||||
"Change Password": "تغير الباسورد",
|
||||
"Chat": "المحادثة",
|
||||
"Chat Bubble UI": "UI الدردشة",
|
||||
@@ -86,35 +92,36 @@
|
||||
"Chunk Params": "Chunk المتغيرات",
|
||||
"Chunk Size": "Chunk حجم",
|
||||
"Citation": "اقتباس",
|
||||
"Clear memory": "",
|
||||
"Click here for help.": "أضغط هنا للمساعدة",
|
||||
"Click here to": "أضغط هنا الانتقال",
|
||||
"Click here to select": "أضغط هنا للاختيار",
|
||||
"Click here to select a csv file.": "أضغط هنا للاختيار ملف csv",
|
||||
"Click here to select a py file.": "",
|
||||
"Click here to select documents.": "انقر هنا لاختيار المستندات",
|
||||
"click here.": "أضغط هنا",
|
||||
"Click on the user role button to change a user's role.": "أضغط على أسم الصلاحيات لتغيرها للمستخدم",
|
||||
"Clone": "",
|
||||
"Clone": "استنساخ",
|
||||
"Close": "أغلق",
|
||||
"Collection": "مجموعة",
|
||||
"ComfyUI": "ComfyUI",
|
||||
"ComfyUI Base URL": "ComfyUI الرابط الافتراضي",
|
||||
"ComfyUI Base URL is required.": "ComfyUI الرابط مطلوب",
|
||||
"Command": "الأوامر",
|
||||
"Concurrent Requests": "",
|
||||
"Concurrent Requests": "الطلبات المتزامنة",
|
||||
"Confirm Password": "تأكيد كلمة المرور",
|
||||
"Connections": "اتصالات",
|
||||
"Contact Admin for WebUI Access": "",
|
||||
"Content": "الاتصال",
|
||||
"Context Length": "طول السياق",
|
||||
"Continue Response": "متابعة الرد",
|
||||
"Conversation Mode": "وضع المحادثة",
|
||||
"Copied shared chat URL to clipboard!": "تم نسخ عنوان URL للدردشة المشتركة إلى الحافظة",
|
||||
"Copy": "نسخ",
|
||||
"Copy last code block": "انسخ كتلة التعليمات البرمجية الأخيرة",
|
||||
"Copy last response": "انسخ الرد الأخير",
|
||||
"Copy Link": "أنسخ الرابط",
|
||||
"Copying to clipboard was successful!": "تم النسخ إلى الحافظة بنجاح",
|
||||
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "قم بإنشاء عبارة موجزة مكونة من 3-5 كلمات كرأس للاستعلام التالي، مع الالتزام الصارم بالحد الأقصى لعدد الكلمات الذي يتراوح بين 3-5 كلمات وتجنب استخدام الكلمة 'عنوان':",
|
||||
"Create a model": "",
|
||||
"Create a model": "إنشاء نموذج",
|
||||
"Create Account": "إنشاء حساب",
|
||||
"Create new key": "عمل مفتاح جديد",
|
||||
"Create new secret key": "عمل سر جديد",
|
||||
@@ -123,38 +130,39 @@
|
||||
"Current Model": "الموديل المختار",
|
||||
"Current Password": "كلمة السر الحالية",
|
||||
"Custom": "مخصص",
|
||||
"Customize models for a specific purpose": "",
|
||||
"Customize models for a specific purpose": "تخصيص النماذج لغرض معين",
|
||||
"Dark": "مظلم",
|
||||
"Dashboard": "",
|
||||
"Database": "قاعدة البيانات",
|
||||
"December": "ديسمبر",
|
||||
"Default": "الإفتراضي",
|
||||
"Default (Automatic1111)": "(Automatic1111) الإفتراضي",
|
||||
"Default (SentenceTransformers)": "(SentenceTransformers) الإفتراضي",
|
||||
"Default (Web API)": "(Web API) الإفتراضي",
|
||||
"Default Model": "",
|
||||
"Default Model": "النموذج الافتراضي",
|
||||
"Default model updated": "الإفتراضي تحديث الموديل",
|
||||
"Default Prompt Suggestions": "الإفتراضي Prompt الاقتراحات",
|
||||
"Default User Role": "الإفتراضي صلاحيات المستخدم",
|
||||
"delete": "حذف",
|
||||
"Delete": "حذف",
|
||||
"Delete a model": "حذف الموديل",
|
||||
"Delete All Chats": "",
|
||||
"Delete All Chats": "حذف جميع الدردشات",
|
||||
"Delete chat": "حذف المحادثه",
|
||||
"Delete Chat": "حذف المحادثه.",
|
||||
"delete this link": "أحذف هذا الرابط",
|
||||
"Delete User": "حذف المستخدم",
|
||||
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} حذف",
|
||||
"Deleted {{name}}": "",
|
||||
"Deleted {{name}}": "حذف {{name}}",
|
||||
"Description": "وصف",
|
||||
"Didn't fully follow instructions": "لم أتبع التعليمات بشكل كامل",
|
||||
"Disabled": "تعطيل",
|
||||
"Discover a model": "",
|
||||
"Discover a model": "اكتشف نموذجا",
|
||||
"Discover a prompt": "اكتشاف موجه",
|
||||
"Discover, download, and explore custom prompts": "اكتشاف وتنزيل واستكشاف المطالبات المخصصة",
|
||||
"Discover, download, and explore model presets": "اكتشاف وتنزيل واستكشاف الإعدادات المسبقة للنموذج",
|
||||
"Dismissible": "",
|
||||
"Display the username instead of You in the Chat": "اعرض اسم المستخدم بدلاً منك في الدردشة",
|
||||
"Document": "المستند",
|
||||
"Document Settings": "أعدادات المستند",
|
||||
"Documentation": "",
|
||||
"Documents": "مستندات",
|
||||
"does not make any external connections, and your data stays securely on your locally hosted server.": "لا يجري أي اتصالات خارجية، وتظل بياناتك آمنة على الخادم المستضاف محليًا.",
|
||||
"Don't Allow": "لا تسمح بذلك",
|
||||
@@ -169,31 +177,31 @@
|
||||
"Edit Doc": "تعديل الملف",
|
||||
"Edit User": "تعديل المستخدم",
|
||||
"Email": "البريد",
|
||||
"Embedding Batch Size": "",
|
||||
"Embedding Model": "نموذج التضمين",
|
||||
"Embedding Model Engine": "تضمين محرك النموذج",
|
||||
"Embedding model set to \"{{embedding_model}}\"": "تم تعيين نموذج التضمين على \"{{embedding_model}}\"",
|
||||
"Enable Chat History": "تمكين سجل الدردشة",
|
||||
"Enable Community Sharing": "",
|
||||
"Enable Community Sharing": "تمكين مشاركة المجتمع",
|
||||
"Enable New Sign Ups": "تفعيل عمليات التسجيل الجديدة",
|
||||
"Enable Web Search": "",
|
||||
"Enabled": "تفعيل",
|
||||
"Enable Web Search": "تمكين بحث الويب",
|
||||
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "تأكد من أن ملف CSV الخاص بك يتضمن 4 أعمدة بهذا الترتيب: Name, Email, Password, Role.",
|
||||
"Enter {{role}} message here": "أدخل رسالة {{role}} هنا",
|
||||
"Enter a detail about yourself for your LLMs to recall": "ادخل معلومات عنك تريد أن يتذكرها الموديل",
|
||||
"Enter Brave Search API Key": "",
|
||||
"Enter Brave Search API Key": "أدخل مفتاح واجهة برمجة تطبيقات البحث الشجاع",
|
||||
"Enter Chunk Overlap": "أدخل الChunk Overlap",
|
||||
"Enter Chunk Size": "أدخل Chunk الحجم",
|
||||
"Enter Github Raw URL": "",
|
||||
"Enter Google PSE API Key": "",
|
||||
"Enter Google PSE Engine Id": "",
|
||||
"Enter Github Raw URL": "أدخل عنوان URL ل Github Raw",
|
||||
"Enter Google PSE API Key": "أدخل مفتاح واجهة برمجة تطبيقات PSE من Google",
|
||||
"Enter Google PSE Engine Id": "أدخل معرف محرك PSE من Google",
|
||||
"Enter Image Size (e.g. 512x512)": "(e.g. 512x512) أدخل حجم الصورة ",
|
||||
"Enter language codes": "أدخل كود اللغة",
|
||||
"Enter model tag (e.g. {{modelTag}})": "(e.g. {{modelTag}}) أدخل الموديل تاق",
|
||||
"Enter Number of Steps (e.g. 50)": "(e.g. 50) أدخل عدد الخطوات",
|
||||
"Enter Score": "أدخل النتيجة",
|
||||
"Enter Searxng Query URL": "",
|
||||
"Enter Serper API Key": "",
|
||||
"Enter Serpstack API Key": "",
|
||||
"Enter Searxng Query URL": "أدخل عنوان URL لاستعلام Searxng",
|
||||
"Enter Serper API Key": "أدخل مفتاح واجهة برمجة تطبيقات Serper",
|
||||
"Enter Serpstack API Key": "أدخل مفتاح واجهة برمجة تطبيقات Serpstack",
|
||||
"Enter stop sequence": "أدخل تسلسل التوقف",
|
||||
"Enter Top K": "أدخل Top K",
|
||||
"Enter URL (e.g. http://127.0.0.1:7860/)": "الرابط (e.g. http://127.0.0.1:7860/)",
|
||||
@@ -202,16 +210,19 @@
|
||||
"Enter Your Full Name": "أدخل الاسم كامل",
|
||||
"Enter Your Password": "ادخل كلمة المرور",
|
||||
"Enter Your Role": "أدخل الصلاحيات",
|
||||
"Error": "",
|
||||
"Error": "خطأ",
|
||||
"Experimental": "تجريبي",
|
||||
"Export": "",
|
||||
"Export": "تصدير",
|
||||
"Export All Chats (All Users)": "تصدير جميع الدردشات (جميع المستخدمين)",
|
||||
"Export chat (.json)": "",
|
||||
"Export Chats": "تصدير جميع الدردشات",
|
||||
"Export Documents Mapping": "تصدير وثائق الخرائط",
|
||||
"Export Models": "",
|
||||
"Export Models": "نماذج التصدير",
|
||||
"Export Prompts": "مطالبات التصدير",
|
||||
"External Models": "",
|
||||
"Failed to create API Key.": "فشل في إنشاء مفتاح API.",
|
||||
"Failed to read clipboard contents": "فشل في قراءة محتويات الحافظة",
|
||||
"Failed to update settings": "",
|
||||
"February": "فبراير",
|
||||
"Feel free to add specific details": "لا تتردد في إضافة تفاصيل محددة",
|
||||
"File Mode": "وضع الملف",
|
||||
@@ -221,15 +232,15 @@
|
||||
"Focus chat input": "التركيز على إدخال الدردشة",
|
||||
"Followed instructions perfectly": "اتبعت التعليمات على أكمل وجه",
|
||||
"Format your variables using square brackets like this:": "قم بتنسيق المتغيرات الخاصة بك باستخدام الأقواس المربعة مثل هذا:",
|
||||
"Frequency Penalty": "",
|
||||
"Full Screen Mode": "وضع ملء الشاشة",
|
||||
"Frequency Penalty": "عقوبة التردد",
|
||||
"General": "عام",
|
||||
"General Settings": "الاعدادات العامة",
|
||||
"Generating search query": "",
|
||||
"Generate Image": "",
|
||||
"Generating search query": "إنشاء استعلام بحث",
|
||||
"Generation Info": "معلومات الجيل",
|
||||
"Good Response": "استجابة جيدة",
|
||||
"Google PSE API Key": "",
|
||||
"Google PSE Engine Id": "",
|
||||
"Google PSE API Key": "مفتاح واجهة برمجة تطبيقات PSE من Google",
|
||||
"Google PSE Engine Id": "معرف محرك PSE من Google",
|
||||
"h:mm a": "الساعة:الدقائق صباحا/مساء",
|
||||
"has no conversations.": "ليس لديه محادثات.",
|
||||
"Hello, {{name}}": " {{name}} مرحبا",
|
||||
@@ -243,40 +254,44 @@
|
||||
"Images": "الصور",
|
||||
"Import Chats": "استيراد الدردشات",
|
||||
"Import Documents Mapping": "استيراد خرائط المستندات",
|
||||
"Import Models": "",
|
||||
"Import Models": "استيراد النماذج",
|
||||
"Import Prompts": "مطالبات الاستيراد",
|
||||
"Include `--api` flag when running stable-diffusion-webui": "قم بتضمين علامة `-api` عند تشغيل Stable-diffusion-webui",
|
||||
"Info": "",
|
||||
"Info": "معلومات",
|
||||
"Input commands": "إدخال الأوامر",
|
||||
"Install from Github URL": "",
|
||||
"Install from Github URL": "التثبيت من عنوان URL لجيثب",
|
||||
"Instant Auto-Send After Voice Transcription": "",
|
||||
"Interface": "واجهه المستخدم",
|
||||
"Invalid Tag": "تاق غير صالحة",
|
||||
"January": "يناير",
|
||||
"join our Discord for help.": "انضم إلى Discord للحصول على المساعدة.",
|
||||
"JSON": "JSON",
|
||||
"JSON Preview": "",
|
||||
"JSON Preview": "معاينة JSON",
|
||||
"July": "يوليو",
|
||||
"June": "يونيو",
|
||||
"JWT Expiration": "JWT تجريبي",
|
||||
"JWT Token": "JWT Token",
|
||||
"Keep Alive": "Keep Alive",
|
||||
"Keyboard shortcuts": "اختصارات لوحة المفاتيح",
|
||||
"Knowledge": "",
|
||||
"Language": "اللغة",
|
||||
"Last Active": "آخر نشاط",
|
||||
"Light": "فاتح",
|
||||
"Listening...": "جاري الاستماع",
|
||||
"Listening...": "",
|
||||
"LLMs can make mistakes. Verify important information.": "يمكن أن تصدر بعض الأخطاء. لذلك يجب التحقق من المعلومات المهمة",
|
||||
"Local Models": "",
|
||||
"LTR": "من جهة اليسار إلى اليمين",
|
||||
"Made by OpenWebUI Community": "OpenWebUI تم إنشاؤه بواسطة مجتمع ",
|
||||
"Make sure to enclose them with": "تأكد من إرفاقها",
|
||||
"Manage": "",
|
||||
"Manage Models": "إدارة النماذج",
|
||||
"Manage Ollama Models": "Ollama إدارة موديلات ",
|
||||
"Manage Pipelines": "",
|
||||
"Manage Pipelines": "إدارة خطوط الأنابيب",
|
||||
"March": "مارس",
|
||||
"Max Tokens (num_predict)": "",
|
||||
"Max Tokens (num_predict)": "ماكس توكنز (num_predict)",
|
||||
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "يمكن تنزيل 3 نماذج كحد أقصى في وقت واحد. الرجاء معاودة المحاولة في وقت لاحق.",
|
||||
"May": "مايو",
|
||||
"Memories accessible by LLMs will be shown here.": "",
|
||||
"Memories accessible by LLMs will be shown here.": "سيتم عرض الذكريات التي يمكن الوصول إليها بواسطة LLMs هنا.",
|
||||
"Memory": "الذاكرة",
|
||||
"Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "لن تتم مشاركة الرسائل التي ترسلها بعد إنشاء الرابط الخاص بك. سيتمكن المستخدمون الذين لديهم عنوان URL من عرض الدردشة المشتركة",
|
||||
"Minimum Score": "الحد الأدنى من النقاط",
|
||||
@@ -288,12 +303,12 @@
|
||||
"Model '{{modelName}}' has been successfully downloaded.": "تم تحميل النموذج '{{modelName}}' بنجاح",
|
||||
"Model '{{modelTag}}' is already in queue for downloading.": "النموذج '{{modelTag}}' موجود بالفعل في قائمة الانتظار للتحميل",
|
||||
"Model {{modelId}} not found": "لم يتم العثور على النموذج {{modelId}}.",
|
||||
"Model {{modelName}} is not vision capable": "",
|
||||
"Model {{name}} is now {{status}}": "",
|
||||
"Model {{modelName}} is not vision capable": "نموذج {{modelName}} غير قادر على الرؤية",
|
||||
"Model {{name}} is now {{status}}": "نموذج {{name}} هو الآن {{status}}",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "تم اكتشاف مسار نظام الملفات النموذجي. الاسم المختصر للنموذج مطلوب للتحديث، ولا يمكن الاستمرار.",
|
||||
"Model ID": "",
|
||||
"Model ID": "رقم الموديل",
|
||||
"Model not selected": "لم تختار موديل",
|
||||
"Model Params": "",
|
||||
"Model Params": "معلمات النموذج",
|
||||
"Model Whitelisting": "القائمة البيضاء للموديل",
|
||||
"Model(s) Whitelisted": "القائمة البيضاء الموديل",
|
||||
"Modelfile Content": "محتوى الملف النموذجي",
|
||||
@@ -301,25 +316,27 @@
|
||||
"More": "المزيد",
|
||||
"Name": "الأسم",
|
||||
"Name Tag": "أسم التاق",
|
||||
"Name your model": "",
|
||||
"Name your model": "قم بتسمية النموذج الخاص بك",
|
||||
"New Chat": "دردشة جديدة",
|
||||
"New Password": "كلمة المرور الجديدة",
|
||||
"No documents found": "",
|
||||
"No results found": "لا توجد نتايج",
|
||||
"No search query generated": "",
|
||||
"No search query generated": "لم يتم إنشاء استعلام بحث",
|
||||
"No source available": "لا يوجد مصدر متاح",
|
||||
"None": "",
|
||||
"None": "اي",
|
||||
"Not factually correct": "ليس صحيحا من حيث الواقع",
|
||||
"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "ملاحظة: إذا قمت بتعيين الحد الأدنى من النقاط، فلن يؤدي البحث إلا إلى إرجاع المستندات التي لها نقاط أكبر من أو تساوي الحد الأدنى من النقاط.",
|
||||
"Notifications": "إشعارات",
|
||||
"November": "نوفمبر",
|
||||
"num_thread (Ollama)": "",
|
||||
"num_thread (Ollama)": "num_thread (أولاما)",
|
||||
"October": "اكتوبر",
|
||||
"Off": "أغلاق",
|
||||
"Okay, Let's Go!": "حسنا دعنا نذهب!",
|
||||
"OLED Dark": "OLED داكن",
|
||||
"Ollama": "Ollama",
|
||||
"Ollama API": "",
|
||||
"Ollama API disabled": "",
|
||||
"Ollama API": "أولاما API",
|
||||
"Ollama API disabled": "أولاما API معطلة",
|
||||
"Ollama API is disabled": "",
|
||||
"Ollama Version": "Ollama الاصدار",
|
||||
"On": "تشغيل",
|
||||
"Only": "فقط",
|
||||
@@ -342,10 +359,12 @@
|
||||
"PDF document (.pdf)": "PDF ملف (.pdf)",
|
||||
"PDF Extract Images (OCR)": "PDF أستخرج الصور (OCR)",
|
||||
"pending": "قيد الانتظار",
|
||||
"Permission denied when accessing media devices": "",
|
||||
"Permission denied when accessing microphone": "",
|
||||
"Permission denied when accessing microphone: {{error}}": "{{error}} تم رفض الإذن عند الوصول إلى الميكروفون ",
|
||||
"Personalization": "التخصيص",
|
||||
"Pipelines": "",
|
||||
"Pipelines Valves": "",
|
||||
"Pipelines": "خطوط الانابيب",
|
||||
"Pipelines Valves": "صمامات خطوط الأنابيب",
|
||||
"Plain text (.txt)": "نص عادي (.txt)",
|
||||
"Playground": "مكان التجربة",
|
||||
"Positive attitude": "موقف ايجابي",
|
||||
@@ -364,6 +383,7 @@
|
||||
"Read Aloud": "أقراء لي",
|
||||
"Record voice": "سجل صوت",
|
||||
"Redirecting you to OpenWebUI Community": "OpenWebUI إعادة توجيهك إلى مجتمع ",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"Refused when it shouldn't have": "رفض عندما لا ينبغي أن يكون",
|
||||
"Regenerate": "تجديد",
|
||||
"Release Notes": "ملاحظات الإصدار",
|
||||
@@ -375,12 +395,14 @@
|
||||
"Reranking Model": "إعادة تقييم النموذج",
|
||||
"Reranking model disabled": "تم تعطيل نموذج إعادة الترتيب",
|
||||
"Reranking model set to \"{{reranking_model}}\"": "تم ضبط نموذج إعادة الترتيب على \"{{reranking_model}}\"",
|
||||
"Reset Upload Directory": "",
|
||||
"Reset Vector Storage": "إعادة تعيين تخزين المتجهات",
|
||||
"Response AutoCopy to Clipboard": "النسخ التلقائي للاستجابة إلى الحافظة",
|
||||
"Role": "منصب",
|
||||
"Rosé Pine": "Rosé Pine",
|
||||
"Rosé Pine Dawn": "Rosé Pine Dawn",
|
||||
"RTL": "من اليمين إلى اليسار",
|
||||
"Running": "",
|
||||
"Save": "حفظ",
|
||||
"Save & Create": "حفظ وإنشاء",
|
||||
"Save & Update": "حفظ وتحديث",
|
||||
@@ -390,53 +412,58 @@
|
||||
"Scan for documents from {{path}}": "{{path}} مسح على الملفات من",
|
||||
"Search": "البحث",
|
||||
"Search a model": "البحث عن موديل",
|
||||
"Search Chats": "",
|
||||
"Search Chats": "البحث في الدردشات",
|
||||
"Search Documents": "البحث المستندات",
|
||||
"Search Models": "",
|
||||
"Search Models": "نماذج البحث",
|
||||
"Search Prompts": "أبحث حث",
|
||||
"Search Result Count": "",
|
||||
"Searched {{count}} sites_zero": "",
|
||||
"Searched {{count}} sites_one": "",
|
||||
"Searched {{count}} sites_two": "",
|
||||
"Searched {{count}} sites_few": "",
|
||||
"Searched {{count}} sites_many": "",
|
||||
"Searched {{count}} sites_other": "",
|
||||
"Searching the web for '{{searchQuery}}'": "",
|
||||
"Searxng Query URL": "",
|
||||
"Search Query Generation Prompt": "",
|
||||
"Search Query Generation Prompt Length Threshold": "",
|
||||
"Search Result Count": "عدد نتائج البحث",
|
||||
"Searched {{count}} sites_zero": "تم البحث في {{count}} sites_zero",
|
||||
"Searched {{count}} sites_one": "تم البحث في {{count}} sites_one",
|
||||
"Searched {{count}} sites_two": "تم البحث في {{count}} sites_two",
|
||||
"Searched {{count}} sites_few": "تم البحث في {{count}} sites_few",
|
||||
"Searched {{count}} sites_many": "تم البحث في {{count}} sites_many",
|
||||
"Searched {{count}} sites_other": "تم البحث في {{count}} sites_other",
|
||||
"Searching the web for '{{searchQuery}}'": "البحث في الويب عن \"{{searchQuery}}\"",
|
||||
"Searxng Query URL": "عنوان URL لاستعلام Searxng",
|
||||
"See readme.md for instructions": "readme.md للحصول على التعليمات",
|
||||
"See what's new": "ما الجديد",
|
||||
"Seed": "Seed",
|
||||
"Select a base model": "",
|
||||
"Select a base model": "حدد نموذجا أساسيا",
|
||||
"Select a engine": "",
|
||||
"Select a mode": "أختار موديل",
|
||||
"Select a model": "أختار الموديل",
|
||||
"Select a pipeline": "",
|
||||
"Select a pipeline url": "",
|
||||
"Select a pipeline": "حدد مسارا",
|
||||
"Select a pipeline url": "حدد عنوان URL لخط الأنابيب",
|
||||
"Select an Ollama instance": "أختار سيرفر ",
|
||||
"Select model": " أختار موديل",
|
||||
"Selected model(s) do not support image inputs": "",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "النموذج (النماذج) المحددة لا تدعم مدخلات الصور",
|
||||
"Send": "تم",
|
||||
"Send a Message": "يُرجى إدخال طلبك هنا",
|
||||
"Send message": "يُرجى إدخال طلبك هنا.",
|
||||
"September": "سبتمبر",
|
||||
"Serper API Key": "",
|
||||
"Serpstack API Key": "",
|
||||
"Serper API Key": "مفتاح واجهة برمجة تطبيقات سيربر",
|
||||
"Serpstack API Key": "مفتاح واجهة برمجة تطبيقات Serpstack",
|
||||
"Server connection verified": "تم التحقق من اتصال الخادم",
|
||||
"Set as default": "الافتراضي",
|
||||
"Set Default Model": "تفعيد الموديل الافتراضي",
|
||||
"Set embedding model (e.g. {{model}})": "ضبط نموذج المتجهات (على سبيل المثال: {{model}})",
|
||||
"Set Image Size": "حجم الصورة",
|
||||
"Set Model": "ضبط النموذج",
|
||||
"Set reranking model (e.g. {{model}})": "ضبط نموذج إعادة الترتيب (على سبيل المثال: {{model}})",
|
||||
"Set Steps": "ضبط الخطوات",
|
||||
"Set Task Model": "",
|
||||
"Set Task Model": "تعيين نموذج المهمة",
|
||||
"Set Voice": "ضبط الصوت",
|
||||
"Settings": "الاعدادات",
|
||||
"Settings saved successfully!": "تم حفظ الاعدادات بنجاح",
|
||||
"Settings updated successfully": "",
|
||||
"Share": "كشاركة",
|
||||
"Share Chat": "مشاركة الدردشة",
|
||||
"Share to OpenWebUI Community": "OpenWebUI شارك في مجتمع",
|
||||
"short-summary": "ملخص قصير",
|
||||
"Show": "عرض",
|
||||
"Show Admin Details in Account Pending Overlay": "",
|
||||
"Show shortcuts": "إظهار الاختصارات",
|
||||
"Showcased creativity": "أظهر الإبداع",
|
||||
"sidebar": "الشريط الجانبي",
|
||||
@@ -447,8 +474,8 @@
|
||||
"Source": "المصدر",
|
||||
"Speech recognition error: {{error}}": "{{error}} خطأ في التعرف على الكلام",
|
||||
"Speech-to-Text Engine": "محرك تحويل الكلام إلى نص",
|
||||
"SpeechRecognition API is not supported in this browser.": "API SpeechRecognition غير مدعومة في هذا المتصفح.",
|
||||
"Stop Sequence": "وقف التسلسل",
|
||||
"STT Model": "",
|
||||
"STT Settings": "STT اعدادات",
|
||||
"Submit": "إرسال",
|
||||
"Subtitle (e.g. about the Roman Empire)": "(e.g. about the Roman Empire) الترجمة",
|
||||
@@ -467,7 +494,9 @@
|
||||
"Thanks for your feedback!": "شكرا لملاحظاتك!",
|
||||
"The score should be a value between 0.0 (0%) and 1.0 (100%).": "يجب أن تكون النتيجة قيمة تتراوح بين 0.0 (0%) و1.0 (100%).",
|
||||
"Theme": "الثيم",
|
||||
"Thinking...": "",
|
||||
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "وهذا يضمن حفظ محادثاتك القيمة بشكل آمن في قاعدة بياناتك الخلفية. شكرًا لك!",
|
||||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||
"This setting does not sync across browsers or devices.": "لا تتم مزامنة هذا الإعداد عبر المتصفحات أو الأجهزة.",
|
||||
"Thorough explanation": "شرح شامل",
|
||||
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "ملاحضة: قم بتحديث عدة فتحات متغيرة على التوالي عن طريق الضغط على مفتاح tab في مدخلات الدردشة بعد كل استبدال.",
|
||||
@@ -479,6 +508,7 @@
|
||||
"to": "الى",
|
||||
"To access the available model names for downloading,": "للوصول إلى أسماء الموديلات المتاحة للتنزيل،",
|
||||
"To access the GGUF models available for downloading,": "للوصول إلى الموديلات GGUF المتاحة للتنزيل،",
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"to chat input.": "الى كتابة المحادثه",
|
||||
"Today": "اليوم",
|
||||
"Toggle settings": "فتح وأغلاق الاعدادات",
|
||||
@@ -486,22 +516,25 @@
|
||||
"Top K": "Top K",
|
||||
"Top P": "Top P",
|
||||
"Trouble accessing Ollama?": "هل تواجه مشكلة في الوصول",
|
||||
"TTS Model": "",
|
||||
"TTS Settings": "TTS اعدادات",
|
||||
"Type": "",
|
||||
"TTS Voice": "",
|
||||
"Type": "نوع",
|
||||
"Type Hugging Face Resolve (Download) URL": "اكتب عنوان URL لحل مشكلة الوجه (تنزيل).",
|
||||
"Uh-oh! There was an issue connecting to {{provider}}.": "{{provider}}خطاء أوه! حدثت مشكلة في الاتصال بـ ",
|
||||
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "نوع ملف غير معروف '{{file_type}}', ولكن القبول والتعامل كنص عادي ",
|
||||
"Update and Copy Link": "تحديث ونسخ الرابط",
|
||||
"Update password": "تحديث كلمة المرور",
|
||||
"Upload a GGUF model": "GGUF رفع موديل نوع",
|
||||
"Upload Files": "",
|
||||
"Upload Files": "تحميل الملفات",
|
||||
"Upload Pipeline": "",
|
||||
"Upload Progress": "جاري التحميل",
|
||||
"URL Mode": "رابط الموديل",
|
||||
"Use '#' in the prompt input to load and select your documents.": "أستخدم '#' في المحادثة لربطهامن المستندات",
|
||||
"Use Gravatar": "Gravatar أستخدم",
|
||||
"Use Initials": "Initials أستخدم",
|
||||
"use_mlock (Ollama)": "",
|
||||
"use_mmap (Ollama)": "",
|
||||
"use_mlock (Ollama)": "use_mlock (أولاما)",
|
||||
"use_mmap (Ollama)": "use_mmap (أولاما)",
|
||||
"user": "مستخدم",
|
||||
"User Permissions": "صلاحيات المستخدم",
|
||||
"Users": "المستخدمين",
|
||||
@@ -510,30 +543,34 @@
|
||||
"variable": "المتغير",
|
||||
"variable to have them replaced with clipboard content.": "متغير لاستبدالها بمحتوى الحافظة.",
|
||||
"Version": "إصدار",
|
||||
"Warning": "",
|
||||
"Warning": "تحذير",
|
||||
"Warning: If you update or change your embedding model, you will need to re-import all documents.": "تحذير: إذا قمت بتحديث أو تغيير نموذج التضمين الخاص بك، فستحتاج إلى إعادة استيراد كافة المستندات.",
|
||||
"Web": "Web",
|
||||
"Web API": "",
|
||||
"Web Loader Settings": "Web تحميل اعدادات",
|
||||
"Web Params": "Web تحميل اعدادات",
|
||||
"Web Search": "",
|
||||
"Web Search Engine": "",
|
||||
"Web Search": "بحث الويب",
|
||||
"Web Search Engine": "محرك بحث الويب",
|
||||
"Webhook URL": "Webhook الرابط",
|
||||
"WebUI Add-ons": "WebUI الأضافات",
|
||||
"WebUI Settings": "WebUI اعدادات",
|
||||
"WebUI will make requests to": "سوف يقوم WebUI بتقديم طلبات ل",
|
||||
"What’s New in": "ما هو الجديد",
|
||||
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "عند إيقاف تشغيل السجل، لن تظهر الدردشات الجديدة على هذا المتصفح في سجلك على أي من أجهزتك.",
|
||||
"Whisper (Local)": "Whisper (Local)",
|
||||
"Whisper (Local)": "",
|
||||
"Widescreen Mode": "",
|
||||
"Workspace": "مساحة العمل",
|
||||
"Write a prompt suggestion (e.g. Who are you?)": "اكتب اقتراحًا سريعًا (على سبيل المثال، من أنت؟)",
|
||||
"Write a summary in 50 words that summarizes [topic or keyword].": "اكتب ملخصًا في 50 كلمة يلخص [الموضوع أو الكلمة الرئيسية]",
|
||||
"Yesterday": "أمس",
|
||||
"You": "انت",
|
||||
"You cannot clone a base model": "",
|
||||
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "",
|
||||
"You cannot clone a base model": "لا يمكنك استنساخ نموذج أساسي",
|
||||
"You have no archived conversations.": "لا تملك محادثات محفوظه",
|
||||
"You have shared this chat": "تم مشاركة هذه المحادثة",
|
||||
"You're a helpful assistant.": "مساعدك المفيد هنا",
|
||||
"You're now logged in.": "لقد قمت الآن بتسجيل الدخول.",
|
||||
"Your account status is currently pending activation.": "",
|
||||
"Youtube": "Youtube",
|
||||
"Youtube Loader Settings": "Youtube تحميل اعدادات"
|
||||
}
|
||||
|
||||
@@ -3,19 +3,21 @@
|
||||
"(Beta)": "(Бета)",
|
||||
"(e.g. `sh webui.sh --api`)": "(например `sh webui.sh --api`)",
|
||||
"(latest)": "(последна)",
|
||||
"{{ models }}": "",
|
||||
"{{ owner }}: You cannot delete a base model": "",
|
||||
"{{ models }}": "{{ модели }}",
|
||||
"{{ owner }}: You cannot delete a base model": "{{ owner }}: Не можете да изтриете базов модел",
|
||||
"{{modelName}} is thinking...": "{{modelName}} мисли ...",
|
||||
"{{user}}'s Chats": "{{user}}'s чатове",
|
||||
"{{webUIName}} Backend Required": "{{webUIName}} Изисква се Бекенд",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "Моделът на задачите се използва при изпълнение на задачи като генериране на заглавия за чатове и заявки за търсене в мрежата",
|
||||
"a user": "потребител",
|
||||
"About": "Относно",
|
||||
"Account": "Акаунт",
|
||||
"Account Activation Pending": "",
|
||||
"Accurate information": "Точни информация",
|
||||
"Active Users": "",
|
||||
"Add": "Добавяне",
|
||||
"Add a model id": "",
|
||||
"Add a short description about what this model does": "",
|
||||
"Add a model id": "Добавяне на ИД на модел",
|
||||
"Add a short description about what this model does": "Добавете кратко описание за това какво прави този модел",
|
||||
"Add a short title for this prompt": "Добавяне на кратко заглавие за този промпт",
|
||||
"Add a tag": "Добавяне на таг",
|
||||
"Add custom prompt": "Добавяне на собствен промпт",
|
||||
@@ -28,15 +30,17 @@
|
||||
"Add User": "Добавяне на потребител",
|
||||
"Adjusting these settings will apply changes universally to all users.": "При промяна на тези настройки промените се прилагат за всички потребители.",
|
||||
"admin": "админ",
|
||||
"Admin": "",
|
||||
"Admin Panel": "Панел на Администратор",
|
||||
"Admin Settings": "Настройки на Администратор",
|
||||
"Advanced Parameters": "Разширени Параметри",
|
||||
"Advanced Params": "",
|
||||
"Advanced Params": "Разширени параметри",
|
||||
"all": "всички",
|
||||
"All Documents": "Всички Документи",
|
||||
"All Users": "Всички Потребители",
|
||||
"Allow": "Позволи",
|
||||
"Allow Chat Deletion": "Позволи Изтриване на Чат",
|
||||
"Allow non-local voices": "",
|
||||
"alphanumeric characters and hyphens": "алфанумерични знаци и тире",
|
||||
"Already have an account?": "Вече имате акаунт? ",
|
||||
"an assistant": "асистент",
|
||||
@@ -48,7 +52,7 @@
|
||||
"API keys": "API Ключове",
|
||||
"April": "Април",
|
||||
"Archive": "Архивирани Чатове",
|
||||
"Archive All Chats": "",
|
||||
"Archive All Chats": "Архив Всички чатове",
|
||||
"Archived Chats": "Архивирани Чатове",
|
||||
"are allowed - Activate this command by typing": "са разрешени - Активирайте тази команда чрез въвеждане",
|
||||
"Are you sure?": "Сигурни ли сте?",
|
||||
@@ -57,20 +61,22 @@
|
||||
"Audio": "Аудио",
|
||||
"August": "Август",
|
||||
"Auto-playback response": "Аувтоматично възпроизвеждане на Отговора",
|
||||
"Auto-send input after 3 sec.": "Аувтоматично изпращане на входа след 3 сек.",
|
||||
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Базов URL",
|
||||
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Базов URL е задължителен.",
|
||||
"available!": "наличен!",
|
||||
"Back": "Назад",
|
||||
"Bad Response": "Невалиден отговор от API",
|
||||
"Banners": "",
|
||||
"Base Model (From)": "",
|
||||
"Banners": "Банери",
|
||||
"Base Model (From)": "Базов модел (от)",
|
||||
"before": "преди",
|
||||
"Being lazy": "Да бъдеш мързелив",
|
||||
"Brave Search API Key": "",
|
||||
"Brave Search API Key": "Смел ключ за API за търсене",
|
||||
"Bypass SSL verification for Websites": "Изключване на SSL проверката за сайтове",
|
||||
"Call": "",
|
||||
"Call feature is not supported when using Web STT engine": "",
|
||||
"Camera": "",
|
||||
"Cancel": "Отказ",
|
||||
"Capabilities": "",
|
||||
"Capabilities": "Възможности",
|
||||
"Change Password": "Промяна на Парола",
|
||||
"Chat": "Чат",
|
||||
"Chat Bubble UI": "UI за чат бублон",
|
||||
@@ -86,35 +92,36 @@
|
||||
"Chunk Params": "Chunk Params",
|
||||
"Chunk Size": "Chunk Size",
|
||||
"Citation": "Цитат",
|
||||
"Clear memory": "",
|
||||
"Click here for help.": "Натиснете тук за помощ.",
|
||||
"Click here to": "Натиснете тук за",
|
||||
"Click here to select": "Натиснете тук, за да изберете",
|
||||
"Click here to select a csv file.": "Натиснете тук, за да изберете csv файл.",
|
||||
"Click here to select a py file.": "",
|
||||
"Click here to select documents.": "Натиснете тук, за да изберете документи.",
|
||||
"click here.": "натиснете тук.",
|
||||
"Click on the user role button to change a user's role.": "Натиснете върху бутона за промяна на ролята на потребителя.",
|
||||
"Clone": "",
|
||||
"Clone": "Клонинг",
|
||||
"Close": "Затвори",
|
||||
"Collection": "Колекция",
|
||||
"ComfyUI": "ComfyUI",
|
||||
"ComfyUI Base URL": "ComfyUI Base URL",
|
||||
"ComfyUI Base URL is required.": "ComfyUI Base URL е задължително.",
|
||||
"Command": "Команда",
|
||||
"Concurrent Requests": "",
|
||||
"Concurrent Requests": "Едновременни искания",
|
||||
"Confirm Password": "Потвърди Парола",
|
||||
"Connections": "Връзки",
|
||||
"Contact Admin for WebUI Access": "",
|
||||
"Content": "Съдържание",
|
||||
"Context Length": "Дължина на Контекста",
|
||||
"Continue Response": "Продължи отговора",
|
||||
"Conversation Mode": "Режим на Чат",
|
||||
"Copied shared chat URL to clipboard!": "Копирана е връзката за чат!",
|
||||
"Copy": "Копирай",
|
||||
"Copy last code block": "Копиране на последен код блок",
|
||||
"Copy last response": "Копиране на последен отговор",
|
||||
"Copy Link": "Копиране на връзка",
|
||||
"Copying to clipboard was successful!": "Копирането в клипборда беше успешно!",
|
||||
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "Създайте кратка фраза от 3-5 думи като заглавие за следващото запитване, като стриктно спазвате ограничението от 3-5 думи и избягвате използването на думата 'заглавие':",
|
||||
"Create a model": "",
|
||||
"Create a model": "Създаване на модел",
|
||||
"Create Account": "Създаване на Акаунт",
|
||||
"Create new key": "Създаване на нов ключ",
|
||||
"Create new secret key": "Създаване на нов секретен ключ",
|
||||
@@ -123,38 +130,39 @@
|
||||
"Current Model": "Текущ модел",
|
||||
"Current Password": "Текуща Парола",
|
||||
"Custom": "Персонализиран",
|
||||
"Customize models for a specific purpose": "",
|
||||
"Customize models for a specific purpose": "Персонализиране на модели за конкретна цел",
|
||||
"Dark": "Тъмен",
|
||||
"Dashboard": "",
|
||||
"Database": "База данни",
|
||||
"December": "Декември",
|
||||
"Default": "По подразбиране",
|
||||
"Default (Automatic1111)": "По подразбиране (Automatic1111)",
|
||||
"Default (SentenceTransformers)": "По подразбиране (SentenceTransformers)",
|
||||
"Default (Web API)": "По подразбиране (Web API)",
|
||||
"Default Model": "",
|
||||
"Default Model": "Модел по подразбиране",
|
||||
"Default model updated": "Моделът по подразбиране е обновен",
|
||||
"Default Prompt Suggestions": "Промпт Предложения по подразбиране",
|
||||
"Default User Role": "Роля на потребителя по подразбиране",
|
||||
"delete": "изтриване",
|
||||
"Delete": "Изтриване",
|
||||
"Delete a model": "Изтриване на модел",
|
||||
"Delete All Chats": "",
|
||||
"Delete All Chats": "Изтриване на всички чатове",
|
||||
"Delete chat": "Изтриване на чат",
|
||||
"Delete Chat": "Изтриване на Чат",
|
||||
"delete this link": "Изтриване на този линк",
|
||||
"Delete User": "Изтриване на потребител",
|
||||
"Deleted {{deleteModelTag}}": "Изтрито {{deleteModelTag}}",
|
||||
"Deleted {{name}}": "",
|
||||
"Deleted {{name}}": "Изтрито {{име}}",
|
||||
"Description": "Описание",
|
||||
"Didn't fully follow instructions": "Не следва инструкциите",
|
||||
"Disabled": "Деактивиран",
|
||||
"Discover a model": "",
|
||||
"Discover a model": "Открийте модел",
|
||||
"Discover a prompt": "Откриване на промпт",
|
||||
"Discover, download, and explore custom prompts": "Откриване, сваляне и преглед на персонализирани промптове",
|
||||
"Discover, download, and explore model presets": "Откриване, сваляне и преглед на пресетове на модели",
|
||||
"Dismissible": "",
|
||||
"Display the username instead of You in the Chat": "Показване на потребителското име вместо Вие в чата",
|
||||
"Document": "Документ",
|
||||
"Document Settings": "Документ Настройки",
|
||||
"Documentation": "",
|
||||
"Documents": "Документи",
|
||||
"does not make any external connections, and your data stays securely on your locally hosted server.": "няма външни връзки, и вашите данни остават сигурни на локално назначен сървър.",
|
||||
"Don't Allow": "Не Позволявай",
|
||||
@@ -169,31 +177,31 @@
|
||||
"Edit Doc": "Редактиране на документ",
|
||||
"Edit User": "Редактиране на потребител",
|
||||
"Email": "Имейл",
|
||||
"Embedding Batch Size": "",
|
||||
"Embedding Model": "Модел за вграждане",
|
||||
"Embedding Model Engine": "Модел за вграждане",
|
||||
"Embedding model set to \"{{embedding_model}}\"": "Модел за вграждане е настроен на \"{{embedding_model}}\"",
|
||||
"Enable Chat History": "Вклюване на Чат История",
|
||||
"Enable Community Sharing": "",
|
||||
"Enable Community Sharing": "Разрешаване на споделяне в общност",
|
||||
"Enable New Sign Ups": "Вклюване на Нови Потребители",
|
||||
"Enable Web Search": "",
|
||||
"Enabled": "Включено",
|
||||
"Enable Web Search": "Разрешаване на търсене в уеб",
|
||||
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Уверете се, че вашият CSV файл включва 4 колони в следния ред: Име, Имейл, Парола, Роля.",
|
||||
"Enter {{role}} message here": "Въведете съобщение за {{role}} тук",
|
||||
"Enter a detail about yourself for your LLMs to recall": "Въведете подробности за себе си, за да се herinnerат вашите LLMs",
|
||||
"Enter Brave Search API Key": "",
|
||||
"Enter Brave Search API Key": "Въведете Brave Search API ключ",
|
||||
"Enter Chunk Overlap": "Въведете Chunk Overlap",
|
||||
"Enter Chunk Size": "Въведете Chunk Size",
|
||||
"Enter Github Raw URL": "",
|
||||
"Enter Google PSE API Key": "",
|
||||
"Enter Google PSE Engine Id": "",
|
||||
"Enter Github Raw URL": "Въведете URL адреса на Github Raw",
|
||||
"Enter Google PSE API Key": "Въведете Google PSE API ключ",
|
||||
"Enter Google PSE Engine Id": "Въведете идентификатор на двигателя на Google PSE",
|
||||
"Enter Image Size (e.g. 512x512)": "Въведете размер на изображението (напр. 512x512)",
|
||||
"Enter language codes": "Въведете кодове на езика",
|
||||
"Enter model tag (e.g. {{modelTag}})": "Въведете таг на модел (напр. {{modelTag}})",
|
||||
"Enter Number of Steps (e.g. 50)": "Въведете брой стъпки (напр. 50)",
|
||||
"Enter Score": "Въведете оценка",
|
||||
"Enter Searxng Query URL": "",
|
||||
"Enter Serper API Key": "",
|
||||
"Enter Serpstack API Key": "",
|
||||
"Enter Searxng Query URL": "Въведете URL адреса на заявката на Searxng",
|
||||
"Enter Serper API Key": "Въведете Serper API ключ",
|
||||
"Enter Serpstack API Key": "Въведете Serpstack API ключ",
|
||||
"Enter stop sequence": "Въведете стоп последователност",
|
||||
"Enter Top K": "Въведете Top K",
|
||||
"Enter URL (e.g. http://127.0.0.1:7860/)": "Въведете URL (напр. http://127.0.0.1:7860/)",
|
||||
@@ -202,16 +210,19 @@
|
||||
"Enter Your Full Name": "Въведете вашето пълно име",
|
||||
"Enter Your Password": "Въведете вашата парола",
|
||||
"Enter Your Role": "Въведете вашата роля",
|
||||
"Error": "",
|
||||
"Error": "Грешка",
|
||||
"Experimental": "Експериментално",
|
||||
"Export": "",
|
||||
"Export": "Износ",
|
||||
"Export All Chats (All Users)": "Експортване на всички чатове (За всички потребители)",
|
||||
"Export chat (.json)": "",
|
||||
"Export Chats": "Експортване на чатове",
|
||||
"Export Documents Mapping": "Експортване на документен мапинг",
|
||||
"Export Models": "",
|
||||
"Export Models": "Експортиране на модели",
|
||||
"Export Prompts": "Експортване на промптове",
|
||||
"External Models": "",
|
||||
"Failed to create API Key.": "Неуспешно създаване на API ключ.",
|
||||
"Failed to read clipboard contents": "Грешка при четене на съдържанието от клипборда",
|
||||
"Failed to update settings": "",
|
||||
"February": "Февруари",
|
||||
"Feel free to add specific details": "Feel free to add specific details",
|
||||
"File Mode": "Файл Мод",
|
||||
@@ -221,15 +232,15 @@
|
||||
"Focus chat input": "Фокусиране на чат вход",
|
||||
"Followed instructions perfectly": "Следвайте инструкциите перфектно",
|
||||
"Format your variables using square brackets like this:": "Форматирайте вашите променливи, като използвате квадратни скоби, както следва:",
|
||||
"Frequency Penalty": "",
|
||||
"Full Screen Mode": "На Цял екран",
|
||||
"Frequency Penalty": "Наказание за честота",
|
||||
"General": "Основни",
|
||||
"General Settings": "Основни Настройки",
|
||||
"Generating search query": "",
|
||||
"Generate Image": "",
|
||||
"Generating search query": "Генериране на заявка за търсене",
|
||||
"Generation Info": "Информация за Генерация",
|
||||
"Good Response": "Добра отговор",
|
||||
"Google PSE API Key": "",
|
||||
"Google PSE Engine Id": "",
|
||||
"Google PSE API Key": "Google PSE API ключ",
|
||||
"Google PSE Engine Id": "Идентификатор на двигателя на Google PSE",
|
||||
"h:mm a": "h:mm a",
|
||||
"has no conversations.": "няма разговори.",
|
||||
"Hello, {{name}}": "Здравей, {{name}}",
|
||||
@@ -243,37 +254,41 @@
|
||||
"Images": "Изображения",
|
||||
"Import Chats": "Импортване на чатове",
|
||||
"Import Documents Mapping": "Импортване на документен мапинг",
|
||||
"Import Models": "",
|
||||
"Import Models": "Импортиране на модели",
|
||||
"Import Prompts": "Импортване на промптове",
|
||||
"Include `--api` flag when running stable-diffusion-webui": "Включете флага `--api`, когато стартирате stable-diffusion-webui",
|
||||
"Info": "",
|
||||
"Info": "Информация",
|
||||
"Input commands": "Въведете команди",
|
||||
"Install from Github URL": "",
|
||||
"Install from Github URL": "Инсталиране от URL адреса на Github",
|
||||
"Instant Auto-Send After Voice Transcription": "",
|
||||
"Interface": "Интерфейс",
|
||||
"Invalid Tag": "Невалиден тег",
|
||||
"January": "Януари",
|
||||
"join our Discord for help.": "свържете се с нашия Discord за помощ.",
|
||||
"JSON": "JSON",
|
||||
"JSON Preview": "",
|
||||
"JSON Preview": "JSON Преглед",
|
||||
"July": "Июл",
|
||||
"June": "Июн",
|
||||
"JWT Expiration": "JWT Expiration",
|
||||
"JWT Token": "JWT Token",
|
||||
"Keep Alive": "Keep Alive",
|
||||
"Keyboard shortcuts": "Клавиши за бърз достъп",
|
||||
"Knowledge": "",
|
||||
"Language": "Език",
|
||||
"Last Active": "Последни активни",
|
||||
"Light": "Светъл",
|
||||
"Listening...": "Слушам...",
|
||||
"Listening...": "",
|
||||
"LLMs can make mistakes. Verify important information.": "LLMs могат да правят грешки. Проверете важните данни.",
|
||||
"Local Models": "",
|
||||
"LTR": "LTR",
|
||||
"Made by OpenWebUI Community": "Направено от OpenWebUI общността",
|
||||
"Make sure to enclose them with": "Уверете се, че са заключени с",
|
||||
"Manage": "",
|
||||
"Manage Models": "Управление на Моделите",
|
||||
"Manage Ollama Models": "Управление на Ollama Моделите",
|
||||
"Manage Pipelines": "",
|
||||
"Manage Pipelines": "Управление на тръбопроводи",
|
||||
"March": "Март",
|
||||
"Max Tokens (num_predict)": "",
|
||||
"Max Tokens (num_predict)": "Макс токени (num_predict)",
|
||||
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Максимум 3 модели могат да бъдат сваляни едновременно. Моля, опитайте отново по-късно.",
|
||||
"May": "Май",
|
||||
"Memories accessible by LLMs will be shown here.": "Мемории достъпни от LLMs ще бъдат показани тук.",
|
||||
@@ -288,12 +303,12 @@
|
||||
"Model '{{modelName}}' has been successfully downloaded.": "Моделът '{{modelName}}' беше успешно свален.",
|
||||
"Model '{{modelTag}}' is already in queue for downloading.": "Моделът '{{modelTag}}' е вече в очакване за сваляне.",
|
||||
"Model {{modelId}} not found": "Моделът {{modelId}} не е намерен",
|
||||
"Model {{modelName}} is not vision capable": "",
|
||||
"Model {{name}} is now {{status}}": "",
|
||||
"Model {{modelName}} is not vision capable": "Моделът {{modelName}} не може да се вижда",
|
||||
"Model {{name}} is now {{status}}": "Моделът {{name}} сега е {{status}}",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Открит е път до файловата система на модела. За актуализацията се изисква съкратено име на модела, не може да продължи.",
|
||||
"Model ID": "",
|
||||
"Model ID": "ИД на модел",
|
||||
"Model not selected": "Не е избран модел",
|
||||
"Model Params": "",
|
||||
"Model Params": "Модел Params",
|
||||
"Model Whitelisting": "Модел Whitelisting",
|
||||
"Model(s) Whitelisted": "Модели Whitelisted",
|
||||
"Modelfile Content": "Съдържание на модфайл",
|
||||
@@ -301,25 +316,27 @@
|
||||
"More": "Повече",
|
||||
"Name": "Име",
|
||||
"Name Tag": "Име Таг",
|
||||
"Name your model": "",
|
||||
"Name your model": "Дайте име на вашия модел",
|
||||
"New Chat": "Нов чат",
|
||||
"New Password": "Нова парола",
|
||||
"No documents found": "",
|
||||
"No results found": "Няма намерени резултати",
|
||||
"No search query generated": "",
|
||||
"No search query generated": "Не е генерирана заявка за търсене",
|
||||
"No source available": "Няма наличен източник",
|
||||
"None": "",
|
||||
"None": "Никой",
|
||||
"Not factually correct": "Не е фактологически правилно",
|
||||
"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Забележка: Ако зададете минимален резултат, търсенето ще върне само документи с резултат, по-голям или равен на минималния резултат.",
|
||||
"Notifications": "Десктоп Известия",
|
||||
"November": "Ноември",
|
||||
"num_thread (Ollama)": "",
|
||||
"num_thread (Ollama)": "num_thread (Ollama)",
|
||||
"October": "Октомври",
|
||||
"Off": "Изкл.",
|
||||
"Okay, Let's Go!": "ОК, Нека започваме!",
|
||||
"OLED Dark": "OLED тъмно",
|
||||
"Ollama": "Ollama",
|
||||
"Ollama API": "",
|
||||
"Ollama API disabled": "",
|
||||
"Ollama API": "Ollama API",
|
||||
"Ollama API disabled": "Ollama API деактивиран",
|
||||
"Ollama API is disabled": "",
|
||||
"Ollama Version": "Ollama Версия",
|
||||
"On": "Вкл.",
|
||||
"Only": "Само",
|
||||
@@ -342,10 +359,12 @@
|
||||
"PDF document (.pdf)": "PDF документ (.pdf)",
|
||||
"PDF Extract Images (OCR)": "PDF Extract Images (OCR)",
|
||||
"pending": "в очакване",
|
||||
"Permission denied when accessing media devices": "",
|
||||
"Permission denied when accessing microphone": "",
|
||||
"Permission denied when accessing microphone: {{error}}": "Permission denied when accessing microphone: {{error}}",
|
||||
"Personalization": "Персонализация",
|
||||
"Pipelines": "",
|
||||
"Pipelines Valves": "",
|
||||
"Pipelines": "Тръбопроводи",
|
||||
"Pipelines Valves": "Тръбопроводи Вентили",
|
||||
"Plain text (.txt)": "Plain text (.txt)",
|
||||
"Playground": "Плейграунд",
|
||||
"Positive attitude": "Позитивна ативност",
|
||||
@@ -364,6 +383,7 @@
|
||||
"Read Aloud": "Прочети на Голос",
|
||||
"Record voice": "Записване на глас",
|
||||
"Redirecting you to OpenWebUI Community": "Пренасочване към OpenWebUI общността",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"Refused when it shouldn't have": "Отказано, когато не трябва да бъде",
|
||||
"Regenerate": "Регенериране",
|
||||
"Release Notes": "Бележки по изданието",
|
||||
@@ -375,12 +395,14 @@
|
||||
"Reranking Model": "Reranking Model",
|
||||
"Reranking model disabled": "Reranking model disabled",
|
||||
"Reranking model set to \"{{reranking_model}}\"": "Reranking model set to \"{{reranking_model}}\"",
|
||||
"Reset Upload Directory": "",
|
||||
"Reset Vector Storage": "Ресет Vector Storage",
|
||||
"Response AutoCopy to Clipboard": "Аувтоматично копиране на отговор в клипборда",
|
||||
"Role": "Роля",
|
||||
"Rosé Pine": "Rosé Pine",
|
||||
"Rosé Pine Dawn": "Rosé Pine Dawn",
|
||||
"RTL": "RTL",
|
||||
"Running": "",
|
||||
"Save": "Запис",
|
||||
"Save & Create": "Запис & Създаване",
|
||||
"Save & Update": "Запис & Актуализиране",
|
||||
@@ -390,49 +412,54 @@
|
||||
"Scan for documents from {{path}}": "Сканиране за документи в {{path}}",
|
||||
"Search": "Търси",
|
||||
"Search a model": "Търси модел",
|
||||
"Search Chats": "",
|
||||
"Search Chats": "Търсене на чатове",
|
||||
"Search Documents": "Търси Документи",
|
||||
"Search Models": "",
|
||||
"Search Models": "Търсене на модели",
|
||||
"Search Prompts": "Търси Промптове",
|
||||
"Search Result Count": "",
|
||||
"Searched {{count}} sites_one": "",
|
||||
"Searched {{count}} sites_other": "",
|
||||
"Searching the web for '{{searchQuery}}'": "",
|
||||
"Searxng Query URL": "",
|
||||
"Search Query Generation Prompt": "",
|
||||
"Search Query Generation Prompt Length Threshold": "",
|
||||
"Search Result Count": "Брой резултати от търсенето",
|
||||
"Searched {{count}} sites_one": "Търси се в {{count}} sites_one",
|
||||
"Searched {{count}} sites_other": "Търси се в {{count}} sites_other",
|
||||
"Searching the web for '{{searchQuery}}'": "Търсене в уеб за '{{searchQuery}}'",
|
||||
"Searxng Query URL": "URL адрес на заявка на Searxng",
|
||||
"See readme.md for instructions": "Виж readme.md за инструкции",
|
||||
"See what's new": "Виж какво е новото",
|
||||
"Seed": "Seed",
|
||||
"Select a base model": "",
|
||||
"Select a base model": "Изберете базов модел",
|
||||
"Select a engine": "",
|
||||
"Select a mode": "Изберете режим",
|
||||
"Select a model": "Изберете модел",
|
||||
"Select a pipeline": "",
|
||||
"Select a pipeline url": "",
|
||||
"Select a pipeline": "Изберете тръбопровод",
|
||||
"Select a pipeline url": "Избор на URL адрес на канал",
|
||||
"Select an Ollama instance": "Изберете Ollama инстанция",
|
||||
"Select model": "Изберете модел",
|
||||
"Selected model(s) do not support image inputs": "",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "Избраният(те) модел(и) не поддържа въвеждане на изображения",
|
||||
"Send": "Изпрати",
|
||||
"Send a Message": "Изпращане на Съобщение",
|
||||
"Send message": "Изпращане на съобщение",
|
||||
"September": "Септември",
|
||||
"Serper API Key": "",
|
||||
"Serpstack API Key": "",
|
||||
"Serper API Key": "Serper API ключ",
|
||||
"Serpstack API Key": "Serpstack API ключ",
|
||||
"Server connection verified": "Server connection verified",
|
||||
"Set as default": "Задай по подразбиране",
|
||||
"Set Default Model": "Задай Модел По Подразбиране",
|
||||
"Set embedding model (e.g. {{model}})": "Задай embedding model (e.g. {{model}})",
|
||||
"Set Image Size": "Задай Размер на Изображението",
|
||||
"Set Model": "Задай Модел",
|
||||
"Set reranking model (e.g. {{model}})": "Задай reranking model (e.g. {{model}})",
|
||||
"Set Steps": "Задай Стъпки",
|
||||
"Set Task Model": "",
|
||||
"Set Task Model": "Задаване на модел на задача",
|
||||
"Set Voice": "Задай Глас",
|
||||
"Settings": "Настройки",
|
||||
"Settings saved successfully!": "Настройките са запазени успешно!",
|
||||
"Settings updated successfully": "",
|
||||
"Share": "Подели",
|
||||
"Share Chat": "Подели Чат",
|
||||
"Share to OpenWebUI Community": "Споделите с OpenWebUI Общността",
|
||||
"short-summary": "short-summary",
|
||||
"Show": "Покажи",
|
||||
"Show Admin Details in Account Pending Overlay": "",
|
||||
"Show shortcuts": "Покажи",
|
||||
"Showcased creativity": "Показана креативност",
|
||||
"sidebar": "sidebar",
|
||||
@@ -443,8 +470,8 @@
|
||||
"Source": "Източник",
|
||||
"Speech recognition error: {{error}}": "Speech recognition error: {{error}}",
|
||||
"Speech-to-Text Engine": "Speech-to-Text Engine",
|
||||
"SpeechRecognition API is not supported in this browser.": "SpeechRecognition API is not supported in this browser.",
|
||||
"Stop Sequence": "Stop Sequence",
|
||||
"STT Model": "",
|
||||
"STT Settings": "STT Настройки",
|
||||
"Submit": "Изпращане",
|
||||
"Subtitle (e.g. about the Roman Empire)": "Подтитул (напр. за Римска империя)",
|
||||
@@ -463,7 +490,9 @@
|
||||
"Thanks for your feedback!": "Благодарим ви за вашия отзив!",
|
||||
"The score should be a value between 0.0 (0%) and 1.0 (100%).": "The score should be a value between 0.0 (0%) and 1.0 (100%).",
|
||||
"Theme": "Тема",
|
||||
"Thinking...": "",
|
||||
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Това гарантира, че ценните ви разговори се запазват сигурно във вашата бекенд база данни. Благодарим ви!",
|
||||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||
"This setting does not sync across browsers or devices.": "Тази настройка не се синхронизира между браузъри или устройства.",
|
||||
"Thorough explanation": "Това е подробно описание.",
|
||||
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Съвет: Актуализирайте няколко слота за променливи последователно, като натискате клавиша Tab в чат входа след всяка подмяна.",
|
||||
@@ -475,6 +504,7 @@
|
||||
"to": "в",
|
||||
"To access the available model names for downloading,": "За да получите достъп до наличните имена на модели за изтегляне,",
|
||||
"To access the GGUF models available for downloading,": "За да получите достъп до GGUF моделите, налични за изтегляне,",
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"to chat input.": "към чат входа.",
|
||||
"Today": "днес",
|
||||
"Toggle settings": "Toggle settings",
|
||||
@@ -482,22 +512,25 @@
|
||||
"Top K": "Top K",
|
||||
"Top P": "Top P",
|
||||
"Trouble accessing Ollama?": "Проблеми с достъпът до Ollama?",
|
||||
"TTS Model": "",
|
||||
"TTS Settings": "TTS Настройки",
|
||||
"Type": "",
|
||||
"TTS Voice": "",
|
||||
"Type": "Вид",
|
||||
"Type Hugging Face Resolve (Download) URL": "Въведете Hugging Face Resolve (Download) URL",
|
||||
"Uh-oh! There was an issue connecting to {{provider}}.": "О, не! Възникна проблем при свързването с {{provider}}.",
|
||||
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Непознат файлов тип '{{file_type}}', но се приема и обработва като текст",
|
||||
"Update and Copy Link": "Обнови и копирай връзка",
|
||||
"Update password": "Обновяване на парола",
|
||||
"Upload a GGUF model": "Качване на GGUF модел",
|
||||
"Upload Files": "",
|
||||
"Upload Files": "Качване на файлове",
|
||||
"Upload Pipeline": "",
|
||||
"Upload Progress": "Прогрес на качването",
|
||||
"URL Mode": "URL Mode",
|
||||
"Use '#' in the prompt input to load and select your documents.": "Използвайте '#' във промпта за да заредите и изберете вашите документи.",
|
||||
"Use Gravatar": "Използвайте Gravatar",
|
||||
"Use Initials": "Използвайте Инициали",
|
||||
"use_mlock (Ollama)": "",
|
||||
"use_mmap (Ollama)": "",
|
||||
"use_mlock (Ollama)": "use_mlock (Ollama)",
|
||||
"use_mmap (Ollama)": "use_mmap (Ollama)",
|
||||
"user": "потребител",
|
||||
"User Permissions": "Права на потребителя",
|
||||
"Users": "Потребители",
|
||||
@@ -506,30 +539,34 @@
|
||||
"variable": "променлива",
|
||||
"variable to have them replaced with clipboard content.": "променливи да се заменят съдържанието от клипборд.",
|
||||
"Version": "Версия",
|
||||
"Warning": "",
|
||||
"Warning": "Предупреждение",
|
||||
"Warning: If you update or change your embedding model, you will need to re-import all documents.": "Предупреждение: Ако актуализирате или промените вашия модел за вграждане, трябва да повторите импортирането на всички документи.",
|
||||
"Web": "Уеб",
|
||||
"Web API": "",
|
||||
"Web Loader Settings": "Настройки за зареждане на уеб",
|
||||
"Web Params": "Параметри за уеб",
|
||||
"Web Search": "",
|
||||
"Web Search Engine": "",
|
||||
"Web Search": "Търсене в уеб",
|
||||
"Web Search Engine": "Уеб търсачка",
|
||||
"Webhook URL": "Уебхук URL",
|
||||
"WebUI Add-ons": "WebUI Добавки",
|
||||
"WebUI Settings": "WebUI Настройки",
|
||||
"WebUI will make requests to": "WebUI ще направи заявки към",
|
||||
"What’s New in": "Какво е новото в",
|
||||
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Когато историята е изключена, нови чатове в този браузър ще не се показват в историята на никои от вашия профил.",
|
||||
"Whisper (Local)": "Whisper (Локален)",
|
||||
"Whisper (Local)": "",
|
||||
"Widescreen Mode": "",
|
||||
"Workspace": "Работно пространство",
|
||||
"Write a prompt suggestion (e.g. Who are you?)": "Напиши предложение за промпт (напр. Кой сте вие?)",
|
||||
"Write a summary in 50 words that summarizes [topic or keyword].": "Напиши описание в 50 знака, което описва [тема или ключова дума].",
|
||||
"Yesterday": "вчера",
|
||||
"You": "вие",
|
||||
"You cannot clone a base model": "",
|
||||
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "",
|
||||
"You cannot clone a base model": "Не можете да клонирате базов модел",
|
||||
"You have no archived conversations.": "Нямате архивирани разговори.",
|
||||
"You have shared this chat": "Вие сте споделели този чат",
|
||||
"You're a helpful assistant.": "Вие сте полезен асистент.",
|
||||
"You're now logged in.": "Сега, вие влязохте в системата.",
|
||||
"Your account status is currently pending activation.": "",
|
||||
"Youtube": "Youtube",
|
||||
"Youtube Loader Settings": "Youtube Loader Settings"
|
||||
}
|
||||
|
||||
@@ -3,19 +3,21 @@
|
||||
"(Beta)": "(পরিক্ষামূলক)",
|
||||
"(e.g. `sh webui.sh --api`)": "(যেমন `sh webui.sh --api`)",
|
||||
"(latest)": "(সর্বশেষ)",
|
||||
"{{ models }}": "",
|
||||
"{{ owner }}: You cannot delete a base model": "",
|
||||
"{{ models }}": "{{ মডেল}}",
|
||||
"{{ owner }}: You cannot delete a base model": "{{ owner}}: আপনি একটি বেস মডেল মুছতে পারবেন না",
|
||||
"{{modelName}} is thinking...": "{{modelName}} চিন্তা করছে...",
|
||||
"{{user}}'s Chats": "{{user}}র চ্যাটস",
|
||||
"{{webUIName}} Backend Required": "{{webUIName}} ব্যাকএন্ড আবশ্যক",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "চ্যাট এবং ওয়েব অনুসন্ধান প্রশ্নের জন্য শিরোনাম তৈরি করার মতো কাজগুলি সম্পাদন করার সময় একটি টাস্ক মডেল ব্যবহার করা হয়",
|
||||
"a user": "একজন ব্যাবহারকারী",
|
||||
"About": "সম্পর্কে",
|
||||
"Account": "একাউন্ট",
|
||||
"Account Activation Pending": "",
|
||||
"Accurate information": "সঠিক তথ্য",
|
||||
"Active Users": "",
|
||||
"Add": "যোগ করুন",
|
||||
"Add a model id": "",
|
||||
"Add a short description about what this model does": "",
|
||||
"Add a model id": "একটি মডেল ID যোগ করুন",
|
||||
"Add a short description about what this model does": "এই মডেলটি কী করে সে সম্পর্কে একটি সংক্ষিপ্ত বিবরণ যুক্ত করুন",
|
||||
"Add a short title for this prompt": "এই প্রম্পটের জন্য একটি সংক্ষিপ্ত টাইটেল যোগ করুন",
|
||||
"Add a tag": "একটি ট্যাগ যোগ করুন",
|
||||
"Add custom prompt": "একটি কাস্টম প্রম্পট যোগ করুন",
|
||||
@@ -28,15 +30,17 @@
|
||||
"Add User": "ইউজার যোগ করুন",
|
||||
"Adjusting these settings will apply changes universally to all users.": "এই সেটিংগুলো পরিবর্তন করলে তা সব ইউজারের উপরেই প্রয়োগ করা হবে",
|
||||
"admin": "এডমিন",
|
||||
"Admin": "",
|
||||
"Admin Panel": "এডমিন প্যানেল",
|
||||
"Admin Settings": "এডমিন সেটিংস",
|
||||
"Advanced Parameters": "এডভান্সড প্যারামিটার্স",
|
||||
"Advanced Params": "",
|
||||
"Advanced Params": "অ্যাডভান্সড প্যারাম",
|
||||
"all": "সব",
|
||||
"All Documents": "সব ডকুমেন্ট",
|
||||
"All Users": "সব ইউজার",
|
||||
"Allow": "অনুমোদন",
|
||||
"Allow Chat Deletion": "চ্যাট ডিলিট করতে দিন",
|
||||
"Allow non-local voices": "",
|
||||
"alphanumeric characters and hyphens": "ইংরেজি অক্ষর, সংখ্যা এবং হাইফেন",
|
||||
"Already have an account?": "আগে থেকেই একাউন্ট আছে?",
|
||||
"an assistant": "একটা এসিস্ট্যান্ট",
|
||||
@@ -48,7 +52,7 @@
|
||||
"API keys": "এপিআই কোডস",
|
||||
"April": "আপ্রিল",
|
||||
"Archive": "আর্কাইভ",
|
||||
"Archive All Chats": "",
|
||||
"Archive All Chats": "আর্কাইভ করুন সকল চ্যাট",
|
||||
"Archived Chats": "চ্যাট ইতিহাস সংরক্ষণাগার",
|
||||
"are allowed - Activate this command by typing": "অনুমোদিত - কমান্ডটি চালু করার জন্য লিখুন",
|
||||
"Are you sure?": "আপনি নিশ্চিত?",
|
||||
@@ -57,20 +61,22 @@
|
||||
"Audio": "অডিও",
|
||||
"August": "আগস্ট",
|
||||
"Auto-playback response": "রেসপন্স অটো-প্লেব্যাক",
|
||||
"Auto-send input after 3 sec.": "৩ সেকেন্ড পর ইনপুট সংয়ক্রিয়ভাবে পাঠান",
|
||||
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 বেজ ইউআরএল",
|
||||
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 বেজ ইউআরএল আবশ্যক",
|
||||
"available!": "উপলব্ধ!",
|
||||
"Back": "পেছনে",
|
||||
"Bad Response": "খারাপ প্রতিক্রিয়া",
|
||||
"Banners": "",
|
||||
"Base Model (From)": "",
|
||||
"Banners": "ব্যানার",
|
||||
"Base Model (From)": "বেস মডেল (থেকে)",
|
||||
"before": "পূর্ববর্তী",
|
||||
"Being lazy": "অলস হওয়া",
|
||||
"Brave Search API Key": "",
|
||||
"Brave Search API Key": "সাহসী অনুসন্ধান API কী",
|
||||
"Bypass SSL verification for Websites": "ওয়েবসাইটের জন্য SSL যাচাই বাতিল করুন",
|
||||
"Call": "",
|
||||
"Call feature is not supported when using Web STT engine": "",
|
||||
"Camera": "",
|
||||
"Cancel": "বাতিল",
|
||||
"Capabilities": "",
|
||||
"Capabilities": "সক্ষমতা",
|
||||
"Change Password": "পাসওয়ার্ড পরিবর্তন করুন",
|
||||
"Chat": "চ্যাট",
|
||||
"Chat Bubble UI": "চ্যাট বাবল UI",
|
||||
@@ -86,35 +92,36 @@
|
||||
"Chunk Params": "চাঙ্ক প্যারামিটার্স",
|
||||
"Chunk Size": "চাঙ্ক সাইজ",
|
||||
"Citation": "উদ্ধৃতি",
|
||||
"Clear memory": "",
|
||||
"Click here for help.": "সাহায্যের জন্য এখানে ক্লিক করুন",
|
||||
"Click here to": "এখানে ক্লিক করুন",
|
||||
"Click here to select": "নির্বাচন করার জন্য এখানে ক্লিক করুন",
|
||||
"Click here to select a csv file.": "একটি csv ফাইল নির্বাচন করার জন্য এখানে ক্লিক করুন",
|
||||
"Click here to select a py file.": "",
|
||||
"Click here to select documents.": "ডকুমেন্টগুলো নির্বাচন করার জন্য এখানে ক্লিক করুন",
|
||||
"click here.": "এখানে ক্লিক করুন",
|
||||
"Click on the user role button to change a user's role.": "ইউজারের পদবি পরিবর্তন করার জন্য ইউজারের পদবি বাটনে ক্লিক করুন",
|
||||
"Clone": "",
|
||||
"Clone": "ক্লোন",
|
||||
"Close": "বন্ধ",
|
||||
"Collection": "সংগ্রহ",
|
||||
"ComfyUI": "ComfyUI",
|
||||
"ComfyUI Base URL": "ComfyUI Base URL",
|
||||
"ComfyUI Base URL is required.": "ComfyUI Base URL আবশ্যক।",
|
||||
"Command": "কমান্ড",
|
||||
"Concurrent Requests": "",
|
||||
"Concurrent Requests": "সমকালীন অনুরোধ",
|
||||
"Confirm Password": "পাসওয়ার্ড নিশ্চিত করুন",
|
||||
"Connections": "কানেকশনগুলো",
|
||||
"Contact Admin for WebUI Access": "",
|
||||
"Content": "বিষয়বস্তু",
|
||||
"Context Length": "কনটেক্সটের দৈর্ঘ্য",
|
||||
"Continue Response": "যাচাই করুন",
|
||||
"Conversation Mode": "কথোপকথন মোড",
|
||||
"Copied shared chat URL to clipboard!": "শেয়ারকৃত কথা-ব্যবহারের URL ক্লিপবোর্ডে কপি করা হয়েছে!",
|
||||
"Copy": "অনুলিপি",
|
||||
"Copy last code block": "সর্বশেষ কোড ব্লক কপি করুন",
|
||||
"Copy last response": "সর্বশেষ রেসপন্স কপি করুন",
|
||||
"Copy Link": "লিংক কপি করুন",
|
||||
"Copying to clipboard was successful!": "ক্লিপবোর্ডে কপি করা সফল হয়েছে",
|
||||
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "'title' শব্দটি ব্যবহার না করে নিম্মোক্ত অনুসন্ধানের জন্য সংক্ষেপে সর্বোচ্চ ৩-৫ শব্দের একটি হেডার তৈরি করুন",
|
||||
"Create a model": "",
|
||||
"Create a model": "একটি মডেল তৈরি করুন",
|
||||
"Create Account": "একাউন্ট তৈরি করুন",
|
||||
"Create new key": "একটি নতুন কী তৈরি করুন",
|
||||
"Create new secret key": "একটি নতুন সিক্রেট কী তৈরি করুন",
|
||||
@@ -123,38 +130,39 @@
|
||||
"Current Model": "বর্তমান মডেল",
|
||||
"Current Password": "বর্তমান পাসওয়ার্ড",
|
||||
"Custom": "কাস্টম",
|
||||
"Customize models for a specific purpose": "",
|
||||
"Customize models for a specific purpose": "একটি নির্দিষ্ট উদ্দেশ্যে মডেল কাস্টমাইজ করুন",
|
||||
"Dark": "ডার্ক",
|
||||
"Dashboard": "",
|
||||
"Database": "ডেটাবেজ",
|
||||
"December": "ডেসেম্বর",
|
||||
"Default": "ডিফল্ট",
|
||||
"Default (Automatic1111)": "ডিফল্ট (Automatic1111)",
|
||||
"Default (SentenceTransformers)": "ডিফল্ট (SentenceTransformers)",
|
||||
"Default (Web API)": "ডিফল্ট (Web API)",
|
||||
"Default Model": "",
|
||||
"Default Model": "ডিফল্ট মডেল",
|
||||
"Default model updated": "ডিফল্ট মডেল আপডেট হয়েছে",
|
||||
"Default Prompt Suggestions": "ডিফল্ট প্রম্পট সাজেশন",
|
||||
"Default User Role": "ইউজারের ডিফল্ট পদবি",
|
||||
"delete": "মুছে ফেলুন",
|
||||
"Delete": "মুছে ফেলুন",
|
||||
"Delete a model": "একটি মডেল মুছে ফেলুন",
|
||||
"Delete All Chats": "",
|
||||
"Delete All Chats": "সব চ্যাট মুছে ফেলুন",
|
||||
"Delete chat": "চ্যাট মুছে ফেলুন",
|
||||
"Delete Chat": "চ্যাট মুছে ফেলুন",
|
||||
"delete this link": "এই লিংক মুছে ফেলুন",
|
||||
"Delete User": "ইউজার মুছে ফেলুন",
|
||||
"Deleted {{deleteModelTag}}": "{{deleteModelTag}} মুছে ফেলা হয়েছে",
|
||||
"Deleted {{name}}": "",
|
||||
"Deleted {{name}}": "{{name}} মোছা হয়েছে",
|
||||
"Description": "বিবরণ",
|
||||
"Didn't fully follow instructions": "ইনস্ট্রাকশন সম্পূর্ণ অনুসরণ করা হয়নি",
|
||||
"Disabled": "অক্ষম",
|
||||
"Discover a model": "",
|
||||
"Discover a model": "একটি মডেল আবিষ্কার করুন",
|
||||
"Discover a prompt": "একটি প্রম্পট খুঁজে বের করুন",
|
||||
"Discover, download, and explore custom prompts": "কাস্টম প্রম্পটগুলো আবিস্কার, ডাউনলোড এবং এক্সপ্লোর করুন",
|
||||
"Discover, download, and explore model presets": "মডেল প্রিসেটগুলো আবিস্কার, ডাউনলোড এবং এক্সপ্লোর করুন",
|
||||
"Dismissible": "",
|
||||
"Display the username instead of You in the Chat": "চ্যাটে 'আপনি'-র পরবর্তে ইউজারনেম দেখান",
|
||||
"Document": "ডকুমেন্ট",
|
||||
"Document Settings": "ডকুমেন্ট সেটিংসমূহ",
|
||||
"Documentation": "",
|
||||
"Documents": "ডকুমেন্টসমূহ",
|
||||
"does not make any external connections, and your data stays securely on your locally hosted server.": "কোন এক্সটার্নাল কানেকশন তৈরি করে না, এবং আপনার ডেটা আর লোকালি হোস্টেড সার্ভারেই নিরাপদে থাকে।",
|
||||
"Don't Allow": "অনুমোদন দেবেন না",
|
||||
@@ -169,31 +177,31 @@
|
||||
"Edit Doc": "ডকুমেন্ট এডিট করুন",
|
||||
"Edit User": "ইউজার এডিট করুন",
|
||||
"Email": "ইমেইল",
|
||||
"Embedding Batch Size": "",
|
||||
"Embedding Model": "ইমেজ ইমেবডিং মডেল",
|
||||
"Embedding Model Engine": "ইমেজ ইমেবডিং মডেল ইঞ্জিন",
|
||||
"Embedding model set to \"{{embedding_model}}\"": "ইমেজ ইমেবডিং মডেল সেট করা হয়েছে - \"{{embedding_model}}\"",
|
||||
"Enable Chat History": "চ্যাট হিস্টোরি চালু করুন",
|
||||
"Enable Community Sharing": "",
|
||||
"Enable Community Sharing": "সম্প্রদায় শেয়ারকরণ সক্ষম করুন",
|
||||
"Enable New Sign Ups": "নতুন সাইনআপ চালু করুন",
|
||||
"Enable Web Search": "",
|
||||
"Enabled": "চালু করা হয়েছে",
|
||||
"Enable Web Search": "ওয়েব অনুসন্ধান সক্ষম করুন",
|
||||
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "আপনার সিএসভি ফাইলটিতে এই ক্রমে 4 টি কলাম অন্তর্ভুক্ত রয়েছে তা নিশ্চিত করুন: নাম, ইমেল, পাসওয়ার্ড, ভূমিকা।.",
|
||||
"Enter {{role}} message here": "{{role}} মেসেজ এখানে লিখুন",
|
||||
"Enter a detail about yourself for your LLMs to recall": "আপনার এলএলএমগুলি স্মরণ করার জন্য নিজের সম্পর্কে একটি বিশদ লিখুন",
|
||||
"Enter Brave Search API Key": "",
|
||||
"Enter Brave Search API Key": "সাহসী অনুসন্ধান API কী লিখুন",
|
||||
"Enter Chunk Overlap": "চাঙ্ক ওভারল্যাপ লিখুন",
|
||||
"Enter Chunk Size": "চাংক সাইজ লিখুন",
|
||||
"Enter Github Raw URL": "",
|
||||
"Enter Google PSE API Key": "",
|
||||
"Enter Google PSE Engine Id": "",
|
||||
"Enter Github Raw URL": "গিটহাব কাঁচা URL লিখুন",
|
||||
"Enter Google PSE API Key": "গুগল পিএসই এপিআই কী লিখুন",
|
||||
"Enter Google PSE Engine Id": "গুগল পিএসই ইঞ্জিন আইডি লিখুন",
|
||||
"Enter Image Size (e.g. 512x512)": "ছবির মাপ লিখুন (যেমন 512x512)",
|
||||
"Enter language codes": "ল্যাঙ্গুয়েজ কোড লিখুন",
|
||||
"Enter model tag (e.g. {{modelTag}})": "মডেল ট্যাগ লিখুন (e.g. {{modelTag}})",
|
||||
"Enter Number of Steps (e.g. 50)": "ধাপের সংখ্যা দিন (যেমন: 50)",
|
||||
"Enter Score": "স্কোর দিন",
|
||||
"Enter Searxng Query URL": "",
|
||||
"Enter Serper API Key": "",
|
||||
"Enter Serpstack API Key": "",
|
||||
"Enter Searxng Query URL": "Searxng ক্যোয়ারী URL লিখুন",
|
||||
"Enter Serper API Key": "Serper API কী লিখুন",
|
||||
"Enter Serpstack API Key": "Serpstack API কী লিখুন",
|
||||
"Enter stop sequence": "স্টপ সিকোয়েন্স লিখুন",
|
||||
"Enter Top K": "Top K লিখুন",
|
||||
"Enter URL (e.g. http://127.0.0.1:7860/)": "ইউআরএল দিন (যেমন http://127.0.0.1:7860/)",
|
||||
@@ -202,16 +210,19 @@
|
||||
"Enter Your Full Name": "আপনার পূর্ণ নাম লিখুন",
|
||||
"Enter Your Password": "আপনার পাসওয়ার্ড লিখুন",
|
||||
"Enter Your Role": "আপনার রোল লিখুন",
|
||||
"Error": "",
|
||||
"Error": "ত্রুটি",
|
||||
"Experimental": "পরিক্ষামূলক",
|
||||
"Export": "",
|
||||
"Export": "রপ্তানি",
|
||||
"Export All Chats (All Users)": "সব চ্যাট এক্সপোর্ট করুন (সব ইউজারের)",
|
||||
"Export chat (.json)": "",
|
||||
"Export Chats": "চ্যাটগুলো এক্সপোর্ট করুন",
|
||||
"Export Documents Mapping": "ডকুমেন্টসমূহ ম্যাপিং এক্সপোর্ট করুন",
|
||||
"Export Models": "",
|
||||
"Export Models": "রপ্তানি মডেল",
|
||||
"Export Prompts": "প্রম্পটগুলো একপোর্ট করুন",
|
||||
"External Models": "",
|
||||
"Failed to create API Key.": "API Key তৈরি করা যায়নি।",
|
||||
"Failed to read clipboard contents": "ক্লিপবোর্ডের বিষয়বস্তু পড়া সম্ভব হয়নি",
|
||||
"Failed to update settings": "",
|
||||
"February": "ফেব্রুয়ারি",
|
||||
"Feel free to add specific details": "নির্দিষ্ট বিবরণ যোগ করতে বিনা দ্বিধায়",
|
||||
"File Mode": "ফাইল মোড",
|
||||
@@ -221,15 +232,15 @@
|
||||
"Focus chat input": "চ্যাট ইনপুট ফোকাস করুন",
|
||||
"Followed instructions perfectly": "নির্দেশাবলী নিখুঁতভাবে অনুসরণ করা হয়েছে",
|
||||
"Format your variables using square brackets like this:": "আপনার ভেরিয়বলগুলো এভাবে স্কয়ার ব্রাকেটের মাধ্যমে সাজান",
|
||||
"Frequency Penalty": "",
|
||||
"Full Screen Mode": "ফুলস্ক্রিন মোড",
|
||||
"Frequency Penalty": "ফ্রিকোয়েন্সি পেনাল্টি",
|
||||
"General": "সাধারণ",
|
||||
"General Settings": "সাধারণ সেটিংসমূহ",
|
||||
"Generating search query": "",
|
||||
"Generate Image": "",
|
||||
"Generating search query": "অনুসন্ধান ক্যোয়ারী তৈরি করা হচ্ছে",
|
||||
"Generation Info": "জেনারেশন ইনফো",
|
||||
"Good Response": "ভালো সাড়া",
|
||||
"Google PSE API Key": "",
|
||||
"Google PSE Engine Id": "",
|
||||
"Google PSE API Key": "গুগল পিএসই এপিআই কী",
|
||||
"Google PSE Engine Id": "গুগল পিএসই ইঞ্জিন আইডি",
|
||||
"h:mm a": "h:mm a",
|
||||
"has no conversations.": "কোন কনভার্সেশন আছে না।",
|
||||
"Hello, {{name}}": "হ্যালো, {{name}}",
|
||||
@@ -243,37 +254,41 @@
|
||||
"Images": "ছবিসমূহ",
|
||||
"Import Chats": "চ্যাটগুলি ইমপোর্ট করুন",
|
||||
"Import Documents Mapping": "ডকুমেন্টসমূহ ম্যাপিং ইমপোর্ট করুন",
|
||||
"Import Models": "",
|
||||
"Import Models": "মডেল আমদানি করুন",
|
||||
"Import Prompts": "প্রম্পটগুলো ইমপোর্ট করুন",
|
||||
"Include `--api` flag when running stable-diffusion-webui": "stable-diffusion-webui চালু করার সময় `--api` ফ্ল্যাগ সংযুক্ত করুন",
|
||||
"Info": "",
|
||||
"Info": "তথ্য",
|
||||
"Input commands": "ইনপুট কমান্ডস",
|
||||
"Install from Github URL": "",
|
||||
"Install from Github URL": "Github URL থেকে ইনস্টল করুন",
|
||||
"Instant Auto-Send After Voice Transcription": "",
|
||||
"Interface": "ইন্টারফেস",
|
||||
"Invalid Tag": "অবৈধ ট্যাগ",
|
||||
"January": "জানুয়ারী",
|
||||
"join our Discord for help.": "সাহায্যের জন্য আমাদের Discord-এ যুক্ত হোন",
|
||||
"JSON": "JSON",
|
||||
"JSON Preview": "",
|
||||
"JSON Preview": "JSON প্রিভিউ",
|
||||
"July": "জুলাই",
|
||||
"June": "জুন",
|
||||
"JWT Expiration": "JWT-র মেয়াদ",
|
||||
"JWT Token": "JWT টোকেন",
|
||||
"Keep Alive": "সচল রাখুন",
|
||||
"Keyboard shortcuts": "কিবোর্ড শর্টকাটসমূহ",
|
||||
"Knowledge": "",
|
||||
"Language": "ভাষা",
|
||||
"Last Active": "সর্বশেষ সক্রিয়",
|
||||
"Light": "লাইট",
|
||||
"Listening...": "শুনছে...",
|
||||
"Listening...": "",
|
||||
"LLMs can make mistakes. Verify important information.": "LLM ভুল করতে পারে। গুরুত্বপূর্ণ তথ্য যাচাই করে নিন।",
|
||||
"Local Models": "",
|
||||
"LTR": "LTR",
|
||||
"Made by OpenWebUI Community": "OpenWebUI কমিউনিটিকর্তৃক নির্মিত",
|
||||
"Make sure to enclose them with": "এটা দিয়ে বন্ধনী দিতে ভুলবেন না",
|
||||
"Manage": "",
|
||||
"Manage Models": "মডেলসমূহ ব্যবস্থাপনা করুন",
|
||||
"Manage Ollama Models": "Ollama মডেলসূহ ব্যবস্থাপনা করুন",
|
||||
"Manage Pipelines": "",
|
||||
"Manage Pipelines": "পাইপলাইন পরিচালনা করুন",
|
||||
"March": "মার্চ",
|
||||
"Max Tokens (num_predict)": "",
|
||||
"Max Tokens (num_predict)": "সর্বোচ্চ টোকেন (num_predict)",
|
||||
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "একসঙ্গে সর্বোচ্চ তিনটি মডেল ডাউনলোড করা যায়। দয়া করে পরে আবার চেষ্টা করুন।",
|
||||
"May": "মে",
|
||||
"Memories accessible by LLMs will be shown here.": "LLMs দ্বারা অ্যাক্সেসযোগ্য মেমোরিগুলি এখানে দেখানো হবে।",
|
||||
@@ -288,12 +303,12 @@
|
||||
"Model '{{modelName}}' has been successfully downloaded.": "'{{modelName}}' মডেল সফলভাবে ডাউনলোড হয়েছে।",
|
||||
"Model '{{modelTag}}' is already in queue for downloading.": "{{modelTag}} ডাউনলোডের জন্য আগে থেকেই অপেক্ষমান আছে।",
|
||||
"Model {{modelId}} not found": "{{modelId}} মডেল পাওয়া যায়নি",
|
||||
"Model {{modelName}} is not vision capable": "",
|
||||
"Model {{name}} is now {{status}}": "",
|
||||
"Model {{modelName}} is not vision capable": "মডেল {{modelName}} দৃষ্টি সক্ষম নয়",
|
||||
"Model {{name}} is now {{status}}": "মডেল {{name}} এখন {{status}}",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "মডেল ফাইলসিস্টেম পাথ পাওয়া গেছে। আপডেটের জন্য মডেলের শর্টনেম আবশ্যক, এগিয়ে যাওয়া যাচ্ছে না।",
|
||||
"Model ID": "",
|
||||
"Model ID": "মডেল ID",
|
||||
"Model not selected": "মডেল নির্বাচন করা হয়নি",
|
||||
"Model Params": "",
|
||||
"Model Params": "মডেল প্যারাম",
|
||||
"Model Whitelisting": "মডেল হোয়াইটলিস্টিং",
|
||||
"Model(s) Whitelisted": "হোয়াইটলিস্টেড মডেল(সমূহ)",
|
||||
"Modelfile Content": "মডেলফাইল কনটেন্ট",
|
||||
@@ -301,25 +316,27 @@
|
||||
"More": "আরো",
|
||||
"Name": "নাম",
|
||||
"Name Tag": "নামের ট্যাগ",
|
||||
"Name your model": "",
|
||||
"Name your model": "আপনার মডেলের নাম দিন",
|
||||
"New Chat": "নতুন চ্যাট",
|
||||
"New Password": "নতুন পাসওয়ার্ড",
|
||||
"No documents found": "",
|
||||
"No results found": "কোন ফলাফল পাওয়া যায়নি",
|
||||
"No search query generated": "",
|
||||
"No search query generated": "কোনও অনুসন্ধান ক্যোয়ারী উত্পন্ন হয়নি",
|
||||
"No source available": "কোন উৎস পাওয়া যায়নি",
|
||||
"None": "",
|
||||
"None": "কোনোটিই নয়",
|
||||
"Not factually correct": "তথ্যগত দিক থেকে সঠিক নয়",
|
||||
"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "দ্রষ্টব্য: আপনি যদি ন্যূনতম স্কোর সেট করেন তবে অনুসন্ধানটি কেবলমাত্র ন্যূনতম স্কোরের চেয়ে বেশি বা সমান স্কোর সহ নথিগুলি ফেরত দেবে।",
|
||||
"Notifications": "নোটিফিকেশনসমূহ",
|
||||
"November": "নভেম্বর",
|
||||
"num_thread (Ollama)": "",
|
||||
"num_thread (Ollama)": "num_thread (ওলামা)",
|
||||
"October": "অক্টোবর",
|
||||
"Off": "বন্ধ",
|
||||
"Okay, Let's Go!": "ঠিক আছে, চলুন যাই!",
|
||||
"OLED Dark": "OLED ডার্ক",
|
||||
"Ollama": "Ollama",
|
||||
"Ollama API": "",
|
||||
"Ollama API disabled": "",
|
||||
"Ollama API": "Ollama API",
|
||||
"Ollama API disabled": "Ollama API নিষ্ক্রিয় করা হয়েছে",
|
||||
"Ollama API is disabled": "",
|
||||
"Ollama Version": "Ollama ভার্সন",
|
||||
"On": "চালু",
|
||||
"Only": "শুধুমাত্র",
|
||||
@@ -342,10 +359,12 @@
|
||||
"PDF document (.pdf)": "PDF ডকুমেন্ট (.pdf)",
|
||||
"PDF Extract Images (OCR)": "পিডিএফ এর ছবি থেকে লেখা বের করুন (OCR)",
|
||||
"pending": "অপেক্ষমান",
|
||||
"Permission denied when accessing media devices": "",
|
||||
"Permission denied when accessing microphone": "",
|
||||
"Permission denied when accessing microphone: {{error}}": "মাইক্রোফোন ব্যবহারের অনুমতি পাওয়া যায়নি: {{error}}",
|
||||
"Personalization": "ডিজিটাল বাংলা",
|
||||
"Pipelines": "",
|
||||
"Pipelines Valves": "",
|
||||
"Pipelines": "পাইপলাইন",
|
||||
"Pipelines Valves": "পাইপলাইন ভালভ",
|
||||
"Plain text (.txt)": "প্লায়েন টেক্সট (.txt)",
|
||||
"Playground": "খেলাঘর",
|
||||
"Positive attitude": "পজিটিভ আক্রমণ",
|
||||
@@ -364,6 +383,7 @@
|
||||
"Read Aloud": "পড়াশোনা করুন",
|
||||
"Record voice": "ভয়েস রেকর্ড করুন",
|
||||
"Redirecting you to OpenWebUI Community": "আপনাকে OpenWebUI কমিউনিটিতে পাঠানো হচ্ছে",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"Refused when it shouldn't have": "যদি উপযুক্ত নয়, তবে রেজিগেনেট করা হচ্ছে",
|
||||
"Regenerate": "রেজিগেনেট করুন",
|
||||
"Release Notes": "রিলিজ নোটসমূহ",
|
||||
@@ -375,12 +395,14 @@
|
||||
"Reranking Model": "রির্যাক্টিং মডেল",
|
||||
"Reranking model disabled": "রির্যাক্টিং মডেল নিষ্ক্রিয় করা",
|
||||
"Reranking model set to \"{{reranking_model}}\"": "রির ্যাঙ্কিং মডেল \"{{reranking_model}}\" -এ সেট করা আছে",
|
||||
"Reset Upload Directory": "",
|
||||
"Reset Vector Storage": "ভেক্টর স্টোরেজ রিসেট করুন",
|
||||
"Response AutoCopy to Clipboard": "রেসপন্সগুলো স্বয়ংক্রিভাবে ক্লিপবোর্ডে কপি হবে",
|
||||
"Role": "পদবি",
|
||||
"Rosé Pine": "রোজ পাইন",
|
||||
"Rosé Pine Dawn": "ভোরের রোজ পাইন",
|
||||
"RTL": "RTL",
|
||||
"Running": "",
|
||||
"Save": "সংরক্ষণ",
|
||||
"Save & Create": "সংরক্ষণ এবং তৈরি করুন",
|
||||
"Save & Update": "সংরক্ষণ এবং আপডেট করুন",
|
||||
@@ -390,49 +412,54 @@
|
||||
"Scan for documents from {{path}}": "ডকুমেন্টসমূহের জন্য {{path}} স্ক্যান করুন",
|
||||
"Search": "অনুসন্ধান",
|
||||
"Search a model": "মডেল অনুসন্ধান করুন",
|
||||
"Search Chats": "",
|
||||
"Search Chats": "চ্যাট অনুসন্ধান করুন",
|
||||
"Search Documents": "ডকুমেন্টসমূহ অনুসন্ধান করুন",
|
||||
"Search Models": "",
|
||||
"Search Models": "অনুসন্ধান মডেল",
|
||||
"Search Prompts": "প্রম্পটসমূহ অনুসন্ধান করুন",
|
||||
"Search Result Count": "",
|
||||
"Searched {{count}} sites_one": "",
|
||||
"Searched {{count}} sites_other": "",
|
||||
"Searching the web for '{{searchQuery}}'": "",
|
||||
"Searxng Query URL": "",
|
||||
"Search Query Generation Prompt": "",
|
||||
"Search Query Generation Prompt Length Threshold": "",
|
||||
"Search Result Count": "অনুসন্ধানের ফলাফল গণনা",
|
||||
"Searched {{count}} sites_one": "{{কাউন্ট}} অনুসন্ধান করা হয়েছে sites_one",
|
||||
"Searched {{count}} sites_other": "{{কাউন্ট}} অনুসন্ধান করা হয়েছে sites_other",
|
||||
"Searching the web for '{{searchQuery}}'": "'{{searchQuery}}' এর জন্য ওয়েবে অনুসন্ধান করা হচ্ছে",
|
||||
"Searxng Query URL": "Searxng ক্যোয়ারী URL",
|
||||
"See readme.md for instructions": "নির্দেশিকার জন্য readme.md দেখুন",
|
||||
"See what's new": "নতুন কী আছে দেখুন",
|
||||
"Seed": "সীড",
|
||||
"Select a base model": "",
|
||||
"Select a base model": "একটি বেস মডেল নির্বাচন করুন",
|
||||
"Select a engine": "",
|
||||
"Select a mode": "একটি মডেল নির্বাচন করুন",
|
||||
"Select a model": "একটি মডেল নির্বাচন করুন",
|
||||
"Select a pipeline": "",
|
||||
"Select a pipeline url": "",
|
||||
"Select a pipeline": "একটি পাইপলাইন নির্বাচন করুন",
|
||||
"Select a pipeline url": "একটি পাইপলাইন URL নির্বাচন করুন",
|
||||
"Select an Ollama instance": "একটি Ollama ইন্সট্যান্স নির্বাচন করুন",
|
||||
"Select model": "মডেল নির্বাচন করুন",
|
||||
"Selected model(s) do not support image inputs": "",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "নির্বাচিত মডেল(গুলি) চিত্র ইনপুট সমর্থন করে না",
|
||||
"Send": "পাঠান",
|
||||
"Send a Message": "একটি মেসেজ পাঠান",
|
||||
"Send message": "মেসেজ পাঠান",
|
||||
"September": "সেপ্টেম্বর",
|
||||
"Serper API Key": "",
|
||||
"Serpstack API Key": "",
|
||||
"Serper API Key": "Serper API Key",
|
||||
"Serpstack API Key": "Serpstack API Key",
|
||||
"Server connection verified": "সার্ভার কানেকশন যাচাই করা হয়েছে",
|
||||
"Set as default": "ডিফল্ট হিসেবে নির্ধারণ করুন",
|
||||
"Set Default Model": "ডিফল্ট মডেল নির্ধারণ করুন",
|
||||
"Set embedding model (e.g. {{model}})": "ইমেম্বিং মডেল নির্ধারণ করুন (উদাহরণ {{model}})",
|
||||
"Set Image Size": "ছবির সাইজ নির্ধারণ করুন",
|
||||
"Set Model": "মডেল নির্ধারণ করুন",
|
||||
"Set reranking model (e.g. {{model}})": "রি-র্যাংকিং মডেল নির্ধারণ করুন (উদাহরণ {{model}})",
|
||||
"Set Steps": "পরবর্তী ধাপসমূহ",
|
||||
"Set Task Model": "",
|
||||
"Set Task Model": "টাস্ক মডেল সেট করুন",
|
||||
"Set Voice": "কন্ঠস্বর নির্ধারণ করুন",
|
||||
"Settings": "সেটিংসমূহ",
|
||||
"Settings saved successfully!": "সেটিংগুলো সফলভাবে সংরক্ষিত হয়েছে",
|
||||
"Settings updated successfully": "",
|
||||
"Share": "শেয়ার করুন",
|
||||
"Share Chat": "চ্যাট শেয়ার করুন",
|
||||
"Share to OpenWebUI Community": "OpenWebUI কমিউনিটিতে শেয়ার করুন",
|
||||
"short-summary": "সংক্ষিপ্ত বিবরণ",
|
||||
"Show": "দেখান",
|
||||
"Show Admin Details in Account Pending Overlay": "",
|
||||
"Show shortcuts": "শর্টকাটগুলো দেখান",
|
||||
"Showcased creativity": "সৃজনশীলতা প্রদর্শন",
|
||||
"sidebar": "সাইডবার",
|
||||
@@ -443,8 +470,8 @@
|
||||
"Source": "উৎস",
|
||||
"Speech recognition error: {{error}}": "স্পিচ রিকগনিশনে সমস্যা: {{error}}",
|
||||
"Speech-to-Text Engine": "স্পিচ-টু-টেক্সট ইঞ্জিন",
|
||||
"SpeechRecognition API is not supported in this browser.": "এই ব্রাউজার স্পিচরিকগনিশন এপিআই সাপোর্ট করে না।",
|
||||
"Stop Sequence": "সিকোয়েন্স থামান",
|
||||
"STT Model": "",
|
||||
"STT Settings": "STT সেটিংস",
|
||||
"Submit": "সাবমিট",
|
||||
"Subtitle (e.g. about the Roman Empire)": "সাবটাইটল (রোমান ইম্পার্টের সম্পর্কে)",
|
||||
@@ -463,7 +490,9 @@
|
||||
"Thanks for your feedback!": "আপনার মতামত ধন্যবাদ!",
|
||||
"The score should be a value between 0.0 (0%) and 1.0 (100%).": "স্কোর একটি 0.0 (0%) এবং 1.0 (100%) এর মধ্যে একটি মান হওয়া উচিত।",
|
||||
"Theme": "থিম",
|
||||
"Thinking...": "",
|
||||
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "এটা নিশ্চিত করে যে, আপনার গুরুত্বপূর্ণ আলোচনা নিরাপদে আপনার ব্যাকএন্ড ডেটাবেজে সংরক্ষিত আছে। ধন্যবাদ!",
|
||||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||
"This setting does not sync across browsers or devices.": "এই সেটিং অন্যন্য ব্রাউজার বা ডিভাইসের সাথে সিঙ্ক্রোনাইজ নয় না।",
|
||||
"Thorough explanation": "পুঙ্খানুপুঙ্খ ব্যাখ্যা",
|
||||
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "পরামর্শ: একাধিক ভেরিয়েবল স্লট একের পর এক রিপ্লেস করার জন্য চ্যাট ইনপুটে কিবোর্ডের Tab বাটন ব্যবহার করুন।",
|
||||
@@ -475,6 +504,7 @@
|
||||
"to": "প্রতি",
|
||||
"To access the available model names for downloading,": "ডাউনলোডের জন্য এভেইলএবল মডেলের নামগুলো এক্সেস করতে,",
|
||||
"To access the GGUF models available for downloading,": "ডাউলোডের জন্য এভেইলএবল GGUF মডেলগুলো এক্সেস করতে,",
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"to chat input.": "চ্যাট ইনপুটে",
|
||||
"Today": "আজ",
|
||||
"Toggle settings": "সেটিংস টোগল",
|
||||
@@ -482,22 +512,25 @@
|
||||
"Top K": "Top K",
|
||||
"Top P": "Top P",
|
||||
"Trouble accessing Ollama?": "Ollama এক্সেস করতে সমস্যা হচ্ছে?",
|
||||
"TTS Model": "",
|
||||
"TTS Settings": "TTS সেটিংসমূহ",
|
||||
"Type": "",
|
||||
"TTS Voice": "",
|
||||
"Type": "টাইপ",
|
||||
"Type Hugging Face Resolve (Download) URL": "Hugging Face থেকে ডাউনলোড করার ইউআরএল টাইপ করুন",
|
||||
"Uh-oh! There was an issue connecting to {{provider}}.": "ওহ-হো! {{provider}} এর সাথে কানেকশনে সমস্যা হয়েছে।",
|
||||
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "অপরিচিত ফাইল ফরম্যাট '{{file_type}}', তবে প্লেইন টেক্সট হিসেবে গ্রহণ করা হলো",
|
||||
"Update and Copy Link": "আপডেট এবং লিংক কপি করুন",
|
||||
"Update password": "পাসওয়ার্ড আপডেট করুন",
|
||||
"Upload a GGUF model": "একটি GGUF মডেল আপলোড করুন",
|
||||
"Upload Files": "",
|
||||
"Upload Files": "ফাইল আপলোড করুন",
|
||||
"Upload Pipeline": "",
|
||||
"Upload Progress": "আপলোড হচ্ছে",
|
||||
"URL Mode": "ইউআরএল মোড",
|
||||
"Use '#' in the prompt input to load and select your documents.": "আপনার ডকুমেন্টসমূহ নির্বাচন করার জন্য আপনার প্রম্পট ইনপুটে '# ব্যবহার করুন।",
|
||||
"Use Gravatar": "Gravatar ব্যবহার করুন",
|
||||
"Use Initials": "নামের আদ্যক্ষর ব্যবহার করুন",
|
||||
"use_mlock (Ollama)": "",
|
||||
"use_mmap (Ollama)": "",
|
||||
"use_mlock (Ollama)": "use_mlock (ওলামা)",
|
||||
"use_mmap (Ollama)": "use_mmap (ওলামা)",
|
||||
"user": "ব্যবহারকারী",
|
||||
"User Permissions": "ইউজার পারমিশনসমূহ",
|
||||
"Users": "ব্যাবহারকারীগণ",
|
||||
@@ -506,30 +539,34 @@
|
||||
"variable": "ভেরিয়েবল",
|
||||
"variable to have them replaced with clipboard content.": "ক্লিপবোর্ডের কন্টেন্ট দিয়ে যেই ভেরিয়েবল রিপ্লেস করা যাবে।",
|
||||
"Version": "ভার্সন",
|
||||
"Warning": "",
|
||||
"Warning": "সতর্কীকরণ",
|
||||
"Warning: If you update or change your embedding model, you will need to re-import all documents.": "সতর্কীকরণ: আপনি যদি আপনার এম্বেডিং মডেল আপডেট বা পরিবর্তন করেন, তাহলে আপনাকে সমস্ত নথি পুনরায় আমদানি করতে হবে।.",
|
||||
"Web": "ওয়েব",
|
||||
"Web API": "",
|
||||
"Web Loader Settings": "ওয়েব লোডার সেটিংস",
|
||||
"Web Params": "ওয়েব প্যারামিটারসমূহ",
|
||||
"Web Search": "",
|
||||
"Web Search Engine": "",
|
||||
"Web Search": "ওয়েব অনুসন্ধান",
|
||||
"Web Search Engine": "ওয়েব সার্চ ইঞ্জিন",
|
||||
"Webhook URL": "ওয়েবহুক URL",
|
||||
"WebUI Add-ons": "WebUI এড-অনসমূহ",
|
||||
"WebUI Settings": "WebUI সেটিংসমূহ",
|
||||
"WebUI will make requests to": "WebUI যেখানে রিকোয়েস্ট পাঠাবে",
|
||||
"What’s New in": "এতে নতুন কী",
|
||||
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "যদি হিস্টোরি বন্ধ থাকে তাহলে এই ব্রাউজারের নতুন চ্যাটগুলো আপনার কোন ডিভাইসের হিস্টোরিতেই দেখা যাবে না।",
|
||||
"Whisper (Local)": "Whisper (লোকাল)",
|
||||
"Whisper (Local)": "",
|
||||
"Widescreen Mode": "",
|
||||
"Workspace": "ওয়ার্কস্পেস",
|
||||
"Write a prompt suggestion (e.g. Who are you?)": "একটি প্রম্পট সাজেশন লিখুন (যেমন Who are you?)",
|
||||
"Write a summary in 50 words that summarizes [topic or keyword].": "৫০ শব্দের মধ্যে [topic or keyword] এর একটি সারসংক্ষেপ লিখুন।",
|
||||
"Yesterday": "আগামী",
|
||||
"You": "আপনি",
|
||||
"You cannot clone a base model": "",
|
||||
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "",
|
||||
"You cannot clone a base model": "আপনি একটি বেস মডেল ক্লোন করতে পারবেন না",
|
||||
"You have no archived conversations.": "আপনার কোনও আর্কাইভ করা কথোপকথন নেই।",
|
||||
"You have shared this chat": "আপনি এই চ্যাটটি শেয়ার করেছেন",
|
||||
"You're a helpful assistant.": "আপনি একজন উপকারী এসিস্ট্যান্ট",
|
||||
"You're now logged in.": "আপনি এখন লগইন করা অবস্থায় আছেন",
|
||||
"Your account status is currently pending activation.": "",
|
||||
"Youtube": "YouTube",
|
||||
"Youtube Loader Settings": "YouTube লোডার সেটিংস"
|
||||
}
|
||||
|
||||
@@ -3,19 +3,21 @@
|
||||
"(Beta)": "(Beta)",
|
||||
"(e.g. `sh webui.sh --api`)": "(p. ex. `sh webui.sh --api`)",
|
||||
"(latest)": "(últim)",
|
||||
"{{ models }}": "",
|
||||
"{{ owner }}: You cannot delete a base model": "",
|
||||
"{{ models }}": "{{ models }}",
|
||||
"{{ owner }}: You cannot delete a base model": "{{ propietari }}: No es pot suprimir un model base",
|
||||
"{{modelName}} is thinking...": "{{modelName}} està pensant...",
|
||||
"{{user}}'s Chats": "{{user}}'s Chats",
|
||||
"{{webUIName}} Backend Required": "Es requereix Backend de {{webUIName}}",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "Un model de tasca s'utilitza quan es realitzen tasques com ara generar títols per a xats i consultes de cerca web",
|
||||
"a user": "un usuari",
|
||||
"About": "Sobre",
|
||||
"Account": "Compte",
|
||||
"Account Activation Pending": "",
|
||||
"Accurate information": "Informació precisa",
|
||||
"Active Users": "",
|
||||
"Add": "Afegir",
|
||||
"Add a model id": "",
|
||||
"Add a short description about what this model does": "",
|
||||
"Add a model id": "Afegir un identificador de model",
|
||||
"Add a short description about what this model does": "Afegiu una breu descripció sobre què fa aquest model",
|
||||
"Add a short title for this prompt": "Afegeix un títol curt per aquest prompt",
|
||||
"Add a tag": "Afegeix una etiqueta",
|
||||
"Add custom prompt": "Afegir un prompt personalitzat",
|
||||
@@ -28,15 +30,17 @@
|
||||
"Add User": "Afegir Usuari",
|
||||
"Adjusting these settings will apply changes universally to all users.": "Ajustar aquests paràmetres aplicarà canvis de manera universal a tots els usuaris.",
|
||||
"admin": "administrador",
|
||||
"Admin": "",
|
||||
"Admin Panel": "Panell d'Administració",
|
||||
"Admin Settings": "Configuració d'Administració",
|
||||
"Advanced Parameters": "Paràmetres Avançats",
|
||||
"Advanced Params": "",
|
||||
"Advanced Params": "Paràmetres avançats",
|
||||
"all": "tots",
|
||||
"All Documents": "Tots els Documents",
|
||||
"All Users": "Tots els Usuaris",
|
||||
"Allow": "Permet",
|
||||
"Allow Chat Deletion": "Permet la Supressió del Xat",
|
||||
"Allow non-local voices": "",
|
||||
"alphanumeric characters and hyphens": "caràcters alfanumèrics i guions",
|
||||
"Already have an account?": "Ja tens un compte?",
|
||||
"an assistant": "un assistent",
|
||||
@@ -48,7 +52,7 @@
|
||||
"API keys": "Claus de l'API",
|
||||
"April": "Abril",
|
||||
"Archive": "Arxiu",
|
||||
"Archive All Chats": "",
|
||||
"Archive All Chats": "Arxiva tots els xats",
|
||||
"Archived Chats": "Arxiu d'historial de xat",
|
||||
"are allowed - Activate this command by typing": "estan permesos - Activa aquesta comanda escrivint",
|
||||
"Are you sure?": "Estàs segur?",
|
||||
@@ -57,20 +61,22 @@
|
||||
"Audio": "Àudio",
|
||||
"August": "Agost",
|
||||
"Auto-playback response": "Resposta de reproducció automàtica",
|
||||
"Auto-send input after 3 sec.": "Enviar entrada automàticament després de 3 segons",
|
||||
"AUTOMATIC1111 Base URL": "URL Base AUTOMATIC1111",
|
||||
"AUTOMATIC1111 Base URL is required.": "Es requereix l'URL Base AUTOMATIC1111.",
|
||||
"available!": "disponible!",
|
||||
"Back": "Enrere",
|
||||
"Bad Response": "Resposta Erroni",
|
||||
"Banners": "",
|
||||
"Base Model (From)": "",
|
||||
"Banners": "Banners",
|
||||
"Base Model (From)": "Model base (des de)",
|
||||
"before": "abans",
|
||||
"Being lazy": "Ser l'estupidez",
|
||||
"Brave Search API Key": "",
|
||||
"Brave Search API Key": "Clau API Brave Search",
|
||||
"Bypass SSL verification for Websites": "Desactivar la verificació SSL per a l'accés a l'Internet",
|
||||
"Call": "",
|
||||
"Call feature is not supported when using Web STT engine": "",
|
||||
"Camera": "",
|
||||
"Cancel": "Cancel·la",
|
||||
"Capabilities": "",
|
||||
"Capabilities": "Capacitats",
|
||||
"Change Password": "Canvia la Contrasenya",
|
||||
"Chat": "Xat",
|
||||
"Chat Bubble UI": "Chat Bubble UI",
|
||||
@@ -86,35 +92,36 @@
|
||||
"Chunk Params": "Paràmetres de Blocs",
|
||||
"Chunk Size": "Mida del Bloc",
|
||||
"Citation": "Citació",
|
||||
"Clear memory": "",
|
||||
"Click here for help.": "Fes clic aquí per ajuda.",
|
||||
"Click here to": "Fes clic aquí per",
|
||||
"Click here to select": "Fes clic aquí per seleccionar",
|
||||
"Click here to select a csv file.": "Fes clic aquí per seleccionar un fitxer csv.",
|
||||
"Click here to select a py file.": "",
|
||||
"Click here to select documents.": "Fes clic aquí per seleccionar documents.",
|
||||
"click here.": "fes clic aquí.",
|
||||
"Click on the user role button to change a user's role.": "Fes clic al botó de rol d'usuari per canviar el rol d'un usuari.",
|
||||
"Clone": "",
|
||||
"Clone": "Clon",
|
||||
"Close": "Tanca",
|
||||
"Collection": "Col·lecció",
|
||||
"ComfyUI": "ComfyUI",
|
||||
"ComfyUI Base URL": "URL base de ComfyUI",
|
||||
"ComfyUI Base URL is required.": "URL base de ComfyUI és obligatòria.",
|
||||
"Command": "Comanda",
|
||||
"Concurrent Requests": "",
|
||||
"Concurrent Requests": "Sol·licituds simultànies",
|
||||
"Confirm Password": "Confirma la Contrasenya",
|
||||
"Connections": "Connexions",
|
||||
"Contact Admin for WebUI Access": "",
|
||||
"Content": "Contingut",
|
||||
"Context Length": "Longitud del Context",
|
||||
"Continue Response": "Continua la Resposta",
|
||||
"Conversation Mode": "Mode de Conversa",
|
||||
"Copied shared chat URL to clipboard!": "S'ha copiat l'URL compartida al porta-retalls!",
|
||||
"Copy": "Copiar",
|
||||
"Copy last code block": "Copia l'últim bloc de codi",
|
||||
"Copy last response": "Copia l'última resposta",
|
||||
"Copy Link": "Copiar l'enllaç",
|
||||
"Copying to clipboard was successful!": "La còpia al porta-retalls ha estat exitosa!",
|
||||
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "Crea una frase concisa de 3-5 paraules com a capçalera per a la següent consulta, seguint estrictament el límit de 3-5 paraules i evitant l'ús de la paraula 'títol':",
|
||||
"Create a model": "",
|
||||
"Create a model": "Crear un model",
|
||||
"Create Account": "Crea un Compte",
|
||||
"Create new key": "Crea una nova clau",
|
||||
"Create new secret key": "Crea una nova clau secreta",
|
||||
@@ -123,38 +130,39 @@
|
||||
"Current Model": "Model Actual",
|
||||
"Current Password": "Contrasenya Actual",
|
||||
"Custom": "Personalitzat",
|
||||
"Customize models for a specific purpose": "",
|
||||
"Customize models for a specific purpose": "Personalitzar models per a un propòsit específic",
|
||||
"Dark": "Fosc",
|
||||
"Dashboard": "",
|
||||
"Database": "Base de Dades",
|
||||
"December": "Desembre",
|
||||
"Default": "Per defecte",
|
||||
"Default (Automatic1111)": "Per defecte (Automatic1111)",
|
||||
"Default (SentenceTransformers)": "Per defecte (SentenceTransformers)",
|
||||
"Default (Web API)": "Per defecte (Web API)",
|
||||
"Default Model": "",
|
||||
"Default Model": "Model per defecte",
|
||||
"Default model updated": "Model per defecte actualitzat",
|
||||
"Default Prompt Suggestions": "Suggeriments de Prompt Per Defecte",
|
||||
"Default User Role": "Rol d'Usuari Per Defecte",
|
||||
"delete": "esborra",
|
||||
"Delete": "Esborra",
|
||||
"Delete a model": "Esborra un model",
|
||||
"Delete All Chats": "",
|
||||
"Delete All Chats": "Suprimir tots els xats",
|
||||
"Delete chat": "Esborra xat",
|
||||
"Delete Chat": "Esborra Xat",
|
||||
"delete this link": "Esborra aquest enllaç",
|
||||
"Delete User": "Esborra Usuari",
|
||||
"Deleted {{deleteModelTag}}": "Esborrat {{deleteModelTag}}",
|
||||
"Deleted {{name}}": "",
|
||||
"Deleted {{name}}": "Suprimit {{nom}}",
|
||||
"Description": "Descripció",
|
||||
"Didn't fully follow instructions": "No s'ha completat els instruccions",
|
||||
"Disabled": "Desactivat",
|
||||
"Discover a model": "",
|
||||
"Discover a model": "Descobreix un model",
|
||||
"Discover a prompt": "Descobreix un prompt",
|
||||
"Discover, download, and explore custom prompts": "Descobreix, descarrega i explora prompts personalitzats",
|
||||
"Discover, download, and explore model presets": "Descobreix, descarrega i explora presets de models",
|
||||
"Dismissible": "",
|
||||
"Display the username instead of You in the Chat": "Mostra el nom d'usuari en lloc de 'Tu' al Xat",
|
||||
"Document": "Document",
|
||||
"Document Settings": "Configuració de Documents",
|
||||
"Documentation": "",
|
||||
"Documents": "Documents",
|
||||
"does not make any external connections, and your data stays securely on your locally hosted server.": "no realitza connexions externes, i les teves dades romanen segures al teu servidor allotjat localment.",
|
||||
"Don't Allow": "No Permetre",
|
||||
@@ -169,31 +177,31 @@
|
||||
"Edit Doc": "Edita Document",
|
||||
"Edit User": "Edita Usuari",
|
||||
"Email": "Correu electrònic",
|
||||
"Embedding Batch Size": "",
|
||||
"Embedding Model": "Model d'embutiment",
|
||||
"Embedding Model Engine": "Motor de model d'embutiment",
|
||||
"Embedding model set to \"{{embedding_model}}\"": "Model d'embutiment configurat a \"{{embedding_model}}\"",
|
||||
"Enable Chat History": "Activa Historial de Xat",
|
||||
"Enable Community Sharing": "",
|
||||
"Enable Community Sharing": "Activar l'ús compartit de la comunitat",
|
||||
"Enable New Sign Ups": "Permet Noves Inscripcions",
|
||||
"Enable Web Search": "",
|
||||
"Enabled": "Activat",
|
||||
"Enable Web Search": "Activa la cerca web",
|
||||
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Assegura't que el fitxer CSV inclou 4 columnes en aquest ordre: Nom, Correu Electrònic, Contrasenya, Rol.",
|
||||
"Enter {{role}} message here": "Introdueix aquí el missatge de {{role}}",
|
||||
"Enter a detail about yourself for your LLMs to recall": "Introdueix un detall sobre tu per que els LLMs puguin recordar-te",
|
||||
"Enter Brave Search API Key": "",
|
||||
"Enter Brave Search API Key": "Introduïu la clau de l'API Brave Search",
|
||||
"Enter Chunk Overlap": "Introdueix el Solapament de Blocs",
|
||||
"Enter Chunk Size": "Introdueix la Mida del Bloc",
|
||||
"Enter Github Raw URL": "",
|
||||
"Enter Google PSE API Key": "",
|
||||
"Enter Google PSE Engine Id": "",
|
||||
"Enter Github Raw URL": "Introduïu l'URL en brut de Github",
|
||||
"Enter Google PSE API Key": "Introduïu la clau de l'API de Google PSE",
|
||||
"Enter Google PSE Engine Id": "Introduïu l'identificador del motor PSE de Google",
|
||||
"Enter Image Size (e.g. 512x512)": "Introdueix la Mida de la Imatge (p. ex. 512x512)",
|
||||
"Enter language codes": "Introdueix els codis de llenguatge",
|
||||
"Enter model tag (e.g. {{modelTag}})": "Introdueix l'etiqueta del model (p. ex. {{modelTag}})",
|
||||
"Enter Number of Steps (e.g. 50)": "Introdueix el Nombre de Passos (p. ex. 50)",
|
||||
"Enter Score": "Introdueix el Puntuació",
|
||||
"Enter Searxng Query URL": "",
|
||||
"Enter Serper API Key": "",
|
||||
"Enter Serpstack API Key": "",
|
||||
"Enter Searxng Query URL": "Introduïu l'URL de consulta de Searxng",
|
||||
"Enter Serper API Key": "Introduïu la clau de l'API Serper",
|
||||
"Enter Serpstack API Key": "Introduïu la clau de l'API Serpstack",
|
||||
"Enter stop sequence": "Introdueix la seqüència de parada",
|
||||
"Enter Top K": "Introdueix Top K",
|
||||
"Enter URL (e.g. http://127.0.0.1:7860/)": "Introdueix l'URL (p. ex. http://127.0.0.1:7860/)",
|
||||
@@ -202,16 +210,19 @@
|
||||
"Enter Your Full Name": "Introdueix el Teu Nom Complet",
|
||||
"Enter Your Password": "Introdueix la Teva Contrasenya",
|
||||
"Enter Your Role": "Introdueix el Teu Ròl",
|
||||
"Error": "",
|
||||
"Error": "Error",
|
||||
"Experimental": "Experimental",
|
||||
"Export": "",
|
||||
"Export": "Exportar",
|
||||
"Export All Chats (All Users)": "Exporta Tots els Xats (Tots els Usuaris)",
|
||||
"Export chat (.json)": "",
|
||||
"Export Chats": "Exporta Xats",
|
||||
"Export Documents Mapping": "Exporta el Mapatge de Documents",
|
||||
"Export Models": "",
|
||||
"Export Models": "Models d'exportació",
|
||||
"Export Prompts": "Exporta Prompts",
|
||||
"External Models": "",
|
||||
"Failed to create API Key.": "No s'ha pogut crear la clau d'API.",
|
||||
"Failed to read clipboard contents": "No s'ha pogut llegir el contingut del porta-retalls",
|
||||
"Failed to update settings": "",
|
||||
"February": "Febrer",
|
||||
"Feel free to add specific details": "Siusplau, afegeix detalls específics",
|
||||
"File Mode": "Mode Arxiu",
|
||||
@@ -221,15 +232,15 @@
|
||||
"Focus chat input": "Enfoca l'entrada del xat",
|
||||
"Followed instructions perfectly": "Siguiu les instruccions perfeicte",
|
||||
"Format your variables using square brackets like this:": "Formata les teves variables utilitzant claudàtors així:",
|
||||
"Frequency Penalty": "",
|
||||
"Full Screen Mode": "Mode de Pantalla Completa",
|
||||
"Frequency Penalty": "Pena de freqüència",
|
||||
"General": "General",
|
||||
"General Settings": "Configuració General",
|
||||
"Generating search query": "",
|
||||
"Generate Image": "",
|
||||
"Generating search query": "Generació de consultes de cerca",
|
||||
"Generation Info": "Informació de Generació",
|
||||
"Good Response": "Resposta bona",
|
||||
"Google PSE API Key": "",
|
||||
"Google PSE Engine Id": "",
|
||||
"Google PSE API Key": "Clau de l'API PSE de Google",
|
||||
"Google PSE Engine Id": "Identificador del motor PSE de Google",
|
||||
"h:mm a": "h:mm a",
|
||||
"has no conversations.": "no té converses.",
|
||||
"Hello, {{name}}": "Hola, {{name}}",
|
||||
@@ -243,37 +254,41 @@
|
||||
"Images": "Imatges",
|
||||
"Import Chats": "Importa Xats",
|
||||
"Import Documents Mapping": "Importa el Mapa de Documents",
|
||||
"Import Models": "",
|
||||
"Import Models": "Models d'importació",
|
||||
"Import Prompts": "Importa Prompts",
|
||||
"Include `--api` flag when running stable-diffusion-webui": "Inclou la bandera `--api` quan executis stable-diffusion-webui",
|
||||
"Info": "",
|
||||
"Info": "Informació",
|
||||
"Input commands": "Entra ordres",
|
||||
"Install from Github URL": "",
|
||||
"Install from Github URL": "Instal·leu des de l'URL de Github",
|
||||
"Instant Auto-Send After Voice Transcription": "",
|
||||
"Interface": "Interfície",
|
||||
"Invalid Tag": "Etiqueta Inválida",
|
||||
"January": "Gener",
|
||||
"join our Discord for help.": "uneix-te al nostre Discord per ajuda.",
|
||||
"JSON": "JSON",
|
||||
"JSON Preview": "",
|
||||
"JSON Preview": "Vista prèvia de JSON",
|
||||
"July": "Juliol",
|
||||
"June": "Juny",
|
||||
"JWT Expiration": "Expiració de JWT",
|
||||
"JWT Token": "Token JWT",
|
||||
"Keep Alive": "Mantén Actiu",
|
||||
"Keyboard shortcuts": "Dreceres de Teclat",
|
||||
"Knowledge": "",
|
||||
"Language": "Idioma",
|
||||
"Last Active": "Últim Actiu",
|
||||
"Light": "Clar",
|
||||
"Listening...": "Escoltant...",
|
||||
"Listening...": "",
|
||||
"LLMs can make mistakes. Verify important information.": "Els LLMs poden cometre errors. Verifica la informació important.",
|
||||
"Local Models": "",
|
||||
"LTR": "LTR",
|
||||
"Made by OpenWebUI Community": "Creat per la Comunitat OpenWebUI",
|
||||
"Make sure to enclose them with": "Assegura't d'envoltar-los amb",
|
||||
"Manage": "",
|
||||
"Manage Models": "Gestiona Models",
|
||||
"Manage Ollama Models": "Gestiona Models Ollama",
|
||||
"Manage Pipelines": "",
|
||||
"Manage Pipelines": "Gestionar canonades",
|
||||
"March": "Març",
|
||||
"Max Tokens (num_predict)": "",
|
||||
"Max Tokens (num_predict)": "Max Fitxes (num_predict)",
|
||||
"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Es poden descarregar un màxim de 3 models simultàniament. Si us plau, prova-ho més tard.",
|
||||
"May": "Maig",
|
||||
"Memories accessible by LLMs will be shown here.": "Els memòries accessible per a LLMs es mostraran aquí.",
|
||||
@@ -288,12 +303,12 @@
|
||||
"Model '{{modelName}}' has been successfully downloaded.": "El model '{{modelName}}' s'ha descarregat amb èxit.",
|
||||
"Model '{{modelTag}}' is already in queue for downloading.": "El model '{{modelTag}}' ja està en cua per ser descarregat.",
|
||||
"Model {{modelId}} not found": "Model {{modelId}} no trobat",
|
||||
"Model {{modelName}} is not vision capable": "",
|
||||
"Model {{name}} is now {{status}}": "",
|
||||
"Model {{modelName}} is not vision capable": "El model {{modelName}} no és capaç de visió",
|
||||
"Model {{name}} is now {{status}}": "El model {{nom}} ara és {{estat}}",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "S'ha detectat el camí del sistema de fitxers del model. És necessari un nom curt del model per a actualitzar, no es pot continuar.",
|
||||
"Model ID": "",
|
||||
"Model ID": "Identificador del model",
|
||||
"Model not selected": "Model no seleccionat",
|
||||
"Model Params": "",
|
||||
"Model Params": "Paràmetres del model",
|
||||
"Model Whitelisting": "Llista Blanca de Models",
|
||||
"Model(s) Whitelisted": "Model(s) a la Llista Blanca",
|
||||
"Modelfile Content": "Contingut del Fitxer de Model",
|
||||
@@ -301,25 +316,27 @@
|
||||
"More": "Més",
|
||||
"Name": "Nom",
|
||||
"Name Tag": "Etiqueta de Nom",
|
||||
"Name your model": "",
|
||||
"Name your model": "Posa un nom al model",
|
||||
"New Chat": "Xat Nou",
|
||||
"New Password": "Nova Contrasenya",
|
||||
"No documents found": "",
|
||||
"No results found": "No s'han trobat resultats",
|
||||
"No search query generated": "",
|
||||
"No search query generated": "No es genera cap consulta de cerca",
|
||||
"No source available": "Sense font disponible",
|
||||
"None": "",
|
||||
"None": "Cap",
|
||||
"Not factually correct": "No està clarament correcte",
|
||||
"Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: Si establiscs una puntuació mínima, la cerca només retornarà documents amb una puntuació major o igual a la puntuació mínima.",
|
||||
"Notifications": "Notificacions d'Escriptori",
|
||||
"November": "Novembre",
|
||||
"num_thread (Ollama)": "",
|
||||
"num_thread (Ollama)": "num_thread (Ollama)",
|
||||
"October": "Octubre",
|
||||
"Off": "Desactivat",
|
||||
"Okay, Let's Go!": "D'acord, Anem!",
|
||||
"OLED Dark": "OLED Fosc",
|
||||
"Ollama": "Ollama",
|
||||
"Ollama API": "",
|
||||
"Ollama API disabled": "",
|
||||
"Ollama API": "API d'Ollama",
|
||||
"Ollama API disabled": "L'API d'Ollama desactivada",
|
||||
"Ollama API is disabled": "",
|
||||
"Ollama Version": "Versió d'Ollama",
|
||||
"On": "Activat",
|
||||
"Only": "Només",
|
||||
@@ -342,10 +359,12 @@
|
||||
"PDF document (.pdf)": "Document PDF (.pdf)",
|
||||
"PDF Extract Images (OCR)": "Extreu Imatges de PDF (OCR)",
|
||||
"pending": "pendent",
|
||||
"Permission denied when accessing media devices": "",
|
||||
"Permission denied when accessing microphone": "",
|
||||
"Permission denied when accessing microphone: {{error}}": "Permís denegat en accedir al micròfon: {{error}}",
|
||||
"Personalization": "Personalització",
|
||||
"Pipelines": "",
|
||||
"Pipelines Valves": "",
|
||||
"Pipelines": "Canonades",
|
||||
"Pipelines Valves": "Vàlvules de canonades",
|
||||
"Plain text (.txt)": "Text pla (.txt)",
|
||||
"Playground": "Zona de Jocs",
|
||||
"Positive attitude": "Attitudin positiva",
|
||||
@@ -364,6 +383,7 @@
|
||||
"Read Aloud": "Llegiu al voltant",
|
||||
"Record voice": "Enregistra veu",
|
||||
"Redirecting you to OpenWebUI Community": "Redirigint-te a la Comunitat OpenWebUI",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"Refused when it shouldn't have": "Refusat quan no hauria d'haver-ho",
|
||||
"Regenerate": "Regenerar",
|
||||
"Release Notes": "Notes de la Versió",
|
||||
@@ -375,12 +395,14 @@
|
||||
"Reranking Model": "Model de Reranking desactivat",
|
||||
"Reranking model disabled": "Model de Reranking desactivat",
|
||||
"Reranking model set to \"{{reranking_model}}\"": "Model de Reranking establert a \"{{reranking_model}}\"",
|
||||
"Reset Upload Directory": "",
|
||||
"Reset Vector Storage": "Reinicia l'Emmagatzematge de Vectors",
|
||||
"Response AutoCopy to Clipboard": "Resposta AutoCopiar al Portapapers",
|
||||
"Role": "Rol",
|
||||
"Rosé Pine": "Rosé Pine",
|
||||
"Rosé Pine Dawn": "Albada Rosé Pine",
|
||||
"RTL": "RTL",
|
||||
"Running": "",
|
||||
"Save": "Guarda",
|
||||
"Save & Create": "Guarda i Crea",
|
||||
"Save & Update": "Guarda i Actualitza",
|
||||
@@ -390,50 +412,55 @@
|
||||
"Scan for documents from {{path}}": "Escaneja documents des de {{path}}",
|
||||
"Search": "Cerca",
|
||||
"Search a model": "Cerca un model",
|
||||
"Search Chats": "",
|
||||
"Search Chats": "Cercar xats",
|
||||
"Search Documents": "Cerca Documents",
|
||||
"Search Models": "",
|
||||
"Search Models": "Models de cerca",
|
||||
"Search Prompts": "Cerca Prompts",
|
||||
"Search Result Count": "",
|
||||
"Searched {{count}} sites_one": "",
|
||||
"Searched {{count}} sites_many": "",
|
||||
"Searched {{count}} sites_other": "",
|
||||
"Searching the web for '{{searchQuery}}'": "",
|
||||
"Searxng Query URL": "",
|
||||
"Search Query Generation Prompt": "",
|
||||
"Search Query Generation Prompt Length Threshold": "",
|
||||
"Search Result Count": "Recompte de resultats de cerca",
|
||||
"Searched {{count}} sites_one": "Cercat {{count}} sites_one",
|
||||
"Searched {{count}} sites_many": "Cercat {{recompte}} sites_many",
|
||||
"Searched {{count}} sites_other": "Cercat {{recompte}} sites_other",
|
||||
"Searching the web for '{{searchQuery}}'": "Cerca a la web de '{{searchQuery}}'",
|
||||
"Searxng Query URL": "Searxng URL de consulta",
|
||||
"See readme.md for instructions": "Consulta el readme.md per a instruccions",
|
||||
"See what's new": "Veure novetats",
|
||||
"Seed": "Llavor",
|
||||
"Select a base model": "",
|
||||
"Select a base model": "Seleccionar un model base",
|
||||
"Select a engine": "",
|
||||
"Select a mode": "Selecciona un mode",
|
||||
"Select a model": "Selecciona un model",
|
||||
"Select a pipeline": "",
|
||||
"Select a pipeline url": "",
|
||||
"Select a pipeline": "Seleccioneu una canonada",
|
||||
"Select a pipeline url": "Seleccionar un URL de canonada",
|
||||
"Select an Ollama instance": "Selecciona una instància d'Ollama",
|
||||
"Select model": "Selecciona un model",
|
||||
"Selected model(s) do not support image inputs": "",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "Els models seleccionats no admeten l'entrada d'imatges",
|
||||
"Send": "Envia",
|
||||
"Send a Message": "Envia un Missatge",
|
||||
"Send message": "Envia missatge",
|
||||
"September": "Setembre",
|
||||
"Serper API Key": "",
|
||||
"Serpstack API Key": "",
|
||||
"Serper API Key": "Clau API Serper",
|
||||
"Serpstack API Key": "Serpstack API Key",
|
||||
"Server connection verified": "Connexió al servidor verificada",
|
||||
"Set as default": "Estableix com a predeterminat",
|
||||
"Set Default Model": "Estableix Model Predeterminat",
|
||||
"Set embedding model (e.g. {{model}})": "Estableix el model d'emboscada (p.ex. {{model}})",
|
||||
"Set Image Size": "Estableix Mida de la Imatge",
|
||||
"Set Model": "Estableix Model",
|
||||
"Set reranking model (e.g. {{model}})": "Estableix el model de reranking (p.ex. {{model}})",
|
||||
"Set Steps": "Estableix Passos",
|
||||
"Set Task Model": "",
|
||||
"Set Task Model": "Defineix el model de tasca",
|
||||
"Set Voice": "Estableix Veu",
|
||||
"Settings": "Configuracions",
|
||||
"Settings saved successfully!": "Configuracions guardades amb èxit!",
|
||||
"Settings updated successfully": "",
|
||||
"Share": "Compartir",
|
||||
"Share Chat": "Compartir el Chat",
|
||||
"Share to OpenWebUI Community": "Comparteix amb la Comunitat OpenWebUI",
|
||||
"short-summary": "resum curt",
|
||||
"Show": "Mostra",
|
||||
"Show Admin Details in Account Pending Overlay": "",
|
||||
"Show shortcuts": "Mostra dreceres",
|
||||
"Showcased creativity": "Mostra la creativitat",
|
||||
"sidebar": "barra lateral",
|
||||
@@ -444,8 +471,8 @@
|
||||
"Source": "Font",
|
||||
"Speech recognition error: {{error}}": "Error de reconeixement de veu: {{error}}",
|
||||
"Speech-to-Text Engine": "Motor de Veu a Text",
|
||||
"SpeechRecognition API is not supported in this browser.": "L'API de Reconèixer Veu no és compatible amb aquest navegador.",
|
||||
"Stop Sequence": "Atura Seqüència",
|
||||
"STT Model": "",
|
||||
"STT Settings": "Configuracions STT",
|
||||
"Submit": "Envia",
|
||||
"Subtitle (e.g. about the Roman Empire)": "Subtítol (per exemple, sobre l'Imperi Romà)",
|
||||
@@ -464,7 +491,9 @@
|
||||
"Thanks for your feedback!": "Gràcies pel teu comentari!",
|
||||
"The score should be a value between 0.0 (0%) and 1.0 (100%).": "El puntuatge ha de ser un valor entre 0.0 (0%) i 1.0 (100%).",
|
||||
"Theme": "Tema",
|
||||
"Thinking...": "",
|
||||
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Això assegura que les teves converses valuoses queden segurament guardades a la teva base de dades backend. Gràcies!",
|
||||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||
"This setting does not sync across browsers or devices.": "Aquesta configuració no es sincronitza entre navegadors ni dispositius.",
|
||||
"Thorough explanation": "Explacació exhaustiva",
|
||||
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Consell: Actualitza diversos espais de variables consecutivament prement la tecla de tabulació en l'entrada del xat després de cada reemplaçament.",
|
||||
@@ -476,6 +505,7 @@
|
||||
"to": "a",
|
||||
"To access the available model names for downloading,": "Per accedir als noms dels models disponibles per descarregar,",
|
||||
"To access the GGUF models available for downloading,": "Per accedir als models GGUF disponibles per descarregar,",
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"to chat input.": "a l'entrada del xat.",
|
||||
"Today": "Avui",
|
||||
"Toggle settings": "Commuta configuracions",
|
||||
@@ -483,22 +513,25 @@
|
||||
"Top K": "Top K",
|
||||
"Top P": "Top P",
|
||||
"Trouble accessing Ollama?": "Problemes accedint a Ollama?",
|
||||
"TTS Model": "",
|
||||
"TTS Settings": "Configuracions TTS",
|
||||
"Type": "",
|
||||
"TTS Voice": "",
|
||||
"Type": "Tipus",
|
||||
"Type Hugging Face Resolve (Download) URL": "Escriu URL de Resolució (Descàrrega) de Hugging Face",
|
||||
"Uh-oh! There was an issue connecting to {{provider}}.": "Uf! Hi va haver un problema connectant-se a {{provider}}.",
|
||||
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Tipus d'Arxiu Desconegut '{{file_type}}', però acceptant i tractant com a text pla",
|
||||
"Update and Copy Link": "Actualitza i Copia enllaç",
|
||||
"Update password": "Actualitza contrasenya",
|
||||
"Upload a GGUF model": "Puja un model GGUF",
|
||||
"Upload Files": "",
|
||||
"Upload Files": "Pujar fitxers",
|
||||
"Upload Pipeline": "",
|
||||
"Upload Progress": "Progrés de Càrrega",
|
||||
"URL Mode": "Mode URL",
|
||||
"Use '#' in the prompt input to load and select your documents.": "Utilitza '#' a l'entrada del prompt per carregar i seleccionar els teus documents.",
|
||||
"Use Gravatar": "Utilitza Gravatar",
|
||||
"Use Initials": "Utilitza Inicials",
|
||||
"use_mlock (Ollama)": "",
|
||||
"use_mmap (Ollama)": "",
|
||||
"use_mlock (Ollama)": "use_mlock (Ollama)",
|
||||
"use_mmap (Ollama)": "use_mmap (Ollama)",
|
||||
"user": "usuari",
|
||||
"User Permissions": "Permisos d'Usuari",
|
||||
"Users": "Usuaris",
|
||||
@@ -507,30 +540,34 @@
|
||||
"variable": "variable",
|
||||
"variable to have them replaced with clipboard content.": "variable per tenir-les reemplaçades amb el contingut del porta-retalls.",
|
||||
"Version": "Versió",
|
||||
"Warning": "",
|
||||
"Warning": "Advertiment",
|
||||
"Warning: If you update or change your embedding model, you will need to re-import all documents.": "Avís: Si actualitzeu o canvieu el model d'incrustació, haureu de tornar a importar tots els documents.",
|
||||
"Web": "Web",
|
||||
"Web API": "",
|
||||
"Web Loader Settings": "Configuració del carregador web",
|
||||
"Web Params": "Paràmetres web",
|
||||
"Web Search": "",
|
||||
"Web Search Engine": "",
|
||||
"Web Search": "Cercador web",
|
||||
"Web Search Engine": "Cercador web",
|
||||
"Webhook URL": "URL del webhook",
|
||||
"WebUI Add-ons": "Complements de WebUI",
|
||||
"WebUI Settings": "Configuració de WebUI",
|
||||
"WebUI will make requests to": "WebUI farà peticions a",
|
||||
"What’s New in": "Què hi ha de Nou en",
|
||||
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Quan l'historial està desactivat, els nous xats en aquest navegador no apareixeran en el teu historial en cap dels teus dispositius.",
|
||||
"Whisper (Local)": "Whisper (Local)",
|
||||
"Whisper (Local)": "",
|
||||
"Widescreen Mode": "",
|
||||
"Workspace": "Treball",
|
||||
"Write a prompt suggestion (e.g. Who are you?)": "Escriu una suggerència de prompt (p. ex. Qui ets tu?)",
|
||||
"Write a summary in 50 words that summarizes [topic or keyword].": "Escriu un resum en 50 paraules que resumeixi [tema o paraula clau].",
|
||||
"Yesterday": "Ayer",
|
||||
"You": "Tu",
|
||||
"You cannot clone a base model": "",
|
||||
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "",
|
||||
"You cannot clone a base model": "No es pot clonar un model base",
|
||||
"You have no archived conversations.": "No tens converses arxivades.",
|
||||
"You have shared this chat": "Has compartit aquest xat",
|
||||
"You're a helpful assistant.": "Ets un assistent útil.",
|
||||
"You're now logged in.": "Ara estàs connectat.",
|
||||
"Your account status is currently pending activation.": "",
|
||||
"Youtube": "Youtube",
|
||||
"Youtube Loader Settings": "Configuració del carregador de Youtube"
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
"a user": "usa ka user",
|
||||
"About": "Mahitungod sa",
|
||||
"Account": "Account",
|
||||
"Account Activation Pending": "",
|
||||
"Accurate information": "",
|
||||
"Active Users": "",
|
||||
"Add": "",
|
||||
"Add a model id": "",
|
||||
"Add a short description about what this model does": "",
|
||||
@@ -28,6 +30,7 @@
|
||||
"Add User": "",
|
||||
"Adjusting these settings will apply changes universally to all users.": "Ang pag-adjust niini nga mga setting magamit ang mga pagbag-o sa tanan nga tiggamit.",
|
||||
"admin": "Administrator",
|
||||
"Admin": "",
|
||||
"Admin Panel": "Admin Panel",
|
||||
"Admin Settings": "Mga setting sa administratibo",
|
||||
"Advanced Parameters": "advanced settings",
|
||||
@@ -37,6 +40,7 @@
|
||||
"All Users": "Ang tanan nga mga tiggamit",
|
||||
"Allow": "Sa pagtugot",
|
||||
"Allow Chat Deletion": "Tugoti nga mapapas ang mga chat",
|
||||
"Allow non-local voices": "",
|
||||
"alphanumeric characters and hyphens": "alphanumeric nga mga karakter ug hyphen",
|
||||
"Already have an account?": "Naa na kay account ?",
|
||||
"an assistant": "usa ka katabang",
|
||||
@@ -57,7 +61,6 @@
|
||||
"Audio": "Audio",
|
||||
"August": "",
|
||||
"Auto-playback response": "Autoplay nga tubag",
|
||||
"Auto-send input after 3 sec.": "Awtomatikong ipadala ang entry pagkahuman sa 3 segundos.",
|
||||
"AUTOMATIC1111 Base URL": "Base URL AUTOMATIC1111",
|
||||
"AUTOMATIC1111 Base URL is required.": "Ang AUTOMATIC1111 base URL gikinahanglan.",
|
||||
"available!": "magamit!",
|
||||
@@ -69,6 +72,9 @@
|
||||
"Being lazy": "",
|
||||
"Brave Search API Key": "",
|
||||
"Bypass SSL verification for Websites": "",
|
||||
"Call": "",
|
||||
"Call feature is not supported when using Web STT engine": "",
|
||||
"Camera": "",
|
||||
"Cancel": "Pagkanselar",
|
||||
"Capabilities": "",
|
||||
"Change Password": "Usba ang password",
|
||||
@@ -86,10 +92,12 @@
|
||||
"Chunk Params": "Mga Setting sa Block",
|
||||
"Chunk Size": "Gidak-on sa block",
|
||||
"Citation": "Mga kinutlo",
|
||||
"Clear memory": "",
|
||||
"Click here for help.": "I-klik dinhi alang sa tabang.",
|
||||
"Click here to": "",
|
||||
"Click here to select": "I-klik dinhi aron makapili",
|
||||
"Click here to select a csv file.": "",
|
||||
"Click here to select a py file.": "",
|
||||
"Click here to select documents.": "Pag-klik dinhi aron mapili ang mga dokumento.",
|
||||
"click here.": "I-klik dinhi.",
|
||||
"Click on the user role button to change a user's role.": "I-klik ang User Role button aron usbon ang role sa user.",
|
||||
@@ -103,17 +111,16 @@
|
||||
"Concurrent Requests": "",
|
||||
"Confirm Password": "Kumpirma ang password",
|
||||
"Connections": "Mga koneksyon",
|
||||
"Contact Admin for WebUI Access": "",
|
||||
"Content": "Kontento",
|
||||
"Context Length": "Ang gitas-on sa konteksto",
|
||||
"Continue Response": "",
|
||||
"Conversation Mode": "Talk mode",
|
||||
"Copied shared chat URL to clipboard!": "",
|
||||
"Copy": "",
|
||||
"Copy last code block": "Kopyaha ang katapusang bloke sa code",
|
||||
"Copy last response": "Kopyaha ang kataposang tubag",
|
||||
"Copy Link": "",
|
||||
"Copying to clipboard was successful!": "Ang pagkopya sa clipboard malampuson!",
|
||||
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "Paghimo og mugbo nga 3-5 ka pulong nga sentence isip usa ka ulohan alang sa mosunod nga pangutana, hugot nga pagsunod sa 3-5 ka pulong nga limitasyon ug paglikay sa paggamit sa pulong nga 'titulo':",
|
||||
"Create a model": "",
|
||||
"Create Account": "Paghimo og account",
|
||||
"Create new key": "",
|
||||
@@ -125,12 +132,12 @@
|
||||
"Custom": "Custom",
|
||||
"Customize models for a specific purpose": "",
|
||||
"Dark": "Ngitngit",
|
||||
"Dashboard": "",
|
||||
"Database": "Database",
|
||||
"December": "",
|
||||
"Default": "Pinaagi sa default",
|
||||
"Default (Automatic1111)": "Default (Awtomatiko1111)",
|
||||
"Default (SentenceTransformers)": "",
|
||||
"Default (Web API)": "Default (Web API)",
|
||||
"Default Model": "",
|
||||
"Default model updated": "Gi-update nga default template",
|
||||
"Default Prompt Suggestions": "Default nga prompt nga mga sugyot",
|
||||
@@ -147,14 +154,15 @@
|
||||
"Deleted {{name}}": "",
|
||||
"Description": "Deskripsyon",
|
||||
"Didn't fully follow instructions": "",
|
||||
"Disabled": "Nabaldado",
|
||||
"Discover a model": "",
|
||||
"Discover a prompt": "Pagkaplag usa ka prompt",
|
||||
"Discover, download, and explore custom prompts": "Pagdiskubre, pag-download ug pagsuhid sa mga naandan nga pag-aghat",
|
||||
"Discover, download, and explore model presets": "Pagdiskobre, pag-download, ug pagsuhid sa mga preset sa template",
|
||||
"Dismissible": "",
|
||||
"Display the username instead of You in the Chat": "Ipakita ang username imbes nga 'Ikaw' sa Panaghisgutan",
|
||||
"Document": "Dokumento",
|
||||
"Document Settings": "Mga Setting sa Dokumento",
|
||||
"Documentation": "",
|
||||
"Documents": "Mga dokumento",
|
||||
"does not make any external connections, and your data stays securely on your locally hosted server.": "wala maghimo ug eksternal nga koneksyon, ug ang imong data nagpabiling luwas sa imong lokal nga host server.",
|
||||
"Don't Allow": "Dili tugotan",
|
||||
@@ -169,6 +177,7 @@
|
||||
"Edit Doc": "I-edit ang dokumento",
|
||||
"Edit User": "I-edit ang tiggamit",
|
||||
"Email": "E-mail",
|
||||
"Embedding Batch Size": "",
|
||||
"Embedding Model": "",
|
||||
"Embedding Model Engine": "",
|
||||
"Embedding model set to \"{{embedding_model}}\"": "",
|
||||
@@ -176,7 +185,6 @@
|
||||
"Enable Community Sharing": "",
|
||||
"Enable New Sign Ups": "I-enable ang bag-ong mga rehistro",
|
||||
"Enable Web Search": "",
|
||||
"Enabled": "Gipaandar",
|
||||
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "",
|
||||
"Enter {{role}} message here": "Pagsulod sa mensahe {{role}} dinhi",
|
||||
"Enter a detail about yourself for your LLMs to recall": "",
|
||||
@@ -206,12 +214,15 @@
|
||||
"Experimental": "Eksperimento",
|
||||
"Export": "",
|
||||
"Export All Chats (All Users)": "I-export ang tanan nga mga chat (Tanan nga tiggamit)",
|
||||
"Export chat (.json)": "",
|
||||
"Export Chats": "I-export ang mga chat",
|
||||
"Export Documents Mapping": "I-export ang pagmapa sa dokumento",
|
||||
"Export Models": "",
|
||||
"Export Prompts": "Export prompts",
|
||||
"External Models": "",
|
||||
"Failed to create API Key.": "",
|
||||
"Failed to read clipboard contents": "Napakyas sa pagbasa sa sulod sa clipboard",
|
||||
"Failed to update settings": "",
|
||||
"February": "",
|
||||
"Feel free to add specific details": "",
|
||||
"File Mode": "File mode",
|
||||
@@ -222,9 +233,9 @@
|
||||
"Followed instructions perfectly": "",
|
||||
"Format your variables using square brackets like this:": "I-format ang imong mga variable gamit ang square brackets sama niini:",
|
||||
"Frequency Penalty": "",
|
||||
"Full Screen Mode": "Full screen mode",
|
||||
"General": "Heneral",
|
||||
"General Settings": "kinatibuk-ang mga setting",
|
||||
"Generate Image": "",
|
||||
"Generating search query": "",
|
||||
"Generation Info": "",
|
||||
"Good Response": "",
|
||||
@@ -249,6 +260,7 @@
|
||||
"Info": "",
|
||||
"Input commands": "Pagsulod sa input commands",
|
||||
"Install from Github URL": "",
|
||||
"Instant Auto-Send After Voice Transcription": "",
|
||||
"Interface": "Interface",
|
||||
"Invalid Tag": "",
|
||||
"January": "",
|
||||
@@ -261,14 +273,17 @@
|
||||
"JWT Token": "JWT token",
|
||||
"Keep Alive": "Padayon nga aktibo",
|
||||
"Keyboard shortcuts": "Mga shortcut sa keyboard",
|
||||
"Knowledge": "",
|
||||
"Language": "Pinulongan",
|
||||
"Last Active": "",
|
||||
"Light": "Kahayag",
|
||||
"Listening...": "Paminaw...",
|
||||
"Listening...": "",
|
||||
"LLMs can make mistakes. Verify important information.": "Ang mga LLM mahimong masayop. ",
|
||||
"Local Models": "",
|
||||
"LTR": "",
|
||||
"Made by OpenWebUI Community": "Gihimo sa komunidad sa OpenWebUI",
|
||||
"Make sure to enclose them with": "Siguruha nga palibutan sila",
|
||||
"Manage": "",
|
||||
"Manage Models": "Pagdumala sa mga templates",
|
||||
"Manage Ollama Models": "Pagdumala sa mga modelo sa Ollama",
|
||||
"Manage Pipelines": "",
|
||||
@@ -304,6 +319,7 @@
|
||||
"Name your model": "",
|
||||
"New Chat": "Bag-ong diskusyon",
|
||||
"New Password": "Bag-ong Password",
|
||||
"No documents found": "",
|
||||
"No results found": "",
|
||||
"No search query generated": "",
|
||||
"No source available": "Walay tinubdan nga anaa",
|
||||
@@ -320,6 +336,7 @@
|
||||
"Ollama": "",
|
||||
"Ollama API": "",
|
||||
"Ollama API disabled": "",
|
||||
"Ollama API is disabled": "",
|
||||
"Ollama Version": "Ollama nga bersyon",
|
||||
"On": "Gipaandar",
|
||||
"Only": "Lamang",
|
||||
@@ -342,6 +359,8 @@
|
||||
"PDF document (.pdf)": "",
|
||||
"PDF Extract Images (OCR)": "PDF Image Extraction (OCR)",
|
||||
"pending": "gipugngan",
|
||||
"Permission denied when accessing media devices": "",
|
||||
"Permission denied when accessing microphone": "",
|
||||
"Permission denied when accessing microphone: {{error}}": "Gidili ang pagtugot sa dihang nag-access sa mikropono: {{error}}",
|
||||
"Personalization": "",
|
||||
"Pipelines": "",
|
||||
@@ -364,6 +383,7 @@
|
||||
"Read Aloud": "",
|
||||
"Record voice": "Irekord ang tingog",
|
||||
"Redirecting you to OpenWebUI Community": "Gi-redirect ka sa komunidad sa OpenWebUI",
|
||||
"Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "",
|
||||
"Refused when it shouldn't have": "",
|
||||
"Regenerate": "",
|
||||
"Release Notes": "Release Notes",
|
||||
@@ -375,12 +395,14 @@
|
||||
"Reranking Model": "",
|
||||
"Reranking model disabled": "",
|
||||
"Reranking model set to \"{{reranking_model}}\"": "",
|
||||
"Reset Upload Directory": "",
|
||||
"Reset Vector Storage": "I-reset ang pagtipig sa vector",
|
||||
"Response AutoCopy to Clipboard": "Awtomatikong kopya sa tubag sa clipboard",
|
||||
"Role": "Papel",
|
||||
"Rosé Pine": "Rosé Pine",
|
||||
"Rosé Pine Dawn": "Aube Pine Rosé",
|
||||
"RTL": "",
|
||||
"Running": "",
|
||||
"Save": "Tipigi",
|
||||
"Save & Create": "I-save ug Paghimo",
|
||||
"Save & Update": "I-save ug I-update",
|
||||
@@ -394,6 +416,8 @@
|
||||
"Search Documents": "Pangitaa ang mga dokumento",
|
||||
"Search Models": "",
|
||||
"Search Prompts": "Pangitaa ang mga prompt",
|
||||
"Search Query Generation Prompt": "",
|
||||
"Search Query Generation Prompt Length Threshold": "",
|
||||
"Search Result Count": "",
|
||||
"Searched {{count}} sites_one": "",
|
||||
"Searched {{count}} sites_other": "",
|
||||
@@ -403,12 +427,14 @@
|
||||
"See what's new": "Tan-awa unsay bag-o",
|
||||
"Seed": "Binhi",
|
||||
"Select a base model": "",
|
||||
"Select a engine": "",
|
||||
"Select a mode": "Pagpili og mode",
|
||||
"Select a model": "Pagpili og modelo",
|
||||
"Select a pipeline": "",
|
||||
"Select a pipeline url": "",
|
||||
"Select an Ollama instance": "Pagpili usa ka pananglitan sa Ollama",
|
||||
"Select model": "Pagpili og modelo",
|
||||
"Select only one model to call": "",
|
||||
"Selected model(s) do not support image inputs": "",
|
||||
"Send": "",
|
||||
"Send a Message": "Magpadala ug mensahe",
|
||||
@@ -421,18 +447,19 @@
|
||||
"Set Default Model": "Ibutang ang default template",
|
||||
"Set embedding model (e.g. {{model}})": "",
|
||||
"Set Image Size": "Ibutang ang gidak-on sa hulagway",
|
||||
"Set Model": "I-configure ang template",
|
||||
"Set reranking model (e.g. {{model}})": "",
|
||||
"Set Steps": "Ipasabot ang mga lakang",
|
||||
"Set Task Model": "",
|
||||
"Set Voice": "Ibutang ang tingog",
|
||||
"Settings": "Mga setting",
|
||||
"Settings saved successfully!": "Malampuson nga na-save ang mga setting!",
|
||||
"Settings updated successfully": "",
|
||||
"Share": "",
|
||||
"Share Chat": "",
|
||||
"Share to OpenWebUI Community": "Ipakigbahin sa komunidad sa OpenWebUI",
|
||||
"short-summary": "mubo nga summary",
|
||||
"Show": "Pagpakita",
|
||||
"Show Admin Details in Account Pending Overlay": "",
|
||||
"Show shortcuts": "Ipakita ang mga shortcut",
|
||||
"Showcased creativity": "",
|
||||
"sidebar": "lateral bar",
|
||||
@@ -443,8 +470,8 @@
|
||||
"Source": "Tinubdan",
|
||||
"Speech recognition error: {{error}}": "Sayop sa pag-ila sa tingog: {{error}}",
|
||||
"Speech-to-Text Engine": "Engine sa pag-ila sa tingog",
|
||||
"SpeechRecognition API is not supported in this browser.": "Ang SpeechRecognition API wala gisuportahan niini nga browser.",
|
||||
"Stop Sequence": "Pagkasunod-sunod sa pagsira",
|
||||
"STT Model": "",
|
||||
"STT Settings": "Mga setting sa STT",
|
||||
"Submit": "Isumite",
|
||||
"Subtitle (e.g. about the Roman Empire)": "",
|
||||
@@ -463,7 +490,9 @@
|
||||
"Thanks for your feedback!": "",
|
||||
"The score should be a value between 0.0 (0%) and 1.0 (100%).": "",
|
||||
"Theme": "Tema",
|
||||
"Thinking...": "",
|
||||
"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Kini nagsiguro nga ang imong bililhon nga mga panag-istoryahanay luwas nga natipig sa imong backend database. ",
|
||||
"This is an experimental feature, it may not function as expected and is subject to change at any time.": "",
|
||||
"This setting does not sync across browsers or devices.": "Kini nga setting wala mag-sync tali sa mga browser o device.",
|
||||
"Thorough explanation": "",
|
||||
"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Sugyot: Pag-update sa daghang variable nga lokasyon nga sunud-sunod pinaagi sa pagpindot sa tab key sa chat entry pagkahuman sa matag puli.",
|
||||
@@ -475,6 +504,7 @@
|
||||
"to": "adunay",
|
||||
"To access the available model names for downloading,": "Aron ma-access ang mga ngalan sa modelo nga ma-download,",
|
||||
"To access the GGUF models available for downloading,": "Aron ma-access ang mga modelo sa GGUF nga magamit alang sa pag-download,",
|
||||
"To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "",
|
||||
"to chat input.": "sa entrada sa iring.",
|
||||
"Today": "",
|
||||
"Toggle settings": "I-toggle ang mga setting",
|
||||
@@ -482,7 +512,9 @@
|
||||
"Top K": "Top K",
|
||||
"Top P": "Ibabaw nga P",
|
||||
"Trouble accessing Ollama?": "Adunay mga problema sa pag-access sa Ollama?",
|
||||
"TTS Model": "",
|
||||
"TTS Settings": "Mga Setting sa TTS",
|
||||
"TTS Voice": "",
|
||||
"Type": "",
|
||||
"Type Hugging Face Resolve (Download) URL": "Pagsulod sa resolusyon (pag-download) URL Hugging Face",
|
||||
"Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! {{provider}}.",
|
||||
@@ -491,6 +523,7 @@
|
||||
"Update password": "I-update ang password",
|
||||
"Upload a GGUF model": "Pag-upload ug modelo sa GGUF",
|
||||
"Upload Files": "",
|
||||
"Upload Pipeline": "",
|
||||
"Upload Progress": "Pag-uswag sa Pag-upload",
|
||||
"URL Mode": "URL mode",
|
||||
"Use '#' in the prompt input to load and select your documents.": "Gamita ang '#' sa dali nga pagsulod aron makarga ug mapili ang imong mga dokumento.",
|
||||
@@ -509,6 +542,7 @@
|
||||
"Warning": "",
|
||||
"Warning: If you update or change your embedding model, you will need to re-import all documents.": "",
|
||||
"Web": "Web",
|
||||
"Web API": "",
|
||||
"Web Loader Settings": "",
|
||||
"Web Params": "",
|
||||
"Web Search": "",
|
||||
@@ -519,17 +553,20 @@
|
||||
"WebUI will make requests to": "Ang WebUI maghimo mga hangyo sa",
|
||||
"What’s New in": "Unsay bag-o sa",
|
||||
"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Kung ang kasaysayan gipalong, ang mga bag-ong chat sa kini nga browser dili makita sa imong kasaysayan sa bisan unsang mga aparato.",
|
||||
"Whisper (Local)": "Whisper (Lokal)",
|
||||
"Whisper (Local)": "",
|
||||
"Widescreen Mode": "",
|
||||
"Workspace": "",
|
||||
"Write a prompt suggestion (e.g. Who are you?)": "Pagsulat og gisugyot nga prompt (eg. Kinsa ka?)",
|
||||
"Write a summary in 50 words that summarizes [topic or keyword].": "Pagsulat og 50 ka pulong nga summary nga nagsumaryo [topic o keyword].",
|
||||
"Yesterday": "",
|
||||
"You": "",
|
||||
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "",
|
||||
"You cannot clone a base model": "",
|
||||
"You have no archived conversations.": "",
|
||||
"You have shared this chat": "",
|
||||
"You're a helpful assistant.": "Usa ka ka mapuslanon nga katabang",
|
||||
"You're now logged in.": "Konektado ka na karon.",
|
||||
"Your account status is currently pending activation.": "",
|
||||
"Youtube": "",
|
||||
"Youtube Loader Settings": ""
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user