Compare commits
355 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b8f923981 | |||
| be41994d40 | |||
| 5361896411 | |||
| c754c53906 | |||
| 78d6647885 | |||
| 4eeb669ac3 | |||
| 26465f3e92 | |||
| 9d25207b83 | |||
| cc19b8049a | |||
| 3a163b6392 | |||
| 405d0561df | |||
| ee33b4e2a3 | |||
| 888479aaf0 | |||
| 82cda6e522 | |||
| 119a7f1933 | |||
| 6c4445d545 | |||
| 92b1acd6fb | |||
| 1767b64135 | |||
| e030f261d1 | |||
| d501ece247 | |||
| 534e4c90ca | |||
| 40f2c3521b | |||
| 07b1327708 | |||
| 525095b3de | |||
| f84513e856 | |||
| bf423b8577 | |||
| ba20c71963 | |||
| 7bbc57f225 | |||
| e703e172e2 | |||
| 3ff52fd1ad | |||
| e19406cdd7 | |||
| 30e65b33f6 | |||
| 3f1255b39e | |||
| 71743b25fe | |||
| c7e93b32c5 | |||
| 3cee507687 | |||
| ff651ddc36 | |||
| c0738cef26 | |||
| a44e9a8dda | |||
| 38b9a63fa5 | |||
| 2d60e42258 | |||
| 60ac69eb27 | |||
| 504d910557 | |||
| e48d66f918 | |||
| 0bfbace9aa | |||
| 019cf8199f | |||
| c5f85eed92 | |||
| 21719ccdf1 | |||
| 299b3d72cf | |||
| 85e9e231ed | |||
| f382a78e31 | |||
| 377cc427b6 | |||
| 7ec72679f0 | |||
| 31b311c3c9 | |||
| e24ab4c6d2 | |||
| 6739983cf1 | |||
| ff00815b61 | |||
| 8f6a927be3 | |||
| f6add92702 | |||
| deedfdceae | |||
| e268ee5675 | |||
| 33d8d818bd | |||
| 9a81a37008 | |||
| 8e620b0c2c | |||
| 214546399a | |||
| fdd27aa321 | |||
| 4ebff2c5ce | |||
| 8266d343bf | |||
| 36ddb19023 | |||
| ebc410d8d4 | |||
| 1772db4712 | |||
| 40d7d7d6dd | |||
| 0d01ea5f2f | |||
| 822c47c171 | |||
| fdf02c4e86 | |||
| 398bc96b1a | |||
| ad82eae6a9 | |||
| d7b64ff447 | |||
| 93cb2be35d | |||
| 8f1b9bdf8a | |||
| f69956bda8 | |||
| e06667ead8 | |||
| 35f64cc53f | |||
| 12f95555fc | |||
| 84c1810b6e | |||
| ba39f9bf56 | |||
| 8eb82265d0 | |||
| ac277e8e9e | |||
| b1521cacad | |||
| 5978e7c9a6 | |||
| fd5e8b4fcf | |||
| 291b6dd744 | |||
| 73ff524a8f | |||
| c2b5bf2130 | |||
| 8532f9da03 | |||
| 2cfe6830df | |||
| 44355a4bdc | |||
| 47a858393b | |||
| d3e80f515d | |||
| c5b93ca631 | |||
| 3335eee1b9 | |||
| 29175405a6 | |||
| d53062a9b0 | |||
| 96ef15362a | |||
| d6d6098378 | |||
| 620e629edc | |||
| c292fd89f9 | |||
| c346130774 | |||
| 768717aaf9 | |||
| d055e1f888 | |||
| 5f84145a2d | |||
| 47a9395a22 | |||
| 8dbac0f7e3 | |||
| 6b463164f4 | |||
| 5f15e9ee68 | |||
| 54d63ece6f | |||
| e35883ca9c | |||
| 19e49e43cb | |||
| e429b5548c | |||
| 839dacc4a4 | |||
| 4225591a26 | |||
| b4adffc3af | |||
| 8fc178ae91 | |||
| 580ad46036 | |||
| f47dffe6e1 | |||
| 00f6b4bf09 | |||
| 9126ceac08 | |||
| ff8a2da751 | |||
| 2f028b45fe | |||
| d3ef9d980b | |||
| cb81dfe4ba | |||
| 426de76690 | |||
| 16a767e04e | |||
| 2adaf9ba3d | |||
| f381850bb2 | |||
| 273787fe78 | |||
| 70d16c3904 | |||
| ee6b1376c3 | |||
| aed8e2156f | |||
| 5d848ad130 | |||
| 98928f6bd7 | |||
| 692f04d457 | |||
| e1ea0c23eb | |||
| 657d443a3e | |||
| 95985e7bbb | |||
| 3a0a1aca11 | |||
| 41926172d3 | |||
| 585b9eb84a | |||
| ffd7d74f77 | |||
| 578d52b89d | |||
| e599d5db3c | |||
| 79b5430a9e | |||
| 9b05fe3c54 | |||
| 5fe2795db8 | |||
| 732f730213 | |||
| c89bb01db9 | |||
| 1cbbf75807 | |||
| bd81fc8bff | |||
| d4df552076 | |||
| b7ad82757d | |||
| d12b6cda4e | |||
| 83855b713b | |||
| bd09b6dbad | |||
| eb9ad47ef8 | |||
| 5b46a252ff | |||
| 39c57c0e94 | |||
| 619dbbe9f5 | |||
| 9be73ea94a | |||
| 4e43663448 | |||
| cd117f5b67 | |||
| 2dad9b9432 | |||
| b0bc36f2af | |||
| 1688f5ecaf | |||
| cba2d31175 | |||
| a2b77fd072 | |||
| 9b83e57372 | |||
| 687cae9b79 | |||
| f8fffdd288 | |||
| 60d6279055 | |||
| 7b330d1490 | |||
| 7078af635c | |||
| e1b57d80a4 | |||
| 8426874426 | |||
| 70dd790afc | |||
| e99cba53fe | |||
| 628d7ae72d | |||
| ff737a9e25 | |||
| 5c16631ec5 | |||
| d52eaf19d6 | |||
| 27dd6ef14e | |||
| ed75f72358 | |||
| 276d629a14 | |||
| aedd77b81d | |||
| 1053863175 | |||
| f448341211 | |||
| 9fc6b999d0 | |||
| ff2fff857a | |||
| dd4cf102cc | |||
| 5d3a89dd25 | |||
| b4f1a0b5a6 | |||
| afa42dd2e4 | |||
| be44af4680 | |||
| e6b6f42139 | |||
| 4d9677e808 | |||
| eacb69074e | |||
| 6477bf37fe | |||
| 1743d3c6c1 | |||
| e723b2a4c6 | |||
| 67f704c98d | |||
| 8d92093570 | |||
| 1ccac9111b | |||
| 98984166f9 | |||
| d1bf18eeb0 | |||
| 2018a6c000 | |||
| 56152230f8 | |||
| 7e8cf5504a | |||
| 3c03d5069d | |||
| 705508a674 | |||
| 771482c6e6 | |||
| d25ccfba5f | |||
| 614c219010 | |||
| 63b3076d64 | |||
| 8e3ad45ce4 | |||
| 984e0c533e | |||
| 5065291f72 | |||
| 8c273ba58a | |||
| 7a9c0946a4 | |||
| 7e0a26ef4e | |||
| 65b7c9898b | |||
| 67f95ddfdc | |||
| d1dbb9a3be | |||
| bb087a5989 | |||
| a0d24105fc | |||
| a5c9160b7d | |||
| 0e2cdbe9fb | |||
| 3c55a46484 | |||
| d6b68f405e | |||
| 351bbdb36c | |||
| 896baf021b | |||
| 499e5e4f60 | |||
| 3ad003bccb | |||
| af92184c93 | |||
| 06debb322b | |||
| b7f0759485 | |||
| bc6f23f82f | |||
| cb9e76c7f9 | |||
| b38986a0aa | |||
| db0c576f48 | |||
| 40d7e5089d | |||
| 9b8d42c670 | |||
| bed3b71860 | |||
| 253791b92c | |||
| 5dd6ae6ec4 | |||
| 902f30c123 | |||
| fa8d7bd9c6 | |||
| d0df2cbe53 | |||
| 27baa00afb | |||
| 869063c743 | |||
| 2f9f568dd9 | |||
| 937ce5f797 | |||
| 1b612209a3 | |||
| d00d3341af | |||
| 3199e26500 | |||
| bc06e7a282 | |||
| 1eec9e2b59 | |||
| d46f652f7e | |||
| d75d638b9a | |||
| b64c9d966a | |||
| 8a928e5356 | |||
| adf958559b | |||
| 82b35492af | |||
| a7ea07036e | |||
| c1d3481c41 | |||
| bc0baa35e6 | |||
| f549cb1f87 | |||
| 823093eea6 | |||
| 939bfd153e | |||
| b943b7d337 | |||
| a1f3ece528 | |||
| 8a4b3e6bc9 | |||
| a9c497612b | |||
| 7df997c992 | |||
| bebc0d2073 | |||
| 857fc3ce99 | |||
| 47e9c12fc2 | |||
| 5970dadead | |||
| 7dc4cb30b2 | |||
| fb122e67d0 | |||
| ca9874757f | |||
| 65d5545cf0 | |||
| e872f5dc78 | |||
| b4ad64586a | |||
| 9661fee554 | |||
| adf26789b8 | |||
| 209e246e6f | |||
| ed2a1e7db9 | |||
| 143ac08c35 | |||
| 53f03f6556 | |||
| 62f1933e3c | |||
| 0b30dc357c | |||
| d5b595a842 | |||
| d05ba042c0 | |||
| eed2d735a1 | |||
| 675403d26d | |||
| 8be6e16513 | |||
| c7fc17da69 | |||
| 4775fe43d8 | |||
| 2a04dd0f9b | |||
| 61ee4bd629 | |||
| dbd661a417 | |||
| 26700ac4ac | |||
| b0defe5524 | |||
| 91cd8c7608 | |||
| 62b01c5f8e | |||
| b33ab6c5fd | |||
| 0886b3a0a4 | |||
| 522afbb0a0 | |||
| d5f13dd9e0 | |||
| 3d6d8c91dd | |||
| 4354f270ce | |||
| 28087ccf40 | |||
| eb0e683b47 | |||
| 1023ff8454 | |||
| 9401f6c821 | |||
| 827c419251 | |||
| 82db64a700 | |||
| f1fae805a2 | |||
| 272e2386dd | |||
| 67e1c0a10b | |||
| f88a86f9b0 | |||
| 601982f52b | |||
| fc839011f6 | |||
| b407f24950 | |||
| d15f57cfff | |||
| 82fbfd69a5 | |||
| 8e6ea49e0e | |||
| 83a3e53d8d | |||
| 698976add0 | |||
| cc1d3c48e0 | |||
| e510c8b11f | |||
| 666086a806 | |||
| 9bdbe88bda | |||
| 50db51ebe0 | |||
| 0beaab51ae | |||
| 0e30b0f9b4 | |||
| 214722d39e | |||
| 7f6dae41f0 | |||
| 9aa8eff44e | |||
| 2544f7eaf0 | |||
| 1a6ce1d5d9 | |||
| 98eaec22d4 | |||
| 2e40719f4e | |||
| 71a2bd2fea | |||
| 1c20db775c | |||
| 0ead3ed37a |
+138
@@ -5,6 +5,144 @@ 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.30] - 2024-09-26
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🍞 Update Available Toast Dismissal**: Enhanced user experience by ensuring that once the update available notification is dismissed, it won't reappear for 24 hours.
|
||||
- **📋 Ollama /embed Form Data**: Adjusted the integration inaccuracies in the /embed form data to ensure it perfectly matches with Ollama's specifications.
|
||||
- **🔧 O1 Max Completion Tokens Issue**: Resolved compatibility issues with OpenAI's o1 models max_completion_tokens param to ensure smooth operation.
|
||||
- **🔄 Pip Install Database Issue**: Fixed a critical issue where database changes during pip installations were reverting and not saving chat logs, now ensuring data persistence and reliability in chat operations.
|
||||
- **🏷️ Chat Rename Tab Update**: Fixed the functionality to change the web browser's tab title simultaneously when a chat is renamed, keeping tab titles consistent.
|
||||
|
||||
## [0.3.29] - 2023-09-25
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔧 KaTeX Rendering Improvement**: Resolved specific corner cases in KaTeX rendering to enhance the display of complex mathematical notation.
|
||||
- **📞 'Call' URL Parameter Fix**: Corrected functionality for 'call' URL search parameter ensuring reliable activation of voice calls through URL triggers.
|
||||
- **🔄 Configuration Reset Fix**: Fixed the RESET_CONFIG_ON_START to ensure settings revert to default correctly upon each startup, improving reliability in configuration management.
|
||||
- **🌍 Filter Outlet Hook Fix**: Addressed issues in the filter outlet hook, ensuring all filter functions operate as intended.
|
||||
|
||||
## [0.3.28] - 2024-09-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔍 Web Search Functionality**: Corrected an issue where the web search option was not functioning properly.
|
||||
|
||||
## [0.3.27] - 2024-09-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔄 Periodic Cleanup Error Resolved**: Fixed a critical RuntimeError related to the 'periodic_usage_pool_cleanup' coroutine, ensuring smooth and efficient performance post-pip install, correcting a persisting issue from version 0.3.26.
|
||||
- **📊 Enhanced LaTeX Rendering**: Improved rendering for LaTeX content, enhancing clarity and visual presentation in documents and mathematical models.
|
||||
|
||||
## [0.3.26] - 2024-09-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🔄 Event Loop Error Resolution**: Addressed a critical error where a missing running event loop caused 'periodic_usage_pool_cleanup' to fail with pip installs. This fix ensures smoother and more reliable updates and installations, enhancing overall system stability.
|
||||
|
||||
## [0.3.25] - 2024-09-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🖼️ Image Generation Functionality**: Resolved an issue where image generation was not functioning, restoring full capability for visual content creation.
|
||||
- **⚖️ Rate Response Corrections**: Addressed a problem where rate responses were not working, ensuring reliable feedback mechanisms are operational.
|
||||
|
||||
## [0.3.24] - 2024-09-24
|
||||
|
||||
### Added
|
||||
|
||||
- **🚀 Rendering Optimization**: Significantly improved message rendering performance, enhancing user experience and webui responsiveness.
|
||||
- **💖 Favorite Response Feature in Chat Overview**: Users can now mark responses as favorite directly from the chat overview, enhancing ease of retrieval and organization of preferred responses.
|
||||
- **💬 Create Message Pairs with Shortcut**: Implemented creation of new message pairs using Cmd/Ctrl+Shift+Enter, making conversation editing faster and more intuitive.
|
||||
- **🌍 Expanded User Prompt Variables**: Added weekday, timezone, and language information variables to user prompts to match system prompt variables.
|
||||
- **🎵 Enhanced Audio Support**: Now includes support for 'audio/x-m4a' files, broadening compatibility with audio content within the platform.
|
||||
- **🔏 Model URL Search Parameter**: Added an ability to select a model directly via URL parameters, streamlining navigation and model access.
|
||||
- **📄 Enhanced PDF Citations**: PDF citations now open at the associated page, streamlining reference checks and document handling.
|
||||
- **🔧Use of Redis in Sockets**: Enhanced socket implementation to fully support Redis, enabling effective stateless instances suitable for scalable load balancing.
|
||||
- **🌍 Stream Individual Model Responses**: Allows specific models to have individualized streaming settings, enhancing performance and customization.
|
||||
- **🕒 Display Model Hash and Last Modified Timestamp for Ollama Models**: Provides critical model details directly in the Models workspace for enhanced tracking.
|
||||
- **❗ Update Info Notification for Admins**: Ensures administrators receive immediate updates upon login, keeping them informed of the latest changes and system statuses.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🗑️ Temporary File Handling On Windows**: Fixed an issue causing errors when accessing a temporary file being used by another process, Tools & Functions should now work as intended.
|
||||
- **🔓 Authentication Toggle Issue**: Resolved the malfunction where setting 'WEBUI_AUTH=False' did not appropriately disable authentication, ensuring that user experience and system security settings function as configured.
|
||||
- **🔧 Save As Copy Issue for Many Model Chats**: Resolved an error preventing users from save messages as copies in many model chats.
|
||||
- **🔒 Sidebar Closure on Mobile**: Resolved an issue where the mobile sidebar remained open after menu engagement, improving user interface responsivity and comfort.
|
||||
- **🛡️ Tooltip XSS Vulnerability**: Resolved a cross-site scripting (XSS) issue within tooltips, ensuring enhanced security and data integrity during user interactions.
|
||||
|
||||
### Changed
|
||||
|
||||
- **↩️ Deprecated Interface Stream Response Settings**: Moved to advanced parameters to streamline interface settings and enhance user clarity.
|
||||
- **⚙️ Renamed 'speedRate' to 'playbackRate'**: Standardizes terminology, improving usability and understanding in media settings.
|
||||
|
||||
## [0.3.23] - 2024-09-21
|
||||
|
||||
### Added
|
||||
|
||||
- **🚀 WebSocket Redis Support**: Enhanced load balancing capabilities for multiple instance setups, promoting better performance and reliability in WebUI.
|
||||
- **🔧 Adjustable Chat Controls**: Introduced width-adjustable chat controls, enabling a personalized and more comfortable user interface.
|
||||
- **🌎 i18n Updates**: Improved and updated the Chinese translations.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🌐 Task Model Unloading Issue**: Modified task handling to use the Ollama /api/chat endpoint instead of OpenAI compatible endpoint, ensuring models stay loaded and ready with custom parameters, thus minimizing delays in task execution.
|
||||
- **📝 Title Generation Fix for OpenAI Compatible APIs**: Resolved an issue preventing the generation of titles, enhancing consistency and reliability when using multiple API providers.
|
||||
- **🗃️ RAG Duplicate Collection Issue**: Fixed a bug causing repeated processing of the same uploaded file. Now utilizes indexed files to prevent unnecessary duplications, optimizing resource usage.
|
||||
- **🖼️ Image Generation Enhancement**: Refactored OpenAI image generation endpoint to be asynchronous, preventing the WebUI from becoming unresponsive during processing, thus enhancing user experience.
|
||||
- **🔓 Downgrade Authlib**: Reverted Authlib to version 1.3.1 to address and resolve issues concerning OAuth functionality.
|
||||
|
||||
### Changed
|
||||
|
||||
- **🔍 Improved Message Interaction**: Enhanced the message node interface to allow for easier focus redirection with a simple click, streamlining user interaction.
|
||||
- **✨ Styling Refactor**: Updated WebUI styling for a cleaner, more modern look, enhancing user experience across the platform.
|
||||
|
||||
## [0.3.22] - 2024-09-19
|
||||
|
||||
### Added
|
||||
|
||||
- **⭐ Chat Overview**: Introducing a node-based interactive messages diagram for improved visualization of conversation flows.
|
||||
- **🔗 Multiple Vector DB Support**: Now supports multiple vector databases, including the newly added Milvus support. Community contributions for additional database support are highly encouraged!
|
||||
- **📡 Experimental Non-Stream Chat Completion**: Experimental feature allowing the use of OpenAI o1 models, which do not support streaming, ensuring more versatile model deployment.
|
||||
- **🔍 Experimental Colbert-AI Reranker Integration**: Added support for "jinaai/jina-colbert-v2" as a reranker, enhancing search relevance and accuracy. Note: it may not function at all on low-spec computers.
|
||||
- **🕸️ ENABLE_WEBSOCKET_SUPPORT**: Added environment variable for instances to ignore websocket upgrades, stabilizing connections on platforms with websocket issues.
|
||||
- **🔊 Azure Speech Service Integration**: Added support for Azure Speech services for Text-to-Speech (TTS).
|
||||
- **🎚️ Customizable Playback Speed**: Playback speed control is now available in Call mode settings, allowing users to adjust audio playback speed to their preferences.
|
||||
- **🧠 Enhanced Error Messaging**: System now displays helpful error messages directly to users during chat completion issues.
|
||||
- **📂 Save Model as Transparent PNG**: Model profile images are now saved as PNGs, supporting transparency and improving visual integration.
|
||||
- **📱 iPhone Compatibility Adjustments**: Added padding to accommodate the iPhone navigation bar, improving UI display on these devices.
|
||||
- **🔗 Secure Response Headers**: Implemented security response headers, bolstering web application security.
|
||||
- **🔧 Enhanced AUTOMATIC1111 Settings**: Users can now configure 'CFG Scale', 'Sampler', and 'Scheduler' parameters directly in the admin settings, enhancing workflow flexibility without source code modifications.
|
||||
- **🌍 i18n Updates**: Enhanced translations for Chinese, Ukrainian, Russian, and French, fostering a better localized experience.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🛠️ Chat Message Deletion**: Resolved issues with chat message deletion, ensuring a smoother user interaction and system stability.
|
||||
- **🔢 Ordered List Numbering**: Fixed the incorrect ordering in lists.
|
||||
|
||||
### Changed
|
||||
|
||||
- **🎨 Transparent Icon Handling**: Allowed model icons to be displayed on transparent backgrounds, improving UI aesthetics.
|
||||
- **📝 Improved RAG Template**: Enhanced Retrieval-Augmented Generation template, optimizing context handling and error checking for more precise operation.
|
||||
|
||||
## [0.3.21] - 2024-09-08
|
||||
|
||||
### Added
|
||||
|
||||
- **📊 Document Count Display**: Now displays the total number of documents directly within the dashboard.
|
||||
- **🚀 Ollama Embed API Endpoint**: Enabled /api/embed endpoint proxy support.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🐳 Docker Launch Issue**: Resolved the problem preventing Open-WebUI from launching correctly when using Docker.
|
||||
|
||||
### Changed
|
||||
|
||||
- **🔍 Enhanced Search Prompts**: Improved the search query generation prompts for better accuracy and user interaction, enhancing the overall search experience.
|
||||
|
||||
## [0.3.20] - 2024-09-07
|
||||
|
||||
### Added
|
||||
|
||||
+7
-2
@@ -74,6 +74,10 @@ ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
|
||||
|
||||
## Hugging Face download cache ##
|
||||
ENV HF_HOME="/app/backend/data/cache/embedding/models"
|
||||
|
||||
## Torch Extensions ##
|
||||
# ENV TORCH_EXTENSIONS_DIR="/.cache/torch_extensions"
|
||||
|
||||
#### Other models ##########################################################
|
||||
|
||||
WORKDIR /app/backend
|
||||
@@ -96,7 +100,7 @@ RUN chown -R $UID:$GID /app $HOME
|
||||
RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
||||
apt-get update && \
|
||||
# Install pandoc and netcat
|
||||
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \
|
||||
apt-get install -y --no-install-recommends git build-essential pandoc netcat-openbsd curl && \
|
||||
apt-get install -y --no-install-recommends gcc python3-dev && \
|
||||
# for RAG OCR
|
||||
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
||||
@@ -109,7 +113,7 @@ RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
||||
else \
|
||||
apt-get update && \
|
||||
# Install pandoc, netcat and gcc
|
||||
apt-get install -y --no-install-recommends pandoc gcc netcat-openbsd curl jq && \
|
||||
apt-get install -y --no-install-recommends git build-essential pandoc gcc netcat-openbsd curl jq && \
|
||||
apt-get install -y --no-install-recommends gcc python3-dev && \
|
||||
# for RAG OCR
|
||||
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
||||
@@ -157,5 +161,6 @@ USER $UID:$GID
|
||||
|
||||
ARG BUILD_HASH
|
||||
ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
|
||||
ENV DOCKER true
|
||||
|
||||
CMD [ "bash", "start.sh"]
|
||||
|
||||
@@ -59,11 +59,31 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
|
||||
|
||||
## How to Install 🚀
|
||||
|
||||
> [!NOTE]
|
||||
> Please note that for certain Docker environments, additional configurations might be needed. If you encounter any connection issues, our detailed guide on [Open WebUI Documentation](https://docs.openwebui.com/) is ready to assist you.
|
||||
### Installation via Python pip 🐍
|
||||
|
||||
Open WebUI can be installed using pip, the Python package installer. Before proceeding, ensure you're using **Python 3.11** to avoid compatibility issues.
|
||||
|
||||
1. **Install Open WebUI**:
|
||||
Open your terminal and run the following command to install Open WebUI:
|
||||
|
||||
```bash
|
||||
pip install open-webui
|
||||
```
|
||||
|
||||
2. **Running Open WebUI**:
|
||||
After installation, you can start Open WebUI by executing:
|
||||
|
||||
```bash
|
||||
open-webui serve
|
||||
```
|
||||
|
||||
This will start the Open WebUI server, which you can access at [http://localhost:8080](http://localhost:8080)
|
||||
|
||||
### Quick Start with Docker 🐳
|
||||
|
||||
> [!NOTE]
|
||||
> Please note that for certain Docker environments, additional configurations might be needed. If you encounter any connection issues, our detailed guide on [Open WebUI Documentation](https://docs.openwebui.com/) is ready to assist you.
|
||||
|
||||
> [!WARNING]
|
||||
> When using Docker to install Open WebUI, make sure to include the `-v open-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data.
|
||||
|
||||
|
||||
@@ -10,14 +10,13 @@ app = typer.Typer()
|
||||
|
||||
KEY_FILE = Path.cwd() / ".webui_secret_key"
|
||||
|
||||
os.environ["FROM_INIT_PY"] = "true"
|
||||
|
||||
|
||||
@app.command()
|
||||
def serve(
|
||||
host: str = "0.0.0.0",
|
||||
port: int = 8080,
|
||||
):
|
||||
os.environ["FROM_INIT_PY"] = "true"
|
||||
if os.getenv("WEBUI_SECRET_KEY") is None:
|
||||
typer.echo(
|
||||
"Loading WEBUI_SECRET_KEY from file, not provided as an environment variable."
|
||||
@@ -40,6 +39,20 @@ def serve(
|
||||
"/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib",
|
||||
]
|
||||
)
|
||||
try:
|
||||
import torch
|
||||
|
||||
assert torch.cuda.is_available(), "CUDA not available"
|
||||
typer.echo("CUDA seems to be working")
|
||||
except Exception as e:
|
||||
typer.echo(
|
||||
"Error when testing CUDA but USE_CUDA_DOCKER is true. "
|
||||
"Resetting USE_CUDA_DOCKER to false and removing "
|
||||
f"LD_LIBRARY_PATH modifications: {e}"
|
||||
)
|
||||
os.environ["USE_CUDA_DOCKER"] = "false"
|
||||
os.environ["LD_LIBRARY_PATH"] = ":".join(LD_LIBRARY_PATH)
|
||||
|
||||
import open_webui.main # we need set environment variables before importing main
|
||||
|
||||
uvicorn.run(open_webui.main.app, host=host, port=port, forwarded_allow_ips="*")
|
||||
|
||||
@@ -19,16 +19,18 @@ from open_webui.config import (
|
||||
AUDIO_TTS_OPENAI_API_KEY,
|
||||
AUDIO_TTS_SPLIT_ON,
|
||||
AUDIO_TTS_VOICE,
|
||||
AUDIO_TTS_AZURE_SPEECH_REGION,
|
||||
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
|
||||
CACHE_DIR,
|
||||
CORS_ALLOW_ORIGIN,
|
||||
DEVICE_TYPE,
|
||||
WHISPER_MODEL,
|
||||
WHISPER_MODEL_AUTO_UPDATE,
|
||||
WHISPER_MODEL_DIR,
|
||||
AppConfig,
|
||||
)
|
||||
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
from open_webui.env import SRC_LOG_LEVELS, DEVICE_TYPE
|
||||
from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
@@ -62,6 +64,9 @@ app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
|
||||
app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY
|
||||
app.state.config.TTS_SPLIT_ON = AUDIO_TTS_SPLIT_ON
|
||||
|
||||
app.state.config.TTS_AZURE_SPEECH_REGION = AUDIO_TTS_AZURE_SPEECH_REGION
|
||||
app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT
|
||||
|
||||
# setting device type for whisper model
|
||||
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
|
||||
log.info(f"whisper_device_type: {whisper_device_type}")
|
||||
@@ -78,6 +83,8 @@ class TTSConfigForm(BaseModel):
|
||||
MODEL: str
|
||||
VOICE: str
|
||||
SPLIT_ON: str
|
||||
AZURE_SPEECH_REGION: str
|
||||
AZURE_SPEECH_OUTPUT_FORMAT: str
|
||||
|
||||
|
||||
class STTConfigForm(BaseModel):
|
||||
@@ -130,6 +137,8 @@ async def get_audio_config(user=Depends(get_admin_user)):
|
||||
"MODEL": app.state.config.TTS_MODEL,
|
||||
"VOICE": app.state.config.TTS_VOICE,
|
||||
"SPLIT_ON": app.state.config.TTS_SPLIT_ON,
|
||||
"AZURE_SPEECH_REGION": app.state.config.TTS_AZURE_SPEECH_REGION,
|
||||
"AZURE_SPEECH_OUTPUT_FORMAT": app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
|
||||
},
|
||||
"stt": {
|
||||
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
|
||||
@@ -151,6 +160,10 @@ async def update_audio_config(
|
||||
app.state.config.TTS_MODEL = form_data.tts.MODEL
|
||||
app.state.config.TTS_VOICE = form_data.tts.VOICE
|
||||
app.state.config.TTS_SPLIT_ON = form_data.tts.SPLIT_ON
|
||||
app.state.config.TTS_AZURE_SPEECH_REGION = form_data.tts.AZURE_SPEECH_REGION
|
||||
app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = (
|
||||
form_data.tts.AZURE_SPEECH_OUTPUT_FORMAT
|
||||
)
|
||||
|
||||
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
|
||||
@@ -166,6 +179,8 @@ async def update_audio_config(
|
||||
"MODEL": app.state.config.TTS_MODEL,
|
||||
"VOICE": app.state.config.TTS_VOICE,
|
||||
"SPLIT_ON": app.state.config.TTS_SPLIT_ON,
|
||||
"AZURE_SPEECH_REGION": app.state.config.TTS_AZURE_SPEECH_REGION,
|
||||
"AZURE_SPEECH_OUTPUT_FORMAT": app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
|
||||
},
|
||||
"stt": {
|
||||
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
|
||||
@@ -301,6 +316,42 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
elif app.state.config.TTS_ENGINE == "azure":
|
||||
payload = None
|
||||
try:
|
||||
payload = json.loads(body.decode("utf-8"))
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON payload")
|
||||
|
||||
region = app.state.config.TTS_AZURE_SPEECH_REGION
|
||||
language = app.state.config.TTS_VOICE
|
||||
locale = "-".join(app.state.config.TTS_VOICE.split("-")[:1])
|
||||
output_format = app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT
|
||||
url = f"https://{region}.tts.speech.microsoft.com/cognitiveservices/v1"
|
||||
|
||||
headers = {
|
||||
"Ocp-Apim-Subscription-Key": app.state.config.TTS_API_KEY,
|
||||
"Content-Type": "application/ssml+xml",
|
||||
"X-Microsoft-OutputFormat": output_format,
|
||||
}
|
||||
|
||||
data = f"""<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="{locale}">
|
||||
<voice name="{language}">{payload["input"]}</voice>
|
||||
</speak>"""
|
||||
|
||||
response = requests.post(url, headers=headers, data=data)
|
||||
|
||||
if response.status_code == 200:
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(response.content)
|
||||
return FileResponse(file_path)
|
||||
else:
|
||||
log.error(f"Error synthesizing speech - {response.reason}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error synthesizing speech - {response.reason}"
|
||||
)
|
||||
|
||||
|
||||
@app.post("/transcriptions")
|
||||
def transcribe(
|
||||
@@ -309,7 +360,7 @@ def transcribe(
|
||||
):
|
||||
log.info(f"file.content_type: {file.content_type}")
|
||||
|
||||
if file.content_type not in ["audio/mpeg", "audio/wav"]:
|
||||
if file.content_type not in ["audio/mpeg", "audio/wav", "audio/ogg", "audio/x-m4a"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED,
|
||||
@@ -443,7 +494,7 @@ def get_available_models() -> list[dict]:
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
"https://api.elevenlabs.io/v1/models", headers=headers
|
||||
"https://api.elevenlabs.io/v1/models", headers=headers, timeout=5
|
||||
)
|
||||
response.raise_for_status()
|
||||
models = response.json()
|
||||
@@ -478,6 +529,21 @@ def get_available_voices() -> dict:
|
||||
except Exception:
|
||||
# Avoided @lru_cache with exception
|
||||
pass
|
||||
elif app.state.config.TTS_ENGINE == "azure":
|
||||
try:
|
||||
region = app.state.config.TTS_AZURE_SPEECH_REGION
|
||||
url = f"https://{region}.tts.speech.microsoft.com/cognitiveservices/voices/list"
|
||||
headers = {"Ocp-Apim-Subscription-Key": app.state.config.TTS_API_KEY}
|
||||
|
||||
response = requests.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
voices = response.json()
|
||||
for voice in voices:
|
||||
ret[voice["ShortName"]] = (
|
||||
f"{voice['DisplayName']} ({voice['ShortName']})"
|
||||
)
|
||||
except requests.RequestException as e:
|
||||
log.error(f"Error fetching voices: {str(e)}")
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ from open_webui.apps.images.utils.comfyui import (
|
||||
from open_webui.config import (
|
||||
AUTOMATIC1111_API_AUTH,
|
||||
AUTOMATIC1111_BASE_URL,
|
||||
AUTOMATIC1111_CFG_SCALE,
|
||||
AUTOMATIC1111_SAMPLER,
|
||||
AUTOMATIC1111_SCHEDULER,
|
||||
CACHE_DIR,
|
||||
COMFYUI_BASE_URL,
|
||||
COMFYUI_WORKFLOW,
|
||||
@@ -65,6 +68,9 @@ app.state.config.MODEL = IMAGE_GENERATION_MODEL
|
||||
|
||||
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
|
||||
app.state.config.AUTOMATIC1111_CFG_SCALE = AUTOMATIC1111_CFG_SCALE
|
||||
app.state.config.AUTOMATIC1111_SAMPLER = AUTOMATIC1111_SAMPLER
|
||||
app.state.config.AUTOMATIC1111_SCHEDULER = AUTOMATIC1111_SCHEDULER
|
||||
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||
app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW
|
||||
app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES
|
||||
@@ -85,6 +91,9 @@ async def get_config(request: Request, user=Depends(get_admin_user)):
|
||||
"automatic1111": {
|
||||
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
|
||||
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
|
||||
"AUTOMATIC1111_CFG_SCALE": app.state.config.AUTOMATIC1111_CFG_SCALE,
|
||||
"AUTOMATIC1111_SAMPLER": app.state.config.AUTOMATIC1111_SAMPLER,
|
||||
"AUTOMATIC1111_SCHEDULER": app.state.config.AUTOMATIC1111_SCHEDULER,
|
||||
},
|
||||
"comfyui": {
|
||||
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
|
||||
@@ -102,6 +111,9 @@ class OpenAIConfigForm(BaseModel):
|
||||
class Automatic1111ConfigForm(BaseModel):
|
||||
AUTOMATIC1111_BASE_URL: str
|
||||
AUTOMATIC1111_API_AUTH: str
|
||||
AUTOMATIC1111_CFG_SCALE: Optional[str]
|
||||
AUTOMATIC1111_SAMPLER: Optional[str]
|
||||
AUTOMATIC1111_SCHEDULER: Optional[str]
|
||||
|
||||
|
||||
class ComfyUIConfigForm(BaseModel):
|
||||
@@ -133,6 +145,22 @@ async def update_config(form_data: ConfigForm, user=Depends(get_admin_user)):
|
||||
form_data.automatic1111.AUTOMATIC1111_API_AUTH
|
||||
)
|
||||
|
||||
app.state.config.AUTOMATIC1111_CFG_SCALE = (
|
||||
float(form_data.automatic1111.AUTOMATIC1111_CFG_SCALE)
|
||||
if form_data.automatic1111.AUTOMATIC1111_CFG_SCALE
|
||||
else None
|
||||
)
|
||||
app.state.config.AUTOMATIC1111_SAMPLER = (
|
||||
form_data.automatic1111.AUTOMATIC1111_SAMPLER
|
||||
if form_data.automatic1111.AUTOMATIC1111_SAMPLER
|
||||
else None
|
||||
)
|
||||
app.state.config.AUTOMATIC1111_SCHEDULER = (
|
||||
form_data.automatic1111.AUTOMATIC1111_SCHEDULER
|
||||
if form_data.automatic1111.AUTOMATIC1111_SCHEDULER
|
||||
else None
|
||||
)
|
||||
|
||||
app.state.config.COMFYUI_BASE_URL = form_data.comfyui.COMFYUI_BASE_URL.strip("/")
|
||||
app.state.config.COMFYUI_WORKFLOW = form_data.comfyui.COMFYUI_WORKFLOW
|
||||
app.state.config.COMFYUI_WORKFLOW_NODES = form_data.comfyui.COMFYUI_WORKFLOW_NODES
|
||||
@@ -147,6 +175,9 @@ async def update_config(form_data: ConfigForm, user=Depends(get_admin_user)):
|
||||
"automatic1111": {
|
||||
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
|
||||
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
|
||||
"AUTOMATIC1111_CFG_SCALE": app.state.config.AUTOMATIC1111_CFG_SCALE,
|
||||
"AUTOMATIC1111_SAMPLER": app.state.config.AUTOMATIC1111_SAMPLER,
|
||||
"AUTOMATIC1111_SCHEDULER": app.state.config.AUTOMATIC1111_SCHEDULER,
|
||||
},
|
||||
"comfyui": {
|
||||
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
|
||||
@@ -439,7 +470,9 @@ async def image_generations(
|
||||
"response_format": "b64_json",
|
||||
}
|
||||
|
||||
r = requests.post(
|
||||
# Use asyncio.to_thread for the requests.post call
|
||||
r = await asyncio.to_thread(
|
||||
requests.post,
|
||||
url=f"{app.state.config.OPENAI_API_BASE_URL}/images/generations",
|
||||
json=data,
|
||||
headers=headers,
|
||||
@@ -524,6 +557,15 @@ async def image_generations(
|
||||
if form_data.negative_prompt is not None:
|
||||
data["negative_prompt"] = form_data.negative_prompt
|
||||
|
||||
if app.state.config.AUTOMATIC1111_CFG_SCALE:
|
||||
data["cfg_scale"] = app.state.config.AUTOMATIC1111_CFG_SCALE
|
||||
|
||||
if app.state.config.AUTOMATIC1111_SAMPLER:
|
||||
data["sampler_name"] = app.state.config.AUTOMATIC1111_SAMPLER
|
||||
|
||||
if app.state.config.AUTOMATIC1111_SCHEDULER:
|
||||
data["scheduler"] = app.state.config.AUTOMATIC1111_SCHEDULER
|
||||
|
||||
# Use asyncio.to_thread for the requests.post call
|
||||
r = await asyncio.to_thread(
|
||||
requests.post,
|
||||
|
||||
@@ -543,6 +543,65 @@ class GenerateEmbeddingsForm(BaseModel):
|
||||
keep_alive: Optional[Union[int, str]] = None
|
||||
|
||||
|
||||
class GenerateEmbedForm(BaseModel):
|
||||
model: str
|
||||
input: str
|
||||
truncate: Optional[bool]
|
||||
options: Optional[dict] = None
|
||||
keep_alive: Optional[Union[int, str]] = None
|
||||
|
||||
|
||||
@app.post("/api/embed")
|
||||
@app.post("/api/embed/{url_idx}")
|
||||
async def generate_embeddings(
|
||||
form_data: GenerateEmbedForm,
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
if url_idx is None:
|
||||
model = form_data.model
|
||||
|
||||
if ":" not in model:
|
||||
model = f"{model}:latest"
|
||||
|
||||
if model in app.state.MODELS:
|
||||
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
||||
)
|
||||
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/embed",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
)
|
||||
try:
|
||||
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 Exception:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
status_code=r.status_code if r else 500,
|
||||
detail=error_detail,
|
||||
)
|
||||
|
||||
|
||||
@app.post("/api/embeddings")
|
||||
@app.post("/api/embeddings/{url_idx}")
|
||||
async def generate_embeddings(
|
||||
@@ -765,7 +824,10 @@ async def generate_chat_completion(
|
||||
log.debug(payload)
|
||||
|
||||
return await post_streaming_url(
|
||||
f"{url}/api/chat", json.dumps(payload), content_type="application/x-ndjson"
|
||||
f"{url}/api/chat",
|
||||
json.dumps(payload),
|
||||
stream=form_data.stream,
|
||||
content_type="application/x-ndjson",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -220,7 +220,17 @@ def merge_models_lists(model_lists):
|
||||
for model in models
|
||||
if "api.openai.com"
|
||||
not in app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||
or "gpt" in model["id"]
|
||||
or not any(
|
||||
name in model["id"]
|
||||
for name in [
|
||||
"babbage",
|
||||
"dall-e",
|
||||
"davinci",
|
||||
"embedding",
|
||||
"tts",
|
||||
"whisper",
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -323,10 +333,24 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_us
|
||||
r.raise_for_status()
|
||||
|
||||
response_data = r.json()
|
||||
|
||||
if "api.openai.com" in url:
|
||||
response_data["data"] = list(
|
||||
filter(lambda model: "gpt" in model["id"], response_data["data"])
|
||||
)
|
||||
# Filter the response data
|
||||
response_data["data"] = [
|
||||
model
|
||||
for model in response_data["data"]
|
||||
if not any(
|
||||
name in model["id"]
|
||||
for name in [
|
||||
"babbage",
|
||||
"dall-e",
|
||||
"davinci",
|
||||
"embedding",
|
||||
"tts",
|
||||
"whisper",
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
return response_data
|
||||
except Exception as e:
|
||||
@@ -381,14 +405,27 @@ async def generate_chat_completion(
|
||||
"role": user.role,
|
||||
}
|
||||
|
||||
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||
key = app.state.config.OPENAI_API_KEYS[idx]
|
||||
|
||||
# Change max_completion_tokens to max_tokens (Backward compatible)
|
||||
if "api.openai.com" not in url and not payload["model"].lower().startswith("o1-"):
|
||||
if "max_completion_tokens" in payload:
|
||||
# Remove "max_completion_tokens" from the payload
|
||||
payload["max_tokens"] = payload["max_completion_tokens"]
|
||||
del payload["max_completion_tokens"]
|
||||
else:
|
||||
if payload["model"].lower().startswith("o1-") and "max_tokens" in payload:
|
||||
payload["max_completion_tokens"] = payload["max_tokens"]
|
||||
del payload["max_tokens"]
|
||||
if "max_tokens" in payload and "max_completion_tokens" in payload:
|
||||
del payload["max_tokens"]
|
||||
|
||||
# Convert the modified body back to JSON
|
||||
payload = json.dumps(payload)
|
||||
|
||||
log.debug(payload)
|
||||
|
||||
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||
key = app.state.config.OPENAI_API_KEYS[idx]
|
||||
|
||||
headers = {}
|
||||
headers["Authorization"] = f"Bearer {key}"
|
||||
headers["Content-Type"] = "application/json"
|
||||
@@ -399,6 +436,7 @@ async def generate_chat_completion(
|
||||
r = None
|
||||
session = None
|
||||
streaming = False
|
||||
response = None
|
||||
|
||||
try:
|
||||
session = aiohttp.ClientSession(
|
||||
@@ -411,8 +449,6 @@ async def generate_chat_completion(
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
# Check if response is SSE
|
||||
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
||||
streaming = True
|
||||
@@ -425,19 +461,23 @@ async def generate_chat_completion(
|
||||
),
|
||||
)
|
||||
else:
|
||||
response_data = await r.json()
|
||||
return response_data
|
||||
try:
|
||||
response = await r.json()
|
||||
except Exception as e:
|
||||
log.error(e)
|
||||
response = await r.text()
|
||||
|
||||
r.raise_for_status()
|
||||
return response
|
||||
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 Exception:
|
||||
error_detail = f"External: {e}"
|
||||
if isinstance(response, dict):
|
||||
if "error" in response:
|
||||
error_detail = f"{response['error']['message'] if 'message' in response['error'] else response['error']}"
|
||||
elif isinstance(response, str):
|
||||
error_detail = response
|
||||
|
||||
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
|
||||
finally:
|
||||
if not streaming and session:
|
||||
|
||||
@@ -10,13 +10,21 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Iterator, Optional, Sequence, Union
|
||||
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import requests
|
||||
import validators
|
||||
|
||||
from fastapi import Depends, FastAPI, File, Form, HTTPException, UploadFile, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
|
||||
from open_webui.apps.rag.search.main import SearchResult
|
||||
from open_webui.apps.rag.search.brave import search_brave
|
||||
from open_webui.apps.rag.search.duckduckgo import search_duckduckgo
|
||||
from open_webui.apps.rag.search.google_pse import search_google_pse
|
||||
from open_webui.apps.rag.search.jina_search import search_jina
|
||||
from open_webui.apps.rag.search.main import SearchResult
|
||||
from open_webui.apps.rag.search.searchapi import search_searchapi
|
||||
from open_webui.apps.rag.search.searxng import search_searxng
|
||||
from open_webui.apps.rag.search.serper import search_serper
|
||||
@@ -33,15 +41,12 @@ from open_webui.apps.rag.utils import (
|
||||
)
|
||||
from open_webui.apps.webui.models.documents import DocumentForm, Documents
|
||||
from open_webui.apps.webui.models.files import Files
|
||||
from chromadb.utils.batch_utils import create_batches
|
||||
from open_webui.config import (
|
||||
BRAVE_SEARCH_API_KEY,
|
||||
CHROMA_CLIENT,
|
||||
CHUNK_OVERLAP,
|
||||
CHUNK_SIZE,
|
||||
CONTENT_EXTRACTION_ENGINE,
|
||||
CORS_ALLOW_ORIGIN,
|
||||
DEVICE_TYPE,
|
||||
DOCS_DIR,
|
||||
ENABLE_RAG_HYBRID_SEARCH,
|
||||
ENABLE_RAG_LOCAL_WEB_FETCH,
|
||||
@@ -64,6 +69,7 @@ from open_webui.config import (
|
||||
RAG_RERANKING_MODEL,
|
||||
RAG_RERANKING_MODEL_AUTO_UPDATE,
|
||||
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
|
||||
DEFAULT_RAG_TEMPLATE,
|
||||
RAG_TEMPLATE,
|
||||
RAG_TOP_K,
|
||||
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||
@@ -84,9 +90,16 @@ from open_webui.config import (
|
||||
AppConfig,
|
||||
)
|
||||
from open_webui.constants import ERROR_MESSAGES
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
from fastapi import Depends, FastAPI, File, Form, HTTPException, UploadFile, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from open_webui.env import SRC_LOG_LEVELS, DEVICE_TYPE, DOCKER
|
||||
from open_webui.utils.misc import (
|
||||
calculate_sha256,
|
||||
calculate_sha256_string,
|
||||
extract_folders_after_data_docs,
|
||||
sanitize_filename,
|
||||
)
|
||||
from open_webui.utils.utils import get_admin_user, get_verified_user
|
||||
from open_webui.apps.rag.vector.connector import VECTOR_DB_CLIENT
|
||||
|
||||
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
||||
from langchain_community.document_loaders import (
|
||||
BSHTMLLoader,
|
||||
@@ -105,14 +118,8 @@ from langchain_community.document_loaders import (
|
||||
YoutubeLoader,
|
||||
)
|
||||
from langchain_core.documents import Document
|
||||
from pydantic import BaseModel
|
||||
from open_webui.utils.misc import (
|
||||
calculate_sha256,
|
||||
calculate_sha256_string,
|
||||
extract_folders_after_data_docs,
|
||||
sanitize_filename,
|
||||
)
|
||||
from open_webui.utils.utils import get_admin_user, get_verified_user
|
||||
from colbert.infra import ColBERTConfig
|
||||
from colbert.modeling.checkpoint import Checkpoint
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
@@ -143,13 +150,11 @@ app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = RAG_EMBEDDING_OPENAI_BATCH_SI
|
||||
app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
|
||||
app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
|
||||
|
||||
|
||||
app.state.config.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
|
||||
app.state.config.OPENAI_API_KEY = RAG_OPENAI_API_KEY
|
||||
|
||||
app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
|
||||
|
||||
|
||||
app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
|
||||
app.state.YOUTUBE_LOADER_TRANSLATION = None
|
||||
|
||||
@@ -175,13 +180,13 @@ app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_
|
||||
|
||||
def update_embedding_model(
|
||||
embedding_model: str,
|
||||
update_model: bool = False,
|
||||
auto_update: bool = False,
|
||||
):
|
||||
if embedding_model and app.state.config.RAG_EMBEDDING_ENGINE == "":
|
||||
import sentence_transformers
|
||||
|
||||
app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
|
||||
get_model_path(embedding_model, update_model),
|
||||
get_model_path(embedding_model, auto_update),
|
||||
device=DEVICE_TYPE,
|
||||
trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
|
||||
)
|
||||
@@ -191,16 +196,108 @@ def update_embedding_model(
|
||||
|
||||
def update_reranking_model(
|
||||
reranking_model: str,
|
||||
update_model: bool = False,
|
||||
auto_update: bool = False,
|
||||
):
|
||||
if reranking_model:
|
||||
import sentence_transformers
|
||||
if any(model in reranking_model for model in ["jinaai/jina-colbert-v2"]):
|
||||
|
||||
app.state.sentence_transformer_rf = sentence_transformers.CrossEncoder(
|
||||
get_model_path(reranking_model, update_model),
|
||||
device=DEVICE_TYPE,
|
||||
trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
|
||||
)
|
||||
class ColBERT:
|
||||
def __init__(self, name) -> None:
|
||||
print("ColBERT: Loading model", name)
|
||||
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
|
||||
if DOCKER:
|
||||
# This is a workaround for the issue with the docker container
|
||||
# where the torch extension is not loaded properly
|
||||
# and the following error is thrown:
|
||||
# /root/.cache/torch_extensions/py311_cpu/segmented_maxsim_cpp/segmented_maxsim_cpp.so: cannot open shared object file: No such file or directory
|
||||
|
||||
lock_file = "/root/.cache/torch_extensions/py311_cpu/segmented_maxsim_cpp/lock"
|
||||
if os.path.exists(lock_file):
|
||||
os.remove(lock_file)
|
||||
|
||||
self.ckpt = Checkpoint(
|
||||
name,
|
||||
colbert_config=ColBERTConfig(model_name=name),
|
||||
).to(self.device)
|
||||
pass
|
||||
|
||||
def calculate_similarity_scores(
|
||||
self, query_embeddings, document_embeddings
|
||||
):
|
||||
|
||||
query_embeddings = query_embeddings.to(self.device)
|
||||
document_embeddings = document_embeddings.to(self.device)
|
||||
|
||||
# Validate dimensions to ensure compatibility
|
||||
if query_embeddings.dim() != 3:
|
||||
raise ValueError(
|
||||
f"Expected query embeddings to have 3 dimensions, but got {query_embeddings.dim()}."
|
||||
)
|
||||
if document_embeddings.dim() != 3:
|
||||
raise ValueError(
|
||||
f"Expected document embeddings to have 3 dimensions, but got {document_embeddings.dim()}."
|
||||
)
|
||||
if query_embeddings.size(0) not in [1, document_embeddings.size(0)]:
|
||||
raise ValueError(
|
||||
"There should be either one query or queries equal to the number of documents."
|
||||
)
|
||||
|
||||
# Transpose the query embeddings to align for matrix multiplication
|
||||
transposed_query_embeddings = query_embeddings.permute(0, 2, 1)
|
||||
# Compute similarity scores using batch matrix multiplication
|
||||
computed_scores = torch.matmul(
|
||||
document_embeddings, transposed_query_embeddings
|
||||
)
|
||||
# Apply max pooling to extract the highest semantic similarity across each document's sequence
|
||||
maximum_scores = torch.max(computed_scores, dim=1).values
|
||||
|
||||
# Sum up the maximum scores across features to get the overall document relevance scores
|
||||
final_scores = maximum_scores.sum(dim=1)
|
||||
|
||||
normalized_scores = torch.softmax(final_scores, dim=0)
|
||||
|
||||
return normalized_scores.detach().cpu().numpy().astype(np.float32)
|
||||
|
||||
def predict(self, sentences):
|
||||
|
||||
query = sentences[0][0]
|
||||
docs = [i[1] for i in sentences]
|
||||
|
||||
# Embedding the documents
|
||||
embedded_docs = self.ckpt.docFromText(docs, bsize=32)[0]
|
||||
# Embedding the queries
|
||||
embedded_queries = self.ckpt.queryFromText([query], bsize=32)
|
||||
embedded_query = embedded_queries[0]
|
||||
|
||||
# Calculate retrieval scores for the query against all documents
|
||||
scores = self.calculate_similarity_scores(
|
||||
embedded_query.unsqueeze(0), embedded_docs
|
||||
)
|
||||
|
||||
return scores
|
||||
|
||||
try:
|
||||
app.state.sentence_transformer_rf = ColBERT(
|
||||
get_model_path(reranking_model, auto_update)
|
||||
)
|
||||
except Exception as e:
|
||||
log.error(f"ColBERT: {e}")
|
||||
app.state.sentence_transformer_rf = None
|
||||
app.state.config.ENABLE_RAG_HYBRID_SEARCH = False
|
||||
else:
|
||||
import sentence_transformers
|
||||
|
||||
try:
|
||||
app.state.sentence_transformer_rf = sentence_transformers.CrossEncoder(
|
||||
get_model_path(reranking_model, auto_update),
|
||||
device=DEVICE_TYPE,
|
||||
trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
|
||||
)
|
||||
except:
|
||||
log.error("CrossEncoder error")
|
||||
app.state.sentence_transformer_rf = None
|
||||
app.state.config.ENABLE_RAG_HYBRID_SEARCH = False
|
||||
else:
|
||||
app.state.sentence_transformer_rf = None
|
||||
|
||||
@@ -593,7 +690,7 @@ async def update_query_settings(
|
||||
form_data: QuerySettingsForm, user=Depends(get_admin_user)
|
||||
):
|
||||
app.state.config.RAG_TEMPLATE = (
|
||||
form_data.template if form_data.template else RAG_TEMPLATE
|
||||
form_data.template if form_data.template != "" else DEFAULT_RAG_TEMPLATE
|
||||
)
|
||||
app.state.config.TOP_K = form_data.k if form_data.k else 4
|
||||
app.state.config.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
|
||||
@@ -998,41 +1095,39 @@ def store_docs_in_vector_db(
|
||||
|
||||
try:
|
||||
if overwrite:
|
||||
for collection in CHROMA_CLIENT.list_collections():
|
||||
if collection_name == collection.name:
|
||||
log.info(f"deleting existing collection {collection_name}")
|
||||
CHROMA_CLIENT.delete_collection(name=collection_name)
|
||||
if VECTOR_DB_CLIENT.has_collection(collection_name=collection_name):
|
||||
log.info(f"deleting existing collection {collection_name}")
|
||||
VECTOR_DB_CLIENT.delete_collection(collection_name=collection_name)
|
||||
|
||||
collection = CHROMA_CLIENT.create_collection(name=collection_name)
|
||||
|
||||
embedding_func = get_embedding_function(
|
||||
app.state.config.RAG_EMBEDDING_ENGINE,
|
||||
app.state.config.RAG_EMBEDDING_MODEL,
|
||||
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))
|
||||
embeddings = embedding_func(embedding_texts)
|
||||
|
||||
for batch in create_batches(
|
||||
api=CHROMA_CLIENT,
|
||||
ids=[str(uuid.uuid4()) for _ in texts],
|
||||
metadatas=metadatas,
|
||||
embeddings=embeddings,
|
||||
documents=texts,
|
||||
):
|
||||
collection.add(*batch)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
if e.__class__.__name__ == "UniqueConstraintError":
|
||||
if VECTOR_DB_CLIENT.has_collection(collection_name=collection_name):
|
||||
log.info(f"collection {collection_name} already exists")
|
||||
return True
|
||||
else:
|
||||
embedding_function = get_embedding_function(
|
||||
app.state.config.RAG_EMBEDDING_ENGINE,
|
||||
app.state.config.RAG_EMBEDDING_MODEL,
|
||||
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,
|
||||
)
|
||||
|
||||
VECTOR_DB_CLIENT.insert(
|
||||
collection_name=collection_name,
|
||||
items=[
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"text": text,
|
||||
"vector": embedding_function(text.replace("\n", " ")),
|
||||
"metadata": metadatas[idx],
|
||||
}
|
||||
for idx, text in enumerate(texts)
|
||||
],
|
||||
)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@@ -1158,7 +1253,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
||||
elif (
|
||||
file_content_type
|
||||
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||
or file_ext in ["doc", "docx"]
|
||||
or file_ext == "docx"
|
||||
):
|
||||
loader = Docx2txtLoader(file_path)
|
||||
elif file_content_type in [
|
||||
@@ -1396,7 +1491,7 @@ def scan_docs_dir(user=Depends(get_admin_user)):
|
||||
|
||||
@app.post("/reset/db")
|
||||
def reset_vector_db(user=Depends(get_admin_user)):
|
||||
CHROMA_CLIENT.reset()
|
||||
VECTOR_DB_CLIENT.reset()
|
||||
|
||||
|
||||
@app.post("/reset/uploads")
|
||||
@@ -1437,7 +1532,7 @@ def reset(user=Depends(get_admin_user)) -> bool:
|
||||
log.error("Failed to delete %s. Reason: %s" % (file_path, e))
|
||||
|
||||
try:
|
||||
CHROMA_CLIENT.reset()
|
||||
VECTOR_DB_CLIENT.reset()
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
|
||||
@@ -1,24 +1,68 @@
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from typing import Optional, Union
|
||||
|
||||
import requests
|
||||
from open_webui.apps.ollama.main import (
|
||||
GenerateEmbeddingsForm,
|
||||
generate_ollama_embeddings,
|
||||
)
|
||||
from open_webui.config import CHROMA_CLIENT
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
from huggingface_hub import snapshot_download
|
||||
from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriever
|
||||
from langchain_community.retrievers import BM25Retriever
|
||||
from langchain_core.documents import Document
|
||||
|
||||
|
||||
from open_webui.apps.ollama.main import (
|
||||
GenerateEmbeddingsForm,
|
||||
generate_ollama_embeddings,
|
||||
)
|
||||
from open_webui.apps.rag.vector.connector import VECTOR_DB_CLIENT
|
||||
from open_webui.utils.misc import get_last_user_message
|
||||
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.callbacks import CallbackManagerForRetrieverRun
|
||||
from langchain_core.retrievers import BaseRetriever
|
||||
|
||||
|
||||
class VectorSearchRetriever(BaseRetriever):
|
||||
collection_name: Any
|
||||
embedding_function: Any
|
||||
top_k: int
|
||||
|
||||
def _get_relevant_documents(
|
||||
self,
|
||||
query: str,
|
||||
*,
|
||||
run_manager: CallbackManagerForRetrieverRun,
|
||||
) -> list[Document]:
|
||||
result = VECTOR_DB_CLIENT.search(
|
||||
collection_name=self.collection_name,
|
||||
vectors=[self.embedding_function(query)],
|
||||
limit=self.top_k,
|
||||
)
|
||||
|
||||
ids = result.ids[0]
|
||||
metadatas = result.metadatas[0]
|
||||
documents = result.documents[0]
|
||||
|
||||
results = []
|
||||
for idx in range(len(ids)):
|
||||
results.append(
|
||||
Document(
|
||||
metadata=metadatas[idx],
|
||||
page_content=documents[idx],
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def query_doc(
|
||||
collection_name: str,
|
||||
query: str,
|
||||
@@ -26,17 +70,18 @@ def query_doc(
|
||||
k: int,
|
||||
):
|
||||
try:
|
||||
collection = CHROMA_CLIENT.get_collection(name=collection_name)
|
||||
query_embeddings = embedding_function(query)
|
||||
|
||||
result = collection.query(
|
||||
query_embeddings=[query_embeddings],
|
||||
n_results=k,
|
||||
result = VECTOR_DB_CLIENT.search(
|
||||
collection_name=collection_name,
|
||||
vectors=[embedding_function(query)],
|
||||
limit=k,
|
||||
)
|
||||
|
||||
print("result", result)
|
||||
|
||||
log.info(f"query_doc:result {result}")
|
||||
return result
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise e
|
||||
|
||||
|
||||
@@ -47,27 +92,25 @@ def query_doc_with_hybrid_search(
|
||||
k: int,
|
||||
reranking_function,
|
||||
r: float,
|
||||
):
|
||||
) -> dict:
|
||||
try:
|
||||
collection = CHROMA_CLIENT.get_collection(name=collection_name)
|
||||
documents = collection.get() # get all documents
|
||||
result = VECTOR_DB_CLIENT.get(collection_name=collection_name)
|
||||
|
||||
bm25_retriever = BM25Retriever.from_texts(
|
||||
texts=documents.get("documents"),
|
||||
metadatas=documents.get("metadatas"),
|
||||
texts=result.documents[0],
|
||||
metadatas=result.metadatas[0],
|
||||
)
|
||||
bm25_retriever.k = k
|
||||
|
||||
chroma_retriever = ChromaRetriever(
|
||||
collection=collection,
|
||||
vector_search_retriever = VectorSearchRetriever(
|
||||
collection_name=collection_name,
|
||||
embedding_function=embedding_function,
|
||||
top_n=k,
|
||||
top_k=k,
|
||||
)
|
||||
|
||||
ensemble_retriever = EnsembleRetriever(
|
||||
retrievers=[bm25_retriever, chroma_retriever], weights=[0.5, 0.5]
|
||||
retrievers=[bm25_retriever, vector_search_retriever], weights=[0.5, 0.5]
|
||||
)
|
||||
|
||||
compressor = RerankCompressor(
|
||||
embedding_function=embedding_function,
|
||||
top_n=k,
|
||||
@@ -92,7 +135,9 @@ def query_doc_with_hybrid_search(
|
||||
raise e
|
||||
|
||||
|
||||
def merge_and_sort_query_results(query_results, k, reverse=False):
|
||||
def merge_and_sort_query_results(
|
||||
query_results: list[dict], k: int, reverse: bool = False
|
||||
) -> list[dict]:
|
||||
# Initialize lists to store combined data
|
||||
combined_distances = []
|
||||
combined_documents = []
|
||||
@@ -138,7 +183,7 @@ def query_collection(
|
||||
query: str,
|
||||
embedding_function,
|
||||
k: int,
|
||||
):
|
||||
) -> dict:
|
||||
results = []
|
||||
for collection_name in collection_names:
|
||||
if collection_name:
|
||||
@@ -149,9 +194,9 @@ def query_collection(
|
||||
k=k,
|
||||
embedding_function=embedding_function,
|
||||
)
|
||||
results.append(result)
|
||||
except Exception:
|
||||
pass
|
||||
results.append(result.model_dump())
|
||||
except Exception as e:
|
||||
log.exception(f"Error when querying the collection: {e}")
|
||||
else:
|
||||
pass
|
||||
|
||||
@@ -165,8 +210,9 @@ def query_collection_with_hybrid_search(
|
||||
k: int,
|
||||
reranking_function,
|
||||
r: float,
|
||||
):
|
||||
) -> dict:
|
||||
results = []
|
||||
error = False
|
||||
for collection_name in collection_names:
|
||||
try:
|
||||
result = query_doc_with_hybrid_search(
|
||||
@@ -178,14 +224,39 @@ def query_collection_with_hybrid_search(
|
||||
r=r,
|
||||
)
|
||||
results.append(result)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
log.exception(
|
||||
"Error when querying the collection with " f"hybrid_search: {e}"
|
||||
)
|
||||
error = True
|
||||
|
||||
if error:
|
||||
raise Exception(
|
||||
"Hybrid search failed for all collections. Using Non hybrid search as fallback."
|
||||
)
|
||||
|
||||
return merge_and_sort_query_results(results, k=k, reverse=True)
|
||||
|
||||
|
||||
def rag_template(template: str, context: str, query: str):
|
||||
template = template.replace("[context]", context)
|
||||
template = template.replace("[query]", query)
|
||||
count = template.count("[context]")
|
||||
assert "[context]" in template, "RAG template does not contain '[context]'"
|
||||
|
||||
if "<context>" in context and "</context>" in context:
|
||||
log.debug(
|
||||
"WARNING: Potential prompt injection attack: the RAG "
|
||||
"context contains '<context>' and '</context>'. This might be "
|
||||
"nothing, or the user might be trying to hack something."
|
||||
)
|
||||
|
||||
if "[query]" in context:
|
||||
query_placeholder = f"[query-{str(uuid.uuid4())}]"
|
||||
template = template.replace("[query]", query_placeholder)
|
||||
template = template.replace("[context]", context)
|
||||
template = template.replace(query_placeholder, query)
|
||||
else:
|
||||
template = template.replace("[context]", context)
|
||||
template = template.replace("[query]", query)
|
||||
return template
|
||||
|
||||
|
||||
@@ -262,19 +333,27 @@ def get_rag_context(
|
||||
continue
|
||||
|
||||
try:
|
||||
context = None
|
||||
if file["type"] == "text":
|
||||
context = file["content"]
|
||||
else:
|
||||
if hybrid_search:
|
||||
context = query_collection_with_hybrid_search(
|
||||
collection_names=collection_names,
|
||||
query=query,
|
||||
embedding_function=embedding_function,
|
||||
k=k,
|
||||
reranking_function=reranking_function,
|
||||
r=r,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
context = query_collection_with_hybrid_search(
|
||||
collection_names=collection_names,
|
||||
query=query,
|
||||
embedding_function=embedding_function,
|
||||
k=k,
|
||||
reranking_function=reranking_function,
|
||||
r=r,
|
||||
)
|
||||
except Exception as e:
|
||||
log.debug(
|
||||
"Error when using hybrid search, using"
|
||||
" non hybrid search as fallback."
|
||||
)
|
||||
|
||||
if (not hybrid_search) or (context is None):
|
||||
context = query_collection(
|
||||
collection_names=collection_names,
|
||||
query=query,
|
||||
@@ -283,7 +362,6 @@ def get_rag_context(
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
context = None
|
||||
|
||||
if context:
|
||||
relevant_contexts.append({**context, "source": file})
|
||||
@@ -391,51 +469,11 @@ def generate_openai_batch_embeddings(
|
||||
return None
|
||||
|
||||
|
||||
from typing import Any
|
||||
|
||||
from langchain_core.callbacks import CallbackManagerForRetrieverRun
|
||||
from langchain_core.retrievers import BaseRetriever
|
||||
|
||||
|
||||
class ChromaRetriever(BaseRetriever):
|
||||
collection: Any
|
||||
embedding_function: Any
|
||||
top_n: int
|
||||
|
||||
def _get_relevant_documents(
|
||||
self,
|
||||
query: str,
|
||||
*,
|
||||
run_manager: CallbackManagerForRetrieverRun,
|
||||
) -> list[Document]:
|
||||
query_embeddings = self.embedding_function(query)
|
||||
|
||||
results = self.collection.query(
|
||||
query_embeddings=[query_embeddings],
|
||||
n_results=self.top_n,
|
||||
)
|
||||
|
||||
ids = results["ids"][0]
|
||||
metadatas = results["metadatas"][0]
|
||||
documents = results["documents"][0]
|
||||
|
||||
results = []
|
||||
for idx in range(len(ids)):
|
||||
results.append(
|
||||
Document(
|
||||
metadata=metadatas[idx],
|
||||
page_content=documents[idx],
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
import operator
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from langchain_core.callbacks import Callbacks
|
||||
from langchain_core.documents import BaseDocumentCompressor, Document
|
||||
from langchain_core.pydantic_v1 import Extra
|
||||
|
||||
|
||||
class RerankCompressor(BaseDocumentCompressor):
|
||||
@@ -445,7 +483,7 @@ class RerankCompressor(BaseDocumentCompressor):
|
||||
r_score: float
|
||||
|
||||
class Config:
|
||||
extra = Extra.forbid
|
||||
extra = "forbid"
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def compress_documents(
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from open_webui.apps.rag.vector.dbs.chroma import ChromaClient
|
||||
from open_webui.apps.rag.vector.dbs.milvus import MilvusClient
|
||||
|
||||
|
||||
from open_webui.config import VECTOR_DB
|
||||
|
||||
if VECTOR_DB == "milvus":
|
||||
VECTOR_DB_CLIENT = MilvusClient()
|
||||
else:
|
||||
VECTOR_DB_CLIENT = ChromaClient()
|
||||
@@ -0,0 +1,122 @@
|
||||
import chromadb
|
||||
from chromadb import Settings
|
||||
from chromadb.utils.batch_utils import create_batches
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.apps.rag.vector.main import VectorItem, SearchResult, GetResult
|
||||
from open_webui.config import (
|
||||
CHROMA_DATA_PATH,
|
||||
CHROMA_HTTP_HOST,
|
||||
CHROMA_HTTP_PORT,
|
||||
CHROMA_HTTP_HEADERS,
|
||||
CHROMA_HTTP_SSL,
|
||||
CHROMA_TENANT,
|
||||
CHROMA_DATABASE,
|
||||
)
|
||||
|
||||
|
||||
class ChromaClient:
|
||||
def __init__(self):
|
||||
if CHROMA_HTTP_HOST != "":
|
||||
self.client = chromadb.HttpClient(
|
||||
host=CHROMA_HTTP_HOST,
|
||||
port=CHROMA_HTTP_PORT,
|
||||
headers=CHROMA_HTTP_HEADERS,
|
||||
ssl=CHROMA_HTTP_SSL,
|
||||
tenant=CHROMA_TENANT,
|
||||
database=CHROMA_DATABASE,
|
||||
settings=Settings(allow_reset=True, anonymized_telemetry=False),
|
||||
)
|
||||
else:
|
||||
self.client = chromadb.PersistentClient(
|
||||
path=CHROMA_DATA_PATH,
|
||||
settings=Settings(allow_reset=True, anonymized_telemetry=False),
|
||||
tenant=CHROMA_TENANT,
|
||||
database=CHROMA_DATABASE,
|
||||
)
|
||||
|
||||
def has_collection(self, collection_name: str) -> bool:
|
||||
# Check if the collection exists based on the collection name.
|
||||
collections = self.client.list_collections()
|
||||
return collection_name in [collection.name for collection in collections]
|
||||
|
||||
def delete_collection(self, collection_name: str):
|
||||
# Delete the collection based on the collection name.
|
||||
return self.client.delete_collection(name=collection_name)
|
||||
|
||||
def search(
|
||||
self, collection_name: str, vectors: list[list[float | int]], limit: int
|
||||
) -> Optional[SearchResult]:
|
||||
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
|
||||
collection = self.client.get_collection(name=collection_name)
|
||||
if collection:
|
||||
result = collection.query(
|
||||
query_embeddings=vectors,
|
||||
n_results=limit,
|
||||
)
|
||||
|
||||
return SearchResult(
|
||||
**{
|
||||
"ids": result["ids"],
|
||||
"distances": result["distances"],
|
||||
"documents": result["documents"],
|
||||
"metadatas": result["metadatas"],
|
||||
}
|
||||
)
|
||||
return None
|
||||
|
||||
def get(self, collection_name: str) -> Optional[GetResult]:
|
||||
# Get all the items in the collection.
|
||||
collection = self.client.get_collection(name=collection_name)
|
||||
if collection:
|
||||
result = collection.get()
|
||||
return GetResult(
|
||||
**{
|
||||
"ids": [result["ids"]],
|
||||
"documents": [result["documents"]],
|
||||
"metadatas": [result["metadatas"]],
|
||||
}
|
||||
)
|
||||
return None
|
||||
|
||||
def insert(self, collection_name: str, items: list[VectorItem]):
|
||||
# Insert the items into the collection, if the collection does not exist, it will be created.
|
||||
collection = self.client.get_or_create_collection(name=collection_name)
|
||||
|
||||
ids = [item["id"] for item in items]
|
||||
documents = [item["text"] for item in items]
|
||||
embeddings = [item["vector"] for item in items]
|
||||
metadatas = [item["metadata"] for item in items]
|
||||
|
||||
for batch in create_batches(
|
||||
api=self.client,
|
||||
documents=documents,
|
||||
embeddings=embeddings,
|
||||
ids=ids,
|
||||
metadatas=metadatas,
|
||||
):
|
||||
collection.add(*batch)
|
||||
|
||||
def upsert(self, collection_name: str, items: list[VectorItem]):
|
||||
# Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created.
|
||||
collection = self.client.get_or_create_collection(name=collection_name)
|
||||
|
||||
ids = [item["id"] for item in items]
|
||||
documents = [item["text"] for item in items]
|
||||
embeddings = [item["vector"] for item in items]
|
||||
metadatas = [item["metadata"] for item in items]
|
||||
|
||||
collection.upsert(
|
||||
ids=ids, documents=documents, embeddings=embeddings, metadatas=metadatas
|
||||
)
|
||||
|
||||
def delete(self, collection_name: str, ids: list[str]):
|
||||
# Delete the items from the collection based on the ids.
|
||||
collection = self.client.get_collection(name=collection_name)
|
||||
if collection:
|
||||
collection.delete(ids=ids)
|
||||
|
||||
def reset(self):
|
||||
# Resets the database. This will delete all collections and item entries.
|
||||
return self.client.reset()
|
||||
@@ -0,0 +1,205 @@
|
||||
from pymilvus import MilvusClient as Client
|
||||
from pymilvus import FieldSchema, DataType
|
||||
import json
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.apps.rag.vector.main import VectorItem, SearchResult, GetResult
|
||||
from open_webui.config import (
|
||||
MILVUS_URI,
|
||||
)
|
||||
|
||||
|
||||
class MilvusClient:
|
||||
def __init__(self):
|
||||
self.collection_prefix = "open_webui"
|
||||
self.client = Client(uri=MILVUS_URI)
|
||||
|
||||
def _result_to_get_result(self, result) -> GetResult:
|
||||
print(result)
|
||||
|
||||
ids = []
|
||||
documents = []
|
||||
metadatas = []
|
||||
|
||||
for match in result:
|
||||
_ids = []
|
||||
_documents = []
|
||||
_metadatas = []
|
||||
|
||||
for item in match:
|
||||
_ids.append(item.get("id"))
|
||||
_documents.append(item.get("data", {}).get("text"))
|
||||
_metadatas.append(item.get("metadata"))
|
||||
|
||||
ids.append(_ids)
|
||||
documents.append(_documents)
|
||||
metadatas.append(_metadatas)
|
||||
|
||||
return GetResult(
|
||||
**{
|
||||
"ids": ids,
|
||||
"documents": documents,
|
||||
"metadatas": metadatas,
|
||||
}
|
||||
)
|
||||
|
||||
def _result_to_search_result(self, result) -> SearchResult:
|
||||
print(result)
|
||||
|
||||
ids = []
|
||||
distances = []
|
||||
documents = []
|
||||
metadatas = []
|
||||
|
||||
for match in result:
|
||||
_ids = []
|
||||
_distances = []
|
||||
_documents = []
|
||||
_metadatas = []
|
||||
|
||||
for item in match:
|
||||
_ids.append(item.get("id"))
|
||||
_distances.append(item.get("distance"))
|
||||
_documents.append(item.get("entity", {}).get("data", {}).get("text"))
|
||||
_metadatas.append(item.get("entity", {}).get("metadata"))
|
||||
|
||||
ids.append(_ids)
|
||||
distances.append(_distances)
|
||||
documents.append(_documents)
|
||||
metadatas.append(_metadatas)
|
||||
|
||||
return SearchResult(
|
||||
**{
|
||||
"ids": ids,
|
||||
"distances": distances,
|
||||
"documents": documents,
|
||||
"metadatas": metadatas,
|
||||
}
|
||||
)
|
||||
|
||||
def _create_collection(self, collection_name: str, dimension: int):
|
||||
schema = self.client.create_schema(
|
||||
auto_id=False,
|
||||
enable_dynamic_field=True,
|
||||
)
|
||||
schema.add_field(
|
||||
field_name="id",
|
||||
datatype=DataType.VARCHAR,
|
||||
is_primary=True,
|
||||
max_length=65535,
|
||||
)
|
||||
schema.add_field(
|
||||
field_name="vector",
|
||||
datatype=DataType.FLOAT_VECTOR,
|
||||
dim=dimension,
|
||||
description="vector",
|
||||
)
|
||||
schema.add_field(field_name="data", datatype=DataType.JSON, description="data")
|
||||
schema.add_field(
|
||||
field_name="metadata", datatype=DataType.JSON, description="metadata"
|
||||
)
|
||||
|
||||
index_params = self.client.prepare_index_params()
|
||||
index_params.add_index(
|
||||
field_name="vector", index_type="HNSW", metric_type="COSINE", params={}
|
||||
)
|
||||
|
||||
self.client.create_collection(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}",
|
||||
schema=schema,
|
||||
index_params=index_params,
|
||||
)
|
||||
|
||||
def has_collection(self, collection_name: str) -> bool:
|
||||
# Check if the collection exists based on the collection name.
|
||||
return self.client.has_collection(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}"
|
||||
)
|
||||
|
||||
def delete_collection(self, collection_name: str):
|
||||
# Delete the collection based on the collection name.
|
||||
return self.client.drop_collection(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}"
|
||||
)
|
||||
|
||||
def search(
|
||||
self, collection_name: str, vectors: list[list[float | int]], limit: int
|
||||
) -> Optional[SearchResult]:
|
||||
# Search for the nearest neighbor items based on the vectors and return 'limit' number of results.
|
||||
result = self.client.search(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}",
|
||||
data=vectors,
|
||||
limit=limit,
|
||||
output_fields=["data", "metadata"],
|
||||
)
|
||||
|
||||
return self._result_to_search_result(result)
|
||||
|
||||
def get(self, collection_name: str) -> Optional[GetResult]:
|
||||
# Get all the items in the collection.
|
||||
result = self.client.query(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}",
|
||||
filter='id != ""',
|
||||
)
|
||||
return self._result_to_get_result([result])
|
||||
|
||||
def insert(self, collection_name: str, items: list[VectorItem]):
|
||||
# Insert the items into the collection, if the collection does not exist, it will be created.
|
||||
if not self.client.has_collection(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}"
|
||||
):
|
||||
self._create_collection(
|
||||
collection_name=collection_name, dimension=len(items[0]["vector"])
|
||||
)
|
||||
|
||||
return self.client.insert(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}",
|
||||
data=[
|
||||
{
|
||||
"id": item["id"],
|
||||
"vector": item["vector"],
|
||||
"data": {"text": item["text"]},
|
||||
"metadata": item["metadata"],
|
||||
}
|
||||
for item in items
|
||||
],
|
||||
)
|
||||
|
||||
def upsert(self, collection_name: str, items: list[VectorItem]):
|
||||
# Update the items in the collection, if the items are not present, insert them. If the collection does not exist, it will be created.
|
||||
if not self.client.has_collection(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}"
|
||||
):
|
||||
self._create_collection(
|
||||
collection_name=collection_name, dimension=len(items[0]["vector"])
|
||||
)
|
||||
|
||||
return self.client.upsert(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}",
|
||||
data=[
|
||||
{
|
||||
"id": item["id"],
|
||||
"vector": item["vector"],
|
||||
"data": {"text": item["text"]},
|
||||
"metadata": item["metadata"],
|
||||
}
|
||||
for item in items
|
||||
],
|
||||
)
|
||||
|
||||
def delete(self, collection_name: str, ids: list[str]):
|
||||
# Delete the items from the collection based on the ids.
|
||||
|
||||
return self.client.delete(
|
||||
collection_name=f"{self.collection_prefix}_{collection_name}",
|
||||
ids=ids,
|
||||
)
|
||||
|
||||
def reset(self):
|
||||
# Resets the database. This will delete all collections and item entries.
|
||||
|
||||
collection_names = self.client.list_collections()
|
||||
for collection_name in collection_names:
|
||||
if collection_name.startswith(self.collection_prefix):
|
||||
self.client.drop_collection(collection_name=collection_name)
|
||||
@@ -0,0 +1,19 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List, Any
|
||||
|
||||
|
||||
class VectorItem(BaseModel):
|
||||
id: str
|
||||
text: str
|
||||
vector: List[float | int]
|
||||
metadata: Any
|
||||
|
||||
|
||||
class GetResult(BaseModel):
|
||||
ids: Optional[List[List[str]]]
|
||||
documents: Optional[List[List[str]]]
|
||||
metadatas: Optional[List[List[Any]]]
|
||||
|
||||
|
||||
class SearchResult(GetResult):
|
||||
distances: Optional[List[List[float | int]]]
|
||||
@@ -1,21 +1,123 @@
|
||||
import asyncio
|
||||
|
||||
import socketio
|
||||
from open_webui.apps.webui.models.users import Users
|
||||
from open_webui.utils.utils import decode_token
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
from open_webui.apps.webui.models.users import Users
|
||||
from open_webui.env import (
|
||||
ENABLE_WEBSOCKET_SUPPORT,
|
||||
WEBSOCKET_MANAGER,
|
||||
WEBSOCKET_REDIS_URL,
|
||||
)
|
||||
from open_webui.utils.utils import decode_token
|
||||
from open_webui.apps.socket.utils import RedisDict
|
||||
|
||||
from open_webui.env import (
|
||||
GLOBAL_LOG_LEVEL,
|
||||
SRC_LOG_LEVELS,
|
||||
)
|
||||
|
||||
|
||||
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["SOCKET"])
|
||||
|
||||
|
||||
if WEBSOCKET_MANAGER == "redis":
|
||||
mgr = socketio.AsyncRedisManager(WEBSOCKET_REDIS_URL)
|
||||
sio = socketio.AsyncServer(
|
||||
cors_allowed_origins=[],
|
||||
async_mode="asgi",
|
||||
transports=(
|
||||
["polling", "websocket"] if ENABLE_WEBSOCKET_SUPPORT else ["polling"]
|
||||
),
|
||||
allow_upgrades=ENABLE_WEBSOCKET_SUPPORT,
|
||||
always_connect=True,
|
||||
client_manager=mgr,
|
||||
)
|
||||
else:
|
||||
sio = socketio.AsyncServer(
|
||||
cors_allowed_origins=[],
|
||||
async_mode="asgi",
|
||||
transports=(
|
||||
["polling", "websocket"] if ENABLE_WEBSOCKET_SUPPORT else ["polling"]
|
||||
),
|
||||
allow_upgrades=ENABLE_WEBSOCKET_SUPPORT,
|
||||
always_connect=True,
|
||||
)
|
||||
|
||||
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 = {}
|
||||
if WEBSOCKET_MANAGER == "redis":
|
||||
SESSION_POOL = RedisDict("open-webui:session_pool", redis_url=WEBSOCKET_REDIS_URL)
|
||||
USER_POOL = RedisDict("open-webui:user_pool", redis_url=WEBSOCKET_REDIS_URL)
|
||||
USAGE_POOL = RedisDict("open-webui:usage_pool", redis_url=WEBSOCKET_REDIS_URL)
|
||||
else:
|
||||
SESSION_POOL = {}
|
||||
USER_POOL = {}
|
||||
USAGE_POOL = {}
|
||||
|
||||
|
||||
# Timeout duration in seconds
|
||||
TIMEOUT_DURATION = 3
|
||||
|
||||
|
||||
async def periodic_usage_pool_cleanup():
|
||||
while True:
|
||||
now = int(time.time())
|
||||
for model_id, connections in list(USAGE_POOL.items()):
|
||||
# Creating a list of sids to remove if they have timed out
|
||||
expired_sids = [
|
||||
sid
|
||||
for sid, details in connections.items()
|
||||
if now - details["updated_at"] > TIMEOUT_DURATION
|
||||
]
|
||||
|
||||
for sid in expired_sids:
|
||||
del connections[sid]
|
||||
|
||||
if not connections:
|
||||
log.debug(f"Cleaning up model {model_id} from usage pool")
|
||||
del USAGE_POOL[model_id]
|
||||
else:
|
||||
USAGE_POOL[model_id] = connections
|
||||
|
||||
# Emit updated usage information after cleaning
|
||||
await sio.emit("usage", {"models": get_models_in_use()})
|
||||
|
||||
await asyncio.sleep(TIMEOUT_DURATION)
|
||||
|
||||
|
||||
app = socketio.ASGIApp(
|
||||
sio,
|
||||
socketio_path="/ws/socket.io",
|
||||
)
|
||||
|
||||
|
||||
def get_models_in_use():
|
||||
# List models that are currently in use
|
||||
models_in_use = list(USAGE_POOL.keys())
|
||||
return models_in_use
|
||||
|
||||
|
||||
@sio.on("usage")
|
||||
async def usage(sid, data):
|
||||
model_id = data["model"]
|
||||
# Record the timestamp for the last update
|
||||
current_time = int(time.time())
|
||||
|
||||
# Store the new usage data and task
|
||||
USAGE_POOL[model_id] = {
|
||||
**(USAGE_POOL[model_id] if model_id in USAGE_POOL else {}),
|
||||
sid: {"updated_at": current_time},
|
||||
}
|
||||
|
||||
# Broadcast the usage data to all clients
|
||||
await sio.emit("usage", {"models": get_models_in_use()})
|
||||
|
||||
|
||||
@sio.event
|
||||
async def connect(sid, environ, auth):
|
||||
user = None
|
||||
@@ -32,15 +134,14 @@ async def connect(sid, environ, auth):
|
||||
else:
|
||||
USER_POOL[user.id] = [sid]
|
||||
|
||||
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
# print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
await sio.emit("user-count", {"count": len(USER_POOL.items())})
|
||||
await sio.emit("usage", {"models": get_models_in_use()})
|
||||
|
||||
|
||||
@sio.on("user-join")
|
||||
async def user_join(sid, data):
|
||||
print("user-join", sid, data)
|
||||
# print("user-join", sid, data)
|
||||
|
||||
auth = data["auth"] if "auth" in data else None
|
||||
if not auth or "token" not in auth:
|
||||
@@ -60,67 +161,14 @@ async def user_join(sid, data):
|
||||
else:
|
||||
USER_POOL[user.id] = [sid]
|
||||
|
||||
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
# print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
await sio.emit("user-count", {"count": len(USER_POOL.items())})
|
||||
|
||||
|
||||
@sio.on("user-count")
|
||||
async def 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)
|
||||
|
||||
return models_in_use
|
||||
|
||||
|
||||
@sio.on("usage")
|
||||
async def usage(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:
|
||||
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]
|
||||
|
||||
# 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
|
||||
await sio.emit("user-count", {"count": len(USER_POOL.items())})
|
||||
|
||||
|
||||
@sio.event
|
||||
@@ -129,14 +177,15 @@ async def disconnect(sid):
|
||||
user_id = SESSION_POOL[sid]
|
||||
del SESSION_POOL[sid]
|
||||
|
||||
USER_POOL[user_id].remove(sid)
|
||||
USER_POOL[user_id] = [_sid for _sid in USER_POOL[user_id] if _sid != sid]
|
||||
|
||||
if len(USER_POOL[user_id]) == 0:
|
||||
del USER_POOL[user_id]
|
||||
|
||||
await sio.emit("user-count", {"count": len(USER_POOL)})
|
||||
else:
|
||||
print(f"Unknown session ID {sid} disconnected")
|
||||
pass
|
||||
# print(f"Unknown session ID {sid} disconnected")
|
||||
|
||||
|
||||
def get_event_emitter(request_info):
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import json
|
||||
import redis
|
||||
|
||||
|
||||
class RedisDict:
|
||||
def __init__(self, name, redis_url):
|
||||
self.name = name
|
||||
self.redis = redis.Redis.from_url(redis_url, decode_responses=True)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
serialized_value = json.dumps(value)
|
||||
self.redis.hset(self.name, key, serialized_value)
|
||||
|
||||
def __getitem__(self, key):
|
||||
value = self.redis.hget(self.name, key)
|
||||
if value is None:
|
||||
raise KeyError(key)
|
||||
return json.loads(value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
result = self.redis.hdel(self.name, key)
|
||||
if result == 0:
|
||||
raise KeyError(key)
|
||||
|
||||
def __contains__(self, key):
|
||||
return self.redis.hexists(self.name, key)
|
||||
|
||||
def __len__(self):
|
||||
return self.redis.hlen(self.name)
|
||||
|
||||
def keys(self):
|
||||
return self.redis.hkeys(self.name)
|
||||
|
||||
def values(self):
|
||||
return [json.loads(v) for v in self.redis.hvals(self.name)]
|
||||
|
||||
def items(self):
|
||||
return [(k, json.loads(v)) for k, v in self.redis.hgetall(self.name).items()]
|
||||
|
||||
def get(self, key, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def clear(self):
|
||||
self.redis.delete(self.name)
|
||||
|
||||
def update(self, other=None, **kwargs):
|
||||
if other is not None:
|
||||
for k, v in other.items() if hasattr(other, "items") else other:
|
||||
self[k] = v
|
||||
for k, v in kwargs.items():
|
||||
self[k] = v
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
if key not in self:
|
||||
self[key] = default
|
||||
return self[key]
|
||||
@@ -188,14 +188,19 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
|
||||
|
||||
@router.post("/signup", response_model=SigninResponse)
|
||||
async def signup(request: Request, response: Response, form_data: SignupForm):
|
||||
if (
|
||||
not request.app.state.config.ENABLE_SIGNUP
|
||||
and request.app.state.config.ENABLE_LOGIN_FORM
|
||||
and WEBUI_AUTH
|
||||
):
|
||||
raise HTTPException(
|
||||
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
||||
)
|
||||
if WEBUI_AUTH:
|
||||
if (
|
||||
not request.app.state.config.ENABLE_SIGNUP
|
||||
or not request.app.state.config.ENABLE_LOGIN_FORM
|
||||
):
|
||||
raise HTTPException(
|
||||
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
||||
)
|
||||
else:
|
||||
if Users.get_num_users() != 0:
|
||||
raise HTTPException(
|
||||
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
||||
)
|
||||
|
||||
if not validate_email_format(form_data.email.lower()):
|
||||
raise HTTPException(
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from open_webui.apps.webui.models.memories import Memories, MemoryModel
|
||||
from open_webui.config import CHROMA_CLIENT
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
from open_webui.apps.rag.vector.connector import VECTOR_DB_CLIENT
|
||||
from open_webui.utils.utils import get_verified_user
|
||||
from open_webui.env import SRC_LOG_LEVELS
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
@@ -49,14 +50,17 @@ async def add_memory(
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
memory = Memories.insert_new_memory(user.id, form_data.content)
|
||||
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
|
||||
|
||||
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
|
||||
collection.upsert(
|
||||
documents=[memory.content],
|
||||
ids=[memory.id],
|
||||
embeddings=[memory_embedding],
|
||||
metadatas=[{"created_at": memory.created_at}],
|
||||
VECTOR_DB_CLIENT.upsert(
|
||||
collection_name=f"user-memory-{user.id}",
|
||||
items=[
|
||||
{
|
||||
"id": memory.id,
|
||||
"text": memory.content,
|
||||
"vector": request.app.state.EMBEDDING_FUNCTION(memory.content),
|
||||
"metadata": {"created_at": memory.created_at},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
return memory
|
||||
@@ -76,12 +80,10 @@ class QueryMemoryForm(BaseModel):
|
||||
async def query_memory(
|
||||
request: Request, form_data: QueryMemoryForm, user=Depends(get_verified_user)
|
||||
):
|
||||
query_embedding = request.app.state.EMBEDDING_FUNCTION(form_data.content)
|
||||
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
|
||||
|
||||
results = collection.query(
|
||||
query_embeddings=[query_embedding],
|
||||
n_results=form_data.k, # how many results to return
|
||||
results = VECTOR_DB_CLIENT.search(
|
||||
collection_name=f"user-memory-{user.id}",
|
||||
vectors=[request.app.state.EMBEDDING_FUNCTION(form_data.content)],
|
||||
limit=form_data.k,
|
||||
)
|
||||
|
||||
return results
|
||||
@@ -94,17 +96,25 @@ async def query_memory(
|
||||
async def reset_memory_from_vector_db(
|
||||
request: Request, user=Depends(get_verified_user)
|
||||
):
|
||||
CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
|
||||
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
|
||||
VECTOR_DB_CLIENT.delete_collection(f"user-memory-{user.id}")
|
||||
|
||||
memories = Memories.get_memories_by_user_id(user.id)
|
||||
for memory in memories:
|
||||
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
|
||||
collection.upsert(
|
||||
documents=[memory.content],
|
||||
ids=[memory.id],
|
||||
embeddings=[memory_embedding],
|
||||
)
|
||||
VECTOR_DB_CLIENT.upsert(
|
||||
collection_name=f"user-memory-{user.id}",
|
||||
items=[
|
||||
{
|
||||
"id": memory.id,
|
||||
"text": memory.content,
|
||||
"vector": request.app.state.EMBEDDING_FUNCTION(memory.content),
|
||||
"metadata": {
|
||||
"created_at": memory.created_at,
|
||||
"updated_at": memory.updated_at,
|
||||
},
|
||||
}
|
||||
for memory in memories
|
||||
],
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -119,7 +129,7 @@ async def delete_memory_by_user_id(user=Depends(get_verified_user)):
|
||||
|
||||
if result:
|
||||
try:
|
||||
CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
|
||||
VECTOR_DB_CLIENT.delete_collection(f"user-memory-{user.id}")
|
||||
except Exception as e:
|
||||
log.error(e)
|
||||
return True
|
||||
@@ -144,16 +154,18 @@ async def update_memory_by_id(
|
||||
raise HTTPException(status_code=404, detail="Memory not found")
|
||||
|
||||
if form_data.content is not None:
|
||||
memory_embedding = request.app.state.EMBEDDING_FUNCTION(form_data.content)
|
||||
collection = CHROMA_CLIENT.get_or_create_collection(
|
||||
name=f"user-memory-{user.id}"
|
||||
)
|
||||
collection.upsert(
|
||||
documents=[form_data.content],
|
||||
ids=[memory.id],
|
||||
embeddings=[memory_embedding],
|
||||
metadatas=[
|
||||
{"created_at": memory.created_at, "updated_at": memory.updated_at}
|
||||
VECTOR_DB_CLIENT.upsert(
|
||||
collection_name=f"user-memory-{user.id}",
|
||||
items=[
|
||||
{
|
||||
"id": memory.id,
|
||||
"text": memory.content,
|
||||
"vector": request.app.state.EMBEDDING_FUNCTION(memory.content),
|
||||
"metadata": {
|
||||
"created_at": memory.created_at,
|
||||
"updated_at": memory.updated_at,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
@@ -170,10 +182,9 @@ async def delete_memory_by_id(memory_id: str, user=Depends(get_verified_user)):
|
||||
result = Memories.delete_memory_by_id_and_user_id(memory_id, user.id)
|
||||
|
||||
if result:
|
||||
collection = CHROMA_CLIENT.get_or_create_collection(
|
||||
name=f"user-memory-{user.id}"
|
||||
VECTOR_DB_CLIENT.delete(
|
||||
collection_name=f"user-memory-{user.id}", ids=[memory_id]
|
||||
)
|
||||
collection.delete(ids=[memory_id])
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -18,8 +18,18 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=list[ModelResponse])
|
||||
async def get_models(user=Depends(get_verified_user)):
|
||||
return Models.get_all_models()
|
||||
async def get_models(id: Optional[str] = None, user=Depends(get_verified_user)):
|
||||
if id:
|
||||
model = Models.get_model_by_id(id)
|
||||
if model:
|
||||
return [model]
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
else:
|
||||
return Models.get_all_models()
|
||||
|
||||
|
||||
############################
|
||||
@@ -50,24 +60,6 @@ async def add_new_model(
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# GetModelById
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=Optional[ModelModel])
|
||||
async def get_model_by_id(id: str, user=Depends(get_verified_user)):
|
||||
model = Models.get_model_by_id(id)
|
||||
|
||||
if model:
|
||||
return model
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
############################
|
||||
# UpdateModelById
|
||||
############################
|
||||
|
||||
@@ -4,7 +4,7 @@ import subprocess
|
||||
import sys
|
||||
from importlib import util
|
||||
import types
|
||||
|
||||
import tempfile
|
||||
|
||||
from open_webui.apps.webui.models.functions import Functions
|
||||
from open_webui.apps.webui.models.tools import Tools
|
||||
@@ -84,7 +84,15 @@ def load_toolkit_module_by_id(toolkit_id, content=None):
|
||||
module = types.ModuleType(module_name)
|
||||
sys.modules[module_name] = module
|
||||
|
||||
# Create a temporary file and use it to define `__file__` so
|
||||
# that it works as expected from the module's perspective.
|
||||
temp_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
temp_file.close()
|
||||
try:
|
||||
with open(temp_file.name, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
module.__dict__["__file__"] = temp_file.name
|
||||
|
||||
# Executing the modified content in the created module's namespace
|
||||
exec(content, module.__dict__)
|
||||
frontmatter = extract_frontmatter(content)
|
||||
@@ -96,9 +104,11 @@ def load_toolkit_module_by_id(toolkit_id, content=None):
|
||||
else:
|
||||
raise Exception("No Tools class found in the module")
|
||||
except Exception as e:
|
||||
print(f"Error loading module: {toolkit_id}")
|
||||
print(f"Error loading module: {toolkit_id}: {e}")
|
||||
del sys.modules[module_name] # Clean up
|
||||
raise e
|
||||
finally:
|
||||
os.unlink(temp_file.name)
|
||||
|
||||
|
||||
def load_function_module_by_id(function_id, content=None):
|
||||
@@ -118,7 +128,15 @@ def load_function_module_by_id(function_id, content=None):
|
||||
module = types.ModuleType(module_name)
|
||||
sys.modules[module_name] = module
|
||||
|
||||
# Create a temporary file and use it to define `__file__` so
|
||||
# that it works as expected from the module's perspective.
|
||||
temp_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
temp_file.close()
|
||||
try:
|
||||
with open(temp_file.name, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
module.__dict__["__file__"] = temp_file.name
|
||||
|
||||
# Execute the modified content in the created module's namespace
|
||||
exec(content, module.__dict__)
|
||||
frontmatter = extract_frontmatter(content)
|
||||
@@ -134,11 +152,13 @@ def load_function_module_by_id(function_id, content=None):
|
||||
else:
|
||||
raise Exception("No Function class found in the module")
|
||||
except Exception as e:
|
||||
print(f"Error loading module: {function_id}")
|
||||
print(f"Error loading module: {function_id}: {e}")
|
||||
del sys.modules[module_name] # Cleanup by removing the module in case of error
|
||||
|
||||
Functions.update_function_by_id(function_id, {"is_active": False})
|
||||
raise e
|
||||
finally:
|
||||
os.unlink(temp_file.name)
|
||||
|
||||
|
||||
def install_frontmatter_requirements(requirements):
|
||||
|
||||
@@ -11,7 +11,6 @@ import chromadb
|
||||
import requests
|
||||
import yaml
|
||||
from open_webui.apps.webui.internal.db import Base, get_db
|
||||
from chromadb import Settings
|
||||
from open_webui.env import (
|
||||
OPEN_WEBUI_DIR,
|
||||
DATA_DIR,
|
||||
@@ -88,6 +87,12 @@ def save_to_db(data):
|
||||
db.commit()
|
||||
|
||||
|
||||
def reset_config():
|
||||
with get_db() as db:
|
||||
db.query(Config).delete()
|
||||
db.commit()
|
||||
|
||||
|
||||
# When initializing, check if config.json exists and migrate it to the database
|
||||
if os.path.exists(f"{DATA_DIR}/config.json"):
|
||||
data = load_json_config()
|
||||
@@ -540,40 +545,6 @@ Path(TOOLS_DIR).mkdir(parents=True, exist_ok=True)
|
||||
FUNCTIONS_DIR = os.getenv("FUNCTIONS_DIR", f"{DATA_DIR}/functions")
|
||||
Path(FUNCTIONS_DIR).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
####################################
|
||||
# LITELLM_CONFIG
|
||||
####################################
|
||||
|
||||
|
||||
def create_config_file(file_path):
|
||||
directory = os.path.dirname(file_path)
|
||||
|
||||
# Check if directory exists, if not, create it
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
# Data to write into the YAML file
|
||||
config_data = {
|
||||
"general_settings": {},
|
||||
"litellm_settings": {},
|
||||
"model_list": [],
|
||||
"router_settings": {},
|
||||
}
|
||||
|
||||
# Write data to YAML file
|
||||
with open(file_path, "w") as file:
|
||||
yaml.dump(config_data, file)
|
||||
|
||||
|
||||
LITELLM_CONFIG_PATH = f"{DATA_DIR}/litellm/config.yaml"
|
||||
|
||||
# if not os.path.exists(LITELLM_CONFIG_PATH):
|
||||
# log.info("Config file doesn't exist. Creating...")
|
||||
# create_config_file(LITELLM_CONFIG_PATH)
|
||||
# log.info("Config file created successfully.")
|
||||
|
||||
|
||||
####################################
|
||||
# OLLAMA_BASE_URL
|
||||
####################################
|
||||
@@ -923,25 +894,12 @@ TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
|
||||
|
||||
|
||||
####################################
|
||||
# RAG document content extraction
|
||||
# Vector Database
|
||||
####################################
|
||||
|
||||
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
|
||||
"CONTENT_EXTRACTION_ENGINE",
|
||||
"rag.CONTENT_EXTRACTION_ENGINE",
|
||||
os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(),
|
||||
)
|
||||
|
||||
TIKA_SERVER_URL = PersistentConfig(
|
||||
"TIKA_SERVER_URL",
|
||||
"rag.tika_server_url",
|
||||
os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment
|
||||
)
|
||||
|
||||
####################################
|
||||
# RAG
|
||||
####################################
|
||||
VECTOR_DB = os.environ.get("VECTOR_DB", "chroma")
|
||||
|
||||
# Chroma
|
||||
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
|
||||
CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
|
||||
CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
|
||||
@@ -958,8 +916,29 @@ else:
|
||||
CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
|
||||
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
|
||||
|
||||
# Milvus
|
||||
|
||||
MILVUS_URI = os.environ.get("MILVUS_URI", f"{DATA_DIR}/vector_db/milvus.db")
|
||||
|
||||
####################################
|
||||
# RAG
|
||||
####################################
|
||||
|
||||
# RAG Content Extraction
|
||||
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
|
||||
"CONTENT_EXTRACTION_ENGINE",
|
||||
"rag.CONTENT_EXTRACTION_ENGINE",
|
||||
os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(),
|
||||
)
|
||||
|
||||
TIKA_SERVER_URL = PersistentConfig(
|
||||
"TIKA_SERVER_URL",
|
||||
"rag.tika_server_url",
|
||||
os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment
|
||||
)
|
||||
|
||||
RAG_TOP_K = PersistentConfig(
|
||||
"RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "5"))
|
||||
"RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "3"))
|
||||
)
|
||||
RAG_RELEVANCE_THRESHOLD = PersistentConfig(
|
||||
"RAG_RELEVANCE_THRESHOLD",
|
||||
@@ -1048,36 +1027,8 @@ RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = (
|
||||
os.environ.get("RAG_RERANKING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
|
||||
)
|
||||
|
||||
|
||||
if CHROMA_HTTP_HOST != "":
|
||||
CHROMA_CLIENT = chromadb.HttpClient(
|
||||
host=CHROMA_HTTP_HOST,
|
||||
port=CHROMA_HTTP_PORT,
|
||||
headers=CHROMA_HTTP_HEADERS,
|
||||
ssl=CHROMA_HTTP_SSL,
|
||||
tenant=CHROMA_TENANT,
|
||||
database=CHROMA_DATABASE,
|
||||
settings=Settings(allow_reset=True, anonymized_telemetry=False),
|
||||
)
|
||||
else:
|
||||
CHROMA_CLIENT = chromadb.PersistentClient(
|
||||
path=CHROMA_DATA_PATH,
|
||||
settings=Settings(allow_reset=True, anonymized_telemetry=False),
|
||||
tenant=CHROMA_TENANT,
|
||||
database=CHROMA_DATABASE,
|
||||
)
|
||||
|
||||
|
||||
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
|
||||
USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
|
||||
|
||||
if USE_CUDA.lower() == "true":
|
||||
DEVICE_TYPE = "cuda"
|
||||
else:
|
||||
DEVICE_TYPE = "cpu"
|
||||
|
||||
CHUNK_SIZE = PersistentConfig(
|
||||
"CHUNK_SIZE", "rag.chunk_size", int(os.environ.get("CHUNK_SIZE", "1500"))
|
||||
"CHUNK_SIZE", "rag.chunk_size", int(os.environ.get("CHUNK_SIZE", "1000"))
|
||||
)
|
||||
CHUNK_OVERLAP = PersistentConfig(
|
||||
"CHUNK_OVERLAP",
|
||||
@@ -1085,19 +1036,25 @@ CHUNK_OVERLAP = PersistentConfig(
|
||||
int(os.environ.get("CHUNK_OVERLAP", "100")),
|
||||
)
|
||||
|
||||
DEFAULT_RAG_TEMPLATE = """Use the following context as your learned knowledge, inside <context></context> XML tags.
|
||||
DEFAULT_RAG_TEMPLATE = """You are given a user query, some textual context and rules, all inside xml tags. You have to answer the query based on the context while respecting the rules.
|
||||
|
||||
<context>
|
||||
[context]
|
||||
[context]
|
||||
</context>
|
||||
|
||||
When answer to user:
|
||||
- If you don't know, just say that you don't know.
|
||||
- If you don't know when you are not sure, ask for clarification.
|
||||
Avoid mentioning that you obtained the information from the context.
|
||||
And answer according to the language of the user's question.
|
||||
<rules>
|
||||
- If you don't know, just say so.
|
||||
- If you are not sure, ask for clarification.
|
||||
- Answer in the same language as the user query.
|
||||
- If the context appears unreadable or of poor quality, tell the user then answer as best as you can.
|
||||
- If the answer is not in the context but you think you know the answer, explain that to the user then answer with your own knowledge.
|
||||
- Answer directly and without using xml tags.
|
||||
</rules>
|
||||
|
||||
Given the context information, answer the query.
|
||||
Query: [query]"""
|
||||
<user_query>
|
||||
[query]
|
||||
</user_query>
|
||||
"""
|
||||
|
||||
RAG_TEMPLATE = PersistentConfig(
|
||||
"RAG_TEMPLATE",
|
||||
@@ -1267,6 +1224,37 @@ AUTOMATIC1111_API_AUTH = PersistentConfig(
|
||||
os.getenv("AUTOMATIC1111_API_AUTH", ""),
|
||||
)
|
||||
|
||||
AUTOMATIC1111_CFG_SCALE = PersistentConfig(
|
||||
"AUTOMATIC1111_CFG_SCALE",
|
||||
"image_generation.automatic1111.cfg_scale",
|
||||
(
|
||||
float(os.environ.get("AUTOMATIC1111_CFG_SCALE"))
|
||||
if os.environ.get("AUTOMATIC1111_CFG_SCALE")
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
AUTOMATIC1111_SAMPLER = PersistentConfig(
|
||||
"AUTOMATIC1111_SAMPLERE",
|
||||
"image_generation.automatic1111.sampler",
|
||||
(
|
||||
os.environ.get("AUTOMATIC1111_SAMPLER")
|
||||
if os.environ.get("AUTOMATIC1111_SAMPLER")
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
AUTOMATIC1111_SCHEDULER = PersistentConfig(
|
||||
"AUTOMATIC1111_SCHEDULER",
|
||||
"image_generation.automatic1111.scheduler",
|
||||
(
|
||||
os.environ.get("AUTOMATIC1111_SCHEDULER")
|
||||
if os.environ.get("AUTOMATIC1111_SCHEDULER")
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
COMFYUI_BASE_URL = PersistentConfig(
|
||||
"COMFYUI_BASE_URL",
|
||||
"image_generation.comfyui.base_url",
|
||||
@@ -1490,3 +1478,17 @@ AUDIO_TTS_SPLIT_ON = PersistentConfig(
|
||||
"audio.tts.split_on",
|
||||
os.getenv("AUDIO_TTS_SPLIT_ON", "punctuation"),
|
||||
)
|
||||
|
||||
AUDIO_TTS_AZURE_SPEECH_REGION = PersistentConfig(
|
||||
"AUDIO_TTS_AZURE_SPEECH_REGION",
|
||||
"audio.tts.azure.speech_region",
|
||||
os.getenv("AUDIO_TTS_AZURE_SPEECH_REGION", "eastus"),
|
||||
)
|
||||
|
||||
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT = PersistentConfig(
|
||||
"AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT",
|
||||
"audio.tts.azure.speech_output_format",
|
||||
os.getenv(
|
||||
"AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT", "audio-24khz-160kbitrate-mono-mp3"
|
||||
),
|
||||
)
|
||||
|
||||
+44
-12
@@ -31,6 +31,28 @@ try:
|
||||
except ImportError:
|
||||
print("dotenv not installed, skipping...")
|
||||
|
||||
DOCKER = os.environ.get("DOCKER", "False").lower() == "true"
|
||||
|
||||
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
|
||||
USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
|
||||
|
||||
if USE_CUDA.lower() == "true":
|
||||
try:
|
||||
import torch
|
||||
|
||||
assert torch.cuda.is_available(), "CUDA not available"
|
||||
DEVICE_TYPE = "cuda"
|
||||
except Exception as e:
|
||||
cuda_error = (
|
||||
"Error when testing CUDA but USE_CUDA_DOCKER is true. "
|
||||
f"Resetting USE_CUDA_DOCKER to false: {e}"
|
||||
)
|
||||
os.environ["USE_CUDA_DOCKER"] = "false"
|
||||
USE_CUDA = "false"
|
||||
DEVICE_TYPE = "cpu"
|
||||
else:
|
||||
DEVICE_TYPE = "cpu"
|
||||
|
||||
|
||||
####################################
|
||||
# LOGGING
|
||||
@@ -47,6 +69,9 @@ else:
|
||||
log = logging.getLogger(__name__)
|
||||
log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
|
||||
|
||||
if "cuda_error" in locals():
|
||||
log.exception(cuda_error)
|
||||
|
||||
log_sources = [
|
||||
"AUDIO",
|
||||
"COMFYUI",
|
||||
@@ -59,6 +84,7 @@ log_sources = [
|
||||
"OPENAI",
|
||||
"RAG",
|
||||
"WEBHOOK",
|
||||
"SOCKET",
|
||||
]
|
||||
|
||||
SRC_LOG_LEVELS = {}
|
||||
@@ -195,6 +221,12 @@ if FROM_INIT_PY:
|
||||
else:
|
||||
shutil.copy2(item, dest)
|
||||
|
||||
# Zip the data directory
|
||||
shutil.make_archive(DATA_DIR.parent / "open_webui_data", "zip", DATA_DIR)
|
||||
|
||||
# Remove the old data directory
|
||||
shutil.rmtree(DATA_DIR)
|
||||
|
||||
DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data"))
|
||||
|
||||
|
||||
@@ -208,18 +240,6 @@ if FROM_INIT_PY:
|
||||
).resolve()
|
||||
|
||||
|
||||
RESET_CONFIG_ON_START = (
|
||||
os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
|
||||
)
|
||||
|
||||
if RESET_CONFIG_ON_START:
|
||||
try:
|
||||
os.remove(f"{DATA_DIR}/config.json")
|
||||
with open(f"{DATA_DIR}/config.json", "w") as f:
|
||||
f.write("{}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
####################################
|
||||
# Database
|
||||
####################################
|
||||
@@ -239,6 +259,10 @@ if "postgres://" in DATABASE_URL:
|
||||
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")
|
||||
|
||||
|
||||
RESET_CONFIG_ON_START = (
|
||||
os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
|
||||
)
|
||||
|
||||
####################################
|
||||
# WEBUI_AUTH (Required for security)
|
||||
####################################
|
||||
@@ -273,3 +297,11 @@ WEBUI_SESSION_COOKIE_SECURE = os.environ.get(
|
||||
|
||||
if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
|
||||
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
|
||||
|
||||
ENABLE_WEBSOCKET_SUPPORT = (
|
||||
os.environ.get("ENABLE_WEBSOCKET_SUPPORT", "True").lower() == "true"
|
||||
)
|
||||
|
||||
WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
|
||||
|
||||
WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", "redis://localhost:6379/0")
|
||||
|
||||
+111
-33
@@ -8,6 +8,8 @@ import shutil
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
import asyncio
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
|
||||
@@ -19,7 +21,9 @@ from open_webui.apps.audio.main import app as audio_app
|
||||
from open_webui.apps.images.main import app as images_app
|
||||
from open_webui.apps.ollama.main import app as ollama_app
|
||||
from open_webui.apps.ollama.main import (
|
||||
generate_openai_chat_completion as generate_ollama_chat_completion,
|
||||
GenerateChatCompletionForm,
|
||||
generate_chat_completion as generate_ollama_chat_completion,
|
||||
generate_openai_chat_completion as generate_ollama_openai_chat_completion,
|
||||
)
|
||||
from open_webui.apps.ollama.main import get_all_models as get_ollama_models
|
||||
from open_webui.apps.openai.main import app as openai_app
|
||||
@@ -29,7 +33,7 @@ from open_webui.apps.openai.main import (
|
||||
from open_webui.apps.openai.main import get_all_models as get_openai_models
|
||||
from open_webui.apps.rag.main import app as rag_app
|
||||
from open_webui.apps.rag.utils import get_rag_context, rag_template
|
||||
from open_webui.apps.socket.main import app as socket_app
|
||||
from open_webui.apps.socket.main import app as socket_app, periodic_usage_pool_cleanup
|
||||
from open_webui.apps.socket.main import get_event_call, get_event_emitter
|
||||
from open_webui.apps.webui.internal.db import Session
|
||||
from open_webui.apps.webui.main import app as webui_app
|
||||
@@ -75,6 +79,7 @@ from open_webui.config import (
|
||||
WEBUI_NAME,
|
||||
AppConfig,
|
||||
run_migrations,
|
||||
reset_config,
|
||||
)
|
||||
from open_webui.constants import ERROR_MESSAGES, TASKS, WEBHOOK_MESSAGES
|
||||
from open_webui.env import (
|
||||
@@ -88,6 +93,7 @@ from open_webui.env import (
|
||||
WEBUI_SESSION_COOKIE_SAME_SITE,
|
||||
WEBUI_SESSION_COOKIE_SECURE,
|
||||
WEBUI_URL,
|
||||
RESET_CONFIG_ON_START,
|
||||
)
|
||||
from fastapi import (
|
||||
Depends,
|
||||
@@ -109,6 +115,7 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from starlette.responses import RedirectResponse, Response, StreamingResponse
|
||||
|
||||
from open_webui.utils.security_headers import SecurityHeadersMiddleware
|
||||
|
||||
from open_webui.utils.misc import (
|
||||
add_or_update_system_message,
|
||||
@@ -134,6 +141,12 @@ from open_webui.utils.utils import (
|
||||
)
|
||||
from open_webui.utils.webhook import post_webhook
|
||||
|
||||
from open_webui.utils.payload import convert_payload_openai_to_ollama
|
||||
from open_webui.utils.response import (
|
||||
convert_response_ollama_to_openai,
|
||||
convert_streaming_response_ollama_to_openai,
|
||||
)
|
||||
|
||||
if SAFE_MODE:
|
||||
print("SAFE MODE ENABLED")
|
||||
Functions.deactivate_all_functions()
|
||||
@@ -175,6 +188,11 @@ https://github.com/open-webui/open-webui
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
run_migrations()
|
||||
|
||||
if RESET_CONFIG_ON_START:
|
||||
reset_config()
|
||||
|
||||
asyncio.create_task(periodic_usage_pool_cleanup())
|
||||
yield
|
||||
|
||||
|
||||
@@ -586,8 +604,17 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
|
||||
if len(contexts) > 0:
|
||||
context_string = "/n".join(contexts).strip()
|
||||
prompt = get_last_user_message(body["messages"])
|
||||
|
||||
if prompt is None:
|
||||
raise Exception("No user message found")
|
||||
if (
|
||||
rag_app.state.config.RELEVANCE_THRESHOLD == 0
|
||||
and context_string.strip() == ""
|
||||
):
|
||||
log.debug(
|
||||
f"With a 0 relevancy threshold for RAG, the context cannot be empty"
|
||||
)
|
||||
|
||||
# Workaround for Ollama 2.0+ system prompt issue
|
||||
# TODO: replace with add_or_update_system_message
|
||||
if model["owned_by"] == "ollama":
|
||||
@@ -780,6 +807,8 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.add_middleware(SecurityHeadersMiddleware)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def commit_session_after_request(request: Request, call_next):
|
||||
@@ -812,8 +841,25 @@ async def update_embedding_function(request: Request, call_next):
|
||||
return response
|
||||
|
||||
|
||||
app.mount("/ws", socket_app)
|
||||
@app.middleware("http")
|
||||
async def inspect_websocket(request: Request, call_next):
|
||||
if (
|
||||
"/ws/socket.io" in request.url.path
|
||||
and request.query_params.get("transport") == "websocket"
|
||||
):
|
||||
upgrade = (request.headers.get("Upgrade") or "").lower()
|
||||
connection = (request.headers.get("Connection") or "").lower().split(",")
|
||||
# Check that there's the correct headers for an upgrade, else reject the connection
|
||||
# This is to work around this upstream issue: https://github.com/miguelgrinberg/python-engineio/issues/367
|
||||
if upgrade != "websocket" or "upgrade" not in connection:
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"detail": "Invalid WebSocket upgrade request"},
|
||||
)
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
app.mount("/ws", socket_app)
|
||||
app.mount("/ollama", ollama_app)
|
||||
app.mount("/openai", openai_app)
|
||||
|
||||
@@ -1018,7 +1064,18 @@ async def generate_chat_completions(form_data: dict, user=Depends(get_verified_u
|
||||
if model.get("pipe"):
|
||||
return await generate_function_chat_completion(form_data, user=user)
|
||||
if model["owned_by"] == "ollama":
|
||||
return await generate_ollama_chat_completion(form_data, user=user)
|
||||
# Using /ollama/api/chat endpoint
|
||||
form_data = convert_payload_openai_to_ollama(form_data)
|
||||
form_data = GenerateChatCompletionForm(**form_data)
|
||||
response = await generate_ollama_chat_completion(form_data=form_data, user=user)
|
||||
if form_data.stream:
|
||||
response.headers["content-type"] = "text/event-stream"
|
||||
return StreamingResponse(
|
||||
convert_streaming_response_ollama_to_openai(response),
|
||||
headers=dict(response.headers),
|
||||
)
|
||||
else:
|
||||
return convert_response_ollama_to_openai(response)
|
||||
else:
|
||||
return await generate_openai_chat_completion(form_data, user=user)
|
||||
|
||||
@@ -1368,9 +1425,10 @@ async def generate_title(form_data: dict, user=Depends(get_verified_user)):
|
||||
|
||||
# Check if the user has a custom task model
|
||||
# If the user has a custom task model, use that model
|
||||
model_id = get_task_model_id(model_id)
|
||||
task_model_id = get_task_model_id(model_id)
|
||||
print(task_model_id)
|
||||
|
||||
print(model_id)
|
||||
model = app.state.MODELS[task_model_id]
|
||||
|
||||
if app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE != "":
|
||||
template = app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE
|
||||
@@ -1397,16 +1455,22 @@ Prompt: {{prompt:middletruncate:8000}}"""
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": model_id,
|
||||
"model": task_model_id,
|
||||
"messages": [{"role": "user", "content": content}],
|
||||
"stream": False,
|
||||
"max_tokens": 50,
|
||||
**(
|
||||
{"max_tokens": 50}
|
||||
if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
|
||||
else {
|
||||
"max_completion_tokens": 50,
|
||||
}
|
||||
),
|
||||
"chat_id": form_data.get("chat_id", None),
|
||||
"metadata": {"task": str(TASKS.TITLE_GENERATION)},
|
||||
}
|
||||
|
||||
log.debug(payload)
|
||||
|
||||
# Handle pipeline filters
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
except Exception as e:
|
||||
@@ -1420,7 +1484,6 @@ Prompt: {{prompt:middletruncate:8000}}"""
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"detail": str(e)},
|
||||
)
|
||||
|
||||
if "chat_id" in payload:
|
||||
del payload["chat_id"]
|
||||
|
||||
@@ -1445,19 +1508,23 @@ async def generate_search_query(form_data: dict, user=Depends(get_verified_user)
|
||||
|
||||
# Check if the user has a custom task model
|
||||
# If the user has a custom task model, use that model
|
||||
model_id = get_task_model_id(model_id)
|
||||
task_model_id = get_task_model_id(model_id)
|
||||
print(task_model_id)
|
||||
|
||||
print(model_id)
|
||||
model = app.state.MODELS[task_model_id]
|
||||
|
||||
if app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE != "":
|
||||
template = app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE
|
||||
else:
|
||||
template = """You are tasked with assessing the need for a web search based on the current question and the context provided by the previous interactions. If the question requires a web search, generate an appropriate query for a Google search and respond with only the query. If the question can be answered without a web search or does not require further information, return an empty string. Today's date is {{CURRENT_DATE}}.
|
||||
template = """Given the user's message and interaction history, decide if a web search is necessary. You must be concise and exclusively provide a search query if one is necessary. Refrain from verbose responses or any additional commentary. Prefer suggesting a search if uncertain to provide comprehensive or updated information. If a search isn't needed at all, respond with an empty string. Default to a search query when in doubt. Today's date is {{CURRENT_DATE}}.
|
||||
|
||||
User Message:
|
||||
{{prompt:end:4000}}
|
||||
|
||||
Interaction History:
|
||||
{{MESSAGES:END:6}}
|
||||
Current Question:
|
||||
{{prompt:end:4000}}"""
|
||||
|
||||
Search Query:"""
|
||||
|
||||
content = search_query_generation_template(
|
||||
template, form_data["messages"], {"name": user.name}
|
||||
@@ -1466,15 +1533,21 @@ Current Question:
|
||||
print("content", content)
|
||||
|
||||
payload = {
|
||||
"model": model_id,
|
||||
"model": task_model_id,
|
||||
"messages": [{"role": "user", "content": content}],
|
||||
"stream": False,
|
||||
"max_tokens": 30,
|
||||
**(
|
||||
{"max_tokens": 30}
|
||||
if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
|
||||
else {
|
||||
"max_completion_tokens": 30,
|
||||
}
|
||||
),
|
||||
"metadata": {"task": str(TASKS.QUERY_GENERATION)},
|
||||
}
|
||||
log.debug(payload)
|
||||
|
||||
print(payload)
|
||||
|
||||
# Handle pipeline filters
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
except Exception as e:
|
||||
@@ -1488,7 +1561,6 @@ Current Question:
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"detail": str(e)},
|
||||
)
|
||||
|
||||
if "chat_id" in payload:
|
||||
del payload["chat_id"]
|
||||
|
||||
@@ -1508,16 +1580,16 @@ async def generate_emoji(form_data: dict, user=Depends(get_verified_user)):
|
||||
|
||||
# Check if the user has a custom task model
|
||||
# If the user has a custom task model, use that model
|
||||
model_id = get_task_model_id(model_id)
|
||||
task_model_id = get_task_model_id(model_id)
|
||||
print(task_model_id)
|
||||
|
||||
print(model_id)
|
||||
model = app.state.MODELS[task_model_id]
|
||||
|
||||
template = '''
|
||||
Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱).
|
||||
|
||||
Message: """{{prompt}}"""
|
||||
'''
|
||||
|
||||
content = title_generation_template(
|
||||
template,
|
||||
form_data["prompt"],
|
||||
@@ -1528,16 +1600,22 @@ Message: """{{prompt}}"""
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": model_id,
|
||||
"model": task_model_id,
|
||||
"messages": [{"role": "user", "content": content}],
|
||||
"stream": False,
|
||||
"max_tokens": 4,
|
||||
**(
|
||||
{"max_tokens": 4}
|
||||
if app.state.MODELS[task_model_id]["owned_by"] == "ollama"
|
||||
else {
|
||||
"max_completion_tokens": 4,
|
||||
}
|
||||
),
|
||||
"chat_id": form_data.get("chat_id", None),
|
||||
"metadata": {"task": str(TASKS.EMOJI_GENERATION)},
|
||||
}
|
||||
|
||||
log.debug(payload)
|
||||
|
||||
# Handle pipeline filters
|
||||
try:
|
||||
payload = filter_pipeline(payload, user)
|
||||
except Exception as e:
|
||||
@@ -1551,7 +1629,6 @@ Message: """{{prompt}}"""
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"detail": str(e)},
|
||||
)
|
||||
|
||||
if "chat_id" in payload:
|
||||
del payload["chat_id"]
|
||||
|
||||
@@ -1571,8 +1648,10 @@ async def generate_moa_response(form_data: dict, user=Depends(get_verified_user)
|
||||
|
||||
# Check if the user has a custom task model
|
||||
# If the user has a custom task model, use that model
|
||||
model_id = get_task_model_id(model_id)
|
||||
print(model_id)
|
||||
task_model_id = get_task_model_id(model_id)
|
||||
print(task_model_id)
|
||||
|
||||
model = app.state.MODELS[task_model_id]
|
||||
|
||||
template = """You have been provided with a set of responses from various models to the latest user query: "{{prompt}}"
|
||||
|
||||
@@ -1587,13 +1666,12 @@ Responses from models: {{responses}}"""
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": model_id,
|
||||
"model": task_model_id,
|
||||
"messages": [{"role": "user", "content": content}],
|
||||
"stream": form_data.get("stream", False),
|
||||
"chat_id": form_data.get("chat_id", None),
|
||||
"metadata": {"task": str(TASKS.MOA_RESPONSE_GENERATION)},
|
||||
}
|
||||
|
||||
log.debug(payload)
|
||||
|
||||
try:
|
||||
@@ -1609,7 +1687,6 @@ Responses from models: {{responses}}"""
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
content={"detail": str(e)},
|
||||
)
|
||||
|
||||
if "chat_id" in payload:
|
||||
del payload["chat_id"]
|
||||
|
||||
@@ -2260,10 +2337,11 @@ async def get_manifest_json():
|
||||
return {
|
||||
"name": WEBUI_NAME,
|
||||
"short_name": WEBUI_NAME,
|
||||
"description": "Open WebUI is an open, extensible, user-friendly interface for AI that adapts to your workflow.",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#343541",
|
||||
"orientation": "portrait-primary",
|
||||
"orientation": "any",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/logo.png",
|
||||
|
||||
@@ -105,17 +105,25 @@ def openai_chat_message_template(model: str):
|
||||
}
|
||||
|
||||
|
||||
def openai_chat_chunk_message_template(model: str, message: str) -> dict:
|
||||
def openai_chat_chunk_message_template(
|
||||
model: str, message: Optional[str] = None
|
||||
) -> dict:
|
||||
template = openai_chat_message_template(model)
|
||||
template["object"] = "chat.completion.chunk"
|
||||
template["choices"][0]["delta"] = {"content": message}
|
||||
if message:
|
||||
template["choices"][0]["delta"] = {"content": message}
|
||||
else:
|
||||
template["choices"][0]["finish_reason"] = "stop"
|
||||
return template
|
||||
|
||||
|
||||
def openai_chat_completion_message_template(model: str, message: str) -> dict:
|
||||
def openai_chat_completion_message_template(
|
||||
model: str, message: Optional[str] = None
|
||||
) -> dict:
|
||||
template = openai_chat_message_template(model)
|
||||
template["object"] = "chat.completion"
|
||||
template["choices"][0]["message"] = {"content": message, "role": "assistant"}
|
||||
if message:
|
||||
template["choices"][0]["message"] = {"content": message, "role": "assistant"}
|
||||
template["choices"][0]["finish_reason"] = "stop"
|
||||
return template
|
||||
|
||||
|
||||
@@ -44,9 +44,9 @@ def apply_model_params_to_body(
|
||||
def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict:
|
||||
mappings = {
|
||||
"temperature": float,
|
||||
"top_p": int,
|
||||
"top_p": float,
|
||||
"max_tokens": int,
|
||||
"frequency_penalty": int,
|
||||
"frequency_penalty": float,
|
||||
"seed": lambda x: x,
|
||||
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
|
||||
}
|
||||
@@ -86,3 +86,49 @@ def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict:
|
||||
form_data[value] = param
|
||||
|
||||
return form_data
|
||||
|
||||
|
||||
def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
|
||||
"""
|
||||
Converts a payload formatted for OpenAI's API to be compatible with Ollama's API endpoint for chat completions.
|
||||
|
||||
Args:
|
||||
openai_payload (dict): The payload originally designed for OpenAI API usage.
|
||||
|
||||
Returns:
|
||||
dict: A modified payload compatible with the Ollama API.
|
||||
"""
|
||||
ollama_payload = {}
|
||||
|
||||
# Mapping basic model and message details
|
||||
ollama_payload["model"] = openai_payload.get("model")
|
||||
ollama_payload["messages"] = openai_payload.get("messages")
|
||||
ollama_payload["stream"] = openai_payload.get("stream", False)
|
||||
|
||||
# If there are advanced parameters in the payload, format them in Ollama's options field
|
||||
ollama_options = {}
|
||||
|
||||
# Handle parameters which map directly
|
||||
for param in ["temperature", "top_p", "seed"]:
|
||||
if param in openai_payload:
|
||||
ollama_options[param] = openai_payload[param]
|
||||
|
||||
# Mapping OpenAI's `max_tokens` -> Ollama's `num_predict`
|
||||
if "max_completion_tokens" in openai_payload:
|
||||
ollama_options["num_predict"] = openai_payload["max_completion_tokens"]
|
||||
elif "max_tokens" in openai_payload:
|
||||
ollama_options["num_predict"] = openai_payload["max_tokens"]
|
||||
|
||||
# Handle frequency / presence_penalty, which needs renaming and checking
|
||||
if "frequency_penalty" in openai_payload:
|
||||
ollama_options["repeat_penalty"] = openai_payload["frequency_penalty"]
|
||||
|
||||
if "presence_penalty" in openai_payload and "penalty" not in ollama_options:
|
||||
# We are assuming presence penalty uses a similar concept in Ollama, which needs custom handling if exists.
|
||||
ollama_options["new_topic_penalty"] = openai_payload["presence_penalty"]
|
||||
|
||||
# Add options to payload if any have been set
|
||||
if ollama_options:
|
||||
ollama_payload["options"] = ollama_options
|
||||
|
||||
return ollama_payload
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import json
|
||||
from open_webui.utils.misc import (
|
||||
openai_chat_chunk_message_template,
|
||||
openai_chat_completion_message_template,
|
||||
)
|
||||
|
||||
|
||||
def convert_response_ollama_to_openai(ollama_response: dict) -> dict:
|
||||
model = ollama_response.get("model", "ollama")
|
||||
message_content = ollama_response.get("message", {}).get("content", "")
|
||||
|
||||
response = openai_chat_completion_message_template(model, message_content)
|
||||
return response
|
||||
|
||||
|
||||
async def convert_streaming_response_ollama_to_openai(ollama_streaming_response):
|
||||
async for data in ollama_streaming_response.body_iterator:
|
||||
data = json.loads(data)
|
||||
|
||||
model = data.get("model", "ollama")
|
||||
message_content = data.get("message", {}).get("content", "")
|
||||
done = data.get("done", False)
|
||||
|
||||
data = openai_chat_chunk_message_template(
|
||||
model, message_content if not done else None
|
||||
)
|
||||
|
||||
line = f"data: {json.dumps(data)}\n\n"
|
||||
if done:
|
||||
line += "data: [DONE]\n\n"
|
||||
|
||||
yield line
|
||||
@@ -0,0 +1,115 @@
|
||||
import re
|
||||
import os
|
||||
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
response = await call_next(request)
|
||||
response.headers.update(set_security_headers())
|
||||
return response
|
||||
|
||||
|
||||
def set_security_headers() -> Dict[str, str]:
|
||||
"""
|
||||
Sets security headers based on environment variables.
|
||||
|
||||
This function reads specific environment variables and uses their values
|
||||
to set corresponding security headers. The headers that can be set are:
|
||||
- cache-control
|
||||
- strict-transport-security
|
||||
- referrer-policy
|
||||
- x-content-type-options
|
||||
- x-download-options
|
||||
- x-frame-options
|
||||
- x-permitted-cross-domain-policies
|
||||
|
||||
Each environment variable is associated with a specific setter function
|
||||
that constructs the header. If the environment variable is set, the
|
||||
corresponding header is added to the options dictionary.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the security headers and their values.
|
||||
"""
|
||||
options = {}
|
||||
header_setters = {
|
||||
"CACHE_CONTROL": set_cache_control,
|
||||
"HSTS": set_hsts,
|
||||
"REFERRER_POLICY": set_referrer,
|
||||
"XCONTENT_TYPE": set_xcontent_type,
|
||||
"XDOWNLOAD_OPTIONS": set_xdownload_options,
|
||||
"XFRAME_OPTIONS": set_xframe,
|
||||
"XPERMITTED_CROSS_DOMAIN_POLICIES": set_xpermitted_cross_domain_policies,
|
||||
}
|
||||
|
||||
for env_var, setter in header_setters.items():
|
||||
value = os.environ.get(env_var, None)
|
||||
if value:
|
||||
header = setter(value)
|
||||
if header:
|
||||
options.update(header)
|
||||
|
||||
return options
|
||||
|
||||
|
||||
# Set HTTP Strict Transport Security(HSTS) response header
|
||||
def set_hsts(value: str):
|
||||
pattern = r"^max-age=(\d+)(;includeSubDomains)?(;preload)?$"
|
||||
match = re.match(pattern, value, re.IGNORECASE)
|
||||
if not match:
|
||||
return "max-age=31536000;includeSubDomains"
|
||||
return {"Strict-Transport-Security": value}
|
||||
|
||||
|
||||
# Set X-Frame-Options response header
|
||||
def set_xframe(value: str):
|
||||
pattern = r"^(DENY|SAMEORIGIN)$"
|
||||
match = re.match(pattern, value, re.IGNORECASE)
|
||||
if not match:
|
||||
value = "DENY"
|
||||
return {"X-Frame-Options": value}
|
||||
|
||||
|
||||
# Set Referrer-Policy response header
|
||||
def set_referrer(value: str):
|
||||
pattern = r"^(no-referrer|no-referrer-when-downgrade|origin|origin-when-cross-origin|same-origin|strict-origin|strict-origin-when-cross-origin|unsafe-url)$"
|
||||
match = re.match(pattern, value, re.IGNORECASE)
|
||||
if not match:
|
||||
value = "no-referrer"
|
||||
return {"Referrer-Policy": value}
|
||||
|
||||
|
||||
# Set Cache-Control response header
|
||||
def set_cache_control(value: str):
|
||||
pattern = r"^(public|private|no-cache|no-store|must-revalidate|proxy-revalidate|max-age=\d+|s-maxage=\d+|no-transform|immutable)(,\s*(public|private|no-cache|no-store|must-revalidate|proxy-revalidate|max-age=\d+|s-maxage=\d+|no-transform|immutable))*$"
|
||||
match = re.match(pattern, value, re.IGNORECASE)
|
||||
if not match:
|
||||
value = "no-store, max-age=0"
|
||||
|
||||
return {"Cache-Control": value}
|
||||
|
||||
|
||||
# Set X-Download-Options response header
|
||||
def set_xdownload_options(value: str):
|
||||
if value != "noopen":
|
||||
value = "noopen"
|
||||
return {"X-Download-Options": value}
|
||||
|
||||
|
||||
# Set X-Content-Type-Options response header
|
||||
def set_xcontent_type(value: str):
|
||||
if value != "nosniff":
|
||||
value = "nosniff"
|
||||
return {"X-Content-Type-Options": value}
|
||||
|
||||
|
||||
# Set X-Permitted-Cross-Domain-Policies response header
|
||||
def set_xpermitted_cross_domain_policies(value: str):
|
||||
pattern = r"^(none|master-only|by-content-type|by-ftp-filename)$"
|
||||
match = re.match(pattern, value, re.IGNORECASE)
|
||||
if not match:
|
||||
value = "none"
|
||||
return {"X-Permitted-Cross-Domain-Policies": value}
|
||||
@@ -40,7 +40,12 @@ langchain-chroma==0.1.2
|
||||
|
||||
fake-useragent==1.5.1
|
||||
chromadb==0.5.5
|
||||
pymilvus==2.4.6
|
||||
|
||||
sentence-transformers==3.0.1
|
||||
colbert-ai==0.2.21
|
||||
einops==0.8.0
|
||||
|
||||
pypdf==4.3.1
|
||||
docx2txt==0.8
|
||||
python-pptx==1.0.0
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
resources:
|
||||
- open-webui.yaml
|
||||
- ollama-service.yaml
|
||||
- ollama-statefulset.yaml
|
||||
- webui-deployment.yaml
|
||||
- webui-service.yaml
|
||||
- webui-ingress.yaml
|
||||
- webui-pvc.yaml
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../base
|
||||
|
||||
patches:
|
||||
- path: ollama-statefulset-gpu.yaml
|
||||
@@ -1,13 +0,0 @@
|
||||
resources:
|
||||
- base/open-webui.yaml
|
||||
- base/ollama-service.yaml
|
||||
- base/ollama-statefulset.yaml
|
||||
- base/webui-deployment.yaml
|
||||
- base/webui-service.yaml
|
||||
- base/webui-ingress.yaml
|
||||
- base/webui-pvc.yaml
|
||||
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
patches:
|
||||
- path: patches/ollama-statefulset-gpu.yaml
|
||||
Generated
+187
-120
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.3.20",
|
||||
"version": "0.3.30",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "open-webui",
|
||||
"version": "0.3.20",
|
||||
"version": "0.3.30",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lang-python": "^6.1.6",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@pyscript/core": "^0.4.32",
|
||||
"@sveltejs/adapter-node": "^2.0.0",
|
||||
"@xyflow/svelte": "^0.1.19",
|
||||
"async": "^3.2.5",
|
||||
"bits-ui": "^0.19.7",
|
||||
"codemirror": "^6.0.1",
|
||||
@@ -31,6 +32,7 @@
|
||||
"katex": "^0.16.9",
|
||||
"marked": "^9.1.0",
|
||||
"mermaid": "^10.9.1",
|
||||
"paneforge": "^0.0.6",
|
||||
"pyodide": "^0.26.1",
|
||||
"socket.io-client": "^4.2.0",
|
||||
"sortablejs": "^1.15.2",
|
||||
@@ -45,7 +47,6 @@
|
||||
"@sveltejs/kit": "^2.5.20",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/bun": "latest",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.17.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
@@ -1166,9 +1167,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz",
|
||||
"integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz",
|
||||
"integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1178,9 +1179,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz",
|
||||
"integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz",
|
||||
"integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1190,9 +1191,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz",
|
||||
"integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz",
|
||||
"integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1202,9 +1203,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz",
|
||||
"integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz",
|
||||
"integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1214,9 +1215,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz",
|
||||
"integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz",
|
||||
"integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1226,9 +1227,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz",
|
||||
"integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz",
|
||||
"integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1238,9 +1239,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz",
|
||||
"integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1250,9 +1251,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz",
|
||||
"integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz",
|
||||
"integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1262,9 +1263,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz",
|
||||
"integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1274,9 +1275,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz",
|
||||
"integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1286,9 +1287,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz",
|
||||
"integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1298,9 +1299,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz",
|
||||
"integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz",
|
||||
"integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1310,9 +1311,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz",
|
||||
"integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz",
|
||||
"integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1322,9 +1323,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz",
|
||||
"integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz",
|
||||
"integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1334,9 +1335,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz",
|
||||
"integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz",
|
||||
"integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1346,9 +1347,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz",
|
||||
"integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz",
|
||||
"integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1368,6 +1369,14 @@
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
||||
},
|
||||
"node_modules/@svelte-put/shortcut": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@svelte-put/shortcut/-/shortcut-3.1.1.tgz",
|
||||
"integrity": "sha512-2L5EYTZXiaKvbEelVkg5znxqvfZGZai3m97+cAiUBhLZwXnGtviTDpHxOoZBsqz41szlfRMcamW/8o0+fbW3ZQ==",
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.55.0 || ^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/adapter-auto": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.2.2.tgz",
|
||||
@@ -1494,20 +1503,32 @@
|
||||
"tailwindcss": ">=3.0.0 || insiders"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bun": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.0.10.tgz",
|
||||
"integrity": "sha512-Jaz6YYAdm1u3NVlgSyEK+qGmrlLQ20sbWeEoXD64b9w6z/YKYNWlfaphu+xF2Kiy5Tpykm5Q9jIquLegwXx4ng==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bun-types": "1.0.33"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
|
||||
},
|
||||
"node_modules/@types/d3-drag": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz",
|
||||
@@ -1521,11 +1542,33 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz",
|
||||
"integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw=="
|
||||
},
|
||||
"node_modules/@types/d3-selection": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz",
|
||||
"integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg=="
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz",
|
||||
"integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw=="
|
||||
},
|
||||
"node_modules/@types/d3-transition": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz",
|
||||
"integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-zoom": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||
"dependencies": {
|
||||
"@types/d3-interpolate": "*",
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||
@@ -1568,7 +1611,7 @@
|
||||
"version": "20.11.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
|
||||
"integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
|
||||
"devOptional": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
@@ -1613,15 +1656,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
|
||||
"integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
|
||||
"integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
@@ -1942,6 +1976,33 @@
|
||||
"resolved": "https://registry.npmjs.org/@webreflection/fetch/-/fetch-0.1.5.tgz",
|
||||
"integrity": "sha512-zCcqCJoNLvdeF41asAK71XPlwSPieeRDsE09albBunJEksuYPYNillKNQjf8p5BqSoTKTuKrW3lUm3MNodUC4g=="
|
||||
},
|
||||
"node_modules/@xyflow/svelte": {
|
||||
"version": "0.1.19",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/svelte/-/svelte-0.1.19.tgz",
|
||||
"integrity": "sha512-yW5w5aI+Yqkob4kLQpVDo/ZmX+E9Pw7459kqwLfv4YG4N1NYXrsDRh9cyph/rapbuDnPi6zqK5E8LKrgaCQC0w==",
|
||||
"dependencies": {
|
||||
"@svelte-put/shortcut": "^3.1.0",
|
||||
"@xyflow/system": "0.0.42",
|
||||
"classcat": "^5.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/system": {
|
||||
"version": "0.0.42",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.42.tgz",
|
||||
"integrity": "sha512-kWYj+Y0GOct0jKYTdyRMNOLPxGNbb2TYvPg2gTmJnZ31DOOMkL5uRBLX825DR2gOACDu+i5FHLxPJUPf/eGOJw==",
|
||||
"dependencies": {
|
||||
"@types/d3-drag": "^3.0.7",
|
||||
"@types/d3-selection": "^3.0.10",
|
||||
"@types/d3-transition": "^3.0.8",
|
||||
"@types/d3-zoom": "^3.0.8",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.11.3",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
||||
@@ -2533,16 +2594,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bun-types": {
|
||||
"version": "1.0.33",
|
||||
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.0.33.tgz",
|
||||
"integrity": "sha512-L5tBIf9g6rBBkvshqysi5NoLQ9NnhSPU1pfJ9FzqoSfofYdyac3WLUnOIuQ+M5za/sooVUOP2ko+E6Tco0OLIA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "~20.11.3",
|
||||
"@types/ws": "~8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
@@ -2777,6 +2828,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/classcat": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
|
||||
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="
|
||||
},
|
||||
"node_modules/clean-stack": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||
@@ -6931,6 +6987,17 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/paneforge": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/paneforge/-/paneforge-0.0.6.tgz",
|
||||
"integrity": "sha512-jYeN/wdREihja5c6nK3S5jritDQ+EbCqC5NrDo97qCZzZ9GkmEcN5C0ZCjF4nmhBwkDKr6tLIgz4QUKWxLXjAw==",
|
||||
"dependencies": {
|
||||
"nanoid": "^5.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^4.0.0 || ^5.0.0-next.1"
|
||||
}
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -7094,9 +7161,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
@@ -7162,9 +7229,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.41",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
|
||||
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
|
||||
"version": "8.4.47",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
|
||||
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -7181,8 +7248,8 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.1",
|
||||
"source-map-js": "^1.2.0"
|
||||
"picocolors": "^1.1.0",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
@@ -7839,9 +7906,9 @@
|
||||
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz",
|
||||
"integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz",
|
||||
"integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.5"
|
||||
},
|
||||
@@ -7853,22 +7920,22 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.20.0",
|
||||
"@rollup/rollup-android-arm64": "4.20.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.20.0",
|
||||
"@rollup/rollup-darwin-x64": "4.20.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.20.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.20.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.20.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.20.0",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.20.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.20.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.20.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.20.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.20.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.20.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.20.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.20.0",
|
||||
"@rollup/rollup-android-arm-eabi": "4.22.4",
|
||||
"@rollup/rollup-android-arm64": "4.22.4",
|
||||
"@rollup/rollup-darwin-arm64": "4.22.4",
|
||||
"@rollup/rollup-darwin-x64": "4.22.4",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.22.4",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.22.4",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.22.4",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.22.4",
|
||||
"@rollup/rollup-linux-x64-musl": "4.22.4",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.22.4",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.22.4",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.22.4",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
@@ -8195,9 +8262,9 @@
|
||||
"integrity": "sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA=="
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -8657,11 +8724,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-sonner": {
|
||||
"version": "0.3.19",
|
||||
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.19.tgz",
|
||||
"integrity": "sha512-jpPOgLtHwRaB6Vqo2dUQMv15/yUV/BQWTjKpEqQ11uqRSHKjAYUKZyGrHB2cQsGmyjR0JUzBD58btpgNqINQ/Q==",
|
||||
"version": "0.3.28",
|
||||
"resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.28.tgz",
|
||||
"integrity": "sha512-K3AmlySeFifF/cKgsYNv5uXqMVNln0NBAacOYgmkQStLa/UoU0LhfAACU6Gr+YYC8bOCHdVmFNoKuDbMEsppJg==",
|
||||
"peerDependencies": {
|
||||
"svelte": ">=3 <5"
|
||||
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte/node_modules/estree-walker": {
|
||||
@@ -9112,7 +9179,7 @@
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"devOptional": true
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/unist-util-stringify-position": {
|
||||
"version": "3.0.3",
|
||||
@@ -9329,13 +9396,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz",
|
||||
"integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==",
|
||||
"version": "5.4.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
|
||||
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.40",
|
||||
"rollup": "^4.13.0"
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.3.20",
|
||||
"version": "0.3.30",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||
@@ -25,7 +25,6 @@
|
||||
"@sveltejs/kit": "^2.5.20",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/bun": "latest",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.17.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
@@ -54,6 +53,7 @@
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@pyscript/core": "^0.4.32",
|
||||
"@sveltejs/adapter-node": "^2.0.0",
|
||||
"@xyflow/svelte": "^0.1.19",
|
||||
"async": "^3.2.5",
|
||||
"bits-ui": "^0.19.7",
|
||||
"codemirror": "^6.0.1",
|
||||
@@ -72,6 +72,7 @@
|
||||
"katex": "^0.16.9",
|
||||
"marked": "^9.1.0",
|
||||
"mermaid": "^10.9.1",
|
||||
"paneforge": "^0.0.6",
|
||||
"pyodide": "^0.26.1",
|
||||
"socket.io-client": "^4.2.0",
|
||||
"sortablejs": "^1.15.2",
|
||||
|
||||
@@ -47,7 +47,12 @@ dependencies = [
|
||||
|
||||
"fake-useragent==1.5.1",
|
||||
"chromadb==0.5.5",
|
||||
"pymilvus==2.4.6",
|
||||
|
||||
"sentence-transformers==3.0.1",
|
||||
"colbert-ai==0.2.21",
|
||||
"einops==0.8.0",
|
||||
|
||||
"pypdf==4.3.1",
|
||||
"docx2txt==0.8",
|
||||
"python-pptx==1.0.0",
|
||||
|
||||
+17
-15
@@ -50,21 +50,6 @@ iframe {
|
||||
@apply rounded-lg;
|
||||
}
|
||||
|
||||
ol > li {
|
||||
counter-increment: list-number;
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.prose ol > li::before {
|
||||
content: counters(list-number, '.') '.';
|
||||
padding-right: 0.5rem;
|
||||
color: var(--tw-prose-counters);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
li p {
|
||||
display: inline;
|
||||
}
|
||||
@@ -171,3 +156,20 @@ input[type='number'] {
|
||||
font-weight: 600;
|
||||
@apply rounded-md dark:bg-gray-800 bg-gray-100 mx-0.5;
|
||||
}
|
||||
|
||||
.svelte-flow {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.svelte-flow__edge > path {
|
||||
stroke-width: 0.5;
|
||||
}
|
||||
|
||||
.svelte-flow__edge.animated > path {
|
||||
stroke-width: 2;
|
||||
@apply stroke-gray-600 dark:stroke-gray-500;
|
||||
}
|
||||
|
||||
.bg-gray-950-90 {
|
||||
background-color: rgba(var(--color-gray-950, #0d0d0d), 0.9);
|
||||
}
|
||||
|
||||
+20
-14
@@ -4,7 +4,11 @@
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="manifest" href="%sveltekit.assets%/manifest.json" crossorigin="use-credentials" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"
|
||||
/>
|
||||
<meta name="theme-color" content="#171717" />
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
<meta name="description" content="Open WebUI" />
|
||||
<link
|
||||
@@ -23,33 +27,33 @@
|
||||
<script>
|
||||
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
|
||||
(() => {
|
||||
const metaThemeColorTag = document.querySelector('meta[name="theme-color"]');
|
||||
const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
if (!localStorage?.theme) {
|
||||
localStorage.theme = 'system';
|
||||
}
|
||||
|
||||
if (localStorage?.theme && localStorage?.theme.includes('oled')) {
|
||||
if (localStorage.theme === 'system') {
|
||||
document.documentElement.classList.add(prefersDarkTheme ? 'dark' : 'light');
|
||||
metaThemeColorTag.setAttribute('content', prefersDarkTheme ? '#171717' : '#ffffff');
|
||||
} else if (localStorage.theme === 'oled-dark') {
|
||||
document.documentElement.style.setProperty('--color-gray-800', '#101010');
|
||||
document.documentElement.style.setProperty('--color-gray-850', '#050505');
|
||||
document.documentElement.style.setProperty('--color-gray-900', '#000000');
|
||||
document.documentElement.style.setProperty('--color-gray-950', '#000000');
|
||||
document.documentElement.classList.add('dark');
|
||||
} else if (
|
||||
localStorage.theme === 'light' ||
|
||||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches)
|
||||
) {
|
||||
metaThemeColorTag.setAttribute('content', '#000000');
|
||||
} else if (localStorage.theme === 'light') {
|
||||
document.documentElement.classList.add('light');
|
||||
} else if (localStorage.theme && localStorage.theme !== 'system') {
|
||||
localStorage.theme.split(' ').forEach((e) => {
|
||||
document.documentElement.classList.add(e);
|
||||
});
|
||||
} else if (localStorage.theme && localStorage.theme === 'system') {
|
||||
systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
document.documentElement.classList.add(systemTheme ? 'dark' : 'light');
|
||||
} else if (localStorage.theme && localStorage.theme === 'her') {
|
||||
metaThemeColorTag.setAttribute('content', '#ffffff');
|
||||
} else if (localStorage.theme === 'her') {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.classList.add('her');
|
||||
metaThemeColorTag.setAttribute('content', '#983724');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
metaThemeColorTag.setAttribute('content', '#171717');
|
||||
}
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addListener((e) => {
|
||||
@@ -57,9 +61,11 @@
|
||||
if (e.matches) {
|
||||
document.documentElement.classList.add('dark');
|
||||
document.documentElement.classList.remove('light');
|
||||
metaThemeColorTag.setAttribute('content', '#171717');
|
||||
} else {
|
||||
document.documentElement.classList.add('light');
|
||||
document.documentElement.classList.remove('dark');
|
||||
metaThemeColorTag.setAttribute('content', '#ffffff');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
let TTS_MODEL = '';
|
||||
let TTS_VOICE = '';
|
||||
let TTS_SPLIT_ON: TTS_RESPONSE_SPLIT = TTS_RESPONSE_SPLIT.PUNCTUATION;
|
||||
let TTS_AZURE_SPEECH_REGION = '';
|
||||
let TTS_AZURE_SPEECH_OUTPUT_FORMAT = '';
|
||||
|
||||
let STT_OPENAI_API_BASE_URL = '';
|
||||
let STT_OPENAI_API_KEY = '';
|
||||
@@ -87,7 +89,9 @@
|
||||
ENGINE: TTS_ENGINE,
|
||||
MODEL: TTS_MODEL,
|
||||
VOICE: TTS_VOICE,
|
||||
SPLIT_ON: TTS_SPLIT_ON
|
||||
SPLIT_ON: TTS_SPLIT_ON,
|
||||
AZURE_SPEECH_REGION: TTS_AZURE_SPEECH_REGION,
|
||||
AZURE_SPEECH_OUTPUT_FORMAT: TTS_AZURE_SPEECH_OUTPUT_FORMAT
|
||||
},
|
||||
stt: {
|
||||
OPENAI_API_BASE_URL: STT_OPENAI_API_BASE_URL,
|
||||
@@ -120,6 +124,9 @@
|
||||
|
||||
TTS_SPLIT_ON = res.tts.SPLIT_ON || TTS_RESPONSE_SPLIT.PUNCTUATION;
|
||||
|
||||
TTS_AZURE_SPEECH_OUTPUT_FORMAT = res.tts.AZURE_SPEECH_OUTPUT_FORMAT;
|
||||
TTS_AZURE_SPEECH_REGION = res.tts.AZURE_SPEECH_REGION;
|
||||
|
||||
STT_OPENAI_API_BASE_URL = res.stt.OPENAI_API_BASE_URL;
|
||||
STT_OPENAI_API_KEY = res.stt.OPENAI_API_KEY;
|
||||
|
||||
@@ -224,6 +231,7 @@
|
||||
<option value="">{$i18n.t('Web API')}</option>
|
||||
<option value="openai">{$i18n.t('OpenAI')}</option>
|
||||
<option value="elevenlabs">{$i18n.t('ElevenLabs')}</option>
|
||||
<option value="azure">{$i18n.t('Azure AI Speech')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,6 +260,23 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if TTS_ENGINE === 'azure'}
|
||||
<div>
|
||||
<div class="mt-1 flex gap-2 mb-1">
|
||||
<input
|
||||
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={TTS_API_KEY}
|
||||
required
|
||||
/>
|
||||
<input
|
||||
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Azure Region')}
|
||||
bind:value={TTS_AZURE_SPEECH_REGION}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
@@ -359,6 +384,49 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if TTS_ENGINE === 'azure'}
|
||||
<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 bg-gray-50 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.id}>{voice.name}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class=" mb-1.5 text-sm font-medium">
|
||||
{$i18n.t('Output format')}
|
||||
<a
|
||||
href="https://learn.microsoft.com/en-us/azure/ai-services/speech-service/rest-text-to-speech?tabs=streaming#audio-outputs"
|
||||
target="_blank"
|
||||
>
|
||||
<small>{$i18n.t('Available list')}</small>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="tts-model-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={TTS_AZURE_SPEECH_OUTPUT_FORMAT}
|
||||
placeholder="Select a output format"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class="dark:border-gray-850 my-2" />
|
||||
|
||||
@@ -150,18 +150,20 @@
|
||||
})()
|
||||
]);
|
||||
|
||||
OPENAI_API_BASE_URLS.forEach(async (url, idx) => {
|
||||
const res = await getOpenAIModels(localStorage.token, idx);
|
||||
if (res.pipelines) {
|
||||
pipelineUrls[url] = true;
|
||||
}
|
||||
});
|
||||
|
||||
const ollamaConfig = await getOllamaConfig(localStorage.token);
|
||||
const openaiConfig = await getOpenAIConfig(localStorage.token);
|
||||
|
||||
ENABLE_OPENAI_API = openaiConfig.ENABLE_OPENAI_API;
|
||||
ENABLE_OLLAMA_API = ollamaConfig.ENABLE_OLLAMA_API;
|
||||
|
||||
if (ENABLE_OPENAI_API) {
|
||||
OPENAI_API_BASE_URLS.forEach(async (url, idx) => {
|
||||
const res = await getOpenAIModels(localStorage.token, idx);
|
||||
if (res.pipelines) {
|
||||
pipelineUrls[url] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -732,11 +732,17 @@
|
||||
|
||||
<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 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="4"
|
||||
/>
|
||||
<Tooltip
|
||||
content={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
placement="top-start"
|
||||
>
|
||||
<textarea
|
||||
bind:value={querySettings.template}
|
||||
placeholder={$i18n.t('Leave empty to use the default prompt, or enter a custom prompt')}
|
||||
class="w-full rounded-lg px-4 py-3 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="4"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -27,6 +27,43 @@
|
||||
|
||||
let models = null;
|
||||
|
||||
let samplers = [
|
||||
'DPM++ 2M',
|
||||
'DPM++ SDE',
|
||||
'DPM++ 2M SDE',
|
||||
'DPM++ 2M SDE Heun',
|
||||
'DPM++ 2S a',
|
||||
'DPM++ 3M SDE',
|
||||
'Euler a',
|
||||
'Euler',
|
||||
'LMS',
|
||||
'Heun',
|
||||
'DPM2',
|
||||
'DPM2 a',
|
||||
'DPM fast',
|
||||
'DPM adaptive',
|
||||
'Restart',
|
||||
'DDIM',
|
||||
'DDIM CFG++',
|
||||
'PLMS',
|
||||
'UniPC'
|
||||
];
|
||||
|
||||
let schedulers = [
|
||||
'Automatic',
|
||||
'Uniform',
|
||||
'Karras',
|
||||
'Exponential',
|
||||
'Polyexponential',
|
||||
'SGM Uniform',
|
||||
'KL Optimal',
|
||||
'Align Your Steps',
|
||||
'Simple',
|
||||
'Normal',
|
||||
'DDIM',
|
||||
'Beta'
|
||||
];
|
||||
|
||||
let requiredWorkflowNodes = [
|
||||
{
|
||||
type: 'prompt',
|
||||
@@ -326,6 +363,66 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---Sampler-->
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Sampler')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<Tooltip content={$i18n.t('Enter Sampler (e.g. Euler a)')} placement="top-start">
|
||||
<input
|
||||
list="sampler-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Enter Sampler (e.g. Euler a)')}
|
||||
bind:value={config.automatic1111.AUTOMATIC1111_SAMPLER}
|
||||
/>
|
||||
|
||||
<datalist id="sampler-list">
|
||||
{#each samplers ?? [] as sampler}
|
||||
<option value={sampler}>{sampler}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!---Scheduler-->
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Scheduler')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<Tooltip content={$i18n.t('Enter Scheduler (e.g. Karras)')} placement="top-start">
|
||||
<input
|
||||
list="scheduler-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Enter Scheduler (e.g. Karras)')}
|
||||
bind:value={config.automatic1111.AUTOMATIC1111_SCHEDULER}
|
||||
/>
|
||||
|
||||
<datalist id="scheduler-list">
|
||||
{#each schedulers ?? [] as scheduler}
|
||||
<option value={scheduler}>{scheduler}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!---CFG scale-->
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set CFG Scale')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<Tooltip content={$i18n.t('Enter CFG Scale (e.g. 7.0)')} placement="top-start">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Enter CFG Scale (e.g. 7.0)')}
|
||||
bind:value={config.automatic1111.AUTOMATIC1111_CFG_SCALE}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if config?.engine === 'comfyui'}
|
||||
<div class="">
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
|
||||
|
||||
@@ -307,9 +307,10 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800"
|
||||
<textarea
|
||||
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800 resize-none"
|
||||
placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
|
||||
rows="3"
|
||||
bind:value={prompt.content}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+598
-432
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { SvelteFlowProvider } from '@xyflow/svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { mobile, showControls, showCallOverlay, showOverview } from '$lib/stores';
|
||||
|
||||
import Modal from '../common/Modal.svelte';
|
||||
import Controls from './Controls/Controls.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { mobile, showCallOverlay } from '$lib/stores';
|
||||
import CallOverlay from './MessageInput/CallOverlay.svelte';
|
||||
import Drawer from '../common/Drawer.svelte';
|
||||
import Overview from './Overview.svelte';
|
||||
import { Pane, PaneResizer } from 'paneforge';
|
||||
import EllipsisVertical from '../icons/EllipsisVertical.svelte';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export let show = false;
|
||||
|
||||
export let history;
|
||||
export let models = [];
|
||||
|
||||
export let chatId = null;
|
||||
@@ -18,10 +24,13 @@
|
||||
export let eventTarget: EventTarget;
|
||||
export let submitPrompt: Function;
|
||||
export let stopResponse: Function;
|
||||
export let showMessage: Function;
|
||||
export let files;
|
||||
export let modelId;
|
||||
|
||||
export let pane;
|
||||
let largeScreen = false;
|
||||
|
||||
onMount(() => {
|
||||
// listen to resize 1024px
|
||||
const mediaQuery = window.matchMedia('(min-width: 1024px)');
|
||||
@@ -31,6 +40,7 @@
|
||||
largeScreen = true;
|
||||
} else {
|
||||
largeScreen = false;
|
||||
pane = null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -42,70 +52,142 @@
|
||||
mediaQuery.removeEventListener('change', handleMediaQuery);
|
||||
};
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
showControls.set(false);
|
||||
});
|
||||
|
||||
$: if (!chatId) {
|
||||
showOverview.set(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !largeScreen}
|
||||
{#if $showCallOverlay}
|
||||
<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"
|
||||
<SvelteFlowProvider>
|
||||
{#if !largeScreen}
|
||||
{#if $showControls}
|
||||
<Drawer
|
||||
show={$showControls}
|
||||
on:close={() => {
|
||||
showControls.set(false);
|
||||
}}
|
||||
>
|
||||
<CallOverlay
|
||||
bind:files
|
||||
{submitPrompt}
|
||||
{stopResponse}
|
||||
{modelId}
|
||||
{chatId}
|
||||
{eventTarget}
|
||||
on:close={() => {
|
||||
show = false;
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class=" {$showCallOverlay || $showOverview ? ' h-screen w-screen' : 'px-6 py-4'} h-full"
|
||||
>
|
||||
{#if $showCallOverlay}
|
||||
<div
|
||||
class=" h-full max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
|
||||
>
|
||||
<CallOverlay
|
||||
bind:files
|
||||
{submitPrompt}
|
||||
{stopResponse}
|
||||
{modelId}
|
||||
{chatId}
|
||||
{eventTarget}
|
||||
on:close={() => {
|
||||
showControls.set(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else if $showOverview}
|
||||
<Overview
|
||||
{history}
|
||||
on:nodeclick={(e) => {
|
||||
showMessage(e.detail.node.data.message);
|
||||
}}
|
||||
on:close={() => {
|
||||
showControls.set(false);
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<Controls
|
||||
on:close={() => {
|
||||
showControls.set(false);
|
||||
}}
|
||||
{models}
|
||||
bind:chatFiles
|
||||
bind:params
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Drawer>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- if $showControls -->
|
||||
<PaneResizer class="relative flex w-2 items-center justify-center bg-background group">
|
||||
<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
|
||||
<EllipsisVertical className="size-4 invisible group-hover:visible" />
|
||||
</div>
|
||||
</div>
|
||||
{:else if show}
|
||||
<Drawer bind:show>
|
||||
<div class=" px-6 py-4 h-full">
|
||||
<Controls
|
||||
on:close={() => {
|
||||
show = false;
|
||||
}}
|
||||
{models}
|
||||
bind:chatFiles
|
||||
bind:params
|
||||
/>
|
||||
</div>
|
||||
</Drawer>
|
||||
</PaneResizer>
|
||||
<Pane
|
||||
bind:pane
|
||||
defaultSize={$showControls
|
||||
? parseInt(localStorage?.chatControlsSize ?? '30')
|
||||
? parseInt(localStorage?.chatControlsSize ?? '30')
|
||||
: 30
|
||||
: 0}
|
||||
onResize={(size) => {
|
||||
if (size === 0) {
|
||||
showControls.set(false);
|
||||
} else {
|
||||
if (!$showControls) {
|
||||
showControls.set(true);
|
||||
}
|
||||
localStorage.chatControlsSize = size;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if $showControls}
|
||||
<div class="pr-4 pb-8 flex max-h-full min-h-full">
|
||||
<div
|
||||
class="w-full {$showOverview && !$showCallOverlay
|
||||
? ' '
|
||||
: 'px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800'} rounded-lg z-50 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
||||
>
|
||||
{#if $showCallOverlay}
|
||||
<div class="w-full h-full flex justify-center">
|
||||
<CallOverlay
|
||||
bind:files
|
||||
{submitPrompt}
|
||||
{stopResponse}
|
||||
{modelId}
|
||||
{chatId}
|
||||
{eventTarget}
|
||||
on:close={() => {
|
||||
showControls.set(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else if $showOverview}
|
||||
<Overview
|
||||
{history}
|
||||
on:nodeclick={(e) => {
|
||||
if (e.detail.node.data.message.favorite) {
|
||||
history.messages[e.detail.node.data.message.id].favorite = true;
|
||||
} else {
|
||||
history.messages[e.detail.node.data.message.id].favorite = null;
|
||||
}
|
||||
|
||||
showMessage(e.detail.node.data.message);
|
||||
}}
|
||||
on:close={() => {
|
||||
showControls.set(false);
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<Controls
|
||||
on:close={() => {
|
||||
showControls.set(false);
|
||||
}}
|
||||
{models}
|
||||
bind:chatFiles
|
||||
bind:params
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Pane>
|
||||
{/if}
|
||||
{:else if show}
|
||||
<div class=" absolute bottom-0 right-0 z-20 h-full pointer-events-none">
|
||||
<div class="pr-4 pt-14 pb-8 w-[24rem] h-full" in:slide={{ duration: 200, axis: 'x' }}>
|
||||
<div
|
||||
class="w-full h-full px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800 rounded-xl z-50 pointer-events-auto overflow-y-auto scrollbar-hidden"
|
||||
>
|
||||
{#if $showCallOverlay}
|
||||
<CallOverlay
|
||||
bind:files
|
||||
{submitPrompt}
|
||||
{stopResponse}
|
||||
{modelId}
|
||||
{chatId}
|
||||
{eventTarget}
|
||||
on:close={() => {
|
||||
show = false;
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<Controls
|
||||
on:close={() => {
|
||||
show = false;
|
||||
}}
|
||||
{models}
|
||||
bind:chatFiles
|
||||
bind:params
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</SvelteFlowProvider>
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
export let transparentBackground = false;
|
||||
|
||||
export let submitPrompt: Function;
|
||||
export let createMessagePair: Function;
|
||||
export let stopResponse: Function;
|
||||
|
||||
export let autoScroll = false;
|
||||
@@ -61,15 +62,14 @@
|
||||
let user = null;
|
||||
let chatInputPlaceholder = '';
|
||||
|
||||
export let files = [];
|
||||
export let history;
|
||||
|
||||
export let prompt = '';
|
||||
export let files = [];
|
||||
export let availableToolIds = [];
|
||||
export let selectedToolIds = [];
|
||||
export let webSearchEnabled = false;
|
||||
|
||||
export let prompt = '';
|
||||
export let messages = [];
|
||||
|
||||
let visionCapableModels = [];
|
||||
$: visionCapableModels = [...(atSelectedModel ? [atSelectedModel] : selectedModels)].filter(
|
||||
(model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.vision ?? true
|
||||
@@ -93,20 +93,6 @@
|
||||
const uploadFileHandler = async (file) => {
|
||||
console.log(file);
|
||||
|
||||
// Check if the file is an audio file and transcribe/convert it to text file
|
||||
if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
|
||||
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
console.log(res);
|
||||
const blob = new Blob([res.text], { type: 'text/plain' });
|
||||
file = blobToFile(blob, `${file.name}.txt`);
|
||||
}
|
||||
}
|
||||
|
||||
const fileItem = {
|
||||
type: 'file',
|
||||
file: '',
|
||||
@@ -120,6 +106,23 @@
|
||||
};
|
||||
files = [...files, fileItem];
|
||||
|
||||
// Check if the file is an audio file and transcribe/convert it to text file
|
||||
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
|
||||
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
console.log(res);
|
||||
const blob = new Blob([res.text], { type: 'text/plain' });
|
||||
file = blobToFile(blob, `${file.name}.txt`);
|
||||
|
||||
fileItem.name = file.name;
|
||||
fileItem.size = file.size;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const uploadedFile = await uploadFile(localStorage.token, file);
|
||||
|
||||
@@ -269,7 +272,7 @@
|
||||
<div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center">
|
||||
<div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full">
|
||||
<div class="relative">
|
||||
{#if autoScroll === false && messages.length > 0}
|
||||
{#if autoScroll === false && history?.currentId}
|
||||
<div
|
||||
class=" absolute -top-12 left-0 right-0 flex justify-center z-30 pointer-events-none"
|
||||
>
|
||||
@@ -349,7 +352,7 @@
|
||||
</div>
|
||||
|
||||
<div class="{transparentBackground ? 'bg-transparent' : 'bg-white dark:bg-gray-900'} ">
|
||||
<div class="max-w-6xl px-2.5 md:px-6 mx-auto inset-x-0">
|
||||
<div class="max-w-6xl px-2.5 md:px-6 mx-auto inset-x-0 pb-safe-bottom">
|
||||
<div class=" pb-2">
|
||||
<input
|
||||
bind:this={filesInputElement}
|
||||
@@ -552,6 +555,12 @@
|
||||
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
||||
const commandsContainerElement = document.getElementById('commands-container');
|
||||
|
||||
// Command/Ctrl + Shift + Enter to submit a message pair
|
||||
if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
|
||||
e.preventDefault();
|
||||
createMessagePair(prompt);
|
||||
}
|
||||
|
||||
// Check if Ctrl + R is pressed
|
||||
if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
|
||||
e.preventDefault();
|
||||
@@ -689,7 +698,7 @@
|
||||
/>
|
||||
|
||||
<div class="self-end mb-2 flex space-x-1 mr-1">
|
||||
{#if messages.length == 0 || messages.at(-1).done == true}
|
||||
{#if !history?.currentId || history.messages[history.currentId]?.done == true}
|
||||
<Tooltip content={$i18n.t('Record voice')}>
|
||||
<button
|
||||
id="voice-input-button"
|
||||
@@ -741,7 +750,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end w-10">
|
||||
{#if messages.length == 0 || messages.at(-1).done == true}
|
||||
{#if !history.currentId || history.messages[history.currentId]?.done == true}
|
||||
{#if prompt === ''}
|
||||
<div class=" flex items-center mb-1">
|
||||
<Tooltip content={$i18n.t('Call')}>
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { config, models, settings, showCallOverlay } from '$lib/stores';
|
||||
import { onMount, tick, getContext, onDestroy, createEventDispatcher } from 'svelte';
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import {
|
||||
blobToFile,
|
||||
calculateSHA256,
|
||||
extractSentencesForAudio,
|
||||
findWordIndices
|
||||
} from '$lib/utils';
|
||||
import { blobToFile } from '$lib/utils';
|
||||
import { generateEmoji } from '$lib/apis';
|
||||
import { synthesizeOpenAISpeech, transcribeAudio } from '$lib/apis/audio';
|
||||
|
||||
@@ -222,7 +220,9 @@
|
||||
};
|
||||
|
||||
const startRecording = async () => {
|
||||
audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
if (!audioStream) {
|
||||
audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
}
|
||||
mediaRecorder = new MediaRecorder(audioStream);
|
||||
|
||||
mediaRecorder.onstart = () => {
|
||||
@@ -238,7 +238,7 @@
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = (e) => {
|
||||
console.log('Recording stopped', e);
|
||||
console.log('Recording stopped', audioStream, e);
|
||||
stopRecordingCallback();
|
||||
};
|
||||
|
||||
@@ -246,10 +246,11 @@
|
||||
};
|
||||
|
||||
const stopAudioStream = async () => {
|
||||
if (audioStream) {
|
||||
const tracks = audioStream.getTracks();
|
||||
tracks.forEach((track) => track.stop());
|
||||
}
|
||||
if (!audioStream) return;
|
||||
|
||||
audioStream.getAudioTracks().forEach(function (track) {
|
||||
track.stop();
|
||||
});
|
||||
|
||||
audioStream = null;
|
||||
};
|
||||
@@ -360,6 +361,7 @@
|
||||
?.at(0) ?? undefined;
|
||||
|
||||
currentUtterance = new SpeechSynthesisUtterance(content);
|
||||
currentUtterance.rate = $settings.audio?.tts?.playbackRate ?? 1;
|
||||
|
||||
if (voice) {
|
||||
currentUtterance.voice = voice;
|
||||
@@ -381,11 +383,12 @@
|
||||
const playAudio = (audio) => {
|
||||
if ($showCallOverlay) {
|
||||
return new Promise((resolve) => {
|
||||
const audioElement = document.getElementById('audioElement');
|
||||
const audioElement = document.getElementById('audioElement') as HTMLAudioElement;
|
||||
|
||||
if (audioElement) {
|
||||
audioElement.src = audio.src;
|
||||
audioElement.muted = true;
|
||||
audioElement.playbackRate = $settings.audio?.tts?.playbackRate ?? 1;
|
||||
|
||||
audioElement
|
||||
.play()
|
||||
@@ -525,6 +528,60 @@
|
||||
console.log(`Audio monitoring and playing stopped for message ID ${id}`);
|
||||
};
|
||||
|
||||
const chatStartHandler = async (e) => {
|
||||
const { id } = e.detail;
|
||||
|
||||
chatStreaming = true;
|
||||
|
||||
if (currentMessageId !== id) {
|
||||
console.log(`Received chat start event for message ID ${id}`);
|
||||
|
||||
currentMessageId = id;
|
||||
if (audioAbortController) {
|
||||
audioAbortController.abort();
|
||||
}
|
||||
audioAbortController = new AbortController();
|
||||
|
||||
assistantSpeaking = true;
|
||||
// Start monitoring and playing audio for the message ID
|
||||
monitorAndPlayAudio(id, audioAbortController.signal);
|
||||
}
|
||||
};
|
||||
|
||||
const chatEventHandler = async (e) => {
|
||||
const { id, content } = e.detail;
|
||||
// "id" here is message id
|
||||
// if "id" is not the same as "currentMessageId" then do not process
|
||||
// "content" here is a sentence from the assistant,
|
||||
// there will be many sentences for the same "id"
|
||||
|
||||
if (currentMessageId === id) {
|
||||
console.log(`Received chat event for message ID ${id}: ${content}`);
|
||||
|
||||
try {
|
||||
if (messages[id] === undefined) {
|
||||
messages[id] = [content];
|
||||
} else {
|
||||
messages[id].push(content);
|
||||
}
|
||||
|
||||
console.log(content);
|
||||
|
||||
fetchAudio(content);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch or play audio:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const chatFinishHandler = async (e) => {
|
||||
const { id, content } = e.detail;
|
||||
// "content" here is the entire message from the assistant
|
||||
finishedMessages[id] = true;
|
||||
|
||||
chatStreaming = false;
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
const setWakeLock = async () => {
|
||||
try {
|
||||
@@ -558,65 +615,15 @@
|
||||
|
||||
startRecording();
|
||||
|
||||
const chatStartHandler = async (e) => {
|
||||
const { id } = e.detail;
|
||||
|
||||
chatStreaming = true;
|
||||
|
||||
if (currentMessageId !== id) {
|
||||
console.log(`Received chat start event for message ID ${id}`);
|
||||
|
||||
currentMessageId = id;
|
||||
if (audioAbortController) {
|
||||
audioAbortController.abort();
|
||||
}
|
||||
audioAbortController = new AbortController();
|
||||
|
||||
assistantSpeaking = true;
|
||||
// Start monitoring and playing audio for the message ID
|
||||
monitorAndPlayAudio(id, audioAbortController.signal);
|
||||
}
|
||||
};
|
||||
|
||||
const chatEventHandler = async (e) => {
|
||||
const { id, content } = e.detail;
|
||||
// "id" here is message id
|
||||
// if "id" is not the same as "currentMessageId" then do not process
|
||||
// "content" here is a sentence from the assistant,
|
||||
// there will be many sentences for the same "id"
|
||||
|
||||
if (currentMessageId === id) {
|
||||
console.log(`Received chat event for message ID ${id}: ${content}`);
|
||||
|
||||
try {
|
||||
if (messages[id] === undefined) {
|
||||
messages[id] = [content];
|
||||
} else {
|
||||
messages[id].push(content);
|
||||
}
|
||||
|
||||
console.log(content);
|
||||
|
||||
fetchAudio(content);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch or play audio:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const chatFinishHandler = async (e) => {
|
||||
const { id, content } = e.detail;
|
||||
// "content" here is the entire message from the assistant
|
||||
finishedMessages[id] = true;
|
||||
|
||||
chatStreaming = false;
|
||||
};
|
||||
|
||||
eventTarget.addEventListener('chat:start', chatStartHandler);
|
||||
eventTarget.addEventListener('chat', chatEventHandler);
|
||||
eventTarget.addEventListener('chat:finish', chatFinishHandler);
|
||||
|
||||
return async () => {
|
||||
await stopAllAudio();
|
||||
|
||||
stopAudioStream();
|
||||
|
||||
eventTarget.removeEventListener('chat:start', chatStartHandler);
|
||||
eventTarget.removeEventListener('chat', chatEventHandler);
|
||||
eventTarget.removeEventListener('chat:finish', chatFinishHandler);
|
||||
@@ -633,6 +640,17 @@
|
||||
|
||||
onDestroy(async () => {
|
||||
await stopAllAudio();
|
||||
stopAudioStream();
|
||||
|
||||
eventTarget.removeEventListener('chat:start', chatStartHandler);
|
||||
eventTarget.removeEventListener('chat', chatEventHandler);
|
||||
eventTarget.removeEventListener('chat:finish', chatFinishHandler);
|
||||
|
||||
audioAbortController.abort();
|
||||
await tick();
|
||||
|
||||
await stopAllAudio();
|
||||
|
||||
await stopRecordingCallback(false);
|
||||
await stopCamera();
|
||||
});
|
||||
@@ -924,6 +942,10 @@
|
||||
on:click={async () => {
|
||||
await stopAudioStream();
|
||||
await stopVideoStream();
|
||||
|
||||
console.log(audioStream);
|
||||
console.log(cameraStream);
|
||||
|
||||
showCallOverlay.set(false);
|
||||
dispatch('close');
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { prompts } from '$lib/stores';
|
||||
import { findWordIndices } from '$lib/utils';
|
||||
import {
|
||||
findWordIndices,
|
||||
getUserPosition,
|
||||
getFormattedDate,
|
||||
getFormattedTime,
|
||||
getCurrentDateTime,
|
||||
getUserTimezone,
|
||||
getWeekday
|
||||
} from '$lib/utils';
|
||||
import { tick, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
@@ -39,8 +47,6 @@
|
||||
return '{{CLIPBOARD}}';
|
||||
});
|
||||
|
||||
console.log(clipboardText);
|
||||
|
||||
const clipboardItems = await navigator.clipboard.read();
|
||||
|
||||
let imageUrl = null;
|
||||
@@ -50,7 +56,6 @@
|
||||
if (type.startsWith('image/')) {
|
||||
const blob = await item.getType(type);
|
||||
imageUrl = URL.createObjectURL(blob);
|
||||
console.log(`Image URL (${type}): ${imageUrl}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,7 +70,42 @@
|
||||
];
|
||||
}
|
||||
|
||||
text = command.content.replaceAll('{{CLIPBOARD}}', clipboardText);
|
||||
text = text.replaceAll('{{CLIPBOARD}}', clipboardText);
|
||||
}
|
||||
|
||||
if (command.content.includes('{{USER_LOCATION}}')) {
|
||||
const location = await getUserPosition();
|
||||
text = text.replaceAll('{{USER_LOCATION}}', String(location));
|
||||
}
|
||||
|
||||
if (command.content.includes('{{USER_LANGUAGE}}')) {
|
||||
const language = localStorage.getItem('locale') || 'en-US';
|
||||
text = text.replaceAll('{{USER_LANGUAGE}}', language);
|
||||
}
|
||||
|
||||
if (command.content.includes('{{CURRENT_DATE}}')) {
|
||||
const date = getFormattedDate();
|
||||
text = text.replaceAll('{{CURRENT_DATE}}', date);
|
||||
}
|
||||
|
||||
if (command.content.includes('{{CURRENT_TIME}}')) {
|
||||
const time = getFormattedTime();
|
||||
text = text.replaceAll('{{CURRENT_TIME}}', time);
|
||||
}
|
||||
|
||||
if (command.content.includes('{{CURRENT_DATETIME}}')) {
|
||||
const dateTime = getCurrentDateTime();
|
||||
text = text.replaceAll('{{CURRENT_DATETIME}}', dateTime);
|
||||
}
|
||||
|
||||
if (command.content.includes('{{CURRENT_TIMEZONE}}')) {
|
||||
const timezone = getUserTimezone();
|
||||
text = text.replaceAll('{{CURRENT_TIMEZONE}}', timezone);
|
||||
}
|
||||
|
||||
if (command.content.includes('{{CURRENT_WEEKDAY}}')) {
|
||||
const weekday = getWeekday();
|
||||
text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
|
||||
}
|
||||
|
||||
prompt = text;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
let prompts = [];
|
||||
|
||||
$: prompts = suggestionPrompts
|
||||
$: prompts = (suggestionPrompts ?? [])
|
||||
.reduce((acc, current) => [...acc, ...[current]], [])
|
||||
.sort(() => Math.random() - 0.5);
|
||||
// suggestionPrompts.length <= 4
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
return `${minutes}:${formattedSeconds}`;
|
||||
};
|
||||
|
||||
let stream;
|
||||
let speechRecognition;
|
||||
|
||||
let mediaRecorder;
|
||||
@@ -159,7 +160,7 @@
|
||||
const startRecording = async () => {
|
||||
startDurationCounter();
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
mediaRecorder = new MediaRecorder(stream);
|
||||
mediaRecorder.onstart = () => {
|
||||
console.log('Recording started');
|
||||
@@ -251,6 +252,13 @@
|
||||
}
|
||||
stopDurationCounter();
|
||||
audioChunks = [];
|
||||
|
||||
if (stream) {
|
||||
const tracks = stream.getTracks();
|
||||
tracks.forEach((track) => track.stop());
|
||||
}
|
||||
|
||||
stream = null;
|
||||
};
|
||||
|
||||
const confirmRecording = async () => {
|
||||
|
||||
@@ -7,30 +7,63 @@
|
||||
import { getChatList, updateChatById } from '$lib/apis/chats';
|
||||
import { copyToClipboard, findWordIndices } from '$lib/utils';
|
||||
|
||||
import UserMessage from './Messages/UserMessage.svelte';
|
||||
import ResponseMessage from './Messages/ResponseMessage.svelte';
|
||||
import Placeholder from './Messages/Placeholder.svelte';
|
||||
import MultiResponseMessages from './Messages/MultiResponseMessages.svelte';
|
||||
import Message from './Messages/Message.svelte';
|
||||
import Loader from '../common/Loader.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let chatId = '';
|
||||
export let readOnly = false;
|
||||
export let user = $_user;
|
||||
|
||||
export let prompt;
|
||||
export let history = {};
|
||||
export let selectedModels;
|
||||
|
||||
let messages = [];
|
||||
|
||||
export let sendPrompt: Function;
|
||||
export let continueGeneration: Function;
|
||||
export let continueResponse: Function;
|
||||
export let regenerateResponse: Function;
|
||||
export let mergeResponses: Function;
|
||||
export let chatActionHandler: Function;
|
||||
export let showMessage: Function = () => {};
|
||||
|
||||
export let readOnly = false;
|
||||
|
||||
export let user = $_user;
|
||||
export let prompt;
|
||||
export let processing = '';
|
||||
export let bottomPadding = false;
|
||||
export let autoScroll;
|
||||
export let history = {};
|
||||
export let messages = [];
|
||||
|
||||
export let selectedModels;
|
||||
let messagesCount = 20;
|
||||
let messagesLoading = false;
|
||||
|
||||
const loadMoreMessages = async () => {
|
||||
// scroll slightly down to disable continuous loading
|
||||
const element = document.getElementById('messages-container');
|
||||
element.scrollTop = element.scrollTop + 100;
|
||||
|
||||
messagesLoading = true;
|
||||
messagesCount += 20;
|
||||
|
||||
await tick();
|
||||
|
||||
messagesLoading = false;
|
||||
};
|
||||
|
||||
$: if (history.currentId) {
|
||||
let _messages = [];
|
||||
|
||||
let message = history.messages[history.currentId];
|
||||
while (message && _messages.length <= messagesCount) {
|
||||
_messages.unshift({ ...message });
|
||||
message = message.parentId !== null ? history.messages[message.parentId] : null;
|
||||
}
|
||||
|
||||
messages = _messages;
|
||||
} else {
|
||||
messages = [];
|
||||
}
|
||||
|
||||
$: if (autoScroll && bottomPadding) {
|
||||
(async () => {
|
||||
@@ -44,56 +77,9 @@
|
||||
element.scrollTop = element.scrollHeight;
|
||||
};
|
||||
|
||||
const copyToClipboardWithToast = async (text) => {
|
||||
const res = await copyToClipboard(text);
|
||||
if (res) {
|
||||
toast.success($i18n.t('Copying to clipboard was successful!'));
|
||||
}
|
||||
};
|
||||
|
||||
const confirmEditMessage = async (messageId, content, submit = true) => {
|
||||
if (submit) {
|
||||
let userPrompt = content;
|
||||
let userMessageId = uuidv4();
|
||||
|
||||
let userMessage = {
|
||||
id: userMessageId,
|
||||
parentId: history.messages[messageId].parentId,
|
||||
childrenIds: [],
|
||||
role: 'user',
|
||||
content: userPrompt,
|
||||
...(history.messages[messageId].files && { files: history.messages[messageId].files }),
|
||||
models: selectedModels
|
||||
};
|
||||
|
||||
let messageParentId = history.messages[messageId].parentId;
|
||||
|
||||
if (messageParentId !== null) {
|
||||
history.messages[messageParentId].childrenIds = [
|
||||
...history.messages[messageParentId].childrenIds,
|
||||
userMessageId
|
||||
];
|
||||
}
|
||||
|
||||
history.messages[userMessageId] = userMessage;
|
||||
history.currentId = userMessageId;
|
||||
|
||||
await tick();
|
||||
await sendPrompt(userPrompt, userMessageId);
|
||||
} else {
|
||||
history.messages[messageId].content = content;
|
||||
await tick();
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateChatMessages = async () => {
|
||||
const updateChatHistory = async () => {
|
||||
await tick();
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
|
||||
@@ -101,22 +87,6 @@
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
};
|
||||
|
||||
const confirmEditResponseMessage = async (messageId, content) => {
|
||||
history.messages[messageId].originalContent = history.messages[messageId].content;
|
||||
history.messages[messageId].content = content;
|
||||
|
||||
await updateChatMessages();
|
||||
};
|
||||
|
||||
const rateMessage = async (messageId, rating) => {
|
||||
history.messages[messageId].annotation = {
|
||||
...history.messages[messageId].annotation,
|
||||
rating: rating
|
||||
};
|
||||
|
||||
await updateChatMessages();
|
||||
};
|
||||
|
||||
const showPreviousMessage = async (message) => {
|
||||
if (message.parentId !== null) {
|
||||
let messageId =
|
||||
@@ -215,60 +185,129 @@
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMessageHandler = async (messageId) => {
|
||||
const messageToDelete = history.messages[messageId];
|
||||
const rateMessage = async (messageId, rating) => {
|
||||
history.messages[messageId].annotation = {
|
||||
...history.messages[messageId].annotation,
|
||||
rating: rating
|
||||
};
|
||||
|
||||
await updateChatHistory();
|
||||
};
|
||||
|
||||
const editMessage = async (messageId, content, submit = true) => {
|
||||
if (history.messages[messageId].role === 'user') {
|
||||
if (submit) {
|
||||
// New user message
|
||||
let userPrompt = content;
|
||||
let userMessageId = uuidv4();
|
||||
|
||||
let userMessage = {
|
||||
id: userMessageId,
|
||||
parentId: history.messages[messageId].parentId,
|
||||
childrenIds: [],
|
||||
role: 'user',
|
||||
content: userPrompt,
|
||||
...(history.messages[messageId].files && { files: history.messages[messageId].files }),
|
||||
models: selectedModels
|
||||
};
|
||||
|
||||
let messageParentId = history.messages[messageId].parentId;
|
||||
|
||||
if (messageParentId !== null) {
|
||||
history.messages[messageParentId].childrenIds = [
|
||||
...history.messages[messageParentId].childrenIds,
|
||||
userMessageId
|
||||
];
|
||||
}
|
||||
|
||||
history.messages[userMessageId] = userMessage;
|
||||
history.currentId = userMessageId;
|
||||
|
||||
await tick();
|
||||
await sendPrompt(userPrompt, userMessageId);
|
||||
} else {
|
||||
// Edit user message
|
||||
history.messages[messageId].content = content;
|
||||
await updateChatHistory();
|
||||
}
|
||||
} else {
|
||||
if (submit) {
|
||||
// New response message
|
||||
const responseMessageId = uuidv4();
|
||||
const message = history.messages[messageId];
|
||||
const parentId = message.parentId;
|
||||
|
||||
const responseMessage = {
|
||||
...message,
|
||||
id: responseMessageId,
|
||||
parentId: parentId,
|
||||
childrenIds: [],
|
||||
content: content,
|
||||
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
|
||||
};
|
||||
|
||||
history.messages[responseMessageId] = responseMessage;
|
||||
history.currentId = responseMessageId;
|
||||
|
||||
// Append messageId to childrenIds of parent message
|
||||
if (parentId !== null) {
|
||||
history.messages[parentId].childrenIds = [
|
||||
...history.messages[parentId].childrenIds,
|
||||
responseMessageId
|
||||
];
|
||||
}
|
||||
|
||||
await updateChatHistory();
|
||||
} else {
|
||||
// Edit response message
|
||||
history.messages[messageId].originalContent = history.messages[messageId].content;
|
||||
history.messages[messageId].content = content;
|
||||
await updateChatHistory();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMessage = async (messageId) => {
|
||||
const messageToDelete = history.messages[messageId];
|
||||
const parentMessageId = messageToDelete.parentId;
|
||||
const childMessageIds = messageToDelete.childrenIds ?? [];
|
||||
|
||||
const hasDescendantMessages = childMessageIds.some(
|
||||
(childId) => history.messages[childId]?.childrenIds?.length > 0
|
||||
// Collect all grandchildren
|
||||
const grandchildrenIds = childMessageIds.flatMap(
|
||||
(childId) => history.messages[childId]?.childrenIds ?? []
|
||||
);
|
||||
|
||||
history.currentId = parentMessageId;
|
||||
await tick();
|
||||
// Update parent's children
|
||||
if (parentMessageId && history.messages[parentMessageId]) {
|
||||
history.messages[parentMessageId].childrenIds = [
|
||||
...history.messages[parentMessageId].childrenIds.filter((id) => id !== messageId),
|
||||
...grandchildrenIds
|
||||
];
|
||||
}
|
||||
|
||||
// Remove the message itself from the parent message's children array
|
||||
history.messages[parentMessageId].childrenIds = history.messages[
|
||||
parentMessageId
|
||||
].childrenIds.filter((id) => id !== messageId);
|
||||
|
||||
await tick();
|
||||
|
||||
childMessageIds.forEach((childId) => {
|
||||
const childMessage = history.messages[childId];
|
||||
|
||||
if (childMessage && childMessage.childrenIds) {
|
||||
if (childMessage.childrenIds.length === 0 && !hasDescendantMessages) {
|
||||
// If there are no other responses/prompts
|
||||
history.messages[parentMessageId].childrenIds = [];
|
||||
} else {
|
||||
childMessage.childrenIds.forEach((grandChildId) => {
|
||||
if (history.messages[grandChildId]) {
|
||||
history.messages[grandChildId].parentId = parentMessageId;
|
||||
history.messages[parentMessageId].childrenIds.push(grandChildId);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Update grandchildren's parent
|
||||
grandchildrenIds.forEach((grandchildId) => {
|
||||
if (history.messages[grandchildId]) {
|
||||
history.messages[grandchildId].parentId = parentMessageId;
|
||||
}
|
||||
});
|
||||
|
||||
// Remove child message id from the parent message's children array
|
||||
history.messages[parentMessageId].childrenIds = history.messages[
|
||||
parentMessageId
|
||||
].childrenIds.filter((id) => id !== childId);
|
||||
// Delete the message and its children
|
||||
[messageId, ...childMessageIds].forEach((id) => {
|
||||
delete history.messages[id];
|
||||
});
|
||||
|
||||
await tick();
|
||||
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
showMessage({ id: parentMessageId });
|
||||
|
||||
// Update the chat
|
||||
await updateChatHistory();
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="h-full flex">
|
||||
{#if messages.length == 0}
|
||||
{#if Object.keys(history?.messages ?? {}).length == 0}
|
||||
<Placeholder
|
||||
modelIds={selectedModels}
|
||||
submitPrompt={async (p) => {
|
||||
@@ -309,114 +348,55 @@
|
||||
{:else}
|
||||
<div class="w-full pt-2">
|
||||
{#key chatId}
|
||||
{#each messages as message, messageIdx (message.id)}
|
||||
<div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}">
|
||||
<div
|
||||
class="flex flex-col justify-between px-5 mb-3 {($settings?.widescreenMode ?? null)
|
||||
? 'max-w-full'
|
||||
: 'max-w-5xl'} mx-auto rounded-lg group"
|
||||
<div class="w-full">
|
||||
{#if messages.at(0)?.parentId !== null}
|
||||
<Loader
|
||||
on:visible={(e) => {
|
||||
console.log('visible');
|
||||
if (!messagesLoading) {
|
||||
loadMoreMessages();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if message.role === 'user'}
|
||||
<UserMessage
|
||||
on:delete={() => deleteMessageHandler(message.id)}
|
||||
{user}
|
||||
{readOnly}
|
||||
{message}
|
||||
isFirstMessage={messageIdx === 0}
|
||||
siblings={message.parentId !== null
|
||||
? (history.messages[message.parentId]?.childrenIds ?? [])
|
||||
: (Object.values(history.messages)
|
||||
.filter((message) => message.parentId === null)
|
||||
.map((message) => message.id) ?? [])}
|
||||
{confirmEditMessage}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
copyToClipboard={copyToClipboardWithToast}
|
||||
/>
|
||||
{:else if (history.messages[message.parentId]?.models?.length ?? 1) === 1}
|
||||
{#key message.id}
|
||||
<ResponseMessage
|
||||
{message}
|
||||
siblings={history.messages[message.parentId]?.childrenIds ?? []}
|
||||
isLastMessage={messageIdx + 1 === messages.length}
|
||||
{readOnly}
|
||||
{updateChatMessages}
|
||||
{confirmEditResponseMessage}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{rateMessage}
|
||||
copyToClipboard={copyToClipboardWithToast}
|
||||
{continueGeneration}
|
||||
{regenerateResponse}
|
||||
on:action={async (e) => {
|
||||
console.log('action', e);
|
||||
if (typeof e.detail === 'string') {
|
||||
await chatActionHandler(chatId, e.detail, message.model, message.id);
|
||||
} else {
|
||||
const { id, event } = e.detail;
|
||||
await chatActionHandler(chatId, id, message.model, message.id, event);
|
||||
}
|
||||
}}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
|
||||
const message = e.detail;
|
||||
history.messages[message.id] = message;
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{:else}
|
||||
{#key message.parentId}
|
||||
<MultiResponseMessages
|
||||
bind:history
|
||||
isLastMessage={messageIdx + 1 === messages.length}
|
||||
{messages}
|
||||
{readOnly}
|
||||
{chatId}
|
||||
parentMessage={history.messages[message.parentId]}
|
||||
{messageIdx}
|
||||
{updateChatMessages}
|
||||
{confirmEditResponseMessage}
|
||||
{rateMessage}
|
||||
copyToClipboard={copyToClipboardWithToast}
|
||||
{continueGeneration}
|
||||
{mergeResponses}
|
||||
{regenerateResponse}
|
||||
on:action={async (e) => {
|
||||
console.log('action', e);
|
||||
if (typeof e.detail === 'string') {
|
||||
await chatActionHandler(chatId, e.detail, message.model, message.id);
|
||||
} else {
|
||||
const { id, event } = e.detail;
|
||||
await chatActionHandler(chatId, id, message.model, message.id, event);
|
||||
}
|
||||
}}
|
||||
on:change={async () => {
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
|
||||
if (autoScroll) {
|
||||
const element = document.getElementById('messages-container');
|
||||
autoScroll =
|
||||
element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
|
||||
<Spinner className=" size-4" />
|
||||
<div class=" ">Loading...</div>
|
||||
</div>
|
||||
</Loader>
|
||||
{/if}
|
||||
|
||||
{#each messages as message, messageIdx (message.id)}
|
||||
<Message
|
||||
{chatId}
|
||||
bind:history
|
||||
messageId={message.id}
|
||||
idx={messageIdx}
|
||||
{user}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{editMessage}
|
||||
{deleteMessage}
|
||||
{rateMessage}
|
||||
{regenerateResponse}
|
||||
{continueResponse}
|
||||
{mergeResponses}
|
||||
{updateChatHistory}
|
||||
{chatActionHandler}
|
||||
{readOnly}
|
||||
on:scroll={() => {
|
||||
if (autoScroll) {
|
||||
const element = document.getElementById('messages-container');
|
||||
autoScroll =
|
||||
element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="pb-12" />
|
||||
{#if bottomPadding}
|
||||
<div class=" pb-6" />
|
||||
{/if}
|
||||
|
||||
@@ -3,14 +3,9 @@
|
||||
|
||||
export let citations = [];
|
||||
|
||||
let showCitationModal = false;
|
||||
let selectedCitation = null;
|
||||
</script>
|
||||
let _citations = [];
|
||||
|
||||
<CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
|
||||
|
||||
<div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
|
||||
{#each citations.reduce((acc, citation) => {
|
||||
$: _citations = citations.reduce((acc, citation) => {
|
||||
citation.document.forEach((document, index) => {
|
||||
const metadata = citation.metadata?.[index];
|
||||
const id = metadata?.source ?? 'N/A';
|
||||
@@ -31,26 +26,42 @@
|
||||
existingSource.document.push(document);
|
||||
existingSource.metadata.push(metadata);
|
||||
} else {
|
||||
acc.push( { id: id, source: source, document: [document], metadata: metadata ? [metadata] : [] } );
|
||||
acc.push({
|
||||
id: id,
|
||||
source: source,
|
||||
document: [document],
|
||||
metadata: metadata ? [metadata] : []
|
||||
});
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}, []) as citation, idx}
|
||||
<div class="flex gap-1 text-xs font-semibold">
|
||||
<button
|
||||
class="flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
}}
|
||||
>
|
||||
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
|
||||
{idx + 1}
|
||||
</div>
|
||||
<div class="flex-1 mx-2 line-clamp-1">
|
||||
{citation.source.name}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
}, []);
|
||||
|
||||
let showCitationModal = false;
|
||||
let selectedCitation = null;
|
||||
</script>
|
||||
|
||||
<CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
|
||||
|
||||
{#if _citations.length > 0}
|
||||
<div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
|
||||
{#each _citations as citation, idx}
|
||||
<div class="flex gap-1 text-xs font-semibold">
|
||||
<button
|
||||
class="flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl max-w-96"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
}}
|
||||
>
|
||||
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
|
||||
{idx + 1}
|
||||
</div>
|
||||
<div class="flex-1 mx-2 line-clamp-1">
|
||||
{citation.source.name}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount, tick } from 'svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
@@ -55,18 +56,28 @@
|
||||
</div>
|
||||
|
||||
{#if document.source?.name}
|
||||
<div class="text-sm dark:text-gray-400">
|
||||
<a
|
||||
href={document?.metadata?.file_id
|
||||
? `/api/v1/files/${document?.metadata?.file_id}/content`
|
||||
: document.source.name.includes('http')
|
||||
? document.source.name
|
||||
: `#`}
|
||||
target="_blank"
|
||||
>
|
||||
{document?.metadata?.name ?? document.source.name}
|
||||
</a>
|
||||
</div>
|
||||
<Tooltip
|
||||
content={$i18n.t('Open file')}
|
||||
placement="left"
|
||||
tippyOptions={{ duration: [500, 0], animation: 'perspective' }}
|
||||
>
|
||||
<div class="text-sm dark:text-gray-400">
|
||||
<a
|
||||
class="hover:text-gray-500 hover:dark:text-gray-100 underline"
|
||||
href={document?.metadata?.file_id
|
||||
? `/api/v1/files/${document?.metadata?.file_id}/content${document?.metadata?.page !== undefined ? `#page=${document.metadata.page + 1}` : ''}`
|
||||
: document.source.name.includes('http')
|
||||
? document.source.name
|
||||
: `#`}
|
||||
target="_blank"
|
||||
>
|
||||
{document?.metadata?.name ?? document.source.name}
|
||||
</a>
|
||||
{document?.metadata?.page
|
||||
? `(${$i18n.t('page')} ${document.metadata.page + 1})`
|
||||
: ''}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<div class="text-sm dark:text-gray-400">
|
||||
{$i18n.t('No source available')}
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
export let lang = '';
|
||||
export let code = '';
|
||||
|
||||
let _token = null;
|
||||
|
||||
let mermaidHtml = null;
|
||||
|
||||
let highlightedCode = null;
|
||||
@@ -226,7 +228,7 @@ __builtins__.input = input`);
|
||||
}
|
||||
};
|
||||
|
||||
$: if (token.raw) {
|
||||
const render = async () => {
|
||||
if (lang === 'mermaid' && (token?.raw ?? '').slice(-4).includes('```')) {
|
||||
(async () => {
|
||||
await drawMermaidDiagram();
|
||||
@@ -242,9 +244,21 @@ __builtins__.input = input`);
|
||||
// Set a new timeout to debounce the code highlighting
|
||||
debounceTimeout = setTimeout(highlightCode, 10);
|
||||
}
|
||||
};
|
||||
|
||||
$: if (token) {
|
||||
if (JSON.stringify(token) !== JSON.stringify(_token)) {
|
||||
console.log('hi');
|
||||
_token = token;
|
||||
}
|
||||
}
|
||||
|
||||
$: if (_token) {
|
||||
render();
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
console.log('codeblock', lang, code);
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</script>
|
||||
|
||||
<!-- {JSON.stringify(tokens)} -->
|
||||
{#each tokens as token, tokenIdx}
|
||||
{#each tokens as token, tokenIdx (tokenIdx)}
|
||||
{#if token.type === 'hr'}
|
||||
<hr />
|
||||
{:else if token.type === 'heading'}
|
||||
@@ -34,34 +34,36 @@
|
||||
code={revertSanitizedResponseContent(token?.text ?? '')}
|
||||
/>
|
||||
{:else if token.type === 'table'}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{#each token.header as header, headerIdx}
|
||||
<th style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}>
|
||||
<MarkdownInlineTokens
|
||||
id={`${id}-${tokenIdx}-header-${headerIdx}`}
|
||||
tokens={header.tokens}
|
||||
/>
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each token.rows as row, rowIdx}
|
||||
<div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
{#each row ?? [] as cell, cellIdx}
|
||||
<td style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}>
|
||||
{#each token.header as header, headerIdx}
|
||||
<th style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}>
|
||||
<MarkdownInlineTokens
|
||||
id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
|
||||
tokens={cell.tokens}
|
||||
id={`${id}-${tokenIdx}-header-${headerIdx}`}
|
||||
tokens={header.tokens}
|
||||
/>
|
||||
</td>
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each token.rows as row, rowIdx}
|
||||
<tr>
|
||||
{#each row ?? [] as cell, cellIdx}
|
||||
<td style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}>
|
||||
<MarkdownInlineTokens
|
||||
id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
|
||||
tokens={cell.tokens}
|
||||
/>
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{:else if token.type === 'blockquote'}
|
||||
<blockquote>
|
||||
<svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} />
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { tick, getContext, onMount, createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { settings } from '$lib/stores';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
import MultiResponseMessages from './MultiResponseMessages.svelte';
|
||||
import ResponseMessage from './ResponseMessage.svelte';
|
||||
import UserMessage from './UserMessage.svelte';
|
||||
import { updateChatById } from '$lib/apis/chats';
|
||||
|
||||
export let chatId;
|
||||
export let idx = 0;
|
||||
|
||||
export let history;
|
||||
export let messageId;
|
||||
|
||||
export let user;
|
||||
|
||||
export let updateChatHistory;
|
||||
export let chatActionHandler;
|
||||
|
||||
export let showPreviousMessage;
|
||||
export let showNextMessage;
|
||||
|
||||
export let editMessage;
|
||||
export let deleteMessage;
|
||||
export let rateMessage;
|
||||
|
||||
export let regenerateResponse;
|
||||
export let continueResponse;
|
||||
|
||||
// MultiResponseMessages
|
||||
export let mergeResponses;
|
||||
|
||||
export let autoScroll = false;
|
||||
export let readOnly = false;
|
||||
|
||||
onMount(() => {
|
||||
// console.log('message', idx);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex flex-col justify-between px-5 mb-3 w-full {($settings?.widescreenMode ?? null)
|
||||
? 'max-w-full'
|
||||
: 'max-w-5xl'} mx-auto rounded-lg group"
|
||||
>
|
||||
{#if history.messages[messageId]}
|
||||
{#if history.messages[messageId].role === 'user'}
|
||||
<UserMessage
|
||||
{user}
|
||||
{history}
|
||||
{messageId}
|
||||
isFirstMessage={idx === 0}
|
||||
siblings={history.messages[messageId].parentId !== null
|
||||
? (history.messages[history.messages[messageId].parentId]?.childrenIds ?? [])
|
||||
: (Object.values(history.messages)
|
||||
.filter((message) => message.parentId === null)
|
||||
.map((message) => message.id) ?? [])}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{editMessage}
|
||||
on:delete={() => deleteMessage(messageId)}
|
||||
{readOnly}
|
||||
/>
|
||||
{:else if (history.messages[history.messages[messageId].parentId]?.models?.length ?? 1) === 1}
|
||||
<ResponseMessage
|
||||
{history}
|
||||
{messageId}
|
||||
isLastMessage={messageId === history.currentId}
|
||||
siblings={history.messages[history.messages[messageId].parentId]?.childrenIds ?? []}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
{editMessage}
|
||||
{rateMessage}
|
||||
{continueResponse}
|
||||
{regenerateResponse}
|
||||
on:action={async (e) => {
|
||||
console.log('action', e);
|
||||
const message = history.messages[messageId];
|
||||
if (typeof e.detail === 'string') {
|
||||
await chatActionHandler(chatId, e.detail, message.model, message.id);
|
||||
} else {
|
||||
const { id, event } = e.detail;
|
||||
await chatActionHandler(chatId, id, message.model, message.id, event);
|
||||
}
|
||||
}}
|
||||
on:update={async (e) => {
|
||||
console.log('update', e);
|
||||
updateChatHistory();
|
||||
}}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
|
||||
const message = e.detail;
|
||||
if (message) {
|
||||
history.messages[message.id] = message;
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
history: history
|
||||
});
|
||||
} else {
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
history: history
|
||||
});
|
||||
}
|
||||
}}
|
||||
{readOnly}
|
||||
/>
|
||||
{:else}
|
||||
<MultiResponseMessages
|
||||
bind:history
|
||||
{chatId}
|
||||
{messageId}
|
||||
isLastMessage={messageId === history?.currentId}
|
||||
{rateMessage}
|
||||
{editMessage}
|
||||
{continueResponse}
|
||||
{regenerateResponse}
|
||||
{mergeResponses}
|
||||
on:action={async (e) => {
|
||||
console.log('action', e);
|
||||
const message = history.messages[messageId];
|
||||
if (typeof e.detail === 'string') {
|
||||
await chatActionHandler(chatId, e.detail, message.model, message.id);
|
||||
} else {
|
||||
const { id, event } = e.detail;
|
||||
await chatActionHandler(chatId, id, message.model, message.id, event);
|
||||
}
|
||||
}}
|
||||
on:update={async (e) => {
|
||||
console.log('update', e);
|
||||
updateChatHistory();
|
||||
}}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
|
||||
const message = e.detail;
|
||||
if (message) {
|
||||
history.messages[message.id] = message;
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
history: history
|
||||
});
|
||||
} else {
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
history: history
|
||||
});
|
||||
}
|
||||
}}
|
||||
on:change={async () => {
|
||||
await tick();
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
history: history
|
||||
});
|
||||
|
||||
dispatch('scroll');
|
||||
}}
|
||||
{readOnly}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -20,40 +20,39 @@
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let chatId;
|
||||
|
||||
export let history;
|
||||
export let messages = [];
|
||||
export let messageIdx;
|
||||
export let messageId;
|
||||
|
||||
export let parentMessage;
|
||||
export let isLastMessage;
|
||||
|
||||
export let readOnly = false;
|
||||
|
||||
export let updateChatMessages: Function;
|
||||
export let confirmEditResponseMessage: Function;
|
||||
export let editMessage: Function;
|
||||
export let rateMessage: Function;
|
||||
|
||||
export let copyToClipboard: Function;
|
||||
export let continueGeneration: Function;
|
||||
export let mergeResponses: Function;
|
||||
export let continueResponse: Function;
|
||||
export let regenerateResponse: Function;
|
||||
export let mergeResponses: Function;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let currentMessageId;
|
||||
let groupedMessages = {};
|
||||
let groupedMessagesIdx = {};
|
||||
let parentMessage;
|
||||
let groupedMessageIds = {};
|
||||
let groupedMessageIdsIdx = {};
|
||||
|
||||
$: if (parentMessage) {
|
||||
initHandler();
|
||||
let message = JSON.parse(JSON.stringify(history.messages[messageId]));
|
||||
$: if (history.messages) {
|
||||
if (JSON.stringify(message) !== JSON.stringify(history.messages[messageId])) {
|
||||
message = JSON.parse(JSON.stringify(history.messages[messageId]));
|
||||
}
|
||||
}
|
||||
|
||||
const showPreviousMessage = (modelIdx) => {
|
||||
groupedMessagesIdx[modelIdx] = Math.max(0, groupedMessagesIdx[modelIdx] - 1);
|
||||
let messageId = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]].id;
|
||||
groupedMessageIdsIdx[modelIdx] = Math.max(0, groupedMessageIdsIdx[modelIdx] - 1);
|
||||
|
||||
let messageId = groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]];
|
||||
console.log(messageId);
|
||||
|
||||
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
|
||||
while (messageChildrenIds.length !== 0) {
|
||||
@@ -66,12 +65,12 @@
|
||||
};
|
||||
|
||||
const showNextMessage = (modelIdx) => {
|
||||
groupedMessagesIdx[modelIdx] = Math.min(
|
||||
groupedMessages[modelIdx].messages.length - 1,
|
||||
groupedMessagesIdx[modelIdx] + 1
|
||||
groupedMessageIdsIdx[modelIdx] = Math.min(
|
||||
groupedMessageIds[modelIdx].messageIds.length - 1,
|
||||
groupedMessageIdsIdx[modelIdx] + 1
|
||||
);
|
||||
|
||||
let messageId = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]].id;
|
||||
let messageId = groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]];
|
||||
console.log(messageId);
|
||||
|
||||
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
@@ -86,33 +85,43 @@
|
||||
};
|
||||
|
||||
const initHandler = async () => {
|
||||
console.log('multiresponse:initHandler');
|
||||
await tick();
|
||||
currentMessageId = messages[messageIdx].id;
|
||||
|
||||
groupedMessages = parentMessage?.models.reduce((a, model, modelIdx) => {
|
||||
currentMessageId = messageId;
|
||||
parentMessage = history.messages[messageId].parentId
|
||||
? history.messages[history.messages[messageId].parentId]
|
||||
: null;
|
||||
|
||||
groupedMessageIds = parentMessage?.models.reduce((a, model, modelIdx) => {
|
||||
// Find all messages that are children of the parent message and have the same model
|
||||
let modelMessages = parentMessage?.childrenIds
|
||||
let modelMessageIds = parentMessage?.childrenIds
|
||||
.map((id) => history.messages[id])
|
||||
.filter((m) => m?.modelIdx === modelIdx);
|
||||
.filter((m) => m?.modelIdx === modelIdx)
|
||||
.map((m) => m.id);
|
||||
|
||||
if (modelMessages.length === 0) {
|
||||
modelMessages = parentMessage?.childrenIds
|
||||
// Legacy support for messages that don't have a modelIdx
|
||||
// Find all messages that are children of the parent message and have the same model
|
||||
if (modelMessageIds.length === 0) {
|
||||
let modelMessages = parentMessage?.childrenIds
|
||||
.map((id) => history.messages[id])
|
||||
.filter((m) => m?.model === model);
|
||||
|
||||
modelMessages.forEach((m) => {
|
||||
m.modelIdx = modelIdx;
|
||||
});
|
||||
|
||||
modelMessageIds = modelMessages.map((m) => m.id);
|
||||
}
|
||||
|
||||
return {
|
||||
...a,
|
||||
[modelIdx]: { messages: modelMessages }
|
||||
[modelIdx]: { messageIds: modelMessageIds }
|
||||
};
|
||||
}, {});
|
||||
|
||||
groupedMessagesIdx = parentMessage?.models.reduce((a, model, modelIdx) => {
|
||||
const idx = groupedMessages[modelIdx].messages.findIndex((m) => m.id === currentMessageId);
|
||||
groupedMessageIdsIdx = parentMessage?.models.reduce((a, model, modelIdx) => {
|
||||
const idx = groupedMessageIds[modelIdx].messageIds.findIndex((id) => id === messageId);
|
||||
if (idx !== -1) {
|
||||
return {
|
||||
...a,
|
||||
@@ -121,39 +130,53 @@
|
||||
} else {
|
||||
return {
|
||||
...a,
|
||||
[modelIdx]: 0
|
||||
[modelIdx]: groupedMessageIds[modelIdx].messageIds.length - 1
|
||||
};
|
||||
}
|
||||
}, {});
|
||||
|
||||
console.log(groupedMessageIds, groupedMessageIdsIdx);
|
||||
|
||||
await tick();
|
||||
};
|
||||
|
||||
const mergeResponsesHandler = async () => {
|
||||
const responses = Object.keys(groupedMessages).map((modelIdx) => {
|
||||
const { messages } = groupedMessages[modelIdx];
|
||||
return messages[groupedMessagesIdx[modelIdx]].content;
|
||||
const responses = Object.keys(groupedMessageIds).map((modelIdx) => {
|
||||
const { messageIds } = groupedMessageIds[modelIdx];
|
||||
const messageId = messageIds[groupedMessageIdsIdx[modelIdx]];
|
||||
|
||||
return history.messages[messageId].content;
|
||||
});
|
||||
mergeResponses(currentMessageId, responses, chatId);
|
||||
mergeResponses(messageId, responses, chatId);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
initHandler();
|
||||
await initHandler();
|
||||
await tick();
|
||||
|
||||
const messageElement = document.getElementById(`message-${messageId}`);
|
||||
console.log(messageElement);
|
||||
if (messageElement) {
|
||||
messageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
|
||||
id="responses-container-{chatId}-{parentMessage.id}"
|
||||
>
|
||||
{#key currentMessageId}
|
||||
{#each Object.keys(groupedMessages) as modelIdx}
|
||||
{#if groupedMessagesIdx[modelIdx] !== undefined && groupedMessages[modelIdx].messages.length > 0}
|
||||
{#if parentMessage}
|
||||
<div>
|
||||
<div
|
||||
class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
|
||||
id="responses-container-{chatId}-{parentMessage.id}"
|
||||
>
|
||||
{#each Object.keys(groupedMessageIds) as modelIdx}
|
||||
{#if groupedMessageIdsIdx[modelIdx] !== undefined && groupedMessageIds[modelIdx].messageIds.length > 0}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
{@const message = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]]}
|
||||
{@const _messageId =
|
||||
groupedMessageIds[modelIdx].messageIds[groupedMessageIdsIdx[modelIdx]]}
|
||||
|
||||
<div
|
||||
class=" snap-center w-full max-w-full m-1 border {history.messages[currentMessageId]
|
||||
class=" snap-center w-full max-w-full m-1 border {history.messages[messageId]
|
||||
?.modelIdx == modelIdx
|
||||
? `border-gray-100 dark:border-gray-800 border-[1.5px] ${
|
||||
$mobile ? 'min-w-full' : 'min-w-[32rem]'
|
||||
@@ -162,17 +185,13 @@
|
||||
$mobile ? 'min-w-full' : 'min-w-80'
|
||||
}`} transition-all p-5 rounded-2xl"
|
||||
on:click={() => {
|
||||
if (currentMessageId != message.id) {
|
||||
currentMessageId = message.id;
|
||||
let messageId = message.id;
|
||||
console.log(messageId);
|
||||
//
|
||||
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
if (messageId != _messageId) {
|
||||
let messageChildrenIds = history.messages[_messageId].childrenIds;
|
||||
while (messageChildrenIds.length !== 0) {
|
||||
messageId = messageChildrenIds.at(-1);
|
||||
messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
messageChildrenIds = history.messages[_messageId].childrenIds;
|
||||
}
|
||||
history.currentId = messageId;
|
||||
history.currentId = _messageId;
|
||||
dispatch('change');
|
||||
}
|
||||
}}
|
||||
@@ -180,95 +199,92 @@
|
||||
{#key history.currentId}
|
||||
{#if message}
|
||||
<ResponseMessage
|
||||
{message}
|
||||
siblings={groupedMessages[modelIdx].messages.map((m) => m.id)}
|
||||
{history}
|
||||
messageId={_messageId}
|
||||
isLastMessage={true}
|
||||
{updateChatMessages}
|
||||
{confirmEditResponseMessage}
|
||||
siblings={groupedMessageIds[modelIdx].messageIds}
|
||||
showPreviousMessage={() => showPreviousMessage(modelIdx)}
|
||||
showNextMessage={() => showNextMessage(modelIdx)}
|
||||
{readOnly}
|
||||
{rateMessage}
|
||||
{copyToClipboard}
|
||||
{continueGeneration}
|
||||
{editMessage}
|
||||
{continueResponse}
|
||||
regenerateResponse={async (message) => {
|
||||
regenerateResponse(message);
|
||||
await tick();
|
||||
groupedMessagesIdx[modelIdx] = groupedMessages[modelIdx].messages.length - 1;
|
||||
groupedMessageIdsIdx[modelIdx] =
|
||||
groupedMessageIds[modelIdx].messageIds.length - 1;
|
||||
}}
|
||||
on:action={async (e) => {
|
||||
dispatch('action', e.detail);
|
||||
}}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
|
||||
const message = e.detail;
|
||||
history.messages[message.id] = message;
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
on:update={async (e) => {
|
||||
dispatch('update', e.detail);
|
||||
}}
|
||||
on:save={async (e) => {
|
||||
dispatch('save', e.detail);
|
||||
}}
|
||||
{readOnly}
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !readOnly && isLastMessage}
|
||||
{#if !Object.keys(groupedMessages).find((modelIdx) => {
|
||||
const { messages } = groupedMessages[modelIdx];
|
||||
return !messages[groupedMessagesIdx[modelIdx]]?.done ?? false;
|
||||
})}
|
||||
<div class="flex justify-end">
|
||||
<div class="w-full">
|
||||
{#if history.messages[currentMessageId]?.merged?.status}
|
||||
{@const message = history.messages[currentMessageId]?.merged}
|
||||
{#if !readOnly && isLastMessage}
|
||||
{#if !Object.keys(groupedMessageIds).find((modelIdx) => {
|
||||
const { messageIds } = groupedMessageIds[modelIdx];
|
||||
const _messageId = messageIds[groupedMessageIdsIdx[modelIdx]];
|
||||
return !history.messages[_messageId]?.done ?? false;
|
||||
})}
|
||||
<div class="flex justify-end">
|
||||
<div class="w-full">
|
||||
{#if history.messages[messageId]?.merged?.status}
|
||||
{@const message = history.messages[messageId]?.merged}
|
||||
|
||||
<div class="w-full rounded-xl pl-5 pr-2 py-2">
|
||||
<Name>
|
||||
Merged Response
|
||||
<div class="w-full rounded-xl pl-5 pr-2 py-2">
|
||||
<Name>
|
||||
Merged Response
|
||||
|
||||
{#if message.timestamp}
|
||||
<span
|
||||
class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
|
||||
>
|
||||
{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
|
||||
</span>
|
||||
{/if}
|
||||
</Name>
|
||||
{#if message.timestamp}
|
||||
<span
|
||||
class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
|
||||
>
|
||||
{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
|
||||
</span>
|
||||
{/if}
|
||||
</Name>
|
||||
|
||||
<div class="mt-1 markdown-prose w-full min-w-full">
|
||||
{#if (message?.content ?? '') === ''}
|
||||
<Skeleton />
|
||||
{:else}
|
||||
<Markdown id={`merged`} content={message.content ?? ''} />
|
||||
{/if}
|
||||
<div class="mt-1 markdown-prose w-full min-w-full">
|
||||
{#if (message?.content ?? '') === ''}
|
||||
<Skeleton />
|
||||
{:else}
|
||||
<Markdown id={`merged`} content={message.content ?? ''} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class=" flex-shrink-0 text-gray-600 dark:text-gray-500 mt-1">
|
||||
<Tooltip content={$i18n.t('Merge Responses')} placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
id="merge-response-button"
|
||||
class="{true
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={() => {
|
||||
mergeResponsesHandler();
|
||||
}}
|
||||
>
|
||||
<Merge className=" size-5 " />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div class=" flex-shrink-0 text-gray-600 dark:text-gray-500 mt-1">
|
||||
<Tooltip content={$i18n.t('Merge Responses')} placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
id="merge-response-button"
|
||||
class="{true
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={() => {
|
||||
mergeResponsesHandler();
|
||||
}}
|
||||
>
|
||||
<Merge className=" size-5 " />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { settings } from '$lib/stores';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import ProfileImageBase from './ProfileImageBase.svelte';
|
||||
|
||||
export let className = 'size-8';
|
||||
|
||||
export let src = '/user.png';
|
||||
export let src = '';
|
||||
</script>
|
||||
|
||||
<div class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}>
|
||||
<img
|
||||
crossorigin="anonymous"
|
||||
src={src.startsWith(WEBUI_BASE_URL) ||
|
||||
src.startsWith('https://www.gravatar.com/avatar/') ||
|
||||
src.startsWith('data:') ||
|
||||
src.startsWith('/')
|
||||
? src
|
||||
: `/user.png`}
|
||||
class=" {className} object-cover rounded-full -translate-y-[1px]"
|
||||
alt="profile"
|
||||
draggable="false"
|
||||
/>
|
||||
<ProfileImageBase {src} {className} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
export let className = 'size-8';
|
||||
export let src = `${WEBUI_BASE_URL}/static/favicon.png`;
|
||||
</script>
|
||||
|
||||
<img
|
||||
crossorigin="anonymous"
|
||||
src={src === ''
|
||||
? `${WEBUI_BASE_URL}/static/favicon.png`
|
||||
: src.startsWith(WEBUI_BASE_URL) ||
|
||||
src.startsWith('https://www.gravatar.com/avatar/') ||
|
||||
src.startsWith('data:') ||
|
||||
src.startsWith('/')
|
||||
? src
|
||||
: `/user.png`}
|
||||
class=" {className} object-cover rounded-full -translate-y-[1px]"
|
||||
alt="profile"
|
||||
draggable="false"
|
||||
/>
|
||||
@@ -7,9 +7,8 @@
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let messageId = null;
|
||||
export let show = false;
|
||||
export let message;
|
||||
export let show = false;
|
||||
|
||||
let LIKE_REASONS = [];
|
||||
let DISLIKE_REASONS = [];
|
||||
@@ -54,8 +53,10 @@
|
||||
const submitHandler = () => {
|
||||
console.log('submitHandler');
|
||||
|
||||
message.annotation.reason = selectedReason;
|
||||
message.annotation.comment = comment;
|
||||
if (!selectedReason) {
|
||||
toast.error($i18n.t('Please select a reason'));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch('submit', {
|
||||
reason: selectedReason,
|
||||
@@ -69,7 +70,7 @@
|
||||
|
||||
<div
|
||||
class=" my-2.5 rounded-xl px-4 py-3 border dark:border-gray-850"
|
||||
id="message-feedback-{messageId}"
|
||||
id="message-feedback-{message.id}"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class=" text-sm">{$i18n.t('Tell us more:')}</div>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import { synthesizeOpenAISpeech } from '$lib/apis/audio';
|
||||
import { imageGenerations } from '$lib/apis/images';
|
||||
import {
|
||||
copyToClipboard as _copyToClipboard,
|
||||
approximateToHumanReadable,
|
||||
extractParagraphsForAudio,
|
||||
extractSentencesForAudio,
|
||||
@@ -76,23 +77,30 @@
|
||||
annotation?: { type: string; rating: number };
|
||||
}
|
||||
|
||||
export let message: MessageType;
|
||||
export let history;
|
||||
export let messageId;
|
||||
|
||||
let message: MessageType = JSON.parse(JSON.stringify(history.messages[messageId]));
|
||||
$: if (history.messages) {
|
||||
if (JSON.stringify(message) !== JSON.stringify(history.messages[messageId])) {
|
||||
message = JSON.parse(JSON.stringify(history.messages[messageId]));
|
||||
}
|
||||
}
|
||||
|
||||
export let siblings;
|
||||
|
||||
export let isLastMessage = true;
|
||||
|
||||
export let readOnly = false;
|
||||
|
||||
export let updateChatMessages: Function;
|
||||
export let confirmEditResponseMessage: Function;
|
||||
export let showPreviousMessage: Function;
|
||||
export let showNextMessage: Function;
|
||||
|
||||
export let editMessage: Function;
|
||||
export let rateMessage: Function;
|
||||
|
||||
export let copyToClipboard: Function;
|
||||
export let continueGeneration: Function;
|
||||
export let continueResponse: Function;
|
||||
export let regenerateResponse: Function;
|
||||
|
||||
export let isLastMessage = true;
|
||||
export let readOnly = false;
|
||||
|
||||
let model = null;
|
||||
$: model = $models.find((m) => m.id === message.model);
|
||||
|
||||
@@ -109,6 +117,13 @@
|
||||
|
||||
let showRateComment = false;
|
||||
|
||||
const copyToClipboard = async (text) => {
|
||||
const res = await _copyToClipboard(text);
|
||||
if (res) {
|
||||
toast.success($i18n.t('Copying to clipboard was successful!'));
|
||||
}
|
||||
};
|
||||
|
||||
const playAudio = (idx: number) => {
|
||||
return new Promise<void>((res) => {
|
||||
speakingIdx = idx;
|
||||
@@ -202,6 +217,8 @@
|
||||
const blob = await res.blob();
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const audio = new Audio(blobUrl);
|
||||
audio.playbackRate = $settings.audio?.tts?.playbackRate ?? 1;
|
||||
|
||||
audioParts[idx] = audio;
|
||||
loadingSpeech = false;
|
||||
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
|
||||
@@ -224,6 +241,7 @@
|
||||
console.log(voice);
|
||||
|
||||
const speak = new SpeechSynthesisUtterance(message.content);
|
||||
speak.rate = $settings.audio?.tts?.playbackRate ?? 1;
|
||||
|
||||
console.log(speak);
|
||||
|
||||
@@ -255,11 +273,16 @@
|
||||
};
|
||||
|
||||
const editMessageConfirmHandler = async () => {
|
||||
if (editedContent === '') {
|
||||
editedContent = ' ';
|
||||
}
|
||||
editMessage(message.id, editedContent ? editedContent : '', false);
|
||||
|
||||
confirmEditResponseMessage(message.id, editedContent);
|
||||
edit = false;
|
||||
editedContent = '';
|
||||
|
||||
await tick();
|
||||
};
|
||||
|
||||
const saveAsCopyHandler = async () => {
|
||||
editMessage(message.id, editedContent ? editedContent : '');
|
||||
|
||||
edit = false;
|
||||
editedContent = '';
|
||||
@@ -281,12 +304,12 @@
|
||||
console.log(res);
|
||||
|
||||
if (res) {
|
||||
message.files = res.map((image) => ({
|
||||
const files = res.map((image) => ({
|
||||
type: 'image',
|
||||
url: `${image.url}`
|
||||
}));
|
||||
|
||||
dispatch('save', message);
|
||||
dispatch('save', { ...message, files: files });
|
||||
}
|
||||
|
||||
generatingImage = false;
|
||||
@@ -299,6 +322,8 @@
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
console.log('ResponseMessage mounted');
|
||||
|
||||
await tick();
|
||||
});
|
||||
</script>
|
||||
@@ -333,7 +358,7 @@
|
||||
{#each message.files as file}
|
||||
<div>
|
||||
{#if file.type === 'image'}
|
||||
<Image src={file.url} />
|
||||
<Image src={file.url} alt={message.content} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
@@ -399,31 +424,45 @@
|
||||
const isEnterPressed = e.key === 'Enter';
|
||||
|
||||
if (isCmdOrCtrlPressed && isEnterPressed) {
|
||||
document.getElementById('save-edit-message-button')?.click();
|
||||
document.getElementById('confirm-edit-message-button')?.click();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
|
||||
<button
|
||||
id="close-edit-message-button"
|
||||
class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl"
|
||||
on:click={() => {
|
||||
cancelEditMessage();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Cancel')}
|
||||
</button>
|
||||
<div class=" mt-2 mb-1 flex justify-between text-sm font-medium">
|
||||
<div>
|
||||
<button
|
||||
id="save-new-message-button"
|
||||
class=" px-4 py-2 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 border dark:border-gray-700 text-gray-700 dark:text-gray-200 transition rounded-3xl"
|
||||
on:click={() => {
|
||||
saveAsCopyHandler();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Save As Copy')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
id="save-edit-message-button"
|
||||
class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl"
|
||||
on:click={() => {
|
||||
editMessageConfirmHandler();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
<div class="flex space-x-1.5">
|
||||
<button
|
||||
id="close-edit-message-button"
|
||||
class="px-4 py-2 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl"
|
||||
on:click={() => {
|
||||
cancelEditMessage();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Cancel')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="confirm-edit-message-button"
|
||||
class=" px-4 py-2 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl"
|
||||
on:click={() => {
|
||||
editMessageConfirmHandler();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -881,7 +920,7 @@
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={() => {
|
||||
continueGeneration();
|
||||
continueResponse();
|
||||
|
||||
(model?.actions ?? [])
|
||||
.filter((action) => action?.__webui__ ?? false)
|
||||
@@ -996,12 +1035,17 @@
|
||||
|
||||
{#if message.done && showRateComment}
|
||||
<RateComment
|
||||
messageId={message.id}
|
||||
bind:show={showRateComment}
|
||||
bind:message
|
||||
bind:show={showRateComment}
|
||||
on:submit={(e) => {
|
||||
updateChatMessages();
|
||||
|
||||
dispatch('save', {
|
||||
...message,
|
||||
annotation: {
|
||||
...message.annotation,
|
||||
comment: e.detail.comment,
|
||||
reason: e.detail.reason
|
||||
}
|
||||
});
|
||||
(model?.actions ?? [])
|
||||
.filter((action) => action?.__webui__ ?? false)
|
||||
.forEach((action) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="w-full mt-2 mb-4">
|
||||
<div class="w-full mt-2 mb-2">
|
||||
<div class="animate-pulse flex w-full">
|
||||
<div class="space-y-2 w-full">
|
||||
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded mr-14" />
|
||||
|
||||
@@ -1,38 +1,58 @@
|
||||
<script lang="ts">
|
||||
import dayjs from 'dayjs';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { tick, createEventDispatcher, getContext, onMount } from 'svelte';
|
||||
|
||||
import { models, settings } from '$lib/stores';
|
||||
import { user as _user } from '$lib/stores';
|
||||
import {
|
||||
copyToClipboard as _copyToClipboard,
|
||||
processResponseContent,
|
||||
replaceTokens
|
||||
} from '$lib/utils';
|
||||
|
||||
import { tick, createEventDispatcher, getContext } from 'svelte';
|
||||
import Name from './Name.svelte';
|
||||
import ProfileImage from './ProfileImage.svelte';
|
||||
import { models, settings } from '$lib/stores';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
import { user as _user } from '$lib/stores';
|
||||
import { getFileContentById } from '$lib/apis/files';
|
||||
import FileItem from '$lib/components/common/FileItem.svelte';
|
||||
import { marked } from 'marked';
|
||||
import { processResponseContent, replaceTokens } from '$lib/utils';
|
||||
import MarkdownTokens from './Markdown/MarkdownTokens.svelte';
|
||||
import Markdown from './Markdown.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let user;
|
||||
export let message;
|
||||
export let siblings;
|
||||
export let isFirstMessage: boolean;
|
||||
export let readOnly: boolean;
|
||||
|
||||
export let confirmEditMessage: Function;
|
||||
export let history;
|
||||
export let messageId;
|
||||
|
||||
export let siblings;
|
||||
|
||||
export let showPreviousMessage: Function;
|
||||
export let showNextMessage: Function;
|
||||
export let copyToClipboard: Function;
|
||||
|
||||
export let editMessage: Function;
|
||||
|
||||
export let isFirstMessage: boolean;
|
||||
export let readOnly: boolean;
|
||||
|
||||
let edit = false;
|
||||
let editedContent = '';
|
||||
let messageEditTextAreaElement: HTMLTextAreaElement;
|
||||
|
||||
let message = JSON.parse(JSON.stringify(history.messages[messageId]));
|
||||
$: if (history.messages) {
|
||||
if (JSON.stringify(message) !== JSON.stringify(history.messages[messageId])) {
|
||||
message = JSON.parse(JSON.stringify(history.messages[messageId]));
|
||||
}
|
||||
}
|
||||
|
||||
const copyToClipboard = async (text) => {
|
||||
const res = await _copyToClipboard(text);
|
||||
if (res) {
|
||||
toast.success($i18n.t('Copying to clipboard was successful!'));
|
||||
}
|
||||
};
|
||||
|
||||
const editMessageHandler = async () => {
|
||||
edit = true;
|
||||
editedContent = message.content;
|
||||
@@ -46,7 +66,7 @@
|
||||
};
|
||||
|
||||
const editMessageConfirmHandler = async (submit = true) => {
|
||||
confirmEditMessage(message.id, editedContent, submit);
|
||||
editMessage(message.id, editedContent, submit);
|
||||
|
||||
edit = false;
|
||||
editedContent = '';
|
||||
@@ -60,9 +80,13 @@
|
||||
const deleteMessageHandler = async () => {
|
||||
dispatch('delete', message.id);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
console.log('UserMessage mounted');
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class=" flex w-full user-message" dir={$settings.chatDirection}>
|
||||
<div class=" flex w-full user-message" dir={$settings.chatDirection} id="message-{message.id}">
|
||||
{#if !($settings?.chatBubble ?? true)}
|
||||
<ProfileImage
|
||||
src={message.user
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
<script lang="ts">
|
||||
import { getContext, createEventDispatcher, onDestroy } from 'svelte';
|
||||
import { useSvelteFlow, useNodesInitialized, useStore } from '@xyflow/svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { onMount, tick } from 'svelte';
|
||||
|
||||
import { writable } from 'svelte/store';
|
||||
import { models, showOverview, theme, user } from '$lib/stores';
|
||||
|
||||
import '@xyflow/svelte/dist/style.css';
|
||||
|
||||
import CustomNode from './Overview/Node.svelte';
|
||||
import Flow from './Overview/Flow.svelte';
|
||||
import XMark from '../icons/XMark.svelte';
|
||||
|
||||
const { width, height } = useStore();
|
||||
|
||||
const { fitView, getViewport } = useSvelteFlow();
|
||||
const nodesInitialized = useNodesInitialized();
|
||||
|
||||
export let history;
|
||||
|
||||
let selectedMessageId = null;
|
||||
|
||||
const nodes = writable([]);
|
||||
const edges = writable([]);
|
||||
|
||||
const nodeTypes = {
|
||||
custom: CustomNode
|
||||
};
|
||||
|
||||
$: if (history) {
|
||||
drawFlow();
|
||||
}
|
||||
|
||||
$: if (history && history.currentId) {
|
||||
focusNode();
|
||||
selectedMessageId = null;
|
||||
}
|
||||
|
||||
const focusNode = async () => {
|
||||
if (selectedMessageId === null) {
|
||||
await fitView({ nodes: [{ id: history.currentId }] });
|
||||
}
|
||||
};
|
||||
|
||||
const drawFlow = async () => {
|
||||
const nodeList = [];
|
||||
const edgeList = [];
|
||||
const levelOffset = 150; // Vertical spacing between layers
|
||||
const siblingOffset = 250; // Horizontal spacing between nodes at the same layer
|
||||
|
||||
// Map to keep track of node positions at each level
|
||||
let positionMap = new Map();
|
||||
|
||||
// Helper function to truncate labels
|
||||
function createLabel(content) {
|
||||
const maxLength = 100;
|
||||
return content.length > maxLength ? content.substr(0, maxLength) + '...' : content;
|
||||
}
|
||||
|
||||
// Create nodes and map children to ensure alignment in width
|
||||
let layerWidths = {}; // Track widths of each layer
|
||||
|
||||
Object.keys(history.messages).forEach((id) => {
|
||||
const message = history.messages[id];
|
||||
const level = message.parentId ? (positionMap.get(message.parentId)?.level ?? -1) + 1 : 0;
|
||||
if (!layerWidths[level]) layerWidths[level] = 0;
|
||||
|
||||
positionMap.set(id, {
|
||||
id: message.id,
|
||||
level,
|
||||
position: layerWidths[level]++
|
||||
});
|
||||
});
|
||||
|
||||
// Adjust positions based on siblings count to centralize vertical spacing
|
||||
Object.keys(history.messages).forEach((id) => {
|
||||
const pos = positionMap.get(id);
|
||||
const xOffset = pos.position * siblingOffset;
|
||||
const y = pos.level * levelOffset;
|
||||
const x = xOffset;
|
||||
|
||||
nodeList.push({
|
||||
id: pos.id,
|
||||
type: 'custom',
|
||||
data: {
|
||||
user: $user,
|
||||
message: history.messages[id],
|
||||
model: $models.find((model) => model.id === history.messages[id].model)
|
||||
},
|
||||
position: { x, y }
|
||||
});
|
||||
|
||||
// Create edges
|
||||
const parentId = history.messages[id].parentId;
|
||||
if (parentId) {
|
||||
edgeList.push({
|
||||
id: parentId + '-' + pos.id,
|
||||
source: parentId,
|
||||
target: pos.id,
|
||||
selectable: false,
|
||||
class: ' dark:fill-gray-300 fill-gray-300',
|
||||
type: 'smoothstep',
|
||||
animated: history.currentId === id || recurseCheckChild(id, history.currentId)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await edges.set([...edgeList]);
|
||||
await nodes.set([...nodeList]);
|
||||
};
|
||||
|
||||
const recurseCheckChild = (nodeId, currentId) => {
|
||||
const node = history.messages[nodeId];
|
||||
return (
|
||||
node.childrenIds &&
|
||||
node.childrenIds.some((id) => id === currentId || recurseCheckChild(id, currentId))
|
||||
);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
drawFlow();
|
||||
|
||||
nodesInitialized.subscribe(async (initialized) => {
|
||||
if (initialized) {
|
||||
await tick();
|
||||
const res = await fitView({ nodes: [{ id: history.currentId }] });
|
||||
}
|
||||
});
|
||||
|
||||
width.subscribe((value) => {
|
||||
if (value) {
|
||||
// fitView();
|
||||
fitView({ nodes: [{ id: history.currentId }] });
|
||||
}
|
||||
});
|
||||
|
||||
height.subscribe((value) => {
|
||||
if (value) {
|
||||
// fitView();
|
||||
fitView({ nodes: [{ id: history.currentId }] });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
console.log('Overview destroyed');
|
||||
|
||||
nodes.set([]);
|
||||
edges.set([]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full relative">
|
||||
<div class=" absolute z-50 w-full flex justify-between dark:text-gray-100 px-5 py-4">
|
||||
<div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Overview')}</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
dispatch('close');
|
||||
showOverview.set(false);
|
||||
}}
|
||||
>
|
||||
<XMark className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if $nodes.length > 0}
|
||||
<Flow
|
||||
{nodes}
|
||||
{nodeTypes}
|
||||
{edges}
|
||||
on:nodeclick={(e) => {
|
||||
console.log(e.detail.node.data);
|
||||
dispatch('nodeclick', e.detail);
|
||||
selectedMessageId = e.detail.node.data.message.id;
|
||||
fitView({ nodes: [{ id: selectedMessageId }] });
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { theme } from '$lib/stores';
|
||||
import { Background, Controls, SvelteFlow, BackgroundVariant } from '@xyflow/svelte';
|
||||
|
||||
export let nodes;
|
||||
export let nodeTypes;
|
||||
export let edges;
|
||||
</script>
|
||||
|
||||
<SvelteFlow
|
||||
{nodes}
|
||||
{nodeTypes}
|
||||
{edges}
|
||||
fitView
|
||||
minZoom={0.001}
|
||||
colorMode={$theme.includes('dark')
|
||||
? 'dark'
|
||||
: $theme === 'system'
|
||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
: 'light'}
|
||||
nodesConnectable={false}
|
||||
nodesDraggable={false}
|
||||
on:nodeclick={(e) => dispatch('nodeclick', e.detail)}
|
||||
oninit={() => {
|
||||
console.log('Flow initialized');
|
||||
}}
|
||||
>
|
||||
<Controls showLock={false} />
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
</SvelteFlow>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
|
||||
|
||||
import ProfileImageBase from '../Messages/ProfileImageBase.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Heart from '$lib/components/icons/Heart.svelte';
|
||||
|
||||
type $$Props = NodeProps;
|
||||
export let data: $$Props['data'];
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="px-4 py-3 shadow-md rounded-xl dark:bg-black bg-white border dark:border-gray-900 w-60 h-20 group"
|
||||
>
|
||||
<Tooltip
|
||||
content={data?.message?.error ? data.message.error.content : data.message.content}
|
||||
class="w-full"
|
||||
allowHTML={false}
|
||||
>
|
||||
{#if data.message.role === 'user'}
|
||||
<div class="flex w-full">
|
||||
<ProfileImageBase
|
||||
src={data.user?.profile_image_url ?? '/user.png'}
|
||||
className={'size-5 -translate-y-[1px]'}
|
||||
/>
|
||||
<div class="ml-2">
|
||||
<div class=" flex justify-between items-center">
|
||||
<div class="text-xs text-black dark:text-white font-medium line-clamp-1">
|
||||
{data?.user?.name ?? 'User'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if data?.message?.error}
|
||||
<div class="text-red-500 line-clamp-2 text-xs mt-0.5">{data.message.error.content}</div>
|
||||
{:else}
|
||||
<div class="text-gray-500 line-clamp-2 text-xs mt-0.5">{data.message.content}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex w-full">
|
||||
<ProfileImageBase
|
||||
src={data?.model?.info?.meta?.profile_image_url ?? ''}
|
||||
className={'size-5 -translate-y-[1px]'}
|
||||
/>
|
||||
|
||||
<div class="ml-2">
|
||||
<div class=" flex justify-between items-center">
|
||||
<div class="text-xs text-black dark:text-white font-medium line-clamp-1">
|
||||
{data?.model?.name ?? data?.message?.model ?? 'Assistant'}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class={data?.message?.favorite ? '' : 'invisible group-hover:visible'}
|
||||
on:click={() => {
|
||||
data.message.favorite = !(data?.message?.favorite ?? false);
|
||||
}}
|
||||
>
|
||||
<Heart
|
||||
className="size-3 {data?.message?.favorite
|
||||
? 'fill-red-500 stroke-red-500'
|
||||
: 'hover:fill-red-500 hover:stroke-red-500'} "
|
||||
strokeWidth="2.5"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if data?.message?.error}
|
||||
<div class="text-red-500 line-clamp-2 text-xs mt-0.5">
|
||||
{data.message.error.content}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-gray-500 line-clamp-2 text-xs mt-0.5">{data.message.content}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Tooltip>
|
||||
<Handle type="target" position={Position.Top} class="w-2 rounded-full dark:bg-gray-900" />
|
||||
<Handle type="source" position={Position.Bottom} class="w-2 rounded-full dark:bg-gray-900" />
|
||||
</div>
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
export let params = {
|
||||
// Advanced
|
||||
stream_response: null, // Set stream responses for this model individually
|
||||
seed: null,
|
||||
stop: null,
|
||||
temperature: null,
|
||||
@@ -41,7 +42,36 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class=" space-y-1 text-xs">
|
||||
<div class=" space-y-1 text-xs pb-safe-bottom">
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Stream Chat Response')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
params.stream_response =
|
||||
(params?.stream_response ?? null) === null
|
||||
? true
|
||||
: params.stream_response
|
||||
? false
|
||||
: null;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if params.stream_response === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else if params.stream_response === false}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Seed')}</div>
|
||||
@@ -734,7 +764,7 @@
|
||||
id="steps-range"
|
||||
type="range"
|
||||
min="-2"
|
||||
max="16000"
|
||||
max="131072"
|
||||
step="1"
|
||||
bind:value={params.max_tokens}
|
||||
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
@@ -746,7 +776,6 @@
|
||||
type="number"
|
||||
class=" bg-transparent text-center w-14"
|
||||
min="-2"
|
||||
max="16000"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
let voices = [];
|
||||
let voice = '';
|
||||
|
||||
// Audio speed control
|
||||
let playbackRate = 1;
|
||||
const speedOptions = [2, 1.75, 1.5, 1.25, 1, 0.75, 0.5];
|
||||
|
||||
const getVoices = async () => {
|
||||
if ($config.audio.tts.engine === '') {
|
||||
const getVoicesLoop = setInterval(async () => {
|
||||
@@ -56,6 +60,7 @@
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
playbackRate = $settings.audio?.tts?.playbackRate ?? 1;
|
||||
conversationMode = $settings.conversationMode ?? false;
|
||||
speechAutoSend = $settings.speechAutoSend ?? false;
|
||||
responseAutoPlayback = $settings.responseAutoPlayback ?? false;
|
||||
@@ -83,6 +88,7 @@
|
||||
engine: STTEngine !== '' ? STTEngine : undefined
|
||||
},
|
||||
tts: {
|
||||
playbackRate: playbackRate,
|
||||
voice: voice !== '' ? voice : undefined,
|
||||
defaultVoice: $config?.audio?.tts?.voice ?? '',
|
||||
nonLocalVoices: $config.audio.tts.engine === '' ? nonLocalVoices : undefined
|
||||
@@ -153,6 +159,21 @@
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Speech Playback Speed')}</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={playbackRate}
|
||||
>
|
||||
{#each speedOptions as option}
|
||||
<option value={option} selected={playbackRate === option}>{option}x</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
|
||||
let params = {
|
||||
// Advanced
|
||||
stream_response: null,
|
||||
seed: null,
|
||||
temperature: null,
|
||||
frequency_penalty: null,
|
||||
@@ -114,6 +115,29 @@
|
||||
document.documentElement.classList.add(e);
|
||||
});
|
||||
|
||||
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
|
||||
if (metaThemeColor) {
|
||||
if (_theme.includes('system')) {
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
console.log('Setting system meta theme color: ' + systemTheme);
|
||||
metaThemeColor.setAttribute('content', systemTheme === 'light' ? '#ffffff' : '#171717');
|
||||
} else {
|
||||
console.log('Setting meta theme color: ' + _theme);
|
||||
metaThemeColor.setAttribute(
|
||||
'content',
|
||||
_theme === 'dark'
|
||||
? '#171717'
|
||||
: _theme === 'oled-dark'
|
||||
? '#000000'
|
||||
: _theme === 'her'
|
||||
? '#983724'
|
||||
: '#ffffff'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(_theme);
|
||||
};
|
||||
|
||||
@@ -304,6 +328,7 @@
|
||||
saveSettings({
|
||||
system: system !== '' ? system : undefined,
|
||||
params: {
|
||||
stream_response: params.stream_response !== null ? params.stream_response : undefined,
|
||||
seed: (params.seed !== null ? params.seed : undefined) ?? undefined,
|
||||
stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined,
|
||||
temperature: params.temperature !== null ? params.temperature : undefined,
|
||||
|
||||
@@ -49,20 +49,20 @@
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
bind:this={modalElement}
|
||||
class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-[9999] overflow-hidden overscroll-contain"
|
||||
class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full h-screen max-h-[100dvh] flex justify-center z-[9999] overflow-hidden overscroll-contain"
|
||||
in:fade={{ duration: 10 }}
|
||||
on:mousedown={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class=" m-auto rounded-2xl max-w-full w-[32rem] mx-2 bg-gray-50 dark:bg-gray-950 shadow-3xl border border-gray-850"
|
||||
class=" m-auto rounded-2xl max-w-full w-[32rem] mx-2 bg-gray-50 dark:bg-gray-950 max-h-[100dvh] shadow-3xl border border-gray-850"
|
||||
in:flyAndScale
|
||||
on:mousedown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div class="px-[1.75rem] py-6">
|
||||
<div class="px-[1.75rem] py-6 flex flex-col">
|
||||
<div class=" text-lg font-semibold dark:text-gray-200 mb-2.5">
|
||||
{#if title !== ''}
|
||||
{title}
|
||||
@@ -72,7 +72,7 @@
|
||||
</div>
|
||||
|
||||
<slot>
|
||||
<div class=" text-sm text-gray-500">
|
||||
<div class=" text-sm text-gray-500 flex-1">
|
||||
{#if message !== ''}
|
||||
{message}
|
||||
{:else}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { fade, fly, slide } from 'svelte/transition';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let show = false;
|
||||
export let size = 'md';
|
||||
|
||||
@@ -42,15 +44,22 @@
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else if (modalElement) {
|
||||
dispatch('close');
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
document.body.removeChild(modalElement);
|
||||
document.body.style.overflow = 'unset';
|
||||
|
||||
if (document.body.contains(modalElement)) {
|
||||
document.body.removeChild(modalElement);
|
||||
document.body.style.overflow = 'unset';
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
show = false;
|
||||
if (modalElement) {
|
||||
document.body.removeChild(modalElement);
|
||||
if (document.body.contains(modalElement)) {
|
||||
document.body.removeChild(modalElement);
|
||||
document.body.style.overflow = 'unset';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
|
||||
let previewElement = null;
|
||||
|
||||
const downloadImage = (url, filename) => {
|
||||
const downloadImage = (url, filename, prefixName = '') => {
|
||||
fetch(url)
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
const objectUrl = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = objectUrl;
|
||||
link.download = filename;
|
||||
link.download = `${prefixName}${filename}`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
@@ -87,7 +87,7 @@
|
||||
<button
|
||||
class=" p-5"
|
||||
on:click={() => {
|
||||
downloadImage(src, src.substring(src.lastIndexOf('/') + 1));
|
||||
downloadImage(src, src.substring(src.lastIndexOf('/') + 1), alt);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let loaderElement: HTMLElement;
|
||||
|
||||
let observer;
|
||||
let intervalId;
|
||||
|
||||
onMount(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
observer = new IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
dispatch('visible');
|
||||
intervalId = setInterval(() => {
|
||||
dispatch('visible');
|
||||
}, 100);
|
||||
// dispatch('visible');
|
||||
// observer.unobserve(loaderElement); // Stop observing until content is loaded
|
||||
} else {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -23,6 +31,14 @@
|
||||
|
||||
observer.observe(loaderElement);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
observer.disconnect();
|
||||
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={loaderElement}>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
import { onDestroy } from 'svelte';
|
||||
import { marked } from 'marked';
|
||||
|
||||
@@ -10,22 +12,25 @@
|
||||
export let touch = true;
|
||||
export let className = 'flex';
|
||||
export let theme = '';
|
||||
export let allowHTML = true;
|
||||
export let tippyOptions = {};
|
||||
|
||||
let tooltipElement;
|
||||
let tooltipInstance;
|
||||
|
||||
$: if (tooltipElement && content) {
|
||||
if (tooltipInstance) {
|
||||
tooltipInstance.setContent(content);
|
||||
tooltipInstance.setContent(DOMPurify.sanitize(content));
|
||||
} else {
|
||||
tooltipInstance = tippy(tooltipElement, {
|
||||
content: content,
|
||||
content: DOMPurify.sanitize(content),
|
||||
placement: placement,
|
||||
allowHTML: true,
|
||||
allowHTML: allowHTML,
|
||||
touch: touch,
|
||||
...(theme !== '' ? { theme } : { theme: 'dark' }),
|
||||
arrow: false,
|
||||
offset: [0, 4]
|
||||
offset: [0, 4],
|
||||
...tippyOptions
|
||||
});
|
||||
}
|
||||
} else if (tooltipInstance && content === '') {
|
||||
@@ -41,6 +46,6 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={tooltipElement} aria-label={content} class={className}>
|
||||
<div bind:this={tooltipElement} aria-label={DOMPurify.sanitize(content)} class={className}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let className = 'size-4';
|
||||
export let strokeWidth = '1.5';
|
||||
</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="m15 11.25-3-3m0 0-3 3m3-3v7.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let className = 'size-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="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184"
|
||||
/>
|
||||
</svg>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let className = 'size-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 6.75V15m6-6v8.25m.503 3.498 4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 0 0-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0Z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -8,7 +8,7 @@
|
||||
mobile,
|
||||
settings,
|
||||
showArchivedChats,
|
||||
showSettings,
|
||||
showControls,
|
||||
showSidebar,
|
||||
user
|
||||
} from '$lib/stores';
|
||||
@@ -22,6 +22,7 @@
|
||||
import UserMenu from './Sidebar/UserMenu.svelte';
|
||||
import MenuLines from '../icons/MenuLines.svelte';
|
||||
import AdjustmentsHorizontal from '../icons/AdjustmentsHorizontal.svelte';
|
||||
import Map from '../icons/Map.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -31,9 +32,7 @@
|
||||
|
||||
export let chat;
|
||||
export let selectedModels;
|
||||
|
||||
export let showModelSelector = true;
|
||||
export let showControls = false;
|
||||
|
||||
let showShareChatModal = false;
|
||||
let showDownloadChatModal = false;
|
||||
@@ -83,7 +82,7 @@
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="hidden md:flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||
class="flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||
id="chat-context-menu-button"
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
@@ -106,19 +105,21 @@
|
||||
</Menu>
|
||||
{/if}
|
||||
|
||||
<Tooltip content={$i18n.t('Controls')}>
|
||||
<button
|
||||
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||
on:click={() => {
|
||||
showControls = !showControls;
|
||||
}}
|
||||
aria-label="Controls"
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
<AdjustmentsHorizontal className=" size-5" strokeWidth="0.5" />
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{#if !$mobile}
|
||||
<Tooltip content={$i18n.t('Controls')}>
|
||||
<button
|
||||
class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||
on:click={async () => {
|
||||
await showControls.set(!$showControls);
|
||||
}}
|
||||
aria-label="Controls"
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
<AdjustmentsHorizontal className=" size-5" strokeWidth="0.5" />
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<Tooltip content={$i18n.t('New Chat')}>
|
||||
<button
|
||||
|
||||
@@ -5,13 +5,19 @@
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { showSettings } from '$lib/stores';
|
||||
import { downloadChatAsPDF } from '$lib/apis/utils';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
import { showOverview, showControls, mobile } from '$lib/stores';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import Tags from '$lib/components/chat/Tags.svelte';
|
||||
|
||||
import { downloadChatAsPDF } from '$lib/apis/utils';
|
||||
import Map from '$lib/components/icons/Map.svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import Clipboard from '$lib/components/icons/Clipboard.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import AdjustmentsHorizontal from '$lib/components/icons/AdjustmentsHorizontal.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -24,19 +30,23 @@
|
||||
export let chat;
|
||||
export let onClose: Function = () => {};
|
||||
|
||||
const downloadTxt = async () => {
|
||||
const getChatAsText = async () => {
|
||||
const _chat = chat.chat;
|
||||
console.log('download', chat);
|
||||
|
||||
const chatText = _chat.messages.reduce((a, message, i, arr) => {
|
||||
return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
|
||||
}, '');
|
||||
|
||||
return chatText.trim();
|
||||
};
|
||||
|
||||
const downloadTxt = async () => {
|
||||
const chatText = await getChatAsText();
|
||||
|
||||
let blob = new Blob([chatText], {
|
||||
type: 'text/plain'
|
||||
});
|
||||
|
||||
saveAs(blob, `chat-${_chat.title}.txt`);
|
||||
saveAs(blob, `chat-${chat.chat.title}.txt`);
|
||||
};
|
||||
|
||||
const downloadPdf = async () => {
|
||||
@@ -117,6 +127,48 @@
|
||||
<div class="flex items-center">{$i18n.t('Settings')}</div>
|
||||
</DropdownMenu.Item> -->
|
||||
|
||||
{#if $mobile}
|
||||
<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-controls-button"
|
||||
on:click={async () => {
|
||||
await showControls.set(true);
|
||||
}}
|
||||
>
|
||||
<AdjustmentsHorizontal className=" size-4" strokeWidth="0.5" />
|
||||
<div class="flex items-center">{$i18n.t('Controls')}</div>
|
||||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
|
||||
<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-overview-button"
|
||||
on:click={async () => {
|
||||
await showControls.set(true);
|
||||
await showOverview.set(true);
|
||||
}}
|
||||
>
|
||||
<Map className=" size-4" strokeWidth="1.5" />
|
||||
<div class="flex items-center">{$i18n.t('Overview')}</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-copy-button"
|
||||
on:click={async () => {
|
||||
const res = await copyToClipboard(await getChatAsText()).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Copied to clipboard'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Clipboard className=" size-4" strokeWidth="1.5" />
|
||||
<div class="flex items-center">{$i18n.t('Copy')}</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"
|
||||
@@ -138,12 +190,7 @@
|
||||
</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={() => {
|
||||
downloadHandler();
|
||||
}}
|
||||
/> -->
|
||||
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger
|
||||
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"
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
let showDeleteConfirm = false;
|
||||
let showDropdown = false;
|
||||
|
||||
let selectedTagName = null;
|
||||
|
||||
let filteredChatList = [];
|
||||
|
||||
// Pagination variables
|
||||
@@ -240,7 +242,7 @@
|
||||
deleteChatHandler(deleteChat.id);
|
||||
}}
|
||||
>
|
||||
<div class=" text-sm text-gray-500">
|
||||
<div class=" text-sm text-gray-500 flex-1 line-clamp-3">
|
||||
{$i18n.t('This will delete')} <span class=" font-semibold">{deleteChat.title}</span>.
|
||||
</div>
|
||||
</DeleteConfirmDialog>
|
||||
@@ -380,11 +382,13 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="relative flex flex-col flex-1 overflow-y-auto">
|
||||
<div
|
||||
class="relative flex flex-col flex-1 overflow-y-auto {$temporaryChatEnabled
|
||||
? 'opacity-20'
|
||||
: ''}"
|
||||
>
|
||||
{#if $temporaryChatEnabled}
|
||||
<div
|
||||
class="absolute z-40 w-full h-full bg-gray-50/90 dark:bg-black/90 flex justify-center"
|
||||
></div>
|
||||
<div class="absolute z-40 w-full h-full flex justify-center"></div>
|
||||
{/if}
|
||||
|
||||
<div class="px-2 mt-0.5 mb-2 flex justify-center space-x-2">
|
||||
@@ -419,10 +423,13 @@
|
||||
</div>
|
||||
|
||||
{#if $tags.filter((t) => t.name !== 'pinned').length > 0}
|
||||
<div class="px-2.5 mb-2 flex gap-1 flex-wrap">
|
||||
<div class="px-3.5 mb-1 flex gap-0.5 flex-wrap">
|
||||
<button
|
||||
class="px-2.5 text-xs font-medium bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
|
||||
class="px-2.5 py-[1px] text-xs transition {selectedTagName === null
|
||||
? 'bg-gray-100 dark:bg-gray-900'
|
||||
: ' '} rounded-md font-medium"
|
||||
on:click={async () => {
|
||||
selectedTagName = null;
|
||||
await enablePagination();
|
||||
}}
|
||||
>
|
||||
@@ -430,8 +437,11 @@
|
||||
</button>
|
||||
{#each $tags.filter((t) => t.name !== 'pinned') as tag}
|
||||
<button
|
||||
class="px-2.5 text-xs font-medium bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
|
||||
class="px-2.5 py-[1px] text-xs transition {selectedTagName === tag.name
|
||||
? 'bg-gray-100 dark:bg-gray-900'
|
||||
: ''} rounded-md font-medium"
|
||||
on:click={async () => {
|
||||
selectedTagName = tag.name;
|
||||
scrollPaginationEnabled.set(false);
|
||||
let chatIds = await getChatListByTagName(localStorage.token, tag.name);
|
||||
if (chatIds.length === 0) {
|
||||
@@ -451,7 +461,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $pinnedChats.length > 0}
|
||||
{#if !search && $pinnedChats.length > 0}
|
||||
<div class="pl-2 py-2 flex flex-col space-y-1">
|
||||
<div class="">
|
||||
<div class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium pb-1.5">
|
||||
@@ -551,7 +561,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-2.5">
|
||||
<div class="px-2.5 pb-safe-bottom">
|
||||
<!-- <hr class=" border-gray-900 mb-1 w-full" /> -->
|
||||
|
||||
<div class="flex flex-col font-primary">
|
||||
|
||||
@@ -14,7 +14,15 @@
|
||||
getChatListByTagName,
|
||||
updateChatById
|
||||
} from '$lib/apis/chats';
|
||||
import { chatId, chats, mobile, pinnedChats, showSidebar, currentChatPage } from '$lib/stores';
|
||||
import {
|
||||
chatId,
|
||||
chatTitle as _chatTitle,
|
||||
chats,
|
||||
mobile,
|
||||
pinnedChats,
|
||||
showSidebar,
|
||||
currentChatPage
|
||||
} from '$lib/stores';
|
||||
|
||||
import ChatMenu from './ChatMenu.svelte';
|
||||
import ShareChatModal from '$lib/components/chat/ShareChatModal.svelte';
|
||||
@@ -33,14 +41,18 @@
|
||||
|
||||
let chatTitle = chat.title;
|
||||
|
||||
const editChatTitle = async (id, _title) => {
|
||||
if (_title === '') {
|
||||
const editChatTitle = async (id, title) => {
|
||||
if (title === '') {
|
||||
toast.error($i18n.t('Title cannot be an empty string.'));
|
||||
} else {
|
||||
await updateChatById(localStorage.token, id, {
|
||||
title: _title
|
||||
title: title
|
||||
});
|
||||
|
||||
if (id === $chatId) {
|
||||
_chatTitle.set(title);
|
||||
}
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { goto } from '$app/navigation';
|
||||
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
||||
import { showSettings, activeUserCount, USAGE_POOL } from '$lib/stores';
|
||||
import { showSettings, activeUserCount, USAGE_POOL, mobile, showSidebar } from '$lib/stores';
|
||||
import { fade, slide } from 'svelte/transition';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
on:click={async () => {
|
||||
await showSettings.set(true);
|
||||
show = false;
|
||||
|
||||
if ($mobile) {
|
||||
showSidebar.set(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
@@ -72,6 +76,10 @@
|
||||
on:click={() => {
|
||||
dispatch('show', 'archived-chat');
|
||||
show = false;
|
||||
|
||||
if ($mobile) {
|
||||
showSidebar.set(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
@@ -86,6 +94,10 @@
|
||||
on:click={() => {
|
||||
goto('/playground');
|
||||
show = false;
|
||||
|
||||
if ($mobile) {
|
||||
showSidebar.set(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
@@ -112,6 +124,10 @@
|
||||
on:click={() => {
|
||||
goto('/admin');
|
||||
show = false;
|
||||
|
||||
if ($mobile) {
|
||||
showSidebar.set(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { WEBUI_VERSION } from '$lib/constants';
|
||||
import XMark from '../icons/XMark.svelte';
|
||||
|
||||
export let version = {
|
||||
current: WEBUI_VERSION,
|
||||
latest: WEBUI_VERSION
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex items-start bg-[#F1F8FE] dark:bg-[#020C1D] border border-[3371D5] dark:border-[#03113B] text-[#3371D5] dark:text-[#6795EC] rounded-lg px-3.5 py-3 text-xs max-w-80 pr-2 w-full shadow-lg"
|
||||
>
|
||||
<div class="flex-1 font-medium">
|
||||
{$i18n.t(`A new version (v{{LATEST_VERSION}}) is now available.`, {
|
||||
LATEST_VERSION: version.latest
|
||||
})}
|
||||
|
||||
<a
|
||||
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>
|
||||
{$i18n.t('Update for the latest features and improvements.')}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class=" flex-shrink-0 pl-2">
|
||||
<button
|
||||
class=" hover:text-blue-900 dark:hover:text-blue-300 transition"
|
||||
on:click={() => {
|
||||
console.log('closeToast');
|
||||
localStorage.setItem('dismissedUpdateToast', Date.now().toString());
|
||||
dispatch('closeToast');
|
||||
}}
|
||||
>
|
||||
<XMark />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,7 +55,7 @@
|
||||
const uploadDoc = async (file, tags?: object) => {
|
||||
console.log(file);
|
||||
// Check if the file is an audio file and transcribe/convert it to text file
|
||||
if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
|
||||
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
|
||||
const transcribeRes = await transcribeAudio(localStorage.token, file).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
@@ -215,7 +215,11 @@
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class=" text-lg font-semibold self-center">{$i18n.t('Documents')}</div>
|
||||
<div class="flex md:self-center text-lg font-medium px-0.5">
|
||||
{$i18n.t('Documents')}
|
||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" />
|
||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{$documents.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { marked } from 'marked';
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
@@ -94,6 +96,58 @@
|
||||
window.addEventListener('message', messageHandler, false);
|
||||
};
|
||||
|
||||
const moveToTopHandler = async (model) => {
|
||||
// find models with position 0 and set them to 1
|
||||
const topModels = _models.filter((m) => m.info?.meta?.position === 0);
|
||||
for (const m of topModels) {
|
||||
let info = m.info;
|
||||
if (!info) {
|
||||
info = {
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
meta: {
|
||||
position: 1
|
||||
},
|
||||
params: {}
|
||||
};
|
||||
}
|
||||
|
||||
info.meta = {
|
||||
...info.meta,
|
||||
position: 1
|
||||
};
|
||||
|
||||
await updateModelById(localStorage.token, info.id, info);
|
||||
}
|
||||
|
||||
let info = model.info;
|
||||
|
||||
if (!info) {
|
||||
info = {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
meta: {
|
||||
position: 0
|
||||
},
|
||||
params: {}
|
||||
};
|
||||
}
|
||||
|
||||
info.meta = {
|
||||
...info.meta,
|
||||
position: 0
|
||||
};
|
||||
|
||||
const res = await updateModelById(localStorage.token, info.id, info);
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t(`Model {{name}} is now at the top`, { name: info.id }));
|
||||
}
|
||||
|
||||
await models.set(await getModels(localStorage.token));
|
||||
_models = $models;
|
||||
};
|
||||
|
||||
const hideModelHandler = async (model) => {
|
||||
let info = model.info;
|
||||
|
||||
@@ -322,7 +376,7 @@
|
||||
>
|
||||
<div class=" self-start w-8 pt-0.5">
|
||||
<div
|
||||
class=" rounded-full bg-stone-700 {(model?.info?.meta?.hidden ?? false)
|
||||
class=" rounded-full object-cover {(model?.info?.meta?.hidden ?? false)
|
||||
? 'brightness-90 dark:brightness-50'
|
||||
: ''} "
|
||||
>
|
||||
@@ -337,9 +391,21 @@
|
||||
<div
|
||||
class=" flex-1 self-center {(model?.info?.meta?.hidden ?? false) ? 'text-gray-500' : ''}"
|
||||
>
|
||||
<div class=" font-semibold line-clamp-1">{model.name}</div>
|
||||
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
|
||||
{!!model?.info?.meta?.description ? model?.info?.meta?.description : model.id}
|
||||
<Tooltip
|
||||
content={marked.parse(
|
||||
model?.ollama?.digest
|
||||
? `${model?.ollama?.digest} *(${model?.ollama?.modified_at})*`
|
||||
: ''
|
||||
)}
|
||||
className=" w-fit"
|
||||
placement="top-start"
|
||||
>
|
||||
<div class=" font-semibold line-clamp-1">{model.name}</div>
|
||||
</Tooltip>
|
||||
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1 text-gray-500">
|
||||
{!!model?.info?.meta?.description
|
||||
? model?.info?.meta?.description
|
||||
: (model?.ollama?.digest ?? model.id)}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -440,6 +506,9 @@
|
||||
exportHandler={() => {
|
||||
exportModelHandler(model);
|
||||
}}
|
||||
moveToTopHandler={() => {
|
||||
moveToTopHandler(model);
|
||||
}}
|
||||
hideHandler={() => {
|
||||
hideModelHandler(model);
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import { marked } from 'marked';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const helpText = {
|
||||
vision: $i18n.t('Model accepts image inputs'),
|
||||
usage: $i18n.t(
|
||||
'Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.'
|
||||
)
|
||||
};
|
||||
|
||||
export let capabilities: {
|
||||
vision?: boolean;
|
||||
usage?: boolean;
|
||||
} = {};
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="flex w-full justify-between mb-1">
|
||||
<div class=" self-center text-sm font-semibold">{$i18n.t('Capabilities')}</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
{#each Object.keys(capabilities) as capability}
|
||||
<div class=" flex items-center gap-2">
|
||||
<Checkbox
|
||||
state={capabilities[capability] ? 'checked' : 'unchecked'}
|
||||
on:change={(e) => {
|
||||
capabilities[capability] = e.detail === 'checked';
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class=" py-0.5 text-sm capitalize">
|
||||
<Tooltip content={marked.parse(helpText[capability])}>
|
||||
{$i18n.t(capability)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -12,6 +12,7 @@
|
||||
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
||||
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
|
||||
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
|
||||
import ArrowUpCircle from '$lib/components/icons/ArrowUpCircle.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -21,6 +22,7 @@
|
||||
export let cloneHandler: Function;
|
||||
export let exportHandler: Function;
|
||||
|
||||
export let moveToTopHandler: Function;
|
||||
export let hideHandler: Function;
|
||||
export let deleteHandler: Function;
|
||||
export let onClose: Function;
|
||||
@@ -80,6 +82,17 @@
|
||||
<div class="flex items-center">{$i18n.t('Export')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
moveToTopHandler();
|
||||
}}
|
||||
>
|
||||
<ArrowUpCircle />
|
||||
|
||||
<div class="flex items-center">{$i18n.t('Move to Top')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
|
||||
on:click={() => {
|
||||
|
||||
@@ -31,7 +31,9 @@ export const SUPPORTED_FILE_TYPE = [
|
||||
'application/x-javascript',
|
||||
'text/markdown',
|
||||
'audio/mpeg',
|
||||
'audio/wav'
|
||||
'audio/wav',
|
||||
'audio/ogg',
|
||||
'audio/x-m4a'
|
||||
];
|
||||
|
||||
export const SUPPORTED_FILE_EXTENSIONS = [
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"{{user}}'s Chats": "دردشات {{user}}",
|
||||
"{{webUIName}} Backend Required": "{{webUIName}} مطلوب",
|
||||
"*Prompt node ID(s) are required for image generation": "",
|
||||
"A new version (v{{LATEST_VERSION}}) is now available.": "",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "يتم استخدام نموذج المهمة عند تنفيذ مهام مثل إنشاء عناوين للدردشات واستعلامات بحث الويب",
|
||||
"a user": "مستخدم",
|
||||
"About": "عن",
|
||||
@@ -73,7 +74,10 @@
|
||||
"AUTOMATIC1111 Api Auth String": "",
|
||||
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 الرابط الرئيسي",
|
||||
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 الرابط مطلوب",
|
||||
"Available list": "",
|
||||
"available!": "متاح",
|
||||
"Azure AI Speech": "",
|
||||
"Azure Region": "",
|
||||
"Back": "خلف",
|
||||
"Bad Response": "استجابة خطاء",
|
||||
"Banners": "لافتات",
|
||||
@@ -94,6 +98,7 @@
|
||||
"Chat Bubble UI": "UI الدردشة",
|
||||
"Chat Controls": "",
|
||||
"Chat direction": "اتجاه المحادثة",
|
||||
"Chat Overview": "",
|
||||
"Chats": "المحادثات",
|
||||
"Check Again": "تحقق مرة اخرى",
|
||||
"Check for updates": "تحقق من التحديثات",
|
||||
@@ -238,6 +243,7 @@
|
||||
"Enter a detail about yourself for your LLMs to recall": "ادخل معلومات عنك تريد أن يتذكرها الموديل",
|
||||
"Enter api auth string (e.g. username:password)": "",
|
||||
"Enter Brave Search API Key": "أدخل مفتاح واجهة برمجة تطبيقات البحث الشجاع",
|
||||
"Enter CFG Scale (e.g. 7.0)": "",
|
||||
"Enter Chunk Overlap": "أدخل الChunk Overlap",
|
||||
"Enter Chunk Size": "أدخل Chunk الحجم",
|
||||
"Enter Github Raw URL": "أدخل عنوان URL ل Github Raw",
|
||||
@@ -248,6 +254,8 @@
|
||||
"Enter Model ID": "",
|
||||
"Enter model tag (e.g. {{modelTag}})": "(e.g. {{modelTag}}) أدخل الموديل تاق",
|
||||
"Enter Number of Steps (e.g. 50)": "(e.g. 50) أدخل عدد الخطوات",
|
||||
"Enter Sampler (e.g. Euler a)": "",
|
||||
"Enter Scheduler (e.g. Karras)": "",
|
||||
"Enter Score": "أدخل النتيجة",
|
||||
"Enter SearchApi API Key": "",
|
||||
"Enter SearchApi Engine": "",
|
||||
@@ -408,6 +416,8 @@
|
||||
"Model {{modelId}} not found": "لم يتم العثور على النموذج {{modelId}}.",
|
||||
"Model {{modelName}} is not vision capable": "نموذج {{modelName}} غير قادر على الرؤية",
|
||||
"Model {{name}} is now {{status}}": "نموذج {{name}} هو الآن {{status}}",
|
||||
"Model {{name}} is now at the top": "",
|
||||
"Model accepts image inputs": "",
|
||||
"Model created successfully!": "",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "تم اكتشاف مسار نظام الملفات النموذجي. الاسم المختصر للنموذج مطلوب للتحديث، ولا يمكن الاستمرار.",
|
||||
"Model ID": "رقم الموديل",
|
||||
@@ -419,6 +429,7 @@
|
||||
"Modelfile Content": "محتوى الملف النموذجي",
|
||||
"Models": "الموديلات",
|
||||
"More": "المزيد",
|
||||
"Move to Top": "",
|
||||
"Name": "الأسم",
|
||||
"Name Tag": "أسم التاق",
|
||||
"Name your model": "قم بتسمية النموذج الخاص بك",
|
||||
@@ -455,6 +466,7 @@
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "خطاء! يبدو أن عنوان URL غير صالح. يرجى التحقق مرة أخرى والمحاولة مرة أخرى.",
|
||||
"Oops! There was an error in the previous response. Please try again or contact admin.": "",
|
||||
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "خطاء! أنت تستخدم طريقة غير مدعومة (الواجهة الأمامية فقط). يرجى تقديم واجهة WebUI من الواجهة الخلفية.",
|
||||
"Open file": "",
|
||||
"Open new chat": "فتح محادثة جديده",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
|
||||
"OpenAI": "OpenAI",
|
||||
@@ -464,6 +476,9 @@
|
||||
"OpenAI URL/Key required.": "URL/مفتاح OpenAI.مطلوب عنوان ",
|
||||
"or": "أو",
|
||||
"Other": "آخر",
|
||||
"Output format": "",
|
||||
"Overview": "",
|
||||
"page": "",
|
||||
"Password": "الباسورد",
|
||||
"PDF document (.pdf)": "PDF ملف (.pdf)",
|
||||
"PDF Extract Images (OCR)": "PDF أستخرج الصور (OCR)",
|
||||
@@ -482,6 +497,7 @@
|
||||
"Plain text (.txt)": "نص عادي (.txt)",
|
||||
"Playground": "مكان التجربة",
|
||||
"Please carefully review the following warnings:": "",
|
||||
"Please select a reason": "",
|
||||
"Positive attitude": "موقف ايجابي",
|
||||
"Previous 30 days": "أخر 30 يوم",
|
||||
"Previous 7 days": "أخر 7 أيام",
|
||||
@@ -526,6 +542,7 @@
|
||||
"Save": "حفظ",
|
||||
"Save & Create": "حفظ وإنشاء",
|
||||
"Save & Update": "حفظ وتحديث",
|
||||
"Save As Copy": "",
|
||||
"Save Tag": "",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "لم يعد حفظ سجلات الدردشة مباشرة في مساحة تخزين متصفحك مدعومًا. يرجى تخصيص بعض الوقت لتنزيل وحذف سجلات الدردشة الخاصة بك عن طريق النقر على الزر أدناه. لا تقلق، يمكنك بسهولة إعادة استيراد سجلات الدردشة الخاصة بك إلى الواجهة الخلفية من خلاله",
|
||||
"Scan": "مسح",
|
||||
@@ -572,16 +589,20 @@
|
||||
"Send": "تم",
|
||||
"Send a Message": "يُرجى إدخال طلبك هنا",
|
||||
"Send message": "يُرجى إدخال طلبك هنا.",
|
||||
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
|
||||
"September": "سبتمبر",
|
||||
"Serper API Key": "مفتاح واجهة برمجة تطبيقات سيربر",
|
||||
"Serply API Key": "",
|
||||
"Serpstack API Key": "مفتاح واجهة برمجة تطبيقات Serpstack",
|
||||
"Server connection verified": "تم التحقق من اتصال الخادم",
|
||||
"Set as default": "الافتراضي",
|
||||
"Set CFG Scale": "",
|
||||
"Set Default Model": "تفعيد الموديل الافتراضي",
|
||||
"Set embedding model (e.g. {{model}})": "ضبط نموذج المتجهات (على سبيل المثال: {{model}})",
|
||||
"Set Image Size": "حجم الصورة",
|
||||
"Set reranking model (e.g. {{model}})": "ضبط نموذج إعادة الترتيب (على سبيل المثال: {{model}})",
|
||||
"Set Sampler": "",
|
||||
"Set Scheduler": "",
|
||||
"Set Steps": "ضبط الخطوات",
|
||||
"Set Task Model": "تعيين نموذج المهمة",
|
||||
"Set Voice": "ضبط الصوت",
|
||||
@@ -602,9 +623,11 @@
|
||||
"Sign up": "تسجيل",
|
||||
"Signing in": "جاري الدخول",
|
||||
"Source": "المصدر",
|
||||
"Speech Playback Speed": "",
|
||||
"Speech recognition error: {{error}}": "{{error}} خطأ في التعرف على الكلام",
|
||||
"Speech-to-Text Engine": "محرك تحويل الكلام إلى نص",
|
||||
"Stop Sequence": "وقف التسلسل",
|
||||
"Stream Chat Response": "",
|
||||
"STT Model": "",
|
||||
"STT Settings": "STT اعدادات",
|
||||
"Submit": "إرسال",
|
||||
@@ -684,6 +707,7 @@
|
||||
"Unpin": "",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "تحديث ونسخ الرابط",
|
||||
"Update for the latest features and improvements.": "",
|
||||
"Update password": "تحديث كلمة المرور",
|
||||
"Updated at": "",
|
||||
"Upload": "",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"{{user}}'s Chats": "{{user}}'s чатове",
|
||||
"{{webUIName}} Backend Required": "{{webUIName}} Изисква се Бекенд",
|
||||
"*Prompt node ID(s) are required for image generation": "",
|
||||
"A new version (v{{LATEST_VERSION}}) is now available.": "",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "Моделът на задачите се използва при изпълнение на задачи като генериране на заглавия за чатове и заявки за търсене в мрежата",
|
||||
"a user": "потребител",
|
||||
"About": "Относно",
|
||||
@@ -73,7 +74,10 @@
|
||||
"AUTOMATIC1111 Api Auth String": "",
|
||||
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Базов URL",
|
||||
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Базов URL е задължителен.",
|
||||
"Available list": "",
|
||||
"available!": "наличен!",
|
||||
"Azure AI Speech": "",
|
||||
"Azure Region": "",
|
||||
"Back": "Назад",
|
||||
"Bad Response": "Невалиден отговор от API",
|
||||
"Banners": "Банери",
|
||||
@@ -94,6 +98,7 @@
|
||||
"Chat Bubble UI": "UI за чат бублон",
|
||||
"Chat Controls": "",
|
||||
"Chat direction": "Направление на чата",
|
||||
"Chat Overview": "",
|
||||
"Chats": "Чатове",
|
||||
"Check Again": "Проверете Още Веднъж",
|
||||
"Check for updates": "Проверка за актуализации",
|
||||
@@ -238,6 +243,7 @@
|
||||
"Enter a detail about yourself for your LLMs to recall": "Въведете подробности за себе си, за да се herinnerат вашите LLMs",
|
||||
"Enter api auth string (e.g. username:password)": "",
|
||||
"Enter Brave Search API Key": "Въведете Brave Search API ключ",
|
||||
"Enter CFG Scale (e.g. 7.0)": "",
|
||||
"Enter Chunk Overlap": "Въведете Chunk Overlap",
|
||||
"Enter Chunk Size": "Въведете Chunk Size",
|
||||
"Enter Github Raw URL": "Въведете URL адреса на Github Raw",
|
||||
@@ -248,6 +254,8 @@
|
||||
"Enter Model ID": "",
|
||||
"Enter model tag (e.g. {{modelTag}})": "Въведете таг на модел (напр. {{modelTag}})",
|
||||
"Enter Number of Steps (e.g. 50)": "Въведете брой стъпки (напр. 50)",
|
||||
"Enter Sampler (e.g. Euler a)": "",
|
||||
"Enter Scheduler (e.g. Karras)": "",
|
||||
"Enter Score": "Въведете оценка",
|
||||
"Enter SearchApi API Key": "",
|
||||
"Enter SearchApi Engine": "",
|
||||
@@ -408,6 +416,8 @@
|
||||
"Model {{modelId}} not found": "Моделът {{modelId}} не е намерен",
|
||||
"Model {{modelName}} is not vision capable": "Моделът {{modelName}} не може да се вижда",
|
||||
"Model {{name}} is now {{status}}": "Моделът {{name}} сега е {{status}}",
|
||||
"Model {{name}} is now at the top": "",
|
||||
"Model accepts image inputs": "",
|
||||
"Model created successfully!": "",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Открит е път до файловата система на модела. За актуализацията се изисква съкратено име на модела, не може да продължи.",
|
||||
"Model ID": "ИД на модел",
|
||||
@@ -419,6 +429,7 @@
|
||||
"Modelfile Content": "Съдържание на модфайл",
|
||||
"Models": "Модели",
|
||||
"More": "Повече",
|
||||
"Move to Top": "",
|
||||
"Name": "Име",
|
||||
"Name Tag": "Име Таг",
|
||||
"Name your model": "Дайте име на вашия модел",
|
||||
@@ -455,6 +466,7 @@
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Упс! Изглежда URL адресът е невалиден. Моля, проверете отново и опитайте пак.",
|
||||
"Oops! There was an error in the previous response. Please try again or contact admin.": "",
|
||||
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Упс! Използвате неподдържан метод (само фронтенд). Моля, сервирайте WebUI от бекенда.",
|
||||
"Open file": "",
|
||||
"Open new chat": "Отвори нов чат",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
|
||||
"OpenAI": "OpenAI",
|
||||
@@ -464,6 +476,9 @@
|
||||
"OpenAI URL/Key required.": "OpenAI URL/Key е задължителен.",
|
||||
"or": "или",
|
||||
"Other": "Other",
|
||||
"Output format": "",
|
||||
"Overview": "",
|
||||
"page": "",
|
||||
"Password": "Парола",
|
||||
"PDF document (.pdf)": "PDF документ (.pdf)",
|
||||
"PDF Extract Images (OCR)": "PDF Extract Images (OCR)",
|
||||
@@ -482,6 +497,7 @@
|
||||
"Plain text (.txt)": "Plain text (.txt)",
|
||||
"Playground": "Плейграунд",
|
||||
"Please carefully review the following warnings:": "",
|
||||
"Please select a reason": "",
|
||||
"Positive attitude": "Позитивна ативност",
|
||||
"Previous 30 days": "Предыдущите 30 дни",
|
||||
"Previous 7 days": "Предыдущите 7 дни",
|
||||
@@ -526,6 +542,7 @@
|
||||
"Save": "Запис",
|
||||
"Save & Create": "Запис & Създаване",
|
||||
"Save & Update": "Запис & Актуализиране",
|
||||
"Save As Copy": "",
|
||||
"Save Tag": "",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Запазването на чат логове директно в хранилището на вашия браузър вече не се поддържа. Моля, отделете малко време, за да изтеглите и изтриете чат логовете си, като щракнете върху бутона по-долу. Не се притеснявайте, можете лесно да импортирате отново чат логовете си в бекенда чрез",
|
||||
"Scan": "Сканиране",
|
||||
@@ -568,16 +585,20 @@
|
||||
"Send": "Изпрати",
|
||||
"Send a Message": "Изпращане на Съобщение",
|
||||
"Send message": "Изпращане на съобщение",
|
||||
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
|
||||
"September": "Септември",
|
||||
"Serper API Key": "Serper API ключ",
|
||||
"Serply API Key": "",
|
||||
"Serpstack API Key": "Serpstack API ключ",
|
||||
"Server connection verified": "Server connection verified",
|
||||
"Set as default": "Задай по подразбиране",
|
||||
"Set CFG Scale": "",
|
||||
"Set Default Model": "Задай Модел По Подразбиране",
|
||||
"Set embedding model (e.g. {{model}})": "Задай embedding model (e.g. {{model}})",
|
||||
"Set Image Size": "Задай Размер на Изображението",
|
||||
"Set reranking model (e.g. {{model}})": "Задай reranking model (e.g. {{model}})",
|
||||
"Set Sampler": "",
|
||||
"Set Scheduler": "",
|
||||
"Set Steps": "Задай Стъпки",
|
||||
"Set Task Model": "Задаване на модел на задача",
|
||||
"Set Voice": "Задай Глас",
|
||||
@@ -598,9 +619,11 @@
|
||||
"Sign up": "Регистрация",
|
||||
"Signing in": "Вписване",
|
||||
"Source": "Източник",
|
||||
"Speech Playback Speed": "",
|
||||
"Speech recognition error: {{error}}": "Speech recognition error: {{error}}",
|
||||
"Speech-to-Text Engine": "Speech-to-Text Engine",
|
||||
"Stop Sequence": "Stop Sequence",
|
||||
"Stream Chat Response": "",
|
||||
"STT Model": "",
|
||||
"STT Settings": "STT Настройки",
|
||||
"Submit": "Изпращане",
|
||||
@@ -680,6 +703,7 @@
|
||||
"Unpin": "",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "Обнови и копирай връзка",
|
||||
"Update for the latest features and improvements.": "",
|
||||
"Update password": "Обновяване на парола",
|
||||
"Updated at": "",
|
||||
"Upload": "",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"{{user}}'s Chats": "{{user}}র চ্যাটস",
|
||||
"{{webUIName}} Backend Required": "{{webUIName}} ব্যাকএন্ড আবশ্যক",
|
||||
"*Prompt node ID(s) are required for image generation": "",
|
||||
"A new version (v{{LATEST_VERSION}}) is now available.": "",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "চ্যাট এবং ওয়েব অনুসন্ধান প্রশ্নের জন্য শিরোনাম তৈরি করার মতো কাজগুলি সম্পাদন করার সময় একটি টাস্ক মডেল ব্যবহার করা হয়",
|
||||
"a user": "একজন ব্যাবহারকারী",
|
||||
"About": "সম্পর্কে",
|
||||
@@ -73,7 +74,10 @@
|
||||
"AUTOMATIC1111 Api Auth String": "",
|
||||
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 বেজ ইউআরএল",
|
||||
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 বেজ ইউআরএল আবশ্যক",
|
||||
"Available list": "",
|
||||
"available!": "উপলব্ধ!",
|
||||
"Azure AI Speech": "",
|
||||
"Azure Region": "",
|
||||
"Back": "পেছনে",
|
||||
"Bad Response": "খারাপ প্রতিক্রিয়া",
|
||||
"Banners": "ব্যানার",
|
||||
@@ -94,6 +98,7 @@
|
||||
"Chat Bubble UI": "চ্যাট বাবল UI",
|
||||
"Chat Controls": "",
|
||||
"Chat direction": "চ্যাট দিকনির্দেশ",
|
||||
"Chat Overview": "",
|
||||
"Chats": "চ্যাটসমূহ",
|
||||
"Check Again": "আবার চেক করুন",
|
||||
"Check for updates": "নতুন আপডেট আছে কিনা চেক করুন",
|
||||
@@ -238,6 +243,7 @@
|
||||
"Enter a detail about yourself for your LLMs to recall": "আপনার এলএলএমগুলি স্মরণ করার জন্য নিজের সম্পর্কে একটি বিশদ লিখুন",
|
||||
"Enter api auth string (e.g. username:password)": "",
|
||||
"Enter Brave Search API Key": "সাহসী অনুসন্ধান API কী লিখুন",
|
||||
"Enter CFG Scale (e.g. 7.0)": "",
|
||||
"Enter Chunk Overlap": "চাঙ্ক ওভারল্যাপ লিখুন",
|
||||
"Enter Chunk Size": "চাংক সাইজ লিখুন",
|
||||
"Enter Github Raw URL": "গিটহাব কাঁচা URL লিখুন",
|
||||
@@ -248,6 +254,8 @@
|
||||
"Enter Model ID": "",
|
||||
"Enter model tag (e.g. {{modelTag}})": "মডেল ট্যাগ লিখুন (e.g. {{modelTag}})",
|
||||
"Enter Number of Steps (e.g. 50)": "ধাপের সংখ্যা দিন (যেমন: 50)",
|
||||
"Enter Sampler (e.g. Euler a)": "",
|
||||
"Enter Scheduler (e.g. Karras)": "",
|
||||
"Enter Score": "স্কোর দিন",
|
||||
"Enter SearchApi API Key": "",
|
||||
"Enter SearchApi Engine": "",
|
||||
@@ -408,6 +416,8 @@
|
||||
"Model {{modelId}} not found": "{{modelId}} মডেল পাওয়া যায়নি",
|
||||
"Model {{modelName}} is not vision capable": "মডেল {{modelName}} দৃষ্টি সক্ষম নয়",
|
||||
"Model {{name}} is now {{status}}": "মডেল {{name}} এখন {{status}}",
|
||||
"Model {{name}} is now at the top": "",
|
||||
"Model accepts image inputs": "",
|
||||
"Model created successfully!": "",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "মডেল ফাইলসিস্টেম পাথ পাওয়া গেছে। আপডেটের জন্য মডেলের শর্টনেম আবশ্যক, এগিয়ে যাওয়া যাচ্ছে না।",
|
||||
"Model ID": "মডেল ID",
|
||||
@@ -419,6 +429,7 @@
|
||||
"Modelfile Content": "মডেলফাইল কনটেন্ট",
|
||||
"Models": "মডেলসমূহ",
|
||||
"More": "আরো",
|
||||
"Move to Top": "",
|
||||
"Name": "নাম",
|
||||
"Name Tag": "নামের ট্যাগ",
|
||||
"Name your model": "আপনার মডেলের নাম দিন",
|
||||
@@ -455,6 +466,7 @@
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "ওহ, মনে হচ্ছে ইউআরএলটা ইনভ্যালিড। দয়া করে আর চেক করে চেষ্টা করুন।",
|
||||
"Oops! There was an error in the previous response. Please try again or contact admin.": "",
|
||||
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "আপনি একটা আনসাপোর্টেড পদ্ধতি (শুধু ফ্রন্টএন্ড) ব্যবহার করছেন। দয়া করে WebUI ব্যাকএন্ড থেকে চালনা করুন।",
|
||||
"Open file": "",
|
||||
"Open new chat": "নতুন চ্যাট খুলুন",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
|
||||
"OpenAI": "OpenAI",
|
||||
@@ -464,6 +476,9 @@
|
||||
"OpenAI URL/Key required.": "OpenAI URL/Key আবশ্যক",
|
||||
"or": "অথবা",
|
||||
"Other": "অন্যান্য",
|
||||
"Output format": "",
|
||||
"Overview": "",
|
||||
"page": "",
|
||||
"Password": "পাসওয়ার্ড",
|
||||
"PDF document (.pdf)": "PDF ডকুমেন্ট (.pdf)",
|
||||
"PDF Extract Images (OCR)": "পিডিএফ এর ছবি থেকে লেখা বের করুন (OCR)",
|
||||
@@ -482,6 +497,7 @@
|
||||
"Plain text (.txt)": "প্লায়েন টেক্সট (.txt)",
|
||||
"Playground": "খেলাঘর",
|
||||
"Please carefully review the following warnings:": "",
|
||||
"Please select a reason": "",
|
||||
"Positive attitude": "পজিটিভ আক্রমণ",
|
||||
"Previous 30 days": "পূর্ব ৩০ দিন",
|
||||
"Previous 7 days": "পূর্ব ৭ দিন",
|
||||
@@ -526,6 +542,7 @@
|
||||
"Save": "সংরক্ষণ",
|
||||
"Save & Create": "সংরক্ষণ এবং তৈরি করুন",
|
||||
"Save & Update": "সংরক্ষণ এবং আপডেট করুন",
|
||||
"Save As Copy": "",
|
||||
"Save Tag": "",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "মাধ্যমে",
|
||||
"Scan": "স্ক্যান",
|
||||
@@ -568,16 +585,20 @@
|
||||
"Send": "পাঠান",
|
||||
"Send a Message": "একটি মেসেজ পাঠান",
|
||||
"Send message": "মেসেজ পাঠান",
|
||||
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
|
||||
"September": "সেপ্টেম্বর",
|
||||
"Serper API Key": "Serper API Key",
|
||||
"Serply API Key": "",
|
||||
"Serpstack API Key": "Serpstack API Key",
|
||||
"Server connection verified": "সার্ভার কানেকশন যাচাই করা হয়েছে",
|
||||
"Set as default": "ডিফল্ট হিসেবে নির্ধারণ করুন",
|
||||
"Set CFG Scale": "",
|
||||
"Set Default Model": "ডিফল্ট মডেল নির্ধারণ করুন",
|
||||
"Set embedding model (e.g. {{model}})": "ইমেম্বিং মডেল নির্ধারণ করুন (উদাহরণ {{model}})",
|
||||
"Set Image Size": "ছবির সাইজ নির্ধারণ করুন",
|
||||
"Set reranking model (e.g. {{model}})": "রি-র্যাংকিং মডেল নির্ধারণ করুন (উদাহরণ {{model}})",
|
||||
"Set Sampler": "",
|
||||
"Set Scheduler": "",
|
||||
"Set Steps": "পরবর্তী ধাপসমূহ",
|
||||
"Set Task Model": "টাস্ক মডেল সেট করুন",
|
||||
"Set Voice": "কন্ঠস্বর নির্ধারণ করুন",
|
||||
@@ -598,9 +619,11 @@
|
||||
"Sign up": "সাইন আপ",
|
||||
"Signing in": "সাইন ইন",
|
||||
"Source": "উৎস",
|
||||
"Speech Playback Speed": "",
|
||||
"Speech recognition error: {{error}}": "স্পিচ রিকগনিশনে সমস্যা: {{error}}",
|
||||
"Speech-to-Text Engine": "স্পিচ-টু-টেক্সট ইঞ্জিন",
|
||||
"Stop Sequence": "সিকোয়েন্স থামান",
|
||||
"Stream Chat Response": "",
|
||||
"STT Model": "",
|
||||
"STT Settings": "STT সেটিংস",
|
||||
"Submit": "সাবমিট",
|
||||
@@ -680,6 +703,7 @@
|
||||
"Unpin": "",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "আপডেট এবং লিংক কপি করুন",
|
||||
"Update for the latest features and improvements.": "",
|
||||
"Update password": "পাসওয়ার্ড আপডেট করুন",
|
||||
"Updated at": "",
|
||||
"Upload": "",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"{{user}}'s Chats": "Els xats de {{user}}",
|
||||
"{{webUIName}} Backend Required": "El Backend de {{webUIName}} és necessari",
|
||||
"*Prompt node ID(s) are required for image generation": "*Els identificadors de nodes d'indicacions són necessaris per a la generació d'imatges",
|
||||
"A new version (v{{LATEST_VERSION}}) is now available.": "",
|
||||
"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 per a la web",
|
||||
"a user": "un usuari",
|
||||
"About": "Sobre",
|
||||
@@ -73,7 +74,10 @@
|
||||
"AUTOMATIC1111 Api Auth String": "Cadena d'autenticació de l'API d'AUTOMATIC1111",
|
||||
"AUTOMATIC1111 Base URL": "URL Base d'AUTOMATIC1111",
|
||||
"AUTOMATIC1111 Base URL is required.": "Es requereix l'URL Base d'AUTOMATIC1111.",
|
||||
"Available list": "Llista de disponibles",
|
||||
"available!": "disponible!",
|
||||
"Azure AI Speech": "Azure AI Speech",
|
||||
"Azure Region": "Regió d'Azure",
|
||||
"Back": "Enrere",
|
||||
"Bad Response": "Resposta errònia",
|
||||
"Banners": "Banners",
|
||||
@@ -94,6 +98,7 @@
|
||||
"Chat Bubble UI": "Chat Bubble UI",
|
||||
"Chat Controls": "Controls de xat",
|
||||
"Chat direction": "Direcció del xat",
|
||||
"Chat Overview": "Vista general del xat",
|
||||
"Chats": "Xats",
|
||||
"Check Again": "Comprovar-ho de nou",
|
||||
"Check for updates": "Comprovar si hi ha actualitzacions",
|
||||
@@ -230,7 +235,7 @@
|
||||
"Enable Message Rating": "Permetre la qualificació de missatges",
|
||||
"Enable New Sign Ups": "Permetre nous registres",
|
||||
"Enable Web Search": "Activar la cerca web",
|
||||
"Enable Web Search Query Generation": "",
|
||||
"Enable Web Search Query Generation": "Activa la generació de consultes de cerca web",
|
||||
"Enabled": "Habilitat",
|
||||
"Engine": "Motor",
|
||||
"Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Assegura't que els teus fitxers CSV inclouen 4 columnes en aquest ordre: Nom, Correu electrònic, Contrasenya, Rol.",
|
||||
@@ -238,6 +243,7 @@
|
||||
"Enter a detail about yourself for your LLMs to recall": "Introdueix un detall sobre tu què els teus models de llenguatge puguin recordar",
|
||||
"Enter api auth string (e.g. username:password)": "Entra la cadena d'autenticació api (p. ex. nom d'usuari:contrasenya)",
|
||||
"Enter Brave Search API Key": "Introdueix la clau API de Brave Search",
|
||||
"Enter CFG Scale (e.g. 7.0)": "Entra l'escala CFG (p.ex. 7.0)",
|
||||
"Enter Chunk Overlap": "Introdueix la mida de solapament de blocs",
|
||||
"Enter Chunk Size": "Introdueix la mida del bloc",
|
||||
"Enter Github Raw URL": "Introdueix l'URL en brut de Github",
|
||||
@@ -248,6 +254,8 @@
|
||||
"Enter Model ID": "Introdueix l'identificador del model",
|
||||
"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 Sampler (e.g. Euler a)": "Introdueix el mostrejador (p.ex. Euler a)",
|
||||
"Enter Scheduler (e.g. Karras)": "Entra el programador (p.ex. Karras)",
|
||||
"Enter Score": "Introdueix la puntuació",
|
||||
"Enter SearchApi API Key": "Introdueix la clau API SearchApi",
|
||||
"Enter SearchApi Engine": "Introdueix el motor SearchApi",
|
||||
@@ -368,7 +376,7 @@
|
||||
"Last Active": "Activitat recent",
|
||||
"Last Modified": "Modificació",
|
||||
"Leave empty for unlimited": "Deixar-ho buit per il·limitat",
|
||||
"Leave empty to use the default prompt, or enter a custom prompt": "",
|
||||
"Leave empty to use the default prompt, or enter a custom prompt": "Deixa-ho en blanc per utilitzar la indicació predeterminada o introdueix una indicació personalitzada",
|
||||
"Light": "Clar",
|
||||
"Listening...": "Escoltant...",
|
||||
"LLMs can make mistakes. Verify important information.": "Els models de llenguatge poden cometre errors. Verifica la informació important.",
|
||||
@@ -408,6 +416,8 @@
|
||||
"Model {{modelId}} not found": "No s'ha trobat el model {{modelId}}",
|
||||
"Model {{modelName}} is not vision capable": "El model {{modelName}} no és capaç de visió",
|
||||
"Model {{name}} is now {{status}}": "El model {{name}} ara és {{status}}",
|
||||
"Model {{name}} is now at the top": "El model {{name}} està ara a dalt de tot",
|
||||
"Model accepts image inputs": "El model accepta entrades d'imatge",
|
||||
"Model created successfully!": "Model creat correctament",
|
||||
"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 actualitzar, no es pot continuar.",
|
||||
"Model ID": "Identificador del model",
|
||||
@@ -419,6 +429,7 @@
|
||||
"Modelfile Content": "Contingut del Modelfile",
|
||||
"Models": "Models",
|
||||
"More": "Més",
|
||||
"Move to Top": "Moure a dalt de tot",
|
||||
"Name": "Nom",
|
||||
"Name Tag": "Etiqueta de nom",
|
||||
"Name your model": "Posa un nom al teu model",
|
||||
@@ -455,6 +466,7 @@
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Ui! Sembla que l'URL no és vàlida. Si us plau, revisa-la i torna-ho a provar.",
|
||||
"Oops! There was an error in the previous response. Please try again or contact admin.": "Ui! Hi ha hagut un error en la resposta anterior. Torna a provar-ho o contacta amb un administrador",
|
||||
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ui! Estàs utilitzant un mètode no suportat (només frontend). Si us plau, serveix la WebUI des del backend.",
|
||||
"Open file": "",
|
||||
"Open new chat": "Obre un xat nou",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "La versió d'Open WebUI (v{{OPEN_WEBUI_VERSION}}) és inferior a la versió requerida (v{{REQUIRED_VERSION}})",
|
||||
"OpenAI": "OpenAI",
|
||||
@@ -464,6 +476,9 @@
|
||||
"OpenAI URL/Key required.": "URL/Clau d'OpenAI requerides.",
|
||||
"or": "o",
|
||||
"Other": "Altres",
|
||||
"Output format": "Format de sortida",
|
||||
"Overview": "Vista general",
|
||||
"page": "",
|
||||
"Password": "Contrasenya",
|
||||
"PDF document (.pdf)": "Document PDF (.pdf)",
|
||||
"PDF Extract Images (OCR)": "Extreu imatges del PDF (OCR)",
|
||||
@@ -482,6 +497,7 @@
|
||||
"Plain text (.txt)": "Text pla (.txt)",
|
||||
"Playground": "Zona de jocs",
|
||||
"Please carefully review the following warnings:": "Si us plau, revisa els següents avisos amb cura:",
|
||||
"Please select a reason": "",
|
||||
"Positive attitude": "Actitud positiva",
|
||||
"Previous 30 days": "30 dies anteriors",
|
||||
"Previous 7 days": "7 dies anteriors",
|
||||
@@ -526,6 +542,7 @@
|
||||
"Save": "Desar",
|
||||
"Save & Create": "Desar i crear",
|
||||
"Save & Update": "Desar i actualitzar",
|
||||
"Save As Copy": "Desar com a còpia",
|
||||
"Save Tag": "Desar l'etiqueta",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Desar els registres de xat directament a l'emmagatzematge del teu navegador ja no està suportat. Si us plau, descarregr i elimina els registres de xat fent clic al botó de sota. No et preocupis, pots tornar a importar fàcilment els teus registres de xat al backend a través de",
|
||||
"Scan": "Escanejar",
|
||||
@@ -569,16 +586,20 @@
|
||||
"Send": "Enviar",
|
||||
"Send a Message": "Enviar un missatge",
|
||||
"Send message": "Enviar missatge",
|
||||
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "Envia `stream_options: { include_usage: true }` a la sol·licitud.\nEls proveïdors compatibles retornaran la informació d'ús del token a la resposta quan s'estableixi.",
|
||||
"September": "Setembre",
|
||||
"Serper API Key": "Clau API de Serper",
|
||||
"Serply API Key": "Clau API de Serply",
|
||||
"Serpstack API Key": "Clau API de Serpstack",
|
||||
"Server connection verified": "Connexió al servidor verificada",
|
||||
"Set as default": "Establir com a predeterminat",
|
||||
"Set CFG Scale": "Establir l'escala CFG",
|
||||
"Set Default Model": "Establir el model predeterminat",
|
||||
"Set embedding model (e.g. {{model}})": "Establir el model d'incrustació (p.ex. {{model}})",
|
||||
"Set Image Size": "Establir la mida de la image",
|
||||
"Set reranking model (e.g. {{model}})": "Establir el model de reavaluació (p.ex. {{model}})",
|
||||
"Set Sampler": "Establir el mostrejador",
|
||||
"Set Scheduler": "Establir el programador",
|
||||
"Set Steps": "Establir el nombre de passos",
|
||||
"Set Task Model": "Establir el model de tasca",
|
||||
"Set Voice": "Establir la veu",
|
||||
@@ -599,9 +620,11 @@
|
||||
"Sign up": "Registrar-se",
|
||||
"Signing in": "Iniciant sessió",
|
||||
"Source": "Font",
|
||||
"Speech Playback Speed": "Velocitat de la parla",
|
||||
"Speech recognition error: {{error}}": "Error de reconeixement de veu: {{error}}",
|
||||
"Speech-to-Text Engine": "Motor de veu a text",
|
||||
"Stop Sequence": "Atura la seqüència",
|
||||
"Stream Chat Response": "Fer streaming de la resposta del xat",
|
||||
"STT Model": "Model SST",
|
||||
"STT Settings": "Preferències de STT",
|
||||
"Submit": "Enviar",
|
||||
@@ -681,6 +704,7 @@
|
||||
"Unpin": "Alliberar",
|
||||
"Update": "Actualitzar",
|
||||
"Update and Copy Link": "Actualitzar i copiar l'enllaç",
|
||||
"Update for the latest features and improvements.": "",
|
||||
"Update password": "Actualitzar la contrasenya",
|
||||
"Updated at": "Actualitzat",
|
||||
"Upload": "Pujar",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"{{user}}'s Chats": "",
|
||||
"{{webUIName}} Backend Required": "Backend {{webUIName}} gikinahanglan",
|
||||
"*Prompt node ID(s) are required for image generation": "",
|
||||
"A new version (v{{LATEST_VERSION}}) is now available.": "",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "",
|
||||
"a user": "usa ka user",
|
||||
"About": "Mahitungod sa",
|
||||
@@ -73,7 +74,10 @@
|
||||
"AUTOMATIC1111 Api Auth String": "",
|
||||
"AUTOMATIC1111 Base URL": "Base URL AUTOMATIC1111",
|
||||
"AUTOMATIC1111 Base URL is required.": "Ang AUTOMATIC1111 base URL gikinahanglan.",
|
||||
"Available list": "",
|
||||
"available!": "magamit!",
|
||||
"Azure AI Speech": "",
|
||||
"Azure Region": "",
|
||||
"Back": "Balik",
|
||||
"Bad Response": "",
|
||||
"Banners": "",
|
||||
@@ -94,6 +98,7 @@
|
||||
"Chat Bubble UI": "",
|
||||
"Chat Controls": "",
|
||||
"Chat direction": "",
|
||||
"Chat Overview": "",
|
||||
"Chats": "Mga panaghisgot",
|
||||
"Check Again": "Susiha pag-usab",
|
||||
"Check for updates": "Susiha ang mga update",
|
||||
@@ -238,6 +243,7 @@
|
||||
"Enter a detail about yourself for your LLMs to recall": "",
|
||||
"Enter api auth string (e.g. username:password)": "",
|
||||
"Enter Brave Search API Key": "",
|
||||
"Enter CFG Scale (e.g. 7.0)": "",
|
||||
"Enter Chunk Overlap": "Pagsulod sa block overlap",
|
||||
"Enter Chunk Size": "Isulod ang block size",
|
||||
"Enter Github Raw URL": "",
|
||||
@@ -248,6 +254,8 @@
|
||||
"Enter Model ID": "",
|
||||
"Enter model tag (e.g. {{modelTag}})": "Pagsulod sa template tag (e.g. {{modelTag}})",
|
||||
"Enter Number of Steps (e.g. 50)": "Pagsulod sa gidaghanon sa mga lakang (e.g. 50)",
|
||||
"Enter Sampler (e.g. Euler a)": "",
|
||||
"Enter Scheduler (e.g. Karras)": "",
|
||||
"Enter Score": "",
|
||||
"Enter SearchApi API Key": "",
|
||||
"Enter SearchApi Engine": "",
|
||||
@@ -408,6 +416,8 @@
|
||||
"Model {{modelId}} not found": "Modelo {{modelId}} wala makit-an",
|
||||
"Model {{modelName}} is not vision capable": "",
|
||||
"Model {{name}} is now {{status}}": "",
|
||||
"Model {{name}} is now at the top": "",
|
||||
"Model accepts image inputs": "",
|
||||
"Model created successfully!": "",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "",
|
||||
"Model ID": "",
|
||||
@@ -419,6 +429,7 @@
|
||||
"Modelfile Content": "Mga sulod sa template file",
|
||||
"Models": "Mga modelo",
|
||||
"More": "",
|
||||
"Move to Top": "",
|
||||
"Name": "Ngalan",
|
||||
"Name Tag": "Tag sa ngalan",
|
||||
"Name your model": "",
|
||||
@@ -455,6 +466,7 @@
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Oops! ",
|
||||
"Oops! There was an error in the previous response. Please try again or contact admin.": "",
|
||||
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! ",
|
||||
"Open file": "",
|
||||
"Open new chat": "Ablihi ang bag-ong diskusyon",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
|
||||
"OpenAI": "",
|
||||
@@ -464,6 +476,9 @@
|
||||
"OpenAI URL/Key required.": "",
|
||||
"or": "O",
|
||||
"Other": "",
|
||||
"Output format": "",
|
||||
"Overview": "",
|
||||
"page": "",
|
||||
"Password": "Password",
|
||||
"PDF document (.pdf)": "",
|
||||
"PDF Extract Images (OCR)": "PDF Image Extraction (OCR)",
|
||||
@@ -482,6 +497,7 @@
|
||||
"Plain text (.txt)": "",
|
||||
"Playground": "Dulaanan",
|
||||
"Please carefully review the following warnings:": "",
|
||||
"Please select a reason": "",
|
||||
"Positive attitude": "",
|
||||
"Previous 30 days": "",
|
||||
"Previous 7 days": "",
|
||||
@@ -526,6 +542,7 @@
|
||||
"Save": "Tipigi",
|
||||
"Save & Create": "I-save ug Paghimo",
|
||||
"Save & Update": "I-save ug I-update",
|
||||
"Save As Copy": "",
|
||||
"Save Tag": "",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Ang pag-save sa mga chat log direkta sa imong browser storage dili na suportado. ",
|
||||
"Scan": "Aron ma-scan",
|
||||
@@ -568,16 +585,20 @@
|
||||
"Send": "",
|
||||
"Send a Message": "Magpadala ug mensahe",
|
||||
"Send message": "Magpadala ug mensahe",
|
||||
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
|
||||
"September": "",
|
||||
"Serper API Key": "",
|
||||
"Serply API Key": "",
|
||||
"Serpstack API Key": "",
|
||||
"Server connection verified": "Gipamatud-an nga koneksyon sa server",
|
||||
"Set as default": "Define pinaagi sa default",
|
||||
"Set CFG Scale": "",
|
||||
"Set Default Model": "Ibutang ang default template",
|
||||
"Set embedding model (e.g. {{model}})": "",
|
||||
"Set Image Size": "Ibutang ang gidak-on sa hulagway",
|
||||
"Set reranking model (e.g. {{model}})": "",
|
||||
"Set Sampler": "",
|
||||
"Set Scheduler": "",
|
||||
"Set Steps": "Ipasabot ang mga lakang",
|
||||
"Set Task Model": "",
|
||||
"Set Voice": "Ibutang ang tingog",
|
||||
@@ -598,9 +619,11 @@
|
||||
"Sign up": "Pagrehistro",
|
||||
"Signing in": "",
|
||||
"Source": "Tinubdan",
|
||||
"Speech Playback Speed": "",
|
||||
"Speech recognition error: {{error}}": "Sayop sa pag-ila sa tingog: {{error}}",
|
||||
"Speech-to-Text Engine": "Engine sa pag-ila sa tingog",
|
||||
"Stop Sequence": "Pagkasunod-sunod sa pagsira",
|
||||
"Stream Chat Response": "",
|
||||
"STT Model": "",
|
||||
"STT Settings": "Mga setting sa STT",
|
||||
"Submit": "Isumite",
|
||||
@@ -680,6 +703,7 @@
|
||||
"Unpin": "",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "",
|
||||
"Update for the latest features and improvements.": "",
|
||||
"Update password": "I-update ang password",
|
||||
"Updated at": "",
|
||||
"Upload": "",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"{{user}}'s Chats": "{{user}}s Unterhaltungen",
|
||||
"{{webUIName}} Backend Required": "{{webUIName}}-Backend erforderlich",
|
||||
"*Prompt node ID(s) are required for image generation": "",
|
||||
"A new version (v{{LATEST_VERSION}}) is now available.": "",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "Aufgabenmodelle können Unterhaltungstitel oder Websuchanfragen generieren.",
|
||||
"a user": "ein Benutzer",
|
||||
"About": "Über",
|
||||
@@ -73,7 +74,10 @@
|
||||
"AUTOMATIC1111 Api Auth String": "AUTOMATIC1111-API-Authentifizierungszeichenfolge",
|
||||
"AUTOMATIC1111 Base URL": "AUTOMATIC1111-Basis-URL",
|
||||
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111-Basis-URL ist erforderlich.",
|
||||
"Available list": "",
|
||||
"available!": "Verfügbar!",
|
||||
"Azure AI Speech": "",
|
||||
"Azure Region": "",
|
||||
"Back": "Zurück",
|
||||
"Bad Response": "Schlechte Antwort",
|
||||
"Banners": "Banner",
|
||||
@@ -94,6 +98,7 @@
|
||||
"Chat Bubble UI": "Chat Bubble UI",
|
||||
"Chat Controls": "",
|
||||
"Chat direction": "Textrichtung",
|
||||
"Chat Overview": "",
|
||||
"Chats": "Unterhaltungen",
|
||||
"Check Again": "Erneut überprüfen",
|
||||
"Check for updates": "Nach Updates suchen",
|
||||
@@ -238,6 +243,7 @@
|
||||
"Enter a detail about yourself for your LLMs to recall": "Geben Sie ein Detail über sich selbst ein, das Ihre Sprachmodelle (LLMs) sich merken sollen",
|
||||
"Enter api auth string (e.g. username:password)": "Geben Sie die API-Authentifizierungszeichenfolge ein (z. B. Benutzername:Passwort)",
|
||||
"Enter Brave Search API Key": "Geben Sie den Brave Search API-Schlüssel ein",
|
||||
"Enter CFG Scale (e.g. 7.0)": "",
|
||||
"Enter Chunk Overlap": "Geben Sie die Blocküberlappung ein",
|
||||
"Enter Chunk Size": "Geben Sie die Blockgröße ein",
|
||||
"Enter Github Raw URL": "Geben Sie die Github Raw-URL ein",
|
||||
@@ -248,6 +254,8 @@
|
||||
"Enter Model ID": "",
|
||||
"Enter model tag (e.g. {{modelTag}})": "Gebn Sie den Model-Tag ein",
|
||||
"Enter Number of Steps (e.g. 50)": "Geben Sie die Anzahl an Schritten ein (z. B. 50)",
|
||||
"Enter Sampler (e.g. Euler a)": "",
|
||||
"Enter Scheduler (e.g. Karras)": "",
|
||||
"Enter Score": "Punktzahl eingeben",
|
||||
"Enter SearchApi API Key": "",
|
||||
"Enter SearchApi Engine": "",
|
||||
@@ -408,6 +416,8 @@
|
||||
"Model {{modelId}} not found": "Modell {{modelId}} nicht gefunden",
|
||||
"Model {{modelName}} is not vision capable": "Das Modell {{modelName}} ist nicht für die Bildverarbeitung geeignet",
|
||||
"Model {{name}} is now {{status}}": "Modell {{name}} ist jetzt {{status}}",
|
||||
"Model {{name}} is now at the top": "",
|
||||
"Model accepts image inputs": "",
|
||||
"Model created successfully!": "Modell erfolgreich erstellt!",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Modell-Dateisystempfad erkannt. Modellkurzname ist für das Update erforderlich, Fortsetzung nicht möglich.",
|
||||
"Model ID": "Modell-ID",
|
||||
@@ -419,6 +429,7 @@
|
||||
"Modelfile Content": "Modelfile-Inhalt",
|
||||
"Models": "Modelle",
|
||||
"More": "Mehr",
|
||||
"Move to Top": "",
|
||||
"Name": "Name",
|
||||
"Name Tag": "Namens-Tag",
|
||||
"Name your model": "Benennen Sie Ihr Modell",
|
||||
@@ -455,6 +466,7 @@
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Hoppla! Es scheint, dass die URL ungültig ist. Bitte überprüfen Sie diese und versuchen Sie es erneut.",
|
||||
"Oops! There was an error in the previous response. Please try again or contact admin.": "Hoppla! Es gab einen Fehler in der vorherigen Antwort. Bitte versuchen Sie es erneut oder kontaktieren Sie den Administrator.",
|
||||
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Hoppla! Sie verwenden eine nicht unterstützte Methode (nur Frontend). Bitte stellen Sie die WebUI vom Backend bereit.",
|
||||
"Open file": "Datei öffnen",
|
||||
"Open new chat": "Neuen Chat öffnen",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "Die installierte Open-WebUI-Version (v{{OPEN_WEBUI_VERSION}}) ist niedriger als die erforderliche Version (v{{REQUIRED_VERSION}})",
|
||||
"OpenAI": "OpenAI",
|
||||
@@ -464,6 +476,9 @@
|
||||
"OpenAI URL/Key required.": "OpenAI-URL/Schlüssel erforderlich.",
|
||||
"or": "oder",
|
||||
"Other": "Andere",
|
||||
"Output format": "",
|
||||
"Overview": "",
|
||||
"page": "Seite",
|
||||
"Password": "Passwort",
|
||||
"PDF document (.pdf)": "PDF-Dokument (.pdf)",
|
||||
"PDF Extract Images (OCR)": "Text von Bildern aus PDFs extrahieren (OCR)",
|
||||
@@ -482,6 +497,7 @@
|
||||
"Plain text (.txt)": "Nur Text (.txt)",
|
||||
"Playground": "Testumgebung",
|
||||
"Please carefully review the following warnings:": "",
|
||||
"Please select a reason": "",
|
||||
"Positive attitude": "Positive Einstellung",
|
||||
"Previous 30 days": "Vorherige 30 Tage",
|
||||
"Previous 7 days": "Vorherige 7 Tage",
|
||||
@@ -526,6 +542,7 @@
|
||||
"Save": "Speichern",
|
||||
"Save & Create": "Erstellen",
|
||||
"Save & Update": "Aktualisieren",
|
||||
"Save As Copy": "",
|
||||
"Save Tag": "Tag speichern",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Das direkte Speichern von Unterhaltungen im Browser-Speicher wird nicht mehr unterstützt. Bitte nehmen Sie einen Moment Zeit, um Ihre Unterhaltungen zu exportieren und zu löschen, indem Sie auf die Schaltfläche unten klicken. Keine Sorge, Sie können Ihre Unterhaltungen problemlos über das Backend wieder importieren.",
|
||||
"Scan": "Scannen",
|
||||
@@ -568,16 +585,20 @@
|
||||
"Send": "Senden",
|
||||
"Send a Message": "Eine Nachricht senden",
|
||||
"Send message": "Nachricht senden",
|
||||
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
|
||||
"September": "September",
|
||||
"Serper API Key": "Serper-API-Schlüssel",
|
||||
"Serply API Key": "Serply-API-Schlüssel",
|
||||
"Serpstack API Key": "Serpstack-API-Schlüssel",
|
||||
"Server connection verified": "Serververbindung überprüft",
|
||||
"Set as default": "Als Standard festlegen",
|
||||
"Set CFG Scale": "",
|
||||
"Set Default Model": "Standardmodell festlegen",
|
||||
"Set embedding model (e.g. {{model}})": "Einbettungsmodell festlegen (z. B. {{model}})",
|
||||
"Set Image Size": "Bildgröße festlegen",
|
||||
"Set reranking model (e.g. {{model}})": "Rerankingmodell festlegen (z. B. {{model}})",
|
||||
"Set Sampler": "",
|
||||
"Set Scheduler": "",
|
||||
"Set Steps": "Schrittgröße festlegen",
|
||||
"Set Task Model": "Aufgabenmodell festlegen",
|
||||
"Set Voice": "Stimme festlegen",
|
||||
@@ -598,9 +619,11 @@
|
||||
"Sign up": "Registrieren",
|
||||
"Signing in": "Anmeldung",
|
||||
"Source": "Quelle",
|
||||
"Speech Playback Speed": "",
|
||||
"Speech recognition error: {{error}}": "Spracherkennungsfehler: {{error}}",
|
||||
"Speech-to-Text Engine": "Sprache-zu-Text-Engine",
|
||||
"Stop Sequence": "Stop-Sequenz",
|
||||
"Stream Chat Response": "",
|
||||
"STT Model": "STT-Modell",
|
||||
"STT Settings": "STT-Einstellungen",
|
||||
"Submit": "Senden",
|
||||
@@ -680,6 +703,7 @@
|
||||
"Unpin": "Lösen",
|
||||
"Update": "Aktualisieren",
|
||||
"Update and Copy Link": "Aktualisieren und Link kopieren",
|
||||
"Update for the latest features and improvements.": "",
|
||||
"Update password": "Passwort aktualisieren",
|
||||
"Updated at": "Aktualisiert am",
|
||||
"Upload": "Hochladen",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"{{user}}'s Chats": "",
|
||||
"{{webUIName}} Backend Required": "{{webUIName}} Backend Much Required",
|
||||
"*Prompt node ID(s) are required for image generation": "",
|
||||
"A new version (v{{LATEST_VERSION}}) is now available.": "",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "",
|
||||
"a user": "such user",
|
||||
"About": "Much About",
|
||||
@@ -73,7 +74,10 @@
|
||||
"AUTOMATIC1111 Api Auth String": "",
|
||||
"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL",
|
||||
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Base URL is required.",
|
||||
"Available list": "",
|
||||
"available!": "available! So excite!",
|
||||
"Azure AI Speech": "",
|
||||
"Azure Region": "",
|
||||
"Back": "Back",
|
||||
"Bad Response": "",
|
||||
"Banners": "",
|
||||
@@ -94,6 +98,7 @@
|
||||
"Chat Bubble UI": "",
|
||||
"Chat Controls": "",
|
||||
"Chat direction": "",
|
||||
"Chat Overview": "",
|
||||
"Chats": "Chats",
|
||||
"Check Again": "Check Again",
|
||||
"Check for updates": "Check for updates",
|
||||
@@ -238,6 +243,7 @@
|
||||
"Enter a detail about yourself for your LLMs to recall": "",
|
||||
"Enter api auth string (e.g. username:password)": "",
|
||||
"Enter Brave Search API Key": "",
|
||||
"Enter CFG Scale (e.g. 7.0)": "",
|
||||
"Enter Chunk Overlap": "Enter Overlap of Chunks",
|
||||
"Enter Chunk Size": "Enter Size of Chunk",
|
||||
"Enter Github Raw URL": "",
|
||||
@@ -248,6 +254,8 @@
|
||||
"Enter Model ID": "",
|
||||
"Enter model tag (e.g. {{modelTag}})": "Enter model doge tag (e.g. {{modelTag}})",
|
||||
"Enter Number of Steps (e.g. 50)": "Enter Number of Steps (e.g. 50)",
|
||||
"Enter Sampler (e.g. Euler a)": "",
|
||||
"Enter Scheduler (e.g. Karras)": "",
|
||||
"Enter Score": "",
|
||||
"Enter SearchApi API Key": "",
|
||||
"Enter SearchApi Engine": "",
|
||||
@@ -408,6 +416,8 @@
|
||||
"Model {{modelId}} not found": "Model {{modelId}} not found",
|
||||
"Model {{modelName}} is not vision capable": "",
|
||||
"Model {{name}} is now {{status}}": "",
|
||||
"Model {{name}} is now at the top": "",
|
||||
"Model accepts image inputs": "",
|
||||
"Model created successfully!": "",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Model filesystem bark detected. Model shortname is required for update, cannot continue.",
|
||||
"Model ID": "",
|
||||
@@ -419,6 +429,7 @@
|
||||
"Modelfile Content": "Modelfile Content",
|
||||
"Models": "Wowdels",
|
||||
"More": "",
|
||||
"Move to Top": "",
|
||||
"Name": "Name",
|
||||
"Name Tag": "Name Tag",
|
||||
"Name your model": "",
|
||||
@@ -455,6 +466,7 @@
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Oops! Looks like the URL is invalid. Please double-check and try again.",
|
||||
"Oops! There was an error in the previous response. Please try again or contact admin.": "",
|
||||
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.",
|
||||
"Open file": "",
|
||||
"Open new chat": "Open new bark",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
|
||||
"OpenAI": "",
|
||||
@@ -464,6 +476,9 @@
|
||||
"OpenAI URL/Key required.": "",
|
||||
"or": "or",
|
||||
"Other": "",
|
||||
"Output format": "",
|
||||
"Overview": "",
|
||||
"page": "",
|
||||
"Password": "Barkword",
|
||||
"PDF document (.pdf)": "",
|
||||
"PDF Extract Images (OCR)": "PDF Extract Wowmages (OCR)",
|
||||
@@ -482,6 +497,7 @@
|
||||
"Plain text (.txt)": "Plain text (.txt)",
|
||||
"Playground": "Playground",
|
||||
"Please carefully review the following warnings:": "",
|
||||
"Please select a reason": "",
|
||||
"Positive attitude": "",
|
||||
"Previous 30 days": "",
|
||||
"Previous 7 days": "",
|
||||
@@ -526,6 +542,7 @@
|
||||
"Save": "Save much wow",
|
||||
"Save & Create": "Save & Create much create",
|
||||
"Save & Update": "Save & Update much update",
|
||||
"Save As Copy": "",
|
||||
"Save Tag": "",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Saving chat logs in browser storage not support anymore. Pls download and delete your chat logs by click button below. Much easy re-import to backend through",
|
||||
"Scan": "Scan much scan",
|
||||
@@ -570,16 +587,20 @@
|
||||
"Send": "",
|
||||
"Send a Message": "Send a Message much message",
|
||||
"Send message": "Send message very send",
|
||||
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
|
||||
"September": "",
|
||||
"Serper API Key": "",
|
||||
"Serply API Key": "",
|
||||
"Serpstack API Key": "",
|
||||
"Server connection verified": "Server connection verified much secure",
|
||||
"Set as default": "Set as default very default",
|
||||
"Set CFG Scale": "",
|
||||
"Set Default Model": "Set Default Model much model",
|
||||
"Set embedding model (e.g. {{model}})": "",
|
||||
"Set Image Size": "Set Image Size very size",
|
||||
"Set reranking model (e.g. {{model}})": "",
|
||||
"Set Sampler": "",
|
||||
"Set Scheduler": "",
|
||||
"Set Steps": "Set Steps so many steps",
|
||||
"Set Task Model": "",
|
||||
"Set Voice": "Set Voice so speak",
|
||||
@@ -600,9 +621,11 @@
|
||||
"Sign up": "Sign up much join",
|
||||
"Signing in": "",
|
||||
"Source": "Source",
|
||||
"Speech Playback Speed": "",
|
||||
"Speech recognition error: {{error}}": "Speech recognition error: {{error}} so error",
|
||||
"Speech-to-Text Engine": "Speech-to-Text Engine much speak",
|
||||
"Stop Sequence": "Stop Sequence much stop",
|
||||
"Stream Chat Response": "",
|
||||
"STT Model": "",
|
||||
"STT Settings": "STT Settings very settings",
|
||||
"Submit": "Submit much submit",
|
||||
@@ -682,6 +705,7 @@
|
||||
"Unpin": "",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "",
|
||||
"Update for the latest features and improvements.": "",
|
||||
"Update password": "Update password much change",
|
||||
"Updated at": "",
|
||||
"Upload": "",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"{{user}}'s Chats": "",
|
||||
"{{webUIName}} Backend Required": "",
|
||||
"*Prompt node ID(s) are required for image generation": "",
|
||||
"A new version (v{{LATEST_VERSION}}) is now available.": "",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "",
|
||||
"a user": "",
|
||||
"About": "",
|
||||
@@ -73,7 +74,10 @@
|
||||
"AUTOMATIC1111 Api Auth String": "",
|
||||
"AUTOMATIC1111 Base URL": "",
|
||||
"AUTOMATIC1111 Base URL is required.": "",
|
||||
"Available list": "",
|
||||
"available!": "",
|
||||
"Azure AI Speech": "",
|
||||
"Azure Region": "",
|
||||
"Back": "",
|
||||
"Bad Response": "",
|
||||
"Banners": "",
|
||||
@@ -94,6 +98,7 @@
|
||||
"Chat Bubble UI": "",
|
||||
"Chat Controls": "",
|
||||
"Chat direction": "",
|
||||
"Chat Overview": "",
|
||||
"Chats": "",
|
||||
"Check Again": "",
|
||||
"Check for updates": "",
|
||||
@@ -238,6 +243,7 @@
|
||||
"Enter a detail about yourself for your LLMs to recall": "",
|
||||
"Enter api auth string (e.g. username:password)": "",
|
||||
"Enter Brave Search API Key": "",
|
||||
"Enter CFG Scale (e.g. 7.0)": "",
|
||||
"Enter Chunk Overlap": "",
|
||||
"Enter Chunk Size": "",
|
||||
"Enter Github Raw URL": "",
|
||||
@@ -248,6 +254,8 @@
|
||||
"Enter Model ID": "",
|
||||
"Enter model tag (e.g. {{modelTag}})": "",
|
||||
"Enter Number of Steps (e.g. 50)": "",
|
||||
"Enter Sampler (e.g. Euler a)": "",
|
||||
"Enter Scheduler (e.g. Karras)": "",
|
||||
"Enter Score": "",
|
||||
"Enter SearchApi API Key": "",
|
||||
"Enter SearchApi Engine": "",
|
||||
@@ -408,6 +416,8 @@
|
||||
"Model {{modelId}} not found": "",
|
||||
"Model {{modelName}} is not vision capable": "",
|
||||
"Model {{name}} is now {{status}}": "",
|
||||
"Model {{name}} is now at the top": "",
|
||||
"Model accepts image inputs": "",
|
||||
"Model created successfully!": "",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "",
|
||||
"Model ID": "",
|
||||
@@ -419,6 +429,7 @@
|
||||
"Modelfile Content": "",
|
||||
"Models": "",
|
||||
"More": "",
|
||||
"Move to Top": "",
|
||||
"Name": "",
|
||||
"Name Tag": "",
|
||||
"Name your model": "",
|
||||
@@ -455,6 +466,7 @@
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "",
|
||||
"Oops! There was an error in the previous response. Please try again or contact admin.": "",
|
||||
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "",
|
||||
"Open file": "",
|
||||
"Open new chat": "",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
|
||||
"OpenAI": "",
|
||||
@@ -464,6 +476,9 @@
|
||||
"OpenAI URL/Key required.": "",
|
||||
"or": "",
|
||||
"Other": "",
|
||||
"Output format": "",
|
||||
"Overview": "",
|
||||
"page": "",
|
||||
"Password": "",
|
||||
"PDF document (.pdf)": "",
|
||||
"PDF Extract Images (OCR)": "",
|
||||
@@ -482,6 +497,7 @@
|
||||
"Plain text (.txt)": "",
|
||||
"Playground": "",
|
||||
"Please carefully review the following warnings:": "",
|
||||
"Please select a reason": "",
|
||||
"Positive attitude": "",
|
||||
"Previous 30 days": "",
|
||||
"Previous 7 days": "",
|
||||
@@ -526,6 +542,7 @@
|
||||
"Save": "",
|
||||
"Save & Create": "",
|
||||
"Save & Update": "",
|
||||
"Save As Copy": "",
|
||||
"Save Tag": "",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "",
|
||||
"Scan": "",
|
||||
@@ -568,16 +585,20 @@
|
||||
"Send": "",
|
||||
"Send a Message": "",
|
||||
"Send message": "",
|
||||
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
|
||||
"September": "",
|
||||
"Serper API Key": "",
|
||||
"Serply API Key": "",
|
||||
"Serpstack API Key": "",
|
||||
"Server connection verified": "",
|
||||
"Set as default": "",
|
||||
"Set CFG Scale": "",
|
||||
"Set Default Model": "",
|
||||
"Set embedding model (e.g. {{model}})": "",
|
||||
"Set Image Size": "",
|
||||
"Set reranking model (e.g. {{model}})": "",
|
||||
"Set Sampler": "",
|
||||
"Set Scheduler": "",
|
||||
"Set Steps": "",
|
||||
"Set Task Model": "",
|
||||
"Set Voice": "",
|
||||
@@ -598,9 +619,11 @@
|
||||
"Sign up": "",
|
||||
"Signing in": "",
|
||||
"Source": "",
|
||||
"Speech Playback Speed": "",
|
||||
"Speech recognition error: {{error}}": "",
|
||||
"Speech-to-Text Engine": "",
|
||||
"Stop Sequence": "",
|
||||
"Stream Chat Response": "",
|
||||
"STT Model": "",
|
||||
"STT Settings": "",
|
||||
"Submit": "",
|
||||
@@ -680,6 +703,7 @@
|
||||
"Unpin": "",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "",
|
||||
"Update for the latest features and improvements.": "",
|
||||
"Update password": "",
|
||||
"Updated at": "",
|
||||
"Upload": "",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"{{user}}'s Chats": "",
|
||||
"{{webUIName}} Backend Required": "",
|
||||
"*Prompt node ID(s) are required for image generation": "",
|
||||
"A new version (v{{LATEST_VERSION}}) is now available.": "",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "",
|
||||
"a user": "",
|
||||
"About": "",
|
||||
@@ -73,7 +74,10 @@
|
||||
"AUTOMATIC1111 Api Auth String": "",
|
||||
"AUTOMATIC1111 Base URL": "",
|
||||
"AUTOMATIC1111 Base URL is required.": "",
|
||||
"Available list": "",
|
||||
"available!": "",
|
||||
"Azure AI Speech": "",
|
||||
"Azure Region": "",
|
||||
"Back": "",
|
||||
"Bad Response": "",
|
||||
"Banners": "",
|
||||
@@ -94,6 +98,7 @@
|
||||
"Chat Bubble UI": "",
|
||||
"Chat Controls": "",
|
||||
"Chat direction": "",
|
||||
"Chat Overview": "",
|
||||
"Chats": "",
|
||||
"Check Again": "",
|
||||
"Check for updates": "",
|
||||
@@ -238,6 +243,7 @@
|
||||
"Enter a detail about yourself for your LLMs to recall": "",
|
||||
"Enter api auth string (e.g. username:password)": "",
|
||||
"Enter Brave Search API Key": "",
|
||||
"Enter CFG Scale (e.g. 7.0)": "",
|
||||
"Enter Chunk Overlap": "",
|
||||
"Enter Chunk Size": "",
|
||||
"Enter Github Raw URL": "",
|
||||
@@ -248,6 +254,8 @@
|
||||
"Enter Model ID": "",
|
||||
"Enter model tag (e.g. {{modelTag}})": "",
|
||||
"Enter Number of Steps (e.g. 50)": "",
|
||||
"Enter Sampler (e.g. Euler a)": "",
|
||||
"Enter Scheduler (e.g. Karras)": "",
|
||||
"Enter Score": "",
|
||||
"Enter SearchApi API Key": "",
|
||||
"Enter SearchApi Engine": "",
|
||||
@@ -408,6 +416,8 @@
|
||||
"Model {{modelId}} not found": "",
|
||||
"Model {{modelName}} is not vision capable": "",
|
||||
"Model {{name}} is now {{status}}": "",
|
||||
"Model {{name}} is now at the top": "",
|
||||
"Model accepts image inputs": "",
|
||||
"Model created successfully!": "",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "",
|
||||
"Model ID": "",
|
||||
@@ -419,6 +429,7 @@
|
||||
"Modelfile Content": "",
|
||||
"Models": "",
|
||||
"More": "",
|
||||
"Move to Top": "",
|
||||
"Name": "",
|
||||
"Name Tag": "",
|
||||
"Name your model": "",
|
||||
@@ -455,6 +466,7 @@
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "",
|
||||
"Oops! There was an error in the previous response. Please try again or contact admin.": "",
|
||||
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "",
|
||||
"Open file": "",
|
||||
"Open new chat": "",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
|
||||
"OpenAI": "",
|
||||
@@ -464,6 +476,9 @@
|
||||
"OpenAI URL/Key required.": "",
|
||||
"or": "",
|
||||
"Other": "",
|
||||
"Output format": "",
|
||||
"Overview": "",
|
||||
"page": "",
|
||||
"Password": "",
|
||||
"PDF document (.pdf)": "",
|
||||
"PDF Extract Images (OCR)": "",
|
||||
@@ -482,6 +497,7 @@
|
||||
"Plain text (.txt)": "",
|
||||
"Playground": "",
|
||||
"Please carefully review the following warnings:": "",
|
||||
"Please select a reason": "",
|
||||
"Positive attitude": "",
|
||||
"Previous 30 days": "",
|
||||
"Previous 7 days": "",
|
||||
@@ -526,6 +542,7 @@
|
||||
"Save": "",
|
||||
"Save & Create": "",
|
||||
"Save & Update": "",
|
||||
"Save As Copy": "",
|
||||
"Save Tag": "",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "",
|
||||
"Scan": "",
|
||||
@@ -568,16 +585,20 @@
|
||||
"Send": "",
|
||||
"Send a Message": "",
|
||||
"Send message": "",
|
||||
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
|
||||
"September": "",
|
||||
"Serper API Key": "",
|
||||
"Serply API Key": "",
|
||||
"Serpstack API Key": "",
|
||||
"Server connection verified": "",
|
||||
"Set as default": "",
|
||||
"Set CFG Scale": "",
|
||||
"Set Default Model": "",
|
||||
"Set embedding model (e.g. {{model}})": "",
|
||||
"Set Image Size": "",
|
||||
"Set reranking model (e.g. {{model}})": "",
|
||||
"Set Sampler": "",
|
||||
"Set Scheduler": "",
|
||||
"Set Steps": "",
|
||||
"Set Task Model": "",
|
||||
"Set Voice": "",
|
||||
@@ -598,9 +619,11 @@
|
||||
"Sign up": "",
|
||||
"Signing in": "",
|
||||
"Source": "",
|
||||
"Speech Playback Speed": "",
|
||||
"Speech recognition error: {{error}}": "",
|
||||
"Speech-to-Text Engine": "",
|
||||
"Stop Sequence": "",
|
||||
"Stream Chat Response": "",
|
||||
"STT Model": "",
|
||||
"STT Settings": "",
|
||||
"Submit": "",
|
||||
@@ -680,6 +703,7 @@
|
||||
"Unpin": "",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "",
|
||||
"Update for the latest features and improvements.": "",
|
||||
"Update password": "",
|
||||
"Updated at": "",
|
||||
"Upload": "",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"{{user}}'s Chats": "{{user}}'s Chats",
|
||||
"{{webUIName}} Backend Required": "{{webUIName}} Servidor Requerido",
|
||||
"*Prompt node ID(s) are required for image generation": "",
|
||||
"A new version (v{{LATEST_VERSION}}) is now available.": "",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "Un modelo de tareas se utiliza cuando se realizan tareas como la generación de títulos para chats y consultas de búsqueda web",
|
||||
"a user": "un usuario",
|
||||
"About": "Sobre nosotros",
|
||||
@@ -73,7 +74,10 @@
|
||||
"AUTOMATIC1111 Api Auth String": "",
|
||||
"AUTOMATIC1111 Base URL": "Dirección URL de AUTOMATIC1111",
|
||||
"AUTOMATIC1111 Base URL is required.": "La dirección URL de AUTOMATIC1111 es requerida.",
|
||||
"Available list": "",
|
||||
"available!": "¡disponible!",
|
||||
"Azure AI Speech": "",
|
||||
"Azure Region": "",
|
||||
"Back": "Volver",
|
||||
"Bad Response": "Respuesta incorrecta",
|
||||
"Banners": "Banners",
|
||||
@@ -94,6 +98,7 @@
|
||||
"Chat Bubble UI": "Burbuja de chat UI",
|
||||
"Chat Controls": "",
|
||||
"Chat direction": "Dirección del Chat",
|
||||
"Chat Overview": "",
|
||||
"Chats": "Chats",
|
||||
"Check Again": "Verifica de nuevo",
|
||||
"Check for updates": "Verificar actualizaciones",
|
||||
@@ -238,6 +243,7 @@
|
||||
"Enter a detail about yourself for your LLMs to recall": "Ingrese un detalle sobre usted para que sus LLMs recuerden",
|
||||
"Enter api auth string (e.g. username:password)": "",
|
||||
"Enter Brave Search API Key": "Ingresa la clave de API de Brave Search",
|
||||
"Enter CFG Scale (e.g. 7.0)": "",
|
||||
"Enter Chunk Overlap": "Ingresar superposición de fragmentos",
|
||||
"Enter Chunk Size": "Ingrese el tamaño del fragmento",
|
||||
"Enter Github Raw URL": "Ingresa la URL sin procesar de Github",
|
||||
@@ -248,6 +254,8 @@
|
||||
"Enter Model ID": "",
|
||||
"Enter model tag (e.g. {{modelTag}})": "Ingrese la etiqueta del modelo (p.ej. {{modelTag}})",
|
||||
"Enter Number of Steps (e.g. 50)": "Ingrese el número de pasos (p.ej., 50)",
|
||||
"Enter Sampler (e.g. Euler a)": "",
|
||||
"Enter Scheduler (e.g. Karras)": "",
|
||||
"Enter Score": "Ingrese la puntuación",
|
||||
"Enter SearchApi API Key": "",
|
||||
"Enter SearchApi Engine": "",
|
||||
@@ -408,6 +416,8 @@
|
||||
"Model {{modelId}} not found": "El modelo {{modelId}} no fue encontrado",
|
||||
"Model {{modelName}} is not vision capable": "El modelo {{modelName}} no es capaz de ver",
|
||||
"Model {{name}} is now {{status}}": "El modelo {{name}} ahora es {{status}}",
|
||||
"Model {{name}} is now at the top": "",
|
||||
"Model accepts image inputs": "",
|
||||
"Model created successfully!": "Modelo creado correctamente!",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Se detectó la ruta del sistema de archivos del modelo. Se requiere el nombre corto del modelo para la actualización, no se puede continuar.",
|
||||
"Model ID": "ID del modelo",
|
||||
@@ -419,6 +429,7 @@
|
||||
"Modelfile Content": "Contenido del Modelfile",
|
||||
"Models": "Modelos",
|
||||
"More": "Más",
|
||||
"Move to Top": "",
|
||||
"Name": "Nombre",
|
||||
"Name Tag": "Nombre de etiqueta",
|
||||
"Name your model": "Asigne un nombre a su modelo",
|
||||
@@ -455,6 +466,7 @@
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "¡Ups! Parece que la URL no es válida. Vuelva a verificar e inténtelo nuevamente.",
|
||||
"Oops! There was an error in the previous response. Please try again or contact admin.": "¡Oops! Hubo un error en la respuesta anterior. Intente de nuevo o póngase en contacto con el administrador.",
|
||||
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "¡Ups! Estás utilizando un método no compatible (solo frontend). Por favor ejecute la WebUI desde el backend.",
|
||||
"Open file": "",
|
||||
"Open new chat": "Abrir nuevo chat",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
|
||||
"OpenAI": "OpenAI",
|
||||
@@ -464,6 +476,9 @@
|
||||
"OpenAI URL/Key required.": "URL/Clave de OpenAI es requerida.",
|
||||
"or": "o",
|
||||
"Other": "Otro",
|
||||
"Output format": "",
|
||||
"Overview": "",
|
||||
"page": "",
|
||||
"Password": "Contraseña",
|
||||
"PDF document (.pdf)": "PDF document (.pdf)",
|
||||
"PDF Extract Images (OCR)": "Extraer imágenes de PDF (OCR)",
|
||||
@@ -482,6 +497,7 @@
|
||||
"Plain text (.txt)": "Texto plano (.txt)",
|
||||
"Playground": "Patio de juegos",
|
||||
"Please carefully review the following warnings:": "",
|
||||
"Please select a reason": "",
|
||||
"Positive attitude": "Actitud positiva",
|
||||
"Previous 30 days": "Últimos 30 días",
|
||||
"Previous 7 days": "Últimos 7 días",
|
||||
@@ -526,6 +542,7 @@
|
||||
"Save": "Guardar",
|
||||
"Save & Create": "Guardar y Crear",
|
||||
"Save & Update": "Guardar y Actualizar",
|
||||
"Save As Copy": "",
|
||||
"Save Tag": "",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Ya no se admite guardar registros de chat directamente en el almacenamiento de su navegador. Tómese un momento para descargar y eliminar sus registros de chat haciendo clic en el botón a continuación. No te preocupes, puedes volver a importar fácilmente tus registros de chat al backend a través de",
|
||||
"Scan": "Escanear",
|
||||
@@ -569,16 +586,20 @@
|
||||
"Send": "Enviar",
|
||||
"Send a Message": "Enviar un Mensaje",
|
||||
"Send message": "Enviar Mensaje",
|
||||
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
|
||||
"September": "Septiembre",
|
||||
"Serper API Key": "Clave API de Serper",
|
||||
"Serply API Key": "Clave API de Serply",
|
||||
"Serpstack API Key": "Clave API de Serpstack",
|
||||
"Server connection verified": "Conexión del servidor verificada",
|
||||
"Set as default": "Establecer por defecto",
|
||||
"Set CFG Scale": "",
|
||||
"Set Default Model": "Establecer modelo predeterminado",
|
||||
"Set embedding model (e.g. {{model}})": "Establecer modelo de embedding (ej. {{model}})",
|
||||
"Set Image Size": "Establecer tamaño de imagen",
|
||||
"Set reranking model (e.g. {{model}})": "Establecer modelo de reranking (ej. {{model}})",
|
||||
"Set Sampler": "",
|
||||
"Set Scheduler": "",
|
||||
"Set Steps": "Establecer Pasos",
|
||||
"Set Task Model": "Establecer modelo de tarea",
|
||||
"Set Voice": "Establecer la voz",
|
||||
@@ -599,9 +620,11 @@
|
||||
"Sign up": "Crear una cuenta",
|
||||
"Signing in": "Iniciando sesión",
|
||||
"Source": "Fuente",
|
||||
"Speech Playback Speed": "",
|
||||
"Speech recognition error: {{error}}": "Error de reconocimiento de voz: {{error}}",
|
||||
"Speech-to-Text Engine": "Motor de voz a texto",
|
||||
"Stop Sequence": "Detener secuencia",
|
||||
"Stream Chat Response": "",
|
||||
"STT Model": "Modelo STT",
|
||||
"STT Settings": "Configuraciones de STT",
|
||||
"Submit": "Enviar",
|
||||
@@ -681,6 +704,7 @@
|
||||
"Unpin": "",
|
||||
"Update": "Actualizar",
|
||||
"Update and Copy Link": "Actualizar y copiar enlace",
|
||||
"Update for the latest features and improvements.": "",
|
||||
"Update password": "Actualizar contraseña",
|
||||
"Updated at": "Actualizado en",
|
||||
"Upload": "Subir",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"{{user}}'s Chats": "{{user}} چت ها",
|
||||
"{{webUIName}} Backend Required": "بکند {{webUIName}} نیاز است.",
|
||||
"*Prompt node ID(s) are required for image generation": "",
|
||||
"A new version (v{{LATEST_VERSION}}) is now available.": "",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "یک مدل وظیفه هنگام انجام وظایف مانند تولید عناوین برای چت ها و نمایش های جستجوی وب استفاده می شود.",
|
||||
"a user": "یک کاربر",
|
||||
"About": "درباره",
|
||||
@@ -73,7 +74,10 @@
|
||||
"AUTOMATIC1111 Api Auth String": "",
|
||||
"AUTOMATIC1111 Base URL": "پایه URL AUTOMATIC1111 ",
|
||||
"AUTOMATIC1111 Base URL is required.": "به URL پایه AUTOMATIC1111 مورد نیاز است.",
|
||||
"Available list": "",
|
||||
"available!": "در دسترس!",
|
||||
"Azure AI Speech": "",
|
||||
"Azure Region": "",
|
||||
"Back": "بازگشت",
|
||||
"Bad Response": "پاسخ خوب نیست",
|
||||
"Banners": "بنر",
|
||||
@@ -94,6 +98,7 @@
|
||||
"Chat Bubble UI": "UI\u200cی\u200c گفتگو\u200c",
|
||||
"Chat Controls": "",
|
||||
"Chat direction": "جهت\u200cگفتگو",
|
||||
"Chat Overview": "",
|
||||
"Chats": "گپ\u200cها",
|
||||
"Check Again": "چک مجدد",
|
||||
"Check for updates": "بررسی به\u200cروزرسانی",
|
||||
@@ -238,6 +243,7 @@
|
||||
"Enter a detail about yourself for your LLMs to recall": "برای ذخیره سازی اطلاعات خود، یک توضیح کوتاه درباره خود را وارد کنید",
|
||||
"Enter api auth string (e.g. username:password)": "",
|
||||
"Enter Brave Search API Key": "کلید API جستجوی شجاع را وارد کنید",
|
||||
"Enter CFG Scale (e.g. 7.0)": "",
|
||||
"Enter Chunk Overlap": "مقدار Chunk Overlap را وارد کنید",
|
||||
"Enter Chunk Size": "مقدار Chunk Size را وارد کنید",
|
||||
"Enter Github Raw URL": "ادرس Github Raw را وارد کنید",
|
||||
@@ -248,6 +254,8 @@
|
||||
"Enter Model ID": "",
|
||||
"Enter model tag (e.g. {{modelTag}})": "تگ مدل را وارد کنید (مثلا {{modelTag}})",
|
||||
"Enter Number of Steps (e.g. 50)": "تعداد گام ها را وارد کنید (مثال: 50)",
|
||||
"Enter Sampler (e.g. Euler a)": "",
|
||||
"Enter Scheduler (e.g. Karras)": "",
|
||||
"Enter Score": "امتیاز را وارد کنید",
|
||||
"Enter SearchApi API Key": "",
|
||||
"Enter SearchApi Engine": "",
|
||||
@@ -408,6 +416,8 @@
|
||||
"Model {{modelId}} not found": "مدل {{modelId}} یافت نشد",
|
||||
"Model {{modelName}} is not vision capable": "مدل {{modelName}} قادر به بینایی نیست",
|
||||
"Model {{name}} is now {{status}}": "مدل {{name}} در حال حاضر {{status}}",
|
||||
"Model {{name}} is now at the top": "",
|
||||
"Model accepts image inputs": "",
|
||||
"Model created successfully!": "",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "مسیر فایل سیستم مدل یافت شد. برای بروزرسانی نیاز است نام کوتاه مدل وجود داشته باشد.",
|
||||
"Model ID": "شناسه مدل",
|
||||
@@ -419,6 +429,7 @@
|
||||
"Modelfile Content": "محتویات فایل مدل",
|
||||
"Models": "مدل\u200cها",
|
||||
"More": "بیشتر",
|
||||
"Move to Top": "",
|
||||
"Name": "نام",
|
||||
"Name Tag": "نام تگ",
|
||||
"Name your model": "نام مدل خود را",
|
||||
@@ -455,6 +466,7 @@
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "اوه! به نظر می رسد URL نامعتبر است. لطفاً دوباره بررسی کنید و دوباره امتحان کنید.",
|
||||
"Oops! There was an error in the previous response. Please try again or contact admin.": "",
|
||||
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "اوه! شما از یک روش پشتیبانی نشده (فقط frontend) استفاده می کنید. لطفاً WebUI را از بکند اجرا کنید.",
|
||||
"Open file": "",
|
||||
"Open new chat": "باز کردن گپ جدید",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
|
||||
"OpenAI": "OpenAI",
|
||||
@@ -464,6 +476,9 @@
|
||||
"OpenAI URL/Key required.": "URL/Key OpenAI مورد نیاز است.",
|
||||
"or": "روشن",
|
||||
"Other": "دیگر",
|
||||
"Output format": "",
|
||||
"Overview": "",
|
||||
"page": "",
|
||||
"Password": "رمز عبور",
|
||||
"PDF document (.pdf)": "PDF سند (.pdf)",
|
||||
"PDF Extract Images (OCR)": "استخراج تصاویر از PDF (OCR)",
|
||||
@@ -482,6 +497,7 @@
|
||||
"Plain text (.txt)": "متن ساده (.txt)",
|
||||
"Playground": "زمین بازی",
|
||||
"Please carefully review the following warnings:": "",
|
||||
"Please select a reason": "",
|
||||
"Positive attitude": "نظرات مثبت",
|
||||
"Previous 30 days": "30 روز قبل",
|
||||
"Previous 7 days": "7 روز قبل",
|
||||
@@ -526,6 +542,7 @@
|
||||
"Save": "ذخیره",
|
||||
"Save & Create": "ذخیره و ایجاد",
|
||||
"Save & Update": "ذخیره و به\u200cروزرسانی",
|
||||
"Save As Copy": "",
|
||||
"Save Tag": "",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "ذخیره گزارش\u200cهای چت مستقیماً در حافظه مرورگر شما دیگر پشتیبانی نمی\u200cشود. لطفاً با کلیک بر روی دکمه زیر، چند لحظه برای دانلود و حذف گزارش های چت خود وقت بگذارید. نگران نباشید، شما به راحتی می توانید گزارش های چت خود را از طریق بکند دوباره وارد کنید",
|
||||
"Scan": "اسکن",
|
||||
@@ -568,16 +585,20 @@
|
||||
"Send": "ارسال",
|
||||
"Send a Message": "ارسال یک پیام",
|
||||
"Send message": "ارسال پیام",
|
||||
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
|
||||
"September": "سپتامبر",
|
||||
"Serper API Key": "کلید API Serper",
|
||||
"Serply API Key": "",
|
||||
"Serpstack API Key": "کلید API Serpstack",
|
||||
"Server connection verified": "اتصال سرور تأیید شد",
|
||||
"Set as default": "تنظیم به عنوان پیشفرض",
|
||||
"Set CFG Scale": "",
|
||||
"Set Default Model": "تنظیم مدل پیش فرض",
|
||||
"Set embedding model (e.g. {{model}})": "تنظیم مدل پیچشی (برای مثال {{model}})",
|
||||
"Set Image Size": "تنظیم اندازه تصویر",
|
||||
"Set reranking model (e.g. {{model}})": "تنظیم مدل ری\u200cراینگ (برای مثال {{model}})",
|
||||
"Set Sampler": "",
|
||||
"Set Scheduler": "",
|
||||
"Set Steps": "تنظیم گام\u200cها",
|
||||
"Set Task Model": "تنظیم مدل تکلیف",
|
||||
"Set Voice": "تنظیم صدا",
|
||||
@@ -598,9 +619,11 @@
|
||||
"Sign up": "ثبت نام",
|
||||
"Signing in": "ورود",
|
||||
"Source": "منبع",
|
||||
"Speech Playback Speed": "",
|
||||
"Speech recognition error: {{error}}": "خطای تشخیص گفتار: {{error}}",
|
||||
"Speech-to-Text Engine": "موتور گفتار به متن",
|
||||
"Stop Sequence": "توالی توقف",
|
||||
"Stream Chat Response": "",
|
||||
"STT Model": "",
|
||||
"STT Settings": "STT تنظیمات",
|
||||
"Submit": "ارسال",
|
||||
@@ -680,6 +703,7 @@
|
||||
"Unpin": "",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "به روزرسانی و کپی لینک",
|
||||
"Update for the latest features and improvements.": "",
|
||||
"Update password": "به روزرسانی رمزعبور",
|
||||
"Updated at": "",
|
||||
"Upload": "",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"{{user}}'s Chats": "{{user}}:n keskustelut",
|
||||
"{{webUIName}} Backend Required": "{{webUIName}} backend vaaditaan",
|
||||
"*Prompt node ID(s) are required for image generation": "",
|
||||
"A new version (v{{LATEST_VERSION}}) is now available.": "",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "Tehtävämallia käytetään tehtävien suorittamiseen, kuten otsikoiden luomiseen keskusteluille ja verkkohakukyselyille",
|
||||
"a user": "käyttäjä",
|
||||
"About": "Tietoja",
|
||||
@@ -73,7 +74,10 @@
|
||||
"AUTOMATIC1111 Api Auth String": "",
|
||||
"AUTOMATIC1111 Base URL": "AUTOMATIC1111-perus-URL",
|
||||
"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111-perus-URL vaaditaan.",
|
||||
"Available list": "",
|
||||
"available!": "saatavilla!",
|
||||
"Azure AI Speech": "",
|
||||
"Azure Region": "",
|
||||
"Back": "Takaisin",
|
||||
"Bad Response": "Epäkelpo vastaus",
|
||||
"Banners": "Bannerit",
|
||||
@@ -94,6 +98,7 @@
|
||||
"Chat Bubble UI": "Keskustelu-pallojen käyttöliittymä",
|
||||
"Chat Controls": "",
|
||||
"Chat direction": "Keskustelun suunta",
|
||||
"Chat Overview": "",
|
||||
"Chats": "Keskustelut",
|
||||
"Check Again": "Tarkista uudelleen",
|
||||
"Check for updates": "Tarkista päivitykset",
|
||||
@@ -238,6 +243,7 @@
|
||||
"Enter a detail about yourself for your LLMs to recall": "Kirjoita tieto itseestäsi LLM:ien muistamiseksi",
|
||||
"Enter api auth string (e.g. username:password)": "",
|
||||
"Enter Brave Search API Key": "Anna Brave Search API -avain",
|
||||
"Enter CFG Scale (e.g. 7.0)": "",
|
||||
"Enter Chunk Overlap": "Syötä osien päällekkäisyys",
|
||||
"Enter Chunk Size": "Syötä osien koko",
|
||||
"Enter Github Raw URL": "Kirjoita Github Raw URL-osoite",
|
||||
@@ -248,6 +254,8 @@
|
||||
"Enter Model ID": "",
|
||||
"Enter model tag (e.g. {{modelTag}})": "Syötä mallitagi (esim. {{modelTag}})",
|
||||
"Enter Number of Steps (e.g. 50)": "Syötä askelien määrä (esim. 50)",
|
||||
"Enter Sampler (e.g. Euler a)": "",
|
||||
"Enter Scheduler (e.g. Karras)": "",
|
||||
"Enter Score": "Syötä pisteet",
|
||||
"Enter SearchApi API Key": "",
|
||||
"Enter SearchApi Engine": "",
|
||||
@@ -408,6 +416,8 @@
|
||||
"Model {{modelId}} not found": "Mallia {{modelId}} ei löytynyt",
|
||||
"Model {{modelName}} is not vision capable": "Malli {{modelName}} ei kykene näkökykyyn",
|
||||
"Model {{name}} is now {{status}}": "Malli {{name}} on nyt {{status}}",
|
||||
"Model {{name}} is now at the top": "",
|
||||
"Model accepts image inputs": "",
|
||||
"Model created successfully!": "",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Mallin tiedostojärjestelmäpolku havaittu. Mallin lyhytnimi vaaditaan päivitykseen, ei voi jatkaa.",
|
||||
"Model ID": "Mallin tunnus",
|
||||
@@ -419,6 +429,7 @@
|
||||
"Modelfile Content": "Mallitiedoston sisältö",
|
||||
"Models": "Mallit",
|
||||
"More": "Lisää",
|
||||
"Move to Top": "",
|
||||
"Name": "Nimi",
|
||||
"Name Tag": "Nimitagi",
|
||||
"Name your model": "Mallin nimeäminen",
|
||||
@@ -455,6 +466,7 @@
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Hups! Näyttää siltä, että URL on virheellinen. Tarkista se ja yritä uudelleen.",
|
||||
"Oops! There was an error in the previous response. Please try again or contact admin.": "",
|
||||
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Hupsista! Käytät ei-tuettua menetelmää. WebUI pitää palvella backendista.",
|
||||
"Open file": "",
|
||||
"Open new chat": "Avaa uusi keskustelu",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
|
||||
"OpenAI": "OpenAI",
|
||||
@@ -464,6 +476,9 @@
|
||||
"OpenAI URL/Key required.": "OpenAI URL/ -avain vaaditaan.",
|
||||
"or": "tai",
|
||||
"Other": "Muu",
|
||||
"Output format": "",
|
||||
"Overview": "",
|
||||
"page": "",
|
||||
"Password": "Salasana",
|
||||
"PDF document (.pdf)": "PDF-tiedosto (.pdf)",
|
||||
"PDF Extract Images (OCR)": "PDF-tiedoston kuvien erottelu (OCR)",
|
||||
@@ -482,6 +497,7 @@
|
||||
"Plain text (.txt)": "Pelkkä teksti (.txt)",
|
||||
"Playground": "Leikkipaikka",
|
||||
"Please carefully review the following warnings:": "",
|
||||
"Please select a reason": "",
|
||||
"Positive attitude": "Positiivinen asenne",
|
||||
"Previous 30 days": "Edelliset 30 päivää",
|
||||
"Previous 7 days": "Edelliset 7 päivää",
|
||||
@@ -526,6 +542,7 @@
|
||||
"Save": "Tallenna",
|
||||
"Save & Create": "Tallenna ja luo",
|
||||
"Save & Update": "Tallenna ja päivitä",
|
||||
"Save As Copy": "",
|
||||
"Save Tag": "",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Keskustelulokien tallentaminen suoraan selaimen tallennustilaan ei ole enää tuettua. Lataa ja poista keskustelulokit napsauttamalla alla olevaa painiketta. Älä huoli, voit helposti tuoda keskustelulokit takaisin backendiin",
|
||||
"Scan": "Skannaa",
|
||||
@@ -568,16 +585,20 @@
|
||||
"Send": "Lähetä",
|
||||
"Send a Message": "Lähetä viesti",
|
||||
"Send message": "Lähetä viesti",
|
||||
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
|
||||
"September": "syyskuu",
|
||||
"Serper API Key": "Serper API -avain",
|
||||
"Serply API Key": "",
|
||||
"Serpstack API Key": "Serpstack API -avain",
|
||||
"Server connection verified": "Palvelinyhteys varmennettu",
|
||||
"Set as default": "Aseta oletukseksi",
|
||||
"Set CFG Scale": "",
|
||||
"Set Default Model": "Aseta oletusmalli",
|
||||
"Set embedding model (e.g. {{model}})": "Aseta upotusmalli (esim. {{model}})",
|
||||
"Set Image Size": "Aseta kuvan koko",
|
||||
"Set reranking model (e.g. {{model}})": "Aseta uudelleenpisteytysmalli (esim. {{model}})",
|
||||
"Set Sampler": "",
|
||||
"Set Scheduler": "",
|
||||
"Set Steps": "Aseta askelmäärä",
|
||||
"Set Task Model": "Aseta tehtävämalli",
|
||||
"Set Voice": "Aseta puheääni",
|
||||
@@ -598,9 +619,11 @@
|
||||
"Sign up": "Rekisteröidy",
|
||||
"Signing in": "Kirjaudutaan sisään",
|
||||
"Source": "Lähde",
|
||||
"Speech Playback Speed": "",
|
||||
"Speech recognition error: {{error}}": "Puheentunnistusvirhe: {{error}}",
|
||||
"Speech-to-Text Engine": "Puheentunnistusmoottori",
|
||||
"Stop Sequence": "Lopetussekvenssi",
|
||||
"Stream Chat Response": "",
|
||||
"STT Model": "",
|
||||
"STT Settings": "Puheentunnistusasetukset",
|
||||
"Submit": "Lähetä",
|
||||
@@ -680,6 +703,7 @@
|
||||
"Unpin": "",
|
||||
"Update": "",
|
||||
"Update and Copy Link": "Päivitä ja kopioi linkki",
|
||||
"Update for the latest features and improvements.": "",
|
||||
"Update password": "Päivitä salasana",
|
||||
"Updated at": "",
|
||||
"Upload": "",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"{{user}}'s Chats": "Discussions de {{user}}",
|
||||
"{{webUIName}} Backend Required": "Backend {{webUIName}} requis",
|
||||
"*Prompt node ID(s) are required for image generation": "",
|
||||
"A new version (v{{LATEST_VERSION}}) is now available.": "",
|
||||
"A task model is used when performing tasks such as generating titles for chats and web search queries": "Un modèle de tâche est utilisé lors de l’exécution de tâches telles que la génération de titres pour les conversations et les requêtes de recherche sur le web.",
|
||||
"a user": "un utilisateur",
|
||||
"About": "À propos",
|
||||
@@ -73,7 +74,10 @@
|
||||
"AUTOMATIC1111 Api Auth String": "AUTOMATIC1111 Chaîne d'authentification de l'API",
|
||||
"AUTOMATIC1111 Base URL": "URL de base AUTOMATIC1111",
|
||||
"AUTOMATIC1111 Base URL is required.": "L'URL de base {AUTOMATIC1111} est requise.",
|
||||
"Available list": "",
|
||||
"available!": "disponible !",
|
||||
"Azure AI Speech": "",
|
||||
"Azure Region": "",
|
||||
"Back": "Retour en arrière",
|
||||
"Bad Response": "Mauvaise réponse",
|
||||
"Banners": "Banniers",
|
||||
@@ -94,6 +98,7 @@
|
||||
"Chat Bubble UI": "Bulles de discussion",
|
||||
"Chat Controls": "",
|
||||
"Chat direction": "Direction du chat",
|
||||
"Chat Overview": "",
|
||||
"Chats": "Conversations",
|
||||
"Check Again": "Vérifiez à nouveau.",
|
||||
"Check for updates": "Vérifier les mises à jour disponibles",
|
||||
@@ -238,6 +243,7 @@
|
||||
"Enter a detail about yourself for your LLMs to recall": "Saisissez un détail sur vous-même que vos LLMs pourront se rappeler",
|
||||
"Enter api auth string (e.g. username:password)": "Entrez la chaîne d'authentification de l'API (par ex. nom d'utilisateur:mot de passe)",
|
||||
"Enter Brave Search API Key": "Entrez la clé API Brave Search",
|
||||
"Enter CFG Scale (e.g. 7.0)": "",
|
||||
"Enter Chunk Overlap": "Entrez le chevauchement de chunk",
|
||||
"Enter Chunk Size": "Entrez la taille de bloc",
|
||||
"Enter Github Raw URL": "Entrez l'URL brute de GitHub",
|
||||
@@ -248,6 +254,8 @@
|
||||
"Enter Model ID": "",
|
||||
"Enter model tag (e.g. {{modelTag}})": "Entrez l'étiquette du modèle (par ex. {{modelTag}})",
|
||||
"Enter Number of Steps (e.g. 50)": "Entrez le nombre de pas (par ex. 50)",
|
||||
"Enter Sampler (e.g. Euler a)": "",
|
||||
"Enter Scheduler (e.g. Karras)": "",
|
||||
"Enter Score": "Entrez votre score",
|
||||
"Enter SearchApi API Key": "",
|
||||
"Enter SearchApi Engine": "",
|
||||
@@ -408,6 +416,8 @@
|
||||
"Model {{modelId}} not found": "Modèle {{modelId}} introuvable",
|
||||
"Model {{modelName}} is not vision capable": "Le modèle {{modelName}} n'a pas de capacités visuelles",
|
||||
"Model {{name}} is now {{status}}": "Le modèle {{name}} est désormais {{status}}.",
|
||||
"Model {{name}} is now at the top": "",
|
||||
"Model accepts image inputs": "",
|
||||
"Model created successfully!": "Le modèle a été créé avec succès !",
|
||||
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Chemin du système de fichiers de modèle détecté. Le nom court du modèle est requis pour la mise à jour, l'opération ne peut pas être poursuivie.",
|
||||
"Model ID": "ID du modèle",
|
||||
@@ -419,6 +429,7 @@
|
||||
"Modelfile Content": "Contenu du Fichier de Modèle",
|
||||
"Models": "Modèles",
|
||||
"More": "Plus de",
|
||||
"Move to Top": "",
|
||||
"Name": "Nom",
|
||||
"Name Tag": "Étiquette de nom",
|
||||
"Name your model": "Nommez votre modèle",
|
||||
@@ -455,6 +466,7 @@
|
||||
"Oops! Looks like the URL is invalid. Please double-check and try again.": "Oups ! Il semble que l'URL soit invalide. Veuillez vérifier à nouveau et réessayer.",
|
||||
"Oops! There was an error in the previous response. Please try again or contact admin.": "Oops ! Il y a eu une erreur dans la réponse précédente. Veuillez réessayer ou contacter l'administrateur.",
|
||||
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oups\u00a0! Vous utilisez une méthode non prise en charge (frontend uniquement). Veuillez servir l'interface Web à partir du backend.",
|
||||
"Open file": "",
|
||||
"Open new chat": "Ouvrir une nouvelle discussion",
|
||||
"Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "",
|
||||
"OpenAI": "OpenAI",
|
||||
@@ -464,6 +476,9 @@
|
||||
"OpenAI URL/Key required.": "URL/Clé OpenAI requise.",
|
||||
"or": "ou",
|
||||
"Other": "Autre",
|
||||
"Output format": "",
|
||||
"Overview": "",
|
||||
"page": "",
|
||||
"Password": "Mot de passe",
|
||||
"PDF document (.pdf)": "Document au format PDF (.pdf)",
|
||||
"PDF Extract Images (OCR)": "Extraction d'images PDF (OCR)",
|
||||
@@ -482,6 +497,7 @@
|
||||
"Plain text (.txt)": "Texte simple (.txt)",
|
||||
"Playground": "Aire de jeux",
|
||||
"Please carefully review the following warnings:": "",
|
||||
"Please select a reason": "",
|
||||
"Positive attitude": "Attitude positive",
|
||||
"Previous 30 days": "30 derniers jours",
|
||||
"Previous 7 days": "7 derniers jours",
|
||||
@@ -526,6 +542,7 @@
|
||||
"Save": "Enregistrer",
|
||||
"Save & Create": "Enregistrer & Créer",
|
||||
"Save & Update": "Enregistrer & Mettre à jour",
|
||||
"Save As Copy": "",
|
||||
"Save Tag": "",
|
||||
"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "La sauvegarde des journaux de discussion directement dans le stockage de votre navigateur n'est plus prise en charge. Veuillez prendre un instant pour télécharger et supprimer vos journaux de discussion en cliquant sur le bouton ci-dessous. Pas de soucis, vous pouvez facilement les réimporter depuis le backend via l'interface ci-dessous",
|
||||
"Scan": "Scanner",
|
||||
@@ -569,16 +586,20 @@
|
||||
"Send": "Envoyer",
|
||||
"Send a Message": "Envoyer un message",
|
||||
"Send message": "Envoyer un message",
|
||||
"Sends `stream_options: { include_usage: true }` in the request.\nSupported providers will return token usage information in the response when set.": "",
|
||||
"September": "Septembre",
|
||||
"Serper API Key": "Clé API Serper",
|
||||
"Serply API Key": "Clé API Serply",
|
||||
"Serpstack API Key": "Clé API Serpstack",
|
||||
"Server connection verified": "Connexion au serveur vérifiée",
|
||||
"Set as default": "Définir comme valeur par défaut",
|
||||
"Set CFG Scale": "",
|
||||
"Set Default Model": "Définir le modèle par défaut",
|
||||
"Set embedding model (e.g. {{model}})": "Définir le modèle d'encodage (par ex. {{model}})",
|
||||
"Set Image Size": "Définir la taille de l'image",
|
||||
"Set reranking model (e.g. {{model}})": "Définir le modèle de reclassement (par ex. {{model}})",
|
||||
"Set Sampler": "",
|
||||
"Set Scheduler": "",
|
||||
"Set Steps": "Définir les étapes",
|
||||
"Set Task Model": "Définir le modèle de tâche",
|
||||
"Set Voice": "Définir la voix",
|
||||
@@ -599,9 +620,11 @@
|
||||
"Sign up": "Inscrivez-vous",
|
||||
"Signing in": "Connexion en cours",
|
||||
"Source": "Source",
|
||||
"Speech Playback Speed": "",
|
||||
"Speech recognition error: {{error}}": "Erreur de reconnaissance vocale\u00a0: {{error}}",
|
||||
"Speech-to-Text Engine": "Moteur de reconnaissance vocale",
|
||||
"Stop Sequence": "Séquence d'arrêt",
|
||||
"Stream Chat Response": "",
|
||||
"STT Model": "Modèle de STT",
|
||||
"STT Settings": "Paramètres de STT",
|
||||
"Submit": "Soumettre",
|
||||
@@ -681,6 +704,7 @@
|
||||
"Unpin": "",
|
||||
"Update": "Mise à jour",
|
||||
"Update and Copy Link": "Mettre à jour et copier le lien",
|
||||
"Update for the latest features and improvements.": "",
|
||||
"Update password": "Mettre à jour le mot de passe",
|
||||
"Updated at": "Mise à jour le",
|
||||
"Upload": "Télécharger",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user